@ikarmic/forge-core 5.0.0 → 5.1.1
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 +1 -1
- package/dist/agent/agent.d.ts +8 -0
- package/dist/agent/agent.js +148 -24
- 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/agent/prompts.d.ts +25 -1
- package/dist/agent/prompts.js +61 -17
- 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 +34 -5
- package/dist/index.js +27 -4
- package/dist/journey/answers-block.d.ts +17 -0
- package/dist/journey/answers-block.js +15 -0
- package/dist/journey/index.d.ts +9 -0
- package/dist/journey/index.js +8 -0
- package/dist/journey/journey.d.ts +13 -0
- package/dist/journey/journey.js +40 -0
- package/dist/journey/offer.d.ts +25 -0
- package/dist/journey/offer.js +48 -0
- package/dist/journey/questions.d.ts +23 -0
- package/dist/journey/questions.js +49 -0
- package/dist/journey/stages.d.ts +13 -0
- package/dist/journey/stages.js +107 -0
- package/dist/journey/survey.d.ts +10 -0
- package/dist/journey/survey.js +203 -0
- package/dist/journey/trigger.d.ts +19 -0
- package/dist/journey/trigger.js +79 -0
- package/dist/journey/types.d.ts +72 -0
- package/dist/journey/types.js +1 -0
- 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/specs/generate.d.ts +18 -9
- package/dist/specs/generate.js +174 -14
- package/dist/specs/index.d.ts +2 -2
- package/dist/specs/index.js +1 -1
- package/dist/specs/store.d.ts +14 -2
- package/dist/specs/store.js +26 -1
- package/dist/specs/types.d.ts +11 -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 +60 -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 +12 -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 +213 -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 +98 -0
- package/dist/worktree/worktree.d.ts +3 -0
- package/dist/worktree/worktree.js +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@ikarmic/forge-core`
|
|
2
2
|
|
|
3
|
-
The agent engine for [IKarmic AI Forge](https://github.com/ikarmic
|
|
3
|
+
The agent engine for [IKarmic AI Forge](https://github.com/ikarmicai/ikarmic-forge). Shared by the CLI and the VS Code extension.
|
|
4
4
|
|
|
5
5
|
## What's in the box
|
|
6
6
|
|
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,11 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { release } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
1
4
|
import { streamText } from 'ai';
|
|
2
5
|
import { lintFile, lintMessage } from '../lint/auto-lint.js';
|
|
3
6
|
import { buildMemoryBlock } from '../memory/memory-store.js';
|
|
7
|
+
import { filesTouchedBy } from '../tools/write-tools.js';
|
|
8
|
+
import { buildExtraInstructionsBlock } from '../steering/extra-instructions.js';
|
|
4
9
|
import { buildProjectInstructionsBlock } from '../steering/project-instructions.js';
|
|
5
10
|
import { buildRepoMap, repoMapBlock } from '../repomap/repo-map.js';
|
|
6
11
|
import { buildLlmSummarizer, compactMessages, effectiveLimitTokens, getModelWindowTokens, shouldCompactForBudget } from '../context/compaction.js';
|
|
@@ -17,6 +22,7 @@ import { embedderFromEnv } from '../semantic/embedder.js';
|
|
|
17
22
|
import { DEFAULT_VERIFY_RETRIES, failureMessage, runVerification } from './verify.js';
|
|
18
23
|
import { LoopGuard, resultKeyOf } from './loop-guard.js';
|
|
19
24
|
import { isListedCommand } from '../tools/command-allow.js';
|
|
25
|
+
import { compilePermissionRules, evaluatePermission, isSensitiveFile, permissionFor, touchesOutsideCwd, } from '../tools/permission-rules.js';
|
|
20
26
|
import { createLanguageModel } from '../providers.js';
|
|
21
27
|
import { BUILTIN_TOOLS } from '../tools/index.js';
|
|
22
28
|
import { ToolResultCache, cachedExecute } from '../tools/result-cache.js';
|
|
@@ -26,15 +32,16 @@ import { composeSkillsPrompt, selectSkills } from '../skills/activator.js';
|
|
|
26
32
|
import { MAX_TOOL_INJECTIONS, selectAtStart, selectOnToolTouch, toWorkspaceRelative, wrapSteering } from '../steering/select.js';
|
|
27
33
|
import { applyCacheHints } from './cache-hints.js';
|
|
28
34
|
import { attachmentParts, formatBytes, mediaPartTokens } from '../attachments.js';
|
|
29
|
-
import {
|
|
35
|
+
import { renderAnswersBlock } from '../journey/answers-block.js';
|
|
36
|
+
import { CONTINUE_PROMPT, DEFAULT_PLAN_MODE_SUFFIX, DEFAULT_SPEC_MODE_SUFFIX, DEFAULT_SYSTEM_PROMPT, environmentBlock } from './prompts.js';
|
|
30
37
|
/** Read-only tool names whose output flows through the redactor. */
|
|
31
38
|
const DEFAULT_MAX_CONTINUATIONS = 3;
|
|
32
39
|
export const DEFAULT_MAX_DURATION_MS = 10 * 60 * 1000;
|
|
33
40
|
const REDACTABLE_TOOLS = new Set(['read_file', 'search_files', 'list_files']);
|
|
34
41
|
/** 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']);
|
|
42
|
+
const STEERING_FILE_TOOLS = new Set(['read_file', 'write_file', 'edit_file', 'edit_notebook', 'apply_patch']);
|
|
36
43
|
/** 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']);
|
|
44
|
+
const FILE_WRITE_TOOLS = new Set(['write_file', 'edit_file', 'edit_notebook', 'apply_patch']);
|
|
38
45
|
/** Pure reads whose results may be reused within a run. Every other tool invalidates the cache. */
|
|
39
46
|
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
47
|
/**
|
|
@@ -52,12 +59,16 @@ export class Agent {
|
|
|
52
59
|
checkpoints;
|
|
53
60
|
browser;
|
|
54
61
|
audit;
|
|
62
|
+
permissionRules;
|
|
63
|
+
todoList = [];
|
|
55
64
|
constructor(config) {
|
|
56
65
|
// Organization policy first: everything below sees the already-tightened config.
|
|
57
66
|
const policy = config.policy === undefined ? getOrgPolicy()?.policy : (config.policy ?? undefined);
|
|
58
67
|
config = applyPolicy(config, policy);
|
|
59
68
|
this.audit = config.audit ?? (policy?.audit?.enabled ? createFileAuditSink(policy.audit.file) : undefined);
|
|
60
69
|
this.config = config;
|
|
70
|
+
this.permissionRules = compilePermissionRules(config.permissionRules);
|
|
71
|
+
this.todoList = config.initialTodos ? [...config.initialTodos] : [];
|
|
61
72
|
const allTools = (config.disableBuiltins
|
|
62
73
|
? [...(config.extraTools ?? [])]
|
|
63
74
|
: [...BUILTIN_TOOLS, ...(config.extraTools ?? [])]);
|
|
@@ -72,12 +83,19 @@ export class Agent {
|
|
|
72
83
|
if (webSearch)
|
|
73
84
|
allTools.push(createWebSearchTool(webSearch));
|
|
74
85
|
if (config.semanticSearch) {
|
|
75
|
-
allTools.push(createCodebaseSearchTool(config.semanticSearch.embedder ?? embedderFromEnv()
|
|
86
|
+
allTools.push(createCodebaseSearchTool(config.semanticSearch.embedder ?? embedderFromEnv(), {
|
|
87
|
+
...(config.semanticSearch.extensions ? { extensions: config.semanticSearch.extensions } : {}),
|
|
88
|
+
...(config.semanticSearch.minScore !== undefined ? { minScore: config.semanticSearch.minScore } : {}),
|
|
89
|
+
...(config.semanticSearch.defaultLimit ? { defaultLimit: config.semanticSearch.defaultLimit } : {}),
|
|
90
|
+
}));
|
|
76
91
|
}
|
|
77
92
|
const memoryFiltered = config.disableMemory
|
|
78
93
|
? allTools.filter((t) => t.name !== 'memory_read' && t.name !== 'memory_write' && t.name !== 'memory_forget')
|
|
79
94
|
: allTools;
|
|
80
|
-
|
|
95
|
+
// `ask_user` is offered only when a surface can answer and the run is not autonomous.
|
|
96
|
+
const canAsk = Boolean(config.askUser) && !config.auto && !config.disableAskUser;
|
|
97
|
+
const askFiltered = canAsk ? memoryFiltered : memoryFiltered.filter((t) => t.name !== 'ask_user');
|
|
98
|
+
const sandboxedTools = applySandbox(askFiltered, config.toolSandbox);
|
|
81
99
|
this.tools = this.wrapToolsWithCache(sandboxedTools, config.cacheSize, config.cacheTtlMs, config.disableCache);
|
|
82
100
|
this.cache = new ToolResultCache({
|
|
83
101
|
maxSize: config.disableCache ? 0 : (config.cacheSize ?? 256),
|
|
@@ -96,7 +114,7 @@ export class Agent {
|
|
|
96
114
|
const cfg = this.checkpoints?.captureBeforeTools;
|
|
97
115
|
if (!cfg) {
|
|
98
116
|
// Default: file-mutating tools + run_command (which can mutate via shell).
|
|
99
|
-
return new Set(['write_file', 'edit_file', 'edit_notebook', 'run_command']);
|
|
117
|
+
return new Set(['write_file', 'edit_file', 'edit_notebook', 'apply_patch', 'run_command']);
|
|
100
118
|
}
|
|
101
119
|
return new Set(cfg);
|
|
102
120
|
}
|
|
@@ -235,12 +253,30 @@ export class Agent {
|
|
|
235
253
|
let verification;
|
|
236
254
|
// Infinite output: text of a reply that hit the output limit and is being continued.
|
|
237
255
|
let continuedText = '';
|
|
256
|
+
// The answer text of the step now streaming, kept so a Stop does not throw away what was already said.
|
|
257
|
+
let partialStepText = '';
|
|
238
258
|
let continuations = 0;
|
|
239
259
|
const maxContinuations = this.config.maxContinuations ?? DEFAULT_MAX_CONTINUATIONS;
|
|
240
260
|
const ctx = {
|
|
241
261
|
cwd: this.config.cwd ?? process.cwd(),
|
|
242
262
|
auto: this.config.auto ?? false,
|
|
243
263
|
parentModel: this.config.model,
|
|
264
|
+
...(this.config.subAgentModel ? { subAgentModel: this.config.subAgentModel } : {}),
|
|
265
|
+
...(this.config.backgroundAgents ? { background: this.config.backgroundAgents } : {}),
|
|
266
|
+
...(this.config.autoApproveTools ? { autoApproveTools: this.config.autoApproveTools } : {}),
|
|
267
|
+
...(this.config.permissionRules ? { permissionRules: this.config.permissionRules } : {}),
|
|
268
|
+
...(this.config.toolSandbox ? { toolSandbox: this.config.toolSandbox } : {}),
|
|
269
|
+
shownFolderInstructions: new Set(),
|
|
270
|
+
...(this.config.maxToolOutputChars ? { maxOutputChars: this.config.maxToolOutputChars } : {}),
|
|
271
|
+
...(this.config.askUser ? { askUser: this.config.askUser } : {}),
|
|
272
|
+
...(this.config.askBudget ? { askBudget: this.config.askBudget } : {}),
|
|
273
|
+
todos: {
|
|
274
|
+
get: () => this.todoList,
|
|
275
|
+
set: (todos) => {
|
|
276
|
+
this.todoList = todos;
|
|
277
|
+
emit({ type: 'todo_updated', todos });
|
|
278
|
+
},
|
|
279
|
+
},
|
|
244
280
|
...(this.config.commandRunner ? { commandRunner: this.config.commandRunner } : {}),
|
|
245
281
|
...(abortSignal ? { abortSignal } : {}),
|
|
246
282
|
...(this.config.memoryMaxBodyChars !== undefined ? { memoryMaxBodyChars: this.config.memoryMaxBodyChars } : {}),
|
|
@@ -289,13 +325,16 @@ export class Agent {
|
|
|
289
325
|
// Track per-turn token deltas so we can compute per-turn cost.
|
|
290
326
|
const turnStart = { input: inputTokens, output: outputTokens, cacheRead: cacheReadTokens, cacheWrite: cacheWriteTokens };
|
|
291
327
|
let reasoningText = '';
|
|
328
|
+
partialStepText = '';
|
|
292
329
|
// Inline <think> blocks (MiniMax over OpenAI-compatible, some open models) become reasoning events.
|
|
293
330
|
const thinkSplitter = new ThinkSplitter();
|
|
294
331
|
let wasInThink = false;
|
|
295
332
|
const emitSegments = (segments) => {
|
|
296
333
|
for (const seg of segments) {
|
|
297
|
-
if (seg.kind === 'text')
|
|
334
|
+
if (seg.kind === 'text') {
|
|
335
|
+
partialStepText += seg.text;
|
|
298
336
|
emit({ type: 'text_delta', text: seg.text });
|
|
337
|
+
}
|
|
299
338
|
else {
|
|
300
339
|
reasoningText += seg.text;
|
|
301
340
|
emit({ type: 'reasoning_delta', text: seg.text });
|
|
@@ -329,6 +368,7 @@ export class Agent {
|
|
|
329
368
|
tools: toAiSdkTools(this.tools),
|
|
330
369
|
abortSignal,
|
|
331
370
|
...(this.config.model.reasoningEffort ? { reasoning: this.config.model.reasoningEffort } : {}),
|
|
371
|
+
...(this.config.model.temperature !== undefined ? { temperature: this.config.model.temperature } : {}),
|
|
332
372
|
onChunk: ({ chunk }) => {
|
|
333
373
|
if (chunk.type === 'text-delta') {
|
|
334
374
|
emitSegments(thinkSplitter.push(chunk.text));
|
|
@@ -351,6 +391,7 @@ export class Agent {
|
|
|
351
391
|
},
|
|
352
392
|
onFinish: ({ text: rawText, usage }) => {
|
|
353
393
|
emitSegments(thinkSplitter.flush());
|
|
394
|
+
partialStepText = '';
|
|
354
395
|
if (reasoningText) {
|
|
355
396
|
emit({ type: 'reasoning_done', text: reasoningText });
|
|
356
397
|
reasoningText = '';
|
|
@@ -577,12 +618,16 @@ export class Agent {
|
|
|
577
618
|
this.audit?.record({ event: 'tool_result', tool: call.toolName, isError: false, durationMs: Date.now() - toolStartedAt }, auditCtx);
|
|
578
619
|
if (FILE_WRITE_TOOLS.has(call.toolName))
|
|
579
620
|
editedSinceVerify = true;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
621
|
+
// Every file the call worked on: a patch can change several (RB-13).
|
|
622
|
+
const touchedFiles = filesTouchedBy(call.toolName, toolArgs);
|
|
623
|
+
if (FILE_WRITE_TOOLS.has(call.toolName))
|
|
624
|
+
for (const p of touchedFiles)
|
|
625
|
+
editedFiles.add(p);
|
|
626
|
+
if (this.config.autoLint && FILE_WRITE_TOOLS.has(call.toolName)) {
|
|
627
|
+
for (const editedPath of touchedFiles) {
|
|
628
|
+
const report = await lintFile(editedPath, this.config.autoLint, { ...ctx, policy: undefined }).catch(() => null);
|
|
629
|
+
if (!report)
|
|
630
|
+
continue;
|
|
586
631
|
emit({ type: 'lint', path: report.path, passed: report.passed, problems: report.problems.length });
|
|
587
632
|
if (!report.passed) {
|
|
588
633
|
const note = lintMessage(report);
|
|
@@ -600,11 +645,8 @@ export class Agent {
|
|
|
600
645
|
}
|
|
601
646
|
currentToolResults.push(toolResult(call.toolCallId, call.toolName, finalResult));
|
|
602
647
|
trackLoop(call.toolName, toolArgs, finalResult);
|
|
603
|
-
if (STEERING_FILE_TOOLS.has(call.toolName))
|
|
604
|
-
|
|
605
|
-
if (typeof touched === 'string')
|
|
606
|
-
touchedPaths.push(touched);
|
|
607
|
-
}
|
|
648
|
+
if (STEERING_FILE_TOOLS.has(call.toolName))
|
|
649
|
+
touchedPaths.push(...filesTouchedBy(call.toolName, toolArgs));
|
|
608
650
|
emit({
|
|
609
651
|
type: 'tool_result',
|
|
610
652
|
name: call.toolName,
|
|
@@ -780,6 +822,14 @@ export class Agent {
|
|
|
780
822
|
const lastMessage = messages[messages.length - 1];
|
|
781
823
|
if (continuedText !== '' && lastMessage?.role === 'user' && lastMessage.content === CONTINUE_PROMPT)
|
|
782
824
|
messages.pop();
|
|
825
|
+
// Stopped mid-answer: keep the part the user already saw, marked as cut short, so "continue" can pick it up
|
|
826
|
+
// instead of starting over. Only when the model is the next speaker (after the user or a tool result).
|
|
827
|
+
if (reason === 'aborted' && partialStepText.trim()) {
|
|
828
|
+
const last = messages[messages.length - 1];
|
|
829
|
+
if (last && (last.role === 'user' || last.role === 'tool')) {
|
|
830
|
+
messages.push({ role: 'assistant', content: `${partialStepText.trim()}\n\n[Stopped by the user before finishing.]` });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
783
833
|
// The browser is per-run: never leave a Chrome process behind.
|
|
784
834
|
await this.browser?.close().catch(() => undefined);
|
|
785
835
|
emit({ type: 'done', reason });
|
|
@@ -883,21 +933,68 @@ export class Agent {
|
|
|
883
933
|
return { ...t, execute: invalidating };
|
|
884
934
|
});
|
|
885
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* What the permission rules say about one tool call: `allow` (every target allowed), `deny` (any target denied,
|
|
938
|
+
* with the reason the model sees), `ask` (a target asks, a `.env` file is read, or a command leaves the workspace),
|
|
939
|
+
* or undefined when no rule applies and the normal approval flow decides.
|
|
940
|
+
*/
|
|
941
|
+
decidePermission(toolName, args, cwd) {
|
|
942
|
+
const p = permissionFor(toolName, args, cwd);
|
|
943
|
+
if (!p || p.targets.length === 0)
|
|
944
|
+
return undefined;
|
|
945
|
+
const rules = this.permissionRules;
|
|
946
|
+
const explicit = rules.filter((r) => r.pattern !== '*' && r.pattern !== '**');
|
|
947
|
+
const actions = p.targets.map((t) => {
|
|
948
|
+
// A broad rule never covers a secrets file: only a rule that names it can allow it.
|
|
949
|
+
// Headless runs (no approve callback) have nobody to ask, so they keep reading as before.
|
|
950
|
+
if (p.permission === 'read' && isSensitiveFile(t))
|
|
951
|
+
return evaluatePermission(explicit, 'read', t) ?? (this.config.approve ? 'ask' : evaluatePermission(rules, 'read', t));
|
|
952
|
+
return evaluatePermission(rules, p.permission, t);
|
|
953
|
+
});
|
|
954
|
+
if (p.permission === 'bash') {
|
|
955
|
+
const command = args.command;
|
|
956
|
+
if (typeof command === 'string' && touchesOutsideCwd(command, cwd)) {
|
|
957
|
+
const external = evaluatePermission(rules, 'external_directory', command);
|
|
958
|
+
if (external === 'deny')
|
|
959
|
+
return { action: 'deny', reason: `Blocked by permission rule (external_directory ${command}).` };
|
|
960
|
+
if (external !== 'allow' && this.config.approve)
|
|
961
|
+
actions.push('ask');
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const denied = actions.indexOf('deny');
|
|
965
|
+
if (denied >= 0)
|
|
966
|
+
return { action: 'deny', reason: `Blocked by permission rule (${p.permission} ${p.targets[denied]}).` };
|
|
967
|
+
if (actions.includes('ask'))
|
|
968
|
+
return { action: 'ask', reason: '' };
|
|
969
|
+
if (actions.every((a) => a === 'allow'))
|
|
970
|
+
return { action: 'allow', reason: '' };
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
886
973
|
async requestApproval(def, id, args, abortSignal, ctx) {
|
|
974
|
+
// Bookkeeping tools (the todo list) never ask.
|
|
975
|
+
if (def.noApproval)
|
|
976
|
+
return { approved: true };
|
|
977
|
+
// Permission rules come first: deny blocks, allow runs, ask skips every shortcut below.
|
|
978
|
+
const rule = this.decidePermission(def.name, args, ctx?.cwd ?? this.config.cwd ?? process.cwd());
|
|
979
|
+
if (rule?.action === 'deny')
|
|
980
|
+
return { approved: false, note: rule.reason };
|
|
981
|
+
if (rule?.action === 'allow')
|
|
982
|
+
return { approved: true };
|
|
983
|
+
const forceAsk = rule?.action === 'ask';
|
|
887
984
|
// 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)) {
|
|
985
|
+
if (!forceAsk && !def.destructive && this.config.autoApproveTools?.includes(def.name)) {
|
|
889
986
|
return { approved: true };
|
|
890
987
|
}
|
|
891
988
|
// 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) {
|
|
989
|
+
if (!forceAsk && def.name === 'memory_write' && this.config.memoryAutoApproveWrites) {
|
|
893
990
|
return { approved: true };
|
|
894
991
|
}
|
|
895
992
|
// 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)) {
|
|
993
|
+
if (!forceAsk && def.name === 'run_command' && this.config.autoApproveCommands?.length && isListedCommand(args, this.config.autoApproveCommands)) {
|
|
897
994
|
return { approved: true };
|
|
898
995
|
}
|
|
899
996
|
const auto = this.config.auto ?? false;
|
|
900
|
-
if (auto && !(def.destructive && !this.config.allowDestructive)) {
|
|
997
|
+
if (!forceAsk && auto && !(def.destructive && !this.config.allowDestructive)) {
|
|
901
998
|
return { approved: true };
|
|
902
999
|
}
|
|
903
1000
|
let preview;
|
|
@@ -1093,6 +1190,15 @@ async function buildInitialMessages(config, task, skills, steeringSystem = [], u
|
|
|
1093
1190
|
// Unreadable instruction files are skipped, never fatal.
|
|
1094
1191
|
}
|
|
1095
1192
|
}
|
|
1193
|
+
let extraInstructionsBlock = '';
|
|
1194
|
+
if (config.extraInstructions?.length) {
|
|
1195
|
+
try {
|
|
1196
|
+
extraInstructionsBlock = await buildExtraInstructionsBlock(cwd, config.extraInstructions);
|
|
1197
|
+
}
|
|
1198
|
+
catch {
|
|
1199
|
+
// Skipped, never fatal.
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1096
1202
|
let mapBlock = '';
|
|
1097
1203
|
if (config.repoMap) {
|
|
1098
1204
|
try {
|
|
@@ -1102,10 +1208,28 @@ async function buildInitialMessages(config, task, skills, steeringSystem = [], u
|
|
|
1102
1208
|
// A map failure shouldn't kill a run: just skip.
|
|
1103
1209
|
}
|
|
1104
1210
|
}
|
|
1211
|
+
// Interview answers from a guided journey. This function runs only when no
|
|
1212
|
+
// history is passed (see the `options.history` branch in `run`), so every
|
|
1213
|
+
// block here is assembled once, on a session's first turn, and then rides
|
|
1214
|
+
// inside `history[0]`. That is why a journey's interview must finish before
|
|
1215
|
+
// the first run: an answer that arrives on turn 3 can never enter the system
|
|
1216
|
+
// message, and a middle turn would be summarised away by compaction.
|
|
1217
|
+
const answersBlock = renderAnswersBlock(config.journeyAnswers ?? []);
|
|
1218
|
+
// Facts the model would otherwise have to guess: the platform decides
|
|
1219
|
+
// whether `cat` or `type` works, and it cannot see the date at all.
|
|
1220
|
+
const envBlock = environmentBlock({
|
|
1221
|
+
cwd,
|
|
1222
|
+
platform: process.platform,
|
|
1223
|
+
osVersion: release(),
|
|
1224
|
+
isGitRepo: existsSync(path.join(cwd, '.git')),
|
|
1225
|
+
today: new Date().toISOString().slice(0, 10),
|
|
1226
|
+
model: config.model?.model,
|
|
1227
|
+
});
|
|
1105
1228
|
// Compose the final system prompt: base + (memory, skills) blocks in a
|
|
1106
|
-
// stable order so cache hits survive across runs that share state.
|
|
1229
|
+
// stable order so cache hits survive across runs that share state. The
|
|
1230
|
+
// environment block goes last: it is the part that varies per session.
|
|
1107
1231
|
const steeringBlock = steeringSystem.map(wrapSteering).join('\n\n');
|
|
1108
|
-
const blocks = [steeringBlock, instructionsBlock, config.projectCommands ?? '', memoryBlock, skillsBlock, mapBlock].filter((b) => b.length > 0);
|
|
1232
|
+
const blocks = [steeringBlock, instructionsBlock, extraInstructionsBlock, config.mcpInstructions ?? '', config.projectCommands ?? '', memoryBlock, skillsBlock, answersBlock, mapBlock, envBlock].filter((b) => b.length > 0);
|
|
1109
1233
|
const systemContent = blocks.length > 0 ? `${basePrompt}\n\n${blocks.join('\n\n')}` : basePrompt;
|
|
1110
1234
|
return [
|
|
1111
1235
|
{ 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 {};
|