(messages)
| 1296 | * envelope baked in via buildAdditionalStep). |
| 1297 | */ |
| 1298 | export function buildAdditionalStepsFromHistory(messages) { |
| 1299 | if (!Array.isArray(messages) || !messages.length) return []; |
| 1300 | const out = []; |
| 1301 | |
| 1302 | // Index tool result content by tool_call_id for fast lookup. |
| 1303 | const toolResultById = new Map(); |
| 1304 | for (const m of messages) { |
| 1305 | if (m?.role !== 'tool' || !m.tool_call_id) continue; |
| 1306 | const content = typeof m.content === 'string' |
| 1307 | ? m.content |
| 1308 | : (Array.isArray(m.content) |
| 1309 | ? m.content.filter(p => typeof p?.text === 'string').map(p => p.text).join('\n') |
| 1310 | : JSON.stringify(m.content ?? '')); |
| 1311 | toolResultById.set(m.tool_call_id, content); |
| 1312 | } |
| 1313 | |
| 1314 | for (const m of messages) { |
| 1315 | if (m?.role !== 'assistant' || !Array.isArray(m.tool_calls)) continue; |
| 1316 | for (const tc of m.tool_calls) { |
| 1317 | const name = tc.function?.name; |
| 1318 | const entry = TOOL_MAP[name]; |
| 1319 | if (!entry) continue; |
| 1320 | const args = safeJsonParse(tc.function?.arguments); |
| 1321 | let cascadeArgs; |
| 1322 | try { cascadeArgs = entry.forward(args); } catch { continue; } |
| 1323 | const observation = toolResultById.get(tc.id); |
| 1324 | // v2.0.70 — apply_patch returns a sentinel from forward() |
| 1325 | // because we can't lossless-encode multi-file patches into a |
| 1326 | // single cascade step. Skip and let partition fallback to |
| 1327 | // emulation toolPreamble. |
| 1328 | if (cascadeArgs?.__apply_patch_unmappable) continue; |
| 1329 | if (typeof observation === 'string') { |
| 1330 | // Overlay the result onto the cascade step's "this is what came |
| 1331 | // back" field. Per-kind because the result field varies: |
| 1332 | // view_file → content (4) |
| 1333 | // run_command → full_output / stdout (proxied via combined_output) |
| 1334 | // grep_search_v2 → raw_output (15) |
| 1335 | // find → raw_output (11) |
| 1336 | // list_directory → children (2, repeated) |
| 1337 | // write_to_file → file_created (4) ← bool, ignore content |
| 1338 | // search_web → summary (5) |
| 1339 | // read_url_content → web_document.text (2.2) |
| 1340 | // propose_code → no native result field (proxy passes back |
| 1341 | // through emulation toolPreamble result text) |
| 1342 | if (entry.kind === 'view_file') cascadeArgs.content = observation; |
| 1343 | else if (entry.kind === 'run_command') { |
| 1344 | cascadeArgs.full_output = observation; |
| 1345 | cascadeArgs.stdout = observation; |
| 1346 | cascadeArgs.exit_code = 0; |
| 1347 | } else if (entry.kind === 'grep_search_v2' || entry.kind === 'grep_search') { |
| 1348 | cascadeArgs.raw_output = observation; |
| 1349 | } else if (entry.kind === 'find') { |
| 1350 | cascadeArgs.raw_output = observation; |
| 1351 | } else if (entry.kind === 'list_directory') { |
| 1352 | cascadeArgs.children = observation |
| 1353 | .split(/\r?\n/) |
| 1354 | .map(s => s.trim()) |
| 1355 | .filter(Boolean); |
no test coverage detected