@ikarmic/forge-core 5.0.0 → 5.1.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/dist/agent/agent.d.ts +8 -0
- package/dist/agent/agent.js +123 -22
- package/dist/agent/background.d.ts +58 -0
- package/dist/agent/background.js +139 -0
- package/dist/agent/enhance-prompt.d.ts +3 -1
- package/dist/agent/enhance-prompt.js +6 -2
- package/dist/agent/goal.d.ts +54 -0
- package/dist/agent/goal.js +65 -0
- package/dist/agents/agent-files.d.ts +43 -0
- package/dist/agents/agent-files.js +109 -0
- package/dist/checkpoints/store.js +5 -1
- package/dist/commands/custom-commands.d.ts +9 -4
- package/dist/commands/custom-commands.js +13 -2
- package/dist/config-file.d.ts +43 -0
- package/dist/config-file.js +249 -5
- package/dist/config-merge.d.ts +13 -0
- package/dist/config-merge.js +67 -0
- package/dist/config-schema.d.ts +340 -0
- package/dist/config-schema.js +126 -0
- package/dist/config.js +2 -1
- package/dist/context/compaction.js +4 -0
- package/dist/context/mask.js +1 -1
- package/dist/custom-providers.d.ts +95 -0
- package/dist/custom-providers.js +120 -0
- package/dist/git/auto-commit.d.ts +2 -2
- package/dist/git/auto-commit.js +5 -4
- package/dist/github/github-tools.d.ts +2 -2
- package/dist/hooks/runner.js +3 -2
- package/dist/import/claude-code.d.ts +39 -0
- package/dist/import/claude-code.js +254 -0
- package/dist/index.d.ts +32 -4
- package/dist/index.js +24 -3
- package/dist/lsp/client.d.ts +5 -0
- package/dist/lsp/client.js +39 -3
- package/dist/mcp/index.d.ts +5 -1
- package/dist/mcp/index.js +3 -1
- package/dist/mcp/manager.d.ts +17 -3
- package/dist/mcp/manager.js +27 -2
- package/dist/mcp/marketplace.d.ts +2 -0
- package/dist/mcp/marketplace.js +10 -4
- package/dist/mcp/mcp-client.d.ts +12 -2
- package/dist/mcp/mcp-client.js +74 -17
- package/dist/mcp/mcp-config.d.ts +2 -0
- package/dist/mcp/mcp-config.js +1 -0
- package/dist/mcp/oauth.d.ts +55 -0
- package/dist/mcp/oauth.js +197 -0
- package/dist/mcp/resource-tools.d.ts +8 -0
- package/dist/mcp/resource-tools.js +56 -0
- package/dist/memory/session-digest.d.ts +12 -0
- package/dist/memory/session-digest.js +47 -0
- package/dist/memory/stale.js +2 -1
- package/dist/model-alias.d.ts +18 -2
- package/dist/model-alias.js +27 -3
- package/dist/model-catalog.js +0 -1
- package/dist/model-list.js +2 -1
- package/dist/model-overrides.d.ts +30 -0
- package/dist/model-overrides.js +48 -0
- package/dist/orchestrator/specialists-registry.js +2 -3
- package/dist/orchestrator/specialists.js +4 -4
- package/dist/policy/ca-certs.d.ts +8 -0
- package/dist/policy/ca-certs.js +30 -0
- package/dist/policy/proxy.d.ts +10 -3
- package/dist/policy/proxy.js +33 -6
- package/dist/pricing.js +4 -0
- package/dist/providers.d.ts +7 -0
- package/dist/providers.js +39 -5
- package/dist/semantic/embedder.d.ts +15 -0
- package/dist/semantic/embedder.js +25 -0
- package/dist/semantic/semantic-index.d.ts +10 -2
- package/dist/semantic/semantic-index.js +16 -7
- package/dist/sessions/session-store.js +2 -1
- package/dist/skills/builtin.d.ts +2 -0
- package/dist/skills/builtin.js +68 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/loader.d.ts +10 -3
- package/dist/skills/loader.js +20 -7
- package/dist/skills/marketplace.js +2 -1
- package/dist/skills/remote.d.ts +14 -0
- package/dist/skills/remote.js +131 -0
- package/dist/skills/types.d.ts +10 -0
- package/dist/steering/extra-instructions.d.ts +8 -0
- package/dist/steering/extra-instructions.js +104 -0
- package/dist/steering/folder-instructions.d.ts +8 -0
- package/dist/steering/folder-instructions.js +28 -0
- package/dist/steering/select.js +2 -1
- package/dist/timed-fetch.d.ts +12 -0
- package/dist/timed-fetch.js +83 -0
- package/dist/tools/apply-patch.d.ts +52 -0
- package/dist/tools/apply-patch.js +197 -0
- package/dist/tools/ask-user.d.ts +27 -0
- package/dist/tools/ask-user.js +32 -0
- package/dist/tools/browser-tools.d.ts +6 -6
- package/dist/tools/browser-tools.js +3 -13
- package/dist/tools/codebase-search.d.ts +4 -2
- package/dist/tools/codebase-search.js +2 -2
- package/dist/tools/edit-file.js +5 -3
- package/dist/tools/fetch-url.d.ts +7 -0
- package/dist/tools/fetch-url.js +52 -19
- package/dist/tools/find-symbol.d.ts +2 -2
- package/dist/tools/ignore-rules.js +2 -1
- package/dist/tools/index.d.ts +5 -1
- package/dist/tools/index.js +10 -1
- package/dist/tools/memory.d.ts +6 -6
- package/dist/tools/notebook.d.ts +2 -2
- package/dist/tools/permission-rules.d.ts +32 -0
- package/dist/tools/permission-rules.js +182 -0
- package/dist/tools/read-file.d.ts +5 -0
- package/dist/tools/read-file.js +7 -2
- package/dist/tools/read-only.d.ts +1 -1
- package/dist/tools/read-only.js +2 -0
- package/dist/tools/run-command.d.ts +4 -2
- package/dist/tools/run-command.js +12 -8
- package/dist/tools/sandbox-loader.js +3 -13
- package/dist/tools/search-files.d.ts +8 -2
- package/dist/tools/search-files.js +43 -0
- package/dist/tools/shared.d.ts +5 -0
- package/dist/tools/shared.js +30 -2
- package/dist/tools/shell-env.d.ts +7 -0
- package/dist/tools/shell-env.js +9 -0
- package/dist/tools/sub-agent-wait.d.ts +29 -0
- package/dist/tools/sub-agent-wait.js +60 -0
- package/dist/tools/sub-agent.d.ts +50 -4
- package/dist/tools/sub-agent.js +210 -125
- package/dist/tools/text-encoding.d.ts +10 -0
- package/dist/tools/text-encoding.js +40 -0
- package/dist/tools/todo.d.ts +36 -0
- package/dist/tools/todo.js +42 -0
- package/dist/tools/write-file.js +15 -9
- package/dist/tools/write-tools.d.ts +12 -0
- package/dist/tools/write-tools.js +18 -0
- package/dist/types.d.ts +73 -0
- package/dist/worktree/worktree.d.ts +3 -0
- package/dist/worktree/worktree.js +22 -0
- package/package.json +1 -1
package/dist/agent/agent.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export declare class Agent {
|
|
|
15
15
|
private readonly checkpoints;
|
|
16
16
|
private readonly browser;
|
|
17
17
|
private readonly audit;
|
|
18
|
+
private readonly permissionRules;
|
|
19
|
+
private todoList;
|
|
18
20
|
constructor(config: AgentConfig);
|
|
19
21
|
/** Files whose modification should trigger a pre-tool checkpoint. */
|
|
20
22
|
private get captureBeforeTools();
|
|
@@ -42,6 +44,12 @@ export declare class Agent {
|
|
|
42
44
|
* we never want to "skip".
|
|
43
45
|
*/
|
|
44
46
|
private wrapToolsWithCache;
|
|
47
|
+
/**
|
|
48
|
+
* What the permission rules say about one tool call: `allow` (every target allowed), `deny` (any target denied,
|
|
49
|
+
* with the reason the model sees), `ask` (a target asks, a `.env` file is read, or a command leaves the workspace),
|
|
50
|
+
* or undefined when no rule applies and the normal approval flow decides.
|
|
51
|
+
*/
|
|
52
|
+
private decidePermission;
|
|
45
53
|
private requestApproval;
|
|
46
54
|
/**
|
|
47
55
|
* Build the default LLM-based summarizer for compaction. Picks the
|
package/dist/agent/agent.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { streamText } from 'ai';
|
|
2
2
|
import { lintFile, lintMessage } from '../lint/auto-lint.js';
|
|
3
3
|
import { buildMemoryBlock } from '../memory/memory-store.js';
|
|
4
|
+
import { filesTouchedBy } from '../tools/write-tools.js';
|
|
5
|
+
import { buildExtraInstructionsBlock } from '../steering/extra-instructions.js';
|
|
4
6
|
import { buildProjectInstructionsBlock } from '../steering/project-instructions.js';
|
|
5
7
|
import { buildRepoMap, repoMapBlock } from '../repomap/repo-map.js';
|
|
6
8
|
import { buildLlmSummarizer, compactMessages, effectiveLimitTokens, getModelWindowTokens, shouldCompactForBudget } from '../context/compaction.js';
|
|
@@ -17,6 +19,7 @@ import { embedderFromEnv } from '../semantic/embedder.js';
|
|
|
17
19
|
import { DEFAULT_VERIFY_RETRIES, failureMessage, runVerification } from './verify.js';
|
|
18
20
|
import { LoopGuard, resultKeyOf } from './loop-guard.js';
|
|
19
21
|
import { isListedCommand } from '../tools/command-allow.js';
|
|
22
|
+
import { compilePermissionRules, evaluatePermission, isSensitiveFile, permissionFor, touchesOutsideCwd, } from '../tools/permission-rules.js';
|
|
20
23
|
import { createLanguageModel } from '../providers.js';
|
|
21
24
|
import { BUILTIN_TOOLS } from '../tools/index.js';
|
|
22
25
|
import { ToolResultCache, cachedExecute } from '../tools/result-cache.js';
|
|
@@ -32,9 +35,9 @@ const DEFAULT_MAX_CONTINUATIONS = 3;
|
|
|
32
35
|
export const DEFAULT_MAX_DURATION_MS = 10 * 60 * 1000;
|
|
33
36
|
const REDACTABLE_TOOLS = new Set(['read_file', 'search_files', 'list_files']);
|
|
34
37
|
/** Tools whose successful `path` argument can trigger mid-run fileMatch steering. */
|
|
35
|
-
const STEERING_FILE_TOOLS = new Set(['read_file', 'write_file', 'edit_file', 'edit_notebook']);
|
|
38
|
+
const STEERING_FILE_TOOLS = new Set(['read_file', 'write_file', 'edit_file', 'edit_notebook', 'apply_patch']);
|
|
36
39
|
/** Tools that change a file, so auto-lint checks their `path` afterwards. */
|
|
37
|
-
const FILE_WRITE_TOOLS = new Set(['write_file', 'edit_file', 'edit_notebook']);
|
|
40
|
+
const FILE_WRITE_TOOLS = new Set(['write_file', 'edit_file', 'edit_notebook', 'apply_patch']);
|
|
38
41
|
/** Pure reads whose results may be reused within a run. Every other tool invalidates the cache. */
|
|
39
42
|
const CACHEABLE_TOOLS = new Set(['read_file', 'read_image', 'list_files', 'search_files', 'find_symbol', 'list_symbols', 'find_references', 'codebase_search', 'memory_read', 'fetch_url', 'web_search']);
|
|
40
43
|
/**
|
|
@@ -52,12 +55,16 @@ export class Agent {
|
|
|
52
55
|
checkpoints;
|
|
53
56
|
browser;
|
|
54
57
|
audit;
|
|
58
|
+
permissionRules;
|
|
59
|
+
todoList = [];
|
|
55
60
|
constructor(config) {
|
|
56
61
|
// Organization policy first: everything below sees the already-tightened config.
|
|
57
62
|
const policy = config.policy === undefined ? getOrgPolicy()?.policy : (config.policy ?? undefined);
|
|
58
63
|
config = applyPolicy(config, policy);
|
|
59
64
|
this.audit = config.audit ?? (policy?.audit?.enabled ? createFileAuditSink(policy.audit.file) : undefined);
|
|
60
65
|
this.config = config;
|
|
66
|
+
this.permissionRules = compilePermissionRules(config.permissionRules);
|
|
67
|
+
this.todoList = config.initialTodos ? [...config.initialTodos] : [];
|
|
61
68
|
const allTools = (config.disableBuiltins
|
|
62
69
|
? [...(config.extraTools ?? [])]
|
|
63
70
|
: [...BUILTIN_TOOLS, ...(config.extraTools ?? [])]);
|
|
@@ -72,12 +79,19 @@ export class Agent {
|
|
|
72
79
|
if (webSearch)
|
|
73
80
|
allTools.push(createWebSearchTool(webSearch));
|
|
74
81
|
if (config.semanticSearch) {
|
|
75
|
-
allTools.push(createCodebaseSearchTool(config.semanticSearch.embedder ?? embedderFromEnv()
|
|
82
|
+
allTools.push(createCodebaseSearchTool(config.semanticSearch.embedder ?? embedderFromEnv(), {
|
|
83
|
+
...(config.semanticSearch.extensions ? { extensions: config.semanticSearch.extensions } : {}),
|
|
84
|
+
...(config.semanticSearch.minScore !== undefined ? { minScore: config.semanticSearch.minScore } : {}),
|
|
85
|
+
...(config.semanticSearch.defaultLimit ? { defaultLimit: config.semanticSearch.defaultLimit } : {}),
|
|
86
|
+
}));
|
|
76
87
|
}
|
|
77
88
|
const memoryFiltered = config.disableMemory
|
|
78
89
|
? allTools.filter((t) => t.name !== 'memory_read' && t.name !== 'memory_write' && t.name !== 'memory_forget')
|
|
79
90
|
: allTools;
|
|
80
|
-
|
|
91
|
+
// `ask_user` is offered only when a surface can answer and the run is not autonomous.
|
|
92
|
+
const canAsk = Boolean(config.askUser) && !config.auto && !config.disableAskUser;
|
|
93
|
+
const askFiltered = canAsk ? memoryFiltered : memoryFiltered.filter((t) => t.name !== 'ask_user');
|
|
94
|
+
const sandboxedTools = applySandbox(askFiltered, config.toolSandbox);
|
|
81
95
|
this.tools = this.wrapToolsWithCache(sandboxedTools, config.cacheSize, config.cacheTtlMs, config.disableCache);
|
|
82
96
|
this.cache = new ToolResultCache({
|
|
83
97
|
maxSize: config.disableCache ? 0 : (config.cacheSize ?? 256),
|
|
@@ -96,7 +110,7 @@ export class Agent {
|
|
|
96
110
|
const cfg = this.checkpoints?.captureBeforeTools;
|
|
97
111
|
if (!cfg) {
|
|
98
112
|
// Default: file-mutating tools + run_command (which can mutate via shell).
|
|
99
|
-
return new Set(['write_file', 'edit_file', 'edit_notebook', 'run_command']);
|
|
113
|
+
return new Set(['write_file', 'edit_file', 'edit_notebook', 'apply_patch', 'run_command']);
|
|
100
114
|
}
|
|
101
115
|
return new Set(cfg);
|
|
102
116
|
}
|
|
@@ -235,12 +249,29 @@ export class Agent {
|
|
|
235
249
|
let verification;
|
|
236
250
|
// Infinite output: text of a reply that hit the output limit and is being continued.
|
|
237
251
|
let continuedText = '';
|
|
252
|
+
// The answer text of the step now streaming, kept so a Stop does not throw away what was already said.
|
|
253
|
+
let partialStepText = '';
|
|
238
254
|
let continuations = 0;
|
|
239
255
|
const maxContinuations = this.config.maxContinuations ?? DEFAULT_MAX_CONTINUATIONS;
|
|
240
256
|
const ctx = {
|
|
241
257
|
cwd: this.config.cwd ?? process.cwd(),
|
|
242
258
|
auto: this.config.auto ?? false,
|
|
243
259
|
parentModel: this.config.model,
|
|
260
|
+
...(this.config.subAgentModel ? { subAgentModel: this.config.subAgentModel } : {}),
|
|
261
|
+
...(this.config.backgroundAgents ? { background: this.config.backgroundAgents } : {}),
|
|
262
|
+
...(this.config.autoApproveTools ? { autoApproveTools: this.config.autoApproveTools } : {}),
|
|
263
|
+
...(this.config.permissionRules ? { permissionRules: this.config.permissionRules } : {}),
|
|
264
|
+
...(this.config.toolSandbox ? { toolSandbox: this.config.toolSandbox } : {}),
|
|
265
|
+
shownFolderInstructions: new Set(),
|
|
266
|
+
...(this.config.maxToolOutputChars ? { maxOutputChars: this.config.maxToolOutputChars } : {}),
|
|
267
|
+
...(this.config.askUser ? { askUser: this.config.askUser } : {}),
|
|
268
|
+
todos: {
|
|
269
|
+
get: () => this.todoList,
|
|
270
|
+
set: (todos) => {
|
|
271
|
+
this.todoList = todos;
|
|
272
|
+
emit({ type: 'todo_updated', todos });
|
|
273
|
+
},
|
|
274
|
+
},
|
|
244
275
|
...(this.config.commandRunner ? { commandRunner: this.config.commandRunner } : {}),
|
|
245
276
|
...(abortSignal ? { abortSignal } : {}),
|
|
246
277
|
...(this.config.memoryMaxBodyChars !== undefined ? { memoryMaxBodyChars: this.config.memoryMaxBodyChars } : {}),
|
|
@@ -289,13 +320,16 @@ export class Agent {
|
|
|
289
320
|
// Track per-turn token deltas so we can compute per-turn cost.
|
|
290
321
|
const turnStart = { input: inputTokens, output: outputTokens, cacheRead: cacheReadTokens, cacheWrite: cacheWriteTokens };
|
|
291
322
|
let reasoningText = '';
|
|
323
|
+
partialStepText = '';
|
|
292
324
|
// Inline <think> blocks (MiniMax over OpenAI-compatible, some open models) become reasoning events.
|
|
293
325
|
const thinkSplitter = new ThinkSplitter();
|
|
294
326
|
let wasInThink = false;
|
|
295
327
|
const emitSegments = (segments) => {
|
|
296
328
|
for (const seg of segments) {
|
|
297
|
-
if (seg.kind === 'text')
|
|
329
|
+
if (seg.kind === 'text') {
|
|
330
|
+
partialStepText += seg.text;
|
|
298
331
|
emit({ type: 'text_delta', text: seg.text });
|
|
332
|
+
}
|
|
299
333
|
else {
|
|
300
334
|
reasoningText += seg.text;
|
|
301
335
|
emit({ type: 'reasoning_delta', text: seg.text });
|
|
@@ -329,6 +363,7 @@ export class Agent {
|
|
|
329
363
|
tools: toAiSdkTools(this.tools),
|
|
330
364
|
abortSignal,
|
|
331
365
|
...(this.config.model.reasoningEffort ? { reasoning: this.config.model.reasoningEffort } : {}),
|
|
366
|
+
...(this.config.model.temperature !== undefined ? { temperature: this.config.model.temperature } : {}),
|
|
332
367
|
onChunk: ({ chunk }) => {
|
|
333
368
|
if (chunk.type === 'text-delta') {
|
|
334
369
|
emitSegments(thinkSplitter.push(chunk.text));
|
|
@@ -351,6 +386,7 @@ export class Agent {
|
|
|
351
386
|
},
|
|
352
387
|
onFinish: ({ text: rawText, usage }) => {
|
|
353
388
|
emitSegments(thinkSplitter.flush());
|
|
389
|
+
partialStepText = '';
|
|
354
390
|
if (reasoningText) {
|
|
355
391
|
emit({ type: 'reasoning_done', text: reasoningText });
|
|
356
392
|
reasoningText = '';
|
|
@@ -577,12 +613,16 @@ export class Agent {
|
|
|
577
613
|
this.audit?.record({ event: 'tool_result', tool: call.toolName, isError: false, durationMs: Date.now() - toolStartedAt }, auditCtx);
|
|
578
614
|
if (FILE_WRITE_TOOLS.has(call.toolName))
|
|
579
615
|
editedSinceVerify = true;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
616
|
+
// Every file the call worked on: a patch can change several (RB-13).
|
|
617
|
+
const touchedFiles = filesTouchedBy(call.toolName, toolArgs);
|
|
618
|
+
if (FILE_WRITE_TOOLS.has(call.toolName))
|
|
619
|
+
for (const p of touchedFiles)
|
|
620
|
+
editedFiles.add(p);
|
|
621
|
+
if (this.config.autoLint && FILE_WRITE_TOOLS.has(call.toolName)) {
|
|
622
|
+
for (const editedPath of touchedFiles) {
|
|
623
|
+
const report = await lintFile(editedPath, this.config.autoLint, { ...ctx, policy: undefined }).catch(() => null);
|
|
624
|
+
if (!report)
|
|
625
|
+
continue;
|
|
586
626
|
emit({ type: 'lint', path: report.path, passed: report.passed, problems: report.problems.length });
|
|
587
627
|
if (!report.passed) {
|
|
588
628
|
const note = lintMessage(report);
|
|
@@ -600,11 +640,8 @@ export class Agent {
|
|
|
600
640
|
}
|
|
601
641
|
currentToolResults.push(toolResult(call.toolCallId, call.toolName, finalResult));
|
|
602
642
|
trackLoop(call.toolName, toolArgs, finalResult);
|
|
603
|
-
if (STEERING_FILE_TOOLS.has(call.toolName))
|
|
604
|
-
|
|
605
|
-
if (typeof touched === 'string')
|
|
606
|
-
touchedPaths.push(touched);
|
|
607
|
-
}
|
|
643
|
+
if (STEERING_FILE_TOOLS.has(call.toolName))
|
|
644
|
+
touchedPaths.push(...filesTouchedBy(call.toolName, toolArgs));
|
|
608
645
|
emit({
|
|
609
646
|
type: 'tool_result',
|
|
610
647
|
name: call.toolName,
|
|
@@ -780,6 +817,14 @@ export class Agent {
|
|
|
780
817
|
const lastMessage = messages[messages.length - 1];
|
|
781
818
|
if (continuedText !== '' && lastMessage?.role === 'user' && lastMessage.content === CONTINUE_PROMPT)
|
|
782
819
|
messages.pop();
|
|
820
|
+
// Stopped mid-answer: keep the part the user already saw, marked as cut short, so "continue" can pick it up
|
|
821
|
+
// instead of starting over. Only when the model is the next speaker (after the user or a tool result).
|
|
822
|
+
if (reason === 'aborted' && partialStepText.trim()) {
|
|
823
|
+
const last = messages[messages.length - 1];
|
|
824
|
+
if (last && (last.role === 'user' || last.role === 'tool')) {
|
|
825
|
+
messages.push({ role: 'assistant', content: `${partialStepText.trim()}\n\n[Stopped by the user before finishing.]` });
|
|
826
|
+
}
|
|
827
|
+
}
|
|
783
828
|
// The browser is per-run: never leave a Chrome process behind.
|
|
784
829
|
await this.browser?.close().catch(() => undefined);
|
|
785
830
|
emit({ type: 'done', reason });
|
|
@@ -883,21 +928,68 @@ export class Agent {
|
|
|
883
928
|
return { ...t, execute: invalidating };
|
|
884
929
|
});
|
|
885
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* What the permission rules say about one tool call: `allow` (every target allowed), `deny` (any target denied,
|
|
933
|
+
* with the reason the model sees), `ask` (a target asks, a `.env` file is read, or a command leaves the workspace),
|
|
934
|
+
* or undefined when no rule applies and the normal approval flow decides.
|
|
935
|
+
*/
|
|
936
|
+
decidePermission(toolName, args, cwd) {
|
|
937
|
+
const p = permissionFor(toolName, args, cwd);
|
|
938
|
+
if (!p || p.targets.length === 0)
|
|
939
|
+
return undefined;
|
|
940
|
+
const rules = this.permissionRules;
|
|
941
|
+
const explicit = rules.filter((r) => r.pattern !== '*' && r.pattern !== '**');
|
|
942
|
+
const actions = p.targets.map((t) => {
|
|
943
|
+
// A broad rule never covers a secrets file: only a rule that names it can allow it.
|
|
944
|
+
// Headless runs (no approve callback) have nobody to ask, so they keep reading as before.
|
|
945
|
+
if (p.permission === 'read' && isSensitiveFile(t))
|
|
946
|
+
return evaluatePermission(explicit, 'read', t) ?? (this.config.approve ? 'ask' : evaluatePermission(rules, 'read', t));
|
|
947
|
+
return evaluatePermission(rules, p.permission, t);
|
|
948
|
+
});
|
|
949
|
+
if (p.permission === 'bash') {
|
|
950
|
+
const command = args.command;
|
|
951
|
+
if (typeof command === 'string' && touchesOutsideCwd(command, cwd)) {
|
|
952
|
+
const external = evaluatePermission(rules, 'external_directory', command);
|
|
953
|
+
if (external === 'deny')
|
|
954
|
+
return { action: 'deny', reason: `Blocked by permission rule (external_directory ${command}).` };
|
|
955
|
+
if (external !== 'allow' && this.config.approve)
|
|
956
|
+
actions.push('ask');
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const denied = actions.indexOf('deny');
|
|
960
|
+
if (denied >= 0)
|
|
961
|
+
return { action: 'deny', reason: `Blocked by permission rule (${p.permission} ${p.targets[denied]}).` };
|
|
962
|
+
if (actions.includes('ask'))
|
|
963
|
+
return { action: 'ask', reason: '' };
|
|
964
|
+
if (actions.every((a) => a === 'allow'))
|
|
965
|
+
return { action: 'allow', reason: '' };
|
|
966
|
+
return undefined;
|
|
967
|
+
}
|
|
886
968
|
async requestApproval(def, id, args, abortSignal, ctx) {
|
|
969
|
+
// Bookkeeping tools (the todo list) never ask.
|
|
970
|
+
if (def.noApproval)
|
|
971
|
+
return { approved: true };
|
|
972
|
+
// Permission rules come first: deny blocks, allow runs, ask skips every shortcut below.
|
|
973
|
+
const rule = this.decidePermission(def.name, args, ctx?.cwd ?? this.config.cwd ?? process.cwd());
|
|
974
|
+
if (rule?.action === 'deny')
|
|
975
|
+
return { approved: false, note: rule.reason };
|
|
976
|
+
if (rule?.action === 'allow')
|
|
977
|
+
return { approved: true };
|
|
978
|
+
const forceAsk = rule?.action === 'ask';
|
|
887
979
|
// Read-only tools the user chose not to be asked about (never a destructive one).
|
|
888
|
-
if (!def.destructive && this.config.autoApproveTools?.includes(def.name)) {
|
|
980
|
+
if (!forceAsk && !def.destructive && this.config.autoApproveTools?.includes(def.name)) {
|
|
889
981
|
return { approved: true };
|
|
890
982
|
}
|
|
891
983
|
// Memory writes the user chose not to be asked about. memory_forget is deleted data: always asked.
|
|
892
|
-
if (def.name === 'memory_write' && this.config.memoryAutoApproveWrites) {
|
|
984
|
+
if (!forceAsk && def.name === 'memory_write' && this.config.memoryAutoApproveWrites) {
|
|
893
985
|
return { approved: true };
|
|
894
986
|
}
|
|
895
987
|
// A plain command the user already said "always allow" for: exact match only (see command-allow.ts).
|
|
896
|
-
if (def.name === 'run_command' && this.config.autoApproveCommands?.length && isListedCommand(args, this.config.autoApproveCommands)) {
|
|
988
|
+
if (!forceAsk && def.name === 'run_command' && this.config.autoApproveCommands?.length && isListedCommand(args, this.config.autoApproveCommands)) {
|
|
897
989
|
return { approved: true };
|
|
898
990
|
}
|
|
899
991
|
const auto = this.config.auto ?? false;
|
|
900
|
-
if (auto && !(def.destructive && !this.config.allowDestructive)) {
|
|
992
|
+
if (!forceAsk && auto && !(def.destructive && !this.config.allowDestructive)) {
|
|
901
993
|
return { approved: true };
|
|
902
994
|
}
|
|
903
995
|
let preview;
|
|
@@ -1093,6 +1185,15 @@ async function buildInitialMessages(config, task, skills, steeringSystem = [], u
|
|
|
1093
1185
|
// Unreadable instruction files are skipped, never fatal.
|
|
1094
1186
|
}
|
|
1095
1187
|
}
|
|
1188
|
+
let extraInstructionsBlock = '';
|
|
1189
|
+
if (config.extraInstructions?.length) {
|
|
1190
|
+
try {
|
|
1191
|
+
extraInstructionsBlock = await buildExtraInstructionsBlock(cwd, config.extraInstructions);
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
// Skipped, never fatal.
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1096
1197
|
let mapBlock = '';
|
|
1097
1198
|
if (config.repoMap) {
|
|
1098
1199
|
try {
|
|
@@ -1105,7 +1206,7 @@ async function buildInitialMessages(config, task, skills, steeringSystem = [], u
|
|
|
1105
1206
|
// Compose the final system prompt: base + (memory, skills) blocks in a
|
|
1106
1207
|
// stable order so cache hits survive across runs that share state.
|
|
1107
1208
|
const steeringBlock = steeringSystem.map(wrapSteering).join('\n\n');
|
|
1108
|
-
const blocks = [steeringBlock, instructionsBlock, config.projectCommands ?? '', memoryBlock, skillsBlock, mapBlock].filter((b) => b.length > 0);
|
|
1209
|
+
const blocks = [steeringBlock, instructionsBlock, extraInstructionsBlock, config.mcpInstructions ?? '', config.projectCommands ?? '', memoryBlock, skillsBlock, mapBlock].filter((b) => b.length > 0);
|
|
1109
1210
|
const systemContent = blocks.length > 0 ? `${basePrompt}\n\n${blocks.join('\n\n')}` : basePrompt;
|
|
1110
1211
|
return [
|
|
1111
1212
|
{ role: 'system', content: systemContent },
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agents that keep working after `sub_agent` has returned. The chat owns one registry per conversation: the
|
|
3
|
+
* model starts a run with `sub_agent { background: true }` and gets an id back at once; it collects the answer with
|
|
4
|
+
* `sub_agent_wait`, or the chat hands it every finished answer it has not seen with the next message. The person sees
|
|
5
|
+
* the runs in a strip above the message box and can stop one, stop all, or dismiss a finished one.
|
|
6
|
+
*/
|
|
7
|
+
import type { SubAgentResult } from '../tools/sub-agent.js';
|
|
8
|
+
export type BackgroundState = 'running' | 'done' | 'failed' | 'stopped';
|
|
9
|
+
export interface BackgroundAgent {
|
|
10
|
+
id: string;
|
|
11
|
+
task: string;
|
|
12
|
+
/** The agent file it runs as, when it was started with one. */
|
|
13
|
+
agent?: string;
|
|
14
|
+
state: BackgroundState;
|
|
15
|
+
startedAt: number;
|
|
16
|
+
endedAt?: number;
|
|
17
|
+
result?: SubAgentResult;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/** More than this at once costs more than anyone meant to spend. */
|
|
21
|
+
export declare const MAX_RUNNING_BACKGROUND = 4;
|
|
22
|
+
export interface StartOptions {
|
|
23
|
+
task: string;
|
|
24
|
+
agent?: string | undefined;
|
|
25
|
+
/** Runs the work; must finish soon after `signal` aborts. */
|
|
26
|
+
run: (signal: AbortSignal) => Promise<SubAgentResult>;
|
|
27
|
+
}
|
|
28
|
+
export declare class BackgroundAgents {
|
|
29
|
+
private readonly entries;
|
|
30
|
+
private readonly listeners;
|
|
31
|
+
private counter;
|
|
32
|
+
start(options: StartOptions): BackgroundAgent;
|
|
33
|
+
list(): BackgroundAgent[];
|
|
34
|
+
get(id: string): BackgroundAgent | undefined;
|
|
35
|
+
stop(id: string): void;
|
|
36
|
+
stopAll(): void;
|
|
37
|
+
/** Stop everything and forget it all (a new chat starts with none). */
|
|
38
|
+
clear(): void;
|
|
39
|
+
/** Remove a finished entry from the list. A running one must be stopped first. */
|
|
40
|
+
dismiss(id: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Wait until the named runs (default: all that are running) have finished, or the timeout passes. Never throws:
|
|
43
|
+
* the answer is the current state of each run.
|
|
44
|
+
*/
|
|
45
|
+
wait(ids?: readonly string[], options?: {
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}): Promise<BackgroundAgent[]>;
|
|
49
|
+
/** Finished runs the model has not been told about yet; each is returned once. */
|
|
50
|
+
takeUndelivered(): BackgroundAgent[];
|
|
51
|
+
/** Mark runs as already known to the model (a `sub_agent_wait` answered for them). */
|
|
52
|
+
markDelivered(ids: readonly string[]): void;
|
|
53
|
+
onChange(listener: () => void): () => void;
|
|
54
|
+
private changed;
|
|
55
|
+
private view;
|
|
56
|
+
}
|
|
57
|
+
/** The model-facing text for finished runs: what each was asked and what it answered. */
|
|
58
|
+
export declare function backgroundResultsBlock(finished: readonly BackgroundAgent[]): string;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** More than this at once costs more than anyone meant to spend. */
|
|
2
|
+
export const MAX_RUNNING_BACKGROUND = 4;
|
|
3
|
+
export class BackgroundAgents {
|
|
4
|
+
entries = new Map();
|
|
5
|
+
listeners = new Set();
|
|
6
|
+
counter = 0;
|
|
7
|
+
start(options) {
|
|
8
|
+
const running = [...this.entries.values()].filter((e) => e.state === 'running').length;
|
|
9
|
+
if (running >= MAX_RUNNING_BACKGROUND) {
|
|
10
|
+
throw new Error(`At most ${MAX_RUNNING_BACKGROUND} background agents can run at once; wait for one to finish or stop one.`);
|
|
11
|
+
}
|
|
12
|
+
this.counter += 1;
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const entry = {
|
|
15
|
+
id: `bg-${this.counter}`,
|
|
16
|
+
task: options.task,
|
|
17
|
+
...(options.agent ? { agent: options.agent } : {}),
|
|
18
|
+
state: 'running',
|
|
19
|
+
startedAt: Date.now(),
|
|
20
|
+
controller,
|
|
21
|
+
finished: Promise.resolve(),
|
|
22
|
+
delivered: false,
|
|
23
|
+
};
|
|
24
|
+
entry.finished = options.run(controller.signal).then((result) => {
|
|
25
|
+
entry.result = result;
|
|
26
|
+
// A stop makes the child return with reason "aborted": that is a stopped run, not a finished one.
|
|
27
|
+
entry.state = controller.signal.aborted || result.reason === 'aborted' ? 'stopped' : 'done';
|
|
28
|
+
entry.endedAt = Date.now();
|
|
29
|
+
this.changed();
|
|
30
|
+
}, (err) => {
|
|
31
|
+
entry.error = err?.message ?? String(err);
|
|
32
|
+
entry.state = controller.signal.aborted ? 'stopped' : 'failed';
|
|
33
|
+
entry.endedAt = Date.now();
|
|
34
|
+
this.changed();
|
|
35
|
+
});
|
|
36
|
+
this.entries.set(entry.id, entry);
|
|
37
|
+
this.changed();
|
|
38
|
+
return this.view(entry);
|
|
39
|
+
}
|
|
40
|
+
list() {
|
|
41
|
+
return [...this.entries.values()].map((e) => this.view(e));
|
|
42
|
+
}
|
|
43
|
+
get(id) {
|
|
44
|
+
const e = this.entries.get(id);
|
|
45
|
+
return e ? this.view(e) : undefined;
|
|
46
|
+
}
|
|
47
|
+
stop(id) {
|
|
48
|
+
const e = this.entries.get(id);
|
|
49
|
+
if (e?.state === 'running')
|
|
50
|
+
e.controller.abort();
|
|
51
|
+
}
|
|
52
|
+
stopAll() {
|
|
53
|
+
for (const e of this.entries.values())
|
|
54
|
+
if (e.state === 'running')
|
|
55
|
+
e.controller.abort();
|
|
56
|
+
}
|
|
57
|
+
/** Stop everything and forget it all (a new chat starts with none). */
|
|
58
|
+
clear() {
|
|
59
|
+
this.stopAll();
|
|
60
|
+
this.entries.clear();
|
|
61
|
+
this.changed();
|
|
62
|
+
}
|
|
63
|
+
/** Remove a finished entry from the list. A running one must be stopped first. */
|
|
64
|
+
dismiss(id) {
|
|
65
|
+
const e = this.entries.get(id);
|
|
66
|
+
if (!e || e.state === 'running')
|
|
67
|
+
return false;
|
|
68
|
+
this.entries.delete(id);
|
|
69
|
+
this.changed();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Wait until the named runs (default: all that are running) have finished, or the timeout passes. Never throws:
|
|
74
|
+
* the answer is the current state of each run.
|
|
75
|
+
*/
|
|
76
|
+
async wait(ids, options = {}) {
|
|
77
|
+
const chosen = ids ? ids.map((id) => this.entries.get(id)).filter((e) => e !== undefined) : [...this.entries.values()].filter((e) => e.state === 'running');
|
|
78
|
+
const all = Promise.all(chosen.map((e) => e.finished));
|
|
79
|
+
const races = [all];
|
|
80
|
+
let timer;
|
|
81
|
+
if (options.timeoutMs !== undefined)
|
|
82
|
+
races.push(new Promise((resolve) => (timer = setTimeout(resolve, options.timeoutMs))));
|
|
83
|
+
if (options.signal) {
|
|
84
|
+
const signal = options.signal;
|
|
85
|
+
races.push(new Promise((resolve) => (signal.aborted ? resolve(undefined) : signal.addEventListener('abort', () => resolve(undefined), { once: true }))));
|
|
86
|
+
}
|
|
87
|
+
await Promise.race(races);
|
|
88
|
+
if (timer)
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
return chosen.map((e) => this.view(e));
|
|
91
|
+
}
|
|
92
|
+
/** Finished runs the model has not been told about yet; each is returned once. */
|
|
93
|
+
takeUndelivered() {
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const e of this.entries.values()) {
|
|
96
|
+
if (e.state !== 'running' && !e.delivered) {
|
|
97
|
+
e.delivered = true;
|
|
98
|
+
out.push(this.view(e));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** Mark runs as already known to the model (a `sub_agent_wait` answered for them). */
|
|
104
|
+
markDelivered(ids) {
|
|
105
|
+
for (const id of ids) {
|
|
106
|
+
const e = this.entries.get(id);
|
|
107
|
+
if (e && e.state !== 'running')
|
|
108
|
+
e.delivered = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
onChange(listener) {
|
|
112
|
+
this.listeners.add(listener);
|
|
113
|
+
return () => this.listeners.delete(listener);
|
|
114
|
+
}
|
|
115
|
+
changed() {
|
|
116
|
+
for (const l of this.listeners) {
|
|
117
|
+
try {
|
|
118
|
+
l();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// a listener that throws must not stop the run bookkeeping
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
view(e) {
|
|
126
|
+
const { controller: _c, finished: _f, delivered: _d, ...rest } = e;
|
|
127
|
+
return { ...rest };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** The model-facing text for finished runs: what each was asked and what it answered. */
|
|
131
|
+
export function backgroundResultsBlock(finished) {
|
|
132
|
+
if (finished.length === 0)
|
|
133
|
+
return '';
|
|
134
|
+
const parts = finished.map((a) => {
|
|
135
|
+
const body = a.state === 'done' ? (a.result?.finalText ?? '').trim() || '(no text)' : a.state === 'failed' ? `failed: ${a.error ?? 'unknown error'}` : `stopped by the user${a.result?.finalText ? `; it had said: ${a.result.finalText.trim()}` : ''}`;
|
|
136
|
+
return `<background-agent id="${a.id}" state="${a.state}" task="${a.task.replace(/"/g, "'").slice(0, 200)}">\n${body}\n</background-agent>`;
|
|
137
|
+
});
|
|
138
|
+
return `<background-agents>\n${parts.join('\n')}\n</background-agents>`;
|
|
139
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { LanguageModel } from 'ai';
|
|
2
2
|
import type { ModelConfig } from '../types.js';
|
|
3
|
+
/** The prompt for rewriting `input`: the user's own instruction when given (a blank one is ignored), else the built-in. */
|
|
4
|
+
export declare function enhanceRequestPrompt(input: string, instruction?: string): string;
|
|
3
5
|
/**
|
|
4
6
|
* Rewrite a draft chat message into a clearer request using `model` (typically the cheap model).
|
|
5
7
|
* Never throws: an empty input, a missing model, a model error or an empty reply all return `input` unchanged.
|
|
6
8
|
*/
|
|
7
|
-
export declare function enhancePrompt(input: string, model: ModelConfig | undefined, modelFactory?: (cfg: ModelConfig) => LanguageModel): Promise<string>;
|
|
9
|
+
export declare function enhancePrompt(input: string, model: ModelConfig | undefined, modelFactory?: (cfg: ModelConfig) => LanguageModel, instruction?: string): Promise<string>;
|
|
@@ -3,15 +3,19 @@ import { createLanguageModel } from '../providers.js';
|
|
|
3
3
|
import { stripThinkTags } from './think-tags.js';
|
|
4
4
|
const ENHANCE_INSTRUCTIONS = 'Rewrite the request below so a coding agent can act on it: say what to change and where, name the expected result, and keep every file, symbol and constraint the user mentioned. ' +
|
|
5
5
|
'Do not invent requirements or add steps the user did not ask for. Reply with the rewritten request only, no quotes, no code fences, no preface.';
|
|
6
|
+
/** The prompt for rewriting `input`: the user's own instruction when given (a blank one is ignored), else the built-in. */
|
|
7
|
+
export function enhanceRequestPrompt(input, instruction) {
|
|
8
|
+
return `${instruction?.trim() || ENHANCE_INSTRUCTIONS}\n\nRequest:\n${input}`;
|
|
9
|
+
}
|
|
6
10
|
/**
|
|
7
11
|
* Rewrite a draft chat message into a clearer request using `model` (typically the cheap model).
|
|
8
12
|
* Never throws: an empty input, a missing model, a model error or an empty reply all return `input` unchanged.
|
|
9
13
|
*/
|
|
10
|
-
export async function enhancePrompt(input, model, modelFactory = createLanguageModel) {
|
|
14
|
+
export async function enhancePrompt(input, model, modelFactory = createLanguageModel, instruction) {
|
|
11
15
|
if (!model || !input.trim())
|
|
12
16
|
return input;
|
|
13
17
|
try {
|
|
14
|
-
const result = await generateText({ model: modelFactory(model), prompt:
|
|
18
|
+
const result = await generateText({ model: modelFactory(model), prompt: enhanceRequestPrompt(input, instruction) });
|
|
15
19
|
return clean(result.text) || input;
|
|
16
20
|
}
|
|
17
21
|
catch {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolDefinition } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Session goals: one objective kept in focus across turns. Pure state machine plus the `goal_report` tool the
|
|
5
|
+
* model uses to say it is done or stuck. The loop that runs the turns lives in the surface (VS Code chat).
|
|
6
|
+
*/
|
|
7
|
+
export type GoalStatus = 'active' | 'paused' | 'blocked' | 'complete';
|
|
8
|
+
export interface GoalState {
|
|
9
|
+
objective: string;
|
|
10
|
+
status: GoalStatus;
|
|
11
|
+
/** The model's own closing report (complete or blocked); not independently verified. */
|
|
12
|
+
report?: string;
|
|
13
|
+
/** Turns used so far. */
|
|
14
|
+
turns: number;
|
|
15
|
+
}
|
|
16
|
+
export interface GoalReport {
|
|
17
|
+
kind: 'complete' | 'blocked';
|
|
18
|
+
reason: string;
|
|
19
|
+
}
|
|
20
|
+
export interface TurnOutcome {
|
|
21
|
+
/** Tool calls the turn made; zero with no report means the model made no progress. */
|
|
22
|
+
toolCalls: number;
|
|
23
|
+
error?: boolean;
|
|
24
|
+
stopped?: boolean;
|
|
25
|
+
report?: GoalReport;
|
|
26
|
+
/** A permission request or tool call was rejected. */
|
|
27
|
+
rejected?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare const MAX_OBJECTIVE_CHARS = 10000;
|
|
30
|
+
export declare function startGoal(objective: string): GoalState;
|
|
31
|
+
/**
|
|
32
|
+
* The goal after one turn. Stop, an error, no progress or an exhausted turn budget pause it; a rejection or a
|
|
33
|
+
* `blocked` report blocks it; a `complete` report finishes it. A report counts only when the turn ended cleanly.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyTurnOutcome(g: GoalState, o: TurnOutcome, maxTurns: number): GoalState;
|
|
36
|
+
export declare function pauseGoal(g: GoalState): GoalState;
|
|
37
|
+
/** Paused and blocked goals continue; a complete one restarts from zero turns. */
|
|
38
|
+
export declare function resumeGoal(g: GoalState): GoalState;
|
|
39
|
+
/** The message that drives one goal turn. */
|
|
40
|
+
export declare function goalPrompt(g: GoalState): string;
|
|
41
|
+
declare const reportParams: z.ZodObject<{
|
|
42
|
+
kind: z.ZodEnum<["complete", "blocked"]>;
|
|
43
|
+
reason: z.ZodString;
|
|
44
|
+
}, "strip", z.ZodTypeAny, {
|
|
45
|
+
kind: "blocked" | "complete";
|
|
46
|
+
reason: string;
|
|
47
|
+
}, {
|
|
48
|
+
kind: "blocked" | "complete";
|
|
49
|
+
reason: string;
|
|
50
|
+
}>;
|
|
51
|
+
export declare function createGoalReportTool(onReport: (r: GoalReport) => void): ToolDefinition<z.infer<typeof reportParams>, {
|
|
52
|
+
recorded: true;
|
|
53
|
+
}>;
|
|
54
|
+
export {};
|