parse registers &last.Tools[i] in a pending map so a result arriving on a
later record can be attached to the call it answers.
This is correct today, and was checked rather than assumed: append reallocates
the []Turn backing array, but Turn.Tools is its own slice and the copy
duplicates the header, not the array, so the registered pointer stays live. It
survives 200 forced reallocations between a call and its result.
The safety rests on an invariant nothing states: no code may append to a
Turn.Tools after that turn is stored. recordToTurn happens to build Tools
completely before the turn is appended. Any later change that adds a call to an
existing turn — merging streamed assistant messages would do exactly that —
reallocates Tools and orphans every registered pointer silently. Results would
attach to a stale array with no error anywhere.
Fix
Key by position and resolve at the end, which also removes the one-field
pendingCall wrapper:
pending := map[string][2]int{} // tool_use id -> {turn index, tool index}
...
pending[ids[i]] = [2]int{len(s.Turns) - 1, i}
...
s.Turns[p[0]].Tools[p[1]].Result = res
parseregisters&last.Tools[i]in apendingmap so a result arriving on alater record can be attached to the call it answers.
This is correct today, and was checked rather than assumed:
appendreallocatesthe
[]Turnbacking array, butTurn.Toolsis its own slice and the copyduplicates the header, not the array, so the registered pointer stays live. It
survives 200 forced reallocations between a call and its result.
The safety rests on an invariant nothing states: no code may append to a
Turn.Toolsafter that turn is stored.recordToTurnhappens to buildToolscompletely before the turn is appended. Any later change that adds a call to an
existing turn — merging streamed assistant messages would do exactly that —
reallocates
Toolsand orphans every registered pointer silently. Results wouldattach to a stale array with no error anywhere.
Fix
Key by position and resolve at the end, which also removes the one-field
pendingCallwrapper: