(events)
| 851 | * `message.id`, and tool results as `tool_result` blocks in user messages. |
| 852 | */ |
| 853 | export function classifySufficiency(events) { |
| 854 | // One action list PER THREAD: 'main', plus one per subagent (keyed by the |
| 855 | // delegating tool_use id, which is what `parent_tool_use_id` carries). |
| 856 | const threads = new Map(); |
| 857 | const nameById = new Map(); |
| 858 | const textById = new Map(); // explore tool_use_id -> response text |
| 859 | for (const ev of events) { |
| 860 | const content = ev?.message?.content; |
| 861 | if (!Array.isArray(content)) continue; |
| 862 | const thread = ev.parent_tool_use_id ?? 'main'; |
| 863 | if (ev.type === 'assistant') { |
| 864 | if (!threads.has(thread)) threads.set(thread, []); |
| 865 | const list = threads.get(thread); |
| 866 | for (const b of content) { |
| 867 | if (b.type !== 'tool_use') continue; |
| 868 | nameById.set(b.id, b.name); |
| 869 | // No message.id (never seen on a real log) degrades to "every call is |
| 870 | // its own message", i.e. same-message calls read as reactions. |
| 871 | list.push({ msgId: ev.message.id || `#${thread}-${list.length}`, id: b.id, name: b.name, input: b.input || {} }); |
| 872 | } |
| 873 | } else if (ev.type === 'user') { |
| 874 | for (const b of content) { |
| 875 | if (b.type !== 'tool_result') continue; |
| 876 | const name = nameById.get(b.tool_use_id) || ''; |
| 877 | if (/codegraph_explore/.test(name) && !b.is_error) textById.set(b.tool_use_id, textOf(b.content)); |
| 878 | } |
| 879 | } |
| 880 | } |
| 881 | |
| 882 | const calls = []; |
| 883 | let errors = 0, concurrent = 0; |
| 884 | // What a delegation really did: the subagent's first substantive call. A |
| 885 | // nested delegation is skipped rather than followed, so a subagent that only |
| 886 | // spawns another subagent leaves the call as "moved on". |
| 887 | const throughDelegation = (action, returned, mentioned, earlier) => { |
| 888 | const first = (threads.get(action.id) || []).find((x) => !TRANSPARENT_TOOLS.has(x.name) && !DELEGATION_TOOLS.has(x.name)); |
| 889 | if (!first) return { bucket: 'sufficient', next: action.name }; |
| 890 | const r = reactionOf(first, returned, mentioned, earlier); |
| 891 | return { ...r, next: `${action.name} → ${r.next}` }; |
| 892 | }; |
| 893 | for (const [thread, actions] of threads) { |
| 894 | const earlier = []; // files previous explores in THIS thread already shipped |
| 895 | for (let i = 0; i < actions.length; i++) { |
| 896 | const a = actions[i]; |
| 897 | if (!/codegraph_explore/.test(a.name)) continue; |
| 898 | const text = textById.get(a.id); |
| 899 | // No response text = the call errored, or the run ended before it |
| 900 | // returned. Nothing to judge the sufficiency of; count it and move on. |
| 901 | if (text === undefined) { errors++; continue; } |
| 902 | const returned = exploreReturnedFiles(text); |
| 903 | const mentioned = text.match(PATH_TOKEN_RE) || []; |
| 904 | let reaction = { bucket: 'sufficient', next: '(final answer)' }; |
| 905 | for (let j = i + 1; j < actions.length; j++) { |
| 906 | const b = actions[j]; |
| 907 | if (b.msgId === a.msgId) { if (isFileAccess(b)) concurrent++; continue; } |
| 908 | if (TRANSPARENT_TOOLS.has(b.name)) continue; |
| 909 | reaction = DELEGATION_TOOLS.has(b.name) |
| 910 | ? throughDelegation(b, returned, mentioned, earlier) |
no test coverage detected