返回 DeepSeek-Reasonix
submission_sync.go
根目录 / internal / control / submission_sync.go
1 package control
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7
8 "reasonix/internal/agent"
9 "reasonix/internal/jobs"
10 "reasonix/internal/skill"
11 "reasonix/internal/tool"
12 )
13
14 // RunTurn executes one foreground turn synchronously through the same lifecycle
15 // used by interactive frontends: transient memory/background-job
16 // composition, checkpoints, hooks, and plan approval. It is for transports that
17 // need a blocking request/response boundary, such as ACP session/prompt.
18 func (c *Controller) RunTurn(ctx context.Context, input string) error {
19 prepared, failures := c.prepareSubmissionImagesContext(ctx, SubmissionRequest{Input: input})
20 if len(failures) > 0 {
21 return ImageReferenceFailures(failures)
22 }
23 ctx = contextWithPreparedImageReferences(ctx, prepared)
24 err := c.runSynchronousTurn(ctx, nil, func(runCtx context.Context) error {
25 return c.runTurn(runCtx, input)
26 })
27 if err != nil {
28 return err
29 }
30 return c.waitForGoalTerminal(ctx)
31 }
32
33 // RunSubagentProfile executes one named runAs=subagent skill synchronously and
34 // returns only its final answer. It is the headless CLI counterpart to explicit
35 // slash invocation: the child keeps an isolated session, while the caller owns
36 // stdout rendering and exit status. readOnly selects the preview-safe runner
37 // used by `reasonix subagent try`.
38 func (c *Controller) RunSubagentProfile(ctx context.Context, name, task string, readOnly bool) (string, error) {
39 ctx = c.withAuthentication(ctx)
40 if err := c.authentication.admissionError(); err != nil {
41 return "", err
42 }
43 if _, ok := ctx.Value(preparedImageReferencesContextKey{}).(preparedImageReferences); !ok {
44 prepared, failures := c.prepareSubmissionImagesContext(ctx, SubmissionRequest{Input: task})
45 if len(failures) > 0 {
46 return "", ImageReferenceFailures(failures)
47 }
48 ctx = contextWithPreparedImageReferences(ctx, prepared)
49 }
50 name = strings.TrimSpace(name)
51 task = strings.TrimSpace(task)
52 if name == "" {
53 return "", fmt.Errorf("subagent name is required")
54 }
55 if task == "" {
56 return "", fmt.Errorf("subagent task is required")
57 }
58 sk, ok := c.skills.bySlashName(name)
59 if !ok {
60 return "", fmt.Errorf("unknown or disabled subagent profile %q", name)
61 }
62 if sk.RunAs != skill.RunSubagent {
63 return "", fmt.Errorf("skill %q is not runAs=subagent", name)
64 }
65 sk = c.skills.prepare(sk)
66 runner := c.skillRunner
67 if readOnly {
68 runner = c.readOnlySkillRunner
69 }
70 if runner == nil {
71 return "", fmt.Errorf("subagent skill runner is unavailable for %q", name)
72 }
73
74 c.maybeSessionStart(ctx)
75 parentSession := c.parentSessionID()
76 ctx = agent.WithParentSession(ctx, parentSession)
77 ctx = jobs.WithSession(ctx, parentSession)
78 ctx = c.withTurnImages(ctx, task)
79 ctx = agent.WithResponseLanguagePreference(ctx, c.responseLanguage)
80 ctx = agent.WithReasoningLanguagePreference(ctx, c.reasoningLanguage)
81 ctx = agent.WithSubagentDepth(ctx, 0)
82 answer, err := runner(ctx, sk, task, skill.SubagentRunOptions{HostInitiated: true})
83 c.authentication.recordFailure(err, c.ModelRef())
84 if err != nil {
85 return "", err
86 }
87 return tool.GuardSubagentHostDecisionText(answer), nil
88 }
89
90 // SubmitHTTPFormat is SubmitHTTP with an optional structured-output format
91 // ("json_object") applied to the turn's completion requests. Empty format
92 // behaves exactly like SubmitHTTP. A format attached to a slash command,
93 // or other non-turn input is discarded; @reference turns preserve it because
94 // the format is bound to every submitted turn rather than a global slot.
95 func (c *Controller) SubmitHTTPFormat(input, format string) {
96 // format 绑定到本次提交的 turn(随请求参数传递),不再写入 Controller
97 // 全局一次性槽——评审 #7234 第 2 点:全局槽存在跨请求串用的逻辑竞态
98 // (后提交的 JSON 请求先写槽,更早的普通请求先启动消费掉)。
99 f := strings.TrimSpace(format)
100 if f != "" && isNonTurnHTTPInput(input) {
101 f = "" // 非 turn 输入(slash 命令/! 前缀)不携带 format
102 }
103 // @ 引用 turn(FileRefLine/SlashPathLineRef 等)同样绑定 format——
104 // runRefTurnWithFormat 族 wrapper 注入 ctx(review fix7234and7168:
105 // format 是每个被接纳 turn 的属性,统一架构)。
106 c.submitHTTPWithFormat(input, "", f)
107 }
108
109 // isNonTurnHTTPInput reports inputs that never reach the agent turn loop, so a
110 // structured-output request attached to them would otherwise leak into the
111 // next real turn (the format slot is consumed only by runGoalLoopWithRawDisplay).
112 func isNonTurnHTTPInput(input string) bool {
113 trimmed := strings.TrimSpace(input)
114 if trimmed == "" {
115 return true
116 }
117 // Memory quick-add / remember shortcuts and goal commands bypass turns.
118 if _, ok := MemoryQuickAddNote(trimmed); ok {
119 return true
120 }
121 if _, ok := RememberCommandNote(trimmed); ok {
122 return true
123 }
124 // "!" shell commands are rejected by submitHTTP before the turn loop
125 // (403 over HTTP); a format attached to them would never be consumed.
126 if strings.HasPrefix(trimmed, "!") {
127 return true
128 }
129 // Slash commands are management verbs (/compact /new /clear /model ...)
130 // or notices, not completion turns.
131 if strings.HasPrefix(trimmed, "/") {
132 return true
133 }
134 return false
135 }
136
137 // isSessionManagementSubmission reports commands that mutate or inspect the
138 // current session without admitting a model turn. They retain the original
139 // submission gate but must not enter attachment preparation: /new and /clear
140 // rotate the owner that attachment preparation is bound to.
141 func isSessionManagementSubmission(input string) bool {
142 trimmed := strings.TrimSpace(input)
143 return trimmed == "/new" || trimmed == "/clear" || trimmed == "/context" ||
144 trimmed == "/compact" || strings.HasPrefix(trimmed, "/compact ")
145 }
146
146 lines GO