Files
Agent-System-Backend/internal/runtime/sub_loop.go
T
weichenandCursor bead03374e
ci / vet-and-test (pull_request) Successful in 39s
Rewind tracked files and fall back before the first token
Why: workspace writes had no checkpoint, and a 5xx before any output stopped the turn instead of trying the next configured model.
Evidence: GOWORK=off go test ./internal/runtime/ ./internal/runmgr/ ./internal/tools/ ./internal/settings/ ./internal/server/ ./internal/app/
Impact: models.chat.fallbacks defaults to empty, so existing runs keep their model. Rewind restores only tracked writes and appends a system note. Usage records the model that served.
Refs: feature/durable-rewind
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 17:31:56 +00:00

249 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package runtime
import (
"context"
"encoding/json"
"fmt"
"math"
"strings"
"time"
nruntime "agent-native/runtime"
agentllm "agent-native/runtime/llm"
agenttools "agent-native/runtime/tools"
"agent-system-backend/internal/prompts"
core "agent-system-core"
)
const subagentFallback = "你是被派生的子代理。独立完成给定子任务,输出完整、结构化的结果;需要分点时分条列出,不要为了简短而省略关键内容。"
const defaultSubagentID = "agent.sub.general"
func fallbackSubagentSpec() prompts.SubagentSpec {
return prompts.SubagentSpec{
Tools: []string{"knowledge_search", "web_search", "tavily_search", "time_now", "list_conversations", "read_conversation", "fs_read_file", "fs_list_dir"},
Model: "lightweight",
}
}
func (l *SubLLMRunner) Name() string { return "llm-subagent" }
func (l *SubLLMRunner) Execute(rc RunCtx) error {
id := defaultSubagentID
if src, ok := rc.(interface{ SubagentID() string }); ok {
if v := strings.TrimSpace(src.SubagentID()); v != "" {
id = v
}
}
spec := fallbackSubagentSpec()
if l.Prompts != nil {
if got, ok := l.Prompts.SubagentSpec(rc.Profile().Owner, id); ok {
spec = got
}
}
maxTurns := spec.MaxTurns
if maxTurns <= 0 {
maxTurns = math.MaxInt32
}
route := ResolveLightweightModel(rc.Profile(), rc.Settings())
if spec.Model == "chat" {
route = ResolveChatModel(rc.Profile(), rc.Settings())
}
stream := l.Stream
if stream == nil {
if route.Model == "" || route.Base == "" {
return fmt.Errorf("llm-subagent: 模型未配置(当前解析:model=%q base=%q)", route.Model, route.Base)
}
client := openLLMClient(route.Base, route.Key)
stream = client.Stream
}
if spec.Model == "chat" {
stream = agentllm.WithFallback(stream, ChatFallbacks(rc.Settings()), nil)
}
decls := subagentDecls(rc, spec)
st := &subRun{rc: rc}
reg := agenttools.NewRegistry("")
for _, d := range decls {
name := d.Name
reg.Register(directTool{name: name, run: func(ctx context.Context, args json.RawMessage) agenttools.Result {
return st.runTool(name, string(args))
}})
}
wrapped := func(ctx context.Context, model string, messages []agentllm.Message, _ []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
in := stream(ctx, model, messages, decls, opts)
out := make(chan agentllm.Event)
go func() {
defer close(out)
for ev := range in {
if ev.Kind == agentllm.KindMessageEnd {
if ev.Message.Role == "" {
ev.Message.Role = agentllm.RoleAssistant
}
if len(ev.Message.ToolCalls) > 0 && ev.Message.StopReason == "" {
ev.Message.StopReason = agentllm.StopToolCalls
}
}
select {
case out <- ev:
case <-ctx.Done():
return
}
}
}()
return out
}
_ = rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: "执行子任务……", Phase: core.PhaseActing})
user := agentllm.Message{Role: agentllm.RoleUser, Content: subagentUserText(rc)}
window, reserve, buffer := contextWindow(rc.Settings())
res, err := nruntime.Run(rc.Context(), nruntime.Config{
Model: route.Model,
SystemPrompt: l.subagentPrompt(rc, id),
Tools: reg,
Stream: wrapped,
MaxTurns: maxTurns,
RepeatGuard: true,
ContinueOnLength: true,
MaxLengthContinuations: -1,
ContextWindow: window,
OutputReserve: reserve,
ContextBuffer: buffer,
OnEvent: st.onEvent,
Trace: l.Trace,
}, user)
if len(st.parts) == 0 {
st.parts = append(st.parts, core.Part{Type: core.PartText, Text: "(子代理未返回内容)"})
}
_ = rc.SetAssistantParts(st.parts)
if err != nil {
return err
}
if res.Err != nil {
return res.Err
}
return nil
}
func (l *SubLLMRunner) subagentPrompt(rc RunCtx, id string) string {
if l.Prompts == nil {
return subagentFallback
}
if body, err := l.Prompts.AgentPrompt(rc.Profile().Owner, id); err == nil && strings.TrimSpace(body) != "" {
return body
}
return subagentFallback
}
func subagentUserText(rc RunCtx) string {
text := rc.Subtask()
if src, ok := rc.(interface{ Subhint() string }); ok {
if hint := strings.TrimSpace(src.Subhint()); hint != "" {
text += "\n\n执行提示:" + hint
}
}
return text
}
func subagentDecls(rc RunCtx, spec prompts.SubagentSpec) []agentllm.ToolDecl {
allow := map[string]bool{}
for _, name := range spec.Tools {
allow[name] = true
}
var decls []agentllm.ToolDecl
for _, tool := range rc.Tools() {
if !allow[tool.Name] || !rc.Profile().Tools.ToolAllowed(tool.Name) {
continue
}
decls = append(decls, agentllm.ToolDecl{Name: tool.Name, Description: tool.Description, Parameters: tool.Parameters})
}
return decls
}
type subSlot struct {
call agentllm.ToolCall
start time.Time
summary string
status string
artifact *core.ArtifactRef
}
type subRun struct {
rc RunCtx
parts []core.Part
queued []agentllm.ToolCall
inflight *subSlot
turnText strings.Builder
}
func (st *subRun) onEvent(ev nruntime.Event) {
switch ev.Type {
case "turn_start":
st.turnText.Reset()
case "text_delta":
st.turnText.WriteString(ev.Text)
_ = st.rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: st.rc.Run().ID, Note: truncateRun(st.turnText.String(), 80)})
case "message_end":
st.queued = append([]agentllm.ToolCall(nil), ev.Message.ToolCalls...)
hasCalls := len(ev.Message.ToolCalls) > 0
if body := strings.TrimSpace(st.turnText.String()); body != "" {
st.parts = append(st.parts, core.Part{Type: core.PartText, Text: body, Process: hasCalls})
}
case "tool_execution_start":
call := agentllm.ToolCall{Name: ev.Tool, Arguments: ev.Detail}
if len(st.queued) > 0 {
call = st.queued[0]
st.queued = st.queued[1:]
}
st.inflight = &subSlot{call: call, start: time.Now()}
_ = st.rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: st.rc.Run().ID, Note: "调用 " + call.Name})
case "tool_execution_end":
slot := st.inflight
if slot == nil {
return
}
end := time.Now()
st.parts = append(st.parts,
core.Part{Type: core.PartToolCall, Process: true, ToolCall: &core.ToolCallPart{ID: slot.call.ID, Name: slot.call.Name, Arguments: slot.call.Arguments, Duration: durationMs(slot.start, end)}},
core.Part{Type: core.PartToolResult, Process: true, ToolResult: &core.ToolResultPart{CallID: slot.call.ID, Status: slot.status, Summary: slot.summary}},
)
if slot.artifact != nil {
st.parts = append(st.parts, core.Part{Type: core.PartArtifactRef, Artifact: slot.artifact})
}
st.inflight = nil
}
}
func (st *subRun) runTool(name, args string) agenttools.Result {
call := agentllm.ToolCall{Name: name, Arguments: args}
if st.inflight != nil && st.inflight.call.Name != "" {
call = st.inflight.call
}
res := st.rc.ExecuteTool(core.ToolCallRequest{CallID: call.ID, Name: call.Name, Arguments: json.RawMessage(call.Arguments)})
summary := res.Output
status := res.Status
if status == "" {
status = "ok"
}
var ref *core.ArtifactRef
if res.Artifact != nil {
ref = &core.ArtifactRef{ArtifactID: res.Artifact.ID, Name: res.Artifact.Name, Kind: res.Artifact.Kind}
}
if st.inflight != nil {
st.inflight.summary = summary
st.inflight.status = status
st.inflight.artifact = ref
}
if status != "ok" {
return agenttools.Result{Content: summary, IsError: true}
}
return agenttools.Result{Content: summary}
}
func subagentProfileID(agent string) string {
switch agent {
case "explore", "research", "plan", "verify", "general", "writer":
return "agent.sub." + agent
default:
return defaultSubagentID
}
}