Files
Agent-System-Backend/internal/runtime/llm_direct.go
T
weichenandCursor 78ce78a7d3 Cap tool results by character and gate stale file writes
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>
2026-10-03 16:21:02 +00:00

699 lines
22 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"
"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()
}