@tambo-ai/react 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -20
- package/dist/v1/hooks/use-tambo-v1-send-message.d.ts.map +1 -1
- package/dist/v1/hooks/use-tambo-v1-send-message.js +4 -31
- package/dist/v1/hooks/use-tambo-v1-send-message.js.map +1 -1
- package/dist/v1/utils/event-accumulator.d.ts +3 -0
- package/dist/v1/utils/event-accumulator.d.ts.map +1 -1
- package/dist/v1/utils/event-accumulator.js +26 -5
- package/dist/v1/utils/event-accumulator.js.map +1 -1
- package/dist/v1/utils/event-accumulator.test.js +113 -0
- package/dist/v1/utils/event-accumulator.test.js.map +1 -1
- package/dist/v1/utils/tool-call-tracker.d.ts +26 -4
- package/dist/v1/utils/tool-call-tracker.d.ts.map +1 -1
- package/dist/v1/utils/tool-call-tracker.js +82 -5
- package/dist/v1/utils/tool-call-tracker.js.map +1 -1
- package/dist/v1/utils/tool-call-tracker.test.js +178 -0
- package/dist/v1/utils/tool-call-tracker.test.js.map +1 -1
- package/dist/v1/utils/unstrictify.d.ts +32 -0
- package/dist/v1/utils/unstrictify.d.ts.map +1 -0
- package/dist/v1/utils/unstrictify.js +159 -0
- package/dist/v1/utils/unstrictify.js.map +1 -0
- package/dist/v1/utils/unstrictify.test.d.ts +2 -0
- package/dist/v1/utils/unstrictify.test.d.ts.map +1 -0
- package/dist/v1/utils/unstrictify.test.js +187 -0
- package/dist/v1/utils/unstrictify.test.js.map +1 -0
- package/esm/v1/hooks/use-tambo-v1-send-message.d.ts.map +1 -1
- package/esm/v1/hooks/use-tambo-v1-send-message.js +4 -31
- package/esm/v1/hooks/use-tambo-v1-send-message.js.map +1 -1
- package/esm/v1/utils/event-accumulator.d.ts +3 -0
- package/esm/v1/utils/event-accumulator.d.ts.map +1 -1
- package/esm/v1/utils/event-accumulator.js +26 -5
- package/esm/v1/utils/event-accumulator.js.map +1 -1
- package/esm/v1/utils/event-accumulator.test.js +113 -0
- package/esm/v1/utils/event-accumulator.test.js.map +1 -1
- package/esm/v1/utils/tool-call-tracker.d.ts +26 -4
- package/esm/v1/utils/tool-call-tracker.d.ts.map +1 -1
- package/esm/v1/utils/tool-call-tracker.js +82 -5
- package/esm/v1/utils/tool-call-tracker.js.map +1 -1
- package/esm/v1/utils/tool-call-tracker.test.js +178 -0
- package/esm/v1/utils/tool-call-tracker.test.js.map +1 -1
- package/esm/v1/utils/unstrictify.d.ts +32 -0
- package/esm/v1/utils/unstrictify.d.ts.map +1 -0
- package/esm/v1/utils/unstrictify.js +155 -0
- package/esm/v1/utils/unstrictify.js.map +1 -0
- package/esm/v1/utils/unstrictify.test.d.ts +2 -0
- package/esm/v1/utils/unstrictify.test.d.ts.map +1 -0
- package/esm/v1/utils/unstrictify.test.js +185 -0
- package/esm/v1/utils/unstrictify.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* Tool Call Tracker
|
|
3
3
|
*
|
|
4
4
|
* Tracks tool calls during streaming, accumulating arguments until complete.
|
|
5
|
-
*
|
|
5
|
+
* Owns the tool name → JSON Schema mapping and handles unstrictification
|
|
6
|
+
* so callers don't need to know about schema conversion.
|
|
6
7
|
*/
|
|
7
8
|
import { type AGUIEvent } from "@ag-ui/core";
|
|
9
|
+
import type { JSONSchema7 } from "json-schema";
|
|
10
|
+
import type { TamboTool } from "../../model/component-metadata.js";
|
|
8
11
|
import type { PendingToolCall } from "./tool-executor.js";
|
|
9
12
|
/**
|
|
10
13
|
* Tracks tool calls during streaming, accumulating arguments until complete.
|
|
@@ -14,21 +17,35 @@ import type { PendingToolCall } from "./tool-executor.js";
|
|
|
14
17
|
* 2. TOOL_CALL_ARGS (multiple) - streams JSON argument fragments
|
|
15
18
|
* 3. TOOL_CALL_END - marks the tool call as complete, triggers JSON parsing
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
20
|
+
* When constructed with a tool registry, the tracker unstrictifies parsed
|
|
21
|
+
* args (both partial and final) using the original JSON Schemas.
|
|
19
22
|
*/
|
|
20
23
|
export declare class ToolCallTracker {
|
|
21
24
|
private pendingToolCalls;
|
|
22
25
|
private accumulatingArgs;
|
|
26
|
+
private _toolSchemas;
|
|
27
|
+
constructor(toolRegistry?: Record<string, TamboTool>);
|
|
28
|
+
/**
|
|
29
|
+
* The tool-name → JSONSchema7 map, for passing to the reducer.
|
|
30
|
+
* @returns The tool schemas map
|
|
31
|
+
*/
|
|
32
|
+
get toolSchemas(): Map<string, JSONSchema7>;
|
|
23
33
|
/**
|
|
24
34
|
* Handles a streaming event, tracking tool call state as needed.
|
|
25
35
|
* @param event - The streaming event to process
|
|
26
36
|
* @throws {Error} If JSON parsing fails on TOOL_CALL_END (fail-fast, no silent fallback)
|
|
27
37
|
*/
|
|
28
38
|
handleEvent(event: AGUIEvent): void;
|
|
39
|
+
/**
|
|
40
|
+
* Parses partial JSON from the accumulated args for a tool call and
|
|
41
|
+
* unstrictifies the result. Used during streaming to get the current
|
|
42
|
+
* best-effort parsed args.
|
|
43
|
+
* @param toolCallId - ID of the tool call to parse
|
|
44
|
+
* @returns Parsed and unstrictified args, or undefined if not parseable yet
|
|
45
|
+
*/
|
|
46
|
+
parsePartialArgs(toolCallId: string): Record<string, unknown> | undefined;
|
|
29
47
|
/**
|
|
30
48
|
* Gets the name and accumulated args for a tool call that is still accumulating.
|
|
31
|
-
* Used by the event loop to get tool state for partial JSON parsing.
|
|
32
49
|
* @param toolCallId - ID of the tool call to look up
|
|
33
50
|
* @returns The tool name and raw accumulated args string, or undefined if not found
|
|
34
51
|
*/
|
|
@@ -47,5 +64,10 @@ export declare class ToolCallTracker {
|
|
|
47
64
|
* @param toolCallIds - IDs of tool calls to clear
|
|
48
65
|
*/
|
|
49
66
|
clearToolCalls(toolCallIds: string[]): void;
|
|
67
|
+
/**
|
|
68
|
+
* Unstrictify params using the schema for the given tool name.
|
|
69
|
+
* Returns params unchanged if no schema is available.
|
|
70
|
+
*/
|
|
71
|
+
private unstrictify;
|
|
50
72
|
}
|
|
51
73
|
//# sourceMappingURL=tool-call-tracker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-call-tracker.d.ts","sourceRoot":"","sources":["../../../src/v1/utils/tool-call-tracker.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tool-call-tracker.d.ts","sourceRoot":"","sources":["../../../src/v1/utils/tool-call-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AA4BvD;;;;;;;;;;GAUG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,gBAAgB,CAAsC;IAC9D,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,YAAY,CAA2B;gBAEnC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC;IAMpD;;;OAGG;IACH,IAAI,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAE1C;IAED;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IA6CnC;;;;;;OAMG;IACH,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAsBzE;;;;OAIG;IACH,uBAAuB,CACrB,UAAU,EAAE,MAAM,GACjB;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;IAOxD;;;;OAIG;IACH,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC;IAWrE;;;OAGG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI;IAO3C;;;OAGG;IACH,OAAO,CAAC,WAAW;CAQpB"}
|
|
@@ -2,9 +2,37 @@
|
|
|
2
2
|
* Tool Call Tracker
|
|
3
3
|
*
|
|
4
4
|
* Tracks tool calls during streaming, accumulating arguments until complete.
|
|
5
|
-
*
|
|
5
|
+
* Owns the tool name → JSON Schema mapping and handles unstrictification
|
|
6
|
+
* so callers don't need to know about schema conversion.
|
|
6
7
|
*/
|
|
7
8
|
import { EventType } from "@ag-ui/core";
|
|
9
|
+
import { parse as parsePartialJson } from "partial-json";
|
|
10
|
+
import { schemaToJsonSchema } from "../../schema/schema.js";
|
|
11
|
+
import { unstrictifyToolCallParamsFromSchema } from "./unstrictify.js";
|
|
12
|
+
/**
|
|
13
|
+
* Build a tool-name → JSONSchema7 map from the tool registry.
|
|
14
|
+
* Handles both modern `inputSchema` and deprecated `toolSchema` formats.
|
|
15
|
+
* Tools whose schema can't be converted are silently skipped.
|
|
16
|
+
* @param toolRegistry - Record of tool name → tool definition
|
|
17
|
+
* @returns Map of tool name → JSON Schema
|
|
18
|
+
*/
|
|
19
|
+
function buildToolSchemas(toolRegistry) {
|
|
20
|
+
const schemas = new Map();
|
|
21
|
+
for (const tool of Object.values(toolRegistry)) {
|
|
22
|
+
try {
|
|
23
|
+
if ("inputSchema" in tool && tool.inputSchema) {
|
|
24
|
+
schemas.set(tool.name, schemaToJsonSchema(tool.inputSchema));
|
|
25
|
+
}
|
|
26
|
+
else if ("toolSchema" in tool && tool.toolSchema) {
|
|
27
|
+
schemas.set(tool.name, schemaToJsonSchema(tool.toolSchema));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Schema conversion failed — tool still works, just without unstrictification
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return schemas;
|
|
35
|
+
}
|
|
8
36
|
/**
|
|
9
37
|
* Tracks tool calls during streaming, accumulating arguments until complete.
|
|
10
38
|
*
|
|
@@ -13,12 +41,25 @@ import { EventType } from "@ag-ui/core";
|
|
|
13
41
|
* 2. TOOL_CALL_ARGS (multiple) - streams JSON argument fragments
|
|
14
42
|
* 3. TOOL_CALL_END - marks the tool call as complete, triggers JSON parsing
|
|
15
43
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
44
|
+
* When constructed with a tool registry, the tracker unstrictifies parsed
|
|
45
|
+
* args (both partial and final) using the original JSON Schemas.
|
|
18
46
|
*/
|
|
19
47
|
export class ToolCallTracker {
|
|
20
48
|
pendingToolCalls = new Map();
|
|
21
49
|
accumulatingArgs = new Map();
|
|
50
|
+
_toolSchemas;
|
|
51
|
+
constructor(toolRegistry) {
|
|
52
|
+
this._toolSchemas = toolRegistry
|
|
53
|
+
? buildToolSchemas(toolRegistry)
|
|
54
|
+
: new Map();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The tool-name → JSONSchema7 map, for passing to the reducer.
|
|
58
|
+
* @returns The tool schemas map
|
|
59
|
+
*/
|
|
60
|
+
get toolSchemas() {
|
|
61
|
+
return this._toolSchemas;
|
|
62
|
+
}
|
|
22
63
|
/**
|
|
23
64
|
* Handles a streaming event, tracking tool call state as needed.
|
|
24
65
|
* @param event - The streaming event to process
|
|
@@ -42,13 +83,16 @@ export class ToolCallTracker {
|
|
|
42
83
|
const jsonStr = this.accumulatingArgs.get(event.toolCallId);
|
|
43
84
|
const toolCall = this.pendingToolCalls.get(event.toolCallId);
|
|
44
85
|
if (toolCall && jsonStr) {
|
|
86
|
+
let parsedInput;
|
|
45
87
|
try {
|
|
46
|
-
|
|
88
|
+
parsedInput = JSON.parse(jsonStr);
|
|
47
89
|
}
|
|
48
90
|
catch (error) {
|
|
49
91
|
// Fail-fast: don't silently continue with empty input
|
|
50
92
|
throw new Error(`Failed to parse tool call arguments for ${event.toolCallId}: ${error instanceof Error ? error.message : "Unknown error"}. JSON: ${jsonStr.slice(0, 100)}${jsonStr.length > 100 ? "..." : ""}`);
|
|
51
93
|
}
|
|
94
|
+
parsedInput = this.unstrictify(toolCall.name, parsedInput);
|
|
95
|
+
toolCall.input = parsedInput;
|
|
52
96
|
}
|
|
53
97
|
break;
|
|
54
98
|
}
|
|
@@ -57,9 +101,32 @@ export class ToolCallTracker {
|
|
|
57
101
|
break;
|
|
58
102
|
}
|
|
59
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Parses partial JSON from the accumulated args for a tool call and
|
|
106
|
+
* unstrictifies the result. Used during streaming to get the current
|
|
107
|
+
* best-effort parsed args.
|
|
108
|
+
* @param toolCallId - ID of the tool call to parse
|
|
109
|
+
* @returns Parsed and unstrictified args, or undefined if not parseable yet
|
|
110
|
+
*/
|
|
111
|
+
parsePartialArgs(toolCallId) {
|
|
112
|
+
const accToolCall = this.getAccumulatingToolCall(toolCallId);
|
|
113
|
+
if (!accToolCall)
|
|
114
|
+
return undefined;
|
|
115
|
+
try {
|
|
116
|
+
const parsed = parsePartialJson(accToolCall.accumulatedArgs);
|
|
117
|
+
if (typeof parsed === "object" &&
|
|
118
|
+
parsed !== null &&
|
|
119
|
+
!Array.isArray(parsed)) {
|
|
120
|
+
return this.unstrictify(accToolCall.name, parsed);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* not parseable yet */
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
60
128
|
/**
|
|
61
129
|
* Gets the name and accumulated args for a tool call that is still accumulating.
|
|
62
|
-
* Used by the event loop to get tool state for partial JSON parsing.
|
|
63
130
|
* @param toolCallId - ID of the tool call to look up
|
|
64
131
|
* @returns The tool name and raw accumulated args string, or undefined if not found
|
|
65
132
|
*/
|
|
@@ -95,5 +162,15 @@ export class ToolCallTracker {
|
|
|
95
162
|
this.accumulatingArgs.delete(id);
|
|
96
163
|
}
|
|
97
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Unstrictify params using the schema for the given tool name.
|
|
167
|
+
* Returns params unchanged if no schema is available.
|
|
168
|
+
*/
|
|
169
|
+
unstrictify(toolName, params) {
|
|
170
|
+
const schema = this._toolSchemas.get(toolName);
|
|
171
|
+
if (!schema)
|
|
172
|
+
return params;
|
|
173
|
+
return unstrictifyToolCallParamsFromSchema(schema, params);
|
|
174
|
+
}
|
|
98
175
|
}
|
|
99
176
|
//# sourceMappingURL=tool-call-tracker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-call-tracker.js","sourceRoot":"","sources":["../../../src/v1/utils/tool-call-tracker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Tool Call Tracker\n *\n * Tracks tool calls during streaming, accumulating arguments until complete.\n * Used by the send message hook to collect tool call state for execution.\n */\n\nimport { EventType, type AGUIEvent } from \"@ag-ui/core\";\nimport type { PendingToolCall } from \"./tool-executor\";\n\n/**\n * Tracks tool calls during streaming, accumulating arguments until complete.\n *\n * Tool calls arrive as a sequence of events:\n * 1. TOOL_CALL_START - initializes the tool call with name\n * 2. TOOL_CALL_ARGS (multiple) - streams JSON argument fragments\n * 3. TOOL_CALL_END - marks the tool call as complete, triggers JSON parsing\n *\n * This class accumulates these events and provides the complete tool call\n * data when requested for execution.\n */\nexport class ToolCallTracker {\n private pendingToolCalls = new Map<string, PendingToolCall>();\n private accumulatingArgs = new Map<string, string>();\n\n /**\n * Handles a streaming event, tracking tool call state as needed.\n * @param event - The streaming event to process\n * @throws {Error} If JSON parsing fails on TOOL_CALL_END (fail-fast, no silent fallback)\n */\n handleEvent(event: AGUIEvent): void {\n switch (event.type) {\n case EventType.TOOL_CALL_START:\n this.pendingToolCalls.set(event.toolCallId, {\n name: event.toolCallName,\n input: {},\n });\n this.accumulatingArgs.set(event.toolCallId, \"\");\n break;\n\n case EventType.TOOL_CALL_ARGS: {\n const current = this.accumulatingArgs.get(event.toolCallId);\n this.accumulatingArgs.set(\n event.toolCallId,\n (current ?? \"\") + event.delta,\n );\n break;\n }\n\n case EventType.TOOL_CALL_END: {\n const jsonStr = this.accumulatingArgs.get(event.toolCallId);\n const toolCall = this.pendingToolCalls.get(event.toolCallId);\n if (toolCall && jsonStr) {\n try {\n toolCall.input = JSON.parse(jsonStr) as Record<string, unknown>;\n } catch (error) {\n // Fail-fast: don't silently continue with empty input\n throw new Error(\n `Failed to parse tool call arguments for ${event.toolCallId}: ${error instanceof Error ? error.message : \"Unknown error\"}. JSON: ${jsonStr.slice(0, 100)}${jsonStr.length > 100 ? \"...\" : \"\"}`,\n );\n }\n }\n break;\n }\n\n default:\n // Other event types are ignored - only tool call events are tracked\n break;\n }\n }\n\n /**\n * Gets the name and accumulated args for a tool call that is still accumulating.\n * Used by the event loop to get tool state for partial JSON parsing.\n * @param toolCallId - ID of the tool call to look up\n * @returns The tool name and raw accumulated args string, or undefined if not found\n */\n getAccumulatingToolCall(\n toolCallId: string,\n ): { name: string; accumulatedArgs: string } | undefined {\n const toolCall = this.pendingToolCalls.get(toolCallId);\n const args = this.accumulatingArgs.get(toolCallId);\n if (!toolCall || args === undefined) return undefined;\n return { name: toolCall.name, accumulatedArgs: args };\n }\n\n /**\n * Gets tool calls for the given IDs, filtered to only those that exist.\n * @param toolCallIds - IDs of tool calls to retrieve\n * @returns Map of tool call ID to pending tool call\n */\n getToolCallsById(toolCallIds: string[]): Map<string, PendingToolCall> {\n const result = new Map<string, PendingToolCall>();\n for (const id of toolCallIds) {\n const toolCall = this.pendingToolCalls.get(id);\n if (toolCall) {\n result.set(id, toolCall);\n }\n }\n return result;\n }\n\n /**\n * Clears tracked tool calls for the given IDs.\n * @param toolCallIds - IDs of tool calls to clear\n */\n clearToolCalls(toolCallIds: string[]): void {\n for (const id of toolCallIds) {\n this.pendingToolCalls.delete(id);\n this.accumulatingArgs.delete(id);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tool-call-tracker.js","sourceRoot":"","sources":["../../../src/v1/utils/tool-call-tracker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Tool Call Tracker\n *\n * Tracks tool calls during streaming, accumulating arguments until complete.\n * Owns the tool name → JSON Schema mapping and handles unstrictification\n * so callers don't need to know about schema conversion.\n */\n\nimport { EventType, type AGUIEvent } from \"@ag-ui/core\";\nimport type { JSONSchema7 } from \"json-schema\";\nimport { parse as parsePartialJson } from \"partial-json\";\nimport type { TamboTool } from \"../../model/component-metadata\";\nimport { schemaToJsonSchema } from \"../../schema/schema\";\nimport type { PendingToolCall } from \"./tool-executor\";\nimport { unstrictifyToolCallParamsFromSchema } from \"./unstrictify\";\n\n/**\n * Build a tool-name → JSONSchema7 map from the tool registry.\n * Handles both modern `inputSchema` and deprecated `toolSchema` formats.\n * Tools whose schema can't be converted are silently skipped.\n * @param toolRegistry - Record of tool name → tool definition\n * @returns Map of tool name → JSON Schema\n */\nfunction buildToolSchemas(\n toolRegistry: Record<string, TamboTool>,\n): Map<string, JSONSchema7> {\n const schemas = new Map<string, JSONSchema7>();\n for (const tool of Object.values(toolRegistry)) {\n try {\n if (\"inputSchema\" in tool && tool.inputSchema) {\n schemas.set(tool.name, schemaToJsonSchema(tool.inputSchema));\n } else if (\"toolSchema\" in tool && tool.toolSchema) {\n schemas.set(tool.name, schemaToJsonSchema(tool.toolSchema));\n }\n } catch {\n // Schema conversion failed — tool still works, just without unstrictification\n }\n }\n return schemas;\n}\n\n/**\n * Tracks tool calls during streaming, accumulating arguments until complete.\n *\n * Tool calls arrive as a sequence of events:\n * 1. TOOL_CALL_START - initializes the tool call with name\n * 2. TOOL_CALL_ARGS (multiple) - streams JSON argument fragments\n * 3. TOOL_CALL_END - marks the tool call as complete, triggers JSON parsing\n *\n * When constructed with a tool registry, the tracker unstrictifies parsed\n * args (both partial and final) using the original JSON Schemas.\n */\nexport class ToolCallTracker {\n private pendingToolCalls = new Map<string, PendingToolCall>();\n private accumulatingArgs = new Map<string, string>();\n private _toolSchemas: Map<string, JSONSchema7>;\n\n constructor(toolRegistry?: Record<string, TamboTool>) {\n this._toolSchemas = toolRegistry\n ? buildToolSchemas(toolRegistry)\n : new Map();\n }\n\n /**\n * The tool-name → JSONSchema7 map, for passing to the reducer.\n * @returns The tool schemas map\n */\n get toolSchemas(): Map<string, JSONSchema7> {\n return this._toolSchemas;\n }\n\n /**\n * Handles a streaming event, tracking tool call state as needed.\n * @param event - The streaming event to process\n * @throws {Error} If JSON parsing fails on TOOL_CALL_END (fail-fast, no silent fallback)\n */\n handleEvent(event: AGUIEvent): void {\n switch (event.type) {\n case EventType.TOOL_CALL_START:\n this.pendingToolCalls.set(event.toolCallId, {\n name: event.toolCallName,\n input: {},\n });\n this.accumulatingArgs.set(event.toolCallId, \"\");\n break;\n\n case EventType.TOOL_CALL_ARGS: {\n const current = this.accumulatingArgs.get(event.toolCallId);\n this.accumulatingArgs.set(\n event.toolCallId,\n (current ?? \"\") + event.delta,\n );\n break;\n }\n\n case EventType.TOOL_CALL_END: {\n const jsonStr = this.accumulatingArgs.get(event.toolCallId);\n const toolCall = this.pendingToolCalls.get(event.toolCallId);\n if (toolCall && jsonStr) {\n let parsedInput: Record<string, unknown>;\n try {\n parsedInput = JSON.parse(jsonStr) as Record<string, unknown>;\n } catch (error) {\n // Fail-fast: don't silently continue with empty input\n throw new Error(\n `Failed to parse tool call arguments for ${event.toolCallId}: ${error instanceof Error ? error.message : \"Unknown error\"}. JSON: ${jsonStr.slice(0, 100)}${jsonStr.length > 100 ? \"...\" : \"\"}`,\n );\n }\n\n parsedInput = this.unstrictify(toolCall.name, parsedInput);\n toolCall.input = parsedInput;\n }\n break;\n }\n\n default:\n // Other event types are ignored - only tool call events are tracked\n break;\n }\n }\n\n /**\n * Parses partial JSON from the accumulated args for a tool call and\n * unstrictifies the result. Used during streaming to get the current\n * best-effort parsed args.\n * @param toolCallId - ID of the tool call to parse\n * @returns Parsed and unstrictified args, or undefined if not parseable yet\n */\n parsePartialArgs(toolCallId: string): Record<string, unknown> | undefined {\n const accToolCall = this.getAccumulatingToolCall(toolCallId);\n if (!accToolCall) return undefined;\n\n try {\n const parsed: unknown = parsePartialJson(accToolCall.accumulatedArgs);\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n !Array.isArray(parsed)\n ) {\n return this.unstrictify(\n accToolCall.name,\n parsed as Record<string, unknown>,\n );\n }\n } catch {\n /* not parseable yet */\n }\n return undefined;\n }\n\n /**\n * Gets the name and accumulated args for a tool call that is still accumulating.\n * @param toolCallId - ID of the tool call to look up\n * @returns The tool name and raw accumulated args string, or undefined if not found\n */\n getAccumulatingToolCall(\n toolCallId: string,\n ): { name: string; accumulatedArgs: string } | undefined {\n const toolCall = this.pendingToolCalls.get(toolCallId);\n const args = this.accumulatingArgs.get(toolCallId);\n if (!toolCall || args === undefined) return undefined;\n return { name: toolCall.name, accumulatedArgs: args };\n }\n\n /**\n * Gets tool calls for the given IDs, filtered to only those that exist.\n * @param toolCallIds - IDs of tool calls to retrieve\n * @returns Map of tool call ID to pending tool call\n */\n getToolCallsById(toolCallIds: string[]): Map<string, PendingToolCall> {\n const result = new Map<string, PendingToolCall>();\n for (const id of toolCallIds) {\n const toolCall = this.pendingToolCalls.get(id);\n if (toolCall) {\n result.set(id, toolCall);\n }\n }\n return result;\n }\n\n /**\n * Clears tracked tool calls for the given IDs.\n * @param toolCallIds - IDs of tool calls to clear\n */\n clearToolCalls(toolCallIds: string[]): void {\n for (const id of toolCallIds) {\n this.pendingToolCalls.delete(id);\n this.accumulatingArgs.delete(id);\n }\n }\n\n /**\n * Unstrictify params using the schema for the given tool name.\n * Returns params unchanged if no schema is available.\n */\n private unstrictify(\n toolName: string,\n params: Record<string, unknown>,\n ): Record<string, unknown> {\n const schema = this._toolSchemas.get(toolName);\n if (!schema) return params;\n return unstrictifyToolCallParamsFromSchema(schema, params);\n }\n}\n"]}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { EventType } from "@ag-ui/core";
|
|
2
2
|
import { ToolCallTracker } from "./tool-call-tracker.js";
|
|
3
|
+
/** Minimal tool definition for tests — only name + inputSchema are needed. */
|
|
4
|
+
function fakeTool(name, inputSchema) {
|
|
5
|
+
return {
|
|
6
|
+
name,
|
|
7
|
+
description: "",
|
|
8
|
+
tool: () => null,
|
|
9
|
+
inputSchema,
|
|
10
|
+
outputSchema: {},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Helper to create a tracker with a started tool call. */
|
|
14
|
+
function createTrackerWithToolCall(toolCallId = "call_1", toolCallName = "get_weather", toolRegistry) {
|
|
15
|
+
const tracker = new ToolCallTracker(toolRegistry);
|
|
16
|
+
tracker.handleEvent({
|
|
17
|
+
type: EventType.TOOL_CALL_START,
|
|
18
|
+
toolCallId,
|
|
19
|
+
toolCallName,
|
|
20
|
+
parentMessageId: "msg_1",
|
|
21
|
+
});
|
|
22
|
+
return tracker;
|
|
23
|
+
}
|
|
3
24
|
describe("ToolCallTracker", () => {
|
|
4
25
|
describe("getAccumulatingToolCall", () => {
|
|
5
26
|
it("returns undefined for unknown tool call ID", () => {
|
|
@@ -61,5 +82,162 @@ describe("ToolCallTracker", () => {
|
|
|
61
82
|
});
|
|
62
83
|
});
|
|
63
84
|
});
|
|
85
|
+
describe("handleEvent - TOOL_CALL_END", () => {
|
|
86
|
+
it("parses accumulated JSON on TOOL_CALL_END", () => {
|
|
87
|
+
const tracker = createTrackerWithToolCall();
|
|
88
|
+
tracker.handleEvent({
|
|
89
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
90
|
+
toolCallId: "call_1",
|
|
91
|
+
delta: '{"city":"NYC"}',
|
|
92
|
+
});
|
|
93
|
+
tracker.handleEvent({
|
|
94
|
+
type: EventType.TOOL_CALL_END,
|
|
95
|
+
toolCallId: "call_1",
|
|
96
|
+
});
|
|
97
|
+
const result = tracker.getToolCallsById(["call_1"]);
|
|
98
|
+
expect(result.get("call_1")?.input).toEqual({ city: "NYC" });
|
|
99
|
+
});
|
|
100
|
+
it("throws on invalid JSON at TOOL_CALL_END", () => {
|
|
101
|
+
const tracker = createTrackerWithToolCall();
|
|
102
|
+
tracker.handleEvent({
|
|
103
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
104
|
+
toolCallId: "call_1",
|
|
105
|
+
delta: "{not valid json",
|
|
106
|
+
});
|
|
107
|
+
expect(() => tracker.handleEvent({
|
|
108
|
+
type: EventType.TOOL_CALL_END,
|
|
109
|
+
toolCallId: "call_1",
|
|
110
|
+
})).toThrow("Failed to parse tool call arguments for call_1");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe("TOOL_CALL_END with unstrictification", () => {
|
|
114
|
+
const schema = {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
city: { type: "string" },
|
|
118
|
+
units: { type: "string" },
|
|
119
|
+
},
|
|
120
|
+
required: ["city"],
|
|
121
|
+
};
|
|
122
|
+
const registry = {
|
|
123
|
+
get_weather: fakeTool("get_weather", schema),
|
|
124
|
+
};
|
|
125
|
+
it("strips null optional params when registry is provided", () => {
|
|
126
|
+
const tracker = createTrackerWithToolCall("call_1", "get_weather", registry);
|
|
127
|
+
tracker.handleEvent({
|
|
128
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
129
|
+
toolCallId: "call_1",
|
|
130
|
+
delta: '{"city":"Seattle","units":null}',
|
|
131
|
+
});
|
|
132
|
+
tracker.handleEvent({
|
|
133
|
+
type: EventType.TOOL_CALL_END,
|
|
134
|
+
toolCallId: "call_1",
|
|
135
|
+
});
|
|
136
|
+
const result = tracker.getToolCallsById(["call_1"]);
|
|
137
|
+
expect(result.get("call_1")?.input).toEqual({ city: "Seattle" });
|
|
138
|
+
});
|
|
139
|
+
it("preserves required null params", () => {
|
|
140
|
+
const tracker = createTrackerWithToolCall("call_1", "get_weather", registry);
|
|
141
|
+
tracker.handleEvent({
|
|
142
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
143
|
+
toolCallId: "call_1",
|
|
144
|
+
delta: '{"city":null,"units":null}',
|
|
145
|
+
});
|
|
146
|
+
tracker.handleEvent({
|
|
147
|
+
type: EventType.TOOL_CALL_END,
|
|
148
|
+
toolCallId: "call_1",
|
|
149
|
+
});
|
|
150
|
+
const result = tracker.getToolCallsById(["call_1"]);
|
|
151
|
+
// city is required, so null is preserved; units is optional, so null is stripped
|
|
152
|
+
expect(result.get("call_1")?.input).toEqual({ city: null });
|
|
153
|
+
});
|
|
154
|
+
it("does not unstrictify when no registry is provided", () => {
|
|
155
|
+
const tracker = createTrackerWithToolCall();
|
|
156
|
+
tracker.handleEvent({
|
|
157
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
158
|
+
toolCallId: "call_1",
|
|
159
|
+
delta: '{"city":"Seattle","units":null}',
|
|
160
|
+
});
|
|
161
|
+
tracker.handleEvent({
|
|
162
|
+
type: EventType.TOOL_CALL_END,
|
|
163
|
+
toolCallId: "call_1",
|
|
164
|
+
});
|
|
165
|
+
const result = tracker.getToolCallsById(["call_1"]);
|
|
166
|
+
// Without registry, nulls are preserved
|
|
167
|
+
expect(result.get("call_1")?.input).toEqual({
|
|
168
|
+
city: "Seattle",
|
|
169
|
+
units: null,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
it("preserves _tambo_* pass-through params", () => {
|
|
173
|
+
const tracker = createTrackerWithToolCall("call_1", "get_weather", registry);
|
|
174
|
+
tracker.handleEvent({
|
|
175
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
176
|
+
toolCallId: "call_1",
|
|
177
|
+
delta: '{"city":"Seattle","units":null,"_tambo_statusMessage":"Loading"}',
|
|
178
|
+
});
|
|
179
|
+
tracker.handleEvent({
|
|
180
|
+
type: EventType.TOOL_CALL_END,
|
|
181
|
+
toolCallId: "call_1",
|
|
182
|
+
});
|
|
183
|
+
const result = tracker.getToolCallsById(["call_1"]);
|
|
184
|
+
expect(result.get("call_1")?.input).toEqual({
|
|
185
|
+
city: "Seattle",
|
|
186
|
+
_tambo_statusMessage: "Loading",
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe("parsePartialArgs", () => {
|
|
191
|
+
const schema = {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
required_string: { type: "string" },
|
|
195
|
+
optional_string: { type: "string" },
|
|
196
|
+
},
|
|
197
|
+
required: ["required_string"],
|
|
198
|
+
};
|
|
199
|
+
const registry = {
|
|
200
|
+
my_tool: fakeTool("my_tool", schema),
|
|
201
|
+
};
|
|
202
|
+
it("returns undefined for unknown tool call ID", () => {
|
|
203
|
+
const tracker = new ToolCallTracker(registry);
|
|
204
|
+
expect(tracker.parsePartialArgs("nonexistent")).toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
it("returns undefined when partial JSON is not parseable", () => {
|
|
207
|
+
const tracker = createTrackerWithToolCall("call_1", "my_tool", registry);
|
|
208
|
+
tracker.handleEvent({
|
|
209
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
210
|
+
toolCallId: "call_1",
|
|
211
|
+
delta: '{"req',
|
|
212
|
+
});
|
|
213
|
+
// partial-json should handle this, but if it can't parse to an object
|
|
214
|
+
// the method returns undefined
|
|
215
|
+
const result = tracker.parsePartialArgs("call_1");
|
|
216
|
+
// partial-json can parse this to { req: undefined } or similar — either
|
|
217
|
+
// way it should not throw
|
|
218
|
+
expect(result === undefined || typeof result === "object").toBe(true);
|
|
219
|
+
});
|
|
220
|
+
it("unstrictifies partial args during streaming", () => {
|
|
221
|
+
const tracker = createTrackerWithToolCall("call_1", "my_tool", registry);
|
|
222
|
+
// Stream the full strict JSON in 3-char chunks
|
|
223
|
+
const fullJson = '{"required_string":"required","optional_string":null}';
|
|
224
|
+
const chunkSize = 3;
|
|
225
|
+
for (let i = 0; i < fullJson.length; i += chunkSize) {
|
|
226
|
+
tracker.handleEvent({
|
|
227
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
228
|
+
toolCallId: "call_1",
|
|
229
|
+
delta: fullJson.slice(i, i + chunkSize),
|
|
230
|
+
});
|
|
231
|
+
const partial = tracker.parsePartialArgs("call_1");
|
|
232
|
+
if (partial) {
|
|
233
|
+
// Must never contain optional_string
|
|
234
|
+
expect(partial).not.toHaveProperty("optional_string");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// After all chunks, should have the required param
|
|
238
|
+
const final = tracker.parsePartialArgs("call_1");
|
|
239
|
+
expect(final).toEqual({ required_string: "required" });
|
|
240
|
+
});
|
|
241
|
+
});
|
|
64
242
|
});
|
|
65
243
|
//# sourceMappingURL=tool-call-tracker.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-call-tracker.test.js","sourceRoot":"","sources":["../../../src/v1/utils/tool-call-tracker.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACvC,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;YACpD,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;YACtC,MAAM,CAAC,OAAO,CAAC,uBAAuB,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;QACzE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;YACvE,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;YACtC,OAAO,CAAC,WAAW,CAAC;gBAClB,IAAI,EAAE,SAAS,CAAC,eAAe;gBAC/B,UAAU,EAAE,QAAQ;gBACpB,YAAY,EAAE,aAAa;gBAC3B,eAAe,EAAE,OAAO;aACzB,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC;YACzD,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,eAAe,EAAE,EAAE,EAAE,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;YACrE,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;YACtC,OAAO,CAAC,WAAW,CAAC;gBAClB,IAAI,EAAE,SAAS,CAAC,eAAe;gBAC/B,UAAU,EAAE,QAAQ;gBACpB,YAAY,EAAE,aAAa;gBAC3B,eAAe,EAAE,OAAO;aACzB,CAAC,CAAC;YACH,OAAO,CAAC,WAAW,CAAC;gBAClB,IAAI,EAAE,SAAS,CAAC,cAAc;gBAC9B,UAAU,EAAE,QAAQ;gBACpB,KAAK,EAAE,gBAAgB;aACxB,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC;YACzD,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;gBACrB,IAAI,EAAE,aAAa;gBACnB,eAAe,EAAE,gBAAgB;aAClC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;YAC1C,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;YACtC,OAAO,CAAC,WAAW,CAAC;gBAClB,IAAI,EAAE,SAAS,CAAC,eAAe;gBAC/B,UAAU,EAAE,QAAQ;gBACpB,YAAY,EAAE,aAAa;gBAC3B,eAAe,EAAE,OAAO;aACzB,CAAC,CAAC;YACH,OAAO,CAAC,WAAW,CAAC;gBAClB,IAAI,EAAE,SAAS,CAAC,cAAc;gBAC9B,UAAU,EAAE,QAAQ;gBACpB,KAAK,EAAE,WAAW;aACnB,CAAC,CAAC;YACH,OAAO,CAAC,WAAW,CAAC;gBAClB,IAAI,EAAE,SAAS,CAAC,cAAc;gBAC9B,UAAU,EAAE,QAAQ;gBACpB,KAAK,EAAE,UAAU;aAClB,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC;YACzD,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;gBACrB,IAAI,EAAE,aAAa;gBACnB,eAAe,EAAE,mBAAmB;aACrC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { EventType } from \"@ag-ui/core\";\nimport { ToolCallTracker } from \"./tool-call-tracker\";\n\ndescribe(\"ToolCallTracker\", () => {\n describe(\"getAccumulatingToolCall\", () => {\n it(\"returns undefined for unknown tool call ID\", () => {\n const tracker = new ToolCallTracker();\n expect(tracker.getAccumulatingToolCall(\"nonexistent\")).toBeUndefined();\n });\n\n it(\"returns name and empty accumulated args after TOOL_CALL_START\", () => {\n const tracker = new ToolCallTracker();\n tracker.handleEvent({\n type: EventType.TOOL_CALL_START,\n toolCallId: \"call_1\",\n toolCallName: \"write_story\",\n parentMessageId: \"msg_1\",\n });\n\n const result = tracker.getAccumulatingToolCall(\"call_1\");\n expect(result).toEqual({ name: \"write_story\", accumulatedArgs: \"\" });\n });\n\n it(\"returns name and accumulated args after START + ARGS events\", () => {\n const tracker = new ToolCallTracker();\n tracker.handleEvent({\n type: EventType.TOOL_CALL_START,\n toolCallId: \"call_1\",\n toolCallName: \"write_story\",\n parentMessageId: \"msg_1\",\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '{\"title\":\"Once',\n });\n\n const result = tracker.getAccumulatingToolCall(\"call_1\");\n expect(result).toEqual({\n name: \"write_story\",\n accumulatedArgs: '{\"title\":\"Once',\n });\n });\n\n it(\"accumulates multiple ARGS deltas\", () => {\n const tracker = new ToolCallTracker();\n tracker.handleEvent({\n type: EventType.TOOL_CALL_START,\n toolCallId: \"call_1\",\n toolCallName: \"write_story\",\n parentMessageId: \"msg_1\",\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '{\"title\":',\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '\"Hello\"}',\n });\n\n const result = tracker.getAccumulatingToolCall(\"call_1\");\n expect(result).toEqual({\n name: \"write_story\",\n accumulatedArgs: '{\"title\":\"Hello\"}',\n });\n });\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"tool-call-tracker.test.js","sourceRoot":"","sources":["../../../src/v1/utils/tool-call-tracker.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventType } from \"@ag-ui/core\";\nimport type { JSONSchema7 } from \"json-schema\";\nimport type { TamboTool } from \"../../model/component-metadata\";\nimport { ToolCallTracker } from \"./tool-call-tracker\";\n\n/** Minimal tool definition for tests — only name + inputSchema are needed. */\nfunction fakeTool(name: string, inputSchema: JSONSchema7): TamboTool {\n return {\n name,\n description: \"\",\n tool: () => null,\n inputSchema,\n outputSchema: {},\n } as TamboTool;\n}\n\n/** Helper to create a tracker with a started tool call. */\nfunction createTrackerWithToolCall(\n toolCallId = \"call_1\",\n toolCallName = \"get_weather\",\n toolRegistry?: Record<string, TamboTool>,\n): ToolCallTracker {\n const tracker = new ToolCallTracker(toolRegistry);\n tracker.handleEvent({\n type: EventType.TOOL_CALL_START,\n toolCallId,\n toolCallName,\n parentMessageId: \"msg_1\",\n });\n return tracker;\n}\n\ndescribe(\"ToolCallTracker\", () => {\n describe(\"getAccumulatingToolCall\", () => {\n it(\"returns undefined for unknown tool call ID\", () => {\n const tracker = new ToolCallTracker();\n expect(tracker.getAccumulatingToolCall(\"nonexistent\")).toBeUndefined();\n });\n\n it(\"returns name and empty accumulated args after TOOL_CALL_START\", () => {\n const tracker = new ToolCallTracker();\n tracker.handleEvent({\n type: EventType.TOOL_CALL_START,\n toolCallId: \"call_1\",\n toolCallName: \"write_story\",\n parentMessageId: \"msg_1\",\n });\n\n const result = tracker.getAccumulatingToolCall(\"call_1\");\n expect(result).toEqual({ name: \"write_story\", accumulatedArgs: \"\" });\n });\n\n it(\"returns name and accumulated args after START + ARGS events\", () => {\n const tracker = new ToolCallTracker();\n tracker.handleEvent({\n type: EventType.TOOL_CALL_START,\n toolCallId: \"call_1\",\n toolCallName: \"write_story\",\n parentMessageId: \"msg_1\",\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '{\"title\":\"Once',\n });\n\n const result = tracker.getAccumulatingToolCall(\"call_1\");\n expect(result).toEqual({\n name: \"write_story\",\n accumulatedArgs: '{\"title\":\"Once',\n });\n });\n\n it(\"accumulates multiple ARGS deltas\", () => {\n const tracker = new ToolCallTracker();\n tracker.handleEvent({\n type: EventType.TOOL_CALL_START,\n toolCallId: \"call_1\",\n toolCallName: \"write_story\",\n parentMessageId: \"msg_1\",\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '{\"title\":',\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '\"Hello\"}',\n });\n\n const result = tracker.getAccumulatingToolCall(\"call_1\");\n expect(result).toEqual({\n name: \"write_story\",\n accumulatedArgs: '{\"title\":\"Hello\"}',\n });\n });\n });\n\n describe(\"handleEvent - TOOL_CALL_END\", () => {\n it(\"parses accumulated JSON on TOOL_CALL_END\", () => {\n const tracker = createTrackerWithToolCall();\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '{\"city\":\"NYC\"}',\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_END,\n toolCallId: \"call_1\",\n });\n\n const result = tracker.getToolCallsById([\"call_1\"]);\n expect(result.get(\"call_1\")?.input).toEqual({ city: \"NYC\" });\n });\n\n it(\"throws on invalid JSON at TOOL_CALL_END\", () => {\n const tracker = createTrackerWithToolCall();\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: \"{not valid json\",\n });\n\n expect(() =>\n tracker.handleEvent({\n type: EventType.TOOL_CALL_END,\n toolCallId: \"call_1\",\n }),\n ).toThrow(\"Failed to parse tool call arguments for call_1\");\n });\n });\n\n describe(\"TOOL_CALL_END with unstrictification\", () => {\n const schema: JSONSchema7 = {\n type: \"object\",\n properties: {\n city: { type: \"string\" },\n units: { type: \"string\" },\n },\n required: [\"city\"],\n };\n\n const registry: Record<string, TamboTool> = {\n get_weather: fakeTool(\"get_weather\", schema),\n };\n\n it(\"strips null optional params when registry is provided\", () => {\n const tracker = createTrackerWithToolCall(\n \"call_1\",\n \"get_weather\",\n registry,\n );\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '{\"city\":\"Seattle\",\"units\":null}',\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_END,\n toolCallId: \"call_1\",\n });\n\n const result = tracker.getToolCallsById([\"call_1\"]);\n expect(result.get(\"call_1\")?.input).toEqual({ city: \"Seattle\" });\n });\n\n it(\"preserves required null params\", () => {\n const tracker = createTrackerWithToolCall(\n \"call_1\",\n \"get_weather\",\n registry,\n );\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '{\"city\":null,\"units\":null}',\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_END,\n toolCallId: \"call_1\",\n });\n\n const result = tracker.getToolCallsById([\"call_1\"]);\n // city is required, so null is preserved; units is optional, so null is stripped\n expect(result.get(\"call_1\")?.input).toEqual({ city: null });\n });\n\n it(\"does not unstrictify when no registry is provided\", () => {\n const tracker = createTrackerWithToolCall();\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '{\"city\":\"Seattle\",\"units\":null}',\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_END,\n toolCallId: \"call_1\",\n });\n\n const result = tracker.getToolCallsById([\"call_1\"]);\n // Without registry, nulls are preserved\n expect(result.get(\"call_1\")?.input).toEqual({\n city: \"Seattle\",\n units: null,\n });\n });\n\n it(\"preserves _tambo_* pass-through params\", () => {\n const tracker = createTrackerWithToolCall(\n \"call_1\",\n \"get_weather\",\n registry,\n );\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta:\n '{\"city\":\"Seattle\",\"units\":null,\"_tambo_statusMessage\":\"Loading\"}',\n });\n tracker.handleEvent({\n type: EventType.TOOL_CALL_END,\n toolCallId: \"call_1\",\n });\n\n const result = tracker.getToolCallsById([\"call_1\"]);\n expect(result.get(\"call_1\")?.input).toEqual({\n city: \"Seattle\",\n _tambo_statusMessage: \"Loading\",\n });\n });\n });\n\n describe(\"parsePartialArgs\", () => {\n const schema: JSONSchema7 = {\n type: \"object\",\n properties: {\n required_string: { type: \"string\" },\n optional_string: { type: \"string\" },\n },\n required: [\"required_string\"],\n };\n\n const registry: Record<string, TamboTool> = {\n my_tool: fakeTool(\"my_tool\", schema),\n };\n\n it(\"returns undefined for unknown tool call ID\", () => {\n const tracker = new ToolCallTracker(registry);\n expect(tracker.parsePartialArgs(\"nonexistent\")).toBeUndefined();\n });\n\n it(\"returns undefined when partial JSON is not parseable\", () => {\n const tracker = createTrackerWithToolCall(\"call_1\", \"my_tool\", registry);\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: '{\"req',\n });\n\n // partial-json should handle this, but if it can't parse to an object\n // the method returns undefined\n const result = tracker.parsePartialArgs(\"call_1\");\n // partial-json can parse this to { req: undefined } or similar — either\n // way it should not throw\n expect(result === undefined || typeof result === \"object\").toBe(true);\n });\n\n it(\"unstrictifies partial args during streaming\", () => {\n const tracker = createTrackerWithToolCall(\"call_1\", \"my_tool\", registry);\n\n // Stream the full strict JSON in 3-char chunks\n const fullJson = '{\"required_string\":\"required\",\"optional_string\":null}';\n const chunkSize = 3;\n for (let i = 0; i < fullJson.length; i += chunkSize) {\n tracker.handleEvent({\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: \"call_1\",\n delta: fullJson.slice(i, i + chunkSize),\n });\n\n const partial = tracker.parsePartialArgs(\"call_1\");\n if (partial) {\n // Must never contain optional_string\n expect(partial).not.toHaveProperty(\"optional_string\");\n }\n }\n\n // After all chunks, should have the required param\n const final = tracker.parsePartialArgs(\"call_1\");\n expect(final).toEqual({ required_string: \"required\" });\n });\n });\n});\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unstrictify tool call parameters using the original JSON Schema.
|
|
3
|
+
*
|
|
4
|
+
* When OpenAI's structured outputs mode is enabled, all optional parameters
|
|
5
|
+
* become required-and-nullable. The LLM then sends `null` for parameters the
|
|
6
|
+
* user didn't specify. This module reverses that transformation by comparing
|
|
7
|
+
* the LLM's output against the original schema and stripping nulls for
|
|
8
|
+
* parameters that were originally optional and non-nullable.
|
|
9
|
+
*
|
|
10
|
+
* Copied from packages/core/src/strictness/tool-call-strict.ts (minus the
|
|
11
|
+
* OpenAI-specific `unstrictifyToolCallRequest` wrapper).
|
|
12
|
+
*/
|
|
13
|
+
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
|
|
14
|
+
/**
|
|
15
|
+
* Unstrictify tool call params using the original JSON Schema.
|
|
16
|
+
*
|
|
17
|
+
* Unlike the private `unstrictifyToolCallParams` which throws on unknown params,
|
|
18
|
+
* this function separates params into schema-defined vs `_tambo_*` pass-through
|
|
19
|
+
* (server-injected params not in the original schema), unstrictifies only the
|
|
20
|
+
* schema-defined ones, and merges pass-through params back. Unknown keys that
|
|
21
|
+
* aren't in the schema and don't have the `_tambo_` prefix are dropped.
|
|
22
|
+
* @returns The params with strictification-induced nulls stripped for optional
|
|
23
|
+
* non-nullable properties, and pass-through params preserved as-is.
|
|
24
|
+
*/
|
|
25
|
+
export declare function unstrictifyToolCallParamsFromSchema(originalSchema: JSONSchema7, params: Record<string, unknown>): Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Check if a JSON Schema definition allows null values.
|
|
28
|
+
* @param originalSchema - The schema definition to check
|
|
29
|
+
* @returns True if the schema allows null values
|
|
30
|
+
*/
|
|
31
|
+
export declare function canBeNull(originalSchema: JSONSchema7Definition): boolean;
|
|
32
|
+
//# sourceMappingURL=unstrictify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unstrictify.d.ts","sourceRoot":"","sources":["../../../src/v1/utils/unstrictify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAsItE;;;;;;;;;;GAUG;AACH,wBAAgB,mCAAmC,CACjD,cAAc,EAAE,WAAW,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyBzB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,cAAc,EAAE,qBAAqB,GAAG,OAAO,CAaxE"}
|