codeep 2.1.3 → 2.3.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 +46 -2
- package/dist/acp/commands.js +57 -0
- package/dist/acp/server.js +5 -1
- package/dist/config/index.d.ts +19 -0
- package/dist/config/index.js +27 -0
- package/dist/renderer/App.js +6 -0
- package/dist/renderer/commands.js +96 -0
- package/dist/renderer/components/Help.js +4 -0
- package/dist/renderer/components/Settings.js +10 -0
- package/dist/utils/agent.d.ts +14 -0
- package/dist/utils/agent.js +203 -10
- package/dist/utils/agentChat.d.ts +15 -0
- package/dist/utils/agentChat.js +79 -0
- package/dist/utils/agents.d.ts +57 -0
- package/dist/utils/agents.js +188 -0
- package/dist/utils/codeepCloud.d.ts +5 -0
- package/dist/utils/codeepCloud.js +58 -0
- package/dist/utils/shell.js +36 -0
- package/dist/utils/userProfile.d.ts +99 -0
- package/dist/utils/userProfile.js +351 -0
- package/package.json +1 -1
package/dist/utils/agent.js
CHANGED
|
@@ -11,8 +11,9 @@ const debug = (...args) => {
|
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
13
|
// Import chat layer (prompt building + API calls)
|
|
14
|
-
import { agentChat, getAgentSystemPrompt, getFallbackSystemPrompt, loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent, } from './agentChat.js';
|
|
14
|
+
import { agentChat, getAgentSystemPrompt, getFallbackSystemPrompt, loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent, summarizeEarlierHistory, } from './agentChat.js';
|
|
15
15
|
import { ApiError } from '../api/index.js';
|
|
16
|
+
import { loadUserProfilePrompt } from './userProfile.js';
|
|
16
17
|
export { loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent };
|
|
17
18
|
/**
|
|
18
19
|
* Calculate dynamic timeout based on task complexity
|
|
@@ -127,8 +128,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
127
128
|
const startTime = Date.now();
|
|
128
129
|
const actions = [];
|
|
129
130
|
const messages = [];
|
|
130
|
-
// Start history session for undo support
|
|
131
|
-
|
|
131
|
+
// Start history session for undo support. Skipped for nested (delegated)
|
|
132
|
+
// runs so we don't reset the parent's currentSession singleton — the
|
|
133
|
+
// sub-agent's actions still record into the parent's open session.
|
|
134
|
+
const sessionId = opts.nested ? '' : startSession(prompt, projectContext.root || process.cwd());
|
|
132
135
|
// Task planning phase (if enabled)
|
|
133
136
|
// Use planning for complex keywords or multi-word prompts
|
|
134
137
|
let taskPlan = null;
|
|
@@ -221,10 +224,53 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
221
224
|
catch {
|
|
222
225
|
// Skill loading failure shouldn't fail the whole agent run.
|
|
223
226
|
}
|
|
227
|
+
// Sub-agents — the `delegate` tool lets the top-level agent hand a
|
|
228
|
+
// self-contained sub-task to a specialist that runs in its own context and
|
|
229
|
+
// returns a summary. Only advertised at depth 0, so sub-agents can't recurse
|
|
230
|
+
// (delegation depth is capped at 1 for v1).
|
|
231
|
+
let agentsCatalogBlock = '';
|
|
232
|
+
if ((opts.depth ?? 0) === 0) {
|
|
233
|
+
try {
|
|
234
|
+
const { loadAgents, formatAgentsForSysprompt } = await import('./agents.js');
|
|
235
|
+
const agents = loadAgents(projectContext.root);
|
|
236
|
+
if (agents.length > 0) {
|
|
237
|
+
mcpToolDefs.push({
|
|
238
|
+
name: 'delegate',
|
|
239
|
+
description: 'Delegate a self-contained sub-task to a specialist sub-agent that runs in its own fresh context and returns a summary. Use it to keep your own context focused.',
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
agent: { type: 'string', description: 'Sub-agent name from the catalog (e.g. "researcher"). Omit for a general-purpose sub-agent.' },
|
|
244
|
+
task: { type: 'string', description: 'A clear, self-contained instruction for the sub-agent.' },
|
|
245
|
+
},
|
|
246
|
+
required: ['task'],
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
agentsCatalogBlock = formatAgentsForSysprompt(agents);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Agent loading must never block the run.
|
|
254
|
+
}
|
|
255
|
+
}
|
|
224
256
|
// Build system prompt - use fallback format if native tools not supported
|
|
225
257
|
let systemPrompt = useNativeTools
|
|
226
258
|
? getAgentSystemPrompt(projectContext)
|
|
227
259
|
: getFallbackSystemPrompt(projectContext, mcpToolDefs);
|
|
260
|
+
// Delegated sub-agent role — its defining instruction. Injected right after
|
|
261
|
+
// the base prompt so it frames everything that follows. Empty for normal runs.
|
|
262
|
+
if (opts.roleAddendum) {
|
|
263
|
+
systemPrompt += '\n\n## Your role (delegated sub-agent)\n' + opts.roleAddendum;
|
|
264
|
+
}
|
|
265
|
+
// Inject the user profile (global ~/.codeep/profile.md + project
|
|
266
|
+
// .codeep/profile.md) so the agent adapts to who it's working with —
|
|
267
|
+
// reply language, style, stack, hard preferences. User-authored and gated
|
|
268
|
+
// by config.userProfile. Lives here (not in the base prompt) so every
|
|
269
|
+
// surface — CLI, ACP, VS Code, Zed — inherits it via this single path.
|
|
270
|
+
const userProfileBlock = loadUserProfilePrompt(projectContext.root);
|
|
271
|
+
if (userProfileBlock) {
|
|
272
|
+
systemPrompt += userProfileBlock;
|
|
273
|
+
}
|
|
228
274
|
// Inject project rules (from .codeep/rules.md or CODEEP.md)
|
|
229
275
|
const projectRules = loadProjectRules(projectContext.root);
|
|
230
276
|
if (projectRules) {
|
|
@@ -242,7 +288,13 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
242
288
|
if (taskCtx) {
|
|
243
289
|
systemPrompt += taskCtx;
|
|
244
290
|
}
|
|
245
|
-
// Inject prior chat session context
|
|
291
|
+
// Inject prior chat session context. When the history overflows the budget,
|
|
292
|
+
// prepend an LLM recap of the dropped (oldest) messages so long sessions
|
|
293
|
+
// keep early decisions/constraints, then the recent messages verbatim.
|
|
294
|
+
const earlierSummary = await summarizeEarlierHistory(opts.chatHistory);
|
|
295
|
+
if (earlierSummary) {
|
|
296
|
+
systemPrompt += earlierSummary;
|
|
297
|
+
}
|
|
246
298
|
const chatHistoryStr = formatChatHistoryForAgent(opts.chatHistory);
|
|
247
299
|
if (chatHistoryStr) {
|
|
248
300
|
systemPrompt += chatHistoryStr;
|
|
@@ -253,6 +305,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
253
305
|
if (skillCatalogBlock) {
|
|
254
306
|
systemPrompt += '\n\n' + skillCatalogBlock;
|
|
255
307
|
}
|
|
308
|
+
// Sub-agent catalog (delegate) — only present at depth 0.
|
|
309
|
+
if (agentsCatalogBlock) {
|
|
310
|
+
systemPrompt += agentsCatalogBlock;
|
|
311
|
+
}
|
|
256
312
|
// Active personality goes LAST — appended after skills / project rules /
|
|
257
313
|
// smart context so its tone overrides earlier conventions. Set via
|
|
258
314
|
// `/personality <name>`; empty when no personality is active.
|
|
@@ -291,6 +347,95 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
291
347
|
...(config.get('agentConfirmExecuteCommand') !== false ? ['execute_command'] : []),
|
|
292
348
|
...(config.get('agentConfirmWriteFile') === true ? ['write_file', 'edit_file'] : []),
|
|
293
349
|
]);
|
|
350
|
+
// Delegation handler: run a named (or generic) sub-agent in its own fresh
|
|
351
|
+
// context and return its summary as the tool result. Reachable only when the
|
|
352
|
+
// `delegate` tool was advertised (depth 0). The sub-agent runs nested (no own
|
|
353
|
+
// undo session) at depth 1 and never gets `delegate`, so depth is capped at 1.
|
|
354
|
+
const runDelegate = async (toolCall) => {
|
|
355
|
+
const params = (toolCall.parameters || {});
|
|
356
|
+
const task = String(params.task || '').trim();
|
|
357
|
+
const fail = (error) => ({ success: false, output: '', error, tool: 'delegate', parameters: toolCall.parameters });
|
|
358
|
+
if (!task)
|
|
359
|
+
return fail('delegate requires a non-empty "task".');
|
|
360
|
+
let def = null;
|
|
361
|
+
try {
|
|
362
|
+
const { findAgent } = await import('./agents.js');
|
|
363
|
+
def = params.agent ? findAgent(params.agent, projectContext.root) : null;
|
|
364
|
+
if (params.agent && !def)
|
|
365
|
+
return fail(`No sub-agent named "${params.agent}". Run /agents to see available agents.`);
|
|
366
|
+
}
|
|
367
|
+
catch { /* fall back to a generic sub-agent */ }
|
|
368
|
+
let roleAddendum = def?.prompt
|
|
369
|
+
|| 'You are a general-purpose sub-agent. Complete the task in your own context and return a concise, self-contained summary of what you did and the outcome.';
|
|
370
|
+
if (def?.tools)
|
|
371
|
+
roleAddendum += `\n\nYou may use ONLY these tools: ${def.tools.join(', ')}.`;
|
|
372
|
+
if (def?.personality) {
|
|
373
|
+
try {
|
|
374
|
+
const { findPersonality } = await import('./personalities.js');
|
|
375
|
+
const p = findPersonality(def.personality, projectContext.root);
|
|
376
|
+
if (p)
|
|
377
|
+
roleAddendum += '\n' + p.prompt;
|
|
378
|
+
}
|
|
379
|
+
catch { /* ignore */ }
|
|
380
|
+
}
|
|
381
|
+
const label = def?.name || 'agent';
|
|
382
|
+
opts.onIteration?.(iteration, `⤷ delegating to ${label}…`);
|
|
383
|
+
const tag = (text) => `⤷ ${label}: ${text}`;
|
|
384
|
+
// Model override — swap config for the nested run, restore in finally.
|
|
385
|
+
const prevModel = config.get('model');
|
|
386
|
+
const prevProvider = config.get('provider');
|
|
387
|
+
let swapped = false;
|
|
388
|
+
if (def?.model) {
|
|
389
|
+
try {
|
|
390
|
+
const m = String(def.model);
|
|
391
|
+
if (m.includes('/')) {
|
|
392
|
+
const { setProvider } = await import('../config/index.js');
|
|
393
|
+
setProvider(m.slice(0, m.indexOf('/')));
|
|
394
|
+
config.set('model', m.slice(m.indexOf('/') + 1));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
config.set('model', m);
|
|
398
|
+
}
|
|
399
|
+
swapped = true;
|
|
400
|
+
}
|
|
401
|
+
catch { /* keep parent's model */ }
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const sub = await runAgent(task, projectContext, {
|
|
405
|
+
...DEFAULT_OPTIONS,
|
|
406
|
+
nested: true,
|
|
407
|
+
depth: (opts.depth ?? 0) + 1,
|
|
408
|
+
allowedTools: def?.tools,
|
|
409
|
+
roleAddendum,
|
|
410
|
+
maxIterations: def?.maxIterations ?? Math.min(15, opts.maxIterations),
|
|
411
|
+
maxDuration: opts.maxDuration,
|
|
412
|
+
abortSignal: opts.abortSignal,
|
|
413
|
+
onRequestPermission: opts.onRequestPermission,
|
|
414
|
+
onExecuteCommand: opts.onExecuteCommand,
|
|
415
|
+
fs: opts.fs,
|
|
416
|
+
mcpSessionId: opts.mcpSessionId,
|
|
417
|
+
autoVerify: false,
|
|
418
|
+
onIteration: (_i, msg) => opts.onIteration?.(iteration, tag(msg)),
|
|
419
|
+
onThinking: (t) => opts.onThinking?.(tag(t)),
|
|
420
|
+
// No chatHistory → the sub-agent gets a fresh context window.
|
|
421
|
+
});
|
|
422
|
+
const summary = sub.finalResponse?.trim() || '(sub-agent finished without a summary)';
|
|
423
|
+
return { success: sub.success, output: `[${label}] ${summary}`, tool: 'delegate', parameters: toolCall.parameters };
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
return fail(`Sub-agent "${label}" failed: ${err.message}`);
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
if (swapped) {
|
|
430
|
+
try {
|
|
431
|
+
const { setProvider } = await import('../config/index.js');
|
|
432
|
+
setProvider(String(prevProvider));
|
|
433
|
+
config.set('model', prevModel);
|
|
434
|
+
}
|
|
435
|
+
catch { /* ignore restore failure */ }
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
294
439
|
const maxTimeoutRetries = 3;
|
|
295
440
|
const maxConsecutiveTimeouts = 30; // Allow more consecutive timeouts before giving up
|
|
296
441
|
const maxConsecutiveRateLimits = 5; // Stop after 5 consecutive rate-limited iterations
|
|
@@ -322,7 +467,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
322
467
|
finalResponse: partialLines.join('\n'),
|
|
323
468
|
error: `Exceeded maximum duration of ${durationMin} min`,
|
|
324
469
|
};
|
|
325
|
-
|
|
470
|
+
if (!opts.nested)
|
|
471
|
+
writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
|
|
326
472
|
return result;
|
|
327
473
|
}
|
|
328
474
|
// Check abort signal
|
|
@@ -584,6 +730,21 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
584
730
|
const toolResults = [];
|
|
585
731
|
for (const toolCall of toolCalls) {
|
|
586
732
|
opts.onToolCall?.(toolCall);
|
|
733
|
+
// Tool scoping for delegated sub-agents: reject any tool outside the
|
|
734
|
+
// agent's allowlist up front — no permission prompt, no execution.
|
|
735
|
+
if (opts.allowedTools && !opts.allowedTools.includes(toolCall.tool)) {
|
|
736
|
+
const denied = {
|
|
737
|
+
success: false,
|
|
738
|
+
output: '',
|
|
739
|
+
error: `Tool "${toolCall.tool}" is not available to this sub-agent.`,
|
|
740
|
+
tool: toolCall.tool,
|
|
741
|
+
parameters: toolCall.parameters,
|
|
742
|
+
};
|
|
743
|
+
opts.onToolResult?.(denied, toolCall);
|
|
744
|
+
actions.push(createActionLog(toolCall, denied));
|
|
745
|
+
toolResults.push(`Tool ${toolCall.tool} is not allowed for this sub-agent. Use only: ${opts.allowedTools.join(', ')}.`);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
587
748
|
// Permission check for dangerous tools (only when callback is provided, e.g. ACP/Zed)
|
|
588
749
|
if (opts.onRequestPermission && dangerousTools.has(toolCall.tool) && !alwaysAllowedTools.has(toolCall.tool)) {
|
|
589
750
|
const rejectResult = () => {
|
|
@@ -619,7 +780,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
619
780
|
}
|
|
620
781
|
}
|
|
621
782
|
let toolResult;
|
|
622
|
-
if (
|
|
783
|
+
if (toolCall.tool === 'delegate') {
|
|
784
|
+
toolResult = await runDelegate(toolCall);
|
|
785
|
+
}
|
|
786
|
+
else if (opts.dryRun) {
|
|
623
787
|
toolResult = {
|
|
624
788
|
success: true,
|
|
625
789
|
output: `[DRY RUN] Would execute: ${toolCall.tool}`,
|
|
@@ -744,7 +908,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
744
908
|
finalResponse: partialLines.join('\n'),
|
|
745
909
|
error: `Exceeded maximum of ${opts.maxIterations} iterations`,
|
|
746
910
|
};
|
|
747
|
-
|
|
911
|
+
if (!opts.nested)
|
|
912
|
+
writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
|
|
748
913
|
return result;
|
|
749
914
|
}
|
|
750
915
|
// Self-verification: Run build/test and fix errors if needed
|
|
@@ -870,13 +1035,39 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
870
1035
|
}
|
|
871
1036
|
}
|
|
872
1037
|
}
|
|
1038
|
+
// Pipeline (Phase 2): optional automatic review pass. After a top-level run
|
|
1039
|
+
// that changed files, delegate to the `reviewer` sub-agent and append its
|
|
1040
|
+
// findings — guaranteeing a review stage without relying on the model to
|
|
1041
|
+
// self-delegate one. Opt-in (agentAutoReview); depth-0 only; never fatal.
|
|
1042
|
+
if (!opts.nested
|
|
1043
|
+
&& (opts.depth ?? 0) === 0
|
|
1044
|
+
&& !opts.dryRun
|
|
1045
|
+
&& config.get('agentAutoReview') === true
|
|
1046
|
+
&& !opts.abortSignal?.aborted
|
|
1047
|
+
&& actions.some(a => a.type === 'write' || a.type === 'edit' || a.type === 'delete')) {
|
|
1048
|
+
try {
|
|
1049
|
+
const reviewTask = `Review the changes just made for this task:\n\n${prompt}\n\nInspect the current state of the changed files (and the git diff). Report concrete issues by severity — correctness/bugs, security, then design — with file:line and a one-line fix each. If it's solid, say so briefly.`;
|
|
1050
|
+
const review = await runDelegate({
|
|
1051
|
+
id: 'auto-review',
|
|
1052
|
+
tool: 'delegate',
|
|
1053
|
+
parameters: { agent: 'reviewer', task: reviewTask },
|
|
1054
|
+
});
|
|
1055
|
+
const body = (review.output || '').replace(/^\[reviewer\]\s*/, '').trim();
|
|
1056
|
+
if (body)
|
|
1057
|
+
finalResponse += `\n\n---\n### Auto-review (reviewer)\n${body}`;
|
|
1058
|
+
}
|
|
1059
|
+
catch {
|
|
1060
|
+
// A failed review must never fail the run.
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
873
1063
|
result = {
|
|
874
1064
|
success: true,
|
|
875
1065
|
iterations: iteration,
|
|
876
1066
|
actions,
|
|
877
1067
|
finalResponse,
|
|
878
1068
|
};
|
|
879
|
-
|
|
1069
|
+
if (!opts.nested)
|
|
1070
|
+
writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
|
|
880
1071
|
return result;
|
|
881
1072
|
}
|
|
882
1073
|
catch (error) {
|
|
@@ -891,8 +1082,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
891
1082
|
return result;
|
|
892
1083
|
}
|
|
893
1084
|
finally {
|
|
894
|
-
// End session and save history
|
|
895
|
-
|
|
1085
|
+
// End session and save history. Skipped for nested runs so we don't write
|
|
1086
|
+
// a separate session file or null out the parent's open session.
|
|
1087
|
+
if (!opts.nested)
|
|
1088
|
+
endSession();
|
|
896
1089
|
}
|
|
897
1090
|
}
|
|
898
1091
|
/**
|
|
@@ -52,6 +52,21 @@ export declare function formatChatHistoryForAgent(history?: Array<{
|
|
|
52
52
|
role: 'user' | 'assistant';
|
|
53
53
|
content: string;
|
|
54
54
|
}>, maxChars?: number): string;
|
|
55
|
+
/**
|
|
56
|
+
* Summarize the OVERFLOW that `formatChatHistoryForAgent` drops. When prior
|
|
57
|
+
* history exceeds `maxChars`, that function keeps only the most recent messages
|
|
58
|
+
* and silently discards the older ones — losing early decisions/constraints on
|
|
59
|
+
* long sessions. This condenses those dropped messages into a short recap that
|
|
60
|
+
* the caller prepends *before* the recent verbatim history.
|
|
61
|
+
*
|
|
62
|
+
* Returns '' when: opted out (`autoSummarizeHistory === false`), nothing
|
|
63
|
+
* overflows, or the summarization call fails (graceful fallback — the recent
|
|
64
|
+
* history still goes in, we just don't add a recap).
|
|
65
|
+
*/
|
|
66
|
+
export declare function summarizeEarlierHistory(history?: Array<{
|
|
67
|
+
role: 'user' | 'assistant';
|
|
68
|
+
content: string;
|
|
69
|
+
}>, maxChars?: number): Promise<string>;
|
|
55
70
|
export declare function getAgentSystemPrompt(projectContext: ProjectContext): string;
|
|
56
71
|
export declare function getFallbackSystemPrompt(projectContext: ProjectContext, additionalTools?: AdditionalToolDef[]): string;
|
|
57
72
|
/**
|
package/dist/utils/agentChat.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
15
15
|
import { join } from 'path';
|
|
16
|
+
import { createHash } from 'crypto';
|
|
16
17
|
import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
|
|
17
18
|
import { loadProjectIntelligence, generateContextFromIntelligence } from './projectIntelligence.js';
|
|
18
19
|
import { syncProgress, generateProjectId } from './codeepCloud.js';
|
|
@@ -180,6 +181,84 @@ export function formatChatHistoryForAgent(history, maxChars = 16000) {
|
|
|
180
181
|
const lines = selected.map(m => `**${m.role === 'user' ? 'User' : 'Assistant'}:** ${m.content}`).join('\n\n');
|
|
181
182
|
return `\n\n## Prior Conversation Context\nThe following is the recent chat history from this session. Use it as background context to understand the user's intent, but focus on completing the current task.\n\n${lines}`;
|
|
182
183
|
}
|
|
184
|
+
// Same noise filter formatChatHistoryForAgent uses — kept in sync so the two
|
|
185
|
+
// functions agree on which messages are "real" conversation.
|
|
186
|
+
function filterAgentHistory(history) {
|
|
187
|
+
return history.filter(m => {
|
|
188
|
+
const content = m.content.trimStart();
|
|
189
|
+
if (content.startsWith('[AGENT]') || content.startsWith('[DRY RUN]'))
|
|
190
|
+
return false;
|
|
191
|
+
if (content.startsWith('Agent completed') || content.startsWith('Agent failed') || content.startsWith('Agent stopped'))
|
|
192
|
+
return false;
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// Cache summaries by a hash of the dropped messages, so re-running the agent in
|
|
197
|
+
// the same session (same overflow) doesn't re-summarize on every task.
|
|
198
|
+
const earlierSummaryCache = new Map();
|
|
199
|
+
/**
|
|
200
|
+
* Summarize the OVERFLOW that `formatChatHistoryForAgent` drops. When prior
|
|
201
|
+
* history exceeds `maxChars`, that function keeps only the most recent messages
|
|
202
|
+
* and silently discards the older ones — losing early decisions/constraints on
|
|
203
|
+
* long sessions. This condenses those dropped messages into a short recap that
|
|
204
|
+
* the caller prepends *before* the recent verbatim history.
|
|
205
|
+
*
|
|
206
|
+
* Returns '' when: opted out (`autoSummarizeHistory === false`), nothing
|
|
207
|
+
* overflows, or the summarization call fails (graceful fallback — the recent
|
|
208
|
+
* history still goes in, we just don't add a recap).
|
|
209
|
+
*/
|
|
210
|
+
export async function summarizeEarlierHistory(history, maxChars = 16000) {
|
|
211
|
+
if (config.get('autoSummarizeHistory') === false)
|
|
212
|
+
return '';
|
|
213
|
+
if (!history || history.length === 0)
|
|
214
|
+
return '';
|
|
215
|
+
const filtered = filterAgentHistory(history);
|
|
216
|
+
if (filtered.length === 0)
|
|
217
|
+
return '';
|
|
218
|
+
// Mirror formatChatHistoryForAgent's newest→oldest budget walk to find which
|
|
219
|
+
// messages it KEEPS; everything older than the oldest kept message is dropped.
|
|
220
|
+
let totalChars = 0;
|
|
221
|
+
let firstKept = filtered.length;
|
|
222
|
+
for (let i = filtered.length - 1; i >= 0; i--) {
|
|
223
|
+
const entry = `${filtered[i].role === 'user' ? 'User' : 'Assistant'}: ${filtered[i].content}`;
|
|
224
|
+
if (totalChars + entry.length > maxChars && firstKept < filtered.length)
|
|
225
|
+
break;
|
|
226
|
+
if (entry.length > maxChars) {
|
|
227
|
+
firstKept = i;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
firstKept = i;
|
|
231
|
+
totalChars += entry.length;
|
|
232
|
+
}
|
|
233
|
+
const dropped = filtered.slice(0, firstKept);
|
|
234
|
+
if (dropped.length === 0)
|
|
235
|
+
return '';
|
|
236
|
+
const key = createHash('sha256')
|
|
237
|
+
.update(dropped.map(m => `${m.role}:${m.content}`).join(''))
|
|
238
|
+
.digest('hex');
|
|
239
|
+
const cached = earlierSummaryCache.get(key);
|
|
240
|
+
if (cached)
|
|
241
|
+
return cached;
|
|
242
|
+
// Compact transcript of the dropped messages, capped so the summarization
|
|
243
|
+
// prompt stays cheap even when a lot has overflowed.
|
|
244
|
+
const transcript = dropped
|
|
245
|
+
.map(m => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content.replace(/\s+/g, ' ').slice(0, 600)}`)
|
|
246
|
+
.join('\n')
|
|
247
|
+
.slice(0, 24000);
|
|
248
|
+
const system = 'You are condensing the EARLIER part of an ongoing coding session that no longer fits the context window. Summarize what happened in 3-6 sentences: concrete decisions made, constraints/requirements stated, files or APIs involved, and anything still unfinished. Past tense, no preamble, no bullet headers — just the recap.';
|
|
249
|
+
try {
|
|
250
|
+
const { chat } = await import('../api/index.js');
|
|
251
|
+
const summary = (await chat(transcript, [{ role: 'system', content: system }])).trim();
|
|
252
|
+
if (!summary)
|
|
253
|
+
return '';
|
|
254
|
+
const block = `\n\n## Earlier Conversation (summarized)\nThe earlier part of this session was condensed to fit context. Treat it as established background:\n\n${summary}`;
|
|
255
|
+
earlierSummaryCache.set(key, block);
|
|
256
|
+
return block;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return ''; // graceful — recent verbatim history still gets injected
|
|
260
|
+
}
|
|
261
|
+
}
|
|
183
262
|
export function getAgentSystemPrompt(projectContext) {
|
|
184
263
|
const root = projectContext.root || process.cwd();
|
|
185
264
|
// State the real underlying model/provider so "which model are you"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agents — named, scoped agent definitions the main ("orchestrator") agent
|
|
3
|
+
* can delegate work to via the `delegate` tool. Each runs as a NESTED agent
|
|
4
|
+
* loop with its own fresh context window, an optional tool allowlist, an
|
|
5
|
+
* optional model override, and a role system prompt — then returns only its
|
|
6
|
+
* final summary to the parent. This keeps the parent's context small and lets
|
|
7
|
+
* each sub-task run with a specialist persona.
|
|
8
|
+
*
|
|
9
|
+
* Storage (mirrors personalities/skills):
|
|
10
|
+
* - **Built-in**: hardcoded below (researcher, reviewer, tester).
|
|
11
|
+
* - **Project**: `<workspace>/.codeep/agents/<name>.md`
|
|
12
|
+
* - **Global**: `~/.codeep/agents/<name>.md`
|
|
13
|
+
* Project shadows global shadows built-in, by name.
|
|
14
|
+
*
|
|
15
|
+
* File format — YAML-ish frontmatter + Markdown body (the role prompt):
|
|
16
|
+
* ```
|
|
17
|
+
* ---
|
|
18
|
+
* name: reviewer
|
|
19
|
+
* description: Reviews a diff for correctness & security
|
|
20
|
+
* tools: [read_file, search_code, execute_command] # allowlist; omit = all
|
|
21
|
+
* model: glm-5.1 # optional provider/model or model override
|
|
22
|
+
* personality: security # optional — reuse a personality preset
|
|
23
|
+
* maxIterations: 15 # optional budget
|
|
24
|
+
* ---
|
|
25
|
+
* You are a senior reviewer. Find correctness & security issues…
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export type AgentScope = 'builtin' | 'project' | 'global';
|
|
29
|
+
export interface AgentDef {
|
|
30
|
+
/** Slug (filename without .md, or built-in id). Lowercase, hyphens. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Human display label. */
|
|
33
|
+
displayName: string;
|
|
34
|
+
/** One-line description shown in the catalog + `/agents`. */
|
|
35
|
+
description: string;
|
|
36
|
+
/** Markdown body — the role system prompt for the sub-agent. */
|
|
37
|
+
prompt: string;
|
|
38
|
+
/** Tool allowlist. Undefined = inherit all of the parent's tools. */
|
|
39
|
+
tools?: string[];
|
|
40
|
+
/** Optional model override ("provider/model" or just "model"). */
|
|
41
|
+
model?: string;
|
|
42
|
+
/** Optional personality preset to layer on (by name). */
|
|
43
|
+
personality?: string;
|
|
44
|
+
/** Optional per-run iteration budget. */
|
|
45
|
+
maxIterations?: number;
|
|
46
|
+
scope: AgentScope;
|
|
47
|
+
}
|
|
48
|
+
export declare function loadAgents(workspaceRoot?: string): AgentDef[];
|
|
49
|
+
export declare function findAgent(name: string, workspaceRoot?: string): AgentDef | null;
|
|
50
|
+
/**
|
|
51
|
+
* The catalog block appended to the orchestrator's system prompt so the model
|
|
52
|
+
* knows which sub-agents it can `delegate` to. Empty string is never returned
|
|
53
|
+
* (built-ins always exist), but callers can choose not to inject it.
|
|
54
|
+
*/
|
|
55
|
+
export declare function formatAgentsForSysprompt(agents: AgentDef[]): string;
|
|
56
|
+
/** `/agents` list view (mirrors formatPersonalityList). */
|
|
57
|
+
export declare function formatAgentList(workspaceRoot?: string): string;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agents — named, scoped agent definitions the main ("orchestrator") agent
|
|
3
|
+
* can delegate work to via the `delegate` tool. Each runs as a NESTED agent
|
|
4
|
+
* loop with its own fresh context window, an optional tool allowlist, an
|
|
5
|
+
* optional model override, and a role system prompt — then returns only its
|
|
6
|
+
* final summary to the parent. This keeps the parent's context small and lets
|
|
7
|
+
* each sub-task run with a specialist persona.
|
|
8
|
+
*
|
|
9
|
+
* Storage (mirrors personalities/skills):
|
|
10
|
+
* - **Built-in**: hardcoded below (researcher, reviewer, tester).
|
|
11
|
+
* - **Project**: `<workspace>/.codeep/agents/<name>.md`
|
|
12
|
+
* - **Global**: `~/.codeep/agents/<name>.md`
|
|
13
|
+
* Project shadows global shadows built-in, by name.
|
|
14
|
+
*
|
|
15
|
+
* File format — YAML-ish frontmatter + Markdown body (the role prompt):
|
|
16
|
+
* ```
|
|
17
|
+
* ---
|
|
18
|
+
* name: reviewer
|
|
19
|
+
* description: Reviews a diff for correctness & security
|
|
20
|
+
* tools: [read_file, search_code, execute_command] # allowlist; omit = all
|
|
21
|
+
* model: glm-5.1 # optional provider/model or model override
|
|
22
|
+
* personality: security # optional — reuse a personality preset
|
|
23
|
+
* maxIterations: 15 # optional budget
|
|
24
|
+
* ---
|
|
25
|
+
* You are a senior reviewer. Find correctness & security issues…
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
29
|
+
import { join } from 'path';
|
|
30
|
+
import { homedir } from 'os';
|
|
31
|
+
const BUILTIN = [
|
|
32
|
+
{
|
|
33
|
+
name: 'planner',
|
|
34
|
+
displayName: 'Planner',
|
|
35
|
+
description: 'Read-only planner — investigates, then returns a concrete step-by-step implementation plan.',
|
|
36
|
+
scope: 'builtin',
|
|
37
|
+
tools: ['read_file', 'search_code', 'list_files', 'find_files'],
|
|
38
|
+
prompt: `You are a planning sub-agent. Investigate, then produce a plan — do NOT write code or run commands.
|
|
39
|
+
- Read the relevant files to ground the plan in how the code actually works.
|
|
40
|
+
- Return a concise, numbered, step-by-step plan: each step names the file(s) to touch and what changes.
|
|
41
|
+
- Call out risks, assumptions, and anything the implementer must verify.
|
|
42
|
+
- Keep it actionable — the implementer will follow it directly. No code, just the plan.`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'researcher',
|
|
46
|
+
displayName: 'Researcher',
|
|
47
|
+
description: 'Read-only explorer — digs through the codebase / web and returns a tight summary.',
|
|
48
|
+
scope: 'builtin',
|
|
49
|
+
tools: ['read_file', 'search_code', 'list_files', 'find_files', 'web_search', 'web_read', 'fetch_url'],
|
|
50
|
+
prompt: `You are a research sub-agent. Your job is to investigate and report — never modify anything.
|
|
51
|
+
- Explore the codebase (and the web when relevant) to answer the task precisely.
|
|
52
|
+
- You CANNOT write or edit files or run commands — read and search only.
|
|
53
|
+
- Return a tight, structured summary: the answer first, then the specific files/lines/sources that back it up.
|
|
54
|
+
- Omit dead ends. The caller only sees your final message, so make it self-contained.`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'reviewer',
|
|
58
|
+
displayName: 'Reviewer',
|
|
59
|
+
description: 'Read-only senior review — finds correctness, security, and design issues.',
|
|
60
|
+
scope: 'builtin',
|
|
61
|
+
tools: ['read_file', 'search_code', 'list_files', 'find_files', 'execute_command'],
|
|
62
|
+
personality: 'security',
|
|
63
|
+
prompt: `You are a senior code-review sub-agent. Review only — do not change code.
|
|
64
|
+
- Read the relevant files (and run read-only git/inspection commands) to understand the change in context.
|
|
65
|
+
- Report concrete issues grouped by severity: correctness/bugs, security, then design/naming/tests.
|
|
66
|
+
- Cite file:line for each finding and suggest the fix in one sentence.
|
|
67
|
+
- If it's solid, say so briefly — don't invent problems.`,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'tester',
|
|
71
|
+
displayName: 'Tester',
|
|
72
|
+
description: 'Writes and runs tests for a target, then reports pass/fail.',
|
|
73
|
+
scope: 'builtin',
|
|
74
|
+
tools: ['read_file', 'write_file', 'edit_file', 'search_code', 'list_files', 'find_files', 'execute_command'],
|
|
75
|
+
prompt: `You are a testing sub-agent. Write focused tests for the target and run them.
|
|
76
|
+
- Match the project's existing test framework and conventions (look at neighbouring tests first).
|
|
77
|
+
- Cover the happy path plus the obvious edge cases; don't over-test.
|
|
78
|
+
- Run the tests and iterate until they pass (or you've found a real bug — then report it).
|
|
79
|
+
- Final message: what you added, the command to run them, and the pass/fail result.`,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
/** Parse `tools: [a, b]` or `tools: a, b` out of a frontmatter line value. */
|
|
83
|
+
function parseToolsValue(raw) {
|
|
84
|
+
const inner = raw.trim().replace(/^\[/, '').replace(/\]$/, '');
|
|
85
|
+
const list = inner.split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
86
|
+
return list.length > 0 ? list : undefined;
|
|
87
|
+
}
|
|
88
|
+
/** Load custom agents from a `.codeep/agents/` directory. */
|
|
89
|
+
function loadFromDir(dir, scope) {
|
|
90
|
+
if (!existsSync(dir))
|
|
91
|
+
return [];
|
|
92
|
+
const out = [];
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = readdirSync(dir);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!entry.endsWith('.md'))
|
|
102
|
+
continue;
|
|
103
|
+
const slug = entry.slice(0, -3).toLowerCase();
|
|
104
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug))
|
|
105
|
+
continue;
|
|
106
|
+
try {
|
|
107
|
+
const raw = readFileSync(join(dir, entry), 'utf8');
|
|
108
|
+
if (raw.length > 64 * 1024)
|
|
109
|
+
continue;
|
|
110
|
+
const fm = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
111
|
+
const meta = {};
|
|
112
|
+
let body = raw;
|
|
113
|
+
if (fm) {
|
|
114
|
+
body = fm[2];
|
|
115
|
+
for (const line of fm[1].split('\n')) {
|
|
116
|
+
const m = line.match(/^([a-zA-Z]+):\s*(.*)$/);
|
|
117
|
+
if (m)
|
|
118
|
+
meta[m[1].toLowerCase()] = m[2].trim();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const displayName = meta.name || slug;
|
|
122
|
+
const description = meta.description || `Custom agent from ${entry}`;
|
|
123
|
+
const tools = meta.tools ? parseToolsValue(meta.tools) : undefined;
|
|
124
|
+
const maxIterations = meta.maxiterations ? parseInt(meta.maxiterations, 10) : undefined;
|
|
125
|
+
out.push({
|
|
126
|
+
name: slug,
|
|
127
|
+
displayName,
|
|
128
|
+
description: description.length > 200 ? description.slice(0, 197) + '…' : description,
|
|
129
|
+
prompt: body.trim(),
|
|
130
|
+
tools,
|
|
131
|
+
model: meta.model || undefined,
|
|
132
|
+
personality: meta.personality || undefined,
|
|
133
|
+
maxIterations: Number.isFinite(maxIterations) ? maxIterations : undefined,
|
|
134
|
+
scope,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Skip broken files — never crash agent loading.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
export function loadAgents(workspaceRoot) {
|
|
144
|
+
const project = workspaceRoot ? loadFromDir(join(workspaceRoot, '.codeep', 'agents'), 'project') : [];
|
|
145
|
+
const global = loadFromDir(join(homedir(), '.codeep', 'agents'), 'global');
|
|
146
|
+
const byName = new Map();
|
|
147
|
+
for (const a of BUILTIN)
|
|
148
|
+
byName.set(a.name, a);
|
|
149
|
+
for (const a of global)
|
|
150
|
+
byName.set(a.name, a);
|
|
151
|
+
for (const a of project)
|
|
152
|
+
byName.set(a.name, a);
|
|
153
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
154
|
+
}
|
|
155
|
+
export function findAgent(name, workspaceRoot) {
|
|
156
|
+
const lower = name.toLowerCase();
|
|
157
|
+
return loadAgents(workspaceRoot).find((a) => a.name === lower) ?? null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The catalog block appended to the orchestrator's system prompt so the model
|
|
161
|
+
* knows which sub-agents it can `delegate` to. Empty string is never returned
|
|
162
|
+
* (built-ins always exist), but callers can choose not to inject it.
|
|
163
|
+
*/
|
|
164
|
+
export function formatAgentsForSysprompt(agents) {
|
|
165
|
+
if (agents.length === 0)
|
|
166
|
+
return '';
|
|
167
|
+
const lines = [
|
|
168
|
+
'\n\n## Sub-agents (delegation)',
|
|
169
|
+
'You can delegate a self-contained sub-task to a specialist sub-agent with the `delegate` tool. It runs in its own fresh context and returns only a summary — use it to keep your own context focused (e.g. send a researcher to explore, a reviewer to critique, a tester to write tests). Available agents:',
|
|
170
|
+
'',
|
|
171
|
+
];
|
|
172
|
+
for (const a of agents)
|
|
173
|
+
lines.push(`- \`${a.name}\` — ${a.description}`);
|
|
174
|
+
lines.push('', 'Call `delegate({ "agent": "<name>", "task": "<clear, self-contained instruction>" })`. Omit `agent` for a general-purpose sub-agent. Do the work yourself for small/quick tasks — delegation has overhead.');
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
/** `/agents` list view (mirrors formatPersonalityList). */
|
|
178
|
+
export function formatAgentList(workspaceRoot) {
|
|
179
|
+
const list = loadAgents(workspaceRoot);
|
|
180
|
+
const lines = ['## Sub-agents', '', 'The agent can `delegate` self-contained sub-tasks to these. Each runs in its own context and returns a summary.', '', '| Name | Scope | Tools | Description |', '|---|---|---|---|'];
|
|
181
|
+
for (const a of list) {
|
|
182
|
+
const tag = a.scope === 'builtin' ? 'built-in' : a.scope;
|
|
183
|
+
const tools = a.tools ? `${a.tools.length} scoped` : 'all';
|
|
184
|
+
lines.push(`| \`${a.name}\` | ${tag} | ${tools} | ${a.description} |`);
|
|
185
|
+
}
|
|
186
|
+
lines.push('', 'Add your own: drop a `<name>.md` with frontmatter (name, description, tools, model, personality) in `.codeep/agents/` (project) or `~/.codeep/agents/` (global).');
|
|
187
|
+
return lines.join('\n');
|
|
188
|
+
}
|