Why: Byte truncation split UTF-8, subagent results could return unbounded text, and a write could overwrite a file that changed after it was read. Evidence: cutRunes keeps a two-character Chinese prefix; tool selection honors -name; checklist reminders go through the reminder bus. Impact: Tool output limits count characters. A subagent result over 32000 characters is stored as an artifact and the parent sees the path. fs_write_file rejects an existing file that was not read in this run. Refs: feature/context-engineering Co-authored-by: Cursor <cursoragent@cursor.com>
699 lines
22 KiB
Go
699 lines
22 KiB
Go
package runtime
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
nruntime "agent-native/runtime"
|
||
agentllm "agent-native/runtime/llm"
|
||
"agent-native/runtime/reminders"
|
||
agenttools "agent-native/runtime/tools"
|
||
"agent-system-backend/internal/squadadapter"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
// directRun 是直连路径在一次 runtime.Run 期间的部件与交付物状态。
|
||
// 工具声明仍用真实 schema 发给模型;注册表 schema 保持宽松,避免内核的
|
||
// 参数校验把今天能执行的空参数调用挡掉。
|
||
type directRun struct {
|
||
l *LLMRunner
|
||
rc RunCtx
|
||
eff core.EffectiveSettings
|
||
cardflow bool
|
||
finalParts []core.Part
|
||
explicitCard bool
|
||
deliverables []*core.ArtifactRef
|
||
usageTotal core.Usage
|
||
|
||
thinkingIdx int
|
||
textIdx int
|
||
turnThink strings.Builder
|
||
turnText strings.Builder
|
||
thinkStart time.Time
|
||
thinkEnd time.Time
|
||
textStart time.Time
|
||
textEnd time.Time
|
||
responding bool
|
||
acting bool
|
||
queued []agentllm.ToolCall
|
||
inflight *toolSlot
|
||
turnBody string
|
||
|
||
evidence []toolEvidence
|
||
todos core.TodoList
|
||
plan core.PlanSnapshot
|
||
closedToolTurn bool
|
||
checklistTouched bool
|
||
toolTurnsSince int
|
||
checklistReminders int
|
||
checks []core.CheckPart
|
||
notes *reminders.Bus
|
||
}
|
||
|
||
type toolEvidence struct {
|
||
Name string
|
||
Status string
|
||
Summary string
|
||
VerifyText string // 给验证器的尾部摘要,比界面上的摘要长
|
||
}
|
||
|
||
type toolSlot struct {
|
||
call agentllm.ToolCall
|
||
start time.Time
|
||
summary string
|
||
verifyText string
|
||
status string
|
||
truncated bool
|
||
artifact *core.ArtifactRef
|
||
}
|
||
|
||
type directTool struct {
|
||
name string
|
||
run func(context.Context, json.RawMessage) agenttools.Result
|
||
}
|
||
|
||
func (t directTool) Name() string { return t.name }
|
||
func (t directTool) Description() string { return t.name }
|
||
func (t directTool) Parameters() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
|
||
func (t directTool) Execute(ctx context.Context, args json.RawMessage) (agenttools.Result, error) {
|
||
return t.run(ctx, args), nil
|
||
}
|
||
|
||
func (l *LLMRunner) runDirect(rc RunCtx, stream agentllm.StreamFunc, model string, msgs []agentllm.Message, decls []agentllm.ToolDecl, streamOpts agentllm.StreamOptions, cardflow bool, eff core.EffectiveSettings, maxTurns, maxTools int) error {
|
||
st := &directRun{l: l, rc: rc, eff: eff, cardflow: cardflow}
|
||
st.loadChecklist()
|
||
if msg, ok := lastAddressed(rc.History()); ok && msg.Role == core.RoleSystem {
|
||
var text string
|
||
for _, p := range msg.Parts {
|
||
if p.Type == core.PartText {
|
||
text += p.Text
|
||
}
|
||
}
|
||
st.pinSubagentArtifacts(text)
|
||
}
|
||
defer func() {
|
||
if sink, ok := rc.(interface{ AddUsage(core.Usage) }); ok {
|
||
sink.AddUsage(st.usageTotal)
|
||
}
|
||
}()
|
||
|
||
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))
|
||
}})
|
||
}
|
||
|
||
var history []agentllm.Message
|
||
user := agentllm.Message{Role: agentllm.RoleUser}
|
||
if len(msgs) > 0 {
|
||
user = msgs[len(msgs)-1]
|
||
history = append(history, msgs[:len(msgs)-1]...)
|
||
}
|
||
wrapped := func(ctx context.Context, model string, messages []agentllm.Message, _ []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
|
||
if streamOpts.MaxTokens > 0 {
|
||
opts.MaxTokens = streamOpts.MaxTokens
|
||
}
|
||
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.KindToolCallDelta {
|
||
st.noteToolDelta(ev)
|
||
}
|
||
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
|
||
}
|
||
|
||
window, reserve, buffer := contextWindow(st.eff)
|
||
res, err := nruntime.Run(rc.Context(), nruntime.Config{
|
||
Model: model,
|
||
History: history,
|
||
Tools: reg,
|
||
Stream: wrapped,
|
||
MaxTurns: maxTurns,
|
||
Budget: toolBudget(maxTools),
|
||
RepeatGuard: true,
|
||
ContinueOnLength: true,
|
||
MaxLengthContinuations: -1,
|
||
ContextWindow: window,
|
||
OutputReserve: reserve,
|
||
ContextBuffer: buffer,
|
||
OnEvent: st.onEvent,
|
||
Stop: st.stop,
|
||
Reminders: st.reminders,
|
||
UnknownTool: func(call agentllm.ToolCall) agenttools.Result {
|
||
return st.runTool(call.Name, call.Arguments)
|
||
},
|
||
}, user)
|
||
composed := false
|
||
if err == nil && res.Err == nil && len(st.evidence) > 0 && st.eff.GetBool("runtime.composeFinal", true) {
|
||
composed = st.composeFinal(stream, model, res.Notice, streamOpts)
|
||
}
|
||
if res.Notice != "" && !composed {
|
||
st.finalParts = append(st.finalParts, core.Part{Type: core.PartText, Text: res.Notice})
|
||
}
|
||
if err == nil && res.Err == nil && len(st.finalParts) == 0 {
|
||
st.finalParts = append(st.finalParts, core.Part{Type: core.PartText, Text: "(模型未返回内容)"})
|
||
}
|
||
for _, ref := range st.deliverables {
|
||
st.finalParts = append(st.finalParts, core.Part{Type: core.PartArtifactRef, Artifact: ref})
|
||
}
|
||
_ = rc.SetAssistantParts(st.finalParts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if res.Err != nil {
|
||
if strings.Contains(res.Err.Error(), "tool calls") {
|
||
return fmt.Errorf("budget: 工具调用超过上限 %d(profile.budget.maxToolCalls)", maxTools)
|
||
}
|
||
if strings.Contains(res.Err.Error(), "max turns") {
|
||
return fmt.Errorf("budget: 轮次超过上限 %d(profile.budget.maxTurns)", maxTurns)
|
||
}
|
||
return res.Err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// noteToolDelta 把模型正在编写的工具参数立刻转成会话事件。
|
||
// 执行开始前的这段时间原先没有任何工具事件,界面只剩光标。
|
||
func (st *directRun) noteToolDelta(ev agentllm.Event) {
|
||
if !st.acting {
|
||
_ = st.rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseActing})
|
||
st.acting = true
|
||
}
|
||
_ = st.rc.Emit(core.EvToolCallDelta, core.ToolCallDeltaPayload{
|
||
Index: ev.ToolIndex, CallID: ev.ToolID, Name: ev.ToolName, Delta: ev.ArgumentsDelta,
|
||
})
|
||
}
|
||
|
||
func (st *directRun) onEvent(ev nruntime.Event) {
|
||
switch ev.Type {
|
||
case "turn_start":
|
||
st.responding = false
|
||
st.acting = false
|
||
st.turnThink.Reset()
|
||
st.turnText.Reset()
|
||
st.thinkStart, st.thinkEnd = time.Time{}, time.Time{}
|
||
st.textStart, st.textEnd = time.Time{}, time.Time{}
|
||
st.turnBody = ""
|
||
case "text_delta":
|
||
if !st.responding {
|
||
_ = st.rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseResponding})
|
||
st.responding = true
|
||
}
|
||
now := st.l.now()
|
||
if st.textStart.IsZero() {
|
||
st.textStart = now
|
||
}
|
||
st.textEnd = now
|
||
st.turnText.WriteString(ev.Text)
|
||
_ = st.rc.Emit(core.EvMessageDelta, core.MessageDeltaPayload{Index: st.textIdx, Text: ev.Text})
|
||
case "reasoning_delta":
|
||
now := st.l.now()
|
||
if st.thinkStart.IsZero() {
|
||
st.thinkStart = now
|
||
}
|
||
st.thinkEnd = now
|
||
st.turnThink.WriteString(ev.Text)
|
||
_ = st.rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: st.thinkingIdx, Text: ev.Text})
|
||
case "message_end":
|
||
st.usageTotal.PromptTokens += ev.PromptTokens
|
||
st.usageTotal.CompletionTokens += ev.CompletionTokens
|
||
st.usageTotal.TotalTokens += ev.TotalTokens
|
||
st.queued = append([]agentllm.ToolCall(nil), ev.Message.ToolCalls...)
|
||
hasCalls := len(ev.Message.ToolCalls) > 0
|
||
if st.turnThink.Len() > 0 {
|
||
st.finalParts = append(st.finalParts, core.Part{Type: core.PartThinking, Text: st.turnThink.String(), Process: true, DurationMs: durationMs(st.thinkStart, st.thinkEnd)})
|
||
st.thinkingIdx++
|
||
}
|
||
if hasCalls {
|
||
st.closedToolTurn = true
|
||
}
|
||
if st.turnText.Len() > 0 {
|
||
body, think := splitThinkBlock(st.turnText.String())
|
||
if think != "" {
|
||
inlineDur := durationMs(st.thinkStart, st.thinkEnd)
|
||
if inlineDur == 0 {
|
||
inlineDur = durationMs(st.textStart, st.textEnd)
|
||
}
|
||
st.finalParts = append(st.finalParts, core.Part{Type: core.PartThinking, Text: think, Process: true, DurationMs: inlineDur})
|
||
st.thinkingIdx++
|
||
}
|
||
if body != "" {
|
||
st.finalParts = append(st.finalParts, core.Part{Type: core.PartText, Text: body, Process: hasCalls})
|
||
st.turnBody = body
|
||
st.textIdx++
|
||
}
|
||
}
|
||
case "tool_execution_start":
|
||
if !st.acting {
|
||
_ = st.rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseActing})
|
||
st.acting = true
|
||
}
|
||
if len(st.queued) == 0 {
|
||
return
|
||
}
|
||
call := st.queued[0]
|
||
st.queued = st.queued[1:]
|
||
st.inflight = &toolSlot{call: call, start: st.l.now()}
|
||
_ = st.rc.Emit(core.EvToolCall, core.ToolCallPayload{CallID: call.ID, Name: call.Name, Arguments: call.Arguments})
|
||
case "tool_execution_end":
|
||
slot := st.inflight
|
||
if slot == nil {
|
||
return
|
||
}
|
||
end := st.l.now()
|
||
st.finalParts = append(st.finalParts,
|
||
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.deliverables = append(st.deliverables, slot.artifact)
|
||
}
|
||
st.evidence = append(st.evidence, toolEvidence{Name: slot.call.Name, Status: slot.status, Summary: slot.summary, VerifyText: slot.verifyText})
|
||
_ = st.rc.Emit(core.EvToolResult, core.ToolResultPayload{CallID: slot.call.ID, Status: slot.status, Summary: slot.summary, Truncated: slot.truncated, DurationMs: durationMs(slot.start, end)})
|
||
st.inflight = nil
|
||
}
|
||
}
|
||
|
||
func (st *directRun) stop(last agentllm.Message, info nruntime.StopInfo) nruntime.StopDecision {
|
||
if block, why := deliverableGate(st.turnBody, st.deliverables, st.rc); block {
|
||
st.demoteLastText()
|
||
if st.eff.GetBool("runtime.verify", false) {
|
||
st.addVerifyNote(why)
|
||
}
|
||
if info.Continuations < maxDeliverableGuards {
|
||
return nruntime.StopDecision{Continue: true, Replacement: discardedClaimNote, Reason: systemCheckReason(deliverableGuardNote)}
|
||
}
|
||
return nruntime.StopDecision{Notice: deliverableFallbackNotice}
|
||
}
|
||
if !st.eff.GetBool("runtime.verify", false) {
|
||
return nruntime.StopDecision{}
|
||
}
|
||
if reasons := checkDeliverables(st.rc, st.deliverables); len(reasons) > 0 {
|
||
return st.continueOrNotice(info, strings.Join(reasons, ";"))
|
||
}
|
||
verdict, err := st.l.runVerifier(st.rc, st.turnBody, st.evidence, st.deliverables)
|
||
if err != nil {
|
||
st.addVerifyNote(err.Error())
|
||
return nruntime.StopDecision{Notice: verifierUnavailableNotice}
|
||
}
|
||
if !verdict.Passed {
|
||
reason := strings.TrimSpace(verdict.Reason)
|
||
if reason == "" {
|
||
reason = "未说明原因"
|
||
}
|
||
if next := strings.TrimSpace(verdict.NextAction); next != "" {
|
||
reason += "。下一步:" + next
|
||
}
|
||
return st.continueOrNotice(info, reason)
|
||
}
|
||
st.addCheck(true, strings.TrimSpace(verdict.Reason))
|
||
return nruntime.StopDecision{}
|
||
}
|
||
|
||
// demoteLastText 把最近一段还没标成过程的正文降级。已经是过程的解说不动。
|
||
func (st *directRun) demoteLastText() int {
|
||
for i := len(st.finalParts) - 1; i >= 0; i-- {
|
||
if st.finalParts[i].Type == core.PartText && !st.finalParts[i].Process {
|
||
st.finalParts[i].Process = true
|
||
return i
|
||
}
|
||
}
|
||
return -1
|
||
}
|
||
|
||
func (st *directRun) addVerifyNote(reason string) {
|
||
st.addCheck(false, reason)
|
||
}
|
||
|
||
// addCheck 记下终轮检查并发出 run.check。有尚未降级的回复时,插在它前面,
|
||
// 这样检查行留在过程区,回复留在过程区外面。
|
||
func (st *directRun) addCheck(passed bool, reason string) {
|
||
part := core.Part{
|
||
Type: core.PartCheck,
|
||
Process: true,
|
||
Check: &core.CheckPart{Passed: passed, Reason: reason},
|
||
}
|
||
idx := -1
|
||
for i := len(st.finalParts) - 1; i >= 0; i-- {
|
||
if st.finalParts[i].Type == core.PartText && !st.finalParts[i].Process {
|
||
idx = i
|
||
break
|
||
}
|
||
}
|
||
if idx >= 0 {
|
||
st.finalParts = append(st.finalParts, core.Part{})
|
||
copy(st.finalParts[idx+1:], st.finalParts[idx:])
|
||
st.finalParts[idx] = part
|
||
} else {
|
||
st.finalParts = append(st.finalParts, part)
|
||
}
|
||
st.checks = append(st.checks, core.CheckPart{Passed: passed, Reason: reason})
|
||
_ = st.rc.Emit(core.EvRunCheck, core.RunCheckPayload{Passed: passed, Reason: reason})
|
||
}
|
||
|
||
func (st *directRun) continueOrNotice(info nruntime.StopInfo, reason string) nruntime.StopDecision {
|
||
if info.Continuations < maxVerifyContinuations {
|
||
st.demoteLastText()
|
||
st.addVerifyNote(reason)
|
||
return nruntime.StopDecision{Continue: true, Reason: systemCheckReason(reason)}
|
||
}
|
||
st.addVerifyNote(reason)
|
||
return nruntime.StopDecision{Notice: verifyCapNotice}
|
||
}
|
||
|
||
func (st *directRun) runTool(name, args string) agenttools.Result {
|
||
call := agentllm.ToolCall{Name: name, Arguments: args}
|
||
if st.inflight != nil {
|
||
call = st.inflight.call
|
||
}
|
||
var out string
|
||
var err error
|
||
switch call.Name {
|
||
case toolSquad:
|
||
if !st.eff.GetBool("runtime.squad", true) {
|
||
out = "小队编排已被设置禁用(runtime.squad)"
|
||
break
|
||
}
|
||
var a struct{ Goal string }
|
||
_ = json.Unmarshal([]byte(call.Arguments), &a)
|
||
dataDir := st.eff.GetString("storage.dataDir", "./data")
|
||
summary, serr := squadadapter.Orchestrate(st.rc, dataDir, a.Goal)
|
||
if serr != nil {
|
||
out = "小队编排失败:" + serr.Error()
|
||
} else {
|
||
out = summary
|
||
}
|
||
case toolDelegate:
|
||
var a struct {
|
||
Agent, Name, Task, Hint string
|
||
Background bool
|
||
}
|
||
_ = json.Unmarshal([]byte(call.Arguments), &a)
|
||
if a.Background {
|
||
if sp, ok := st.rc.(interface {
|
||
SpawnBackground(agentID, name, task, hint string) (core.ID, error)
|
||
}); ok {
|
||
id, serr := sp.SpawnBackground(subagentProfileID(a.Agent), orDefault(a.Name, "子代理"), a.Task, a.Hint)
|
||
if serr != nil {
|
||
out = "子代理失败:" + serr.Error()
|
||
} else {
|
||
out = fmt.Sprintf("已在后台启动 %s,完成后会自动通知", id)
|
||
}
|
||
break
|
||
}
|
||
}
|
||
var res core.AgentResultPayload
|
||
var serr error
|
||
if sp, ok := st.rc.(interface {
|
||
SpawnAgent(agentID, name, task, hint string) (core.AgentResultPayload, error)
|
||
}); ok {
|
||
res, serr = sp.SpawnAgent(subagentProfileID(a.Agent), orDefault(a.Name, "子代理"), a.Task, a.Hint)
|
||
} else {
|
||
res, serr = st.rc.SpawnSubagent(orDefault(a.Name, "子代理"), a.Task, a.Hint)
|
||
}
|
||
if serr != nil {
|
||
out = "子代理失败:" + serr.Error()
|
||
} else {
|
||
out = orDefault(res.Summary, "子代理完成")
|
||
st.pinSubagentArtifacts(res.Summary + "\n" + res.Text)
|
||
}
|
||
case toolReview:
|
||
out = st.reviewFindings(call.Arguments)
|
||
case toolPlan:
|
||
var p core.PlanSnapshot
|
||
if uerr := json.Unmarshal([]byte(call.Arguments), &p); uerr == nil {
|
||
_ = st.rc.UpdatePlan(p)
|
||
st.plan = p
|
||
st.checklistTouched = true
|
||
out = "计划已更新"
|
||
} else {
|
||
out = "计划参数错误: " + uerr.Error()
|
||
}
|
||
case toolTodos:
|
||
var t core.TodoList
|
||
if uerr := json.Unmarshal([]byte(call.Arguments), &t); uerr == nil {
|
||
_ = st.rc.UpdateTodos(t)
|
||
st.todos = t
|
||
st.checklistTouched = true
|
||
out = "待办已更新"
|
||
} else {
|
||
out = "待办参数错误: " + uerr.Error()
|
||
}
|
||
case toolRunWorkflow:
|
||
out = st.runRegisteredWorkflow(call.Arguments)
|
||
case toolCard:
|
||
out = st.submitCard(call.Arguments)
|
||
case toolCardBegin:
|
||
out = st.beginCard(call.Arguments)
|
||
case toolCardAppend:
|
||
out = st.appendCard(call.Arguments)
|
||
case toolCardCommit:
|
||
out = st.commitCard()
|
||
default:
|
||
res := st.rc.ExecuteTool(core.ToolCallRequest{CallID: call.ID, Name: call.Name, Arguments: json.RawMessage(call.Arguments)})
|
||
if res.Status != "ok" {
|
||
err = fmt.Errorf("%s", res.Output)
|
||
} else {
|
||
out = res.Output
|
||
}
|
||
if res.Artifact != nil && st.inflight != nil {
|
||
st.inflight.artifact = &core.ArtifactRef{ArtifactID: res.Artifact.ID, Name: res.Artifact.Name, Kind: res.Artifact.Kind}
|
||
if st.cardflow && res.Truncated && !st.explicitCard {
|
||
fc, ferr := st.rc.SubmitCard(artifactFacadeCard(call.Name, *res.Artifact, summarize(out, nil)))
|
||
if ferr == nil {
|
||
st.finalParts = append(st.finalParts, core.Part{Type: core.PartCard, Card: &core.CardPart{Kind: string(core.CardKindResult), CardID: fc.ID}})
|
||
}
|
||
}
|
||
}
|
||
if st.inflight != nil {
|
||
st.inflight.truncated = res.Truncated
|
||
}
|
||
}
|
||
|
||
summary := summarize(out, nil)
|
||
status := statusOf(err)
|
||
if err != nil {
|
||
summary = "error: " + err.Error()
|
||
out = summary
|
||
}
|
||
if st.inflight != nil {
|
||
st.inflight.summary = summary
|
||
st.inflight.status = status
|
||
st.inflight.verifyText = tailRunes(out, 600)
|
||
}
|
||
if len(out) > 16000 {
|
||
out = out[:16000] + "…(超长截断)"
|
||
}
|
||
return agenttools.Result{Content: out, IsError: err != nil}
|
||
}
|
||
|
||
func (st *directRun) runRegisteredWorkflow(args string) string {
|
||
var a struct {
|
||
Workflow string `json:"workflow"`
|
||
Inputs map[string]string `json:"inputs"`
|
||
Goal string `json:"goal"`
|
||
Script string `json:"script"`
|
||
}
|
||
if err := json.Unmarshal([]byte(args), &a); err != nil {
|
||
return "run_workflow 参数不是合法 JSON"
|
||
}
|
||
n := 0
|
||
for _, s := range []string{a.Workflow, a.Goal, a.Script} {
|
||
if strings.TrimSpace(s) != "" {
|
||
n++
|
||
}
|
||
}
|
||
if n != 1 {
|
||
return "run_workflow 需要且只能指定 workflow、goal、script 之一"
|
||
}
|
||
if strings.TrimSpace(a.Goal) != "" {
|
||
return st.runPlannedWorkflow(strings.TrimSpace(a.Goal))
|
||
}
|
||
if strings.TrimSpace(a.Script) != "" {
|
||
return st.runScriptWorkflow(a.Script)
|
||
}
|
||
lookup, ok := st.rc.(interface {
|
||
WorkflowByID(id string) (core.WorkflowDef, bool)
|
||
})
|
||
if !ok {
|
||
return "当前运行不能查找工作流"
|
||
}
|
||
def, ok := lookup.WorkflowByID(a.Workflow)
|
||
if !ok {
|
||
return "未注册的工作流 " + a.Workflow
|
||
}
|
||
res, err := (WorkflowRunner{}).run(st.rc, registeredWorkflowLaunch(st.rc, def, a.Inputs))
|
||
st.pinSubagentArtifacts(res.Summary)
|
||
if err != nil {
|
||
if res.Summary != "" {
|
||
return res.Summary
|
||
}
|
||
return err.Error()
|
||
}
|
||
return res.Summary
|
||
}
|
||
|
||
func (st *directRun) runScriptWorkflow(body string) string {
|
||
text, arts, err := runModelScript(st.rc, WorkflowRunner{}, body)
|
||
var ids []string
|
||
for _, art := range arts {
|
||
if art != nil {
|
||
ids = append(ids, string(art.ArtifactID))
|
||
}
|
||
}
|
||
if len(ids) > 0 {
|
||
if text != "" {
|
||
text += "\n"
|
||
}
|
||
text += "工件:" + strings.Join(ids, "、")
|
||
}
|
||
st.pinSubagentArtifacts(text)
|
||
if err != nil {
|
||
if text != "" {
|
||
return text + "\n" + err.Error()
|
||
}
|
||
return err.Error()
|
||
}
|
||
return text
|
||
}
|
||
|
||
func (st *directRun) runPlannedWorkflow(goal string) string {
|
||
res, err := planWorkflow(st.rc, goal, WorkflowRunner{})
|
||
st.pinSubagentArtifacts(res.Summary)
|
||
if err != nil {
|
||
if res.Summary != "" {
|
||
return res.Summary
|
||
}
|
||
return err.Error()
|
||
}
|
||
return res.Summary
|
||
}
|
||
|
||
func (st *directRun) submitCard(args string) string {
|
||
if !st.cardflow {
|
||
return "submit_card 未启用(设置 ui.cardflow 关闭);内容直接写在对话回复里"
|
||
}
|
||
var a struct{ Title, Summary, Body, Scope string }
|
||
if err := json.Unmarshal([]byte(args), &a); err != nil || strings.TrimSpace(a.Title) == "" || strings.TrimSpace(a.Body) == "" {
|
||
return "submit_card 参数错误(title/body 必填): " + errString(err)
|
||
}
|
||
body := []core.UINode{{Type: "markdown", Markdown: a.Body}}
|
||
if strings.TrimSpace(a.Summary) != "" {
|
||
body = append(body, core.UINode{Type: "kv", KV: [][2]string{{"摘要", a.Summary}}})
|
||
}
|
||
newCard := core.ActivityCard{Kind: core.CardKindResult, Title: a.Title, Status: core.CardDone, Tone: "ok", Body: body}
|
||
if a.Scope == "project" {
|
||
newCard.ProjectID = core.ID(st.rc.Run().ProjectID)
|
||
}
|
||
card, cerr := st.rc.SubmitCard(newCard)
|
||
if cerr != nil {
|
||
return "submit_card 失败: " + cerr.Error()
|
||
}
|
||
st.finalParts = append(st.finalParts, core.Part{Type: core.PartCard, Card: &core.CardPart{Kind: string(core.CardKindResult), CardID: card.ID}})
|
||
st.explicitCard = true
|
||
where := "对话"
|
||
if a.Scope == "project" {
|
||
where = "项目"
|
||
}
|
||
return fmt.Sprintf("已作为卡片发出到%s收件箱(%s,card:%s)。对话中保留简短结论即可,不要重复卡片全文。", where, a.Title, card.ID)
|
||
}
|
||
|
||
func (st *directRun) beginCard(args string) string {
|
||
if !st.cardflow {
|
||
return "card_begin 未启用(设置 ui.cardflow 关闭);内容直接写在对话回复里"
|
||
}
|
||
var a struct{ Title, Summary, Scope string }
|
||
if err := json.Unmarshal([]byte(args), &a); err != nil || strings.TrimSpace(a.Title) == "" {
|
||
return "card_begin 参数错误(title 必填): " + errString(err)
|
||
}
|
||
var body []core.UINode
|
||
if strings.TrimSpace(a.Summary) != "" {
|
||
body = append(body, core.UINode{Type: "kv", KV: [][2]string{{"摘要", a.Summary}}})
|
||
}
|
||
newCard := core.ActivityCard{Kind: core.CardKindResult, Title: a.Title, Status: core.CardActive, Tone: "ok", Body: body}
|
||
if a.Scope == "project" {
|
||
newCard.ProjectID = core.ID(st.rc.Run().ProjectID)
|
||
}
|
||
card, cerr := st.rc.BeginStreamCard(newCard)
|
||
if cerr != nil {
|
||
return "card_begin 失败: " + cerr.Error()
|
||
}
|
||
return fmt.Sprintf("流式卡已开始(%s,card:%s)。用 card_append 逐段追加正文,写完调用 card_commit 收尾。", a.Title, card.ID)
|
||
}
|
||
|
||
func (st *directRun) appendCard(args string) string {
|
||
if !st.cardflow {
|
||
return "card_append 未启用(设置 ui.cardflow 关闭);内容直接写在对话回复里"
|
||
}
|
||
var a struct{ Text string }
|
||
if err := json.Unmarshal([]byte(args), &a); err != nil {
|
||
return "card_append 参数错误: " + err.Error()
|
||
}
|
||
if aerr := st.rc.AppendStreamCard(a.Text); aerr != nil {
|
||
return "card_append 失败: " + aerr.Error()
|
||
}
|
||
return "已追加"
|
||
}
|
||
|
||
func (st *directRun) commitCard() string {
|
||
if !st.cardflow {
|
||
return "card_commit 未启用(设置 ui.cardflow 关闭);内容直接写在对话回复里"
|
||
}
|
||
card, cerr := st.rc.CommitStreamCard()
|
||
if cerr != nil {
|
||
return "card_commit 失败: " + cerr.Error()
|
||
}
|
||
st.finalParts = append(st.finalParts, core.Part{Type: core.PartCard, Card: &core.CardPart{Kind: string(core.CardKindResult), CardID: card.ID}})
|
||
st.explicitCard = true
|
||
return fmt.Sprintf("已作为卡片发出(%s,card:%s)。对话中保留简短结论即可,不要重复卡片全文。", card.Title, card.ID)
|
||
}
|
||
|
||
func (st *directRun) pinSubagentArtifacts(text string) {
|
||
seen := map[string]bool{}
|
||
for _, ref := range st.deliverables {
|
||
if ref != nil {
|
||
seen[ref.ArtifactID] = true
|
||
}
|
||
}
|
||
for _, id := range artIDRe.FindAllString(text, -1) {
|
||
if seen[id] {
|
||
continue
|
||
}
|
||
art, _, ok := st.rc.Artifact(id)
|
||
if !ok {
|
||
continue
|
||
}
|
||
seen[id] = true
|
||
st.deliverables = append(st.deliverables, &core.ArtifactRef{ArtifactID: art.ID, Name: art.Name, Kind: art.Kind})
|
||
}
|
||
}
|
||
|
||
func errString(err error) string {
|
||
if err == nil {
|
||
return ""
|
||
}
|
||
return err.Error()
|
||
}
|