@kkelly-offical/kkcode 0.1.6 → 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.
- package/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2929
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +17 -4
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
|
@@ -1,390 +1,396 @@
|
|
|
1
|
-
import { ProviderError } from "../core/errors.mjs"
|
|
2
|
-
import { requestWithRetry } from "./retry-policy.mjs"
|
|
3
|
-
import { parseSSE } from "./sse.mjs"
|
|
4
|
-
|
|
5
|
-
function sleep(ms) {
|
|
6
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function mapTools(tools) {
|
|
10
|
-
if (!tools || !tools.length) return []
|
|
11
|
-
const mapped = tools.map((tool) => ({
|
|
12
|
-
name: tool.name,
|
|
13
|
-
description: tool.description,
|
|
14
|
-
input_schema: tool.inputSchema
|
|
15
|
-
}))
|
|
16
|
-
// Cache the tool definitions (they rarely change within a session)
|
|
17
|
-
if (mapped.length > 0) {
|
|
18
|
-
mapped[mapped.length - 1].cache_control = { type: "ephemeral" }
|
|
19
|
-
}
|
|
20
|
-
return mapped
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function systemWithCacheControl(system) {
|
|
24
|
-
if (!system) return undefined
|
|
25
|
-
|
|
26
|
-
// Structured format from buildSystemPromptBlocks: { text, blocks }
|
|
27
|
-
// Strategy: merge all stable content into ONE block with cache_control,
|
|
28
|
-
// keeping dynamic content separate. Combined with the tool breakpoint in
|
|
29
|
-
// mapTools, this gives us 2 breakpoints total — well within the 4-max limit
|
|
30
|
-
// and ensures the cumulative prefix easily exceeds the minimum cacheable
|
|
31
|
-
// threshold (4096 tokens for Opus, 1024 for Sonnet).
|
|
32
|
-
if (system.blocks && Array.isArray(system.blocks)) {
|
|
33
|
-
const stableParts = []
|
|
34
|
-
const dynamicParts = []
|
|
35
|
-
for (const block of system.blocks) {
|
|
36
|
-
if (block.cacheable === false) {
|
|
37
|
-
dynamicParts.push(block.text)
|
|
38
|
-
} else {
|
|
39
|
-
stableParts.push(block.text)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const contentBlocks = []
|
|
44
|
-
if (stableParts.length) {
|
|
45
|
-
contentBlocks.push({
|
|
46
|
-
type: "text",
|
|
47
|
-
text: stableParts.join("\n\n"),
|
|
48
|
-
cache_control: { type: "ephemeral" }
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
if (dynamicParts.length) {
|
|
52
|
-
contentBlocks.push({ type: "text", text: dynamicParts.join("\n\n") })
|
|
53
|
-
}
|
|
54
|
-
return contentBlocks.length ? contentBlocks : undefined
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Legacy: plain string
|
|
58
|
-
if (typeof system === "string") {
|
|
59
|
-
return [{ type: "text", text: system, cache_control: { type: "ephemeral" } }]
|
|
60
|
-
}
|
|
61
|
-
// Legacy: array of strings/blocks
|
|
62
|
-
if (Array.isArray(system)) {
|
|
63
|
-
const blocks = system.map((b) => (typeof b === "string" ? { type: "text", text: b } : { ...b }))
|
|
64
|
-
if (blocks.length > 0) {
|
|
65
|
-
blocks[blocks.length - 1].cache_control = { type: "ephemeral" }
|
|
66
|
-
}
|
|
67
|
-
return blocks
|
|
68
|
-
}
|
|
69
|
-
return system
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function mapContentBlock(block) {
|
|
73
|
-
if (block.type === "image" && block.data) {
|
|
74
|
-
return {
|
|
75
|
-
type: "image",
|
|
76
|
-
source: {
|
|
77
|
-
type: "base64",
|
|
78
|
-
media_type: block.mediaType || "image/png",
|
|
79
|
-
data: block.data
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// Native Anthropic tool_use block — pass through
|
|
84
|
-
if (block.type === "tool_use") {
|
|
85
|
-
return { type: "tool_use", id: block.id, name: block.name, input: block.input || {} }
|
|
86
|
-
}
|
|
87
|
-
// Native Anthropic tool_result block — pass through
|
|
88
|
-
if (block.type === "tool_result") {
|
|
89
|
-
return {
|
|
90
|
-
type: "tool_result",
|
|
91
|
-
tool_use_id: block.tool_use_id,
|
|
92
|
-
content: String(block.content || ""),
|
|
93
|
-
...(block.is_error ? { is_error: true } : {})
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return { type: "text", text: String(block.text || block.content || "") }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function mapMessages(messages) {
|
|
100
|
-
const mapped = messages.map((message) => {
|
|
101
|
-
const role = message.role === "assistant" ? "assistant" : "user"
|
|
102
|
-
const content = message.content
|
|
103
|
-
if (Array.isArray(content)) {
|
|
104
|
-
return { role, content: content.map(mapContentBlock) }
|
|
105
|
-
}
|
|
106
|
-
return { role, content: String(content || "") }
|
|
107
|
-
})
|
|
108
|
-
// Add cache_control to last user message for multi-turn caching
|
|
109
|
-
for (let i = mapped.length - 1; i >= 0; i--) {
|
|
110
|
-
if (mapped[i].role === "user") {
|
|
111
|
-
const c = mapped[i].content
|
|
112
|
-
if (Array.isArray(c) && c.length) {
|
|
113
|
-
c[c.length - 1].cache_control = { type: "ephemeral" }
|
|
114
|
-
} else if (typeof c === "string") {
|
|
115
|
-
mapped[i].content = [{ type: "text", text: c, cache_control: { type: "ephemeral" } }]
|
|
116
|
-
}
|
|
117
|
-
break
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return mapped
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function parseContentBlocks(content) {
|
|
124
|
-
const blocks = Array.isArray(content) ? content : []
|
|
125
|
-
const text = blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n")
|
|
126
|
-
const toolCalls = blocks
|
|
127
|
-
.filter((block) => block.type === "tool_use" && block.name)
|
|
128
|
-
.map((block) => ({
|
|
129
|
-
id: block.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
130
|
-
name: block.name,
|
|
131
|
-
args: block.input || {}
|
|
132
|
-
}))
|
|
133
|
-
return { text, toolCalls }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function timeoutSignal(ms, parentSignal = null) {
|
|
137
|
-
const own = AbortSignal.timeout(ms)
|
|
138
|
-
if (!parentSignal) return own
|
|
139
|
-
return AbortSignal.any([parentSignal, own])
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export async function requestAnthropic(input) {
|
|
143
|
-
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, maxTokens = 16384, retry = {}, signal = null } = input
|
|
144
|
-
if (!apiKey) {
|
|
145
|
-
throw new ProviderError(`missing API key for anthropic provider (env: ${input.apiKeyEnv || "unknown"})`, {
|
|
146
|
-
provider: "anthropic"
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const endpoint = `${baseUrl.replace(/\/$/, "")}/messages`
|
|
151
|
-
const mappedTools = mapTools(tools)
|
|
152
|
-
const payload = {
|
|
153
|
-
model,
|
|
154
|
-
max_tokens: maxTokens,
|
|
155
|
-
metadata: { user_id: "kkcode" },
|
|
156
|
-
system: systemWithCacheControl(system),
|
|
157
|
-
messages: mapMessages(messages),
|
|
158
|
-
tools: mappedTools.length ? mappedTools : undefined
|
|
159
|
-
}
|
|
160
|
-
if (input.thinking?.type) {
|
|
161
|
-
payload.thinking = { type: input.thinking.type, budget_tokens: input.thinking.budget_tokens || 10000 }
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return requestWithRetry({
|
|
165
|
-
attempts: Number(retry.attempts ?? 3),
|
|
166
|
-
baseDelayMs: Number(retry.baseDelayMs ?? 800),
|
|
167
|
-
signal,
|
|
168
|
-
execute: async () => {
|
|
169
|
-
const response = await fetch(endpoint, {
|
|
170
|
-
method: "POST",
|
|
171
|
-
headers: {
|
|
172
|
-
"content-type": "application/json",
|
|
173
|
-
"x-api-key": apiKey,
|
|
174
|
-
"anthropic-version": "2023-06-01",
|
|
175
|
-
"anthropic-beta": "prompt-caching-2024-07-31"
|
|
176
|
-
},
|
|
177
|
-
body: JSON.stringify(payload),
|
|
178
|
-
signal: timeoutSignal(timeoutMs, signal)
|
|
179
|
-
})
|
|
180
|
-
if (!response.ok) {
|
|
181
|
-
const text = await response.text().catch(() => "")
|
|
182
|
-
const error = new ProviderError(`anthropic request failed: ${response.status} ${text}`, {
|
|
183
|
-
provider: "anthropic",
|
|
184
|
-
model,
|
|
185
|
-
endpoint
|
|
186
|
-
})
|
|
187
|
-
error.httpStatus = response.status
|
|
188
|
-
throw error
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
1
|
+
import { ProviderError } from "../core/errors.mjs"
|
|
2
|
+
import { requestWithRetry } from "./retry-policy.mjs"
|
|
3
|
+
import { parseSSE } from "./sse.mjs"
|
|
4
|
+
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function mapTools(tools) {
|
|
10
|
+
if (!tools || !tools.length) return []
|
|
11
|
+
const mapped = tools.map((tool) => ({
|
|
12
|
+
name: tool.name,
|
|
13
|
+
description: tool.description,
|
|
14
|
+
input_schema: tool.inputSchema
|
|
15
|
+
}))
|
|
16
|
+
// Cache the tool definitions (they rarely change within a session)
|
|
17
|
+
if (mapped.length > 0) {
|
|
18
|
+
mapped[mapped.length - 1].cache_control = { type: "ephemeral" }
|
|
19
|
+
}
|
|
20
|
+
return mapped
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function systemWithCacheControl(system) {
|
|
24
|
+
if (!system) return undefined
|
|
25
|
+
|
|
26
|
+
// Structured format from buildSystemPromptBlocks: { text, blocks }
|
|
27
|
+
// Strategy: merge all stable content into ONE block with cache_control,
|
|
28
|
+
// keeping dynamic content separate. Combined with the tool breakpoint in
|
|
29
|
+
// mapTools, this gives us 2 breakpoints total — well within the 4-max limit
|
|
30
|
+
// and ensures the cumulative prefix easily exceeds the minimum cacheable
|
|
31
|
+
// threshold (4096 tokens for Opus, 1024 for Sonnet).
|
|
32
|
+
if (system.blocks && Array.isArray(system.blocks)) {
|
|
33
|
+
const stableParts = []
|
|
34
|
+
const dynamicParts = []
|
|
35
|
+
for (const block of system.blocks) {
|
|
36
|
+
if (block.cacheable === false) {
|
|
37
|
+
dynamicParts.push(block.text)
|
|
38
|
+
} else {
|
|
39
|
+
stableParts.push(block.text)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const contentBlocks = []
|
|
44
|
+
if (stableParts.length) {
|
|
45
|
+
contentBlocks.push({
|
|
46
|
+
type: "text",
|
|
47
|
+
text: stableParts.join("\n\n"),
|
|
48
|
+
cache_control: { type: "ephemeral" }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
if (dynamicParts.length) {
|
|
52
|
+
contentBlocks.push({ type: "text", text: dynamicParts.join("\n\n") })
|
|
53
|
+
}
|
|
54
|
+
return contentBlocks.length ? contentBlocks : undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Legacy: plain string
|
|
58
|
+
if (typeof system === "string") {
|
|
59
|
+
return [{ type: "text", text: system, cache_control: { type: "ephemeral" } }]
|
|
60
|
+
}
|
|
61
|
+
// Legacy: array of strings/blocks
|
|
62
|
+
if (Array.isArray(system)) {
|
|
63
|
+
const blocks = system.map((b) => (typeof b === "string" ? { type: "text", text: b } : { ...b }))
|
|
64
|
+
if (blocks.length > 0) {
|
|
65
|
+
blocks[blocks.length - 1].cache_control = { type: "ephemeral" }
|
|
66
|
+
}
|
|
67
|
+
return blocks
|
|
68
|
+
}
|
|
69
|
+
return system
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mapContentBlock(block) {
|
|
73
|
+
if (block.type === "image" && block.data) {
|
|
74
|
+
return {
|
|
75
|
+
type: "image",
|
|
76
|
+
source: {
|
|
77
|
+
type: "base64",
|
|
78
|
+
media_type: block.mediaType || "image/png",
|
|
79
|
+
data: block.data
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Native Anthropic tool_use block — pass through
|
|
84
|
+
if (block.type === "tool_use") {
|
|
85
|
+
return { type: "tool_use", id: block.id, name: block.name, input: block.input || {} }
|
|
86
|
+
}
|
|
87
|
+
// Native Anthropic tool_result block — pass through
|
|
88
|
+
if (block.type === "tool_result") {
|
|
89
|
+
return {
|
|
90
|
+
type: "tool_result",
|
|
91
|
+
tool_use_id: block.tool_use_id,
|
|
92
|
+
content: String(block.content || ""),
|
|
93
|
+
...(block.is_error ? { is_error: true } : {})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { type: "text", text: String(block.text || block.content || "") }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mapMessages(messages) {
|
|
100
|
+
const mapped = messages.map((message) => {
|
|
101
|
+
const role = message.role === "assistant" ? "assistant" : "user"
|
|
102
|
+
const content = message.content
|
|
103
|
+
if (Array.isArray(content)) {
|
|
104
|
+
return { role, content: content.map(mapContentBlock) }
|
|
105
|
+
}
|
|
106
|
+
return { role, content: String(content || "") }
|
|
107
|
+
})
|
|
108
|
+
// Add cache_control to last user message for multi-turn caching
|
|
109
|
+
for (let i = mapped.length - 1; i >= 0; i--) {
|
|
110
|
+
if (mapped[i].role === "user") {
|
|
111
|
+
const c = mapped[i].content
|
|
112
|
+
if (Array.isArray(c) && c.length) {
|
|
113
|
+
c[c.length - 1].cache_control = { type: "ephemeral" }
|
|
114
|
+
} else if (typeof c === "string") {
|
|
115
|
+
mapped[i].content = [{ type: "text", text: c, cache_control: { type: "ephemeral" } }]
|
|
116
|
+
}
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return mapped
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseContentBlocks(content) {
|
|
124
|
+
const blocks = Array.isArray(content) ? content : []
|
|
125
|
+
const text = blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n")
|
|
126
|
+
const toolCalls = blocks
|
|
127
|
+
.filter((block) => block.type === "tool_use" && block.name)
|
|
128
|
+
.map((block) => ({
|
|
129
|
+
id: block.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
130
|
+
name: block.name,
|
|
131
|
+
args: block.input || {}
|
|
132
|
+
}))
|
|
133
|
+
return { text, toolCalls }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function timeoutSignal(ms, parentSignal = null) {
|
|
137
|
+
const own = AbortSignal.timeout(ms)
|
|
138
|
+
if (!parentSignal) return own
|
|
139
|
+
return AbortSignal.any([parentSignal, own])
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function requestAnthropic(input) {
|
|
143
|
+
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, maxTokens = 16384, retry = {}, signal = null } = input
|
|
144
|
+
if (!apiKey) {
|
|
145
|
+
throw new ProviderError(`missing API key for anthropic provider (env: ${input.apiKeyEnv || "unknown"})`, {
|
|
146
|
+
provider: "anthropic"
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/messages`
|
|
151
|
+
const mappedTools = mapTools(tools)
|
|
152
|
+
const payload = {
|
|
153
|
+
model,
|
|
154
|
+
max_tokens: maxTokens,
|
|
155
|
+
metadata: { user_id: "kkcode" },
|
|
156
|
+
system: systemWithCacheControl(system),
|
|
157
|
+
messages: mapMessages(messages),
|
|
158
|
+
tools: mappedTools.length ? mappedTools : undefined
|
|
159
|
+
}
|
|
160
|
+
if (input.thinking?.type) {
|
|
161
|
+
payload.thinking = { type: input.thinking.type, budget_tokens: input.thinking.budget_tokens || 10000 }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return requestWithRetry({
|
|
165
|
+
attempts: Number(retry.attempts ?? 3),
|
|
166
|
+
baseDelayMs: Number(retry.baseDelayMs ?? 800),
|
|
167
|
+
signal,
|
|
168
|
+
execute: async () => {
|
|
169
|
+
const response = await fetch(endpoint, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: {
|
|
172
|
+
"content-type": "application/json",
|
|
173
|
+
"x-api-key": apiKey,
|
|
174
|
+
"anthropic-version": "2023-06-01",
|
|
175
|
+
"anthropic-beta": "prompt-caching-2024-07-31"
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify(payload),
|
|
178
|
+
signal: timeoutSignal(timeoutMs, signal)
|
|
179
|
+
})
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
const text = await response.text().catch(() => "")
|
|
182
|
+
const error = new ProviderError(`anthropic request failed: ${response.status} ${text}`, {
|
|
183
|
+
provider: "anthropic",
|
|
184
|
+
model,
|
|
185
|
+
endpoint
|
|
186
|
+
})
|
|
187
|
+
error.httpStatus = response.status
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
190
|
+
let json
|
|
191
|
+
try {
|
|
192
|
+
json = await response.json()
|
|
193
|
+
} catch (parseErr) {
|
|
194
|
+
throw new ProviderError(`anthropic response JSON parse failed: ${parseErr.message}`, { provider: "anthropic", model, endpoint })
|
|
195
|
+
}
|
|
196
|
+
const parsed = parseContentBlocks(json?.content)
|
|
197
|
+
const usage = {
|
|
198
|
+
input: json?.usage?.input_tokens ?? 0,
|
|
199
|
+
output: json?.usage?.output_tokens ?? 0,
|
|
200
|
+
cacheRead: json?.usage?.cache_read_input_tokens ?? 0,
|
|
201
|
+
cacheWrite: json?.usage?.cache_creation_input_tokens ?? 0
|
|
202
|
+
}
|
|
203
|
+
return { text: parsed.text, usage, toolCalls: parsed.toolCalls }
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function countTokensAnthropic(input) {
|
|
209
|
+
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 10000 } = input
|
|
210
|
+
if (!apiKey) return null
|
|
211
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/messages/count_tokens`
|
|
212
|
+
const mappedTools = mapTools(tools)
|
|
213
|
+
const payload = {
|
|
214
|
+
model,
|
|
215
|
+
system: systemWithCacheControl(system),
|
|
216
|
+
messages: mapMessages(messages),
|
|
217
|
+
tools: mappedTools.length ? mappedTools : undefined
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch(endpoint, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: {
|
|
223
|
+
"content-type": "application/json",
|
|
224
|
+
"x-api-key": apiKey,
|
|
225
|
+
"anthropic-version": "2023-06-01"
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify(payload),
|
|
228
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
229
|
+
})
|
|
230
|
+
if (!res.ok) return null
|
|
231
|
+
const json = await res.json()
|
|
232
|
+
return json?.input_tokens ?? null
|
|
233
|
+
} catch {
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function* requestAnthropicStream(input) {
|
|
239
|
+
const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, streamIdleTimeoutMs = 120000, maxTokens = 16384, retry = {}, signal = null, compaction = null } = input
|
|
240
|
+
if (!apiKey) {
|
|
241
|
+
throw new ProviderError(`missing API key for anthropic provider (env: ${input.apiKeyEnv || "unknown"})`, {
|
|
242
|
+
provider: "anthropic"
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/messages`
|
|
247
|
+
const mappedTools = mapTools(tools)
|
|
248
|
+
const payload = {
|
|
249
|
+
model,
|
|
250
|
+
max_tokens: maxTokens,
|
|
251
|
+
metadata: { user_id: "kkcode" },
|
|
252
|
+
system: systemWithCacheControl(system),
|
|
253
|
+
messages: mapMessages(messages),
|
|
254
|
+
tools: mappedTools.length ? mappedTools : undefined,
|
|
255
|
+
stream: true,
|
|
256
|
+
...(compaction ? { context_management: { edits: [{ type: "compact_20260112", trigger: { tokens: compaction.trigger || 150000 } }] } } : {})
|
|
257
|
+
}
|
|
258
|
+
if (input.thinking?.type) {
|
|
259
|
+
payload.thinking = { type: input.thinking.type, budget_tokens: input.thinking.budget_tokens || 10000 }
|
|
260
|
+
}
|
|
261
|
+
const attempts = Number(retry.attempts ?? 3)
|
|
262
|
+
const baseDelayMs = Number(retry.baseDelayMs ?? 800)
|
|
263
|
+
|
|
264
|
+
let response
|
|
265
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
266
|
+
let connTimer = null
|
|
267
|
+
const connController = new AbortController()
|
|
268
|
+
try {
|
|
269
|
+
// Use a connection-only timeout for the initial fetch.
|
|
270
|
+
// Once headers arrive, clear it — the SSE idle timeout handles the streaming phase.
|
|
271
|
+
connTimer = setTimeout(() => connController.abort(), timeoutMs)
|
|
272
|
+
const fetchSignal = signal
|
|
273
|
+
? AbortSignal.any([signal, connController.signal])
|
|
274
|
+
: connController.signal
|
|
275
|
+
|
|
276
|
+
response = await fetch(endpoint, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"content-type": "application/json",
|
|
280
|
+
"x-api-key": apiKey,
|
|
281
|
+
"anthropic-version": "2023-06-01",
|
|
282
|
+
"anthropic-beta": compaction ? "prompt-caching-2024-07-31,compact-2026-01-12" : "prompt-caching-2024-07-31"
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify(payload),
|
|
285
|
+
signal: fetchSignal
|
|
286
|
+
})
|
|
287
|
+
clearTimeout(connTimer)
|
|
288
|
+
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
const text = await response.text().catch(() => "")
|
|
291
|
+
const error = new ProviderError(`anthropic stream failed: ${response.status} ${text}`, {
|
|
292
|
+
provider: "anthropic", model, endpoint
|
|
293
|
+
})
|
|
294
|
+
error.httpStatus = response.status
|
|
295
|
+
throw error
|
|
296
|
+
}
|
|
297
|
+
break
|
|
298
|
+
} catch (err) {
|
|
299
|
+
clearTimeout(connTimer)
|
|
300
|
+
if (signal?.aborted) throw err
|
|
301
|
+
const isNetwork = err?.code === "ETIMEDOUT" || err?.code === "ECONNRESET" || err?.name === "AbortError"
|
|
302
|
+
if (!isNetwork || attempt >= attempts) throw err
|
|
303
|
+
await sleep(baseDelayMs * Math.pow(2, attempt - 1))
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let currentBlock = null
|
|
308
|
+
let inputUsage = { input: 0, cacheRead: 0, cacheWrite: 0 }
|
|
309
|
+
let outputTokens = 0
|
|
310
|
+
let stopReason = null
|
|
311
|
+
|
|
312
|
+
for await (const { event, data } of parseSSE(response.body, signal, { idleTimeoutMs: streamIdleTimeoutMs })) {
|
|
313
|
+
let parsed
|
|
314
|
+
try { parsed = JSON.parse(data) } catch { continue }
|
|
315
|
+
|
|
316
|
+
if (event === "message_start") {
|
|
317
|
+
const u = parsed.message?.usage
|
|
318
|
+
inputUsage.input = u?.input_tokens ?? 0
|
|
319
|
+
inputUsage.cacheRead = u?.cache_read_input_tokens ?? 0
|
|
320
|
+
inputUsage.cacheWrite = u?.cache_creation_input_tokens ?? 0
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (event === "content_block_start") {
|
|
324
|
+
const block = parsed.content_block
|
|
325
|
+
currentBlock = {
|
|
326
|
+
type: block?.type,
|
|
327
|
+
id: block?.id || null,
|
|
328
|
+
name: block?.name || null,
|
|
329
|
+
jsonParts: []
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (event === "content_block_delta") {
|
|
334
|
+
if (parsed.delta?.type === "text_delta") {
|
|
335
|
+
const text = parsed.delta.text || ""
|
|
336
|
+
if (text) yield { type: "text", content: text }
|
|
337
|
+
}
|
|
338
|
+
if (parsed.delta?.type === "thinking_delta" && currentBlock?.type !== "redacted_thinking") {
|
|
339
|
+
const thinking = parsed.delta.thinking || ""
|
|
340
|
+
if (thinking) yield { type: "thinking", content: thinking }
|
|
341
|
+
}
|
|
342
|
+
if (parsed.delta?.type === "input_json_delta") {
|
|
343
|
+
if (currentBlock) currentBlock.jsonParts.push(parsed.delta.partial_json || "")
|
|
344
|
+
}
|
|
345
|
+
if (parsed.delta?.type === "compaction_delta") {
|
|
346
|
+
if (currentBlock) currentBlock.compactionContent = parsed.delta.content || ""
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (event === "content_block_stop" && currentBlock) {
|
|
351
|
+
if (currentBlock.type === "tool_use") {
|
|
352
|
+
const raw = currentBlock.jsonParts.join("") || "{}"
|
|
353
|
+
let args = {}
|
|
354
|
+
try {
|
|
355
|
+
args = JSON.parse(raw)
|
|
356
|
+
} catch (parseErr) {
|
|
357
|
+
console.error(`[anthropic] tool_call JSON parse failed for "${currentBlock.name}": ${parseErr.message} (${raw.length} chars, first 200: ${raw.slice(0, 200)})`)
|
|
358
|
+
args = { __parse_error: true, __raw_length: raw.length, __error: parseErr.message }
|
|
359
|
+
}
|
|
360
|
+
yield {
|
|
361
|
+
type: "tool_call",
|
|
362
|
+
call: {
|
|
363
|
+
id: currentBlock.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
364
|
+
name: currentBlock.name,
|
|
365
|
+
args
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (currentBlock.type === "compaction") {
|
|
370
|
+
yield { type: "compaction", content: currentBlock.compactionContent || "" }
|
|
371
|
+
}
|
|
372
|
+
currentBlock = null
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (event === "message_delta") {
|
|
376
|
+
outputTokens = parsed.usage?.output_tokens ?? outputTokens
|
|
377
|
+
if (parsed.delta?.stop_reason) {
|
|
378
|
+
stopReason = parsed.delta.stop_reason
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (event === "message_stop") {
|
|
383
|
+
yield {
|
|
384
|
+
type: "usage",
|
|
385
|
+
usage: {
|
|
386
|
+
input: inputUsage.input,
|
|
387
|
+
output: outputTokens,
|
|
388
|
+
cacheRead: inputUsage.cacheRead,
|
|
389
|
+
cacheWrite: inputUsage.cacheWrite
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Normalize: "end_turn" → "end_turn", "max_tokens" → "max_tokens", "tool_use" → "tool_use"
|
|
393
|
+
yield { type: "stop", reason: stopReason || "end_turn" }
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|