freddie 0.0.41

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 (307) hide show
  1. package/AGENTS.md +180 -0
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +130 -0
  4. package/bin/freddie.js +116 -0
  5. package/package.json +59 -0
  6. package/skills/creative/README.md +3 -0
  7. package/skills/creative/architecture-diagram/SKILL.md +52 -0
  8. package/skills/creative/ascii-video/SKILL.md +60 -0
  9. package/skills/creative/concept-diagrams/SKILL.md +65 -0
  10. package/skills/data/README.md +3 -0
  11. package/skills/data/etl-pipelines/SKILL.md +60 -0
  12. package/skills/data/sql-explainer/SKILL.md +60 -0
  13. package/skills/ops/README.md +3 -0
  14. package/skills/ops/incident-response/SKILL.md +74 -0
  15. package/skills/ops/log-triage/SKILL.md +79 -0
  16. package/skills/planning/README.md +3 -0
  17. package/skills/planning/okr-drafter/SKILL.md +60 -0
  18. package/skills/planning/weekly-review/SKILL.md +64 -0
  19. package/skills/software-development/README.md +3 -0
  20. package/skills/software-development/code-review/SKILL.md +70 -0
  21. package/skills/software-development/rfc-writer/SKILL.md +68 -0
  22. package/skills/software-development/systematic-debugging/SKILL.md +80 -0
  23. package/src/acp/auth.js +21 -0
  24. package/src/acp/entry.js +2 -0
  25. package/src/acp/events.js +10 -0
  26. package/src/acp/main.js +8 -0
  27. package/src/acp/permissions.js +29 -0
  28. package/src/acp/server.js +84 -0
  29. package/src/acp/session.js +26 -0
  30. package/src/acp/tools.js +17 -0
  31. package/src/agent/account_usage.js +19 -0
  32. package/src/agent/acptoapi-bridge.js +80 -0
  33. package/src/agent/anthropic_adapter.js +10 -0
  34. package/src/agent/auxiliary_client.js +20 -0
  35. package/src/agent/bedrock_adapter.js +11 -0
  36. package/src/agent/codex_responses_adapter.js +10 -0
  37. package/src/agent/compress/compressor.js +55 -0
  38. package/src/agent/compress/fallback.js +14 -0
  39. package/src/agent/compress/index.js +6 -0
  40. package/src/agent/compress/policy.js +47 -0
  41. package/src/agent/compress/prompt.js +46 -0
  42. package/src/agent/compress/prune.js +16 -0
  43. package/src/agent/compress/tokens.js +31 -0
  44. package/src/agent/context_references.js +40 -0
  45. package/src/agent/copilot_acp_client.js +6 -0
  46. package/src/agent/credential_pool.js +30 -0
  47. package/src/agent/credential_sources.js +18 -0
  48. package/src/agent/curator.js +5 -0
  49. package/src/agent/display.js +23 -0
  50. package/src/agent/error_classifier.js +15 -0
  51. package/src/agent/file_safety.js +9 -0
  52. package/src/agent/gemini_cloudcode_adapter.js +9 -0
  53. package/src/agent/gemini_native_adapter.js +11 -0
  54. package/src/agent/gemini_schema.js +19 -0
  55. package/src/agent/google_code_assist.js +8 -0
  56. package/src/agent/google_oauth.js +21 -0
  57. package/src/agent/image_gen_provider.js +8 -0
  58. package/src/agent/image_gen_registry.js +6 -0
  59. package/src/agent/image_routing.js +13 -0
  60. package/src/agent/insights.js +9 -0
  61. package/src/agent/llm_resolver.js +21 -0
  62. package/src/agent/lmstudio_reasoning.js +13 -0
  63. package/src/agent/machine.js +102 -0
  64. package/src/agent/manual_compression_feedback.js +5 -0
  65. package/src/agent/memory_manager.js +14 -0
  66. package/src/agent/memory_provider.js +1 -0
  67. package/src/agent/model_metadata.js +28 -0
  68. package/src/agent/models_dev.js +13 -0
  69. package/src/agent/moonshot_schema.js +11 -0
  70. package/src/agent/oauth_endpoints.js +79 -0
  71. package/src/agent/onboarding.js +16 -0
  72. package/src/agent/pi-bridge.js +37 -0
  73. package/src/agent/prompt_builder.js +12 -0
  74. package/src/agent/prompt_caching.js +24 -0
  75. package/src/agent/rate_limit_tracker.js +12 -0
  76. package/src/agent/redact.js +25 -0
  77. package/src/agent/retry_utils.js +17 -0
  78. package/src/agent/shell_hooks.js +16 -0
  79. package/src/agent/skill_commands.js +16 -0
  80. package/src/agent/skill_preprocessing.js +12 -0
  81. package/src/agent/skill_utils.js +14 -0
  82. package/src/agent/subdirectory_hints.js +17 -0
  83. package/src/agent/title_generator.js +13 -0
  84. package/src/agent/trajectory.js +9 -0
  85. package/src/agent/usage_pricing.js +16 -0
  86. package/src/auth.js +84 -0
  87. package/src/batch.js +32 -0
  88. package/src/cli/auth_commands.js +17 -0
  89. package/src/cli/azure_detect.js +9 -0
  90. package/src/cli/backup.js +17 -0
  91. package/src/cli/banner.js +13 -0
  92. package/src/cli/browser_connect.js +11 -0
  93. package/src/cli/callbacks.js +5 -0
  94. package/src/cli/claw.js +8 -0
  95. package/src/cli/cli_output.js +19 -0
  96. package/src/cli/clipboard.js +24 -0
  97. package/src/cli/codex_models.js +8 -0
  98. package/src/cli/colors.js +13 -0
  99. package/src/cli/completer.js +98 -0
  100. package/src/cli/completion.js +21 -0
  101. package/src/cli/copilot_auth.js +9 -0
  102. package/src/cli/curator_cli.js +5 -0
  103. package/src/cli/curses.js +15 -0
  104. package/src/cli/debug.js +6 -0
  105. package/src/cli/default_soul.js +20 -0
  106. package/src/cli/dingtalk_auth.js +12 -0
  107. package/src/cli/doctor.js +15 -0
  108. package/src/cli/dump.js +11 -0
  109. package/src/cli/env_loader.js +25 -0
  110. package/src/cli/fallback_cmd.js +9 -0
  111. package/src/cli/gateway_cli.js +17 -0
  112. package/src/cli/hooks.js +9 -0
  113. package/src/cli/interactive.js +61 -0
  114. package/src/cli/logs.js +32 -0
  115. package/src/cli/main.js +7 -0
  116. package/src/cli/mcp_config.js +9 -0
  117. package/src/cli/memory_setup.js +12 -0
  118. package/src/cli/model_catalog.js +23 -0
  119. package/src/cli/model_normalize.js +12 -0
  120. package/src/cli/model_switch.js +11 -0
  121. package/src/cli/models.js +13 -0
  122. package/src/cli/nous_subscription.js +12 -0
  123. package/src/cli/oneshot.js +6 -0
  124. package/src/cli/pairing.js +21 -0
  125. package/src/cli/platforms.js +14 -0
  126. package/src/cli/plugins.js +4 -0
  127. package/src/cli/plugins_cmd.js +21 -0
  128. package/src/cli/profiles_cli.js +6 -0
  129. package/src/cli/providers.js +18 -0
  130. package/src/cli/pty_bridge.js +16 -0
  131. package/src/cli/relaunch.js +7 -0
  132. package/src/cli/runtime_provider.js +9 -0
  133. package/src/cli/setup.js +131 -0
  134. package/src/cli/skills_config.js +6 -0
  135. package/src/cli/skills_hub.js +8 -0
  136. package/src/cli/slack_cli.js +17 -0
  137. package/src/cli/status.js +10 -0
  138. package/src/cli/timeouts.js +5 -0
  139. package/src/cli/tips.js +14 -0
  140. package/src/cli/tools_config.js +15 -0
  141. package/src/cli/uninstall.js +8 -0
  142. package/src/cli/vercel_auth.js +13 -0
  143. package/src/cli/voice.js +6 -0
  144. package/src/cli/web_server.js +13 -0
  145. package/src/cli/webhook.js +12 -0
  146. package/src/commands/profile.js +72 -0
  147. package/src/commands/registry.js +94 -0
  148. package/src/config.js +125 -0
  149. package/src/context/engine.js +42 -0
  150. package/src/cron/cron-parse.js +27 -0
  151. package/src/cron/scheduler.js +63 -0
  152. package/src/db.js +178 -0
  153. package/src/gateway/base.js +13 -0
  154. package/src/gateway/builtin_hooks/boot.js +5 -0
  155. package/src/gateway/builtin_hooks/broadcast.js +3 -0
  156. package/src/gateway/builtin_hooks/deny.js +6 -0
  157. package/src/gateway/builtin_hooks/index.js +17 -0
  158. package/src/gateway/builtin_hooks/presence.js +4 -0
  159. package/src/gateway/builtin_hooks/routing.js +7 -0
  160. package/src/gateway/helpers.js +27 -0
  161. package/src/gateway/platforms/api_server.js +21 -0
  162. package/src/gateway/platforms/bluebubbles.js +32 -0
  163. package/src/gateway/platforms/dingtalk.js +32 -0
  164. package/src/gateway/platforms/discord.js +24 -0
  165. package/src/gateway/platforms/email.js +51 -0
  166. package/src/gateway/platforms/feishu.js +32 -0
  167. package/src/gateway/platforms/feishu_comment.js +12 -0
  168. package/src/gateway/platforms/feishu_comment_rules.js +11 -0
  169. package/src/gateway/platforms/homeassistant.js +32 -0
  170. package/src/gateway/platforms/matrix.js +40 -0
  171. package/src/gateway/platforms/mattermost.js +29 -0
  172. package/src/gateway/platforms/qqbot.js +32 -0
  173. package/src/gateway/platforms/signal.js +33 -0
  174. package/src/gateway/platforms/slack.js +34 -0
  175. package/src/gateway/platforms/sms.js +34 -0
  176. package/src/gateway/platforms/telegram.js +38 -0
  177. package/src/gateway/platforms/telegram_network.js +17 -0
  178. package/src/gateway/platforms/webhook.js +19 -0
  179. package/src/gateway/platforms/wecom.js +32 -0
  180. package/src/gateway/platforms/wecom_callback.js +15 -0
  181. package/src/gateway/platforms/wecom_crypto.js +16 -0
  182. package/src/gateway/platforms/weixin.js +32 -0
  183. package/src/gateway/platforms/whatsapp.js +40 -0
  184. package/src/gateway/platforms/yuanbao.js +9 -0
  185. package/src/gateway/platforms/yuanbao_media.js +5 -0
  186. package/src/gateway/platforms/yuanbao_proto.js +9 -0
  187. package/src/gateway/platforms/yuanbao_sticker.js +6 -0
  188. package/src/gateway/run.js +42 -0
  189. package/src/gateway/service.js +143 -0
  190. package/src/home.js +44 -0
  191. package/src/index.js +47 -0
  192. package/src/mcp/server.js +49 -0
  193. package/src/observability/debug.js +31 -0
  194. package/src/observability/log.js +38 -0
  195. package/src/plugins/achievements/index.js +9 -0
  196. package/src/plugins/cockpit/index.js +8 -0
  197. package/src/plugins/context_engine/index.js +13 -0
  198. package/src/plugins/disk_cleanup/index.js +22 -0
  199. package/src/plugins/google_meet/index.js +19 -0
  200. package/src/plugins/image_gen/index.js +5 -0
  201. package/src/plugins/manager.js +66 -0
  202. package/src/plugins/memory/_index.js +8 -0
  203. package/src/plugins/memory/byterover.js +25 -0
  204. package/src/plugins/memory/hindsight.js +25 -0
  205. package/src/plugins/memory/holographic.js +31 -0
  206. package/src/plugins/memory/honcho.js +25 -0
  207. package/src/plugins/memory/mem0.js +25 -0
  208. package/src/plugins/memory/openviking.js +25 -0
  209. package/src/plugins/memory/provider.js +35 -0
  210. package/src/plugins/memory/retaindb.js +25 -0
  211. package/src/plugins/memory/supermemory.js +25 -0
  212. package/src/plugins/observability/index.js +18 -0
  213. package/src/plugins/platforms/index.js +20 -0
  214. package/src/plugins/spotify/index.js +22 -0
  215. package/src/rl/atropos.js +22 -0
  216. package/src/rl/cli.js +18 -0
  217. package/src/sessions.js +84 -0
  218. package/src/skills/index.js +49 -0
  219. package/src/skin/engine.js +81 -0
  220. package/src/swe/runner.js +26 -0
  221. package/src/time.js +25 -0
  222. package/src/tools/ansi_strip.js +8 -0
  223. package/src/tools/approval.js +15 -0
  224. package/src/tools/bash.js +35 -0
  225. package/src/tools/binary_extensions.js +22 -0
  226. package/src/tools/browser.js +48 -0
  227. package/src/tools/budget_config.js +13 -0
  228. package/src/tools/checkpoint.js +29 -0
  229. package/src/tools/clarify.js +15 -0
  230. package/src/tools/code_execution.js +27 -0
  231. package/src/tools/credential_files.js +16 -0
  232. package/src/tools/cronjob.js +16 -0
  233. package/src/tools/debug_helpers.js +9 -0
  234. package/src/tools/delegate.js +28 -0
  235. package/src/tools/discord_tool.js +13 -0
  236. package/src/tools/edit.js +31 -0
  237. package/src/tools/env_passthrough.js +15 -0
  238. package/src/tools/environments/base.js +26 -0
  239. package/src/tools/environments/daytona.js +48 -0
  240. package/src/tools/environments/docker.js +14 -0
  241. package/src/tools/environments/file_sync.js +60 -0
  242. package/src/tools/environments/index.js +36 -0
  243. package/src/tools/environments/local.js +31 -0
  244. package/src/tools/environments/modal.js +33 -0
  245. package/src/tools/environments/singularity.js +38 -0
  246. package/src/tools/environments/ssh.js +14 -0
  247. package/src/tools/environments/vercel_sandbox.js +47 -0
  248. package/src/tools/feishu_doc.js +15 -0
  249. package/src/tools/feishu_drive.js +14 -0
  250. package/src/tools/file_operations.js +17 -0
  251. package/src/tools/file_state.js +16 -0
  252. package/src/tools/file_tools.js +23 -0
  253. package/src/tools/fuzzy_match.js +8 -0
  254. package/src/tools/grep.js +51 -0
  255. package/src/tools/homeassistant_tool.js +15 -0
  256. package/src/tools/image_gen.js +33 -0
  257. package/src/tools/interrupt.js +18 -0
  258. package/src/tools/managed_tool_gateway.js +11 -0
  259. package/src/tools/mcp_oauth.js +21 -0
  260. package/src/tools/mcp_oauth_manager.js +20 -0
  261. package/src/tools/mcp_tool.js +36 -0
  262. package/src/tools/memory.js +66 -0
  263. package/src/tools/mixture_of_agents.js +14 -0
  264. package/src/tools/neutts_synth.js +13 -0
  265. package/src/tools/openrouter_client.js +13 -0
  266. package/src/tools/osv_check.js +11 -0
  267. package/src/tools/patch_parser.js +42 -0
  268. package/src/tools/path_security.js +16 -0
  269. package/src/tools/process_registry.js +17 -0
  270. package/src/tools/read.js +26 -0
  271. package/src/tools/registry.js +54 -0
  272. package/src/tools/rl_training.js +13 -0
  273. package/src/tools/schema_sanitizer.js +18 -0
  274. package/src/tools/send_message.js +32 -0
  275. package/src/tools/session_search.js +23 -0
  276. package/src/tools/skill_manager.js +17 -0
  277. package/src/tools/skill_usage.js +20 -0
  278. package/src/tools/skills_guard.js +17 -0
  279. package/src/tools/skills_hub.js +31 -0
  280. package/src/tools/skills_index.js +14 -0
  281. package/src/tools/skills_sync.js +19 -0
  282. package/src/tools/skills_tool.js +11 -0
  283. package/src/tools/slash_confirm.js +16 -0
  284. package/src/tools/terminal.js +29 -0
  285. package/src/tools/tirith_security.js +25 -0
  286. package/src/tools/todo.js +54 -0
  287. package/src/tools/tool_backend_helpers.js +26 -0
  288. package/src/tools/tool_output_limits.js +15 -0
  289. package/src/tools/tool_result_storage.js +20 -0
  290. package/src/tools/transcription.js +19 -0
  291. package/src/tools/tts.js +19 -0
  292. package/src/tools/url_safety.js +15 -0
  293. package/src/tools/vision.js +18 -0
  294. package/src/tools/voice_mode.js +10 -0
  295. package/src/tools/web_search.js +37 -0
  296. package/src/tools/web_tools.js +18 -0
  297. package/src/tools/website_policy.js +14 -0
  298. package/src/tools/write.js +25 -0
  299. package/src/tools/xai_http.js +13 -0
  300. package/src/tools/yuanbao_tools.js +13 -0
  301. package/src/toolset_distributions.js +18 -0
  302. package/src/toolsets.js +26 -0
  303. package/src/tui/index.js +26 -0
  304. package/src/utils.js +54 -0
  305. package/src/web/app.js +547 -0
  306. package/src/web/index.html +167 -0
  307. package/src/web/server.js +109 -0
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: systematic-debugging
3
+ description: "Apply systematic debugging methodology: reproduction, hypothesis, experiment, root cause, fix verification"
4
+ category: software-development
5
+ ---
6
+
7
+ # Systematic Debugging
8
+
9
+ Do not guess. Form hypotheses, design experiments, eliminate candidates, converge on root cause through observation.
10
+
11
+ ## The debugging loop
12
+
13
+ ```
14
+ OBSERVE → HYPOTHESISE → EXPERIMENT → ELIMINATE → CONVERGE
15
+ ```
16
+
17
+ Never skip to EXPERIMENT without OBSERVE. Never fix without CONVERGE.
18
+
19
+ ## Phase 1: OBSERVE — reproduce the bug
20
+
21
+ 1. Write a **reproduction recipe** — exact steps, inputs, environment.
22
+ 2. Confirm **deterministic** (always fails) vs **intermittent** (% failure rate).
23
+ 3. Find the **smallest reproduction** — strip everything not needed to trigger the bug.
24
+ 4. Capture: error message, stack trace, logs, HTTP request/response, query.
25
+
26
+ ## Phase 2: HYPOTHESISE — list candidate causes
27
+
28
+ Write every possible cause. Rank by:
29
+ - **Proximity** to the failure point
30
+ - **Recent changes** (last deploy, last config change)
31
+ - **Complexity** (more complex code = more places to hide bugs)
32
+
33
+ ## Phase 3: EXPERIMENT — test one hypothesis at a time
34
+
35
+ ```
36
+ Hypothesis: Redis is evicting sessions under memory pressure
37
+ Experiment: redis-cli INFO memory | grep used_memory_human; redis-cli MONITOR | grep DEL
38
+ Expected if TRUE: memory near maxmemory, DEL commands on session keys
39
+ Expected if FALSE: memory headroom, no unexpected DELs
40
+ Result: ...
41
+ ```
42
+
43
+ **Never change two things at once.** One variable per experiment.
44
+
45
+ ## Phase 4: ELIMINATE — cross off falsified hypotheses
46
+
47
+ - **FALSIFIED** — evidence rules it out
48
+ - **CONFIRMED** — evidence supports it
49
+ - **INCONCLUSIVE** — need more data
50
+
51
+ Stop when exactly one hypothesis is CONFIRMED and all others FALSIFIED.
52
+
53
+ ## Phase 5: CONVERGE — fix at root cause
54
+
55
+ 1. Write a **failing test** that reproduces the bug.
56
+ 2. Implement the fix.
57
+ 3. Confirm the test now passes.
58
+ 4. Check for **similar bugs** in adjacent code.
59
+
60
+ ## Debugging tools
61
+
62
+ | Situation | Tool |
63
+ |---|---|
64
+ | Slow code path | Profiler (node --prof, py-spy) |
65
+ | Memory leak | Heap snapshot (Chrome DevTools) |
66
+ | Network issue | tcpdump, curl -v |
67
+ | DB slow query | EXPLAIN ANALYZE |
68
+ | Race condition | Thread sanitiser, stress test |
69
+ | Intermittent failure | Log correlation by request ID |
70
+
71
+ ## Rules
72
+
73
+ - **Never assume** — every assumption is an untested hypothesis.
74
+ - **Reproduce first** — if you can't reproduce it, say so.
75
+ - **Binary search** — midpoint log to halve the search space.
76
+ - **Read the error message** — 40% of bugs solved this way.
77
+ - **Check recent changes** — `git log --oneline -20` before anything else.
78
+ - **Take notes** — a session without notes wastes the next session.
79
+
80
+ Describe the bug (symptoms, error, stack trace, recent changes) and I will guide you through systematic diagnosis.
@@ -0,0 +1,21 @@
1
+ import { getAuthStore } from '../auth.js'
2
+ import crypto from 'node:crypto'
3
+ const KEY = 'ACP_SHARED_SECRET'
4
+ export async function getSharedSecret() {
5
+ if (process.env.ACP_SHARED_SECRET) return process.env.ACP_SHARED_SECRET
6
+ const stored = await getAuthStore().getCredential(KEY)
7
+ return stored?.value || null
8
+ }
9
+ export async function setSharedSecret(secret) { return await getAuthStore().setCredential(KEY, secret) }
10
+ export async function rotateSecret() {
11
+ const fresh = crypto.randomBytes(32).toString('hex')
12
+ await setSharedSecret(fresh)
13
+ return fresh
14
+ }
15
+ export async function authenticateRequest(headers) {
16
+ const expected = await getSharedSecret()
17
+ if (!expected) return { ok: true, mode: 'open' }
18
+ const got = headers?.['x-acp-secret'] || headers?.authorization?.replace(/^Bearer\s+/, '')
19
+ if (got && got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) return { ok: true, mode: 'shared-secret' }
20
+ return { ok: false, reason: 'invalid or missing ACP secret' }
21
+ }
@@ -0,0 +1,2 @@
1
+ import { startAcpStdio } from './main.js'
2
+ export function entry() { return startAcpStdio() }
@@ -0,0 +1,10 @@
1
+ export function emitEvent(send, kind, payload) { send({ jsonrpc: '2.0', method: 'event/' + kind, params: payload }) }
2
+ export const Events = {
3
+ toolStart: (send, p) => emitEvent(send, 'tool.start', p),
4
+ toolProgress: (send, p) => emitEvent(send, 'tool.progress', p),
5
+ toolComplete: (send, p) => emitEvent(send, 'tool.complete', p),
6
+ messageDelta: (send, p) => emitEvent(send, 'message.delta', p),
7
+ messageComplete: (send, p) => emitEvent(send, 'message.complete', p),
8
+ permissionRequest: (send, p) => emitEvent(send, 'permission.request', p),
9
+ sessionEnded: (send, p) => emitEvent(send, 'session.ended', p),
10
+ }
@@ -0,0 +1,8 @@
1
+ import { AcpServer } from './server.js'
2
+ export function startAcpStdio() {
3
+ const srv = new AcpServer()
4
+ srv.start()
5
+ process.on('SIGINT', () => { srv.stop(); process.exit(0) })
6
+ process.on('SIGTERM', () => { srv.stop(); process.exit(0) })
7
+ return srv
8
+ }
@@ -0,0 +1,29 @@
1
+ import { getConfigValue } from '../config.js'
2
+
3
+ const _allowed = new Map()
4
+ const _denied = new Map()
5
+
6
+ export function isAlwaysAllow(tool) {
7
+ const list = getConfigValue('acp.always_allow', []) || []
8
+ return list.includes(tool)
9
+ }
10
+ export function isAlwaysDeny(tool) {
11
+ const list = getConfigValue('acp.always_deny', []) || []
12
+ return list.includes(tool)
13
+ }
14
+ export function rememberAllow(sessionId, tool) {
15
+ if (!_allowed.has(sessionId)) _allowed.set(sessionId, new Set())
16
+ _allowed.get(sessionId).add(tool)
17
+ }
18
+ export function rememberDeny(sessionId, tool) {
19
+ if (!_denied.has(sessionId)) _denied.set(sessionId, new Set())
20
+ _denied.get(sessionId).add(tool)
21
+ }
22
+ export function checkPermission(sessionId, tool) {
23
+ if (isAlwaysDeny(tool)) return 'deny'
24
+ if (isAlwaysAllow(tool)) return 'allow'
25
+ if (_denied.get(sessionId)?.has(tool)) return 'deny'
26
+ if (_allowed.get(sessionId)?.has(tool)) return 'allow'
27
+ return 'ask'
28
+ }
29
+ export function resetForTests() { _allowed.clear(); _denied.clear() }
@@ -0,0 +1,84 @@
1
+ import readline from 'node:readline'
2
+ import { EventEmitter } from 'node:events'
3
+ import { registry, discoverBuiltinTools } from '../tools/registry.js'
4
+ import { runTurn } from '../agent/machine.js'
5
+ import { logger } from '../observability/log.js'
6
+ import { Events } from './events.js'
7
+ import { checkPermission, rememberAllow, rememberDeny } from './permissions.js'
8
+ import { AcpSessionManager } from './session.js'
9
+
10
+ const log = logger('acp')
11
+
12
+ const CAPABILITIES = {
13
+ name: 'freddie', version: '0.4.0',
14
+ methods: ['initialize', 'session.new', 'session.resume', 'session.list', 'session.end', 'prompt.submit', 'tool.list', 'permission.respond', 'shutdown'],
15
+ events: ['tool.start', 'tool.progress', 'tool.complete', 'message.delta', 'message.complete', 'permission.request', 'session.ended'],
16
+ }
17
+
18
+ export class AcpServer extends EventEmitter {
19
+ constructor({ stdin = process.stdin, stdout = process.stdout, callLLM = null } = {}) {
20
+ super()
21
+ this.in = stdin; this.out = stdout; this.callLLM = callLLM
22
+ this.sessions = new AcpSessionManager()
23
+ this._pendingPerm = new Map()
24
+ }
25
+ start() {
26
+ const rl = readline.createInterface({ input: this.in, crlfDelay: Infinity })
27
+ rl.on('line', (l) => this.handle(l).catch(e => this.send({ jsonrpc: '2.0', error: { message: String(e) } })))
28
+ this.rl = rl
29
+ }
30
+ stop() { this.rl?.close() }
31
+ send(o) { this.out.write(JSON.stringify(o) + '\n') }
32
+ async handle(line) {
33
+ if (!line.trim()) return
34
+ let req; try { req = JSON.parse(line) } catch { return this.send({ jsonrpc: '2.0', error: { message: 'invalid json' } }) }
35
+ const { id, method, params = {} } = req
36
+ log.info('rpc', { method, id })
37
+ const fn = METHODS[method]
38
+ if (!fn) return this.send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown method: ' + method } })
39
+ try {
40
+ const result = await fn(this, params)
41
+ this.send({ jsonrpc: '2.0', id, result })
42
+ } catch (e) { this.send({ jsonrpc: '2.0', id, error: { message: String(e?.message || e) } }) }
43
+ }
44
+ requestPermission(sessionId, tool) {
45
+ const decided = checkPermission(sessionId, tool)
46
+ if (decided !== 'ask') return Promise.resolve(decided)
47
+ return new Promise((resolve) => {
48
+ const reqId = sessionId + ':' + tool + ':' + Date.now()
49
+ this._pendingPerm.set(reqId, { resolve, sessionId, tool })
50
+ Events.permissionRequest((o) => this.send(o), { reqId, sessionId, tool })
51
+ })
52
+ }
53
+ }
54
+
55
+ const METHODS = {
56
+ initialize: () => CAPABILITIES,
57
+ 'session.new': (srv, params) => srv.sessions.new(params),
58
+ 'session.resume': (srv, { sessionId }) => srv.sessions.resume(sessionId) || (() => { throw new Error('session not found') })(),
59
+ 'session.list': (srv) => srv.sessions.list(),
60
+ 'session.end': (srv, { sessionId }) => { Events.sessionEnded((o) => srv.send(o), { sessionId }); return srv.sessions.end(sessionId) },
61
+ 'tool.list': async () => {
62
+ await discoverBuiltinTools()
63
+ return { tools: registry.list().map(t => ({ name: t.name, toolset: t.toolset, schema: t.schema })) }
64
+ },
65
+ 'permission.respond': (srv, { reqId, decision }) => {
66
+ const pending = srv._pendingPerm.get(reqId)
67
+ if (!pending) return { ok: false, error: 'unknown reqId' }
68
+ srv._pendingPerm.delete(reqId)
69
+ if (decision === 'allow' || decision === 'always_allow') rememberAllow(pending.sessionId, pending.tool)
70
+ if (decision === 'deny' || decision === 'always_deny') rememberDeny(pending.sessionId, pending.tool)
71
+ pending.resolve(decision === 'allow' || decision === 'always_allow' ? 'allow' : 'deny')
72
+ return { ok: true }
73
+ },
74
+ 'prompt.submit': async (srv, { sessionId, prompt }) => {
75
+ if (!srv.sessions.isActive(sessionId)) throw new Error('session not active')
76
+ srv.sessions.appendUser(sessionId, prompt)
77
+ Events.messageDelta((o) => srv.send(o), { sessionId, role: 'user', content: prompt })
78
+ const out = await runTurn({ prompt, callLLM: srv.callLLM })
79
+ srv.sessions.appendAssistant(sessionId, out.result || '')
80
+ Events.messageComplete((o) => srv.send(o), { sessionId, role: 'assistant', content: out.result || '' })
81
+ return { result: out.result, error: out.error, iterations: out.iterations }
82
+ },
83
+ shutdown: (srv) => { srv.stop(); return { ok: true } },
84
+ }
@@ -0,0 +1,26 @@
1
+ import { createSession, appendMessage, getMessages, listSessions } from '../sessions.js'
2
+
3
+ export class AcpSessionManager {
4
+ constructor() { this.active = new Map() }
5
+ new(opts = {}) {
6
+ const id = createSession({ platform: 'acp', ...opts })
7
+ this.active.set(id, { id, created: Date.now(), opts })
8
+ return { sessionId: id }
9
+ }
10
+ resume(sessionId) {
11
+ const messages = getMessages(sessionId)
12
+ if (!messages) return null
13
+ this.active.set(sessionId, { id: sessionId, resumed: Date.now(), messages })
14
+ return { sessionId, messages }
15
+ }
16
+ list() {
17
+ return { sessions: listSessions(50).filter(s => s.platform === 'acp') }
18
+ }
19
+ end(sessionId) {
20
+ this.active.delete(sessionId)
21
+ return { ended: sessionId }
22
+ }
23
+ appendUser(sessionId, content) { appendMessage(sessionId, { role: 'user', content }) }
24
+ appendAssistant(sessionId, content) { appendMessage(sessionId, { role: 'assistant', content }) }
25
+ isActive(sessionId) { return this.active.has(sessionId) }
26
+ }
@@ -0,0 +1,17 @@
1
+ import { registry, discoverBuiltinTools } from '../tools/registry.js'
2
+ import { Events } from './events.js'
3
+ export async function listToolsForAcp() {
4
+ await discoverBuiltinTools()
5
+ return registry.list().map(t => ({ name: t.name, toolset: t.toolset, schema: t.schema, requiresEnv: t.requiresEnv || [] }))
6
+ }
7
+ export async function dispatchWithEvents({ name, args, send, sessionId = null }) {
8
+ Events.toolStart(send, { sessionId, name, args })
9
+ try {
10
+ const result = await registry.dispatch(name, args, { sessionId })
11
+ Events.toolComplete(send, { sessionId, name, result })
12
+ return { ok: true, result }
13
+ } catch (e) {
14
+ Events.toolComplete(send, { sessionId, name, error: String(e?.message || e) })
15
+ return { ok: false, error: String(e?.message || e) }
16
+ }
17
+ }
@@ -0,0 +1,19 @@
1
+ import { db } from '../db.js'
2
+
3
+ async function init() {
4
+ const d = await db()
5
+ d.exec(`CREATE TABLE IF NOT EXISTS account_usage (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, model TEXT, prompt_tokens INTEGER, completion_tokens INTEGER, cost_usd REAL, ts INTEGER NOT NULL)`)
6
+ return d
7
+ }
8
+ export async function record({ sessionId = null, model, promptTokens = 0, completionTokens = 0, costUsd = 0 } = {}) {
9
+ (await init()).prepare(`INSERT INTO account_usage (session_id, model, prompt_tokens, completion_tokens, cost_usd, ts) VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, model, promptTokens, completionTokens, costUsd, Date.now())
10
+ }
11
+ export async function totalForSession(sessionId) {
12
+ return (await init()).prepare(`SELECT SUM(prompt_tokens) AS prompt, SUM(completion_tokens) AS completion, SUM(cost_usd) AS cost FROM account_usage WHERE session_id = ?`).get(sessionId) || { prompt: 0, completion: 0, cost: 0 }
13
+ }
14
+ export async function totalLifetime() {
15
+ return (await init()).prepare(`SELECT SUM(prompt_tokens) AS prompt, SUM(completion_tokens) AS completion, SUM(cost_usd) AS cost FROM account_usage`).get() || { prompt: 0, completion: 0, cost: 0 }
16
+ }
17
+ export async function listRecent(limit = 50) {
18
+ return (await init()).prepare(`SELECT * FROM account_usage ORDER BY id DESC LIMIT ?`).all(limit)
19
+ }
@@ -0,0 +1,80 @@
1
+ import { logger } from '../observability/log.js'
2
+
3
+ const log = logger('acptoapi')
4
+
5
+ export function getAcptoapiUrl() {
6
+ return process.env.FREDDIE_LLM_URL || 'http://127.0.0.1:4800/v1'
7
+ }
8
+
9
+ export function getAcptoapiModel() {
10
+ return process.env.FREDDIE_LLM_MODEL || 'claude/haiku'
11
+ }
12
+
13
+ export async function callLLM({ messages, tools = [], model } = {}) {
14
+ const base = getAcptoapiUrl()
15
+ const useModel = model || getAcptoapiModel()
16
+ const body = {
17
+ model: useModel,
18
+ messages: messages.map(adaptMessage),
19
+ stream: false,
20
+ max_tokens: 1024,
21
+ }
22
+ if (Array.isArray(tools) && tools.length) body.tools = tools.map(adaptTool)
23
+ const res = await fetch(base.replace(/\/$/, '') + '/chat/completions', {
24
+ method: 'POST',
25
+ headers: { 'content-type': 'application/json', authorization: 'Bearer none' },
26
+ body: JSON.stringify(body),
27
+ })
28
+ if (!res.ok) {
29
+ const text = await res.text()
30
+ throw new Error(`acptoapi ${res.status}: ${text.slice(0, 400)}`)
31
+ }
32
+ const json = await res.json()
33
+ log.info('completed', { model: useModel, usage: json.usage })
34
+ return adaptResponse(json)
35
+ }
36
+
37
+ function adaptMessage(m) {
38
+ if (m.role === 'tool') return { role: 'tool', tool_call_id: m.tool_call_id, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }
39
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
40
+ return {
41
+ role: 'assistant',
42
+ content: m.content || '',
43
+ tool_calls: m.tool_calls.map(tc => ({
44
+ id: tc.id || tc.tool_call_id,
45
+ type: 'function',
46
+ function: { name: tc.name || tc.function?.name, arguments: typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments || tc.function?.arguments || {}) },
47
+ })),
48
+ }
49
+ }
50
+ return { role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }
51
+ }
52
+
53
+ function adaptTool(t) {
54
+ return {
55
+ type: 'function',
56
+ function: {
57
+ name: t.name,
58
+ description: t.description,
59
+ parameters: t.parameters || t.input_schema || { type: 'object', properties: {} },
60
+ },
61
+ }
62
+ }
63
+
64
+ function adaptResponse(r) {
65
+ const choice = r.choices?.[0]?.message || {}
66
+ const content = typeof choice.content === 'string' ? choice.content : ''
67
+ const tool_calls = Array.isArray(choice.tool_calls)
68
+ ? choice.tool_calls.map(tc => ({ id: tc.id, name: tc.function?.name, arguments: tryParseJson(tc.function?.arguments) }))
69
+ : []
70
+ return { content, tool_calls, raw: r }
71
+ }
72
+
73
+ function tryParseJson(s) { try { return typeof s === 'string' ? JSON.parse(s) : (s || {}) } catch { return {} } }
74
+
75
+ export async function isReachable() {
76
+ try {
77
+ const res = await fetch(getAcptoapiUrl().replace(/\/$/, '') + '/models', { headers: { authorization: 'Bearer none' } })
78
+ return res.ok
79
+ } catch { return false }
80
+ }
@@ -0,0 +1,10 @@
1
+ import { callLLM as piCallLLM } from './pi-bridge.js'
2
+ import { resolveKey } from './credential_sources.js'
3
+ import { annotateBreakpoints } from './prompt_caching.js'
4
+ export async function chat({ messages, tools = [], model = 'claude-sonnet-4-6', cache = true } = {}) {
5
+ const k = await resolveKey('anthropic')
6
+ if (!k.value) throw new Error('ANTHROPIC_API_KEY required (source: ' + k.source + ')')
7
+ if (!process.env.ANTHROPIC_API_KEY) process.env.ANTHROPIC_API_KEY = k.value
8
+ return await piCallLLM({ messages: cache ? annotateBreakpoints(messages, { provider: 'anthropic' }) : messages, tools, model, provider: 'anthropic' })
9
+ }
10
+ export const provider = 'anthropic'
@@ -0,0 +1,20 @@
1
+ import { resolveKey } from './credential_sources.js'
2
+ import { calculateCost } from './usage_pricing.js'
3
+ import { record as recordUsage } from './account_usage.js'
4
+ import { retryAsync } from './retry_utils.js'
5
+ import { record as recordRateLimit } from './rate_limit_tracker.js'
6
+ export async function call_llm({ messages, model = 'claude-haiku-4-5', provider = 'anthropic', tools = [], max_tokens = 4096, sessionId = null } = {}) {
7
+ const k = await resolveKey(provider)
8
+ if (!k.value) throw new Error(provider.toUpperCase() + '_API_KEY required (source: ' + k.source + ')')
9
+ return await retryAsync(async () => {
10
+ try {
11
+ const { callLLM } = await import('./pi-bridge.js')
12
+ if (!process.env[provider.toUpperCase() + '_API_KEY']) process.env[provider.toUpperCase() + '_API_KEY'] = k.value
13
+ const out = await callLLM({ messages, tools, model, provider })
14
+ const usage = out?.raw?.usage || {}
15
+ const cost = calculateCost({ model, prompt_tokens: usage.input_tokens || 0, completion_tokens: usage.output_tokens || 0 })
16
+ recordUsage({ sessionId, model, promptTokens: usage.input_tokens || 0, completionTokens: usage.output_tokens || 0, costUsd: cost })
17
+ return { ...out, usage, cost }
18
+ } catch (e) { recordRateLimit(provider, e); throw e }
19
+ }, { attempts: 3, backoff: 250 })
20
+ }
@@ -0,0 +1,11 @@
1
+ import { resolveKey } from './credential_sources.js'
2
+ const REGION = () => process.env.AWS_REGION || 'us-east-1'
3
+ export async function chat({ messages, model = 'anthropic.claude-sonnet-4-v1:0', tools = [] } = {}) {
4
+ const id = (await resolveKey('aws')).value || process.env.AWS_ACCESS_KEY_ID
5
+ if (!id) throw new Error('AWS_ACCESS_KEY_ID required for bedrock')
6
+ const url = 'https://bedrock-runtime.' + REGION() + '.amazonaws.com/model/' + encodeURIComponent(model) + '/invoke'
7
+ const body = JSON.stringify({ anthropic_version: 'bedrock-2023-05-31', max_tokens: 4096, messages, ...(tools.length ? { tools } : {}) })
8
+ const r = await fetch(url, { method: 'POST', headers: { authorization: 'AWS4-HMAC-SHA256 ' + (process.env.AWS_SESSION_TOKEN || ''), 'content-type': 'application/json' }, body })
9
+ return await r.json()
10
+ }
11
+ export const provider = 'bedrock'
@@ -0,0 +1,10 @@
1
+ import { resolveKey } from './credential_sources.js'
2
+ import { isCodexModel } from '../cli/codex_models.js'
3
+ export async function chat({ input, model = 'o3-mini', tools = [], reasoning_effort = 'medium' } = {}) {
4
+ const k = await resolveKey('openai')
5
+ if (!k.value) throw new Error('OPENAI_API_KEY required (source: ' + k.source + ')')
6
+ if (!isCodexModel(model)) console.warn('[codex_responses] non-codex model: ' + model)
7
+ const r = await fetch('https://api.openai.com/v1/responses', { method: 'POST', headers: { authorization: 'Bearer ' + k.value, 'content-type': 'application/json' }, body: JSON.stringify({ model, input, ...(tools.length ? { tools } : {}), reasoning: { effort: reasoning_effort } }) })
8
+ return await r.json()
9
+ }
10
+ export const provider = 'codex_responses'
@@ -0,0 +1,55 @@
1
+ import { shouldCompress, computeCompressionPlan, MINIMUM_CONTEXT_LENGTH } from './policy.js'
2
+ import { pruneOldToolResults } from './prune.js'
3
+ import { SUMMARY_PREFIX, LEGACY_SUMMARY_PREFIX, SUMMARIZER_SYSTEM_PROMPT, buildSummarizerInput } from './prompt.js'
4
+ import { markFailure, shouldRetry } from './fallback.js'
5
+ import { logger } from '../../observability/log.js'
6
+
7
+ const log = logger('compressor')
8
+
9
+ export async function compress({ messages, modelContextLength = MINIMUM_CONTEXT_LENGTH, callLLM, auxModel = null, threshold } = {}) {
10
+ if (!shouldCompress({ messages, modelContextLength, threshold })) return { compressedMessages: messages, summary: null, didCompress: false, reason: 'below threshold' }
11
+ if (!shouldRetry()) return { compressedMessages: messages, summary: null, didCompress: false, reason: 'cooldown' }
12
+ if (typeof callLLM !== 'function') throw new Error('compress: callLLM required')
13
+
14
+ const plan = computeCompressionPlan(messages, modelContextLength)
15
+ if (plan.middle.length === 0) return { compressedMessages: messages, summary: null, didCompress: false, reason: 'no middle' }
16
+
17
+ const existing = extractExistingSummary(plan.head)
18
+ const prunedMiddle = pruneOldToolResults(plan.middle, 0)
19
+ const summarizerMessages = [
20
+ { role: 'system', content: SUMMARIZER_SYSTEM_PROMPT },
21
+ { role: 'user', content: (existing ? `Previous summary:\n${existing}\n\nNew turns to fold in:\n` : '') + buildSummarizerInput(prunedMiddle) },
22
+ ]
23
+ let summary
24
+ try {
25
+ const out = await callLLM({ messages: summarizerMessages, tools: [], model: auxModel, maxTokens: plan.summaryBudget })
26
+ summary = (out?.content || '').trim()
27
+ if (!summary) throw new Error('empty summary')
28
+ } catch (e) {
29
+ markFailure()
30
+ log.error('summarization failed', { err: String(e) })
31
+ return { compressedMessages: messages, summary: null, didCompress: false, error: String(e) }
32
+ }
33
+
34
+ const headWithoutOldSummary = stripExistingSummary(plan.head)
35
+ const summaryMsg = { role: 'user', content: `${SUMMARY_PREFIX}\n\n${summary}` }
36
+ const compressedMessages = [...headWithoutOldSummary, summaryMsg, ...plan.tail]
37
+ log.info('compressed', { in: messages.length, out: compressedMessages.length, summary_chars: summary.length })
38
+ return { compressedMessages, summary, didCompress: true, plan }
39
+ }
40
+
41
+ function extractExistingSummary(head) {
42
+ for (const m of head) {
43
+ const c = typeof m.content === 'string' ? m.content : ''
44
+ if (c.startsWith(SUMMARY_PREFIX)) return c.slice(SUMMARY_PREFIX.length).trim()
45
+ if (c.startsWith(LEGACY_SUMMARY_PREFIX)) return c.slice(LEGACY_SUMMARY_PREFIX.length).trim()
46
+ }
47
+ return null
48
+ }
49
+
50
+ function stripExistingSummary(head) {
51
+ return head.filter(m => {
52
+ const c = typeof m.content === 'string' ? m.content : ''
53
+ return !c.startsWith(SUMMARY_PREFIX) && !c.startsWith(LEGACY_SUMMARY_PREFIX)
54
+ })
55
+ }
@@ -0,0 +1,14 @@
1
+ export const SUMMARY_FAILURE_COOLDOWN_SECONDS = 600
2
+
3
+ let _lastFailure = null
4
+
5
+ export function markFailure(now = Date.now()) { _lastFailure = now }
6
+
7
+ export function shouldRetry(now = Date.now()) {
8
+ if (_lastFailure === null) return true
9
+ return (now - _lastFailure) >= SUMMARY_FAILURE_COOLDOWN_SECONDS * 1000
10
+ }
11
+
12
+ export function clearFailure() { _lastFailure = null }
13
+
14
+ export function lastFailureAt() { return _lastFailure }
@@ -0,0 +1,6 @@
1
+ export { compress } from './compressor.js'
2
+ export { shouldCompress, computeCompressionPlan, COMPRESSION_THRESHOLD, MINIMUM_CONTEXT_LENGTH, SUMMARY_RATIO } from './policy.js'
3
+ export { SUMMARY_PREFIX, LEGACY_SUMMARY_PREFIX, SUMMARIZER_SYSTEM_PROMPT, buildSummarizerInput } from './prompt.js'
4
+ export { pruneOldToolResults, PRUNED_TOOL_PLACEHOLDER } from './prune.js'
5
+ export { estimateMessagesTokens, estimateMessageTokens, contentLengthForBudget, IMAGE_TOKEN_ESTIMATE, CHARS_PER_TOKEN } from './tokens.js'
6
+ export { markFailure, shouldRetry, clearFailure, SUMMARY_FAILURE_COOLDOWN_SECONDS } from './fallback.js'
@@ -0,0 +1,47 @@
1
+ import { estimateMessagesTokens } from './tokens.js'
2
+
3
+ export const MINIMUM_CONTEXT_LENGTH = 8000
4
+ export const SUMMARY_RATIO = 0.20
5
+ export const MIN_SUMMARY_TOKENS = 2000
6
+ export const SUMMARY_TOKENS_CEILING = 12000
7
+ export const COMPRESSION_THRESHOLD = 0.85
8
+
9
+ export function shouldCompress({ messages, modelContextLength = MINIMUM_CONTEXT_LENGTH, threshold = COMPRESSION_THRESHOLD } = {}) {
10
+ if (!Array.isArray(messages) || messages.length < 4) return false
11
+ const used = estimateMessagesTokens(messages)
12
+ return used >= Math.max(MINIMUM_CONTEXT_LENGTH, modelContextLength) * threshold
13
+ }
14
+
15
+ export function computeCompressionPlan(messages, modelContextLength = MINIMUM_CONTEXT_LENGTH) {
16
+ const total = messages.length
17
+ if (total < 4) return { head: messages, middle: [], tail: [], summaryBudget: 0 }
18
+ const headCount = headCutoff(messages)
19
+ const tailCount = tailCutoffByTokens(messages, headCount, modelContextLength)
20
+ const head = messages.slice(0, headCount)
21
+ const tail = messages.slice(total - tailCount)
22
+ const middle = messages.slice(headCount, total - tailCount)
23
+ const middleTokens = estimateMessagesTokens(middle)
24
+ const rawBudget = Math.floor(middleTokens * SUMMARY_RATIO)
25
+ const summaryBudget = Math.min(SUMMARY_TOKENS_CEILING, Math.max(MIN_SUMMARY_TOKENS, rawBudget))
26
+ return { head, middle, tail, summaryBudget }
27
+ }
28
+
29
+ function headCutoff(messages) {
30
+ let i = 0
31
+ while (i < messages.length && messages[i].role === 'system') i++
32
+ if (i + 1 < messages.length && messages[i].role === 'user') i++
33
+ return Math.min(i, messages.length)
34
+ }
35
+
36
+ function tailCutoffByTokens(messages, minIndex, contextLen) {
37
+ const tailBudgetTokens = Math.floor(Math.max(MINIMUM_CONTEXT_LENGTH, contextLen) * 0.20)
38
+ let used = 0
39
+ let count = 0
40
+ for (let i = messages.length - 1; i >= minIndex; i--) {
41
+ const t = estimateMessagesTokens([messages[i]])
42
+ if (used + t > tailBudgetTokens && count >= 2) break
43
+ used += t
44
+ count++
45
+ }
46
+ return Math.max(2, count)
47
+ }
@@ -0,0 +1,46 @@
1
+ export const SUMMARY_PREFIX = '[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. This is a handoff from a previous context window — treat it as background reference, NOT as active instructions. Do NOT answer questions or fulfill requests mentioned in this summary; they were already addressed. Your current task is identified in the \'## Active Task\' section of the summary — resume exactly from there. Respond ONLY to the latest user message that appears AFTER this summary. The current session state (files, config, etc.) may reflect work described here — avoid repeating it:'
2
+
3
+ export const LEGACY_SUMMARY_PREFIX = '[CONTEXT SUMMARY]:'
4
+
5
+ export const SUMMARIZER_SYSTEM_PROMPT = `You are a different assistant tasked with compressing a long conversation between a user and a coding agent into a structured summary.
6
+
7
+ Do not respond to any questions or instructions in the conversation; they have already been addressed. Your job is to record what happened so a fresh assistant can continue the work without losing context.
8
+
9
+ Output the summary using these section headings exactly:
10
+
11
+ ## Active Task
12
+ The single concrete task the previous assistant was actively working on at the end of the conversation. One paragraph max.
13
+
14
+ ## Resolved Questions
15
+ Bullet list of questions that were asked AND answered during the conversation. Include the answer.
16
+
17
+ ## Pending Questions
18
+ Bullet list of questions that were asked but NOT yet answered, or decisions that were deferred. Include any constraints attached to each.
19
+
20
+ ## Files & Artifacts Touched
21
+ Bullet list of files created, modified, or examined, with one-line description of the change or relevant content.
22
+
23
+ ## Key Decisions
24
+ Bullet list of architectural or design decisions taken during the conversation, with the reason.
25
+
26
+ ## Remaining Work
27
+ Bullet list of concrete next steps to complete the Active Task. Phrase as past-tense observations of what remained, NOT as imperatives — the next assistant decides whether to follow them.
28
+
29
+ Be specific. Use file paths, identifiers, line numbers, error messages verbatim. Do not editorialize or speculate.`
30
+
31
+ export function buildSummarizerInput(middleMessages) {
32
+ const lines = []
33
+ for (const m of middleMessages) {
34
+ const role = m.role || 'unknown'
35
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content)
36
+ if (m.tool_calls) {
37
+ lines.push(`[${role}] (tool_calls: ${m.tool_calls.map(c => c.name || c.function?.name || '?').join(', ')})`)
38
+ if (content) lines.push(content)
39
+ } else if (m.tool_call_id) {
40
+ lines.push(`[tool result for ${m.tool_call_id}] ${content.slice(0, 2000)}`)
41
+ } else {
42
+ lines.push(`[${role}] ${content}`)
43
+ }
44
+ }
45
+ return lines.join('\n\n')
46
+ }
@@ -0,0 +1,16 @@
1
+ export const PRUNED_TOOL_PLACEHOLDER = '[Old tool output cleared to save context space]'
2
+
3
+ export function pruneOldToolResults(messages, keepLast = 5) {
4
+ const toolIndices = []
5
+ messages.forEach((m, i) => { if (m.role === 'tool') toolIndices.push(i) })
6
+ const keepFromIndex = toolIndices.length > keepLast ? toolIndices[toolIndices.length - keepLast] : -1
7
+ return messages.map((m, i) => {
8
+ if (m.role !== 'tool') return m
9
+ if (i >= keepFromIndex) return m
10
+ return { ...m, content: PRUNED_TOOL_PLACEHOLDER }
11
+ })
12
+ }
13
+
14
+ export function countToolMessages(messages) {
15
+ return messages.filter(m => m.role === 'tool').length
16
+ }