@kkelly-offical/kkcode 0.1.3 → 0.1.7

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 (66) hide show
  1. package/README.md +110 -172
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +220 -170
  4. package/src/agent/prompt/bug-hunter.txt +90 -0
  5. package/src/agent/prompt/frontend-designer.txt +58 -0
  6. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  7. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  8. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  9. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  10. package/src/config/defaults.mjs +260 -195
  11. package/src/config/schema.mjs +71 -6
  12. package/src/core/constants.mjs +91 -46
  13. package/src/index.mjs +1 -1
  14. package/src/knowledge/frontend-aesthetics.txt +39 -0
  15. package/src/knowledge/loader.mjs +2 -1
  16. package/src/knowledge/tailwind.txt +12 -3
  17. package/src/mcp/client-http.mjs +141 -157
  18. package/src/mcp/client-sse.mjs +288 -286
  19. package/src/mcp/client-stdio.mjs +533 -451
  20. package/src/mcp/constants.mjs +2 -0
  21. package/src/mcp/registry.mjs +479 -394
  22. package/src/mcp/stdio-framing.mjs +133 -127
  23. package/src/mcp/tool-result.mjs +24 -0
  24. package/src/observability/index.mjs +42 -0
  25. package/src/observability/metrics.mjs +137 -0
  26. package/src/observability/tracer.mjs +137 -0
  27. package/src/orchestration/background-manager.mjs +372 -358
  28. package/src/orchestration/background-worker.mjs +305 -245
  29. package/src/orchestration/longagent-manager.mjs +171 -116
  30. package/src/orchestration/stage-scheduler.mjs +728 -489
  31. package/src/permission/exec-policy.mjs +9 -11
  32. package/src/provider/anthropic.mjs +1 -0
  33. package/src/provider/openai.mjs +340 -339
  34. package/src/provider/retry-policy.mjs +68 -68
  35. package/src/provider/router.mjs +241 -228
  36. package/src/provider/sse.mjs +104 -91
  37. package/src/repl.mjs +59 -7
  38. package/src/session/checkpoint.mjs +66 -3
  39. package/src/session/compaction.mjs +298 -276
  40. package/src/session/engine.mjs +232 -225
  41. package/src/session/longagent-4stage.mjs +460 -0
  42. package/src/session/longagent-hybrid.mjs +1097 -0
  43. package/src/session/longagent-plan.mjs +365 -329
  44. package/src/session/longagent-project-memory.mjs +53 -0
  45. package/src/session/longagent-scaffold.mjs +291 -100
  46. package/src/session/longagent-task-bus.mjs +54 -0
  47. package/src/session/longagent-utils.mjs +472 -0
  48. package/src/session/longagent.mjs +900 -1462
  49. package/src/session/loop.mjs +65 -40
  50. package/src/session/project-context.mjs +30 -0
  51. package/src/session/prompt/agent.txt +25 -0
  52. package/src/session/prompt/plan.txt +31 -9
  53. package/src/session/rollback.mjs +196 -0
  54. package/src/session/store.mjs +519 -503
  55. package/src/session/system-prompt.mjs +273 -260
  56. package/src/session/task-validator.mjs +4 -3
  57. package/src/skill/builtin/design.mjs +76 -0
  58. package/src/skill/builtin/frontend.mjs +8 -0
  59. package/src/skill/registry.mjs +390 -336
  60. package/src/storage/ghost-commit-store.mjs +18 -8
  61. package/src/tool/executor.mjs +11 -0
  62. package/src/tool/git-auto.mjs +0 -19
  63. package/src/tool/question-prompt.mjs +93 -86
  64. package/src/tool/registry.mjs +71 -37
  65. package/src/ui/activity-renderer.mjs +664 -410
  66. package/src/util/git.mjs +23 -0
@@ -1,225 +1,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 { ToolRegistry } from "../tool/registry.mjs"
10
- import { resolveAgentForMode } from "../agent/agent.mjs"
11
- import { estimateStringTokens } from "./compaction.mjs"
12
-
13
- let sinkReady = false
14
-
15
- function estimateTokens(text) {
16
- return Math.max(1, estimateStringTokens(text || ""))
17
- }
18
-
19
- export function resolveMode(inputMode = "agent") {
20
- const mode = String(inputMode || "agent").toLowerCase()
21
- if (["ask", "plan", "agent", "longagent"].includes(mode)) return mode
22
- return "agent"
23
- }
24
-
25
- export function newSessionId() {
26
- return `ses_${randomUUID().slice(0, 12)}`
27
- }
28
-
29
- function maybeRegisterSink() {
30
- if (sinkReady) return
31
- EventBus.registerSink(async (event) => {
32
- await appendEventLog(event)
33
- })
34
- sinkReady = true
35
- }
36
-
37
- function evaluateBudget(config, meter) {
38
- const budget = config.usage?.budget || {}
39
- const warnings = []
40
- const strategy = budget.strategy || "warn"
41
- const warnAt = Number(budget.warn_at_percent || 80)
42
- let exceeded = false
43
-
44
- if (budget.session_usd && meter.session.cost > 0) {
45
- const ratio = (meter.session.cost / budget.session_usd) * 100
46
- if (ratio >= 100) exceeded = true
47
- if (ratio >= warnAt) warnings.push(`session budget ${ratio.toFixed(1)}% (${meter.session.cost.toFixed(4)}/${budget.session_usd})`)
48
- }
49
- if (budget.global_usd && meter.global.cost > 0) {
50
- const ratio = (meter.global.cost / budget.global_usd) * 100
51
- if (ratio >= 100) exceeded = true
52
- if (ratio >= warnAt) warnings.push(`global budget ${ratio.toFixed(1)}% (${meter.global.cost.toFixed(4)}/${budget.global_usd})`)
53
- }
54
- return { warnings, exceeded, strategy }
55
- }
56
-
57
- export async function executeTurn({
58
- prompt,
59
- contentBlocks = null,
60
- mode,
61
- model,
62
- sessionId,
63
- configState,
64
- providerType = null,
65
- baseUrl = null,
66
- apiKeyEnv = null,
67
- maxIterations = null,
68
- signal = null,
69
- output = null,
70
- allowQuestion = true,
71
- toolContext = {}
72
- }) {
73
- maybeRegisterSink()
74
-
75
- const resolvedProviderType = providerType || configState.config.provider.default
76
- const agent = resolveAgentForMode(mode)
77
- await ToolRegistry.initialize({
78
- config: configState.config,
79
- cwd: process.cwd()
80
- })
81
- await touchSession({
82
- sessionId,
83
- mode,
84
- model,
85
- providerType: resolvedProviderType,
86
- cwd: process.cwd(),
87
- status: mode === "longagent" ? "running-longagent" : "active"
88
- })
89
-
90
- const turn =
91
- mode === "longagent"
92
- ? await runLongAgent({
93
- prompt,
94
- model,
95
- providerType: resolvedProviderType,
96
- sessionId,
97
- configState,
98
- baseUrl,
99
- apiKeyEnv,
100
- agent,
101
- maxIterations:
102
- maxIterations === null
103
- ? Number(configState.config.agent.longagent.max_iterations || 0)
104
- : Number(maxIterations),
105
- signal,
106
- output,
107
- allowQuestion,
108
- toolContext
109
- })
110
- : await processTurnLoop({
111
- prompt,
112
- contentBlocks,
113
- mode,
114
- model,
115
- providerType: resolvedProviderType,
116
- sessionId,
117
- configState,
118
- baseUrl,
119
- apiKeyEnv,
120
- agent,
121
- output,
122
- signal,
123
- allowQuestion,
124
- toolContext
125
- })
126
-
127
- const usage = { ...turn.usage }
128
- let estimated = false
129
- if ((usage.input || 0) === 0 && (usage.output || 0) === 0) {
130
- usage.input = estimateTokens(prompt)
131
- usage.output = estimateTokens(turn.reply)
132
- estimated = true
133
- }
134
-
135
- const pricingInfo = await loadPricing(configState)
136
- const costInfo = calculateCost(pricingInfo.pricing, model, usage)
137
- const meter = await recordTurn({ sessionId, usage, cost: costInfo.amount })
138
- const budgetResult = evaluateBudget(configState.config, meter)
139
-
140
- await setBudgetState(sessionId, {
141
- lastTurnCost: costInfo.amount,
142
- warnings: budgetResult.warnings,
143
- exceeded: budgetResult.exceeded,
144
- updatedAt: Date.now()
145
- })
146
-
147
- if (budgetResult.exceeded && budgetResult.strategy === "block") {
148
- const msg = `budget exceeded — ${budgetResult.warnings.join("; ")}. strategy=block, stopping execution.`
149
- return {
150
- reply: msg,
151
- mode,
152
- model,
153
- sessionId,
154
- turnId: turn.turnId,
155
- emittedText: turn.emittedText,
156
- context: turn.context,
157
- tokenMeter: { ...meter, estimated: estimated || costInfo.unknown },
158
- cost: costInfo.amount,
159
- costSavings: costInfo.savings,
160
- pricingWarnings: pricingInfo.errors,
161
- budgetWarnings: budgetResult.warnings,
162
- budgetExceeded: true,
163
- toolEvents: turn.toolEvents,
164
- longagent: mode === "longagent"
165
- ? {
166
- status: turn.status,
167
- phase: turn.phase,
168
- gateStatus: turn.gateStatus,
169
- currentGate: turn.currentGate,
170
- lastGateFailures: turn.lastGateFailures || [],
171
- iterations: turn.iterations,
172
- recoveryCount: turn.recoveryCount,
173
- progress: turn.progress,
174
- elapsed: turn.elapsed,
175
- stageIndex: turn.stageIndex,
176
- stageCount: turn.stageCount,
177
- currentStageId: turn.currentStageId,
178
- planFrozen: turn.planFrozen,
179
- taskProgress: turn.taskProgress,
180
- stageProgress: turn.stageProgress,
181
- remainingFilesCount: turn.remainingFilesCount,
182
- fileChanges: turn.fileChanges || []
183
- }
184
- : null
185
- }
186
- }
187
-
188
- return {
189
- reply: turn.reply,
190
- mode,
191
- model,
192
- sessionId,
193
- turnId: turn.turnId,
194
- emittedText: turn.emittedText,
195
- context: turn.context,
196
- tokenMeter: { ...meter, estimated: estimated || costInfo.unknown },
197
- cost: costInfo.amount,
198
- costSavings: costInfo.savings,
199
- pricingWarnings: pricingInfo.errors,
200
- budgetWarnings: budgetResult.warnings,
201
- budgetExceeded: false,
202
- toolEvents: turn.toolEvents,
203
- longagent: mode === "longagent"
204
- ? {
205
- status: turn.status,
206
- phase: turn.phase,
207
- gateStatus: turn.gateStatus,
208
- currentGate: turn.currentGate,
209
- lastGateFailures: turn.lastGateFailures || [],
210
- iterations: turn.iterations,
211
- recoveryCount: turn.recoveryCount,
212
- progress: turn.progress,
213
- elapsed: turn.elapsed,
214
- stageIndex: turn.stageIndex,
215
- stageCount: turn.stageCount,
216
- currentStageId: turn.currentStageId,
217
- planFrozen: turn.planFrozen,
218
- taskProgress: turn.taskProgress,
219
- stageProgress: turn.stageProgress,
220
- remainingFilesCount: turn.remainingFilesCount,
221
- fileChanges: turn.fileChanges || []
222
- }
223
- : null
224
- }
225
- }
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
+ }