@kkelly-offical/kkcode 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2981
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +298 -298
  96. package/src/session/engine.mjs +417 -232
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1097
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -900
  105. package/src/session/loop.mjs +1005 -930
  106. package/src/session/prompt/agent.txt +25 -25
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +31 -31
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +196 -195
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -519
  116. package/src/session/system-prompt.mjs +308 -273
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +99 -93
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -1,232 +1,417 @@
1
- import { randomUUID } from "node:crypto"
2
- import { loadPricing, calculateCost } from "../usage/pricing.mjs"
3
- import { recordTurn } from "../usage/usage-meter.mjs"
4
- import { processTurnLoop } from "./loop.mjs"
5
- import { runLongAgent } from "./longagent.mjs"
6
- import { touchSession, setBudgetState } from "./store.mjs"
7
- import { appendEventLog } from "../storage/event-log.mjs"
8
- import { EventBus } from "../core/events.mjs"
9
- import { initialize as initObservability } from "../observability/index.mjs"
10
- import { ToolRegistry } from "../tool/registry.mjs"
11
- import { resolveAgentForMode } from "../agent/agent.mjs"
12
- import { estimateStringTokens } from "./compaction.mjs"
13
-
14
- let sinkReady = false
15
-
16
- function estimateTokens(text) {
17
- return Math.max(1, estimateStringTokens(text || ""))
18
- }
19
-
20
- export function resolveMode(inputMode = "agent") {
21
- const mode = String(inputMode || "agent").toLowerCase()
22
- if (["ask", "plan", "agent", "longagent"].includes(mode)) return mode
23
- return "agent"
24
- }
25
-
26
- export function newSessionId() {
27
- return `ses_${randomUUID().slice(0, 12)}`
28
- }
29
-
30
- function maybeRegisterSink() {
31
- if (sinkReady) return
32
- EventBus.registerSink(async (event) => {
33
- await appendEventLog(event)
34
- })
35
- initObservability(EventBus)
36
- sinkReady = true
37
- }
38
-
39
- function evaluateBudget(config, meter) {
40
- const budget = config.usage?.budget || {}
41
- const warnings = []
42
- const strategy = budget.strategy || "warn"
43
- const warnAt = Number(budget.warn_at_percent || 80)
44
- let exceeded = false
45
-
46
- if (budget.session_usd && meter.session.cost > 0) {
47
- const ratio = (meter.session.cost / budget.session_usd) * 100
48
- if (ratio >= 100) exceeded = true
49
- if (ratio >= warnAt) warnings.push(`session budget ${ratio.toFixed(1)}% (${meter.session.cost.toFixed(4)}/${budget.session_usd})`)
50
- }
51
- if (budget.global_usd && meter.global.cost > 0) {
52
- const ratio = (meter.global.cost / budget.global_usd) * 100
53
- if (ratio >= 100) exceeded = true
54
- if (ratio >= warnAt) warnings.push(`global budget ${ratio.toFixed(1)}% (${meter.global.cost.toFixed(4)}/${budget.global_usd})`)
55
- }
56
- return { warnings, exceeded, strategy }
57
- }
58
-
59
- export async function executeTurn({
60
- prompt,
61
- contentBlocks = null,
62
- mode,
63
- model,
64
- sessionId,
65
- configState,
66
- providerType = null,
67
- baseUrl = null,
68
- apiKeyEnv = null,
69
- maxIterations = null,
70
- signal = null,
71
- output = null,
72
- allowQuestion = true,
73
- toolContext = {}
74
- }) {
75
- maybeRegisterSink()
76
-
77
- const resolvedProviderType = providerType || configState.config.provider.default
78
- const agent = resolveAgentForMode(mode)
79
- await ToolRegistry.initialize({
80
- config: configState.config,
81
- cwd: process.cwd()
82
- })
83
- // Auto-name session from first user prompt (truncated to 50 chars)
84
- const autoTitle = typeof prompt === "string"
85
- ? prompt.replace(/\s+/g, " ").trim().slice(0, 50)
86
- : null
87
- await touchSession({
88
- sessionId,
89
- mode,
90
- model,
91
- providerType: resolvedProviderType,
92
- cwd: process.cwd(),
93
- title: autoTitle || null,
94
- status: mode === "longagent" ? "running-longagent" : "active"
95
- })
96
-
97
- const turn =
98
- mode === "longagent"
99
- ? await runLongAgent({
100
- prompt,
101
- model,
102
- providerType: resolvedProviderType,
103
- sessionId,
104
- configState,
105
- baseUrl,
106
- apiKeyEnv,
107
- agent,
108
- maxIterations:
109
- maxIterations === null
110
- ? Number(configState.config.agent.longagent.max_iterations || 0)
111
- : Number(maxIterations),
112
- signal,
113
- output,
114
- allowQuestion,
115
- toolContext
116
- })
117
- : await processTurnLoop({
118
- prompt,
119
- contentBlocks,
120
- mode,
121
- model,
122
- providerType: resolvedProviderType,
123
- sessionId,
124
- configState,
125
- baseUrl,
126
- apiKeyEnv,
127
- agent,
128
- output,
129
- signal,
130
- allowQuestion,
131
- toolContext
132
- })
133
-
134
- const usage = { ...turn.usage }
135
- let estimated = false
136
- if ((usage.input || 0) === 0 && (usage.output || 0) === 0) {
137
- usage.input = estimateTokens(prompt)
138
- usage.output = estimateTokens(turn.reply)
139
- estimated = true
140
- }
141
-
142
- const pricingInfo = await loadPricing(configState)
143
- const costInfo = calculateCost(pricingInfo.pricing, model, usage)
144
- const meter = await recordTurn({ sessionId, usage, cost: costInfo.amount })
145
- const budgetResult = evaluateBudget(configState.config, meter)
146
-
147
- await setBudgetState(sessionId, {
148
- lastTurnCost: costInfo.amount,
149
- warnings: budgetResult.warnings,
150
- exceeded: budgetResult.exceeded,
151
- updatedAt: Date.now()
152
- })
153
-
154
- if (budgetResult.exceeded && budgetResult.strategy === "block") {
155
- const msg = `budget exceeded — ${budgetResult.warnings.join("; ")}. strategy=block, stopping execution.`
156
- return {
157
- reply: msg,
158
- mode,
159
- model,
160
- sessionId,
161
- turnId: turn.turnId,
162
- emittedText: turn.emittedText,
163
- context: turn.context,
164
- tokenMeter: { ...meter, estimated: estimated || costInfo.unknown },
165
- cost: costInfo.amount,
166
- costSavings: costInfo.savings,
167
- pricingWarnings: pricingInfo.errors,
168
- budgetWarnings: budgetResult.warnings,
169
- budgetExceeded: true,
170
- toolEvents: turn.toolEvents,
171
- longagent: mode === "longagent"
172
- ? {
173
- status: turn.status,
174
- phase: turn.phase,
175
- gateStatus: turn.gateStatus,
176
- currentGate: turn.currentGate,
177
- lastGateFailures: turn.lastGateFailures || [],
178
- iterations: turn.iterations,
179
- recoveryCount: turn.recoveryCount,
180
- progress: turn.progress,
181
- elapsed: turn.elapsed,
182
- stageIndex: turn.stageIndex,
183
- stageCount: turn.stageCount,
184
- currentStageId: turn.currentStageId,
185
- planFrozen: turn.planFrozen,
186
- taskProgress: turn.taskProgress,
187
- stageProgress: turn.stageProgress,
188
- remainingFilesCount: turn.remainingFilesCount,
189
- fileChanges: turn.fileChanges || []
190
- }
191
- : null
192
- }
193
- }
194
-
195
- return {
196
- reply: turn.reply,
197
- mode,
198
- model,
199
- sessionId,
200
- turnId: turn.turnId,
201
- emittedText: turn.emittedText,
202
- context: turn.context,
203
- tokenMeter: { ...meter, estimated: estimated || costInfo.unknown },
204
- cost: costInfo.amount,
205
- costSavings: costInfo.savings,
206
- pricingWarnings: pricingInfo.errors,
207
- budgetWarnings: budgetResult.warnings,
208
- budgetExceeded: false,
209
- toolEvents: turn.toolEvents,
210
- longagent: mode === "longagent"
211
- ? {
212
- status: turn.status,
213
- phase: turn.phase,
214
- gateStatus: turn.gateStatus,
215
- currentGate: turn.currentGate,
216
- lastGateFailures: turn.lastGateFailures || [],
217
- iterations: turn.iterations,
218
- recoveryCount: turn.recoveryCount,
219
- progress: turn.progress,
220
- elapsed: turn.elapsed,
221
- stageIndex: turn.stageIndex,
222
- stageCount: turn.stageCount,
223
- currentStageId: turn.currentStageId,
224
- planFrozen: turn.planFrozen,
225
- taskProgress: turn.taskProgress,
226
- stageProgress: turn.stageProgress,
227
- remainingFilesCount: turn.remainingFilesCount,
228
- fileChanges: turn.fileChanges || []
229
- }
230
- : null
231
- }
232
- }
1
+ import { randomUUID } from "node:crypto"
2
+ import { loadPricing, calculateCost } from "../usage/pricing.mjs"
3
+ import { recordTurn } from "../usage/usage-meter.mjs"
4
+ import { processTurnLoop } from "./loop.mjs"
5
+ import { runLongAgent } from "./longagent.mjs"
6
+ import { touchSession, setBudgetState } from "./store.mjs"
7
+ import { appendEventLog } from "../storage/event-log.mjs"
8
+ import { EventBus } from "../core/events.mjs"
9
+ import { initialize as initObservability } from "../observability/index.mjs"
10
+ import { ToolRegistry } from "../tool/registry.mjs"
11
+ import { SkillRegistry } from "../skill/registry.mjs"
12
+ import { resolveAgentForMode } from "../agent/agent.mjs"
13
+ import { estimateStringTokens } from "./compaction.mjs"
14
+ import { classifyTaskMode, explainTaskModeReason } from "./longagent-utils.mjs"
15
+
16
+ let sinkReady = false
17
+
18
+ export const PUBLIC_MODE_CONTRACT = Object.freeze([
19
+ {
20
+ mode: "assistant",
21
+ summary: "default CLI personal assistant lane",
22
+ guarantee: "assistant handles bounded terminal-native personal assistant work under normal tool permissions"
23
+ },
24
+ {
25
+ mode: "plan",
26
+ summary: "produce a spec/plan only",
27
+ guarantee: "plan does not execute file mutations"
28
+ },
29
+ {
30
+ mode: "agent",
31
+ summary: "dedicated coding execution lane",
32
+ guarantee: "agent is the dedicated lane for bounded coding inspect/patch/verify work"
33
+ },
34
+ {
35
+ mode: "longagent",
36
+ summary: "heavyweight staged multi-file delivery lane",
37
+ guarantee: "longagent stays reserved for structured multi-file or system-level work"
38
+ }
39
+ ])
40
+
41
+ function estimateTokens(text) {
42
+ return Math.max(1, estimateStringTokens(text || ""))
43
+ }
44
+
45
+ export function resolveMode(inputMode = "assistant") {
46
+ const mode = String(inputMode || "assistant").toLowerCase()
47
+ if (mode === "code" || mode === "coding") return "agent"
48
+ if (mode === "ask") return "assistant"
49
+ if (["assistant", "plan", "agent", "longagent"].includes(mode)) return mode
50
+ return "assistant"
51
+ }
52
+
53
+ export function getPublicModeContract(inputMode = "assistant") {
54
+ const mode = resolveMode(inputMode)
55
+ return PUBLIC_MODE_CONTRACT.find((item) => item.mode === mode) || PUBLIC_MODE_CONTRACT[0]
56
+ }
57
+
58
+ export function formatPublicModeSummary(inputMode = "assistant") {
59
+ const contract = getPublicModeContract(inputMode)
60
+ return `${contract.mode}: ${contract.summary}`
61
+ }
62
+
63
+ export function renderPublicModeContract() {
64
+ return [
65
+ "# Mode Contract",
66
+ "",
67
+ "- `assistant`: default CLI personal assistant lane for bounded terminal-native personal work, explanation, and analysis.",
68
+ "- `plan`: produce a spec/plan only; do not execute file mutations.",
69
+ "- `agent` / `code` / `coding`: dedicated coding lane for inspect/patch/verify work.",
70
+ "- `longagent`: heavyweight staged multi-file delivery lane with explicit gates.",
71
+ "- Route from `assistant` to `agent` when coding mutation, debugging, or test work is explicit.",
72
+ "- Upgrade from `assistant` or `agent` to `longagent` only when heavy multi-file or system-level evidence appears.",
73
+ "- Keep `plan` explicit and mutation-free even when later execution is likely."
74
+ ].join("\n")
75
+ }
76
+
77
+ function summarizeRouteEvidence(classification) {
78
+ const evidence = Array.isArray(classification?.evidence) ? classification.evidence : []
79
+ if (!evidence.length) return "evidence=none"
80
+ return `evidence=${evidence.join(", ")}`
81
+ }
82
+
83
+ function summarizeRouteTopology(classification) {
84
+ const topology = classification?.topology || "open_ended"
85
+ const continuity = classification?.continuity || "new_transaction"
86
+ return `topology=${topology}; continuity=${continuity}`
87
+ }
88
+
89
+ export function summarizeRouteDecision(route) {
90
+ if (!route) return ""
91
+ const parts = [summarizeRouteTopology(route), summarizeRouteEvidence(route)]
92
+ if (route.suggestion) parts.push(`upgrade_path=${route.mode}->${route.suggestion}`)
93
+ return parts.join("; ")
94
+ }
95
+
96
+ /**
97
+ * 智能模式路由:根据 prompt 内容判断最适合的执行模式
98
+ * @returns {{ mode: string, changed: boolean, reason: string, confidence: string, forced: boolean }}
99
+ * forced=true 表示用户强制使用了不匹配的模式(需要确认)
100
+ */
101
+ function finalizeRouteDecision(req, classification, base = {}) {
102
+ const effectiveMode = base.changed ? base.mode : req
103
+ const evidenceSummary = summarizeRouteEvidence(classification)
104
+ const topologySummary = summarizeRouteTopology(classification)
105
+ const upgradePath = base.suggestion ? `${effectiveMode}->${base.suggestion}` : null
106
+ return {
107
+ ...base,
108
+ modeContract: getPublicModeContract(effectiveMode),
109
+ topology: classification.topology || "open_ended",
110
+ evidence: Array.isArray(classification.evidence) ? classification.evidence : [],
111
+ pathHints: Array.isArray(classification.pathHints) ? classification.pathHints : [],
112
+ continuity: classification.continuity || "new_transaction",
113
+ evidenceSummary,
114
+ topologySummary,
115
+ upgradePath,
116
+ observability: {
117
+ requestedMode: req,
118
+ effectiveMode,
119
+ suggestedMode: classification.mode,
120
+ changed: Boolean(base.changed),
121
+ forced: Boolean(base.forced),
122
+ suggestion: base.suggestion || null,
123
+ modeContract: getPublicModeContract(effectiveMode),
124
+ reason: base.reason,
125
+ confidence: base.confidence,
126
+ topology: classification.topology || "open_ended",
127
+ evidence: Array.isArray(classification.evidence) ? classification.evidence : [],
128
+ pathHints: Array.isArray(classification.pathHints) ? classification.pathHints : [],
129
+ continuity: classification.continuity || "new_transaction",
130
+ evidenceSummary,
131
+ topologySummary,
132
+ upgradePath,
133
+ stayedLocal: (effectiveMode === "assistant" && classification.mode === "assistant") || (effectiveMode === "agent" && classification.mode === "agent"),
134
+ deferredLongagent: (req === "assistant" || req === "agent") && base.suggestion === "longagent",
135
+ overEscalatedToLongagent: req === "longagent" && classification.mode === "agent"
136
+ }
137
+ }
138
+ }
139
+
140
+ export function routeMode(prompt, requestedMode, options = {}) {
141
+ const req = resolveMode(requestedMode)
142
+ // plan 模式不参与自动路由
143
+ if (req === "plan") {
144
+ return finalizeRouteDecision(req, {
145
+ mode: req,
146
+ topology: "open_ended",
147
+ evidence: [],
148
+ pathHints: [],
149
+ continuity: "new_transaction"
150
+ }, {
151
+ mode: req,
152
+ changed: false,
153
+ reason: "plan_mode_exempt",
154
+ explanation: explainTaskModeReason("plan_mode_exempt"),
155
+ confidence: "high",
156
+ forced: false
157
+ })
158
+ }
159
+
160
+ const classification = classifyTaskMode(prompt, options)
161
+ const suggested = classification.mode
162
+ const explanation = classification.explanation || explainTaskModeReason(classification.reason)
163
+
164
+ // 相同模式,无需路由
165
+ if (suggested === req) {
166
+ return finalizeRouteDecision(req, classification, { mode: req, changed: false, reason: classification.reason, explanation, confidence: classification.confidence, forced: false })
167
+ }
168
+
169
+ // 低置信度不自动路由
170
+ if (classification.confidence === "low") {
171
+ return finalizeRouteDecision(req, classification, { mode: req, changed: false, reason: "low_confidence", explanation: explainTaskModeReason("low_confidence"), confidence: "low", forced: false })
172
+ }
173
+
174
+ // 高置信度:assistant/agent 模式下检测到 longagent 任务 → 建议切换(无需确认,只提示)
175
+ if ((req === "assistant" || req === "agent") && suggested === "longagent" && classification.confidence === "high") {
176
+ return finalizeRouteDecision(req, classification, { mode: req, changed: false, reason: classification.reason, explanation, confidence: "high", forced: false, suggestion: "longagent" })
177
+ }
178
+
179
+ // assistant 模式下检测到明确编码任务 → 自动切换到专门 coding lane
180
+ if (req === "assistant" && suggested === "agent" && classification.confidence !== "low") {
181
+ return finalizeRouteDecision(req, classification, { mode: "agent", changed: true, reason: classification.reason, explanation, confidence: classification.confidence, forced: false })
182
+ }
183
+
184
+ // agent 模式下检测到通用问答 / 分析任务 → 回到通用 assistant lane
185
+ if (req === "agent" && suggested === "assistant" && classification.confidence !== "low") {
186
+ return finalizeRouteDecision(req, classification, { mode: "assistant", changed: true, reason: classification.reason, explanation, confidence: classification.confidence, forced: false })
187
+ }
188
+
189
+ // 高置信度:用户强制 longagent 但任务是简单 agent 任务 → 需要确认
190
+ if (req === "longagent" && suggested === "agent" && classification.confidence === "high") {
191
+ return finalizeRouteDecision(req, classification, { mode: req, changed: false, reason: classification.reason, explanation, confidence: "high", forced: true, suggestion: "agent" })
192
+ }
193
+
194
+ return finalizeRouteDecision(req, classification, { mode: req, changed: false, reason: classification.reason, explanation, confidence: classification.confidence, forced: false })
195
+ }
196
+
197
+ export function resolvePromptMode(prompt, requestedMode = "agent", options = {}) {
198
+ const requested = resolveMode(requestedMode)
199
+ const route = routeMode(prompt, requested, options)
200
+ return {
201
+ requestedMode: requested,
202
+ effectiveMode: route.changed ? route.mode : requested,
203
+ effectiveContract: getPublicModeContract(route.changed ? route.mode : requested),
204
+ route
205
+ }
206
+ }
207
+
208
+ export function newSessionId() {
209
+ return `ses_${randomUUID().slice(0, 12)}`
210
+ }
211
+
212
+ export function ensureEventSinks() {
213
+ if (sinkReady) return
214
+ EventBus.registerSink(async (event) => {
215
+ await appendEventLog(event)
216
+ })
217
+ initObservability(EventBus)
218
+ sinkReady = true
219
+ }
220
+
221
+ function evaluateBudget(config, meter) {
222
+ const budget = config.usage?.budget || {}
223
+ const warnings = []
224
+ const strategy = budget.strategy || "warn"
225
+ const warnAt = Number(budget.warn_at_percent || 80)
226
+ let exceeded = false
227
+
228
+ if (budget.session_usd && meter.session.cost > 0) {
229
+ const ratio = (meter.session.cost / budget.session_usd) * 100
230
+ if (ratio >= 100) exceeded = true
231
+ if (ratio >= warnAt) warnings.push(`session budget ${ratio.toFixed(1)}% (${meter.session.cost.toFixed(4)}/${budget.session_usd})`)
232
+ }
233
+ if (budget.global_usd && meter.global.cost > 0) {
234
+ const ratio = (meter.global.cost / budget.global_usd) * 100
235
+ if (ratio >= 100) exceeded = true
236
+ if (ratio >= warnAt) warnings.push(`global budget ${ratio.toFixed(1)}% (${meter.global.cost.toFixed(4)}/${budget.global_usd})`)
237
+ }
238
+ return { warnings, exceeded, strategy }
239
+ }
240
+
241
+ export async function executeTurn({
242
+ prompt,
243
+ contentBlocks = null,
244
+ mode,
245
+ model,
246
+ sessionId,
247
+ configState,
248
+ providerType = null,
249
+ baseUrl = null,
250
+ apiKeyEnv = null,
251
+ maxIterations = null,
252
+ signal = null,
253
+ output = null,
254
+ allowQuestion = true,
255
+ toolContext = {},
256
+ longagentImpl = null
257
+ }) {
258
+ ensureEventSinks()
259
+
260
+ const resolvedProviderType = providerType || configState.config.provider.default
261
+ const agent = resolveAgentForMode(mode)
262
+ await ToolRegistry.initialize({
263
+ config: configState.config,
264
+ cwd: process.cwd()
265
+ })
266
+ await SkillRegistry.initialize(configState.config, process.cwd())
267
+ // Auto-name session from first user prompt (truncated to 50 chars)
268
+ const autoTitle = typeof prompt === "string"
269
+ ? prompt.replace(/\s+/g, " ").trim().slice(0, 50)
270
+ : null
271
+ await touchSession({
272
+ sessionId,
273
+ mode,
274
+ model,
275
+ providerType: resolvedProviderType,
276
+ cwd: process.cwd(),
277
+ title: autoTitle || null,
278
+ status: mode === "longagent" ? "running-longagent" : "active"
279
+ })
280
+
281
+ const turn =
282
+ mode === "longagent"
283
+ ? await runLongAgent({
284
+ prompt,
285
+ model,
286
+ providerType: resolvedProviderType,
287
+ sessionId,
288
+ configState,
289
+ baseUrl,
290
+ apiKeyEnv,
291
+ agent,
292
+ maxIterations:
293
+ maxIterations === null
294
+ ? Number(configState.config.agent.longagent.max_iterations || 0)
295
+ : Number(maxIterations),
296
+ signal,
297
+ output,
298
+ allowQuestion,
299
+ toolContext,
300
+ longagentImpl
301
+ })
302
+ : await processTurnLoop({
303
+ prompt,
304
+ contentBlocks,
305
+ mode,
306
+ model,
307
+ providerType: resolvedProviderType,
308
+ sessionId,
309
+ configState,
310
+ baseUrl,
311
+ apiKeyEnv,
312
+ agent,
313
+ output,
314
+ signal,
315
+ allowQuestion,
316
+ toolContext
317
+ })
318
+
319
+ const usage = { ...turn.usage }
320
+ let estimated = false
321
+ if ((usage.input || 0) === 0 && (usage.output || 0) === 0) {
322
+ usage.input = estimateTokens(prompt)
323
+ usage.output = estimateTokens(turn.reply)
324
+ estimated = true
325
+ }
326
+
327
+ const pricingInfo = await loadPricing(configState)
328
+ const costInfo = calculateCost(pricingInfo.pricing, model, usage)
329
+ const meter = await recordTurn({ sessionId, usage, cost: costInfo.amount })
330
+ const budgetResult = evaluateBudget(configState.config, meter)
331
+
332
+ await setBudgetState(sessionId, {
333
+ lastTurnCost: costInfo.amount,
334
+ warnings: budgetResult.warnings,
335
+ exceeded: budgetResult.exceeded,
336
+ updatedAt: Date.now()
337
+ })
338
+
339
+ if (budgetResult.exceeded && budgetResult.strategy === "block") {
340
+ const msg = `budget exceeded — ${budgetResult.warnings.join("; ")}. strategy=block, stopping execution.`
341
+ return {
342
+ reply: msg,
343
+ mode,
344
+ model,
345
+ sessionId,
346
+ turnId: turn.turnId,
347
+ emittedText: turn.emittedText,
348
+ context: turn.context,
349
+ tokenMeter: { ...meter, estimated: estimated || costInfo.unknown },
350
+ cost: costInfo.amount,
351
+ costSavings: costInfo.savings,
352
+ pricingWarnings: pricingInfo.errors,
353
+ budgetWarnings: budgetResult.warnings,
354
+ budgetExceeded: true,
355
+ toolEvents: turn.toolEvents,
356
+ longagent: mode === "longagent"
357
+ ? {
358
+ status: turn.status,
359
+ phase: turn.phase,
360
+ gateStatus: turn.gateStatus,
361
+ currentGate: turn.currentGate,
362
+ lastGateFailures: turn.lastGateFailures || [],
363
+ iterations: turn.iterations,
364
+ recoveryCount: turn.recoveryCount,
365
+ progress: turn.progress,
366
+ elapsed: turn.elapsed,
367
+ stageIndex: turn.stageIndex,
368
+ stageCount: turn.stageCount,
369
+ currentStageId: turn.currentStageId,
370
+ planFrozen: turn.planFrozen,
371
+ taskProgress: turn.taskProgress,
372
+ stageProgress: turn.stageProgress,
373
+ remainingFilesCount: turn.remainingFilesCount,
374
+ fileChanges: turn.fileChanges || []
375
+ }
376
+ : null
377
+ }
378
+ }
379
+
380
+ return {
381
+ reply: turn.reply,
382
+ mode,
383
+ model,
384
+ sessionId,
385
+ turnId: turn.turnId,
386
+ emittedText: turn.emittedText,
387
+ context: turn.context,
388
+ tokenMeter: { ...meter, estimated: estimated || costInfo.unknown },
389
+ cost: costInfo.amount,
390
+ costSavings: costInfo.savings,
391
+ pricingWarnings: pricingInfo.errors,
392
+ budgetWarnings: budgetResult.warnings,
393
+ budgetExceeded: false,
394
+ toolEvents: turn.toolEvents,
395
+ longagent: mode === "longagent"
396
+ ? {
397
+ status: turn.status,
398
+ phase: turn.phase,
399
+ gateStatus: turn.gateStatus,
400
+ currentGate: turn.currentGate,
401
+ lastGateFailures: turn.lastGateFailures || [],
402
+ iterations: turn.iterations,
403
+ recoveryCount: turn.recoveryCount,
404
+ progress: turn.progress,
405
+ elapsed: turn.elapsed,
406
+ stageIndex: turn.stageIndex,
407
+ stageCount: turn.stageCount,
408
+ currentStageId: turn.currentStageId,
409
+ planFrozen: turn.planFrozen,
410
+ taskProgress: turn.taskProgress,
411
+ stageProgress: turn.stageProgress,
412
+ remainingFilesCount: turn.remainingFilesCount,
413
+ fileChanges: turn.fileChanges || []
414
+ }
415
+ : null
416
+ }
417
+ }