78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"sync"
|
|
"testing"
|
|
|
|
"agent-native/runtime/llm"
|
|
"agent-native/runtime/tools"
|
|
)
|
|
|
|
// ProcessEventVocabulary 是过程流消费方(小队过程视图,对应 PI proc 帧转发)
|
|
// 依赖的最小事件词汇。U9b Batch1-③ 契约:runtime 必须在真实执行中产出
|
|
// 全部词汇;新增词汇向后兼容,删除/改名是破坏性变更(消费方见
|
|
// squad process_bridge 的 case 集)。
|
|
var ProcessEventVocabulary = []string{
|
|
"text_delta",
|
|
"reasoning_delta",
|
|
"tool_call_delta",
|
|
"tool_execution_start",
|
|
"tool_execution_end",
|
|
"message_end",
|
|
}
|
|
|
|
// vocabScript 构造一段覆盖全部必需词汇的模型流:
|
|
// 推理增量 → 文本增量 → 工具调用增量(触发执行)→ 消息结束。
|
|
func vocabScript() llm.StreamFunc {
|
|
var mu sync.Mutex
|
|
step := 0
|
|
return func(ctx context.Context, model string, messages []llm.Message, td []llm.ToolDecl, opts llm.StreamOptions) <-chan llm.Event {
|
|
ch := make(chan llm.Event, 8)
|
|
go func() {
|
|
defer close(ch)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
switch step {
|
|
case 0:
|
|
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.KindMessageEnd, Message: llm.Message{
|
|
Role: llm.RoleAssistant, StopReason: llm.StopToolCalls,
|
|
ToolCalls: []llm.ToolCall{{ID: "c1", Name: "list_dir", Arguments: `{"path":"."}`}},
|
|
}}
|
|
default:
|
|
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{
|
|
Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd,
|
|
}}
|
|
}
|
|
}()
|
|
return ch
|
|
}
|
|
}
|
|
|
|
func TestLoopEmitsProcessVocabulary(t *testing.T) {
|
|
reg := tools.WorkspaceTools(t.TempDir())
|
|
var observed []string
|
|
_, err := Run(context.Background(), Config{
|
|
Model: "fake",
|
|
Tools: reg,
|
|
Stream: vocabScript(),
|
|
OnEvent: func(ev Event) {
|
|
observed = append(observed, ev.Type)
|
|
},
|
|
MaxTurns: 4,
|
|
}, llm.Message{Role: llm.RoleUser, Content: "run"})
|
|
if err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
for _, want := range ProcessEventVocabulary {
|
|
if !slices.Contains(observed, want) {
|
|
t.Fatalf("required process event %q not observed; got %v", want, observed)
|
|
}
|
|
}
|
|
}
|