codeep 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp/commands.js +56 -0
- package/dist/acp/server.js +3 -0
- package/dist/api/index.js +25 -5
- package/dist/renderer/App.js +4 -0
- package/dist/renderer/commands.js +59 -0
- package/dist/renderer/components/Help.js +2 -0
- package/dist/utils/agentChat.js +24 -3
- package/dist/utils/agentStream.js +21 -4
- package/dist/utils/planMode.d.ts +45 -0
- package/dist/utils/planMode.js +94 -0
- package/dist/utils/tokenTracker.d.ts +23 -0
- package/dist/utils/tokenTracker.js +56 -4
- package/package.json +1 -1
package/dist/acp/commands.js
CHANGED
|
@@ -600,6 +600,62 @@ Anything else the agent should know — edge cases, gotchas, things to double-ch
|
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
602
|
// ─── Export ────────────────────────────────────────────────────────────────
|
|
603
|
+
// ─── Plan mode (2.0.2) ────────────────────────────────────────────────────
|
|
604
|
+
case 'plan': {
|
|
605
|
+
// Identical contract to TUI /plan: generate a pre-execution plan,
|
|
606
|
+
// surface it, hold as pending so /go can execute it without re-planning.
|
|
607
|
+
if (!args.length) {
|
|
608
|
+
const { getPendingPlan } = await import('../utils/planMode.js');
|
|
609
|
+
const cur = getPendingPlan();
|
|
610
|
+
return {
|
|
611
|
+
handled: true,
|
|
612
|
+
response: cur
|
|
613
|
+
? `**Pending plan for:** _${cur.task}_\n\n${cur.plan}\n\n---\nRun \`/go\` to execute, or \`/plan <revised task>\` to revise.`
|
|
614
|
+
: 'Usage: `/plan <task>` — generates a plan you can review, then `/go` to execute.',
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const task = args.join(' ');
|
|
618
|
+
onChunk(`_Generating plan for: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}_\n\n`);
|
|
619
|
+
try {
|
|
620
|
+
const { generatePlan } = await import('../utils/planMode.js');
|
|
621
|
+
const plan = await generatePlan(task);
|
|
622
|
+
return {
|
|
623
|
+
handled: true,
|
|
624
|
+
response: `${plan}\n\n---\nRun \`/go\` to execute this plan, or \`/plan <revised task>\` to refine it.`,
|
|
625
|
+
streaming: true,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
return { handled: true, response: `Plan generation failed: ${err.message}`, streaming: true };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
case 'go': {
|
|
633
|
+
const { getPendingPlan, composeExecutionPrompt, clearPendingPlan } = await import('../utils/planMode.js');
|
|
634
|
+
const cur = getPendingPlan();
|
|
635
|
+
if (!cur) {
|
|
636
|
+
return { handled: true, response: 'No pending plan. Run `/plan <task>` first.' };
|
|
637
|
+
}
|
|
638
|
+
const prompt = composeExecutionPrompt(cur);
|
|
639
|
+
clearPendingPlan();
|
|
640
|
+
onChunk(`_Executing approved plan…_\n\n`);
|
|
641
|
+
try {
|
|
642
|
+
const { buildProjectContext } = await import('./session.js');
|
|
643
|
+
const ctx = buildProjectContext(session.workspaceRoot);
|
|
644
|
+
const agentResult = await runAgent(prompt, ctx, {
|
|
645
|
+
abortSignal,
|
|
646
|
+
onIteration: (_i, msg) => { onChunk(msg + '\n'); },
|
|
647
|
+
onThinking: (text) => { onChunk(text); },
|
|
648
|
+
});
|
|
649
|
+
return {
|
|
650
|
+
handled: true,
|
|
651
|
+
response: agentResult.finalResponse || '_(plan executed; no final summary)_',
|
|
652
|
+
streaming: true,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
return { handled: true, response: `Plan execution failed: ${err.message}`, streaming: true };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
603
659
|
case 'export': {
|
|
604
660
|
if (!session.history.length)
|
|
605
661
|
return { handled: true, response: 'No messages to export.' };
|
package/dist/acp/server.js
CHANGED
|
@@ -52,6 +52,9 @@ const AVAILABLE_COMMANDS = [
|
|
|
52
52
|
{ name: 'mcp', description: 'Manage MCP servers, marketplace, resources, prompts', input: { hint: '[browse | install <id> | add | remove | reload | resources | read <uri> | prompts | prompt <server> <name>]' } },
|
|
53
53
|
{ name: 'openrouter', description: 'OpenRouter routing preferences (prefer/ignore/fallbacks/privacy/clear)', input: { hint: '[show | prefer <p,...> | ignore <p,...> | fallbacks on|off | privacy strict|allow | clear]' } },
|
|
54
54
|
{ name: 'export', description: 'Export conversation', input: { hint: 'json | md | txt' } },
|
|
55
|
+
// Plan mode (2.0.2)
|
|
56
|
+
{ name: 'plan', description: 'Generate a numbered plan for a task — review before /go executes', input: { hint: '<task>' } },
|
|
57
|
+
{ name: 'go', description: 'Execute the pending plan from /plan' },
|
|
55
58
|
// Project intelligence
|
|
56
59
|
{ name: 'scan', description: 'Scan project structure and generate summary' },
|
|
57
60
|
{ name: 'review', description: 'Run code review on project or specific files', input: { hint: '[file…]' } },
|
package/dist/api/index.js
CHANGED
|
@@ -593,6 +593,13 @@ async function chatAnthropic(message, history, model, apiKey, onChunk, abortSign
|
|
|
593
593
|
headers['x-api-key'] = apiKey;
|
|
594
594
|
}
|
|
595
595
|
try {
|
|
596
|
+
// Anthropic prompt caching: wrap system as an array with a
|
|
597
|
+
// `cache_control` marker so the static system prompt (typically large
|
|
598
|
+
// and stable across a session) is cached. Below 1024 input tokens
|
|
599
|
+
// Anthropic silently skips caching — no error.
|
|
600
|
+
const cachedSystem = useNativeSystem
|
|
601
|
+
? { system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }] }
|
|
602
|
+
: {};
|
|
596
603
|
const response = await fetch(`${baseUrl}/v1/messages`, {
|
|
597
604
|
method: 'POST',
|
|
598
605
|
headers,
|
|
@@ -602,7 +609,7 @@ async function chatAnthropic(message, history, model, apiKey, onChunk, abortSign
|
|
|
602
609
|
max_tokens: maxTokens,
|
|
603
610
|
temperature,
|
|
604
611
|
stream,
|
|
605
|
-
...
|
|
612
|
+
...cachedSystem,
|
|
606
613
|
}),
|
|
607
614
|
signal: controller.signal,
|
|
608
615
|
});
|
|
@@ -649,6 +656,8 @@ async function handleAnthropicStream(body, onChunk) {
|
|
|
649
656
|
let buffer = '';
|
|
650
657
|
let inputTokens = 0;
|
|
651
658
|
let outputTokens = 0;
|
|
659
|
+
let cacheCreationTokens = 0;
|
|
660
|
+
let cacheReadTokens = 0;
|
|
652
661
|
let streamModel = '';
|
|
653
662
|
while (true) {
|
|
654
663
|
const { done, value } = await reader.read();
|
|
@@ -669,9 +678,13 @@ async function handleAnthropicStream(body, onChunk) {
|
|
|
669
678
|
onChunk(text);
|
|
670
679
|
}
|
|
671
680
|
}
|
|
672
|
-
// message_start contains input_tokens
|
|
681
|
+
// message_start contains input_tokens (and cache create/read
|
|
682
|
+
// when prompt caching is in play).
|
|
673
683
|
if (parsed.type === 'message_start' && parsed.message?.usage) {
|
|
674
|
-
|
|
684
|
+
const u = parsed.message.usage;
|
|
685
|
+
inputTokens = u.input_tokens || 0;
|
|
686
|
+
cacheCreationTokens = u.cache_creation_input_tokens || 0;
|
|
687
|
+
cacheReadTokens = u.cache_read_input_tokens || 0;
|
|
675
688
|
streamModel = parsed.message.model || '';
|
|
676
689
|
}
|
|
677
690
|
// message_delta contains output_tokens
|
|
@@ -686,8 +699,15 @@ async function handleAnthropicStream(body, onChunk) {
|
|
|
686
699
|
}
|
|
687
700
|
}
|
|
688
701
|
// Record token usage
|
|
689
|
-
if (inputTokens > 0 || outputTokens > 0) {
|
|
690
|
-
|
|
702
|
+
if (inputTokens > 0 || outputTokens > 0 || cacheReadTokens > 0 || cacheCreationTokens > 0) {
|
|
703
|
+
const totalPrompt = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
704
|
+
recordTokenUsage({
|
|
705
|
+
promptTokens: totalPrompt,
|
|
706
|
+
completionTokens: outputTokens,
|
|
707
|
+
totalTokens: totalPrompt + outputTokens,
|
|
708
|
+
cacheCreationTokens: cacheCreationTokens || undefined,
|
|
709
|
+
cacheReadTokens: cacheReadTokens || undefined,
|
|
710
|
+
}, streamModel || 'unknown', config.get('provider'));
|
|
691
711
|
}
|
|
692
712
|
// Strip <think> tags from MiniMax responses
|
|
693
713
|
return stripThinkTags(chunks.join(''));
|
package/dist/renderer/App.js
CHANGED
|
@@ -91,6 +91,8 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
91
91
|
'hooks': 'List installed lifecycle hooks (.codeep/hooks/<event>.sh)',
|
|
92
92
|
'mcp': 'Manage MCP servers (browse, install, add, remove, resources, prompts)',
|
|
93
93
|
'openrouter': 'Tune OpenRouter routing (preferred / ignore providers, fallbacks, privacy)',
|
|
94
|
+
'plan': 'Generate a numbered plan for a task — review before /go executes it',
|
|
95
|
+
'go': 'Execute the pending plan from /plan',
|
|
94
96
|
};
|
|
95
97
|
import { helpCategories, keyboardShortcuts } from './components/Help.js';
|
|
96
98
|
import { handleSettingsKey, SETTINGS } from './components/Settings.js';
|
|
@@ -230,6 +232,8 @@ export class App {
|
|
|
230
232
|
// Keep in lockstep with COMMAND_DESCRIPTIONS below and helpCategories.
|
|
231
233
|
'compact', 'commands', 'checkpoint', 'checkpoints', 'rewind',
|
|
232
234
|
'hooks', 'mcp', 'openrouter',
|
|
235
|
+
// 2.0.2 — plan mode.
|
|
236
|
+
'plan', 'go',
|
|
233
237
|
'c', 't', 'd', 'r', 'f', 'e', 'o', 'b', 'p',
|
|
234
238
|
];
|
|
235
239
|
constructor(options) {
|
|
@@ -216,6 +216,65 @@ export async function handleCommand(command, args, ctx) {
|
|
|
216
216
|
runAgentTask(args.join(' '), true, ctx, () => null, () => { });
|
|
217
217
|
break;
|
|
218
218
|
}
|
|
219
|
+
case 'plan': {
|
|
220
|
+
// Plan mode: ask the model for a plan, surface it, hold as pending.
|
|
221
|
+
// The user runs /go to execute or /plan <revised> to revise. See
|
|
222
|
+
// src/utils/planMode.ts for the rationale + system prompt.
|
|
223
|
+
if (!args.length) {
|
|
224
|
+
const { getPendingPlan } = await import('../utils/planMode.js');
|
|
225
|
+
const cur = getPendingPlan();
|
|
226
|
+
if (cur) {
|
|
227
|
+
ctx.app.addMessage({
|
|
228
|
+
role: 'system',
|
|
229
|
+
content: `**Pending plan for:** _${cur.task}_\n\n${cur.plan}\n\n---\nRun \`/go\` to execute, or \`/plan <revised task>\` to revise.`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
ctx.app.notify('Usage: /plan <task> — generates a plan you can review, then /go to execute.');
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (ctx.isAgentRunning()) {
|
|
238
|
+
ctx.app.notify('Agent already running. Use /stop first.');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const task = args.join(' ');
|
|
242
|
+
ctx.app.addMessage({ role: 'user', content: `/plan ${task}` });
|
|
243
|
+
ctx.app.notify('Generating plan…');
|
|
244
|
+
try {
|
|
245
|
+
const { generatePlan } = await import('../utils/planMode.js');
|
|
246
|
+
const plan = await generatePlan(task);
|
|
247
|
+
ctx.app.addMessage({
|
|
248
|
+
role: 'assistant',
|
|
249
|
+
content: `${plan}\n\n---\nRun \`/go\` to execute this plan, or \`/plan <revised task>\` to refine it.`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
ctx.app.notify(`Plan generation failed: ${err.message}`);
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'go': {
|
|
258
|
+
// Execute the pending plan from /plan. The agent loop sees the
|
|
259
|
+
// task + plan as a single prompt, so MCP tools, hooks, permissions,
|
|
260
|
+
// and verification all apply unchanged.
|
|
261
|
+
const { getPendingPlan, composeExecutionPrompt, clearPendingPlan } = await import('../utils/planMode.js');
|
|
262
|
+
const cur = getPendingPlan();
|
|
263
|
+
if (!cur) {
|
|
264
|
+
ctx.app.notify('No pending plan. Run `/plan <task>` first.');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (ctx.isAgentRunning()) {
|
|
268
|
+
ctx.app.notify('Agent already running. Use /stop first.');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const prompt = composeExecutionPrompt(cur);
|
|
272
|
+
clearPendingPlan();
|
|
273
|
+
ctx.app.notify(`Executing plan for: ${cur.task.slice(0, 80)}${cur.task.length > 80 ? '…' : ''}`);
|
|
274
|
+
const { runAgentTask } = await import('./agentExecution.js');
|
|
275
|
+
runAgentTask(prompt, false, ctx, () => null, () => { });
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
219
278
|
case 'stop': {
|
|
220
279
|
if (ctx.isAgentRunning() && ctx.abortController) {
|
|
221
280
|
ctx.abortController.abort();
|
|
@@ -46,6 +46,8 @@ export const helpCategories = [
|
|
|
46
46
|
items: [
|
|
47
47
|
{ key: '/agent <task>', description: 'Run agent with task' },
|
|
48
48
|
{ key: '/agent-dry <task>', description: 'Dry run (no changes)' },
|
|
49
|
+
{ key: '/plan <task>', description: 'Generate a plan first — review before /go executes' },
|
|
50
|
+
{ key: '/go', description: 'Execute the pending plan from /plan' },
|
|
49
51
|
{ key: '/stop', description: 'Stop running agent' },
|
|
50
52
|
{ key: '/undo', description: 'Undo last agent action' },
|
|
51
53
|
{ key: '/undo-all', description: 'Undo all agent actions' },
|
package/dist/utils/agentChat.js
CHANGED
|
@@ -301,9 +301,25 @@ additionalTools) {
|
|
|
301
301
|
}
|
|
302
302
|
else {
|
|
303
303
|
endpoint = `${baseUrl}/v1/messages`;
|
|
304
|
+
// Anthropic prompt caching. Two cache breakpoints:
|
|
305
|
+
// 1. `system` (largest stable block — system prompt + skills catalog)
|
|
306
|
+
// 2. last tool in `tools` (Anthropic caches everything up to and
|
|
307
|
+
// including the marker, so this caches the entire tools array)
|
|
308
|
+
// Cache hits cost 0.1× input. Misses ("cache creation") cost 1.25×.
|
|
309
|
+
// Net win after the 2nd same-shape request. Below 1024 input tokens
|
|
310
|
+
// Anthropic silently skips caching — no error path to handle.
|
|
311
|
+
const anthropicTools = getAnthropicTools(additionalTools);
|
|
312
|
+
const cachedTools = anthropicTools.length > 0
|
|
313
|
+
? [
|
|
314
|
+
...anthropicTools.slice(0, -1),
|
|
315
|
+
{ ...anthropicTools[anthropicTools.length - 1], cache_control: { type: 'ephemeral' } },
|
|
316
|
+
]
|
|
317
|
+
: anthropicTools;
|
|
304
318
|
body = {
|
|
305
|
-
model,
|
|
306
|
-
|
|
319
|
+
model,
|
|
320
|
+
system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }],
|
|
321
|
+
messages,
|
|
322
|
+
tools: cachedTools, stream: useStreaming,
|
|
307
323
|
...tempParam, max_tokens: getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384)),
|
|
308
324
|
};
|
|
309
325
|
}
|
|
@@ -435,10 +451,15 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
|
|
|
435
451
|
}
|
|
436
452
|
else {
|
|
437
453
|
endpoint = `${baseUrl}/v1/messages`;
|
|
454
|
+
// Fallback path injects system+tools as the first user message
|
|
455
|
+
// (no native tool API). Cache that block — it's large and stable.
|
|
438
456
|
body = {
|
|
439
457
|
model,
|
|
440
458
|
messages: [
|
|
441
|
-
{
|
|
459
|
+
{
|
|
460
|
+
role: 'user',
|
|
461
|
+
content: [{ type: 'text', text: fallbackPrompt, cache_control: { type: 'ephemeral' } }],
|
|
462
|
+
},
|
|
442
463
|
{ role: 'assistant', content: 'Understood. I will use the tools as specified.' },
|
|
443
464
|
...messages,
|
|
444
465
|
],
|
|
@@ -169,13 +169,30 @@ export async function handleAnthropicAgentStream(body, onChunk, model, providerI
|
|
|
169
169
|
const data = line.slice(6);
|
|
170
170
|
try {
|
|
171
171
|
const parsed = JSON.parse(data);
|
|
172
|
-
// message_start has input tokens
|
|
172
|
+
// message_start has input tokens (incl. cache create/read fields if
|
|
173
|
+
// prompt caching is in use); message_delta has output tokens —
|
|
174
|
+
// merge both so extractAnthropicUsage sees the full picture.
|
|
173
175
|
if (parsed.type === 'message_start' && parsed.message?.usage) {
|
|
174
|
-
|
|
176
|
+
const u = parsed.message.usage;
|
|
177
|
+
usageData = {
|
|
178
|
+
usage: {
|
|
179
|
+
input_tokens: u.input_tokens || 0,
|
|
180
|
+
output_tokens: 0,
|
|
181
|
+
cache_creation_input_tokens: u.cache_creation_input_tokens || 0,
|
|
182
|
+
cache_read_input_tokens: u.cache_read_input_tokens || 0,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
175
185
|
}
|
|
176
186
|
else if (parsed.type === 'message_delta' && parsed.usage) {
|
|
177
|
-
const
|
|
178
|
-
usageData = {
|
|
187
|
+
const prev = usageData?.usage ?? {};
|
|
188
|
+
usageData = {
|
|
189
|
+
usage: {
|
|
190
|
+
input_tokens: prev.input_tokens || 0,
|
|
191
|
+
output_tokens: parsed.usage.output_tokens || 0,
|
|
192
|
+
cache_creation_input_tokens: prev.cache_creation_input_tokens || 0,
|
|
193
|
+
cache_read_input_tokens: prev.cache_read_input_tokens || 0,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
179
196
|
}
|
|
180
197
|
if (parsed.type === 'content_block_start') {
|
|
181
198
|
const block = parsed.content_block;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan mode — explicit pre-execution preview.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. User runs `/plan <task>` — we ask the LLM for a numbered plan
|
|
6
|
+
* (no tool calls, no file changes) and surface it to the user.
|
|
7
|
+
* 2. We hold the (task, plan) pair as the *pending* plan, scoped to
|
|
8
|
+
* the current process.
|
|
9
|
+
* 3. User runs `/go` to execute, or `/plan <revised task>` to refine.
|
|
10
|
+
* `/go` hands the original task + approved plan to the regular
|
|
11
|
+
* agent loop as a single prompt, so the existing tool execution,
|
|
12
|
+
* verification, and permission paths apply unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Why this design (MVP):
|
|
15
|
+
* - Zero changes to the agent loop, MCP wiring, or ACP server.
|
|
16
|
+
* - Plan rendering reuses the chat markdown renderer (no new TUI
|
|
17
|
+
* panel to maintain).
|
|
18
|
+
* - Edit flow is just "/plan <revised task>" — generates a new plan
|
|
19
|
+
* that replaces the pending one; user pays one extra LLM call but
|
|
20
|
+
* gets human-readable revision history in the chat above.
|
|
21
|
+
* - When we ship a proper plan-mode UI later (TUI panel with
|
|
22
|
+
* Accept/Edit/Reject buttons + per-step progress), it can keep
|
|
23
|
+
* using this module as the backend.
|
|
24
|
+
*/
|
|
25
|
+
export interface PendingPlan {
|
|
26
|
+
task: string;
|
|
27
|
+
plan: string;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ask the model for a plan for the given task. Stores the (task, plan)
|
|
32
|
+
* pair as the pending plan so a subsequent `/go` can execute it.
|
|
33
|
+
* Throws on chat failure — caller renders the error.
|
|
34
|
+
*/
|
|
35
|
+
export declare function generatePlan(task: string, onChunk?: (text: string) => void): Promise<string>;
|
|
36
|
+
export declare function getPendingPlan(): PendingPlan | null;
|
|
37
|
+
export declare function clearPendingPlan(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Compose the prompt the agent loop receives when the user runs `/go`.
|
|
40
|
+
* The agent treats this as a normal task, so tool calls / verification /
|
|
41
|
+
* permissions / hooks all flow through the existing paths — we just
|
|
42
|
+
* front-load the plan as context so the model doesn't re-plan
|
|
43
|
+
* implicitly.
|
|
44
|
+
*/
|
|
45
|
+
export declare function composeExecutionPrompt(plan: PendingPlan): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan mode — explicit pre-execution preview.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. User runs `/plan <task>` — we ask the LLM for a numbered plan
|
|
6
|
+
* (no tool calls, no file changes) and surface it to the user.
|
|
7
|
+
* 2. We hold the (task, plan) pair as the *pending* plan, scoped to
|
|
8
|
+
* the current process.
|
|
9
|
+
* 3. User runs `/go` to execute, or `/plan <revised task>` to refine.
|
|
10
|
+
* `/go` hands the original task + approved plan to the regular
|
|
11
|
+
* agent loop as a single prompt, so the existing tool execution,
|
|
12
|
+
* verification, and permission paths apply unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Why this design (MVP):
|
|
15
|
+
* - Zero changes to the agent loop, MCP wiring, or ACP server.
|
|
16
|
+
* - Plan rendering reuses the chat markdown renderer (no new TUI
|
|
17
|
+
* panel to maintain).
|
|
18
|
+
* - Edit flow is just "/plan <revised task>" — generates a new plan
|
|
19
|
+
* that replaces the pending one; user pays one extra LLM call but
|
|
20
|
+
* gets human-readable revision history in the chat above.
|
|
21
|
+
* - When we ship a proper plan-mode UI later (TUI panel with
|
|
22
|
+
* Accept/Edit/Reject buttons + per-step progress), it can keep
|
|
23
|
+
* using this module as the backend.
|
|
24
|
+
*/
|
|
25
|
+
import { chat } from '../api/index.js';
|
|
26
|
+
const PLAN_SYSTEM_PROMPT = `You are in PLAN MODE.
|
|
27
|
+
|
|
28
|
+
The user has given you a task. **Do not execute anything.** Do not call
|
|
29
|
+
tools. Do not modify files. Do not run shell commands. Instead, produce
|
|
30
|
+
a numbered plan of the steps you would take, so the user can review and
|
|
31
|
+
approve before any changes happen.
|
|
32
|
+
|
|
33
|
+
Format your response as Markdown:
|
|
34
|
+
|
|
35
|
+
## Plan: <one-line summary of the task>
|
|
36
|
+
|
|
37
|
+
1. **<short step name>** — what you'd do (e.g. read file, edit lines, run command).
|
|
38
|
+
_Why:_ rationale (1 sentence).
|
|
39
|
+
_Expected outcome:_ what the user should see after this step.
|
|
40
|
+
|
|
41
|
+
2. **<step name>** — …
|
|
42
|
+
|
|
43
|
+
(continue numbering)
|
|
44
|
+
|
|
45
|
+
End with these three lines (verbatim labels, fill in values):
|
|
46
|
+
|
|
47
|
+
**Risk:** <low | medium | high> — <one-sentence reason; e.g. "schema change, requires migration">
|
|
48
|
+
**Files affected:** <comma-separated list of paths you'd touch, or "none">
|
|
49
|
+
**Commands run:** <comma-separated shell commands, or "none">
|
|
50
|
+
|
|
51
|
+
Rules:
|
|
52
|
+
- Be concrete. Reference real file paths from the project if you know them.
|
|
53
|
+
- Keep steps small enough that each maps to one or two tool calls when
|
|
54
|
+
later executed.
|
|
55
|
+
- If the task is ambiguous, list the assumption(s) you're making at the
|
|
56
|
+
top of the plan ("Assumes: ...") so the user can correct before /go.
|
|
57
|
+
- Do NOT produce code — describe what you'd change, not the change
|
|
58
|
+
itself. Code generation belongs in execution, not planning.
|
|
59
|
+
- If the task is trivial (single-file rename, single-line edit), say so
|
|
60
|
+
in one sentence and skip the formal plan — don't bloat tiny work.`;
|
|
61
|
+
let pending = null;
|
|
62
|
+
/**
|
|
63
|
+
* Ask the model for a plan for the given task. Stores the (task, plan)
|
|
64
|
+
* pair as the pending plan so a subsequent `/go` can execute it.
|
|
65
|
+
* Throws on chat failure — caller renders the error.
|
|
66
|
+
*/
|
|
67
|
+
export async function generatePlan(task, onChunk) {
|
|
68
|
+
const history = [
|
|
69
|
+
{ role: 'system', content: PLAN_SYSTEM_PROMPT },
|
|
70
|
+
];
|
|
71
|
+
const plan = await chat(task, history, onChunk);
|
|
72
|
+
pending = { task, plan, createdAt: Date.now() };
|
|
73
|
+
return plan;
|
|
74
|
+
}
|
|
75
|
+
export function getPendingPlan() {
|
|
76
|
+
return pending;
|
|
77
|
+
}
|
|
78
|
+
export function clearPendingPlan() {
|
|
79
|
+
pending = null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Compose the prompt the agent loop receives when the user runs `/go`.
|
|
83
|
+
* The agent treats this as a normal task, so tool calls / verification /
|
|
84
|
+
* permissions / hooks all flow through the existing paths — we just
|
|
85
|
+
* front-load the plan as context so the model doesn't re-plan
|
|
86
|
+
* implicitly.
|
|
87
|
+
*/
|
|
88
|
+
export function composeExecutionPrompt(plan) {
|
|
89
|
+
return `${plan.task}
|
|
90
|
+
|
|
91
|
+
I've reviewed the following plan and approved it. Execute it step by step. If any step turns out to be wrong or impossible mid-execution, stop and report — don't silently improvise.
|
|
92
|
+
|
|
93
|
+
${plan.plan}`;
|
|
94
|
+
}
|
|
@@ -5,6 +5,13 @@ export interface TokenUsage {
|
|
|
5
5
|
promptTokens: number;
|
|
6
6
|
completionTokens: number;
|
|
7
7
|
totalTokens: number;
|
|
8
|
+
/** Anthropic prompt caching: tokens written to the cache on this call
|
|
9
|
+
* (billed at ~1.25× input rate). Undefined for providers that don't
|
|
10
|
+
* support caching or for calls below the cache size threshold. */
|
|
11
|
+
cacheCreationTokens?: number;
|
|
12
|
+
/** Anthropic prompt caching: tokens read from cache on this call
|
|
13
|
+
* (billed at ~0.1× input rate — the big savings live here). */
|
|
14
|
+
cacheReadTokens?: number;
|
|
8
15
|
}
|
|
9
16
|
export interface SessionTokenStats {
|
|
10
17
|
totalPromptTokens: number;
|
|
@@ -18,6 +25,9 @@ interface TokenRecord {
|
|
|
18
25
|
promptTokens: number;
|
|
19
26
|
completionTokens: number;
|
|
20
27
|
totalTokens: number;
|
|
28
|
+
/** Anthropic prompt caching breakdown — see TokenUsage. */
|
|
29
|
+
cacheCreationTokens?: number;
|
|
30
|
+
cacheReadTokens?: number;
|
|
21
31
|
model: string;
|
|
22
32
|
provider: string;
|
|
23
33
|
/** Authoritative per-call USD from the provider (OpenRouter), if available. */
|
|
@@ -59,6 +69,19 @@ export interface ProviderCostBreakdown {
|
|
|
59
69
|
* Get cost breakdown grouped by provider/model
|
|
60
70
|
*/
|
|
61
71
|
export declare function getCostBreakdown(): ProviderCostBreakdown[];
|
|
72
|
+
/**
|
|
73
|
+
* Aggregate Anthropic prompt-caching stats for the current session.
|
|
74
|
+
* Returns the breakdown plus an estimate of what the input billing would
|
|
75
|
+
* have been *without* caching, so we can surface "you saved $X" to the
|
|
76
|
+
* user.
|
|
77
|
+
*/
|
|
78
|
+
export interface CacheStats {
|
|
79
|
+
cacheCreationTokens: number;
|
|
80
|
+
cacheReadTokens: number;
|
|
81
|
+
/** Sum of estimatedSavings across all Anthropic-priced records. */
|
|
82
|
+
estimatedSavingsUsd: number;
|
|
83
|
+
}
|
|
84
|
+
export declare function getCacheStats(): CacheStats;
|
|
62
85
|
/**
|
|
63
86
|
* Get session stats
|
|
64
87
|
*/
|
|
@@ -81,6 +81,8 @@ export function recordTokenUsage(usage, model, provider, actualCostUsd) {
|
|
|
81
81
|
promptTokens: usage.promptTokens,
|
|
82
82
|
completionTokens: usage.completionTokens,
|
|
83
83
|
totalTokens: usage.totalTokens,
|
|
84
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
85
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
84
86
|
model,
|
|
85
87
|
provider,
|
|
86
88
|
actualCostUsd,
|
|
@@ -104,10 +106,19 @@ export function extractOpenAIUsage(data) {
|
|
|
104
106
|
*/
|
|
105
107
|
export function extractAnthropicUsage(data) {
|
|
106
108
|
if (data?.usage) {
|
|
109
|
+
const inputTokens = data.usage.input_tokens || 0;
|
|
110
|
+
const outputTokens = data.usage.output_tokens || 0;
|
|
111
|
+
const cacheCreation = data.usage.cache_creation_input_tokens || 0;
|
|
112
|
+
const cacheRead = data.usage.cache_read_input_tokens || 0;
|
|
113
|
+
// Anthropic returns input_tokens EXCLUSIVE of cache creation and cache
|
|
114
|
+
// read tokens — they're reported separately. Total prompt = sum of all
|
|
115
|
+
// three so our context window math doesn't undercount.
|
|
107
116
|
return {
|
|
108
|
-
promptTokens:
|
|
109
|
-
completionTokens:
|
|
110
|
-
totalTokens:
|
|
117
|
+
promptTokens: inputTokens + cacheCreation + cacheRead,
|
|
118
|
+
completionTokens: outputTokens,
|
|
119
|
+
totalTokens: inputTokens + cacheCreation + cacheRead + outputTokens,
|
|
120
|
+
cacheCreationTokens: cacheCreation || undefined,
|
|
121
|
+
cacheReadTokens: cacheRead || undefined,
|
|
111
122
|
};
|
|
112
123
|
}
|
|
113
124
|
return null;
|
|
@@ -132,13 +143,42 @@ export function getCostBreakdown() {
|
|
|
132
143
|
else {
|
|
133
144
|
const pricing = MODEL_PRICING[record.model];
|
|
134
145
|
if (pricing) {
|
|
135
|
-
|
|
146
|
+
// Anthropic prompt caching: cache_creation_input is billed at 1.25×
|
|
147
|
+
// the base input rate, cache_read_input at 0.1×. The remaining
|
|
148
|
+
// (uncached) prompt tokens bill at the standard 1.0× rate.
|
|
149
|
+
const cacheCreate = record.cacheCreationTokens ?? 0;
|
|
150
|
+
const cacheRead = record.cacheReadTokens ?? 0;
|
|
151
|
+
const uncachedPrompt = Math.max(0, record.promptTokens - cacheCreate - cacheRead);
|
|
152
|
+
existing.estimatedCost +=
|
|
153
|
+
(uncachedPrompt / 1_000_000) * pricing.inputPer1M
|
|
154
|
+
+ (cacheCreate / 1_000_000) * pricing.inputPer1M * 1.25
|
|
155
|
+
+ (cacheRead / 1_000_000) * pricing.inputPer1M * 0.1
|
|
156
|
+
+ (record.completionTokens / 1_000_000) * pricing.outputPer1M;
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
159
|
grouped.set(key, existing);
|
|
139
160
|
}
|
|
140
161
|
return Array.from(grouped.values());
|
|
141
162
|
}
|
|
163
|
+
export function getCacheStats() {
|
|
164
|
+
let cacheCreate = 0;
|
|
165
|
+
let cacheRead = 0;
|
|
166
|
+
let savings = 0;
|
|
167
|
+
for (const record of records) {
|
|
168
|
+
cacheCreate += record.cacheCreationTokens ?? 0;
|
|
169
|
+
cacheRead += record.cacheReadTokens ?? 0;
|
|
170
|
+
// Savings = what cache-read tokens would have cost at full input rate,
|
|
171
|
+
// minus what they actually cost at 0.1×. (Cache creation is a slight
|
|
172
|
+
// *penalty* of 0.25× — netted in for honest reporting.)
|
|
173
|
+
const pricing = MODEL_PRICING[record.model];
|
|
174
|
+
if (pricing) {
|
|
175
|
+
const cReadSaved = ((record.cacheReadTokens ?? 0) / 1_000_000) * pricing.inputPer1M * 0.9;
|
|
176
|
+
const cCreateCost = ((record.cacheCreationTokens ?? 0) / 1_000_000) * pricing.inputPer1M * 0.25;
|
|
177
|
+
savings += cReadSaved - cCreateCost;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { cacheCreationTokens: cacheCreate, cacheReadTokens: cacheRead, estimatedSavingsUsd: Math.max(0, savings) };
|
|
181
|
+
}
|
|
142
182
|
/**
|
|
143
183
|
* Get session stats
|
|
144
184
|
*/
|
|
@@ -207,6 +247,18 @@ export function formatCostReport() {
|
|
|
207
247
|
lines.push(`| \`${b.provider}\` / \`${b.model}\` | ${formatTokenCount(b.promptTokens)} | ${formatTokenCount(b.completionTokens)} | $${b.estimatedCost.toFixed(4)} |`);
|
|
208
248
|
}
|
|
209
249
|
}
|
|
250
|
+
// Prompt caching summary — only shown if at least one cached call landed.
|
|
251
|
+
const cache = getCacheStats();
|
|
252
|
+
if (cache.cacheReadTokens > 0 || cache.cacheCreationTokens > 0) {
|
|
253
|
+
lines.push('', '### Prompt caching');
|
|
254
|
+
lines.push(`**Cache reads:** ${formatTokenCount(cache.cacheReadTokens)} tokens (billed at 0.1× input rate)`);
|
|
255
|
+
if (cache.cacheCreationTokens > 0) {
|
|
256
|
+
lines.push(`**Cache writes:** ${formatTokenCount(cache.cacheCreationTokens)} tokens (billed at 1.25× input rate)`);
|
|
257
|
+
}
|
|
258
|
+
if (cache.estimatedSavingsUsd > 0) {
|
|
259
|
+
lines.push(`**Estimated savings vs no caching:** $${cache.estimatedSavingsUsd.toFixed(4)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
210
262
|
// Models with no pricing entry don't contribute to cost — flag so users
|
|
211
263
|
// aren't surprised the total looks low.
|
|
212
264
|
const untracked = breakdown.filter(b => b.estimatedCost === 0 && (b.promptTokens + b.completionTokens) > 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|