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