Skip to content

Commit 88434b1

Browse files
bugerclaude
andauthored
feat: produce structured progress reports when agent hits iteration/time limits (#548)
When the agent reaches its max iteration or time budget limit, instead of a generic "unable to complete" message, it now produces a structured progress report (Task, Completed Work, Key Findings, Attempted but Inconclusive, Not Started/Remaining, Suggested Next Steps) so that a follow-up agent can continue without starting from scratch. Also enriches _toolCallLog with result briefs and step numbers, and improves tool arg capture for the programmatic fallback report. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ea15f55 commit 88434b1

4 files changed

Lines changed: 90 additions & 30 deletions

File tree

npm/src/agent/ProbeAgent.js

Lines changed: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4084,7 +4084,7 @@ or
40844084
}
40854085
return {
40864086
toolChoice: 'none',
4087-
userMessage: `⚠️ TIME BUDGET EXHAUSTED. Your allocated time for this task has run out. You have ${remaining} step(s) remaining to provide your answer.\n\nIMPORTANT: This is a time budget constraint, NOT a system shutdown or error. The system is working perfectly — you simply used all your allocated time.\n\nDo NOT say things like "the system is shutting down" or "try again later" — the user submitted a request and is waiting for YOUR answer right now.\n\nProvide your BEST answer NOW using the information you have already gathered. Do NOT call any more tools. Summarize your findings and respond completely. If something was not completed, honestly state what was not done and provide any partial results or recommendations you can offer.`
4087+
userMessage: `⚠️ TIME BUDGET EXHAUSTED. Your allocated time for this task has run out. You have ${remaining} step(s) remaining to provide your answer.\n\nIMPORTANT: This is a time budget constraint, NOT a system shutdown or error. The system is working perfectly — you simply used all your allocated time.\n\nDo NOT say things like "the system is shutting down" or "try again later" — the user submitted a request and is waiting for YOUR answer right now.\n\nYou MUST now produce a detailed PROGRESS REPORT so that a follow-up agent can continue your work without starting over. Structure your response as follows:\n\n## Task\nWhat was the original request / goal.\n\n## Completed Work\nWhat you successfully accomplished — include ALL findings, code snippets, file paths, data, and conclusions gathered. Be specific and include actual content, not just descriptions.\n\n## Key Findings\nConcrete facts, answers, or data points you discovered. Include file paths with line numbers, code snippets, configuration values, etc.\n\n## Attempted but Inconclusive\nWhat you tried that did not yield clear results — include the approach and why it was inconclusive, so the next agent does not repeat it.\n\n## Not Started / Remaining\nWhat parts of the task you did not get to, and any recommendations for how to approach them.\n\n## Suggested Next Steps\nSpecific, actionable steps for a follow-up agent to continue this work efficiently.\n\nIMPORTANT: Include ALL useful data you gathered inline — do not just say "I found X", actually include X. The next agent will only see this report, not your tool call history.`
40884088
};
40894089
}
40904090

@@ -4094,22 +4094,48 @@ or
40944094
return { toolChoice: 'none' };
40954095
}
40964096

4097-
// Last-iteration warning — force text-only and tell the AI to summarize
4097+
// Last-iteration warning — force text-only and request a structured progress report
40984098
if (stepNumber === maxIterations - 1) {
4099-
// Build a brief summary of tools used so the model can reference them in its answer
4100-
const searchesTried = _toolCallLog
4101-
.filter(tc => tc.name === 'search')
4102-
.map(tc => `"${tc.args.query || ''}"${tc.args.exact ? ' (exact)' : ''}`)
4103-
.filter((v, i, a) => a.indexOf(v) === i); // unique
4104-
const searchSummary = searchesTried.length > 0
4105-
? `\nSearches attempted: ${searchesTried.join(', ')}`
4106-
: '';
4099+
// Build a detailed activity log so the model can produce an accurate handoff report
4100+
const toolActivity = _toolCallLog
4101+
.filter(tc => tc.name !== '_assistant_text')
4102+
.map(tc => {
4103+
const argStr = tc.name === 'search'
4104+
? `query="${tc.args.query || ''}"${tc.args.exact ? ' exact' : ''} path=${tc.args.path || '.'}`
4105+
: JSON.stringify(tc.args || {}).substring(0, 200);
4106+
const brief = tc.resultBrief ? ` → ${tc.resultBrief.substring(0, 150)}` : '';
4107+
return ` [step ${tc.step}] ${tc.name}(${argStr})${brief}`;
4108+
})
4109+
.join('\n');
4110+
const activityLog = toolActivity ? `\n\nTool activity so far:\n${toolActivity}` : '';
41074111

41084112
// For code-searcher subagents: instruct to output structured JSON even on partial results
41094113
const isCodeSearcher = this.promptType === 'code-searcher';
41104114
const lastIterMessage = isCodeSearcher
4111-
? `⚠️ LAST ITERATION — you are out of tool calls. Output your JSON response NOW with whatever files you have verified so far. Set confidence to "low" if your search was incomplete. Include the "searches" array listing all search queries you made with their paths and outcomes.${searchSummary}`
4112-
: `⚠️ LAST ITERATION — you are out of tool calls. Provide your BEST answer NOW with the information gathered so far. If you could not find what was requested, explain exactly what you searched for and why it did not work, so the caller can try a different approach.${searchSummary}`;
4115+
? `⚠️ LAST ITERATION — you are out of tool calls. Output your JSON response NOW with whatever files you have verified so far. Set confidence to "low" if your search was incomplete. Include the "searches" array listing all search queries you made with their paths and outcomes.${activityLog}`
4116+
: `⚠️ ITERATION LIMIT REACHED — you have no more tool calls. You MUST now produce a detailed PROGRESS REPORT so that a follow-up agent can continue your work without starting over.
4117+
4118+
Structure your response as follows:
4119+
4120+
## Task
4121+
What was the original request / goal.
4122+
4123+
## Completed Work
4124+
What you successfully accomplished — include ALL findings, code snippets, file paths, data, and conclusions gathered. Be specific and include actual content, not just descriptions.
4125+
4126+
## Key Findings
4127+
Concrete facts, answers, or data points you discovered. Include file paths with line numbers, code snippets, configuration values, etc.
4128+
4129+
## Attempted but Inconclusive
4130+
What you tried that did not yield clear results — include the approach and why it was inconclusive, so the next agent does not repeat it.
4131+
4132+
## Not Started / Remaining
4133+
What parts of the task you did not get to, and any recommendations for how to approach them.
4134+
4135+
## Suggested Next Steps
4136+
Specific, actionable steps for a follow-up agent to continue this work efficiently.
4137+
4138+
IMPORTANT: Include ALL useful data you gathered inline — do not just say "I found X", actually include X. The next agent will only see this report, not your tool call history.${activityLog}`;
41134139

41144140
return {
41154141
toolChoice: 'none',
@@ -4208,13 +4234,26 @@ Double-check your response based on the criteria above. If everything looks good
42084234
currentIteration++;
42094235
toolContext.currentIteration = currentIteration;
42104236

4211-
// Track tool calls for failure diagnostics
4237+
// Track tool calls for failure diagnostics and progress reports
42124238
if (toolCalls?.length > 0) {
4213-
for (const tc of toolCalls) {
4214-
_toolCallLog.push({ name: tc.toolName, args: tc.args || {} });
4239+
for (let i = 0; i < toolCalls.length; i++) {
4240+
const tc = toolCalls[i];
4241+
const tr = toolResults?.[i];
4242+
let resultBrief = '';
4243+
if (tr) {
4244+
const raw = typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result);
4245+
resultBrief = raw ? raw.substring(0, 500) : '';
4246+
}
4247+
const tcArgs = tc.args || (typeof tc.input === 'string' ? (() => { try { return JSON.parse(tc.input); } catch { return {}; } })() : tc.input) || {};
4248+
_toolCallLog.push({ name: tc.toolName, args: tcArgs, resultBrief, step: currentIteration });
42154249
}
42164250
}
42174251

4252+
// Track assistant text output per step for progress reports
4253+
if (text && text.trim()) {
4254+
_toolCallLog.push({ name: '_assistant_text', args: {}, resultBrief: text.substring(0, 1000), step: currentIteration });
4255+
}
4256+
42184257
// Record telemetry — include model's reasoning and tool call details
42194258
if (this.tracer) {
42204259
const stepEvent = {
@@ -4633,13 +4672,16 @@ Double-check your response based on the criteria above. If everything looks good
46334672
`Some of your tool calls were cancelled mid-execution because the timeout observer determined the time limit was reached.\n\n` +
46344673
`IMPORTANT: This is a time budget constraint, NOT a system shutdown or error. The system is working perfectly — you simply used all your allocated time. ` +
46354674
`Do NOT say things like "the system is shutting down" or "try again later." The user is waiting for your answer RIGHT NOW.\n\n` +
4636-
`Please provide a DETAILED summary of:\n` +
4637-
`1. What you were asked to do (the original task)\n` +
4638-
`2. What you accomplished — include ALL findings, code snippets, data, and conclusions you gathered\n` +
4639-
`3. What was still in progress or not yet started\n` +
4640-
`4. Any partial results or recommendations you can offer based on what you found so far` +
4675+
`You MUST produce a detailed PROGRESS REPORT so that a follow-up agent can continue your work without starting over. ` +
4676+
`Structure your response with these sections:\n\n` +
4677+
`## Task\nWhat was the original request / goal.\n\n` +
4678+
`## Completed Work\nWhat you successfully accomplished — include ALL findings, code snippets, file paths, data, and conclusions gathered. Be specific and include actual content, not just descriptions.\n\n` +
4679+
`## Key Findings\nConcrete facts, answers, or data points you discovered. Include file paths with line numbers, code snippets, configuration values, etc.\n\n` +
4680+
`## Attempted but Inconclusive\nWhat you tried that did not yield clear results — include the approach and why it was inconclusive, so the next agent does not repeat it.\n\n` +
4681+
`## Not Started / Remaining\nWhat parts of the task you did not get to, and any recommendations for how to approach them.\n\n` +
4682+
`## Suggested Next Steps\nSpecific, actionable steps for a follow-up agent to continue this work efficiently.` +
46414683
`${taskContext}${schemaContext}\n\n` +
4642-
`Be thorough — this is the user's only response. Include all useful information you collected.`;
4684+
`IMPORTANT: Include ALL useful data you gathered inline — do not just say "I found X", actually include X. The next agent will only see this report, not your tool call history.`;
46434685

46444686
const summaryMessages = [
46454687
...currentMessages,
@@ -4791,21 +4833,33 @@ Double-check your response based on the criteria above. If everything looks good
47914833
const searchQueries = [];
47924834
const searchDetails = [];
47934835
const toolCounts = {};
4836+
const toolTimeline = [];
47944837
for (const tc of _toolCallLog) {
4838+
if (tc.name === '_assistant_text') continue;
47954839
toolCounts[tc.name] = (toolCounts[tc.name] || 0) + 1;
47964840
if (tc.name === 'search') {
47974841
const q = tc.args.query || '';
47984842
const p = tc.args.path || '.';
47994843
const exact = tc.args.exact ? ' (exact)' : '';
48004844
searchQueries.push(`"${q}"${exact}`);
4801-
searchDetails.push({ query: q, path: p, had_results: false });
4845+
searchDetails.push({ query: q, path: p, had_results: !!(tc.resultBrief && tc.resultBrief.trim()) });
48024846
}
4847+
const argStr = tc.name === 'search'
4848+
? `query="${tc.args.query || ''}"${tc.args.exact ? ' exact' : ''}`
4849+
: JSON.stringify(tc.args || {}).substring(0, 150);
4850+
const brief = tc.resultBrief ? ` → ${tc.resultBrief.substring(0, 200)}` : ' → (no result)';
4851+
toolTimeline.push(` [step ${tc.step}] ${tc.name}(${argStr})${brief}`);
48034852
}
48044853
const toolBreakdown = Object.entries(toolCounts)
48054854
.map(([name, count]) => `${name}: ${count}x`)
48064855
.join(', ');
48074856
const uniqueSearches = [...new Set(searchQueries)];
48084857

4858+
// Collect any assistant text fragments as partial findings
4859+
const assistantTexts = _toolCallLog
4860+
.filter(tc => tc.name === '_assistant_text' && tc.resultBrief)
4861+
.map(tc => tc.resultBrief);
4862+
48094863
// For code-searcher subagents: produce structured JSON so the parent
48104864
// can still use partial results instead of getting a plain error string.
48114865
if (this.promptType === 'code-searcher') {
@@ -4816,12 +4870,18 @@ Double-check your response based on the criteria above. If everything looks good
48164870
searches: searchDetails
48174871
});
48184872
} else {
4819-
let summary = `I was unable to complete your request after ${currentIteration} tool iterations.\n\n`;
4820-
summary += `Tool calls made: ${toolBreakdown || 'none'}\n`;
4873+
let summary = `## Progress Report (iteration limit reached after ${currentIteration} steps)\n\n`;
4874+
summary += `### Tool Usage Summary\n${toolBreakdown || 'none'}\n\n`;
48214875
if (uniqueSearches.length > 0) {
4822-
summary += `Search queries tried: ${uniqueSearches.join(', ')}\n`;
4876+
summary += `### Search Queries Attempted\n${uniqueSearches.join(', ')}\n\n`;
4877+
}
4878+
if (toolTimeline.length > 0) {
4879+
summary += `### Step-by-Step Activity Log\n${toolTimeline.join('\n')}\n\n`;
4880+
}
4881+
if (assistantTexts.length > 0) {
4882+
summary += `### Partial Findings\n${assistantTexts.join('\n\n')}\n\n`;
48234883
}
4824-
summary += `\nThe search approach may be fundamentally wrong for this query. Consider: using exact=true for literal string matching, using bash/grep for pattern-based file searches, or trying a completely different strategy instead of repeating similar searches.`;
4884+
summary += `### Recommendation for Follow-Up\nThe iteration limit was reached before the task could be completed. A follow-up agent should review the activity log above to avoid repeating the same searches, and consider alternative approaches such as: using exact=true for literal string matching, using bash/grep for pattern-based file searches, or trying a different strategy.`;
48254885
finalResult = summary;
48264886
}
48274887
} catch {

npm/tests/unit/code-searcher-iteration-limit.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('Code-searcher iteration limit handling', () => {
8484
expect(result).toBeDefined();
8585
expect(result.toolChoice).toBe('none');
8686
// Regular agent should get the generic message
87-
expect(result.userMessage).toContain('Provide your BEST answer');
87+
expect(result.userMessage).toContain('PROGRESS REPORT');
8888
// Should NOT mention JSON output format
8989
expect(result.userMessage).not.toContain('JSON response');
9090

npm/tests/unit/graceful-timeout.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ describe('prepareStep wind-down behavior', () => {
120120
expect(result.toolChoice).toBe('none');
121121
expect(result.userMessage).toContain('TIME BUDGET EXHAUSTED');
122122
expect(result.userMessage).toContain('3 step(s) remaining');
123-
expect(result.userMessage).toContain('Do NOT call any more tools');
123+
expect(result.userMessage).toContain('PROGRESS REPORT');
124124
expect(gracefulTimeoutState.bonusStepsUsed).toBe(1);
125125
});
126126

npm/tests/unit/negotiated-timeout.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ describe('prepareStep extension message delivery', () => {
445445
const result = prepareStep({ steps: [], stepNumber: 0 });
446446
// Should enter graceful wind-down, not deliver extension message
447447
expect(result.toolChoice).toBe('none');
448-
expect(result.userMessage).toContain('Do NOT call any more tools');
448+
expect(result.userMessage).toContain('PROGRESS REPORT');
449449
});
450450
});
451451

@@ -483,7 +483,7 @@ describe('Full negotiated timeout lifecycle', () => {
483483
// prepareStep should now show graceful wind-down
484484
const step2 = prepareStep({ steps: [], stepNumber: 1 });
485485
expect(step2.toolChoice).toBe('none');
486-
expect(step2.userMessage).toContain('Do NOT call any more tools');
486+
expect(step2.userMessage).toContain('PROGRESS REPORT');
487487

488488
// stopWhen should stop after bonus steps exhausted
489489
gracefulTimeoutState.bonusStepsUsed = 2;

0 commit comments

Comments
 (0)