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.
- package/AGENTS.md +180 -0
- package/CHANGELOG.md +32 -0
- package/README.md +130 -0
- package/bin/freddie.js +116 -0
- package/package.json +59 -0
- package/skills/creative/README.md +3 -0
- package/skills/creative/architecture-diagram/SKILL.md +52 -0
- package/skills/creative/ascii-video/SKILL.md +60 -0
- package/skills/creative/concept-diagrams/SKILL.md +65 -0
- package/skills/data/README.md +3 -0
- package/skills/data/etl-pipelines/SKILL.md +60 -0
- package/skills/data/sql-explainer/SKILL.md +60 -0
- package/skills/ops/README.md +3 -0
- package/skills/ops/incident-response/SKILL.md +74 -0
- package/skills/ops/log-triage/SKILL.md +79 -0
- package/skills/planning/README.md +3 -0
- package/skills/planning/okr-drafter/SKILL.md +60 -0
- package/skills/planning/weekly-review/SKILL.md +64 -0
- package/skills/software-development/README.md +3 -0
- package/skills/software-development/code-review/SKILL.md +70 -0
- package/skills/software-development/rfc-writer/SKILL.md +68 -0
- package/skills/software-development/systematic-debugging/SKILL.md +80 -0
- package/src/acp/auth.js +21 -0
- package/src/acp/entry.js +2 -0
- package/src/acp/events.js +10 -0
- package/src/acp/main.js +8 -0
- package/src/acp/permissions.js +29 -0
- package/src/acp/server.js +84 -0
- package/src/acp/session.js +26 -0
- package/src/acp/tools.js +17 -0
- package/src/agent/account_usage.js +19 -0
- package/src/agent/acptoapi-bridge.js +80 -0
- package/src/agent/anthropic_adapter.js +10 -0
- package/src/agent/auxiliary_client.js +20 -0
- package/src/agent/bedrock_adapter.js +11 -0
- package/src/agent/codex_responses_adapter.js +10 -0
- package/src/agent/compress/compressor.js +55 -0
- package/src/agent/compress/fallback.js +14 -0
- package/src/agent/compress/index.js +6 -0
- package/src/agent/compress/policy.js +47 -0
- package/src/agent/compress/prompt.js +46 -0
- package/src/agent/compress/prune.js +16 -0
- package/src/agent/compress/tokens.js +31 -0
- package/src/agent/context_references.js +40 -0
- package/src/agent/copilot_acp_client.js +6 -0
- package/src/agent/credential_pool.js +30 -0
- package/src/agent/credential_sources.js +18 -0
- package/src/agent/curator.js +5 -0
- package/src/agent/display.js +23 -0
- package/src/agent/error_classifier.js +15 -0
- package/src/agent/file_safety.js +9 -0
- package/src/agent/gemini_cloudcode_adapter.js +9 -0
- package/src/agent/gemini_native_adapter.js +11 -0
- package/src/agent/gemini_schema.js +19 -0
- package/src/agent/google_code_assist.js +8 -0
- package/src/agent/google_oauth.js +21 -0
- package/src/agent/image_gen_provider.js +8 -0
- package/src/agent/image_gen_registry.js +6 -0
- package/src/agent/image_routing.js +13 -0
- package/src/agent/insights.js +9 -0
- package/src/agent/llm_resolver.js +21 -0
- package/src/agent/lmstudio_reasoning.js +13 -0
- package/src/agent/machine.js +102 -0
- package/src/agent/manual_compression_feedback.js +5 -0
- package/src/agent/memory_manager.js +14 -0
- package/src/agent/memory_provider.js +1 -0
- package/src/agent/model_metadata.js +28 -0
- package/src/agent/models_dev.js +13 -0
- package/src/agent/moonshot_schema.js +11 -0
- package/src/agent/oauth_endpoints.js +79 -0
- package/src/agent/onboarding.js +16 -0
- package/src/agent/pi-bridge.js +37 -0
- package/src/agent/prompt_builder.js +12 -0
- package/src/agent/prompt_caching.js +24 -0
- package/src/agent/rate_limit_tracker.js +12 -0
- package/src/agent/redact.js +25 -0
- package/src/agent/retry_utils.js +17 -0
- package/src/agent/shell_hooks.js +16 -0
- package/src/agent/skill_commands.js +16 -0
- package/src/agent/skill_preprocessing.js +12 -0
- package/src/agent/skill_utils.js +14 -0
- package/src/agent/subdirectory_hints.js +17 -0
- package/src/agent/title_generator.js +13 -0
- package/src/agent/trajectory.js +9 -0
- package/src/agent/usage_pricing.js +16 -0
- package/src/auth.js +84 -0
- package/src/batch.js +32 -0
- package/src/cli/auth_commands.js +17 -0
- package/src/cli/azure_detect.js +9 -0
- package/src/cli/backup.js +17 -0
- package/src/cli/banner.js +13 -0
- package/src/cli/browser_connect.js +11 -0
- package/src/cli/callbacks.js +5 -0
- package/src/cli/claw.js +8 -0
- package/src/cli/cli_output.js +19 -0
- package/src/cli/clipboard.js +24 -0
- package/src/cli/codex_models.js +8 -0
- package/src/cli/colors.js +13 -0
- package/src/cli/completer.js +98 -0
- package/src/cli/completion.js +21 -0
- package/src/cli/copilot_auth.js +9 -0
- package/src/cli/curator_cli.js +5 -0
- package/src/cli/curses.js +15 -0
- package/src/cli/debug.js +6 -0
- package/src/cli/default_soul.js +20 -0
- package/src/cli/dingtalk_auth.js +12 -0
- package/src/cli/doctor.js +15 -0
- package/src/cli/dump.js +11 -0
- package/src/cli/env_loader.js +25 -0
- package/src/cli/fallback_cmd.js +9 -0
- package/src/cli/gateway_cli.js +17 -0
- package/src/cli/hooks.js +9 -0
- package/src/cli/interactive.js +61 -0
- package/src/cli/logs.js +32 -0
- package/src/cli/main.js +7 -0
- package/src/cli/mcp_config.js +9 -0
- package/src/cli/memory_setup.js +12 -0
- package/src/cli/model_catalog.js +23 -0
- package/src/cli/model_normalize.js +12 -0
- package/src/cli/model_switch.js +11 -0
- package/src/cli/models.js +13 -0
- package/src/cli/nous_subscription.js +12 -0
- package/src/cli/oneshot.js +6 -0
- package/src/cli/pairing.js +21 -0
- package/src/cli/platforms.js +14 -0
- package/src/cli/plugins.js +4 -0
- package/src/cli/plugins_cmd.js +21 -0
- package/src/cli/profiles_cli.js +6 -0
- package/src/cli/providers.js +18 -0
- package/src/cli/pty_bridge.js +16 -0
- package/src/cli/relaunch.js +7 -0
- package/src/cli/runtime_provider.js +9 -0
- package/src/cli/setup.js +131 -0
- package/src/cli/skills_config.js +6 -0
- package/src/cli/skills_hub.js +8 -0
- package/src/cli/slack_cli.js +17 -0
- package/src/cli/status.js +10 -0
- package/src/cli/timeouts.js +5 -0
- package/src/cli/tips.js +14 -0
- package/src/cli/tools_config.js +15 -0
- package/src/cli/uninstall.js +8 -0
- package/src/cli/vercel_auth.js +13 -0
- package/src/cli/voice.js +6 -0
- package/src/cli/web_server.js +13 -0
- package/src/cli/webhook.js +12 -0
- package/src/commands/profile.js +72 -0
- package/src/commands/registry.js +94 -0
- package/src/config.js +125 -0
- package/src/context/engine.js +42 -0
- package/src/cron/cron-parse.js +27 -0
- package/src/cron/scheduler.js +63 -0
- package/src/db.js +178 -0
- package/src/gateway/base.js +13 -0
- package/src/gateway/builtin_hooks/boot.js +5 -0
- package/src/gateway/builtin_hooks/broadcast.js +3 -0
- package/src/gateway/builtin_hooks/deny.js +6 -0
- package/src/gateway/builtin_hooks/index.js +17 -0
- package/src/gateway/builtin_hooks/presence.js +4 -0
- package/src/gateway/builtin_hooks/routing.js +7 -0
- package/src/gateway/helpers.js +27 -0
- package/src/gateway/platforms/api_server.js +21 -0
- package/src/gateway/platforms/bluebubbles.js +32 -0
- package/src/gateway/platforms/dingtalk.js +32 -0
- package/src/gateway/platforms/discord.js +24 -0
- package/src/gateway/platforms/email.js +51 -0
- package/src/gateway/platforms/feishu.js +32 -0
- package/src/gateway/platforms/feishu_comment.js +12 -0
- package/src/gateway/platforms/feishu_comment_rules.js +11 -0
- package/src/gateway/platforms/homeassistant.js +32 -0
- package/src/gateway/platforms/matrix.js +40 -0
- package/src/gateway/platforms/mattermost.js +29 -0
- package/src/gateway/platforms/qqbot.js +32 -0
- package/src/gateway/platforms/signal.js +33 -0
- package/src/gateway/platforms/slack.js +34 -0
- package/src/gateway/platforms/sms.js +34 -0
- package/src/gateway/platforms/telegram.js +38 -0
- package/src/gateway/platforms/telegram_network.js +17 -0
- package/src/gateway/platforms/webhook.js +19 -0
- package/src/gateway/platforms/wecom.js +32 -0
- package/src/gateway/platforms/wecom_callback.js +15 -0
- package/src/gateway/platforms/wecom_crypto.js +16 -0
- package/src/gateway/platforms/weixin.js +32 -0
- package/src/gateway/platforms/whatsapp.js +40 -0
- package/src/gateway/platforms/yuanbao.js +9 -0
- package/src/gateway/platforms/yuanbao_media.js +5 -0
- package/src/gateway/platforms/yuanbao_proto.js +9 -0
- package/src/gateway/platforms/yuanbao_sticker.js +6 -0
- package/src/gateway/run.js +42 -0
- package/src/gateway/service.js +143 -0
- package/src/home.js +44 -0
- package/src/index.js +47 -0
- package/src/mcp/server.js +49 -0
- package/src/observability/debug.js +31 -0
- package/src/observability/log.js +38 -0
- package/src/plugins/achievements/index.js +9 -0
- package/src/plugins/cockpit/index.js +8 -0
- package/src/plugins/context_engine/index.js +13 -0
- package/src/plugins/disk_cleanup/index.js +22 -0
- package/src/plugins/google_meet/index.js +19 -0
- package/src/plugins/image_gen/index.js +5 -0
- package/src/plugins/manager.js +66 -0
- package/src/plugins/memory/_index.js +8 -0
- package/src/plugins/memory/byterover.js +25 -0
- package/src/plugins/memory/hindsight.js +25 -0
- package/src/plugins/memory/holographic.js +31 -0
- package/src/plugins/memory/honcho.js +25 -0
- package/src/plugins/memory/mem0.js +25 -0
- package/src/plugins/memory/openviking.js +25 -0
- package/src/plugins/memory/provider.js +35 -0
- package/src/plugins/memory/retaindb.js +25 -0
- package/src/plugins/memory/supermemory.js +25 -0
- package/src/plugins/observability/index.js +18 -0
- package/src/plugins/platforms/index.js +20 -0
- package/src/plugins/spotify/index.js +22 -0
- package/src/rl/atropos.js +22 -0
- package/src/rl/cli.js +18 -0
- package/src/sessions.js +84 -0
- package/src/skills/index.js +49 -0
- package/src/skin/engine.js +81 -0
- package/src/swe/runner.js +26 -0
- package/src/time.js +25 -0
- package/src/tools/ansi_strip.js +8 -0
- package/src/tools/approval.js +15 -0
- package/src/tools/bash.js +35 -0
- package/src/tools/binary_extensions.js +22 -0
- package/src/tools/browser.js +48 -0
- package/src/tools/budget_config.js +13 -0
- package/src/tools/checkpoint.js +29 -0
- package/src/tools/clarify.js +15 -0
- package/src/tools/code_execution.js +27 -0
- package/src/tools/credential_files.js +16 -0
- package/src/tools/cronjob.js +16 -0
- package/src/tools/debug_helpers.js +9 -0
- package/src/tools/delegate.js +28 -0
- package/src/tools/discord_tool.js +13 -0
- package/src/tools/edit.js +31 -0
- package/src/tools/env_passthrough.js +15 -0
- package/src/tools/environments/base.js +26 -0
- package/src/tools/environments/daytona.js +48 -0
- package/src/tools/environments/docker.js +14 -0
- package/src/tools/environments/file_sync.js +60 -0
- package/src/tools/environments/index.js +36 -0
- package/src/tools/environments/local.js +31 -0
- package/src/tools/environments/modal.js +33 -0
- package/src/tools/environments/singularity.js +38 -0
- package/src/tools/environments/ssh.js +14 -0
- package/src/tools/environments/vercel_sandbox.js +47 -0
- package/src/tools/feishu_doc.js +15 -0
- package/src/tools/feishu_drive.js +14 -0
- package/src/tools/file_operations.js +17 -0
- package/src/tools/file_state.js +16 -0
- package/src/tools/file_tools.js +23 -0
- package/src/tools/fuzzy_match.js +8 -0
- package/src/tools/grep.js +51 -0
- package/src/tools/homeassistant_tool.js +15 -0
- package/src/tools/image_gen.js +33 -0
- package/src/tools/interrupt.js +18 -0
- package/src/tools/managed_tool_gateway.js +11 -0
- package/src/tools/mcp_oauth.js +21 -0
- package/src/tools/mcp_oauth_manager.js +20 -0
- package/src/tools/mcp_tool.js +36 -0
- package/src/tools/memory.js +66 -0
- package/src/tools/mixture_of_agents.js +14 -0
- package/src/tools/neutts_synth.js +13 -0
- package/src/tools/openrouter_client.js +13 -0
- package/src/tools/osv_check.js +11 -0
- package/src/tools/patch_parser.js +42 -0
- package/src/tools/path_security.js +16 -0
- package/src/tools/process_registry.js +17 -0
- package/src/tools/read.js +26 -0
- package/src/tools/registry.js +54 -0
- package/src/tools/rl_training.js +13 -0
- package/src/tools/schema_sanitizer.js +18 -0
- package/src/tools/send_message.js +32 -0
- package/src/tools/session_search.js +23 -0
- package/src/tools/skill_manager.js +17 -0
- package/src/tools/skill_usage.js +20 -0
- package/src/tools/skills_guard.js +17 -0
- package/src/tools/skills_hub.js +31 -0
- package/src/tools/skills_index.js +14 -0
- package/src/tools/skills_sync.js +19 -0
- package/src/tools/skills_tool.js +11 -0
- package/src/tools/slash_confirm.js +16 -0
- package/src/tools/terminal.js +29 -0
- package/src/tools/tirith_security.js +25 -0
- package/src/tools/todo.js +54 -0
- package/src/tools/tool_backend_helpers.js +26 -0
- package/src/tools/tool_output_limits.js +15 -0
- package/src/tools/tool_result_storage.js +20 -0
- package/src/tools/transcription.js +19 -0
- package/src/tools/tts.js +19 -0
- package/src/tools/url_safety.js +15 -0
- package/src/tools/vision.js +18 -0
- package/src/tools/voice_mode.js +10 -0
- package/src/tools/web_search.js +37 -0
- package/src/tools/web_tools.js +18 -0
- package/src/tools/website_policy.js +14 -0
- package/src/tools/write.js +25 -0
- package/src/tools/xai_http.js +13 -0
- package/src/tools/yuanbao_tools.js +13 -0
- package/src/toolset_distributions.js +18 -0
- package/src/toolsets.js +26 -0
- package/src/tui/index.js +26 -0
- package/src/utils.js +54 -0
- package/src/web/app.js +547 -0
- package/src/web/index.html +167 -0
- 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.
|
package/src/acp/auth.js
ADDED
|
@@ -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
|
+
}
|
package/src/acp/entry.js
ADDED
|
@@ -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
|
+
}
|
package/src/acp/main.js
ADDED
|
@@ -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
|
+
}
|
package/src/acp/tools.js
ADDED
|
@@ -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
|
+
}
|