protoagent 0.1.5 → 0.1.6
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/dist/App.js +11 -0
- package/dist/agentic-loop.js +103 -8
- package/dist/sub-agent.js +11 -3
- package/package.json +1 -1
package/dist/App.js
CHANGED
|
@@ -503,6 +503,17 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
break;
|
|
506
|
+
case 'sub_agent_iteration':
|
|
507
|
+
if (event.subAgentTool) {
|
|
508
|
+
const { tool, status } = event.subAgentTool;
|
|
509
|
+
if (status === 'running') {
|
|
510
|
+
setActiveTool(`sub_agent → ${tool}`);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
setActiveTool(null);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
506
517
|
case 'tool_call':
|
|
507
518
|
if (event.toolCall) {
|
|
508
519
|
const toolCall = event.toolCall;
|
package/dist/agentic-loop.js
CHANGED
|
@@ -126,6 +126,21 @@ function extractFirstCompleteJsonValue(value) {
|
|
|
126
126
|
}
|
|
127
127
|
return null;
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Repair invalid JSON escape sequences in a string value.
|
|
131
|
+
*
|
|
132
|
+
* JSON only allows: \" \\ \/ \b \f \n \r \t \uXXXX
|
|
133
|
+
* Models sometimes emit \| \! \- etc. (e.g. grep regex args) which make
|
|
134
|
+
* JSON.parse throw, and Anthropic strict-validates tool_call arguments on
|
|
135
|
+
* every subsequent request, bricking the session permanently.
|
|
136
|
+
*
|
|
137
|
+
* We double the backslash for any \X where X is not a valid JSON escape char.
|
|
138
|
+
*/
|
|
139
|
+
function repairInvalidEscapes(value) {
|
|
140
|
+
// Match a backslash followed by any character that is NOT a valid JSON escape
|
|
141
|
+
// Valid escapes: " \ / b f n r t u
|
|
142
|
+
return value.replace(/\\([^"\\\/bfnrtu])/g, '\\\\$1');
|
|
143
|
+
}
|
|
129
144
|
function normalizeJsonArguments(argumentsText) {
|
|
130
145
|
const trimmed = argumentsText.trim();
|
|
131
146
|
if (!trimmed)
|
|
@@ -157,6 +172,25 @@ function normalizeJsonArguments(argumentsText) {
|
|
|
157
172
|
// Give up and return the original text below.
|
|
158
173
|
}
|
|
159
174
|
}
|
|
175
|
+
// Heuristic: repair invalid escape sequences (e.g. \| from grep regex args)
|
|
176
|
+
const repaired = repairInvalidEscapes(trimmed);
|
|
177
|
+
if (repaired !== trimmed) {
|
|
178
|
+
try {
|
|
179
|
+
JSON.parse(repaired);
|
|
180
|
+
return repaired;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Try repair + first-value extraction together
|
|
184
|
+
const repairedFirst = extractFirstCompleteJsonValue(repaired);
|
|
185
|
+
if (repairedFirst) {
|
|
186
|
+
try {
|
|
187
|
+
JSON.parse(repairedFirst);
|
|
188
|
+
return repairedFirst;
|
|
189
|
+
}
|
|
190
|
+
catch { /* give up */ }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
160
194
|
return argumentsText;
|
|
161
195
|
}
|
|
162
196
|
function sanitizeToolCall(toolCall, validToolNames) {
|
|
@@ -231,6 +265,7 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
231
265
|
}
|
|
232
266
|
let iterationCount = 0;
|
|
233
267
|
let repairRetryCount = 0;
|
|
268
|
+
let contextRetryCount = 0;
|
|
234
269
|
const validToolNames = getValidToolNames();
|
|
235
270
|
while (iterationCount < maxIterations) {
|
|
236
271
|
// Check if abort was requested
|
|
@@ -389,10 +424,24 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
389
424
|
})),
|
|
390
425
|
});
|
|
391
426
|
updatedMessages.push(assistantMessage);
|
|
427
|
+
// Track which tool_call_ids still need a tool result message.
|
|
428
|
+
// This set is used to inject stub responses on abort, preventing
|
|
429
|
+
// orphaned tool_call_ids from permanently bricking the session.
|
|
430
|
+
const pendingToolCallIds = new Set(assistantMessage.tool_calls.map((tc) => tc.id));
|
|
431
|
+
const injectStubsForPendingToolCalls = () => {
|
|
432
|
+
for (const id of pendingToolCallIds) {
|
|
433
|
+
updatedMessages.push({
|
|
434
|
+
role: 'tool',
|
|
435
|
+
tool_call_id: id,
|
|
436
|
+
content: 'Aborted by user.',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
};
|
|
392
440
|
for (const toolCall of assistantMessage.tool_calls) {
|
|
393
441
|
// Check abort between tool calls
|
|
394
442
|
if (abortSignal?.aborted) {
|
|
395
443
|
logger.debug('Agentic loop aborted between tool calls');
|
|
444
|
+
injectStubsForPendingToolCalls();
|
|
396
445
|
emitAbortAndFinish(onEvent);
|
|
397
446
|
return updatedMessages;
|
|
398
447
|
}
|
|
@@ -408,16 +457,11 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
408
457
|
if (name === 'sub_agent') {
|
|
409
458
|
const subProgress = (evt) => {
|
|
410
459
|
onEvent({
|
|
411
|
-
type: '
|
|
412
|
-
|
|
413
|
-
id: toolCall.id,
|
|
414
|
-
name: `sub_agent → ${evt.tool}`,
|
|
415
|
-
args: '',
|
|
416
|
-
status: evt.status === 'running' ? 'running' : 'done',
|
|
417
|
-
},
|
|
460
|
+
type: 'sub_agent_iteration',
|
|
461
|
+
subAgentTool: { tool: evt.tool, status: evt.status, iteration: evt.iteration },
|
|
418
462
|
});
|
|
419
463
|
};
|
|
420
|
-
result = await runSubAgent(client, model, args.task, args.max_iterations, requestDefaults, subProgress);
|
|
464
|
+
result = await runSubAgent(client, model, args.task, args.max_iterations, requestDefaults, subProgress, abortSignal);
|
|
421
465
|
}
|
|
422
466
|
else {
|
|
423
467
|
result = await handleToolCall(name, args, { sessionId, abortSignal });
|
|
@@ -433,6 +477,7 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
433
477
|
tool_call_id: toolCall.id,
|
|
434
478
|
content: result,
|
|
435
479
|
});
|
|
480
|
+
pendingToolCallIds.delete(toolCall.id);
|
|
436
481
|
onEvent({
|
|
437
482
|
type: 'tool_result',
|
|
438
483
|
toolCall: { id: toolCall.id, name, args: argsStr, status: 'done', result },
|
|
@@ -445,6 +490,14 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
445
490
|
tool_call_id: toolCall.id,
|
|
446
491
|
content: `Error: ${errMsg}`,
|
|
447
492
|
});
|
|
493
|
+
pendingToolCallIds.delete(toolCall.id);
|
|
494
|
+
// If the tool was aborted, inject stubs for remaining pending calls and stop
|
|
495
|
+
if (abortSignal?.aborted || (err instanceof Error && (err.name === 'AbortError' || err.message === 'Operation aborted'))) {
|
|
496
|
+
logger.debug('Agentic loop aborted during tool execution');
|
|
497
|
+
injectStubsForPendingToolCalls();
|
|
498
|
+
emitAbortAndFinish(onEvent);
|
|
499
|
+
return updatedMessages;
|
|
500
|
+
}
|
|
448
501
|
onEvent({
|
|
449
502
|
type: 'tool_result',
|
|
450
503
|
toolCall: { id: toolCall.id, name, args: argsStr, status: 'error', result: errMsg },
|
|
@@ -520,6 +573,48 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
520
573
|
continue;
|
|
521
574
|
}
|
|
522
575
|
}
|
|
576
|
+
// Handle context-window-exceeded (prompt too long) — attempt forced compaction
|
|
577
|
+
// This fires when our token estimate was too low (e.g. base64 images from MCP tools)
|
|
578
|
+
// and the request actually hit the hard provider limit.
|
|
579
|
+
const isContextTooLong = apiError?.status === 400 &&
|
|
580
|
+
typeof errMsg === 'string' &&
|
|
581
|
+
/prompt.{0,30}too long|context.{0,30}length|maximum.{0,30}token|tokens?.{0,10}exceed/i.test(errMsg);
|
|
582
|
+
if (isContextTooLong && contextRetryCount < 2) {
|
|
583
|
+
contextRetryCount++;
|
|
584
|
+
logger.warn(`Prompt too long (attempt ${contextRetryCount}); forcing compaction`, { errMsg });
|
|
585
|
+
onEvent({
|
|
586
|
+
type: 'error',
|
|
587
|
+
error: 'Prompt too long. Compacting conversation and retrying...',
|
|
588
|
+
transient: true,
|
|
589
|
+
});
|
|
590
|
+
if (pricing) {
|
|
591
|
+
// Use the normal LLM-based compaction path
|
|
592
|
+
try {
|
|
593
|
+
const compacted = await compactIfNeeded(client, model, updatedMessages, pricing.contextWindow,
|
|
594
|
+
// Pass the context window itself as currentTokens to force compaction
|
|
595
|
+
pricing.contextWindow, requestDefaults, sessionId);
|
|
596
|
+
updatedMessages.length = 0;
|
|
597
|
+
updatedMessages.push(...compacted);
|
|
598
|
+
}
|
|
599
|
+
catch (compactErr) {
|
|
600
|
+
logger.error(`Forced compaction failed: ${compactErr}`);
|
|
601
|
+
// Fall through to truncation fallback below
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Fallback: truncate any tool result messages whose content looks like
|
|
605
|
+
// base64 or is extremely large (e.g. MCP screenshot data)
|
|
606
|
+
const MAX_TOOL_RESULT_CHARS = 20_000;
|
|
607
|
+
for (let i = 0; i < updatedMessages.length; i++) {
|
|
608
|
+
const m = updatedMessages[i];
|
|
609
|
+
if (m.role === 'tool' && typeof m.content === 'string' && m.content.length > MAX_TOOL_RESULT_CHARS) {
|
|
610
|
+
updatedMessages[i] = {
|
|
611
|
+
...m,
|
|
612
|
+
content: m.content.slice(0, MAX_TOOL_RESULT_CHARS) + '\n... (truncated — content was too large)',
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
523
618
|
// Retry on 429 (rate limit) with backoff
|
|
524
619
|
if (apiError?.status === 429) {
|
|
525
620
|
const retryAfter = parseInt(apiError?.headers?.['retry-after'] || '5', 10);
|
package/dist/sub-agent.js
CHANGED
|
@@ -39,7 +39,7 @@ export const subAgentTool = {
|
|
|
39
39
|
* Run a sub-agent with its own isolated conversation.
|
|
40
40
|
* Returns the sub-agent's final text response.
|
|
41
41
|
*/
|
|
42
|
-
export async function runSubAgent(client, model, task, maxIterations = 30, requestDefaults = {}, onProgress) {
|
|
42
|
+
export async function runSubAgent(client, model, task, maxIterations = 30, requestDefaults = {}, onProgress, abortSignal) {
|
|
43
43
|
const op = logger.startOperation('sub-agent');
|
|
44
44
|
const subAgentSessionId = `sub-agent-${crypto.randomUUID()}`;
|
|
45
45
|
const systemPrompt = await generateSystemPrompt();
|
|
@@ -56,13 +56,17 @@ Do NOT ask the user questions — work autonomously with the tools available.`;
|
|
|
56
56
|
];
|
|
57
57
|
try {
|
|
58
58
|
for (let i = 0; i < maxIterations; i++) {
|
|
59
|
+
// Check abort at the top of each iteration
|
|
60
|
+
if (abortSignal?.aborted) {
|
|
61
|
+
return '(sub-agent aborted)';
|
|
62
|
+
}
|
|
59
63
|
const response = await client.chat.completions.create({
|
|
60
64
|
...requestDefaults,
|
|
61
65
|
model,
|
|
62
66
|
messages,
|
|
63
67
|
tools: getAllTools(),
|
|
64
68
|
tool_choice: 'auto',
|
|
65
|
-
});
|
|
69
|
+
}, { signal: abortSignal });
|
|
66
70
|
const message = response.choices[0]?.message;
|
|
67
71
|
if (!message)
|
|
68
72
|
break;
|
|
@@ -70,12 +74,16 @@ Do NOT ask the user questions — work autonomously with the tools available.`;
|
|
|
70
74
|
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
71
75
|
messages.push(message);
|
|
72
76
|
for (const toolCall of message.tool_calls) {
|
|
77
|
+
// Check abort between tool calls
|
|
78
|
+
if (abortSignal?.aborted) {
|
|
79
|
+
return '(sub-agent aborted)';
|
|
80
|
+
}
|
|
73
81
|
const { name, arguments: argsStr } = toolCall.function;
|
|
74
82
|
logger.debug(`Sub-agent tool call: ${name}`);
|
|
75
83
|
onProgress?.({ tool: name, status: 'running', iteration: i });
|
|
76
84
|
try {
|
|
77
85
|
const args = JSON.parse(argsStr);
|
|
78
|
-
const result = await handleToolCall(name, args, { sessionId: subAgentSessionId });
|
|
86
|
+
const result = await handleToolCall(name, args, { sessionId: subAgentSessionId, abortSignal });
|
|
79
87
|
messages.push({
|
|
80
88
|
role: 'tool',
|
|
81
89
|
tool_call_id: toolCall.id,
|