Preserve streamed tool delta identity
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
ci / vet-and-test (pull_request) Successful in 52s

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:
weichen
2026-10-14 02:22:34 +00:00
parent 94afd71f00
commit e86e74bc36
2 changed files with 25 additions and 3 deletions
+20 -1
View File
@@ -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
View File
@@ -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: