skimpyclaw 0.3.14 → 0.4.0
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/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
|
@@ -4,33 +4,205 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Falls back to mechanical truncation if the LLM call fails.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// Uses a generic compactMessages() driven by MessageFormatHelper adapters,
|
|
8
|
+
// so the compaction algorithm is written once regardless of provider format.
|
|
9
9
|
const DEFAULT_MAX_CONTEXT_TOKENS = 200_000;
|
|
10
10
|
const KEEP_TAIL = 8; // always keep last N messages/items untouched
|
|
11
|
-
const RESULT_MAX_CHARS =
|
|
12
|
-
const SUMMARY_MAX_TOKENS =
|
|
11
|
+
const RESULT_MAX_CHARS = 200; // fallback truncation length
|
|
12
|
+
const SUMMARY_MAX_TOKENS = 1024; // max tokens for summary response
|
|
13
13
|
// Preferred compaction models in priority order (cheap & fast).
|
|
14
14
|
// Can be overridden via contextManagement.compactionModel in config.
|
|
15
15
|
const COMPACTION_MODEL_CANDIDATES = [
|
|
16
|
-
'anthropic/claude-haiku-
|
|
17
|
-
'openai/gpt-4o-mini',
|
|
18
|
-
'groq/llama-3.1-8b-instant',
|
|
16
|
+
'anthropic/claude-haiku-4-5',
|
|
19
17
|
];
|
|
20
18
|
/** Rough token estimate: 1 token ≈ 4 chars of JSON. */
|
|
21
19
|
export function estimateTokens(data) {
|
|
22
20
|
return Math.ceil(JSON.stringify(data).length / 4);
|
|
23
21
|
}
|
|
24
22
|
// --- LLM Summarization ---
|
|
25
|
-
const COMPACTION_SYSTEM_PROMPT = `
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
const COMPACTION_SYSTEM_PROMPT = `Summarize this AI coding assistant conversation concisely.
|
|
24
|
+
Preserve: file paths, variable names, error messages, decisions, code changes.
|
|
25
|
+
Summarize tool results briefly. Note unresolved issues. Use bullet points.
|
|
26
|
+
Output ONLY the summary.`;
|
|
27
|
+
/**
|
|
28
|
+
* Pick the best available compaction model from candidates.
|
|
29
|
+
* Checks which providers are initialized and returns the first match.
|
|
30
|
+
*/
|
|
31
|
+
async function pickCompactionModel(config) {
|
|
32
|
+
const { isAnthropicAvailable } = await import('./anthropic.js');
|
|
33
|
+
for (const candidate of COMPACTION_MODEL_CANDIDATES) {
|
|
34
|
+
const provider = candidate.split('/')[0];
|
|
35
|
+
if (provider === 'anthropic' && isAnthropicAvailable())
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
// Last resort: return the first candidate and let chat() fail → fallback to truncation
|
|
39
|
+
return COMPACTION_MODEL_CANDIDATES[0];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Call the LLM to summarize a conversation transcript.
|
|
43
|
+
* Returns the summary text, or null if the call fails.
|
|
44
|
+
*/
|
|
45
|
+
async function llmSummarize(transcript, config, compactionModel) {
|
|
46
|
+
try {
|
|
47
|
+
// Dynamically import to avoid circular dependency
|
|
48
|
+
const { chat } = await import('./index.js');
|
|
49
|
+
const model = compactionModel || await pickCompactionModel(config);
|
|
50
|
+
const messages = [
|
|
51
|
+
{ role: 'system', content: COMPACTION_SYSTEM_PROMPT },
|
|
52
|
+
{
|
|
53
|
+
role: 'user',
|
|
54
|
+
content: `Summarize the following conversation between an AI coding assistant and a user. This summary will replace the old messages in the context window so the assistant can continue working.\n\n---\n${transcript}\n---`,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
console.log(`[context-manager] Requesting LLM summary via ${model}`);
|
|
58
|
+
const summary = await chat(messages, {
|
|
59
|
+
model,
|
|
60
|
+
maxTokens: SUMMARY_MAX_TOKENS,
|
|
61
|
+
}, config);
|
|
62
|
+
if (!summary || summary.trim().length === 0) {
|
|
63
|
+
console.warn('[context-manager] LLM returned empty summary, falling back to truncation');
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
console.log(`[context-manager] LLM summary: ${summary.length} chars`);
|
|
67
|
+
return summary.trim();
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.warn(`[context-manager] LLM summarization failed, falling back to truncation: ${err instanceof Error ? err.message : err}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// --- Track whether we already compacted for a given conversation ---
|
|
75
|
+
const compactedMarker = new WeakSet();
|
|
76
|
+
// =====================================================================
|
|
77
|
+
// Generic compaction — single algorithm, format-agnostic via helper
|
|
78
|
+
// =====================================================================
|
|
79
|
+
/**
|
|
80
|
+
* Generic compaction function for any message format.
|
|
81
|
+
* Delegates format-specific concerns (truncation, serialization, summary building)
|
|
82
|
+
* to the provided MessageFormatHelper.
|
|
83
|
+
*
|
|
84
|
+
* Does NOT mutate the input array — returns a new array.
|
|
85
|
+
*/
|
|
86
|
+
export async function compactMessages(items, helper, config, iteration = 0, fullConfig) {
|
|
87
|
+
if (config?.enabled === false)
|
|
88
|
+
return { messages: items, compacted: false };
|
|
89
|
+
const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
|
|
90
|
+
const estimated = estimateTokens(items);
|
|
91
|
+
if (estimated <= maxTokens)
|
|
92
|
+
return { messages: items, compacted: false };
|
|
93
|
+
const tail = items.slice(-KEEP_TAIL);
|
|
94
|
+
const head = items.slice(0, -KEEP_TAIL);
|
|
95
|
+
// If we already compacted this array, use truncation fallback
|
|
96
|
+
// to progressively shrink rather than re-summarizing repeatedly.
|
|
97
|
+
if (compactedMarker.has(items)) {
|
|
98
|
+
console.log(`[context-manager] Already compacted, using truncation fallback (iteration ${iteration})`);
|
|
99
|
+
const truncatedHead = head.map(item => helper.isToolResult(item) ? helper.truncateToolResult(item, RESULT_MAX_CHARS) : item);
|
|
100
|
+
const result = [...truncatedHead, ...tail];
|
|
101
|
+
return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
|
|
102
|
+
}
|
|
103
|
+
console.log(`[context-manager] Compacting at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
|
|
104
|
+
// Attempt LLM summarization
|
|
105
|
+
if (fullConfig) {
|
|
106
|
+
const transcript = helper.serialize(head);
|
|
107
|
+
const summary = await llmSummarize(transcript, fullConfig, config?.compactionModel);
|
|
108
|
+
if (summary) {
|
|
109
|
+
const summaryItem = helper.buildSummaryMessage(summary);
|
|
110
|
+
const result = [summaryItem, ...tail];
|
|
111
|
+
compactedMarker.add(result);
|
|
112
|
+
const tokensAfter = estimateTokens(result);
|
|
113
|
+
return { messages: result, compacted: true, method: 'llm', summary, tokensBefore: estimated, tokensAfter };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Fallback: mechanical truncation
|
|
117
|
+
const truncatedHead = head.map(item => helper.isToolResult(item) ? helper.truncateToolResult(item, RESULT_MAX_CHARS) : item);
|
|
118
|
+
const result = [...truncatedHead, ...tail];
|
|
119
|
+
return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
|
|
120
|
+
}
|
|
121
|
+
// =====================================================================
|
|
122
|
+
// Provider-specific MessageFormatHelper implementations
|
|
123
|
+
// =====================================================================
|
|
124
|
+
/** Anthropic message format helper. */
|
|
125
|
+
export const anthropicFormatHelper = {
|
|
126
|
+
isToolResult(item) {
|
|
127
|
+
if (!Array.isArray(item.content))
|
|
128
|
+
return false;
|
|
129
|
+
return item.content.some((block) => block.type === 'tool_result');
|
|
130
|
+
},
|
|
131
|
+
truncateToolResult(item, maxChars) {
|
|
132
|
+
if (!Array.isArray(item.content))
|
|
133
|
+
return item;
|
|
134
|
+
let changed = false;
|
|
135
|
+
const newContent = item.content.map((block) => {
|
|
136
|
+
if (block.type !== 'tool_result')
|
|
137
|
+
return block;
|
|
138
|
+
const raw = typeof block.content === 'string'
|
|
139
|
+
? block.content
|
|
140
|
+
: JSON.stringify(block.content);
|
|
141
|
+
if (raw.length <= maxChars)
|
|
142
|
+
return block;
|
|
143
|
+
changed = true;
|
|
144
|
+
return { ...block, content: raw.slice(0, maxChars) + ' [truncated]' };
|
|
145
|
+
});
|
|
146
|
+
return changed ? { ...item, content: newContent } : item;
|
|
147
|
+
},
|
|
148
|
+
serialize(items) {
|
|
149
|
+
return serializeAnthropicMessages(items);
|
|
150
|
+
},
|
|
151
|
+
buildSummaryMessage(summary) {
|
|
152
|
+
return {
|
|
153
|
+
role: 'user',
|
|
154
|
+
content: [{ type: 'text', text: `[Conversation Summary]\n${summary}` }],
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
/** OpenAI message format helper. */
|
|
159
|
+
export const openaiFormatHelper = {
|
|
160
|
+
isToolResult(item) {
|
|
161
|
+
return item.role === 'tool';
|
|
162
|
+
},
|
|
163
|
+
truncateToolResult(item, maxChars) {
|
|
164
|
+
if (typeof item.content !== 'string')
|
|
165
|
+
return item;
|
|
166
|
+
if (item.content.length <= maxChars)
|
|
167
|
+
return item;
|
|
168
|
+
return { ...item, content: item.content.slice(0, maxChars) + ' [truncated]' };
|
|
169
|
+
},
|
|
170
|
+
serialize(items) {
|
|
171
|
+
return serializeOpenAIMessages(items);
|
|
172
|
+
},
|
|
173
|
+
buildSummaryMessage(summary) {
|
|
174
|
+
return {
|
|
175
|
+
role: 'user',
|
|
176
|
+
content: `[Conversation Summary]\n${summary}`,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
/** Codex message format helper. */
|
|
181
|
+
export const codexFormatHelper = {
|
|
182
|
+
isToolResult(item) {
|
|
183
|
+
return item.type === 'function_call_output';
|
|
184
|
+
},
|
|
185
|
+
truncateToolResult(item, maxChars) {
|
|
186
|
+
if (typeof item.output !== 'string')
|
|
187
|
+
return item;
|
|
188
|
+
if (item.output.length <= maxChars)
|
|
189
|
+
return item;
|
|
190
|
+
return { ...item, output: item.output.slice(0, maxChars) + ' [truncated]' };
|
|
191
|
+
},
|
|
192
|
+
serialize(items) {
|
|
193
|
+
return serializeCodexMessages(items);
|
|
194
|
+
},
|
|
195
|
+
buildSummaryMessage(summary) {
|
|
196
|
+
return {
|
|
197
|
+
type: 'message',
|
|
198
|
+
role: 'user',
|
|
199
|
+
content: `[Conversation Summary]\n${summary}`,
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
// =====================================================================
|
|
204
|
+
// Serialization helpers (used by format helpers and exported for tests)
|
|
205
|
+
// =====================================================================
|
|
34
206
|
/**
|
|
35
207
|
* Serialize Anthropic-format messages into a human-readable conversation transcript
|
|
36
208
|
* suitable for LLM summarization.
|
|
@@ -125,227 +297,31 @@ function serializeCodexMessages(items) {
|
|
|
125
297
|
}
|
|
126
298
|
return lines.join('\n');
|
|
127
299
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const { isAnthropicAvailable } = await import('./anthropic.js');
|
|
134
|
-
const { isOpenAIAvailable } = await import('./openai.js');
|
|
135
|
-
for (const candidate of COMPACTION_MODEL_CANDIDATES) {
|
|
136
|
-
const provider = candidate.split('/')[0];
|
|
137
|
-
if (provider === 'anthropic' && isAnthropicAvailable())
|
|
138
|
-
return candidate;
|
|
139
|
-
if (isOpenAIAvailable(provider))
|
|
140
|
-
return candidate;
|
|
141
|
-
}
|
|
142
|
-
// Last resort: return the first candidate and let chat() fail → fallback to truncation
|
|
143
|
-
return COMPACTION_MODEL_CANDIDATES[0];
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Call the LLM to summarize a conversation transcript.
|
|
147
|
-
* Returns the summary text, or null if the call fails.
|
|
148
|
-
*/
|
|
149
|
-
async function llmSummarize(transcript, config, compactionModel) {
|
|
150
|
-
try {
|
|
151
|
-
// Dynamically import to avoid circular dependency
|
|
152
|
-
const { chat } = await import('./index.js');
|
|
153
|
-
const model = compactionModel || await pickCompactionModel(config);
|
|
154
|
-
const messages = [
|
|
155
|
-
{ role: 'system', content: COMPACTION_SYSTEM_PROMPT },
|
|
156
|
-
{
|
|
157
|
-
role: 'user',
|
|
158
|
-
content: `Summarize the following conversation between an AI coding assistant and a user. This summary will replace the old messages in the context window so the assistant can continue working.\n\n---\n${transcript}\n---`,
|
|
159
|
-
},
|
|
160
|
-
];
|
|
161
|
-
console.log(`[context-manager] Requesting LLM summary via ${model}`);
|
|
162
|
-
const summary = await chat(messages, {
|
|
163
|
-
model,
|
|
164
|
-
maxTokens: SUMMARY_MAX_TOKENS,
|
|
165
|
-
}, config);
|
|
166
|
-
if (!summary || summary.trim().length === 0) {
|
|
167
|
-
console.warn('[context-manager] LLM returned empty summary, falling back to truncation');
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
console.log(`[context-manager] LLM summary: ${summary.length} chars`);
|
|
171
|
-
return summary.trim();
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
console.warn(`[context-manager] LLM summarization failed, falling back to truncation: ${err instanceof Error ? err.message : err}`);
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
// --- Fallback truncation (original mechanical approach) ---
|
|
179
|
-
function truncateAnthropicHead(head) {
|
|
180
|
-
return head.map(msg => {
|
|
181
|
-
if (!Array.isArray(msg.content))
|
|
182
|
-
return msg;
|
|
183
|
-
let changed = false;
|
|
184
|
-
const newContent = msg.content.map((block) => {
|
|
185
|
-
if (block.type !== 'tool_result')
|
|
186
|
-
return block;
|
|
187
|
-
const raw = typeof block.content === 'string'
|
|
188
|
-
? block.content
|
|
189
|
-
: JSON.stringify(block.content);
|
|
190
|
-
if (raw.length <= RESULT_MAX_CHARS)
|
|
191
|
-
return block;
|
|
192
|
-
changed = true;
|
|
193
|
-
return { ...block, content: raw.slice(0, RESULT_MAX_CHARS) + ' [truncated]' };
|
|
194
|
-
});
|
|
195
|
-
return changed ? { ...msg, content: newContent } : msg;
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
function truncateOpenAIHead(head) {
|
|
199
|
-
return head.map(msg => {
|
|
200
|
-
if (msg.role !== 'tool')
|
|
201
|
-
return msg;
|
|
202
|
-
if (typeof msg.content !== 'string')
|
|
203
|
-
return msg;
|
|
204
|
-
if (msg.content.length <= RESULT_MAX_CHARS)
|
|
205
|
-
return msg;
|
|
206
|
-
return { ...msg, content: msg.content.slice(0, RESULT_MAX_CHARS) + ' [truncated]' };
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
function truncateCodexHead(head) {
|
|
210
|
-
return head.map(item => {
|
|
211
|
-
if (item.type !== 'function_call_output')
|
|
212
|
-
return item;
|
|
213
|
-
if (typeof item.output !== 'string')
|
|
214
|
-
return item;
|
|
215
|
-
if (item.output.length <= RESULT_MAX_CHARS)
|
|
216
|
-
return item;
|
|
217
|
-
return { ...item, output: item.output.slice(0, RESULT_MAX_CHARS) + ' [truncated]' };
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
// --- Track whether we already compacted for a given conversation ---
|
|
221
|
-
// Key: a hash of the tail messages to avoid re-summarizing the same head repeatedly.
|
|
222
|
-
// This is a WeakMap so we don't leak memory across conversations.
|
|
223
|
-
const compactedMarker = new WeakSet();
|
|
300
|
+
// =====================================================================
|
|
301
|
+
// Legacy wrapper functions — delegate to generic compactMessages()
|
|
302
|
+
// These preserve backward compatibility for the old provider tool loops
|
|
303
|
+
// (anthropic.ts, codex.ts) until Phase 5 removes them.
|
|
304
|
+
// =====================================================================
|
|
224
305
|
/**
|
|
225
306
|
* Compact Anthropic-format apiMessages when over threshold.
|
|
226
|
-
*
|
|
227
|
-
* Does NOT mutate the input array — returns a new array.
|
|
307
|
+
* @deprecated Use compactMessages() with anthropicFormatHelper instead.
|
|
228
308
|
*/
|
|
229
309
|
export async function compactAnthropicMessages(messages, config, iteration = 0, fullConfig) {
|
|
230
|
-
|
|
231
|
-
return { messages, compacted: false };
|
|
232
|
-
const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
|
|
233
|
-
const estimated = estimateTokens(messages);
|
|
234
|
-
if (estimated <= maxTokens)
|
|
235
|
-
return { messages, compacted: false };
|
|
236
|
-
// If we already compacted this array (it has a summary message), use truncation fallback
|
|
237
|
-
// to progressively shrink rather than re-summarizing repeatedly.
|
|
238
|
-
if (compactedMarker.has(messages)) {
|
|
239
|
-
console.log(`[context-manager] Already compacted, using truncation fallback (iteration ${iteration})`);
|
|
240
|
-
const tail = messages.slice(-KEEP_TAIL);
|
|
241
|
-
const head = messages.slice(0, -KEEP_TAIL);
|
|
242
|
-
const result = [...truncateAnthropicHead(head), ...tail];
|
|
243
|
-
return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
|
|
244
|
-
}
|
|
245
|
-
console.log(`[context-manager] Compacting at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
|
|
246
|
-
const tail = messages.slice(-KEEP_TAIL);
|
|
247
|
-
const head = messages.slice(0, -KEEP_TAIL);
|
|
248
|
-
// Attempt LLM summarization
|
|
249
|
-
if (fullConfig) {
|
|
250
|
-
const transcript = serializeAnthropicMessages(head);
|
|
251
|
-
const summary = await llmSummarize(transcript, fullConfig, config?.compactionModel);
|
|
252
|
-
if (summary) {
|
|
253
|
-
const summaryMessage = {
|
|
254
|
-
role: 'user',
|
|
255
|
-
content: [{ type: 'text', text: `[Conversation Summary]\n${summary}` }],
|
|
256
|
-
};
|
|
257
|
-
const result = [summaryMessage, ...tail];
|
|
258
|
-
compactedMarker.add(result);
|
|
259
|
-
const tokensAfter = estimateTokens(result);
|
|
260
|
-
return { messages: result, compacted: true, method: 'llm', summary, tokensBefore: estimated, tokensAfter };
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
// Fallback: mechanical truncation
|
|
264
|
-
const result = [...truncateAnthropicHead(head), ...tail];
|
|
265
|
-
return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
|
|
310
|
+
return compactMessages(messages, anthropicFormatHelper, config, iteration, fullConfig);
|
|
266
311
|
}
|
|
267
312
|
/**
|
|
268
313
|
* Compact OpenAI-format apiMessages when over threshold.
|
|
269
|
-
*
|
|
270
|
-
* Does NOT mutate the input array — returns a new array.
|
|
314
|
+
* @deprecated Use compactMessages() with openaiFormatHelper instead.
|
|
271
315
|
*/
|
|
272
316
|
export async function compactOpenAIMessages(messages, config, iteration = 0, fullConfig) {
|
|
273
|
-
|
|
274
|
-
return { messages, compacted: false };
|
|
275
|
-
const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
|
|
276
|
-
const estimated = estimateTokens(messages);
|
|
277
|
-
if (estimated <= maxTokens)
|
|
278
|
-
return { messages, compacted: false };
|
|
279
|
-
if (compactedMarker.has(messages)) {
|
|
280
|
-
console.log(`[context-manager] Already compacted, using truncation fallback (iteration ${iteration})`);
|
|
281
|
-
const tail = messages.slice(-KEEP_TAIL);
|
|
282
|
-
const head = messages.slice(0, -KEEP_TAIL);
|
|
283
|
-
const result = [...truncateOpenAIHead(head), ...tail];
|
|
284
|
-
return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
|
|
285
|
-
}
|
|
286
|
-
console.log(`[context-manager] Compacting OpenAI messages at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
|
|
287
|
-
const tail = messages.slice(-KEEP_TAIL);
|
|
288
|
-
const head = messages.slice(0, -KEEP_TAIL);
|
|
289
|
-
// Attempt LLM summarization
|
|
290
|
-
if (fullConfig) {
|
|
291
|
-
const transcript = serializeOpenAIMessages(head);
|
|
292
|
-
const summary = await llmSummarize(transcript, fullConfig, config?.compactionModel);
|
|
293
|
-
if (summary) {
|
|
294
|
-
const summaryMessage = {
|
|
295
|
-
role: 'user',
|
|
296
|
-
content: `[Conversation Summary]\n${summary}`,
|
|
297
|
-
};
|
|
298
|
-
const result = [summaryMessage, ...tail];
|
|
299
|
-
compactedMarker.add(result);
|
|
300
|
-
const tokensAfter = estimateTokens(result);
|
|
301
|
-
return { messages: result, compacted: true, method: 'llm', summary, tokensBefore: estimated, tokensAfter };
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
// Fallback: mechanical truncation
|
|
305
|
-
const result = [...truncateOpenAIHead(head), ...tail];
|
|
306
|
-
return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
|
|
317
|
+
return compactMessages(messages, openaiFormatHelper, config, iteration, fullConfig);
|
|
307
318
|
}
|
|
308
319
|
/**
|
|
309
320
|
* Compact Codex-format input items when over threshold.
|
|
310
|
-
*
|
|
311
|
-
* Does NOT mutate the input array — returns a new array.
|
|
321
|
+
* @deprecated Use compactMessages() with codexFormatHelper instead.
|
|
312
322
|
*/
|
|
313
323
|
export async function compactCodexMessages(input, config, iteration = 0, fullConfig) {
|
|
314
|
-
|
|
315
|
-
return { messages: input, compacted: false };
|
|
316
|
-
const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
|
|
317
|
-
const estimated = estimateTokens(input);
|
|
318
|
-
if (estimated <= maxTokens)
|
|
319
|
-
return { messages: input, compacted: false };
|
|
320
|
-
if (compactedMarker.has(input)) {
|
|
321
|
-
console.log(`[context-manager] Already compacted, using truncation fallback (iteration ${iteration})`);
|
|
322
|
-
const tail = input.slice(-KEEP_TAIL);
|
|
323
|
-
const head = input.slice(0, -KEEP_TAIL);
|
|
324
|
-
const result = [...truncateCodexHead(head), ...tail];
|
|
325
|
-
return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
|
|
326
|
-
}
|
|
327
|
-
console.log(`[context-manager] Compacting Codex input at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
|
|
328
|
-
const tail = input.slice(-KEEP_TAIL);
|
|
329
|
-
const head = input.slice(0, -KEEP_TAIL);
|
|
330
|
-
// Attempt LLM summarization
|
|
331
|
-
if (fullConfig) {
|
|
332
|
-
const transcript = serializeCodexMessages(head);
|
|
333
|
-
const summary = await llmSummarize(transcript, fullConfig, config?.compactionModel);
|
|
334
|
-
if (summary) {
|
|
335
|
-
const summaryItem = {
|
|
336
|
-
type: 'message',
|
|
337
|
-
role: 'user',
|
|
338
|
-
content: `[Conversation Summary]\n${summary}`,
|
|
339
|
-
};
|
|
340
|
-
const result = [summaryItem, ...tail];
|
|
341
|
-
compactedMarker.add(result);
|
|
342
|
-
const tokensAfter = estimateTokens(result);
|
|
343
|
-
return { messages: result, compacted: true, method: 'llm', summary, tokensBefore: estimated, tokensAfter };
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// Fallback: mechanical truncation
|
|
347
|
-
const result = [...truncateCodexHead(head), ...tail];
|
|
348
|
-
return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
|
|
324
|
+
return compactMessages(input, codexFormatHelper, config, iteration, fullConfig);
|
|
349
325
|
}
|
|
350
326
|
// --- Exported helpers for testing ---
|
|
351
327
|
export { serializeAnthropicMessages, serializeOpenAIMessages, serializeCodexMessages };
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import type { Config, ChatMessage, ChatOptions, ToolConfig } from '../types.js';
|
|
2
2
|
import type { ExecuteToolContext } from '../tools/execute-context.js';
|
|
3
3
|
import type { ToolChatResult } from './types.js';
|
|
4
|
+
import type { ProviderAdapter } from './adapter.js';
|
|
4
5
|
export type { ToolChatResult, ProviderChatParams, ProviderToolChatParams } from './types.js';
|
|
6
|
+
export type { ProviderAdapter } from './adapter.js';
|
|
5
7
|
export { TOOL_GUARD, setUsingOAuth, isUsingOAuth, buildSystemParam, addToolCacheBreakpoint, contentToText, toOpenAITools, resolveModel, resolveProviderRoute, shouldUseCodexAliasProvider, getProvider, stripProvider, buildThinkingConfig, } from './utils.js';
|
|
6
8
|
export { toOpenAIContent, toCodexContent, toCodexToolDefinitions } from './content.js';
|
|
7
9
|
export { setLangfuseHelpers, toCostDetails, toAnthropicUsageDetails, toUsageDetails, toNumericUsageDetails, } from './observability.js';
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a provider name to a ProviderAdapter instance.
|
|
12
|
+
* Adapters are lightweight — creating one per call is fine.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getAdapter(provider: string): ProviderAdapter;
|
|
8
15
|
export { setAnthropicClient, isAnthropicAvailable, chatAnthropic, chatWithToolsAnthropic, } from './anthropic.js';
|
|
9
|
-
export { addOpenAIClient, getOpenAIClient, hasOpenAIClient, clearOpenAIClients, resetOpenAIProviderState, isOpenAIAvailable, chatOpenAI, chatWithToolsOpenAI, } from './openai.js';
|
|
10
16
|
export { addResponsesApiProvider, isResponsesApiProvider, setCodexAuthPath, setCodexBaseUrl, initCodexAuth, resetCodexProviderState, loadCodexAuth, getCodexAuth, isCodexAvailable, chatCodex, chatWithToolsCodex, } from './codex.js';
|
|
11
17
|
export declare function initProviders(config: Config): Promise<void>;
|
|
18
|
+
/** Unified chat function that routes to appropriate provider via adapter. */
|
|
12
19
|
export declare function chat(messages: ChatMessage[], options: ChatOptions, config: Config): Promise<string>;
|
|
20
|
+
/** Unified chatWithTools function that routes to appropriate provider via adapter. */
|
|
13
21
|
export declare function chatWithTools(messages: ChatMessage[], options: ChatOptions, config: Config, toolConfig: ToolConfig, toolContext?: ExecuteToolContext): Promise<ToolChatResult>;
|