Skip to content

Commit 50687c1

Browse files
committed
fix(agent): a tool call after an accepted wrap_up in the same response is not run
wrap_up is where the run's edits are checked and the hand-back is shown; a call the same response made after it (an edit, a command) still ran, after the check and behind the box that said the work was done. Once a wrap_up in a response is accepted, any later call in that response is answered "Not run … do the work first, then call wrap_up last" instead. A wrap_up that verification refused does not count: the call after it is the fix, and runs.
1 parent 5cdbdd6 commit 50687c1

2 files changed

Lines changed: 34 additions & 0 deletions

File tree

‎extensions/levelcode-ai/agent.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,8 +1498,16 @@ async function runAgent(ctx) {
14981498
// next API call 400s ("tool_use ids were found without tool_result blocks").
14991499
const results = [];
15001500
let cancelled = false;
1501+
// Set once a wrap_up in THIS response is accepted: it was checked and shown as the end of the
1502+
// work, so a call the same response makes after it would change things behind the hand-back.
1503+
let handedBack = false;
15011504
for (const tu of toolUses) {
15021505
if (cancelled || ctx.signal.aborted) { cancelled = true; dbg('tool.cancelled', { name: tu.name }); results.push({ type: 'tool_result', tool_use_id: tu.id, content: 'Cancelled by the user.' }); continue; }
1506+
if (handedBack) {
1507+
dbg('tool.afterWrapUp', { name: tu.name });
1508+
results.push({ type: 'tool_result', tool_use_id: tu.id, content: 'Not run: wrap_up already handed the work back in this response, and it was checked as it stood. Do the work first, then call wrap_up last.' });
1509+
continue;
1510+
}
15031511
if (turn.malformed && turn.malformed.has(tu.id)) {
15041512
dbg('tool.malformed', { name: tu.name });
15051513
// A diagram spec is the one input worth a second look: JSON with a trailing comma or a
@@ -1517,7 +1525,9 @@ async function runAgent(ctx) {
15171525
continue;
15181526
}
15191527
if (tu.name === 'wrap_up') {
1528+
const before = wrapped;
15201529
results.push({ type: 'tool_result', tool_use_id: tu.id, content: await wrapUp(tu) });
1530+
if (wrapped && wrapped !== before) { handedBack = true; } // accepted (not refused by verification)
15211531
continue;
15221532
}
15231533
dbg('tool.call', { name: tu.name, input: inputPreview(tu.input, !!(ctx.mcpRoutes && ctx.mcpRoutes.has(tu.name))) });

‎extensions/levelcode-ai/test/agentPlanProgress.test.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ async function test(name, fn) { await fn(); n++; console.log(' ok - ' + name);
5858
let seq = 0;
5959
const call = (name, input) => ({ stop_reason: 'tool_use', content: [{ type: 'tool_use', id: 't' + (++seq), name, input }] });
6060
const say = (text) => ({ stop_reason: 'end_turn', content: [{ type: 'text', text }] });
61+
/** One response with several tool calls, in order. */
62+
const calls = (...cs) => ({ stop_reason: 'tool_use', content: cs.map(([name, input]) => ({ type: 'tool_use', id: 't' + (++seq), name, input })) });
6163

6264
/**
6365
* Run one goal on scripted turns. `extra` is merged into the run's ctx (plan, verify, takeSteers …).
@@ -182,6 +184,28 @@ const ITEMS = { items: [{ content: 'Read the parser', status: 'in_progress' }, {
182184
assert.deepStrictEqual(r.done, ['done']);
183185
});
184186

187+
await test('AFTER WRAP-UP, SAME RESPONSE: a later call is answered, not run — the hand-back was checked as it stood', async () => {
188+
const r = await run([
189+
call('update_plan', ITEMS),
190+
calls(['wrap_up', { recap: 'The parser reads dates.' }], ['update_plan', { items: [{ content: 'Something new', status: 'in_progress' }] }]),
191+
say('Done: dates parse.')
192+
], { plan: { steps: null } });
193+
assert.match(r.results[2], /^Not run: wrap_up already handed the work back in this response/);
194+
assert.deepStrictEqual(r.plans, ['in_progress pending pending', 'completed completed completed'], 'the call after wrap_up changed the list');
195+
assert.deepStrictEqual(r.done, ['done']);
196+
});
197+
198+
await test('…but after a wrap-up that verification REFUSED, the same response\'s next call runs (it is the fix)', async () => {
199+
const ctx = { plan: { steps: null }, editCount: 1, verify: { enabled: true, maxRounds: 2, command: '' },
200+
getTouchedUris: () => ['src/parse.js'], getNewDiagnostics: async () => ({ text: 'src/parse.js:4 error Missing semicolon', count: 1 }) };
201+
const r = await run([
202+
calls(['wrap_up', { recap: 'Dates parse now.' }], ['update_plan', ITEMS]),
203+
say('Done: still fixing.')
204+
], ctx);
205+
assert.match(r.results[0], /^Not shown: your changes did not pass verification/);
206+
assert.match(r.results[1], /^Task list shown/);
207+
});
208+
185209
await test('SEND NOW, AFTER A STEP: the message rides with the tool results, and the chat is told it landed', async () => {
186210
const inbox = [];
187211
const r = await run([

0 commit comments

Comments
 (0)