Now we have server tool and client tool

This commit is contained in:
Xin Wang
2026-02-10 19:13:54 +08:00
parent 54eb48fb74
commit 6cac24918d
5 changed files with 257 additions and 82 deletions

View File

@@ -1000,37 +1000,6 @@ const TOOL_PARAMETER_HINTS: Record<string, any> = {
const getDefaultToolParameters = (toolId: string) =>
TOOL_PARAMETER_HINTS[toolId] || { type: 'object', properties: {} };
const parseToolArgs = (raw: unknown): Record<string, any> => {
if (raw && typeof raw === 'object') return raw as Record<string, any>;
if (typeof raw !== 'string') return {};
const text = raw.trim();
if (!text) return {};
try {
const parsed = JSON.parse(text);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
};
const evaluateCalculatorExpression = (expression: string): { ok: true; result: number } | { ok: false; error: string } => {
const expr = String(expression || '').trim();
if (!expr) return { ok: false, error: 'empty expression' };
if (expr.length > 200) return { ok: false, error: 'expression too long' };
if (!/^[0-9+\-*/().%\s]+$/.test(expr)) return { ok: false, error: 'invalid characters' };
if (expr.includes('**') || expr.includes('//')) return { ok: false, error: 'unsupported operator' };
try {
const value = Function(`"use strict"; return (${expr});`)();
if (typeof value !== 'number' || !Number.isFinite(value)) {
return { ok: false, error: 'expression is not finite number' };
}
return { ok: true, result: value };
} catch {
return { ok: false, error: 'invalid expression' };
}
};
// Stable transcription log so the scroll container is not recreated on every render (avoids scroll jumping)
const TranscriptionLog: React.FC<{
scrollRef: React.RefObject<HTMLDivElement | null>;
@@ -1165,12 +1134,18 @@ export const DebugDrawer: React.FC<{
const byId = new Map(tools.map((t) => [t.id, t]));
return ids.map((id) => {
const item = byId.get(id);
const toolId = item?.id || id;
const isClientTool =
toolId.startsWith('client_') ||
toolId.startsWith('browser_') ||
['camera', 'microphone', 'page_control', 'local_file'].includes(toolId);
return {
type: 'function',
executor: isClientTool ? 'client' : 'server',
function: {
name: item?.id || id,
name: toolId,
description: item?.description || item?.name || id,
parameters: getDefaultToolParameters(item?.id || id),
parameters: getDefaultToolParameters(toolId),
},
};
});
@@ -1760,64 +1735,25 @@ export const DebugDrawer: React.FC<{
const toolCall = payload?.tool_call || {};
const toolCallId = String(toolCall?.id || '').trim();
const toolName = String(toolCall?.function?.name || toolCall?.name || 'unknown_tool');
const executor = String(toolCall?.executor || 'server').toLowerCase();
const rawArgs = String(toolCall?.function?.arguments || '');
const argText = rawArgs.length > 160 ? `${rawArgs.slice(0, 160)}...` : rawArgs;
setMessages((prev) => [
...prev,
{
role: 'tool',
text: `call ${toolName}${argText ? ` args=${argText}` : ''}`,
text: `call ${toolName} executor=${executor}${argText ? ` args=${argText}` : ''}`,
},
]);
if (toolCallId && ws.readyState === WebSocket.OPEN) {
const argsObj = parseToolArgs(toolCall?.function?.arguments);
const normalizedToolName = toolName.trim().toLowerCase();
let resultPayload: any = {
if (executor === 'client' && toolCallId && ws.readyState === WebSocket.OPEN) {
const resultPayload: any = {
tool_call_id: toolCallId,
name: toolName,
output: {
message: 'Tool execution is not implemented in debug web client',
message: 'Client tool execution is not implemented in debug web client',
},
status: { code: 501, message: 'not_implemented' },
};
const isCalculatorTool =
normalizedToolName === 'calculator' ||
normalizedToolName.endsWith('.calculator') ||
normalizedToolName.includes('calculator');
if (isCalculatorTool) {
const expression = String(argsObj.expression || argsObj.input || '').trim();
if (!expression) {
resultPayload = {
tool_call_id: toolCallId,
name: toolName,
output: { error: 'missing expression' },
status: { code: 400, message: 'bad_request' },
};
} else {
const calc = evaluateCalculatorExpression(expression);
if (calc.ok) {
resultPayload = {
tool_call_id: toolCallId,
name: toolName,
output: {
expression,
result: calc.result,
},
status: { code: 200, message: 'ok' },
};
} else {
resultPayload = {
tool_call_id: toolCallId,
name: toolName,
output: { expression, error: calc.error },
status: { code: 422, message: 'invalid_expression' },
};
}
}
}
ws.send(
JSON.stringify({
type: 'tool_call.results',
@@ -1841,6 +1777,21 @@ export const DebugDrawer: React.FC<{
return;
}
if (type === 'assistant.tool_result') {
const result = payload?.result || {};
const toolName = String(result?.name || 'unknown_tool');
const statusCode = Number(result?.status?.code || 500);
const statusMessage = String(result?.status?.message || 'error');
const source = String(payload?.source || 'server');
const output = result?.output;
const resultText =
statusCode === 200
? `result ${toolName} source=${source} ${JSON.stringify(output)}`
: `result ${toolName} source=${source} status=${statusCode} ${statusMessage}`;
setMessages((prev) => [...prev, { role: 'tool', text: resultText }]);
return;
}
if (type === 'session.started') {
wsReadyRef.current = true;
setWsStatus('ready');