Preserve streamed tool delta identity
Why: Backend emitted tool deltas from a separate goroutine, racing with serialized run events and occasionally losing a UI delta. Evidence: Backend race detector reproduced concurrent event writes; Agent-Native identity test RED→GREEN; full vet and tests pass. Impact: runtime tool delta events now carry provider slot and call ID for single-owner downstream projection. Refs: runtime/loop.go; runtime/event_vocabulary_test.go
This commit is contained in:
@@ -39,7 +39,7 @@ func vocabScript() llm.StreamFunc {
|
||||
step++
|
||||
ch <- llm.Event{Kind: llm.KindReasoningDelta, ReasoningDelta: "思考"}
|
||||
ch <- llm.Event{Kind: llm.KindTextDelta, Text: "正文"}
|
||||
ch <- llm.Event{Kind: llm.KindToolCallDelta, ToolName: "list_dir", ArgumentsDelta: `{"path":"."}`}
|
||||
ch <- llm.Event{Kind: llm.KindToolCallDelta, ToolIndex: 2, ToolID: "c1", ToolName: "list_dir", ArgumentsDelta: `{"path":"."}`}
|
||||
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{
|
||||
Role: llm.RoleAssistant, StopReason: llm.StopToolCalls,
|
||||
ToolCalls: []llm.ToolCall{{ID: "c1", Name: "list_dir", Arguments: `{"path":"."}`}},
|
||||
@@ -54,6 +54,25 @@ func vocabScript() llm.StreamFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoopPreservesToolCallDeltaIdentity(t *testing.T) {
|
||||
reg := tools.WorkspaceTools(t.TempDir())
|
||||
var got Event
|
||||
_, err := Run(context.Background(), Config{
|
||||
Model: "fake", Tools: reg, Stream: vocabScript(), MaxTurns: 4,
|
||||
OnEvent: func(ev Event) {
|
||||
if ev.Type == "tool_call_delta" {
|
||||
got = ev
|
||||
}
|
||||
},
|
||||
}, llm.Message{Role: llm.RoleUser, Content: "run"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.ToolIndex != 2 || got.CallID != "c1" || got.Tool != "list_dir" || got.Detail != `{"path":"."}` {
|
||||
t.Fatalf("tool delta lost its provider identity: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoopEmitsProcessVocabulary(t *testing.T) {
|
||||
reg := tools.WorkspaceTools(t.TempDir())
|
||||
var observed []string
|
||||
|
||||
+5
-2
@@ -38,8 +38,10 @@ type Event struct {
|
||||
Message llm.Message
|
||||
Tool string
|
||||
Text string
|
||||
// ToolIndex preserves the provider's call slot for tool_call_delta.
|
||||
ToolIndex int
|
||||
// CallID 在 tool_execution_start / tool_execution_end 时携带工具调用的
|
||||
// ID:并行执行下同回合多个调用并发进出,消费方按 ID 配对而非按顺序。
|
||||
// ID;tool_call_delta 也携带同一 ID。消费方按 ID 配对而非按顺序。
|
||||
CallID string
|
||||
// Detail carries tool arguments/results or reasoning deltas for
|
||||
// process-visualization consumers; log writers may ignore it.
|
||||
@@ -535,7 +537,8 @@ func consume(ctx context.Context, cfg Config, messages []llm.Message) (llm.Event
|
||||
case llm.KindReasoningDelta:
|
||||
emit(cfg, Event{Type: "reasoning_delta", Text: ev.ReasoningDelta})
|
||||
case llm.KindToolCallDelta:
|
||||
emit(cfg, Event{Type: "tool_call_delta", Tool: ev.ToolName, Detail: ev.ArgumentsDelta})
|
||||
emit(cfg, Event{Type: "tool_call_delta", ToolIndex: ev.ToolIndex,
|
||||
CallID: ev.ToolID, Tool: ev.ToolName, Detail: ev.ArgumentsDelta})
|
||||
case llm.KindError:
|
||||
return ev, ev.Err
|
||||
case llm.KindMessageEnd:
|
||||
|
||||
Reference in New Issue
Block a user