@dotsetlabs/dotclaw 2.6.0 → 2.6.2
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/config-examples/runtime.json +2 -1
- package/container/agent-runner/package-lock.json +2 -2
- package/container/agent-runner/package.json +1 -1
- package/container/agent-runner/src/index.ts +43 -28
- package/container/agent-runner/src/memory.ts +106 -6
- package/container/agent-runner/src/openrouter-followup.ts +87 -0
- package/container/agent-runner/src/openrouter-input.ts +310 -17
- package/dist/agent-context.d.ts.map +1 -1
- package/dist/agent-context.js +44 -8
- package/dist/agent-context.js.map +1 -1
- package/dist/runtime-config.d.ts +1 -0
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +3 -1
- package/dist/runtime-config.js.map +1 -1
- package/package.json +3 -1
- package/scripts/preflight-prod-chat.js +304 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotclaw-agent-runner",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "dotclaw-agent-runner",
|
|
9
|
-
"version": "2.3.
|
|
9
|
+
"version": "2.3.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@openrouter/sdk": "^0.3.0",
|
|
12
12
|
"cron-parser": "^5.0.0",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
|
-
import { OpenRouter } from '@openrouter/sdk';
|
|
9
|
+
import { OpenRouter, type ResolvedCallModelInput } from '@openrouter/sdk';
|
|
10
10
|
import { createTools, discoverMcpTools, ToolCallRecord, type ToolResultRecord } from './tools.js';
|
|
11
11
|
import { createIpcHandlers } from './ipc.js';
|
|
12
12
|
import { loadAgentConfig } from './agent-config.js';
|
|
@@ -53,9 +53,15 @@ import {
|
|
|
53
53
|
injectImagesIntoContextInput,
|
|
54
54
|
loadImageAttachmentsForInput,
|
|
55
55
|
messagesToOpenRouterInput,
|
|
56
|
+
sanitizeConversationInputForResponses,
|
|
56
57
|
} from './openrouter-input.js';
|
|
58
|
+
import {
|
|
59
|
+
extractFunctionCallsForReplay,
|
|
60
|
+
toReplayFunctionCallItems,
|
|
61
|
+
} from './openrouter-followup.js';
|
|
57
62
|
|
|
58
63
|
type OpenRouterResult = ReturnType<OpenRouter['callModel']>;
|
|
64
|
+
type OpenRouterResolvedInputArray = Extract<NonNullable<ResolvedCallModelInput['input']>, unknown[]>;
|
|
59
65
|
|
|
60
66
|
|
|
61
67
|
const SESSION_ROOT = '/workspace/session';
|
|
@@ -208,21 +214,6 @@ function extractTextFromApiResponse(response: any): string {
|
|
|
208
214
|
return '';
|
|
209
215
|
}
|
|
210
216
|
|
|
211
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
-
function extractFunctionCalls(response: any): Array<{ id: string; name: string; arguments: any }> {
|
|
213
|
-
const calls: Array<{ id: string; name: string; arguments: unknown }> = [];
|
|
214
|
-
for (const item of response?.output || []) {
|
|
215
|
-
if (item?.type === 'function_call') {
|
|
216
|
-
let args = item.arguments;
|
|
217
|
-
if (typeof args === 'string') {
|
|
218
|
-
try { args = JSON.parse(args); } catch { /* keep as string */ }
|
|
219
|
-
}
|
|
220
|
-
calls.push({ id: item.callId, name: item.name, arguments: args });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return calls;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
217
|
function writeOutput(output: ContainerOutput): void {
|
|
227
218
|
console.log(OUTPUT_START_MARKER);
|
|
228
219
|
console.log(JSON.stringify(output));
|
|
@@ -1223,6 +1214,21 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1223
1214
|
}
|
|
1224
1215
|
};
|
|
1225
1216
|
|
|
1217
|
+
const sanitizeConversationInput = (
|
|
1218
|
+
items: unknown[],
|
|
1219
|
+
label: string
|
|
1220
|
+
): OpenRouterResolvedInputArray => {
|
|
1221
|
+
const sanitized = sanitizeConversationInputForResponses(items);
|
|
1222
|
+
if (sanitized.rewrittenCount > 0 || sanitized.droppedCount > 0) {
|
|
1223
|
+
log(`Sanitized OpenRouter input (${label}): rewritten=${sanitized.rewrittenCount}, dropped=${sanitized.droppedCount}`);
|
|
1224
|
+
}
|
|
1225
|
+
if (sanitized.items.length === 0) {
|
|
1226
|
+
log(`Sanitized OpenRouter input (${label}) produced empty payload; inserting fallback message`);
|
|
1227
|
+
return [{ role: 'user', content: '[No usable conversation context available.]' }] as OpenRouterResolvedInputArray;
|
|
1228
|
+
}
|
|
1229
|
+
return sanitized.items as OpenRouterResolvedInputArray;
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1226
1232
|
try {
|
|
1227
1233
|
const { instructions: resolvedInstructions, instructionsTokens: resolvedInstructionTokens, contextMessages } = buildContext();
|
|
1228
1234
|
// Apply 1.3x safety margin to account for token estimation inaccuracy.
|
|
@@ -1244,12 +1250,13 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1244
1250
|
}
|
|
1245
1251
|
}
|
|
1246
1252
|
|
|
1247
|
-
const
|
|
1253
|
+
const rawContextInput = messagesToOpenRouterInput(contextMessages);
|
|
1248
1254
|
|
|
1249
1255
|
// Inject vision content into the last user message if images are present.
|
|
1250
1256
|
// Uses OpenRouter Responses API content part types (input_text/input_image).
|
|
1251
1257
|
const imageContent = loadImageAttachmentsForInput(input.attachments, { log });
|
|
1252
|
-
injectImagesIntoContextInput(
|
|
1258
|
+
injectImagesIntoContextInput(rawContextInput, imageContent);
|
|
1259
|
+
const contextInput: OpenRouterResolvedInputArray = sanitizeConversationInput(rawContextInput, 'context');
|
|
1253
1260
|
|
|
1254
1261
|
let lastError: unknown = null;
|
|
1255
1262
|
for (let attempt = 0; attempt < modelChain.length; attempt++) {
|
|
@@ -1272,11 +1279,11 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1272
1279
|
// We use schema-only tools (no execute functions) so the SDK returns tool calls
|
|
1273
1280
|
// without auto-executing, then run the loop ourselves with full context.
|
|
1274
1281
|
|
|
1275
|
-
|
|
1276
|
-
let conversationInput: any[] = [...contextInput];
|
|
1282
|
+
let conversationInput: OpenRouterResolvedInputArray = sanitizeConversationInput([...contextInput], 'initial_conversation');
|
|
1277
1283
|
let step = 0;
|
|
1278
1284
|
|
|
1279
1285
|
// Initial call — uses streaming for real-time delivery
|
|
1286
|
+
conversationInput = sanitizeConversationInput(conversationInput, `initial_call:${currentModel}`);
|
|
1280
1287
|
const initialResult = openrouter.callModel({
|
|
1281
1288
|
model: currentModel,
|
|
1282
1289
|
instructions: resolvedInstructions,
|
|
@@ -1311,7 +1318,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1311
1318
|
}
|
|
1312
1319
|
|
|
1313
1320
|
responseText = extractTextFromApiResponse(lastResponse);
|
|
1314
|
-
let pendingCalls =
|
|
1321
|
+
let pendingCalls = extractFunctionCallsForReplay(lastResponse);
|
|
1315
1322
|
const callSignatureCounts = new Map<string, number>();
|
|
1316
1323
|
let previousRoundSignature = '';
|
|
1317
1324
|
let repeatedRoundCount = 0;
|
|
@@ -1430,9 +1437,12 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1430
1437
|
reason: nudgeReason,
|
|
1431
1438
|
attempt: toolRequirementNudgeAttempt
|
|
1432
1439
|
});
|
|
1433
|
-
|
|
1434
|
-
|
|
1440
|
+
if (responseText) {
|
|
1441
|
+
conversationInput = [...conversationInput, { role: 'assistant', content: responseText }];
|
|
1442
|
+
}
|
|
1443
|
+
conversationInput = [...conversationInput, { role: 'user', content: nudgePrompt }];
|
|
1435
1444
|
try {
|
|
1445
|
+
conversationInput = sanitizeConversationInput(conversationInput, `tool_nudge:${toolRequirementNudgeAttempt}`);
|
|
1436
1446
|
const nudgeResult = openrouter.callModel({
|
|
1437
1447
|
model: currentModel,
|
|
1438
1448
|
instructions: resolvedInstructions,
|
|
@@ -1448,7 +1458,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1448
1458
|
responseText = nudgeText;
|
|
1449
1459
|
writeStreamChunk(nudgeText);
|
|
1450
1460
|
}
|
|
1451
|
-
pendingCalls =
|
|
1461
|
+
pendingCalls = extractFunctionCallsForReplay(lastResponse);
|
|
1452
1462
|
} catch (nudgeErr) {
|
|
1453
1463
|
log(`Tool requirement nudge failed: ${nudgeErr instanceof Error ? nudgeErr.message : String(nudgeErr)}`);
|
|
1454
1464
|
break;
|
|
@@ -1588,7 +1598,8 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1588
1598
|
|
|
1589
1599
|
// Build follow-up input with FULL conversation context:
|
|
1590
1600
|
// original messages + model output + tool results (accumulated each round)
|
|
1591
|
-
|
|
1601
|
+
const replayFunctionCalls = toReplayFunctionCallItems(pendingCalls);
|
|
1602
|
+
conversationInput = [...conversationInput, ...replayFunctionCalls, ...toolResults];
|
|
1592
1603
|
|
|
1593
1604
|
// Compact oversized tool payloads before follow-up calls to reduce context bloat.
|
|
1594
1605
|
const compactedConversation = compactToolConversationItems(conversationInput, {
|
|
@@ -1643,6 +1654,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1643
1654
|
}
|
|
1644
1655
|
|
|
1645
1656
|
// Follow-up call with complete context — model sees the full conversation
|
|
1657
|
+
conversationInput = sanitizeConversationInput(conversationInput, `tool_followup:${step}`);
|
|
1646
1658
|
const followupResult = openrouter.callModel({
|
|
1647
1659
|
model: currentModel,
|
|
1648
1660
|
instructions: resolvedInstructions,
|
|
@@ -1680,6 +1692,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1680
1692
|
if (cleared > 0) {
|
|
1681
1693
|
log(`Hard-cleared ${cleared} tool results, retrying`);
|
|
1682
1694
|
try {
|
|
1695
|
+
conversationInput = sanitizeConversationInput(conversationInput, `tool_followup_retry:${step}`);
|
|
1683
1696
|
const retryResult = openrouter.callModel({
|
|
1684
1697
|
model: currentModel,
|
|
1685
1698
|
instructions: resolvedInstructions,
|
|
@@ -1695,7 +1708,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1695
1708
|
responseText = retryText;
|
|
1696
1709
|
writeStreamChunk(retryText);
|
|
1697
1710
|
}
|
|
1698
|
-
pendingCalls =
|
|
1711
|
+
pendingCalls = extractFunctionCallsForReplay(lastResponse);
|
|
1699
1712
|
continue;
|
|
1700
1713
|
} catch (retryErr) {
|
|
1701
1714
|
log(`Context overflow retry failed: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
|
|
@@ -1715,7 +1728,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1715
1728
|
writeStreamChunk(followupText);
|
|
1716
1729
|
}
|
|
1717
1730
|
|
|
1718
|
-
pendingCalls =
|
|
1731
|
+
pendingCalls = extractFunctionCallsForReplay(lastResponse);
|
|
1719
1732
|
}
|
|
1720
1733
|
|
|
1721
1734
|
if (toolExecutionRequirement.required && toolCalls.length === 0) {
|
|
@@ -1745,6 +1758,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1745
1758
|
});
|
|
1746
1759
|
conversationInput = [...conversationInput, { role: 'user', content: continuationPrompt }];
|
|
1747
1760
|
try {
|
|
1761
|
+
conversationInput = sanitizeConversationInput(conversationInput, `forced_synthesis:${synthesisReason}`);
|
|
1748
1762
|
const synthesisResult = openrouter.callModel({
|
|
1749
1763
|
model: currentModel,
|
|
1750
1764
|
instructions: resolvedInstructions,
|
|
@@ -1849,10 +1863,11 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1849
1863
|
keepRecentCount: Math.max(1, toKeep.length)
|
|
1850
1864
|
}).retryInput;
|
|
1851
1865
|
try {
|
|
1866
|
+
const sanitizedCompactedInput = sanitizeConversationInput(compactedInput, 'context_overflow_retry');
|
|
1852
1867
|
const retryResult = openrouter.callModel({
|
|
1853
1868
|
model: currentModel,
|
|
1854
1869
|
instructions: minInstructions,
|
|
1855
|
-
input:
|
|
1870
|
+
input: sanitizedCompactedInput,
|
|
1856
1871
|
tools: schemaTools,
|
|
1857
1872
|
maxOutputTokens: effectiveMaxOutputTokens,
|
|
1858
1873
|
temperature: config.temperature,
|
|
@@ -158,21 +158,121 @@ export function appendHistory(ctx: SessionContext, role: 'user' | 'assistant', c
|
|
|
158
158
|
return message;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
function safeStringifyHistoryValue(value: unknown): string {
|
|
162
|
+
try {
|
|
163
|
+
const serialized = JSON.stringify(value);
|
|
164
|
+
return typeof serialized === 'string' ? serialized : String(value);
|
|
165
|
+
} catch {
|
|
166
|
+
return String(value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function coerceHistoryContentToString(value: unknown): string {
|
|
171
|
+
if (typeof value === 'string') return value;
|
|
172
|
+
if (value == null) return '';
|
|
173
|
+
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
const parts = value
|
|
176
|
+
.map((part) => {
|
|
177
|
+
if (!part || typeof part !== 'object') return null;
|
|
178
|
+
const record = part as Record<string, unknown>;
|
|
179
|
+
if (typeof record.text === 'string' && record.text.trim()) return record.text;
|
|
180
|
+
if (typeof record.content === 'string' && record.content.trim()) return record.content;
|
|
181
|
+
if (typeof record.output === 'string' && record.output.trim()) return record.output;
|
|
182
|
+
if (typeof record.refusal === 'string' && record.refusal.trim()) return record.refusal;
|
|
183
|
+
return null;
|
|
184
|
+
})
|
|
185
|
+
.filter((part): part is string => typeof part === 'string');
|
|
186
|
+
if (parts.length > 0) return parts.join('\n');
|
|
187
|
+
return safeStringifyHistoryValue(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeof value === 'object') {
|
|
191
|
+
const record = value as Record<string, unknown>;
|
|
192
|
+
if (typeof record.text === 'string' && record.text.trim()) return record.text;
|
|
193
|
+
if (typeof record.content === 'string' && record.content.trim()) return record.content;
|
|
194
|
+
if (typeof record.output === 'string' && record.output.trim()) return record.output;
|
|
195
|
+
if (typeof record.refusal === 'string' && record.refusal.trim()) return record.refusal;
|
|
196
|
+
return safeStringifyHistoryValue(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return String(value);
|
|
200
|
+
}
|
|
201
|
+
|
|
161
202
|
export function loadHistory(ctx: SessionContext): Message[] {
|
|
162
203
|
if (!fs.existsSync(ctx.historyPath)) return [];
|
|
163
|
-
const
|
|
204
|
+
const raw = fs.readFileSync(ctx.historyPath, 'utf-8');
|
|
205
|
+
if (!raw.trim()) return [];
|
|
206
|
+
const lines = raw.trim().split('\n');
|
|
164
207
|
const messages: Message[] = [];
|
|
208
|
+
let needsRewrite = false;
|
|
209
|
+
let highestSeq = 0;
|
|
165
210
|
for (const line of lines) {
|
|
166
|
-
if (!line.trim())
|
|
211
|
+
if (!line.trim()) {
|
|
212
|
+
needsRewrite = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
167
215
|
try {
|
|
168
|
-
const parsed = JSON.parse(line)
|
|
169
|
-
|
|
170
|
-
|
|
216
|
+
const parsed = JSON.parse(line) as Record<string, unknown>;
|
|
217
|
+
const role = parsed?.role === 'user' || parsed?.role === 'assistant'
|
|
218
|
+
? parsed.role
|
|
219
|
+
: null;
|
|
220
|
+
if (!role) {
|
|
221
|
+
needsRewrite = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const content = coerceHistoryContentToString(parsed.content);
|
|
226
|
+
if (typeof parsed.content !== 'string') {
|
|
227
|
+
needsRewrite = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const timestamp = typeof parsed.timestamp === 'string' && parsed.timestamp
|
|
231
|
+
? parsed.timestamp
|
|
232
|
+
: new Date().toISOString();
|
|
233
|
+
if (timestamp !== parsed.timestamp) {
|
|
234
|
+
needsRewrite = true;
|
|
171
235
|
}
|
|
236
|
+
|
|
237
|
+
const parsedSeq = Number(parsed.seq);
|
|
238
|
+
const seq = Number.isFinite(parsedSeq) && parsedSeq > 0
|
|
239
|
+
? Math.floor(parsedSeq)
|
|
240
|
+
: (highestSeq + 1);
|
|
241
|
+
if (seq !== parsedSeq) {
|
|
242
|
+
needsRewrite = true;
|
|
243
|
+
}
|
|
244
|
+
highestSeq = Math.max(highestSeq, seq);
|
|
245
|
+
|
|
246
|
+
messages.push({
|
|
247
|
+
role,
|
|
248
|
+
content,
|
|
249
|
+
timestamp,
|
|
250
|
+
seq
|
|
251
|
+
});
|
|
172
252
|
} catch {
|
|
173
|
-
|
|
253
|
+
needsRewrite = true;
|
|
174
254
|
}
|
|
175
255
|
}
|
|
256
|
+
|
|
257
|
+
messages.sort((a, b) => a.seq - b.seq);
|
|
258
|
+
let nextSeq = 1;
|
|
259
|
+
for (const message of messages) {
|
|
260
|
+
if (message.seq < nextSeq) {
|
|
261
|
+
message.seq = nextSeq;
|
|
262
|
+
needsRewrite = true;
|
|
263
|
+
}
|
|
264
|
+
nextSeq = message.seq + 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (ctx.meta.nextSeq < nextSeq) {
|
|
268
|
+
ctx.meta.nextSeq = nextSeq;
|
|
269
|
+
saveSessionMeta(ctx);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (needsRewrite) {
|
|
273
|
+
writeHistory(ctx, messages);
|
|
274
|
+
}
|
|
275
|
+
|
|
176
276
|
return messages;
|
|
177
277
|
}
|
|
178
278
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
type RawRecord = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export type ExtractedFunctionCall = {
|
|
4
|
+
id: string;
|
|
5
|
+
itemId?: string;
|
|
6
|
+
name: string;
|
|
7
|
+
arguments: unknown;
|
|
8
|
+
argumentsText: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ReplayFunctionCallItem = {
|
|
12
|
+
type: 'function_call';
|
|
13
|
+
id: string;
|
|
14
|
+
callId: string;
|
|
15
|
+
name: string;
|
|
16
|
+
arguments: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function normalizeCallId(record: RawRecord): { callId?: string; itemId?: string } {
|
|
20
|
+
const callId = typeof record.callId === 'string' && record.callId.trim()
|
|
21
|
+
? record.callId.trim()
|
|
22
|
+
: (typeof record.id === 'string' && record.id.trim() ? record.id.trim() : undefined);
|
|
23
|
+
const itemId = typeof record.id === 'string' && record.id.trim()
|
|
24
|
+
? record.id.trim()
|
|
25
|
+
: undefined;
|
|
26
|
+
return { callId, itemId };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeArguments(raw: unknown): { arguments: unknown; argumentsText: string } {
|
|
30
|
+
if (typeof raw === 'string') {
|
|
31
|
+
const trimmed = raw.trim();
|
|
32
|
+
if (!trimmed) return { arguments: {}, argumentsText: '{}' };
|
|
33
|
+
try {
|
|
34
|
+
return { arguments: JSON.parse(trimmed), argumentsText: trimmed };
|
|
35
|
+
} catch {
|
|
36
|
+
return { arguments: raw, argumentsText: raw };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (raw == null) return { arguments: {}, argumentsText: '{}' };
|
|
40
|
+
try {
|
|
41
|
+
const serialized = JSON.stringify(raw);
|
|
42
|
+
if (typeof serialized === 'string') {
|
|
43
|
+
return { arguments: raw, argumentsText: serialized };
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// fall through
|
|
47
|
+
}
|
|
48
|
+
return { arguments: raw, argumentsText: String(raw) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractFromOutput(output: unknown[]): ExtractedFunctionCall[] {
|
|
52
|
+
const calls: ExtractedFunctionCall[] = [];
|
|
53
|
+
for (const item of output) {
|
|
54
|
+
if (!item || typeof item !== 'object') continue;
|
|
55
|
+
const record = item as RawRecord;
|
|
56
|
+
if (record.type !== 'function_call') continue;
|
|
57
|
+
const { callId, itemId } = normalizeCallId(record);
|
|
58
|
+
const name = typeof record.name === 'string' ? record.name.trim() : '';
|
|
59
|
+
if (!callId || !name) continue;
|
|
60
|
+
const normalizedArguments = normalizeArguments(record.arguments);
|
|
61
|
+
calls.push({
|
|
62
|
+
id: callId,
|
|
63
|
+
itemId,
|
|
64
|
+
name,
|
|
65
|
+
arguments: normalizedArguments.arguments,
|
|
66
|
+
argumentsText: normalizedArguments.argumentsText
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return calls;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function extractFunctionCallsForReplay(response: unknown): ExtractedFunctionCall[] {
|
|
73
|
+
if (!response || typeof response !== 'object') return [];
|
|
74
|
+
const output = (response as { output?: unknown }).output;
|
|
75
|
+
if (!Array.isArray(output)) return [];
|
|
76
|
+
return extractFromOutput(output);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function toReplayFunctionCallItems(calls: ExtractedFunctionCall[]): ReplayFunctionCallItem[] {
|
|
80
|
+
return calls.map((call) => ({
|
|
81
|
+
type: 'function_call',
|
|
82
|
+
id: call.itemId || call.id,
|
|
83
|
+
callId: call.id,
|
|
84
|
+
name: call.name,
|
|
85
|
+
arguments: call.argumentsText || '{}'
|
|
86
|
+
}));
|
|
87
|
+
}
|