@dotsetlabs/dotclaw 2.4.0 → 2.6.0
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/.env.example +9 -10
- package/README.md +8 -4
- package/config-examples/runtime.json +34 -8
- package/config-examples/tool-policy.json +12 -2
- package/container/agent-runner/package-lock.json +2 -2
- package/container/agent-runner/package.json +1 -1
- package/container/agent-runner/src/agent-config.ts +19 -3
- package/container/agent-runner/src/container-protocol.ts +11 -0
- package/container/agent-runner/src/context-overflow-recovery.ts +39 -0
- package/container/agent-runner/src/index.ts +603 -165
- package/container/agent-runner/src/openrouter-input.ts +159 -0
- package/container/agent-runner/src/system-prompt.ts +13 -3
- package/container/agent-runner/src/tool-loop-policy.ts +741 -0
- package/container/agent-runner/src/tools.ts +211 -8
- package/dist/agent-context.d.ts +1 -0
- package/dist/agent-context.d.ts.map +1 -1
- package/dist/agent-context.js +21 -9
- package/dist/agent-context.js.map +1 -1
- package/dist/agent-execution.d.ts +2 -0
- package/dist/agent-execution.d.ts.map +1 -1
- package/dist/agent-execution.js +164 -15
- package/dist/agent-execution.js.map +1 -1
- package/dist/agent-semaphore.d.ts +24 -1
- package/dist/agent-semaphore.d.ts.map +1 -1
- package/dist/agent-semaphore.js +109 -20
- package/dist/agent-semaphore.js.map +1 -1
- package/dist/cli.js +3 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/container-protocol.d.ts +22 -0
- package/dist/container-protocol.d.ts.map +1 -1
- package/dist/container-protocol.js.map +1 -1
- package/dist/container-runner.d.ts +7 -0
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +417 -143
- package/dist/container-runner.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +46 -12
- package/dist/db.js.map +1 -1
- package/dist/error-messages.d.ts.map +1 -1
- package/dist/error-messages.js +18 -4
- package/dist/error-messages.js.map +1 -1
- package/dist/failover-policy.d.ts +41 -0
- package/dist/failover-policy.d.ts.map +1 -0
- package/dist/failover-policy.js +261 -0
- package/dist/failover-policy.js.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ipc-dispatcher.d.ts.map +1 -1
- package/dist/ipc-dispatcher.js +27 -43
- package/dist/ipc-dispatcher.js.map +1 -1
- package/dist/mcp-config.d.ts +22 -0
- package/dist/mcp-config.d.ts.map +1 -0
- package/dist/mcp-config.js +94 -0
- package/dist/mcp-config.js.map +1 -0
- package/dist/memory-backend.d.ts +27 -0
- package/dist/memory-backend.d.ts.map +1 -0
- package/dist/memory-backend.js +112 -0
- package/dist/memory-backend.js.map +1 -0
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +135 -22
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-store.d.ts +1 -0
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +55 -7
- package/dist/memory-store.js.map +1 -1
- package/dist/message-pipeline.d.ts +24 -0
- package/dist/message-pipeline.d.ts.map +1 -1
- package/dist/message-pipeline.js +131 -27
- package/dist/message-pipeline.js.map +1 -1
- package/dist/metrics.d.ts +1 -0
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +9 -0
- package/dist/metrics.js.map +1 -1
- package/dist/providers/discord/discord-provider.d.ts.map +1 -1
- package/dist/providers/discord/discord-provider.js +72 -4
- package/dist/providers/discord/discord-provider.js.map +1 -1
- package/dist/providers/telegram/telegram-provider.d.ts.map +1 -1
- package/dist/providers/telegram/telegram-provider.js +65 -3
- package/dist/providers/telegram/telegram-provider.js.map +1 -1
- package/dist/recall-policy.d.ts +12 -0
- package/dist/recall-policy.d.ts.map +1 -0
- package/dist/recall-policy.js +89 -0
- package/dist/recall-policy.js.map +1 -0
- package/dist/runtime-config.d.ts +33 -0
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +109 -9
- package/dist/runtime-config.js.map +1 -1
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +125 -33
- package/dist/streaming.js.map +1 -1
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +4 -2
- package/dist/task-scheduler.js.map +1 -1
- package/dist/tool-policy.d.ts.map +1 -1
- package/dist/tool-policy.js +26 -4
- package/dist/tool-policy.js.map +1 -1
- package/dist/trace-writer.d.ts +12 -0
- package/dist/trace-writer.d.ts.map +1 -1
- package/dist/trace-writer.js.map +1 -1
- package/dist/turn-hygiene.d.ts +14 -0
- package/dist/turn-hygiene.d.ts.map +1 -0
- package/dist/turn-hygiene.js +214 -0
- package/dist/turn-hygiene.js.map +1 -0
- package/dist/webhook.d.ts.map +1 -1
- package/dist/webhook.js +1 -0
- package/dist/webhook.js.map +1 -1
- package/package.json +15 -1
- package/scripts/benchmark-baseline.js +365 -0
- package/scripts/benchmark-harness.js +1413 -0
- package/scripts/benchmark-scenarios.js +301 -0
- package/scripts/canary-suite.js +123 -0
- package/scripts/generate-controlled-traces.js +230 -0
- package/scripts/release-slo-check.js +214 -0
- package/scripts/run-live-canary.js +339 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
export type ToolCallLike = {
|
|
2
|
+
id?: string;
|
|
3
|
+
name: string;
|
|
4
|
+
arguments?: unknown;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type ToolResultLike = {
|
|
8
|
+
name: string;
|
|
9
|
+
ok: boolean;
|
|
10
|
+
output?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ToolConversationCompactionOptions = {
|
|
15
|
+
maxOutputChars: number;
|
|
16
|
+
outputHeadChars: number;
|
|
17
|
+
outputTailChars: number;
|
|
18
|
+
maxArgumentChars: number;
|
|
19
|
+
maxArgumentArrayItems: number;
|
|
20
|
+
maxArgumentObjectKeys: number;
|
|
21
|
+
argumentRedactKeys: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ToolCallClass = 'idempotent' | 'mutating' | 'unknown';
|
|
25
|
+
|
|
26
|
+
export type ToolExecutionRequirement = {
|
|
27
|
+
required: boolean;
|
|
28
|
+
reason?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type CreateReadFileInstruction = {
|
|
32
|
+
path: string;
|
|
33
|
+
lines: string[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ListReadNewestInstruction = {
|
|
37
|
+
directory: string;
|
|
38
|
+
count: number;
|
|
39
|
+
bulletCount?: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const IDEMPOTENT_TOOL_NAMES = new Set([
|
|
43
|
+
'read',
|
|
44
|
+
'glob',
|
|
45
|
+
'grep',
|
|
46
|
+
'webfetch',
|
|
47
|
+
'websearch',
|
|
48
|
+
'analyzeimage',
|
|
49
|
+
'listtasks',
|
|
50
|
+
'listgroups',
|
|
51
|
+
'getconfig',
|
|
52
|
+
'mcp__dotclaw__memory_search',
|
|
53
|
+
'mcp__dotclaw__memory_list',
|
|
54
|
+
'mcp__dotclaw__memory_stats',
|
|
55
|
+
'mcp__dotclaw__list_tasks',
|
|
56
|
+
'mcp__dotclaw__list_groups',
|
|
57
|
+
'mcp__dotclaw__get_config'
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const IDEMPOTENT_PREFIXES = [
|
|
61
|
+
'mcp__dotclaw__memory_search',
|
|
62
|
+
'mcp__dotclaw__memory_list',
|
|
63
|
+
'mcp__dotclaw__memory_stats',
|
|
64
|
+
'mcp__dotclaw__list_',
|
|
65
|
+
'mcp__dotclaw__get_'
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const MUTATING_TOOL_NAMES = new Set([
|
|
69
|
+
'write',
|
|
70
|
+
'edit',
|
|
71
|
+
'bash',
|
|
72
|
+
'python',
|
|
73
|
+
'gitclone',
|
|
74
|
+
'packageinstall',
|
|
75
|
+
'sendmessage',
|
|
76
|
+
'sendfile',
|
|
77
|
+
'sendphoto',
|
|
78
|
+
'sendvoice',
|
|
79
|
+
'sendaudio',
|
|
80
|
+
'sendlocation',
|
|
81
|
+
'sendcontact',
|
|
82
|
+
'sendpoll',
|
|
83
|
+
'sendbuttons',
|
|
84
|
+
'editmessage',
|
|
85
|
+
'deletemessage',
|
|
86
|
+
'downloadurl',
|
|
87
|
+
'scheduletask',
|
|
88
|
+
'runtask',
|
|
89
|
+
'pausetask',
|
|
90
|
+
'resumetask',
|
|
91
|
+
'canceltask',
|
|
92
|
+
'updatetask',
|
|
93
|
+
'registergroup',
|
|
94
|
+
'removegroup',
|
|
95
|
+
'setmodel',
|
|
96
|
+
'mcp__dotclaw__set_model',
|
|
97
|
+
'mcp__dotclaw__set_behavior',
|
|
98
|
+
'mcp__dotclaw__set_mcp_config',
|
|
99
|
+
'mcp__dotclaw__set_tool_policy',
|
|
100
|
+
'mcp__dotclaw__memory_upsert',
|
|
101
|
+
'mcp__dotclaw__memory_forget'
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const MUTATING_PREFIXES = [
|
|
105
|
+
'plugin__',
|
|
106
|
+
'mcp__dotclaw__send_',
|
|
107
|
+
'mcp__dotclaw__set_',
|
|
108
|
+
'mcp__dotclaw__schedule_',
|
|
109
|
+
'mcp__dotclaw__update_',
|
|
110
|
+
'mcp__dotclaw__register_',
|
|
111
|
+
'mcp__dotclaw__remove_',
|
|
112
|
+
'mcp__dotclaw__memory_upsert',
|
|
113
|
+
'mcp__dotclaw__memory_forget'
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const POSITIVE_INT_FIELDS_BY_TOOL: Record<string, string[]> = {
|
|
117
|
+
read: ['maxBytes'],
|
|
118
|
+
glob: ['maxResults'],
|
|
119
|
+
grep: ['maxResults'],
|
|
120
|
+
webfetch: ['maxBytes'],
|
|
121
|
+
websearch: ['count'],
|
|
122
|
+
bash: ['timeoutMs'],
|
|
123
|
+
process: ['timeoutMs'],
|
|
124
|
+
gitclone: ['depth'],
|
|
125
|
+
sendmessage: ['reply_to_message_id'],
|
|
126
|
+
sendfile: ['reply_to_message_id'],
|
|
127
|
+
sendphoto: ['reply_to_message_id'],
|
|
128
|
+
sendvoice: ['duration', 'reply_to_message_id'],
|
|
129
|
+
sendaudio: ['duration', 'reply_to_message_id'],
|
|
130
|
+
sendlocation: ['reply_to_message_id'],
|
|
131
|
+
sendcontact: ['reply_to_message_id'],
|
|
132
|
+
sendpoll: ['reply_to_message_id'],
|
|
133
|
+
editmessage: ['message_id'],
|
|
134
|
+
deletemessage: ['message_id'],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const NONNEGATIVE_INT_FIELDS_BY_TOOL: Record<string, string[]> = {
|
|
138
|
+
websearch: ['offset']
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const PATH_LIKE_FIELDS_BY_TOOL: Record<string, string[]> = {
|
|
142
|
+
read: ['path'],
|
|
143
|
+
write: ['path'],
|
|
144
|
+
edit: ['path'],
|
|
145
|
+
glob: ['pattern'],
|
|
146
|
+
grep: ['path', 'glob'],
|
|
147
|
+
sendfile: ['path'],
|
|
148
|
+
sendphoto: ['path'],
|
|
149
|
+
sendvoice: ['path'],
|
|
150
|
+
sendaudio: ['path'],
|
|
151
|
+
analyzeimage: ['path'],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const TRANSIENT_ERROR_PATTERNS = [
|
|
155
|
+
/\b429\b/i,
|
|
156
|
+
/\b5\d{2}\b/i,
|
|
157
|
+
/rate.?limit/i,
|
|
158
|
+
/timeout|timed out|deadline/i,
|
|
159
|
+
/temporar|transient|unavailable|overloaded|busy/i,
|
|
160
|
+
/econnreset|econnrefused|enotfound|eai_again|socket hang up/i
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const NON_RETRYABLE_ERROR_PATTERNS = [
|
|
164
|
+
/tool is disabled by policy/i,
|
|
165
|
+
/tool not allowed by policy/i,
|
|
166
|
+
/usage limit reached/i,
|
|
167
|
+
/invalid input|validation|zod/i,
|
|
168
|
+
/malformed arguments|unterminated string|unexpected end of json input/i,
|
|
169
|
+
/path is required|content is required|command is required|code is required/i,
|
|
170
|
+
/out of range|received undefined/i,
|
|
171
|
+
/path is outside allowed roots|path does not exist|must be inside/i,
|
|
172
|
+
/permission denied|forbidden|unauthorized/i
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const DEFAULT_ARGUMENT_REDACT_KEYS = [
|
|
176
|
+
'content',
|
|
177
|
+
'text',
|
|
178
|
+
'body',
|
|
179
|
+
'input',
|
|
180
|
+
'code',
|
|
181
|
+
'script',
|
|
182
|
+
'patch',
|
|
183
|
+
'diff',
|
|
184
|
+
'markdown',
|
|
185
|
+
'html',
|
|
186
|
+
'xml',
|
|
187
|
+
'json',
|
|
188
|
+
'yaml'
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
const TOOL_REQUIRED_SCENARIO_PATTERN = /\[(?:scenario:)?tool_heavy\]/i;
|
|
192
|
+
const EXPLICIT_TOOL_INSTRUCTION_PATTERN = /\b(use|call|run)\s+(?:the\s+)?(?:read|write|edit|glob|grep|bash|python|tool|tools)\b/i;
|
|
193
|
+
const FILE_ACTION_VERB_PATTERN = /\b(create|write|edit|update|append|delete|remove|rename|read|open|list|show|find|search|grep|cat|head|tail)\b/i;
|
|
194
|
+
const FILE_OBJECT_PATTERN = /\b(file|files|folder|directory|path|paths|inbox|workspace|repo|repository)\b/i;
|
|
195
|
+
const PATH_HINT_PATTERN = /(?:\b[\w.-]+\/[\w./-]+|\b[\w.-]+\.(?:txt|md|json|yaml|yml|csv|log|js|jsx|ts|tsx|py|sh|toml|xml|html)\b)/i;
|
|
196
|
+
const TOOL_ACTION_PHRASE_PATTERN = /\b(read it back|verify|newest files?|list the \d+ newest files?|exact filename)\b/i;
|
|
197
|
+
const CONVERSATION_RECALL_PATTERN = /\b(from\s+(?:this|our)\s+(?:same\s+)?(?:conversation|chat)|what\s+(?:exact\s+)?(?:file\s*name|filename)\s+did\s+you\s+just\s+create|what\s+did\s+(?:i|you)\s+just)\b/i;
|
|
198
|
+
const CREATE_READ_FILE_PATTERN = /create file\s+["']([^"']+)["']\s+with\s+\d+\s+lines?:\s*([^\n.]+)\./i;
|
|
199
|
+
const LIST_NEWEST_READ_PATTERN = /list\s+(?:the\s+)?(\d+)\s+newest\s+files?\s+(?:under|in)\s+["'`]?([^"'`\s,.;]+\/?)["'`]?(?:,|\s).*?\bread\s+the\s+newest\s+one\b/i;
|
|
200
|
+
const LIST_NEWEST_READ_FALLBACK_PATTERN = /list\s+(?:the\s+)?newest\s+files?\s+(?:under|in)\s+["'`]?([^"'`\s,.;]+\/?)["'`]?(?:,|\s).*?\bread\s+the\s+newest\s+one\b/i;
|
|
201
|
+
const EXACT_BULLET_COUNT_PATTERN = /\bexactly\s+(\d+)\s+bullet(?:\s+point)?s?\b/i;
|
|
202
|
+
|
|
203
|
+
function normalizeToolName(name: string): string {
|
|
204
|
+
return (name || '').trim().toLowerCase();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function detectToolExecutionRequirement(prompt: string): ToolExecutionRequirement {
|
|
208
|
+
const text = String(prompt || '').trim();
|
|
209
|
+
if (!text) return { required: false };
|
|
210
|
+
|
|
211
|
+
if (TOOL_REQUIRED_SCENARIO_PATTERN.test(text)) {
|
|
212
|
+
return { required: true, reason: 'scenario_tool_heavy' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (EXPLICIT_TOOL_INSTRUCTION_PATTERN.test(text)) {
|
|
216
|
+
return { required: true, reason: 'explicit_tool_instruction' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (CONVERSATION_RECALL_PATTERN.test(text)) {
|
|
220
|
+
return { required: false };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const hasFileAction = FILE_ACTION_VERB_PATTERN.test(text);
|
|
224
|
+
const hasFileTarget = FILE_OBJECT_PATTERN.test(text) || PATH_HINT_PATTERN.test(text) || TOOL_ACTION_PHRASE_PATTERN.test(text);
|
|
225
|
+
if (hasFileAction && hasFileTarget) {
|
|
226
|
+
return { required: true, reason: 'workspace_file_action' };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { required: false };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function buildToolExecutionNudgePrompt(params: {
|
|
233
|
+
reason?: string;
|
|
234
|
+
attempt?: number;
|
|
235
|
+
}): string {
|
|
236
|
+
const reason = params.reason || 'required_tool_execution';
|
|
237
|
+
const attempt = Math.max(1, Math.floor(params.attempt || 1));
|
|
238
|
+
const isFileAction = reason === 'workspace_file_action' || reason === 'scenario_tool_heavy';
|
|
239
|
+
return [
|
|
240
|
+
'[SYSTEM CONTINUATION]',
|
|
241
|
+
`The previous response did not execute tools for a tool-required request (${reason}). Attempt ${attempt}.`,
|
|
242
|
+
'You MUST emit at least one function_call in your next response before any user-facing prose.',
|
|
243
|
+
isFileAction
|
|
244
|
+
? 'For file work, use appropriate tools (for example: Write/Edit then Read, or Glob/Read/Bash for listing and verification).'
|
|
245
|
+
: 'Use the appropriate tools to gather/act on required state before finalizing.',
|
|
246
|
+
'Do not claim file/system/web actions unless corresponding tool calls in this turn succeeded.',
|
|
247
|
+
'If a required tool fails, report the failure and the next best action instead of claiming success.',
|
|
248
|
+
'Return only the final user-facing answer after tool execution.'
|
|
249
|
+
].join('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function parseCreateReadFileInstruction(prompt: string): CreateReadFileInstruction | null {
|
|
253
|
+
const text = String(prompt || '').trim();
|
|
254
|
+
if (!text) return null;
|
|
255
|
+
const match = text.match(CREATE_READ_FILE_PATTERN);
|
|
256
|
+
if (!match) return null;
|
|
257
|
+
const filePath = String(match[1] || '').trim();
|
|
258
|
+
if (!filePath) return null;
|
|
259
|
+
const rawLines = String(match[2] || '').replace(/\s+\band\b\s+/gi, ',');
|
|
260
|
+
const lines = rawLines
|
|
261
|
+
.split(',')
|
|
262
|
+
.map(item => item.trim())
|
|
263
|
+
.filter(Boolean);
|
|
264
|
+
if (lines.length === 0) return null;
|
|
265
|
+
return { path: filePath, lines };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function parseListReadNewestInstruction(prompt: string): ListReadNewestInstruction | null {
|
|
269
|
+
const text = String(prompt || '').trim();
|
|
270
|
+
if (!text) return null;
|
|
271
|
+
|
|
272
|
+
let directory = '';
|
|
273
|
+
let count = 5;
|
|
274
|
+
|
|
275
|
+
const explicitMatch = text.match(LIST_NEWEST_READ_PATTERN);
|
|
276
|
+
if (explicitMatch) {
|
|
277
|
+
const parsedCount = Number(explicitMatch[1]);
|
|
278
|
+
if (Number.isFinite(parsedCount) && parsedCount > 0) {
|
|
279
|
+
count = Math.min(50, Math.max(1, Math.floor(parsedCount)));
|
|
280
|
+
}
|
|
281
|
+
directory = String(explicitMatch[2] || '').trim();
|
|
282
|
+
} else {
|
|
283
|
+
const fallbackMatch = text.match(LIST_NEWEST_READ_FALLBACK_PATTERN);
|
|
284
|
+
if (!fallbackMatch) return null;
|
|
285
|
+
directory = String(fallbackMatch[1] || '').trim();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
directory = directory.replace(/[.,;:]+$/, '');
|
|
289
|
+
if (!directory) return null;
|
|
290
|
+
|
|
291
|
+
const bulletMatch = text.match(EXACT_BULLET_COUNT_PATTERN);
|
|
292
|
+
const bulletCount = bulletMatch
|
|
293
|
+
? Math.min(6, Math.max(1, Math.floor(Number(bulletMatch[1]) || 0)))
|
|
294
|
+
: undefined;
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
directory,
|
|
298
|
+
count,
|
|
299
|
+
bulletCount: bulletCount && Number.isFinite(bulletCount) ? bulletCount : undefined
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function coerceIntegerField(value: unknown, mode: 'positive' | 'nonnegative'): number | null {
|
|
304
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
305
|
+
const rounded = Math.floor(value);
|
|
306
|
+
if (mode === 'positive') return rounded > 0 ? rounded : null;
|
|
307
|
+
return rounded >= 0 ? rounded : null;
|
|
308
|
+
}
|
|
309
|
+
if (typeof value === 'string') {
|
|
310
|
+
const trimmed = value.trim();
|
|
311
|
+
if (!trimmed) return null;
|
|
312
|
+
const parsed = Number(trimmed);
|
|
313
|
+
if (!Number.isFinite(parsed)) return null;
|
|
314
|
+
const rounded = Math.floor(parsed);
|
|
315
|
+
if (mode === 'positive') return rounded > 0 ? rounded : null;
|
|
316
|
+
return rounded >= 0 ? rounded : null;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function sanitizeObjectArgumentsForTool(toolName: string, record: Record<string, unknown>): Record<string, unknown> {
|
|
322
|
+
const normalized = normalizeToolName(toolName);
|
|
323
|
+
const out = { ...record };
|
|
324
|
+
const positiveFields = POSITIVE_INT_FIELDS_BY_TOOL[normalized] || [];
|
|
325
|
+
const nonnegativeFields = NONNEGATIVE_INT_FIELDS_BY_TOOL[normalized] || [];
|
|
326
|
+
|
|
327
|
+
for (const field of positiveFields) {
|
|
328
|
+
if (!(field in out)) continue;
|
|
329
|
+
const coerced = coerceIntegerField(out[field], 'positive');
|
|
330
|
+
if (coerced === null) {
|
|
331
|
+
delete out[field];
|
|
332
|
+
} else {
|
|
333
|
+
out[field] = coerced;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const field of nonnegativeFields) {
|
|
337
|
+
if (!(field in out)) continue;
|
|
338
|
+
const coerced = coerceIntegerField(out[field], 'nonnegative');
|
|
339
|
+
if (coerced === null) {
|
|
340
|
+
delete out[field];
|
|
341
|
+
} else {
|
|
342
|
+
out[field] = coerced;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function validateSanitizedArgumentsForTool(toolName: string, record: Record<string, unknown>): string | undefined {
|
|
349
|
+
const normalized = normalizeToolName(toolName);
|
|
350
|
+
const pathLikeFields = PATH_LIKE_FIELDS_BY_TOOL[normalized] || [];
|
|
351
|
+
for (const field of pathLikeFields) {
|
|
352
|
+
if (!(field in record)) continue;
|
|
353
|
+
const value = record[field];
|
|
354
|
+
if (typeof value !== 'string') continue;
|
|
355
|
+
const trimmed = value.trim();
|
|
356
|
+
if (!trimmed) continue;
|
|
357
|
+
const hasControlBreak = /[\r\n]/.test(trimmed) || trimmed.includes('\0');
|
|
358
|
+
if (trimmed.includes('$(') || /[`]/.test(trimmed) || hasControlBreak) {
|
|
359
|
+
return `${field} contains unsupported shell syntax`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function stableJson(value: unknown): string {
|
|
366
|
+
if (value === null || value === undefined) return '';
|
|
367
|
+
if (typeof value !== 'object') return JSON.stringify(value);
|
|
368
|
+
if (Array.isArray(value)) {
|
|
369
|
+
return `[${value.map(item => stableJson(item)).join(',')}]`;
|
|
370
|
+
}
|
|
371
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
372
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
373
|
+
.map(([key, val]) => `${JSON.stringify(key)}:${stableJson(val)}`);
|
|
374
|
+
return `{${entries.join(',')}}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function classifyToolCallClass(name: string): ToolCallClass {
|
|
378
|
+
const normalized = normalizeToolName(name);
|
|
379
|
+
if (!normalized) return 'unknown';
|
|
380
|
+
if (MUTATING_TOOL_NAMES.has(normalized) || MUTATING_PREFIXES.some(prefix => normalized.startsWith(prefix))) {
|
|
381
|
+
return 'mutating';
|
|
382
|
+
}
|
|
383
|
+
if (IDEMPOTENT_TOOL_NAMES.has(normalized) || IDEMPOTENT_PREFIXES.some(prefix => normalized.startsWith(prefix))) {
|
|
384
|
+
return 'idempotent';
|
|
385
|
+
}
|
|
386
|
+
return 'unknown';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function isNonRetryableToolError(error: unknown): boolean {
|
|
390
|
+
const message = error instanceof Error ? error.message : String(error || '');
|
|
391
|
+
if (!message) return false;
|
|
392
|
+
return NON_RETRYABLE_ERROR_PATTERNS.some(pattern => pattern.test(message));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function isRetryableToolError(error: unknown): boolean {
|
|
396
|
+
const message = error instanceof Error ? error.message : String(error || '');
|
|
397
|
+
if (!message) return false;
|
|
398
|
+
if (isNonRetryableToolError(message)) return false;
|
|
399
|
+
return TRANSIENT_ERROR_PATTERNS.some(pattern => pattern.test(message));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function shouldRetryIdempotentToolCall(params: {
|
|
403
|
+
toolName: string;
|
|
404
|
+
error: unknown;
|
|
405
|
+
attempt: number;
|
|
406
|
+
maxAttempts: number;
|
|
407
|
+
}): boolean {
|
|
408
|
+
if (params.attempt >= params.maxAttempts) return false;
|
|
409
|
+
if (classifyToolCallClass(params.toolName) !== 'idempotent') return false;
|
|
410
|
+
return isRetryableToolError(params.error);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function normalizeToolCallSignature(call: ToolCallLike): string {
|
|
414
|
+
const name = normalizeToolName(call.name) || 'unknown';
|
|
415
|
+
const args = stableJson(call.arguments);
|
|
416
|
+
return `${name}:${args}`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function normalizeToolRoundSignature(calls: ToolCallLike[]): string {
|
|
420
|
+
const signatures = calls
|
|
421
|
+
.map(call => normalizeToolCallSignature(call))
|
|
422
|
+
.sort();
|
|
423
|
+
return signatures.join('|');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function looksLikeJsonCandidate(value: string): boolean {
|
|
427
|
+
if (!value) return false;
|
|
428
|
+
const trimmed = value.trim();
|
|
429
|
+
return trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function coerceScalarArguments(toolName: string, value: string): Record<string, unknown> | null {
|
|
433
|
+
const normalized = normalizeToolName(toolName);
|
|
434
|
+
if (!normalized) return null;
|
|
435
|
+
if (!value.trim()) return null;
|
|
436
|
+
switch (normalized) {
|
|
437
|
+
case 'bash':
|
|
438
|
+
case 'process':
|
|
439
|
+
return { command: value };
|
|
440
|
+
case 'python':
|
|
441
|
+
return { code: value };
|
|
442
|
+
case 'webfetch':
|
|
443
|
+
return { url: value.trim() };
|
|
444
|
+
case 'websearch':
|
|
445
|
+
return { query: value };
|
|
446
|
+
case 'read':
|
|
447
|
+
return { path: value };
|
|
448
|
+
case 'glob':
|
|
449
|
+
return { pattern: value };
|
|
450
|
+
case 'grep':
|
|
451
|
+
return { pattern: value };
|
|
452
|
+
default:
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function normalizeToolCallArguments(params: {
|
|
458
|
+
toolName: string;
|
|
459
|
+
rawArguments: unknown;
|
|
460
|
+
}): { arguments: unknown; malformedReason?: string } {
|
|
461
|
+
const raw = params.rawArguments;
|
|
462
|
+
if (raw && typeof raw === 'object') {
|
|
463
|
+
if (Array.isArray(raw)) {
|
|
464
|
+
return { arguments: raw, malformedReason: 'arguments must be an object' };
|
|
465
|
+
}
|
|
466
|
+
const sanitized = sanitizeObjectArgumentsForTool(params.toolName, raw as Record<string, unknown>);
|
|
467
|
+
const validationError = validateSanitizedArgumentsForTool(params.toolName, sanitized);
|
|
468
|
+
return validationError
|
|
469
|
+
? { arguments: sanitized, malformedReason: validationError }
|
|
470
|
+
: { arguments: sanitized };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (typeof raw === 'string') {
|
|
474
|
+
const trimmed = raw.trim();
|
|
475
|
+
if (!trimmed) {
|
|
476
|
+
return { arguments: raw, malformedReason: 'arguments are empty' };
|
|
477
|
+
}
|
|
478
|
+
if (looksLikeJsonCandidate(trimmed)) {
|
|
479
|
+
try {
|
|
480
|
+
let parsed: unknown = trimmed;
|
|
481
|
+
for (let i = 0; i < 2 && typeof parsed === 'string'; i += 1) {
|
|
482
|
+
parsed = JSON.parse(parsed);
|
|
483
|
+
}
|
|
484
|
+
if (parsed && typeof parsed === 'object') {
|
|
485
|
+
if (Array.isArray(parsed)) {
|
|
486
|
+
return { arguments: parsed, malformedReason: 'arguments must be an object' };
|
|
487
|
+
}
|
|
488
|
+
const sanitized = sanitizeObjectArgumentsForTool(params.toolName, parsed as Record<string, unknown>);
|
|
489
|
+
const validationError = validateSanitizedArgumentsForTool(params.toolName, sanitized);
|
|
490
|
+
return validationError
|
|
491
|
+
? { arguments: sanitized, malformedReason: validationError }
|
|
492
|
+
: { arguments: sanitized };
|
|
493
|
+
}
|
|
494
|
+
if (typeof parsed === 'string') {
|
|
495
|
+
const coerced = coerceScalarArguments(params.toolName, parsed);
|
|
496
|
+
if (coerced) return { arguments: coerced };
|
|
497
|
+
}
|
|
498
|
+
} catch {
|
|
499
|
+
return { arguments: raw, malformedReason: 'malformed JSON arguments (possibly truncated)' };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const coerced = coerceScalarArguments(params.toolName, trimmed);
|
|
503
|
+
if (coerced) {
|
|
504
|
+
const sanitized = sanitizeObjectArgumentsForTool(params.toolName, coerced);
|
|
505
|
+
const validationError = validateSanitizedArgumentsForTool(params.toolName, sanitized);
|
|
506
|
+
return validationError
|
|
507
|
+
? { arguments: sanitized, malformedReason: validationError }
|
|
508
|
+
: { arguments: sanitized };
|
|
509
|
+
}
|
|
510
|
+
return { arguments: raw, malformedReason: 'arguments must be an object' };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (typeof raw === 'number' || typeof raw === 'boolean') {
|
|
514
|
+
const coerced = coerceScalarArguments(params.toolName, String(raw));
|
|
515
|
+
if (coerced) {
|
|
516
|
+
const sanitized = sanitizeObjectArgumentsForTool(params.toolName, coerced);
|
|
517
|
+
const validationError = validateSanitizedArgumentsForTool(params.toolName, sanitized);
|
|
518
|
+
return validationError
|
|
519
|
+
? { arguments: sanitized, malformedReason: validationError }
|
|
520
|
+
: { arguments: sanitized };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (raw === null || raw === undefined) {
|
|
525
|
+
return { arguments: raw, malformedReason: 'arguments are missing' };
|
|
526
|
+
}
|
|
527
|
+
return { arguments: raw, malformedReason: 'unsupported argument type' };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function buildMalformedArgumentsRecoveryHint(params: {
|
|
531
|
+
toolName: string;
|
|
532
|
+
malformedReason?: string;
|
|
533
|
+
}): string | null {
|
|
534
|
+
const toolName = String(params.toolName || '').trim().toLowerCase();
|
|
535
|
+
const reason = String(params.malformedReason || '').trim().toLowerCase();
|
|
536
|
+
if (!reason) return null;
|
|
537
|
+
|
|
538
|
+
const looksTruncated = reason.includes('truncated') || reason.includes('malformed json');
|
|
539
|
+
if (!looksTruncated) return null;
|
|
540
|
+
|
|
541
|
+
if (toolName === 'write' || toolName === 'edit') {
|
|
542
|
+
return 'Retry with smaller file chunks (for example <=2000 chars per call) and apply multiple Write/Edit steps.';
|
|
543
|
+
}
|
|
544
|
+
return 'Retry with shorter tool arguments or split the action into multiple tool calls.';
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function compactLine(text: string, maxChars: number): string {
|
|
548
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
549
|
+
if (normalized.length <= maxChars) return normalized;
|
|
550
|
+
return `${normalized.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function compactWithHeadTail(text: string, maxChars: number, headChars: number, tailChars: number, label: string): string {
|
|
554
|
+
if (!text) return text;
|
|
555
|
+
const trimmed = text.trim();
|
|
556
|
+
if (trimmed.length <= maxChars) return trimmed;
|
|
557
|
+
const head = trimmed.slice(0, Math.max(0, headChars));
|
|
558
|
+
const tail = trimmed.slice(Math.max(0, trimmed.length - Math.max(0, tailChars)));
|
|
559
|
+
return `${head}\n...\n${tail}\n[${label}: kept first ${head.length} and last ${tail.length} of ${trimmed.length} chars.]`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function compactArgumentValue(
|
|
563
|
+
value: unknown,
|
|
564
|
+
options: ToolConversationCompactionOptions,
|
|
565
|
+
depth = 0,
|
|
566
|
+
keyHint = ''
|
|
567
|
+
): unknown {
|
|
568
|
+
if (depth > 4) {
|
|
569
|
+
return '[argument depth trimmed]';
|
|
570
|
+
}
|
|
571
|
+
if (typeof value === 'string') {
|
|
572
|
+
const normalizedKey = String(keyHint || '').trim().toLowerCase();
|
|
573
|
+
const redactSet = new Set(options.argumentRedactKeys.map(key => key.toLowerCase()));
|
|
574
|
+
if (redactSet.has(normalizedKey) && value.length > Math.floor(options.maxArgumentChars * 0.6)) {
|
|
575
|
+
return `[${normalizedKey || 'value'} omitted: ${value.length} chars]`;
|
|
576
|
+
}
|
|
577
|
+
if (value.length > options.maxArgumentChars) {
|
|
578
|
+
return compactWithHeadTail(
|
|
579
|
+
value,
|
|
580
|
+
options.maxArgumentChars,
|
|
581
|
+
Math.min(Math.floor(options.maxArgumentChars * 0.6), options.maxArgumentChars),
|
|
582
|
+
Math.min(Math.floor(options.maxArgumentChars * 0.25), options.maxArgumentChars),
|
|
583
|
+
'Argument trimmed'
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return value;
|
|
587
|
+
}
|
|
588
|
+
if (Array.isArray(value)) {
|
|
589
|
+
const limited = value.slice(0, Math.max(1, options.maxArgumentArrayItems));
|
|
590
|
+
const mapped = limited.map(item => compactArgumentValue(item, options, depth + 1));
|
|
591
|
+
if (value.length > limited.length) {
|
|
592
|
+
mapped.push(`[${value.length - limited.length} items omitted]`);
|
|
593
|
+
}
|
|
594
|
+
return mapped;
|
|
595
|
+
}
|
|
596
|
+
if (value && typeof value === 'object') {
|
|
597
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
598
|
+
const limitedEntries = entries.slice(0, Math.max(1, options.maxArgumentObjectKeys));
|
|
599
|
+
const out: Record<string, unknown> = {};
|
|
600
|
+
for (const [key, val] of limitedEntries) {
|
|
601
|
+
out[key] = compactArgumentValue(val, options, depth + 1, key);
|
|
602
|
+
}
|
|
603
|
+
if (entries.length > limitedEntries.length) {
|
|
604
|
+
out.__trimmed_keys = entries.length - limitedEntries.length;
|
|
605
|
+
}
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
608
|
+
return value;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function compactToolConversationItemInternal(item: unknown, options: ToolConversationCompactionOptions): { item: unknown; compacted: boolean } {
|
|
612
|
+
if (!item || typeof item !== 'object') {
|
|
613
|
+
return { item, compacted: false };
|
|
614
|
+
}
|
|
615
|
+
const record = item as Record<string, unknown>;
|
|
616
|
+
if (record.type === 'function_call_output') {
|
|
617
|
+
const output = record.output;
|
|
618
|
+
if (typeof output !== 'string' || output.length <= options.maxOutputChars) {
|
|
619
|
+
return { item, compacted: false };
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
item: {
|
|
623
|
+
...record,
|
|
624
|
+
output: compactWithHeadTail(
|
|
625
|
+
output,
|
|
626
|
+
options.maxOutputChars,
|
|
627
|
+
options.outputHeadChars,
|
|
628
|
+
options.outputTailChars,
|
|
629
|
+
'Tool output trimmed'
|
|
630
|
+
)
|
|
631
|
+
},
|
|
632
|
+
compacted: true
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (record.type === 'function_call') {
|
|
637
|
+
const compactedArgs = compactArgumentValue(record.arguments, options);
|
|
638
|
+
const compacted = JSON.stringify(compactedArgs) !== JSON.stringify(record.arguments);
|
|
639
|
+
if (!compacted) {
|
|
640
|
+
return { item, compacted: false };
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
item: {
|
|
644
|
+
...record,
|
|
645
|
+
arguments: compactedArgs
|
|
646
|
+
},
|
|
647
|
+
compacted: true
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return { item, compacted: false };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function compactToolConversationItems(
|
|
655
|
+
items: unknown[],
|
|
656
|
+
partialOptions: Partial<ToolConversationCompactionOptions> = {}
|
|
657
|
+
): { items: unknown[]; compacted: number } {
|
|
658
|
+
const options: ToolConversationCompactionOptions = {
|
|
659
|
+
maxOutputChars: Math.max(800, Math.floor(partialOptions.maxOutputChars || 3000)),
|
|
660
|
+
outputHeadChars: Math.max(200, Math.floor(partialOptions.outputHeadChars || 1200)),
|
|
661
|
+
outputTailChars: Math.max(120, Math.floor(partialOptions.outputTailChars || 600)),
|
|
662
|
+
maxArgumentChars: Math.max(300, Math.floor(partialOptions.maxArgumentChars || 1000)),
|
|
663
|
+
maxArgumentArrayItems: Math.max(3, Math.floor(partialOptions.maxArgumentArrayItems || 20)),
|
|
664
|
+
maxArgumentObjectKeys: Math.max(4, Math.floor(partialOptions.maxArgumentObjectKeys || 24)),
|
|
665
|
+
argumentRedactKeys: Array.isArray(partialOptions.argumentRedactKeys) && partialOptions.argumentRedactKeys.length > 0
|
|
666
|
+
? partialOptions.argumentRedactKeys.map(item => String(item || '').trim()).filter(Boolean)
|
|
667
|
+
: [...DEFAULT_ARGUMENT_REDACT_KEYS]
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const next: unknown[] = [];
|
|
671
|
+
let compacted = 0;
|
|
672
|
+
for (const item of items || []) {
|
|
673
|
+
const result = compactToolConversationItemInternal(item, options);
|
|
674
|
+
next.push(result.item);
|
|
675
|
+
if (result.compacted) compacted += 1;
|
|
676
|
+
}
|
|
677
|
+
return { items: next, compacted };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function selectToolOutcomeHighlights(toolOutputs: ToolResultLike[], limit = 4): string[] {
|
|
681
|
+
if (!Array.isArray(toolOutputs) || toolOutputs.length === 0) return [];
|
|
682
|
+
const lines: string[] = [];
|
|
683
|
+
for (let i = toolOutputs.length - 1; i >= 0 && lines.length < limit; i -= 1) {
|
|
684
|
+
const item = toolOutputs[i];
|
|
685
|
+
if (!item || !item.name) continue;
|
|
686
|
+
if (item.ok) {
|
|
687
|
+
if (!item.output || !item.output.trim()) continue;
|
|
688
|
+
lines.push(`- ${item.name}: ${compactLine(item.output, 260)}`);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (item.error && item.error.trim()) {
|
|
692
|
+
lines.push(`- ${item.name} (error): ${compactLine(item.error, 200)}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return lines;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function buildForcedSynthesisPrompt(params: {
|
|
699
|
+
reason: string;
|
|
700
|
+
pendingCalls: ToolCallLike[];
|
|
701
|
+
toolOutputs: ToolResultLike[];
|
|
702
|
+
}): string {
|
|
703
|
+
const pendingSummary = params.pendingCalls.length > 0
|
|
704
|
+
? params.pendingCalls.map(call => `- ${call.name}`).slice(0, 8).join('\n')
|
|
705
|
+
: '- none';
|
|
706
|
+
const outcomeLines = selectToolOutcomeHighlights(params.toolOutputs, 6);
|
|
707
|
+
const outcomes = outcomeLines.length > 0 ? outcomeLines.join('\n') : '- no tool outputs captured';
|
|
708
|
+
return [
|
|
709
|
+
'[SYSTEM CONTINUATION]',
|
|
710
|
+
`Tool loop ended because: ${params.reason}.`,
|
|
711
|
+
'Produce a final assistant response now.',
|
|
712
|
+
'Do not call more tools in this response.',
|
|
713
|
+
'If there is missing data, state what is missing and provide the best next action.',
|
|
714
|
+
'',
|
|
715
|
+
'Pending tool calls:',
|
|
716
|
+
pendingSummary,
|
|
717
|
+
'',
|
|
718
|
+
'Recent tool outcomes:',
|
|
719
|
+
outcomes
|
|
720
|
+
].join('\n');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function buildToolOutcomeFallback(params: {
|
|
724
|
+
reason: string;
|
|
725
|
+
toolOutputs: ToolResultLike[];
|
|
726
|
+
pendingCalls: ToolCallLike[];
|
|
727
|
+
}): string {
|
|
728
|
+
const highlights = selectToolOutcomeHighlights(params.toolOutputs, 5);
|
|
729
|
+
const lines = [
|
|
730
|
+
`I completed tool work but could not produce a full final response (${params.reason}).`,
|
|
731
|
+
];
|
|
732
|
+
if (highlights.length > 0) {
|
|
733
|
+
lines.push('Here are the most relevant tool outcomes:');
|
|
734
|
+
lines.push(...highlights);
|
|
735
|
+
}
|
|
736
|
+
if (params.pendingCalls.length > 0) {
|
|
737
|
+
lines.push(`Unresolved tool calls: ${params.pendingCalls.map(call => call.name).slice(0, 8).join(', ')}.`);
|
|
738
|
+
}
|
|
739
|
+
lines.push('Tell me if you want me to continue from this point or change approach.');
|
|
740
|
+
return lines.join('\n');
|
|
741
|
+
}
|