@kkelly-offical/kkcode 0.1.2
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.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- package/src/util/yaml.mjs +100 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { requestProvider } from "../provider/router.mjs"
|
|
2
|
+
import { getConversationHistory, replaceMessages } from "./store.mjs"
|
|
3
|
+
import { HookBus } from "../plugin/hook-bus.mjs"
|
|
4
|
+
import { saveCheckpoint } from "./checkpoint.mjs"
|
|
5
|
+
import { recordTurn } from "../usage/usage-meter.mjs"
|
|
6
|
+
import { loadPricing, calculateCost } from "../usage/pricing.mjs"
|
|
7
|
+
|
|
8
|
+
const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured summary preserving all critical information for continued work.
|
|
9
|
+
|
|
10
|
+
## Output Format
|
|
11
|
+
|
|
12
|
+
<summary>
|
|
13
|
+
<goal>The user's overall goal or current task</goal>
|
|
14
|
+
<completed>
|
|
15
|
+
- Completed task with specific details (file paths, function names, line numbers)
|
|
16
|
+
</completed>
|
|
17
|
+
<in_progress>Current work being done, if any</in_progress>
|
|
18
|
+
<files_modified>
|
|
19
|
+
- path/to/file: specific change description
|
|
20
|
+
</files_modified>
|
|
21
|
+
<key_decisions>
|
|
22
|
+
- Decision and reasoning
|
|
23
|
+
- User preferences or constraints
|
|
24
|
+
</key_decisions>
|
|
25
|
+
<errors_resolved>
|
|
26
|
+
- Error description → fix applied
|
|
27
|
+
</errors_resolved>
|
|
28
|
+
<next_steps>
|
|
29
|
+
- Specific next action items
|
|
30
|
+
</next_steps>
|
|
31
|
+
</summary>
|
|
32
|
+
|
|
33
|
+
Rules:
|
|
34
|
+
- Use the SAME LANGUAGE as the conversation
|
|
35
|
+
- Preserve ALL file paths, function names, variable names, and technical identifiers exactly
|
|
36
|
+
- Include specific code changes, not just "modified file X"
|
|
37
|
+
- Omit tool call metadata and message formatting details
|
|
38
|
+
- Be concise but never drop actionable information`
|
|
39
|
+
|
|
40
|
+
const DEFAULT_THRESHOLD_MESSAGES = 50
|
|
41
|
+
const DEFAULT_THRESHOLD_RATIO = 0.7
|
|
42
|
+
const DEFAULT_KEEP_RECENT = 6
|
|
43
|
+
const TOOL_RESULT_PREVIEW_LIMIT = 200
|
|
44
|
+
|
|
45
|
+
// Estimate tokens from a string, accounting for CJK characters (~1.5 chars/token vs ~4 for Latin)
|
|
46
|
+
export function estimateStringTokens(str) {
|
|
47
|
+
if (!str) return 0
|
|
48
|
+
let cjk = 0
|
|
49
|
+
for (let i = 0; i < str.length; i++) {
|
|
50
|
+
const code = str.charCodeAt(i)
|
|
51
|
+
if ((code >= 0x4E00 && code <= 0x9FFF) || (code >= 0x3000 && code <= 0x30FF) ||
|
|
52
|
+
(code >= 0xAC00 && code <= 0xD7AF)) cjk++
|
|
53
|
+
}
|
|
54
|
+
const latin = str.length - cjk
|
|
55
|
+
return Math.ceil(latin / 4 + cjk / 1.5)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const MSG_OVERHEAD = 4 // ~4 tokens per message for role/metadata
|
|
59
|
+
|
|
60
|
+
export function estimateTokenCount(messages) {
|
|
61
|
+
let tokens = 0
|
|
62
|
+
for (const msg of messages) {
|
|
63
|
+
tokens += MSG_OVERHEAD
|
|
64
|
+
const content = msg.content
|
|
65
|
+
if (Array.isArray(content)) {
|
|
66
|
+
for (const block of content) {
|
|
67
|
+
if (block.type === "image") {
|
|
68
|
+
tokens += 1600 // conservative estimate for a typical image
|
|
69
|
+
} else if (block.type === "tool_use") {
|
|
70
|
+
tokens += estimateStringTokens(block.name || "")
|
|
71
|
+
tokens += estimateStringTokens(JSON.stringify(block.input || {}))
|
|
72
|
+
} else if (block.type === "tool_result") {
|
|
73
|
+
tokens += estimateStringTokens(String(block.content || ""))
|
|
74
|
+
} else {
|
|
75
|
+
tokens += estimateStringTokens(block.text || block.content || "")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
tokens += estimateStringTokens(content || "")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return tokens
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Pre-prune messages before LLM summarization.
|
|
87
|
+
* - Truncate large tool_result content to a short preview
|
|
88
|
+
* - Keep tool_use blocks intact (they show model intent)
|
|
89
|
+
* - Truncate very long plain-text assistant/user messages
|
|
90
|
+
*/
|
|
91
|
+
export function pruneForSummary(messages, previewLimit = TOOL_RESULT_PREVIEW_LIMIT) {
|
|
92
|
+
return messages.map((msg) => {
|
|
93
|
+
const content = msg.content
|
|
94
|
+
if (Array.isArray(content)) {
|
|
95
|
+
const pruned = content.map((block) => {
|
|
96
|
+
if (block.type === "tool_result") {
|
|
97
|
+
const raw = String(block.content || "")
|
|
98
|
+
if (raw.length > previewLimit) {
|
|
99
|
+
return {
|
|
100
|
+
...block,
|
|
101
|
+
content: `${raw.slice(0, previewLimit)}... [truncated ${raw.length} chars]`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return block
|
|
106
|
+
})
|
|
107
|
+
return { ...msg, content: pruned }
|
|
108
|
+
}
|
|
109
|
+
// Truncate very long plain-text messages (e.g. large tool output pasted as text)
|
|
110
|
+
if (typeof content === "string" && content.length > 2000) {
|
|
111
|
+
return { ...msg, content: `${content.slice(0, 2000)}... [truncated ${content.length} chars]` }
|
|
112
|
+
}
|
|
113
|
+
return msg
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const BUILTIN_CONTEXT = {
|
|
118
|
+
"gpt-5": 272000, "o3": 200000, "o1": 200000,
|
|
119
|
+
"claude-opus-4": 200000, "claude-3-5": 200000, "claude-3.5": 200000, "claude": 200000,
|
|
120
|
+
"gemini-2": 1048576, "gemini-1.5": 1048576, "gemini": 128000,
|
|
121
|
+
"gpt-4o": 128000, "gpt-4": 128000, "gpt-3.5": 16000,
|
|
122
|
+
"deepseek": 64000, "qwen": 128000
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function modelContextLimit(model, configState = null) {
|
|
126
|
+
const m = String(model || "").toLowerCase()
|
|
127
|
+
// 1) Check provider-level context_limit for the active provider
|
|
128
|
+
const providerCfg = configState?.config?.provider
|
|
129
|
+
if (providerCfg) {
|
|
130
|
+
// Per-model override from provider.model_context map
|
|
131
|
+
const mc = providerCfg.model_context
|
|
132
|
+
if (mc) {
|
|
133
|
+
if (mc[model]) return mc[model]
|
|
134
|
+
for (const key of Object.keys(mc)) {
|
|
135
|
+
if (m.startsWith(key.toLowerCase())) return mc[key]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Provider-level context_limit
|
|
139
|
+
const active = providerCfg[providerCfg.default]
|
|
140
|
+
if (active?.context_limit > 0) return active.context_limit
|
|
141
|
+
}
|
|
142
|
+
// 2) Builtin prefix match
|
|
143
|
+
for (const [prefix, limit] of Object.entries(BUILTIN_CONTEXT)) {
|
|
144
|
+
if (m.includes(prefix)) return limit
|
|
145
|
+
}
|
|
146
|
+
return 128000
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function contextUtilization(messages, model, configState = null) {
|
|
150
|
+
const tokens = estimateTokenCount(messages)
|
|
151
|
+
const limit = modelContextLimit(model, configState)
|
|
152
|
+
const ratio = limit > 0 ? Math.min(1, tokens / limit) : 0
|
|
153
|
+
return {
|
|
154
|
+
tokens,
|
|
155
|
+
limit,
|
|
156
|
+
ratio,
|
|
157
|
+
percent: Math.round(ratio * 100)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function supportsNativeCompaction(providerType, model) {
|
|
162
|
+
if (providerType !== "anthropic") return false
|
|
163
|
+
const m = String(model || "").toLowerCase()
|
|
164
|
+
return m.includes("claude") && (m.includes("opus") || m.includes("sonnet"))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function shouldCompact({ messages, model, thresholdMessages = DEFAULT_THRESHOLD_MESSAGES, thresholdRatio = DEFAULT_THRESHOLD_RATIO, configState = null, realTokenCount = null }) {
|
|
168
|
+
if (messages.length >= thresholdMessages) return true
|
|
169
|
+
const limit = modelContextLimit(model, configState)
|
|
170
|
+
const tokens = realTokenCount != null ? realTokenCount : estimateTokenCount(messages)
|
|
171
|
+
return tokens >= limit * thresholdRatio
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function compactSession({
|
|
175
|
+
sessionId,
|
|
176
|
+
model,
|
|
177
|
+
providerType,
|
|
178
|
+
configState,
|
|
179
|
+
keepRecent = DEFAULT_KEEP_RECENT,
|
|
180
|
+
baseUrl = null,
|
|
181
|
+
apiKeyEnv = null
|
|
182
|
+
}) {
|
|
183
|
+
const history = await getConversationHistory(sessionId, 9999)
|
|
184
|
+
if (history.length <= keepRecent + 2) return { compacted: false, reason: "too few messages" }
|
|
185
|
+
|
|
186
|
+
// Find split point that doesn't break tool_use/tool_result pairs
|
|
187
|
+
let splitIdx = history.length - keepRecent
|
|
188
|
+
while (splitIdx > 0 && splitIdx < history.length) {
|
|
189
|
+
const msg = history[splitIdx]
|
|
190
|
+
const content = msg.content
|
|
191
|
+
if (Array.isArray(content) && content.some(b => b.type === "tool_result")) {
|
|
192
|
+
splitIdx-- // include the paired assistant tool_use message
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
const toSummarize = history.slice(0, splitIdx)
|
|
198
|
+
const kept = history.slice(splitIdx)
|
|
199
|
+
|
|
200
|
+
// Layer 1: prune large tool outputs before sending to LLM
|
|
201
|
+
const pruned = pruneForSummary(toSummarize)
|
|
202
|
+
const summaryPrompt = pruned.map((m) => {
|
|
203
|
+
const content = m.content
|
|
204
|
+
if (Array.isArray(content)) {
|
|
205
|
+
return `[${m.role}]: ${content.map((b) => {
|
|
206
|
+
if (b.type === "text") return b.text || ""
|
|
207
|
+
if (b.type === "tool_use") return `[tool_use:${b.name}(${JSON.stringify(b.input || {}).slice(0, 120)})]`
|
|
208
|
+
if (b.type === "tool_result") return `[tool_result:${b.is_error ? "ERROR " : ""}${b.content || ""}]`
|
|
209
|
+
return ""
|
|
210
|
+
}).filter(Boolean).join("\n")}`
|
|
211
|
+
}
|
|
212
|
+
return `[${m.role}]: ${content}`
|
|
213
|
+
}).join("\n\n")
|
|
214
|
+
|
|
215
|
+
const hookPayload = await HookBus.sessionCompacting({
|
|
216
|
+
sessionId,
|
|
217
|
+
messageCount: history.length,
|
|
218
|
+
summarizeCount: toSummarize.length,
|
|
219
|
+
keepCount: kept.length
|
|
220
|
+
})
|
|
221
|
+
if (hookPayload?.skip) return { compacted: false, reason: "skipped by hook" }
|
|
222
|
+
|
|
223
|
+
let summaryText
|
|
224
|
+
let compactionUsage = null
|
|
225
|
+
try {
|
|
226
|
+
const response = await requestProvider({
|
|
227
|
+
configState,
|
|
228
|
+
providerType,
|
|
229
|
+
model,
|
|
230
|
+
system: COMPACTION_SYSTEM,
|
|
231
|
+
messages: [{ role: "user", content: summaryPrompt }],
|
|
232
|
+
tools: [],
|
|
233
|
+
baseUrl,
|
|
234
|
+
apiKeyEnv
|
|
235
|
+
})
|
|
236
|
+
summaryText = (response.text || "").trim()
|
|
237
|
+
compactionUsage = response.usage || null
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return { compacted: false, reason: `compaction LLM call failed: ${error.message}` }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!summaryText) return { compacted: false, reason: "empty summary from LLM" }
|
|
243
|
+
|
|
244
|
+
// Replace all messages with: [summary] + [kept recent messages]
|
|
245
|
+
const summaryMessage = {
|
|
246
|
+
role: "user",
|
|
247
|
+
content: `<compaction-summary>\n${summaryText}\n</compaction-summary>`
|
|
248
|
+
}
|
|
249
|
+
await replaceMessages(sessionId, [summaryMessage, ...kept])
|
|
250
|
+
|
|
251
|
+
// Record compaction LLM usage so it's not "invisible"
|
|
252
|
+
if (compactionUsage) {
|
|
253
|
+
try {
|
|
254
|
+
const { pricing } = await loadPricing(configState)
|
|
255
|
+
const { amount } = calculateCost(pricing, model, compactionUsage)
|
|
256
|
+
await recordTurn({ sessionId, usage: compactionUsage, cost: amount })
|
|
257
|
+
} catch { /* best-effort */ }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await saveCheckpoint(sessionId, {
|
|
261
|
+
kind: "compaction",
|
|
262
|
+
iteration: 0,
|
|
263
|
+
compactedAt: Date.now(),
|
|
264
|
+
summarizeCount: toSummarize.length,
|
|
265
|
+
keepCount: kept.length,
|
|
266
|
+
summaryVersion: 1,
|
|
267
|
+
summaryLength: summaryText.length
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
compacted: true,
|
|
272
|
+
summarizedCount: toSummarize.length,
|
|
273
|
+
keptCount: kept.length,
|
|
274
|
+
summaryLength: summaryText.length
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,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 { 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
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { memoryDir } from "../storage/paths.mjs"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Instinct Manager — automatic pattern learning system.
|
|
7
|
+
*
|
|
8
|
+
* Instincts are small, atomic learned behaviors extracted from sessions.
|
|
9
|
+
* Each instinct has a confidence score (0.3–0.9) that increases with repeated observation.
|
|
10
|
+
* High-confidence instincts are injected into the system prompt to guide future behavior.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function instinctsPath(cwd) {
|
|
14
|
+
return path.join(memoryDir(cwd), "instincts.json")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function generateId() {
|
|
18
|
+
return "inst_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function loadStore(cwd) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(instinctsPath(cwd), "utf8")
|
|
24
|
+
return JSON.parse(raw)
|
|
25
|
+
} catch {
|
|
26
|
+
return { instincts: [], version: 1 }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function saveStore(cwd, store) {
|
|
31
|
+
const dir = memoryDir(cwd)
|
|
32
|
+
await mkdir(dir, { recursive: true })
|
|
33
|
+
await writeFile(instinctsPath(cwd), JSON.stringify(store, null, 2) + "\n", "utf8")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add or reinforce an instinct.
|
|
38
|
+
* If a similar pattern already exists (fuzzy match), increase its confidence.
|
|
39
|
+
* Otherwise create a new instinct at base confidence 0.3.
|
|
40
|
+
*/
|
|
41
|
+
export async function addInstinct(cwd, pattern, category = "workflow") {
|
|
42
|
+
const store = await loadStore(cwd)
|
|
43
|
+
const normalized = pattern.trim().toLowerCase()
|
|
44
|
+
|
|
45
|
+
// Fuzzy match: check if any existing instinct is substantially similar
|
|
46
|
+
const existing = store.instincts.find((inst) => {
|
|
47
|
+
const existingNorm = inst.pattern.trim().toLowerCase()
|
|
48
|
+
return existingNorm === normalized || similarity(existingNorm, normalized) > 0.8
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if (existing) {
|
|
52
|
+
existing.observations = (existing.observations || 1) + 1
|
|
53
|
+
existing.confidence = Math.min(0.9, existing.confidence + 0.1)
|
|
54
|
+
existing.lastSeenAt = Date.now()
|
|
55
|
+
} else {
|
|
56
|
+
store.instincts.push({
|
|
57
|
+
id: generateId(),
|
|
58
|
+
pattern: pattern.trim(),
|
|
59
|
+
confidence: 0.3,
|
|
60
|
+
observations: 1,
|
|
61
|
+
category,
|
|
62
|
+
createdAt: Date.now(),
|
|
63
|
+
lastSeenAt: Date.now()
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await saveStore(cwd, store)
|
|
68
|
+
return existing || store.instincts[store.instincts.length - 1]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* List instincts at or above a minimum confidence threshold.
|
|
73
|
+
*/
|
|
74
|
+
export async function listInstincts(cwd, minConfidence = 0.0) {
|
|
75
|
+
const store = await loadStore(cwd)
|
|
76
|
+
return store.instincts
|
|
77
|
+
.filter((inst) => inst.confidence >= minConfidence)
|
|
78
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Remove an instinct by ID.
|
|
83
|
+
*/
|
|
84
|
+
export async function removeInstinct(cwd, id) {
|
|
85
|
+
const store = await loadStore(cwd)
|
|
86
|
+
const before = store.instincts.length
|
|
87
|
+
store.instincts = store.instincts.filter((inst) => inst.id !== id)
|
|
88
|
+
if (store.instincts.length < before) {
|
|
89
|
+
await saveStore(cwd, store)
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Export all instincts for team sharing.
|
|
97
|
+
*/
|
|
98
|
+
export async function exportInstincts(cwd) {
|
|
99
|
+
const store = await loadStore(cwd)
|
|
100
|
+
return {
|
|
101
|
+
exportedAt: Date.now(),
|
|
102
|
+
count: store.instincts.length,
|
|
103
|
+
instincts: store.instincts
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Import instincts from a teammate's export.
|
|
109
|
+
* Merges by pattern similarity — existing patterns get reinforced, new ones get added at 0.3.
|
|
110
|
+
*/
|
|
111
|
+
export async function importInstincts(cwd, data) {
|
|
112
|
+
if (!data || !Array.isArray(data.instincts)) return { imported: 0, reinforced: 0 }
|
|
113
|
+
let imported = 0
|
|
114
|
+
let reinforced = 0
|
|
115
|
+
for (const inst of data.instincts) {
|
|
116
|
+
if (!inst.pattern) continue
|
|
117
|
+
const result = await addInstinct(cwd, inst.pattern, inst.category || "workflow")
|
|
118
|
+
if (result.observations > 1) reinforced++
|
|
119
|
+
else imported++
|
|
120
|
+
}
|
|
121
|
+
return { imported, reinforced }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Format high-confidence instincts for system prompt injection.
|
|
126
|
+
* Returns a prompt section string, or empty string if no qualifying instincts.
|
|
127
|
+
*/
|
|
128
|
+
export async function formatInstinctsForPrompt(cwd, minConfidence = 0.5) {
|
|
129
|
+
const instincts = await listInstincts(cwd, minConfidence)
|
|
130
|
+
if (instincts.length === 0) return ""
|
|
131
|
+
|
|
132
|
+
const lines = [
|
|
133
|
+
"",
|
|
134
|
+
"## Learned Patterns",
|
|
135
|
+
"",
|
|
136
|
+
"These patterns have been observed across your sessions. Follow them unless the user explicitly requests otherwise:",
|
|
137
|
+
""
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
for (const inst of instincts.slice(0, 20)) {
|
|
141
|
+
const conf = inst.confidence.toFixed(1)
|
|
142
|
+
lines.push(`- [${conf}] ${inst.pattern}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (instincts.length > 20) {
|
|
146
|
+
lines.push(` ... and ${instincts.length - 20} more learned patterns`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return lines.join("\n")
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Simple string similarity (Dice coefficient) ──
|
|
153
|
+
|
|
154
|
+
function similarity(a, b) {
|
|
155
|
+
if (a === b) return 1
|
|
156
|
+
if (a.length < 2 || b.length < 2) return 0
|
|
157
|
+
const bigrams = new Map()
|
|
158
|
+
for (let i = 0; i < a.length - 1; i++) {
|
|
159
|
+
const bi = a.slice(i, i + 2)
|
|
160
|
+
bigrams.set(bi, (bigrams.get(bi) || 0) + 1)
|
|
161
|
+
}
|
|
162
|
+
let matches = 0
|
|
163
|
+
for (let i = 0; i < b.length - 1; i++) {
|
|
164
|
+
const bi = b.slice(i, i + 2)
|
|
165
|
+
const count = bigrams.get(bi) || 0
|
|
166
|
+
if (count > 0) {
|
|
167
|
+
matches++
|
|
168
|
+
bigrams.set(bi, count - 1)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return (2 * matches) / (a.length + b.length - 2)
|
|
172
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { access, readFile } from "node:fs/promises"
|
|
3
|
+
|
|
4
|
+
const CANDIDATES = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md", "KKCODE.md", ".kkcode.md", "kkcode.md"]
|
|
5
|
+
|
|
6
|
+
async function exists(file) {
|
|
7
|
+
try {
|
|
8
|
+
await access(file)
|
|
9
|
+
return true
|
|
10
|
+
} catch {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function loadInstructions(cwd = process.cwd()) {
|
|
16
|
+
const blocks = []
|
|
17
|
+
for (const file of CANDIDATES) {
|
|
18
|
+
const target = path.join(cwd, file)
|
|
19
|
+
if (!(await exists(target))) continue
|
|
20
|
+
const content = (await readFile(target, "utf8")).trim()
|
|
21
|
+
if (!content) continue
|
|
22
|
+
blocks.push(`Instructions from ${target}\n${content}`)
|
|
23
|
+
}
|
|
24
|
+
return blocks
|
|
25
|
+
}
|