@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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 (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -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/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -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
+ }