") || strings.Contains(strings.ToLower(got), "autoresearch") {
+ t.Fatalf("unified research Goal prompt = %q", got)
+ }
+ if c.GoalRuntime().TurnsLimit != 40 {
+ t.Fatalf("research budget = %+v", c.GoalRuntime())
}
}
func TestGoalAutoResearchCanBeForcedOrDisabled(t *testing.T) {
c := New(Options{})
c.SetGoalWithResearchMode("fix the typo and add a test", GoalResearchOn)
- if got := c.Compose("start"); !strings.Contains(got, "AutoResearch protocol") {
- t.Fatalf("forced research goal should include AutoResearch protocol:\n%s", got)
+ if got := c.Compose("start"); strings.Contains(strings.ToLower(got), "autoresearch") || c.GoalRuntime().TurnsLimit != 40 {
+ t.Fatalf("forced research Goal should use hidden 40-turn budget: %q %+v", got, c.GoalRuntime())
}
c.SetGoalWithResearchMode("持续排查这个线上卡顿直到根因明确", GoalResearchOff)
- if got := c.Compose("start"); strings.Contains(got, "AutoResearch protocol") {
- t.Fatalf("simple override should suppress AutoResearch protocol:\n%s", got)
+ if got := c.Compose("start"); strings.Contains(strings.ToLower(got), "autoresearch") || c.GoalRuntime().TurnsLimit == 40 {
+ t.Fatalf("simple override should use non-research budget: %q %+v", got, c.GoalRuntime())
}
}
@@ -876,27 +870,27 @@ func TestGoalCommandPreservesResearchModeFlags(t *testing.T) {
if !c.applyGoalCommand("/goal --research fix the typo", "") {
t.Fatal("goal command was not parsed")
}
- if got := c.Compose("start"); !strings.Contains(got, "AutoResearch protocol") {
- t.Fatalf("/goal --research should force AutoResearch through command dispatch:\n%s", got)
+ if got := c.Compose("start"); strings.Contains(strings.ToLower(got), "autoresearch") || c.GoalRuntime().TurnsLimit != 40 {
+ t.Fatalf("/goal --research should select research budget: %q %+v", got, c.GoalRuntime())
}
c = New(Options{})
if !c.applyGoalCommand("/goal --simple 持续排查这个线上卡顿直到根因明确", "") {
t.Fatal("goal command was not parsed")
}
- if got := c.Compose("start"); strings.Contains(got, "AutoResearch protocol") {
- t.Fatalf("/goal --simple should suppress AutoResearch through command dispatch:\n%s", got)
+ if got := c.Compose("start"); strings.Contains(strings.ToLower(got), "autoresearch") || c.GoalRuntime().TurnsLimit == 40 {
+ t.Fatalf("/goal --simple should suppress research budget: %q %+v", got, c.GoalRuntime())
}
}
func TestParseGoalCommandResearchFlags(t *testing.T) {
cmd, ok := ParseGoalCommand("/goal --research fix the typo")
- if !ok || cmd.Action != GoalCommandSet || cmd.Text != "fix the typo" || cmd.ResearchMode != GoalResearchOn {
+ if !ok || cmd.Action != GoalCommandSet || cmd.Text != "fix the typo" || cmd.ResearchMode != GoalResearchOn || !cmd.DeprecatedBudgetFlag {
t.Fatalf("ParseGoalCommand --research = %+v ok=%v", cmd, ok)
}
cmd, ok = ParseGoalCommand("/goal --simple 持续排查直到根因明确")
- if !ok || cmd.Action != GoalCommandSet || cmd.Text != "持续排查直到根因明确" || cmd.ResearchMode != GoalResearchOff {
+ if !ok || cmd.Action != GoalCommandSet || cmd.Text != "持续排查直到根因明确" || cmd.ResearchMode != GoalResearchOff || !cmd.DeprecatedBudgetFlag {
t.Fatalf("ParseGoalCommand --simple = %+v ok=%v", cmd, ok)
}
}
diff --git a/internal/control/planner_gate_test.go b/internal/control/planner_gate_test.go
index cacc4948cd..a58e7854ee 100644
--- a/internal/control/planner_gate_test.go
+++ b/internal/control/planner_gate_test.go
@@ -72,8 +72,8 @@ func TestTaskWarrantsPlanner(t *testing.T) {
{"explain how to migrate from v1 to v2", true},
{goalContinueTurn, false},
{"Goal signaled complete but issues remain:\n- the following tasks are still incomplete:\n - Fix login (in_progress)\nFix or use todo_write/complete_step to mark done, then report complete again via update_goal.", false},
- {activeGoalBlock("execute plan: fix the parser", GoalResearchAuto) + "\n\n" + goalContinueTurn, false},
- {activeGoalBlock("implement the new caching layer", GoalResearchAuto) + "\n\nimplement the new caching layer across the backend", true},
+ {activeGoalBlock("execute plan: fix the parser") + "\n\n" + goalContinueTurn, false},
+ {activeGoalBlock("implement the new caching layer") + "\n\nimplement the new caching layer across the backend", true},
}
for _, c := range cases {
if got := TaskWarrantsPlanner(c.input); got != c.want {
@@ -454,7 +454,7 @@ func TestPlannerPolicyUsesPristineMetadataInsteadOfInjectedContext(t *testing.T)
ctx := withPlannerTurnMetadata(context.Background(), plannerTurnMetadata{
UserText: "fix typo in README",
})
- input := activeGoalBlock("migrate authentication across the backend", GoalResearchAuto) +
+ input := activeGoalBlock("migrate authentication across the backend") +
"\n\n\nhigh risk migration\n\n\nfix typo in README"
got := DecidePlannerRoute(ctx, input)
if got.Route != agent.PlannerRouteExecutorOnly || got.Reason != plannerReasonAtomicEdit {
diff --git a/internal/control/port.go b/internal/control/port.go
index 8e6f8c83e8..dd64e30724 100644
--- a/internal/control/port.go
+++ b/internal/control/port.go
@@ -4,7 +4,6 @@ import (
"context"
"reasonix/internal/agent"
- "reasonix/internal/autoresearch"
"reasonix/internal/billing"
"reasonix/internal/checkpoint"
"reasonix/internal/command"
@@ -101,16 +100,14 @@ type Goals interface {
Goal() string
GoalStatus() string
SetGoal(goal string)
+ // SetGoalWithResearchMode is retained for deprecated CLI budget flags. The
+ // mode is translated at the boundary and is not stored in the Goal runtime.
SetGoalWithResearchMode(goal string, researchMode GoalResearchMode)
ResumeGoal() bool
PauseGoal() bool
GoalRuntime() GoalRuntimeView
GoalStrict(strict bool)
ClearGoal()
- AutoResearchSummary() (*autoresearch.Summary, bool)
- AutoResearchList() ([]autoresearch.Summary, bool)
- AutoResearchFindings(limit int) ([]autoresearch.Finding, bool)
- RecordAutoResearchEvidence(criterionID string, input AutoResearchEvidenceInput) error
ResetPlannerSession()
PlanMode() bool
SetPlanMode(v bool)
diff --git a/internal/control/slash.go b/internal/control/slash.go
index 9e414d665e..bf208d44a7 100644
--- a/internal/control/slash.go
+++ b/internal/control/slash.go
@@ -131,8 +131,6 @@ func goalArgItems(prior []string) []SlashItem {
return nil
}
return []SlashItem{
- {Label: "--research", Insert: "--research ", Hint: "force durable AutoResearch state"},
- {Label: "--simple", Insert: "--simple ", Hint: "force lightweight Goal"},
{Label: "status", Insert: "status", Hint: "show active goal and budget runtime"},
{Label: "pause", Insert: "pause", Hint: "pause the running goal (keeps all state)"},
{Label: "resume", Insert: "resume", Hint: "resume a paused goal (adds one turn slice)"},
diff --git a/internal/control/slash_test.go b/internal/control/slash_test.go
index ec55e28883..859a8f6e92 100644
--- a/internal/control/slash_test.go
+++ b/internal/control/slash_test.go
@@ -145,8 +145,8 @@ func TestSlashArgItems(t *testing.T) {
}
// /goal
items, _ = SlashArgItems("/goal ", data)
- if !has(items, "--research") || !has(items, "--simple") || !has(items, "status") || !has(items, "clear") {
- t.Errorf("/goal should offer research overrides and management commands; got %v", labelsOf(items))
+ if has(items, "--research") || has(items, "--simple") || !has(items, "status") || !has(items, "clear") {
+ t.Errorf("/goal should hide legacy budget flags and offer management commands; got %v", labelsOf(items))
}
if items, _ := SlashArgItems("/goal --research ", data); len(items) != 0 {
t.Errorf("/goal after a research flag should accept free-form objectives; got %v", labelsOf(items))
diff --git a/internal/control/turn_orchestrator.go b/internal/control/turn_orchestrator.go
index 91cb860b6f..cf96b93199 100644
--- a/internal/control/turn_orchestrator.go
+++ b/internal/control/turn_orchestrator.go
@@ -5,11 +5,9 @@ import (
"encoding/json"
"errors"
"fmt"
- "strings"
"time"
"reasonix/internal/agent"
- "reasonix/internal/autoresearch"
"reasonix/internal/event"
"reasonix/internal/evidence"
"reasonix/internal/jobs"
@@ -207,8 +205,6 @@ func (o *turnOrchestrator) runOrchestratedTurn(ctx context.Context, turn orchest
false,
continuation.goal,
GoalStatusRunning,
- continuation.researchMode,
- continuation.autoResearchTaskID,
)
} else {
input = c.compose(turn.input, turn.raw, !turn.synthetic)
@@ -259,14 +255,6 @@ func (o *turnOrchestrator) runOrchestratedTurn(ctx context.Context, turn orchest
defer func() { c.hooks.StopResult(context.Background(), lastAssistantText(c.History()), turn, err) }()
}
c.markInFlightTurn(startMessages, !turn.synthetic && !IsSyntheticUserMessage(turn.raw))
- var autoResearchTaskID string
- if continuation != nil {
- autoResearchTaskID = continuation.autoResearchTaskID
- } else {
- autoResearchTaskID = c.goals.currentAutoResearchTaskID()
- }
- autoResearchAcceptedBefore := c.autoResearch.acceptedEvidenceIDs(autoResearchTaskID)
- c.autoResearch.heartbeat(autoResearchTaskID, autoresearch.HeartbeatStartingTurn, "")
if continuation != nil {
ctx = agent.WithDeliveryExecutionScope(ctx, agent.DeliveryExecutionScope{
ID: continuation.scopeID,
@@ -302,13 +290,8 @@ func (o *turnOrchestrator) runOrchestratedTurn(ctx context.Context, turn orchest
err = c.runner.Run(ctx, modelInput)
c.persistGoalDeliveryCheckpoint()
if err == nil {
- assistantText := lastAssistantText(c.History())
- c.autoResearch.recordEvidenceFromAssistant(autoResearchTaskID, assistantText)
- c.autoResearch.recordTurnProgress(autoResearchTaskID, autoResearchAcceptedBefore, assistantText)
- c.autoResearch.heartbeat(autoResearchTaskID, autoresearch.HeartbeatTurnDone, "")
c.clearInFlightTurn()
} else {
- c.autoResearch.heartbeat(autoResearchTaskID, autoresearch.HeartbeatWarning, err.Error())
// When the user explicitly cancels, keep the real prompt and any fully
// paired tool work. Partial reasoning/output remains durable for display
// but is marked local-only, and a bounded recovery summary is folded into
@@ -515,17 +498,6 @@ func (o *turnOrchestrator) advanceGoalAfterTurn(ctx context.Context, expectedCon
} else if c.executor != nil {
readiness = c.executor.ReadinessResult()
}
- if arReadiness := c.autoResearchReadinessFailure(); arReadiness != "" {
- readiness.Ready = false
- readiness.Missing = append(readiness.Missing, "autoresearch")
- if readiness.Reason != "" {
- readiness.Reason += "\n" + arReadiness
- } else {
- readiness.Reason = arReadiness
- }
- }
- autoResearchTaskID := c.goals.currentAutoResearchTaskID()
-
// The validated update_goal report for this turn, if any.
var report *goalTurnReport
if recorder != nil {
@@ -567,7 +539,6 @@ func (o *turnOrchestrator) advanceGoalAfterTurn(ctx context.Context, expectedCon
})
c.persistGoalState(res.path, res.data, res.ok)
if res.notice != "" {
- c.finalizeAutoResearchTask(autoResearchTaskID, res.notice)
c.notice(res.notice)
}
if res.notice == goalCompleteNotice && c.executor != nil {
@@ -576,32 +547,6 @@ func (o *turnOrchestrator) advanceGoalAfterTurn(ctx context.Context, expectedCon
return res
}
-func (c *Controller) finalizeAutoResearchTask(taskID, notice string) {
- if !c.autoResearch.enabled() || strings.TrimSpace(taskID) == "" {
- return
- }
- switch {
- case notice == goalCompleteNotice:
- status := autoresearch.StatusComplete
- if err := c.autoResearch.updateProgress(taskID, autoresearch.ProgressPatch{Status: &status}); err != nil {
- c.noticeDetail("AutoResearch status update failed.", "autoresearch task completion update failed: "+err.Error())
- return
- }
- c.notice("autoresearch task completed: " + taskID)
- case strings.HasPrefix(notice, "goal blocked: ") || notice == "goal continuation limit reached":
- status := autoresearch.StatusBlocked
- reason := strings.TrimPrefix(notice, "goal blocked: ")
- if reason == "" {
- reason = notice
- }
- if err := c.autoResearch.updateProgress(taskID, autoresearch.ProgressPatch{Status: &status, BlockedReason: &reason}); err != nil {
- c.noticeDetail("AutoResearch status update failed.", "autoresearch task blocked update failed: "+err.Error())
- return
- }
- c.noticeDetail("AutoResearch task marked blocked.", "autoresearch task blocked: "+taskID+"\nreason: "+reason)
- }
-}
-
// completeRemainingGoalTodos force-completes any remaining incomplete canonical
// todos when the goal FSM transitions to completed and emits a synthetic
// todo_write event so the frontend panel reflects the final state. Handles the
diff --git a/internal/goaleval/evaluator.go b/internal/goaleval/evaluator.go
index a962b2f142..d4c131d1d3 100644
--- a/internal/goaleval/evaluator.go
+++ b/internal/goaleval/evaluator.go
@@ -60,13 +60,12 @@ const (
// MaxEvidenceBytes caps the serialized evidence JSON.
MaxEvidenceBytes = 6 * 1024
// Field budgets keep the total request inside boundedllm.DefaultMaxTotalBytes.
- MaxGoalBytes = 600
- MaxAssistantFinal = 1200
- MaxTodoSummary = 600
- MaxAutoResearchBytes = 600
- MaxTurnStatusBytes = 300
- MaxLastReasonBytes = 200
- MaxReasonBytes = 500
+ MaxGoalBytes = 600
+ MaxAssistantFinal = 1200
+ MaxTodoSummary = 600
+ MaxTurnStatusBytes = 300
+ MaxLastReasonBytes = 200
+ MaxReasonBytes = 500
)
// Outcome is the evaluator's structured verdict disposition.
@@ -95,8 +94,6 @@ type GoalEvidence struct {
AssistantFinal string
// TodoSummary is a host-built todo/readiness summary.
TodoSummary string
- // AutoResearchSummary is the AutoResearch success-criteria summary.
- AutoResearchSummary string
// TurnStatus describes turn/budget state.
TurnStatus string
// LastContinuationReason is the previous continuation's recorded reason.
@@ -184,13 +181,12 @@ func (s *Session) Evaluate(ctx context.Context, evidence GoalEvidence) (Verdict,
}
type evidencePayload struct {
- Notice string `json:"notice"`
- GoalContract string `json:"goal_contract,omitempty"`
- AssistantFinal string `json:"assistant_final,omitempty"`
- TodoSummary string `json:"todo_summary,omitempty"`
- AutoResearchSummary string `json:"autoresearch_summary,omitempty"`
- TurnStatus string `json:"turn_status,omitempty"`
- LastReason string `json:"last_reason,omitempty"`
+ Notice string `json:"notice"`
+ GoalContract string `json:"goal_contract,omitempty"`
+ AssistantFinal string `json:"assistant_final,omitempty"`
+ TodoSummary string `json:"todo_summary,omitempty"`
+ TurnStatus string `json:"turn_status,omitempty"`
+ LastReason string `json:"last_reason,omitempty"`
}
// buildEvidence budgets every field before marshaling; the serialized payload
@@ -208,9 +204,6 @@ func buildEvidence(evidence GoalEvidence) (string, error) {
if s := clip(strings.TrimSpace(evidence.TodoSummary), MaxTodoSummary); s != "" {
payload.TodoSummary = s
}
- if s := clip(strings.TrimSpace(evidence.AutoResearchSummary), MaxAutoResearchBytes); s != "" {
- payload.AutoResearchSummary = s
- }
if s := clip(strings.TrimSpace(evidence.TurnStatus), MaxTurnStatusBytes); s != "" {
payload.TurnStatus = s
}
diff --git a/internal/taskintent/boundary_test.go b/internal/taskintent/boundary_test.go
index e573fd693b..a7cdad3642 100644
--- a/internal/taskintent/boundary_test.go
+++ b/internal/taskintent/boundary_test.go
@@ -19,14 +19,17 @@ var allowedExports = map[string]bool{
"ObservableRead": true, "Mutation": true, "PersistentAction": true,
"Classify": true, "NeedsEvidence": true, "NeedsMutation": true,
"NeedsPersistentAction": true, "GoalNeedsWriteBudget": true,
+ "BudgetClassSimple": true, "BudgetClassWrite": true, "BudgetClassResearch": true,
+ "BudgetTurns": true, "ClassifyGoalBudget": true,
}
// lineBudgets caps the heuristic files: vocabulary growth must displace
// something or justify a deliberate budget bump in review.
var lineBudgets = map[string]int{
- "intent.go": 620,
- "heuristic.go": 180,
- "goal_budget.go": 140,
+ "intent.go": 620,
+ "heuristic.go": 180,
+ "goal_budget.go": 140,
+ "goal_research_budget.go": 120,
}
func TestExportSurfaceIsFrozen(t *testing.T) {
diff --git a/internal/taskintent/doc.go b/internal/taskintent/doc.go
index 252cbb454a..360a605496 100644
--- a/internal/taskintent/doc.go
+++ b/internal/taskintent/doc.go
@@ -1,9 +1,9 @@
-// Package taskintent answers exactly one question from task text: is this
-// obviously chat, a read, a mutation, or a persistent action. That is its
-// whole charter. It must not grow into complexity, risk, planner depth,
-// verification depth, completion, budget, or tool-surface decisions —
-// those belong to runtime evidence (see internal/taskcontract), where a
-// receipt outranks any keyword.
+// Package taskintent answers classification questions from task text: is this
+// obviously chat, a read, a mutation, or a persistent action, and which Goal
+// turn-budget class (simple/write/research) the objective should start on.
+// It must not grow into complexity, risk, planner depth, verification depth,
+// completion, or tool-surface decisions — those belong to runtime evidence
+// (see internal/taskcontract), where a receipt outranks any keyword.
//
// The vocabulary is a liability, not an asset: every added keyword, negation
// rule, or language case moves this package toward an unowned NLP parser.
diff --git a/internal/taskintent/goal_budget_test.go b/internal/taskintent/goal_budget_test.go
index f779e6323d..9211a1b8f1 100644
--- a/internal/taskintent/goal_budget_test.go
+++ b/internal/taskintent/goal_budget_test.go
@@ -75,6 +75,21 @@ func TestGoalBareFaultDoesNotChangeDeliveryClassification(t *testing.T) {
}
}
+func TestClassifyGoalBudgetMatrix(t *testing.T) {
+ if got := ClassifyGoalBudget("hello"); got != BudgetClassSimple {
+ t.Fatalf("simple = %q", got)
+ }
+ if got := ClassifyGoalBudget("fix the crash in a.go"); got != BudgetClassWrite {
+ t.Fatalf("write = %q", got)
+ }
+ if got := ClassifyGoalBudget("持续排查这个线上卡顿直到根因明确,并验证修复"); got != BudgetClassResearch {
+ t.Fatalf("research = %q", got)
+ }
+ if BudgetTurns(BudgetClassSimple) != 10 || BudgetTurns(BudgetClassWrite) != 20 || BudgetTurns(BudgetClassResearch) != 40 {
+ t.Fatalf("quotas simple=%d write=%d research=%d", BudgetTurns(BudgetClassSimple), BudgetTurns(BudgetClassWrite), BudgetTurns(BudgetClassResearch))
+ }
+}
+
func TestTaskFaultSignalsSharedWithGoalClassification(t *testing.T) {
// Shared fault list must keep task recognition and Goal classification
// aligned for bare problem statements.
diff --git a/internal/taskintent/goal_research_budget.go b/internal/taskintent/goal_research_budget.go
new file mode 100644
index 0000000000..5caaee37ce
--- /dev/null
+++ b/internal/taskintent/goal_research_budget.go
@@ -0,0 +1,89 @@
+package taskintent
+
+import "strings"
+
+// Goal turn-budget classes. Quotas are fixed; classes never gate permissions.
+const (
+ BudgetClassSimple = "simple"
+ BudgetClassWrite = "write"
+ BudgetClassResearch = "research"
+)
+
+// BudgetTurns returns the default turn quota for a Goal budget class.
+func BudgetTurns(class string) int {
+ switch class {
+ case BudgetClassResearch:
+ return 40
+ case BudgetClassWrite:
+ return 20
+ default:
+ return 10
+ }
+}
+
+// ClassifyGoalBudget selects simple/write/research from goal text alone.
+// Legacy CLI flags and sidecars apply on/off overrides in the control package.
+func ClassifyGoalBudget(goal string) string {
+ if needsResearchBudget(goal) {
+ return BudgetClassResearch
+ }
+ if GoalNeedsWriteBudget(goal) {
+ return BudgetClassWrite
+ }
+ return BudgetClassSimple
+}
+
+func needsResearchBudget(goal string) bool {
+ trimmed := strings.TrimSpace(goal)
+ if trimmed == "" {
+ return false
+ }
+ lower := strings.ToLower(trimmed)
+ if strings.Contains(lower, ".reasonix/autoresearch/") {
+ return true
+ }
+ for _, kw := range researchBudgetStrongKeywords {
+ if strings.Contains(lower, kw) {
+ return true
+ }
+ }
+ return researchBudgetPhaseCount(lower) >= 4
+}
+
+func researchBudgetPhaseCount(lower string) int {
+ categories := 0
+ for _, group := range researchBudgetPhaseKeywords {
+ if containsAnyGoalKeyword(lower, group) {
+ categories++
+ }
+ }
+ return categories
+}
+
+func containsAnyGoalKeyword(s string, needles []string) bool {
+ for _, needle := range needles {
+ if strings.Contains(s, needle) {
+ return true
+ }
+ }
+ return false
+}
+
+var researchBudgetStrongKeywords = []string{
+ "持续", "长期", "彻底", "直到根因", "根因明确", "多轮",
+ "不要原地打转", "别原地打转", "完整方案", "完整做成方案",
+ "跑实验", "反复验证", "长期优化", "系统性研究", "持续研究",
+ "持续排查", "持续推进", "长期跑",
+ "long-horizon", "long horizon", "long-running", "keep researching",
+ "keep working", "root cause", "until the root cause", "do not spin",
+ "don't spin", "thoroughly", "systematically",
+}
+
+var researchBudgetPhaseKeywords = [][]string{
+ {"研究", "调研", "排查", "分析", "定位", "诊断", "research", "investigate", "diagnose", "analyze", "analysis"},
+ {"实现", "修复", "改造", "开发", "重构", "implement", "build", "fix", "refactor"},
+ {"验证", "测试", "复现", "联调", "benchmark", "verify", "validate", "test", "reproduce"},
+ {"优化", "完善", "提升", "收敛", "optimize", "improve", "tune", "polish"},
+ {"文档", "方案", "说明", "总结", "document", "docs", "writeup", "plan"},
+ {"发布", "上线", "提交", "pull request", "publish", "ship", "deploy"},
+}
diff --git a/internal/tool/goal.go b/internal/tool/goal.go
index f931d05163..1ad0ea538a 100644
--- a/internal/tool/goal.go
+++ b/internal/tool/goal.go
@@ -27,6 +27,11 @@ type GoalTurnRecorder interface {
type goalTurnRecorderKey struct{}
+// noGoalTurnRecorder shadows an ancestor recorder while preserving the rest
+// of the context chain. Child agents must not report disposition for the
+// parent's goal turn.
+type noGoalTurnRecorder struct{}
+
// WithGoalTurnRecorder stamps ctx with the per-turn goal recorder so the
// update_goal tool can reach it from inside the run loop.
func WithGoalTurnRecorder(ctx context.Context, r GoalTurnRecorder) context.Context {
@@ -36,6 +41,13 @@ func WithGoalTurnRecorder(ctx context.Context, r GoalTurnRecorder) context.Conte
return context.WithValue(ctx, goalTurnRecorderKey{}, r)
}
+// WithoutGoalTurnRecorder returns a child context that cannot access a goal
+// recorder inherited from its parent. Other values and cancellation continue
+// to flow through the context normally.
+func WithoutGoalTurnRecorder(ctx context.Context) context.Context {
+ return context.WithValue(ctx, goalTurnRecorderKey{}, noGoalTurnRecorder{})
+}
+
// GoalTurnRecorderFromContext returns the active goal turn's recorder, if any.
func GoalTurnRecorderFromContext(ctx context.Context) (GoalTurnRecorder, bool) {
if ctx == nil {
diff --git a/internal/tool/goal_test.go b/internal/tool/goal_test.go
new file mode 100644
index 0000000000..e0bdbaf9de
--- /dev/null
+++ b/internal/tool/goal_test.go
@@ -0,0 +1,30 @@
+package tool
+
+import (
+ "context"
+ "testing"
+)
+
+type goalTestRecorder struct{}
+
+func (goalTestRecorder) RecordGoalReport(GoalReport) (string, error) { return "recorded", nil }
+
+type preservedGoalContextKey struct{}
+
+func TestWithoutGoalTurnRecorderShadowsOnlyRecorder(t *testing.T) {
+ parent, cancel := context.WithCancel(context.Background())
+ parent = context.WithValue(parent, preservedGoalContextKey{}, "preserved")
+ parent = WithGoalTurnRecorder(parent, goalTestRecorder{})
+
+ child := WithoutGoalTurnRecorder(parent)
+ if _, ok := GoalTurnRecorderFromContext(child); ok {
+ t.Fatal("child context inherited the parent goal recorder")
+ }
+ if got := child.Value(preservedGoalContextKey{}); got != "preserved" {
+ t.Fatalf("unrelated context value = %v, want preserved", got)
+ }
+ cancel()
+ if child.Err() != context.Canceled {
+ t.Fatalf("child cancellation = %v, want context.Canceled", child.Err())
+ }
+}
diff --git a/scripts/check-cache-impact.sh b/scripts/check-cache-impact.sh
index b3866f3f1a..90ec4115b0 100755
--- a/scripts/check-cache-impact.sh
+++ b/scripts/check-cache-impact.sh
@@ -64,15 +64,22 @@ for file in "${changed_files[@]:-}"; do
internal/agent/ask.go|\
internal/agent/cache*|\
internal/agent/compact*|\
+ internal/agent/goal_display.go|\
internal/agent/parallel_tasks.go|\
+ internal/agent/planner_registry.go|\
internal/agent/prune*|\
internal/agent/subagent_registry*|\
+ internal/agent/subagent_identity.go|\
internal/agent/task.go|\
internal/boot/*|\
internal/command/slashtool.go|\
internal/config/config.go|\
internal/config/system_prompt*|\
+ internal/control/goal.go|\
+ internal/control/input.go|\
+ internal/control/turn_orchestrator.go|\
internal/environment/*|\
+ internal/goaleval/*|\
internal/history/tool.go|\
internal/installsource/*|\
internal/lsp/tool.go|\
@@ -81,6 +88,7 @@ for file in "${changed_files[@]:-}"; do
internal/plugin/*|\
internal/provider/*|\
internal/skill/*|\
+ internal/taskintent/*|\
internal/tool/*|\
scripts/cache-guard.sh|\
scripts/check-cache-impact.sh)
diff --git a/site/src/pages/docs.astro b/site/src/pages/docs.astro
index ec2ea0d9d8..7d6bc636b9 100644
--- a/site/src/pages/docs.astro
+++ b/site/src/pages/docs.astro
@@ -301,7 +301,7 @@ reasonix upgrade
Mouse capture is on by default so Reasonix can handle transcript selection, wheel scroll, and the scrollbar. Turn it off with /mouse, or start with REASONIX_DISABLE_MOUSE=1, when you prefer the terminal's own selection behavior.默认会开启鼠标接管,用于对话选中、滚轮滚动和滚动条。需要终端自己的选中行为时,用 /mouse 关闭;也可以用 REASONIX_DISABLE_MOUSE=1 默认关闭。
In a local session, releasing an in-app text selection copies through the native system clipboard and shows success only after the write completes. SSH falls back to a clearly labelled OSC 52 request. Text paste remains your terminal's bracketed-paste shortcut, such as Cmd+V on macOS. Image paste is separate: use Ctrl+V on macOS/Linux, Alt+V on Windows, or /paste-image; the footer shows Pasting image… while the attachment is prepared.本地会话中,应用内文本选区会写入系统剪贴板,只有写入完成后才提示成功;SSH 会回退到明确标记的 OSC 52 请求。文本继续使用终端原生 bracketed-paste 快捷键,例如 macOS 的 Cmd+V。图片粘贴使用独立入口:macOS/Linux 按 Ctrl+V,Windows 按 Alt+V,或运行 /paste-image;附件准备期间底栏显示“正在粘贴图片…”。
/branch [name] forks the current conversation tip, /switch <id|name> loads another branch, and /clear confirms before discarding unsaved context. Custom commands are Markdown files under .reasonix/commands/ or ~/.reasonix/commands/./branch [name] 从当前会话尖端分叉,/switch <id|name> 加载另一条分支,/clear 会确认后丢弃未保存上下文。自定义命令是 .reasonix/commands/ 或 ~/.reasonix/commands/ 下的 Markdown 文件。
- /goal is for long-running objectives. Ordinary chat never changes mode automatically. Goals run under a per-class budget (simple 10 turns / 200k tokens, write 20 turns / 400k tokens, AutoResearch 40 turns / 800k tokens; 4 turns without host-verifiable progress pause) — /goal status shows the runtime, /goal pause suspends, /goal resume continues (budget pauses add one more slice). Each goal turn ends with a structured update_goal report (continue/complete/blocked) that the host validates against Delivery readiness; without a report, an independent bounded evaluator judges the turn once and any failure pauses safely. Clearly long-horizon work can use the AutoResearch strategy, which keeps state under .reasonix/autoresearch/..., tracks evidence, and forces a new direction when progress stalls. Use /goal --research <objective> to force it or /goal --simple <objective> to keep the lightweight path. AutoResearch is a Goal strategy, not a separate app-start daemon or standalone built-in skill./goal 用于长目标。普通聊天不会自动切换模式。Goal 按类别运行在预算内(简单 10 轮 / 20 万 token,写入型 20 轮 / 40 万 token,AutoResearch 40 轮 / 80 万 token;连续 4 轮无宿主可验证进展会暂停)——/goal status 显示运行摘要,/goal pause 暂停,/goal resume 继续(预算型暂停追加一档额度)。每个目标 turn 结束时通过结构化的 update_goal 报告(continue/complete/blocked),宿主会用 Delivery readiness 校验;没有报告时由独立有界 evaluator 判定一次,任何故障都会安全暂停。明显长周期的任务可以启用 AutoResearch 策略,在 .reasonix/autoresearch/... 下保存状态、记录证据,并在进展停滞时强制换方向。用 /goal --research <目标> 强制启用,或用 /goal --simple <目标> 保持轻量路径。AutoResearch 是 Goal 的策略,不是 App 启动即运行的 daemon,也不是独立内置 skill。
+ /goal is for long-running objectives. Ordinary chat never changes mode automatically. Goal selects a simple (10), write (20), or research (40) turn budget and pauses after four turns without host-verifiable progress. /goal status shows the runtime, /goal pause suspends, and /goal resume continues. Every class uses the same Goal state machine, structured update_goal reports, host receipts, Delivery readiness, and bounded evaluator. Legacy research archives are read-only and new Goals never create them./goal 用于长目标。普通聊天不会自动切换模式。Goal 自动选择简单(10)、写入(20)或研究(40)轮预算,连续 4 轮无宿主可验证进展会暂停。/goal status 显示运行摘要,/goal pause 暂停,/goal resume 继续。所有预算类别共用同一个 Goal 状态机、结构化 update_goal、宿主 receipt、Delivery readiness 与有界 evaluator。旧研究归档保持只读,新 Goal 不再创建这些目录。
Use @path to inject files or directories, and @server:uri for MCP resources. Plan Mode is an explicit user choice: select it in the desktop collaboration control or cycle to it with Shift+Tab in the CLI. reasonix config reasoning-language auto|zh|en updates the user default from scripts; --local remains available for settings that support project-local overrides.用 @path 注入文件或目录,用 @server:uri 引入 MCP resource。计划模式始终由用户显式选择:桌面端在协作方式中选择,CLI 用 Shift+Tab 切换。脚本中可用 reasonix config reasoning-language auto|zh|en 更新用户级默认值;--local 仍可用于支持项目级覆盖的设置。
Built-in documentation search内置文档检索