@ikarmic/forge-core 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent.js +324 -122
- package/dist/agent/cache-hints.d.ts +2 -0
- package/dist/agent/cache-hints.js +18 -3
- package/dist/agent/enhance-prompt.js +2 -1
- package/dist/agent/history-adapt.d.ts +23 -0
- package/dist/agent/history-adapt.js +114 -0
- package/dist/agent/prompts.d.ts +19 -2
- package/dist/agent/prompts.js +44 -2
- package/dist/agent/retry.d.ts +50 -0
- package/dist/agent/retry.js +192 -0
- package/dist/agent/shell-command.d.ts +23 -0
- package/dist/agent/shell-command.js +93 -0
- package/dist/agent/tool-repair.d.ts +8 -0
- package/dist/agent/tool-repair.js +16 -0
- package/dist/catalog/models-dev-snapshot.d.ts +1 -0
- package/dist/catalog/models-dev-snapshot.js +3 -0
- package/dist/catalog/models-dev.d.ts +45 -0
- package/dist/catalog/models-dev.js +155 -0
- package/dist/checkpoints/store.js +3 -1
- package/dist/context/compaction.d.ts +21 -6
- package/dist/context/compaction.js +94 -28
- package/dist/context/mask.d.ts +9 -9
- package/dist/context/mask.js +30 -1
- package/dist/format/auto-format.d.ts +22 -0
- package/dist/format/auto-format.js +39 -0
- package/dist/git/auto-commit.d.ts +4 -2
- package/dist/git/auto-commit.js +16 -5
- package/dist/github/github-tools.d.ts +1 -0
- package/dist/github/github-tools.js +9 -9
- package/dist/github/pr.d.ts +34 -0
- package/dist/github/pr.js +70 -0
- package/dist/human-text.d.ts +6 -0
- package/dist/human-text.js +37 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.js +14 -6
- package/dist/journey/answers-block.d.ts +2 -4
- package/dist/lsp/diagnostics.d.ts +4 -0
- package/dist/lsp/diagnostics.js +8 -2
- package/dist/lsp/index.d.ts +1 -1
- package/dist/lsp/index.js +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/json-schema-to-zod.js +2 -4
- package/dist/mcp/mcp-client.d.ts +14 -0
- package/dist/mcp/mcp-client.js +102 -18
- package/dist/memory/session-digest.js +2 -1
- package/dist/orchestrator/specialists-registry.js +14 -1
- package/dist/orchestrator/specialists.js +3 -3
- package/dist/pricing.js +12 -0
- package/dist/semantic/embedder.d.ts +11 -0
- package/dist/semantic/embedder.js +16 -0
- package/dist/semantic/semantic-index.d.ts +39 -1
- package/dist/semantic/semantic-index.js +43 -13
- package/dist/semantic/vector-store.d.ts +2 -0
- package/dist/semantic/vector-store.js +13 -1
- package/dist/sessions/index.d.ts +2 -2
- package/dist/sessions/index.js +1 -1
- package/dist/sessions/search.js +2 -2
- package/dist/sessions/session-export.d.ts +1 -1
- package/dist/sessions/session-export.js +41 -33
- package/dist/sessions/session-store.d.ts +20 -1
- package/dist/sessions/session-store.js +7 -0
- package/dist/sessions/watch.js +4 -4
- package/dist/skills/activator.d.ts +6 -0
- package/dist/skills/activator.js +17 -0
- package/dist/skills/skill-tool.d.ts +12 -0
- package/dist/skills/skill-tool.js +28 -0
- package/dist/tools/background-process.d.ts +63 -0
- package/dist/tools/background-process.js +150 -0
- package/dist/tools/command-sandbox.d.ts +24 -0
- package/dist/tools/command-sandbox.js +73 -0
- package/dist/tools/edit-file.d.ts +8 -0
- package/dist/tools/edit-file.js +39 -16
- package/dist/tools/edit-match.d.ts +16 -0
- package/dist/tools/edit-match.js +115 -0
- package/dist/tools/fetch-url.d.ts +15 -1
- package/dist/tools/fetch-url.js +7 -47
- package/dist/tools/get-diagnostics.d.ts +14 -0
- package/dist/tools/get-diagnostics.js +44 -0
- package/dist/tools/glob-filter.d.ts +7 -0
- package/dist/tools/glob-filter.js +12 -0
- package/dist/tools/html-to-markdown.d.ts +13 -0
- package/dist/tools/html-to-markdown.js +507 -0
- package/dist/tools/index.d.ts +5 -1
- package/dist/tools/index.js +11 -1
- package/dist/tools/list-files.d.ts +1 -0
- package/dist/tools/list-files.js +17 -5
- package/dist/tools/list-symbols.js +2 -2
- package/dist/tools/network-policy.d.ts +5 -0
- package/dist/tools/network-policy.js +77 -0
- package/dist/tools/office-docs.d.ts +6 -0
- package/dist/tools/office-docs.js +393 -0
- package/dist/tools/permission-rules.js +55 -5
- package/dist/tools/read-file.d.ts +2 -0
- package/dist/tools/read-file.js +47 -10
- package/dist/tools/read-image.js +2 -2
- package/dist/tools/read-only.d.ts +1 -1
- package/dist/tools/read-only.js +4 -0
- package/dist/tools/repo-clone.d.ts +40 -0
- package/dist/tools/repo-clone.js +218 -0
- package/dist/tools/result-cap.d.ts +8 -0
- package/dist/tools/result-cap.js +44 -0
- package/dist/tools/run-command.d.ts +18 -1
- package/dist/tools/run-command.js +44 -9
- package/dist/tools/search-files.js +12 -53
- package/dist/tools/search-worker.d.ts +23 -0
- package/dist/tools/search-worker.js +86 -0
- package/dist/tools/shared.d.ts +14 -1
- package/dist/tools/shared.js +76 -4
- package/dist/tools/shell.d.ts +27 -0
- package/dist/tools/shell.js +80 -0
- package/dist/tools/sub-agent.d.ts +1 -1
- package/dist/tools/sub-agent.js +4 -1
- package/dist/tools/suggest-plan.d.ts +18 -0
- package/dist/tools/suggest-plan.js +28 -0
- package/dist/types.d.ts +132 -1
- package/dist/wakeup/wakeup-tools.d.ts +22 -0
- package/dist/wakeup/wakeup-tools.js +34 -0
- package/dist/wakeup/wakeup.d.ts +75 -0
- package/dist/wakeup/wakeup.js +151 -0
- package/dist/worktree/health.d.ts +51 -0
- package/dist/worktree/health.js +186 -0
- package/dist/worktree/index.d.ts +4 -0
- package/dist/worktree/index.js +2 -0
- package/dist/worktree/setup.d.ts +51 -0
- package/dist/worktree/setup.js +173 -0
- package/dist/worktree/worktree.d.ts +7 -0
- package/dist/worktree/worktree.js +9 -4
- package/package.json +1 -1
package/dist/types.d.ts
CHANGED
|
@@ -107,12 +107,45 @@ export interface ToolContext {
|
|
|
107
107
|
* the default `child_process` runner.
|
|
108
108
|
*/
|
|
109
109
|
commandRunner?: CommandRunner;
|
|
110
|
+
/** How long `search_files` may match before its worker is stopped. Default 20 s. */
|
|
111
|
+
searchTimeoutMs?: number;
|
|
112
|
+
/** Run `run_command` inside the OS sandbox (`AgentConfig.commandSandbox`); handed to sub-agents. */
|
|
113
|
+
commandSandbox?: CommandSandbox;
|
|
110
114
|
/** The model sub-agents use instead of the parent's (`AgentConfig.subAgentModel`). */
|
|
111
115
|
subAgentModel?: ModelConfig;
|
|
112
116
|
/** Folder `AGENTS.md` paths already shown in this run; `read_file` adds each folder's file once. */
|
|
113
117
|
shownFolderInstructions?: Set<string>;
|
|
114
118
|
/** Longest output `run_command` returns per stream before cutting the middle (`AgentConfig.maxToolOutputChars`). */
|
|
115
119
|
maxOutputChars?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Folders outside the project that READ tools may also reach: a repository this run cloned with `repo_clone`.
|
|
122
|
+
* Added to by that tool only; write tools never see them (shared.ts `resolveSafePath` `read` option). Not handed to a child.
|
|
123
|
+
*/
|
|
124
|
+
readRoots?: string[];
|
|
125
|
+
/**
|
|
126
|
+
* Asks whether a directory outside the project may be read (G16). Given the real directory, after links. Absent: such a
|
|
127
|
+
* path is refused. Not handed to a child, which has no one to ask.
|
|
128
|
+
*/
|
|
129
|
+
askReadOutside?: (dir: string) => boolean | Promise<boolean>;
|
|
130
|
+
/** Directories the person already refused in this run, so they are not asked again. */
|
|
131
|
+
deniedReadDirs?: Set<string>;
|
|
132
|
+
/** Where `repo_clone` keeps repositories (default `~/.forge/cache/repos`). `testGitConfig` is for tests only. */
|
|
133
|
+
repoCache?: {
|
|
134
|
+
root: string;
|
|
135
|
+
testGitConfig?: string[];
|
|
136
|
+
};
|
|
137
|
+
/** A chat can ask the person about planning first (`AgentConfig.suggestPlan`). */
|
|
138
|
+
suggestPlan?: boolean;
|
|
139
|
+
/** The person already chose "just do it" for this request (`AgentConfig.planDeclined`). */
|
|
140
|
+
planDeclined?: boolean;
|
|
141
|
+
/** Which shell the default `run_command` runner uses (`AgentConfig.shell`). */
|
|
142
|
+
shell?: import('./tools/shell.js').ShellPreference;
|
|
143
|
+
/** The chat's wakeup scheduler (`AgentConfig.wakeups`); never handed to a child. */
|
|
144
|
+
wakeups?: import('./wakeup/wakeup.js').WakeupScheduler;
|
|
145
|
+
/** The run's background processes (`bg_start`); the agent stops them when the run ends. Never handed to a child. */
|
|
146
|
+
processes?: import('./tools/background-process.js').BackgroundProcesses;
|
|
147
|
+
/** Language servers for `get_diagnostics` (`AgentConfig.verify.lsp` when it is an object). */
|
|
148
|
+
lspConfig?: import('./lsp/index.js').LspConfig;
|
|
116
149
|
/** Asks the person a question (`ask_user`); absent when no surface can answer. */
|
|
117
150
|
askUser?: (question: UserQuestion, signal?: AbortSignal) => Promise<string>;
|
|
118
151
|
/**
|
|
@@ -176,6 +209,23 @@ export type ModelRouter = (ctx: {
|
|
|
176
209
|
/** Tool names the model called on the previous step (empty on the first). */
|
|
177
210
|
lastToolNames: string[];
|
|
178
211
|
}) => ModelConfig | undefined;
|
|
212
|
+
/** What `run_command`'s OS sandbox allows besides writing to the workspace and the temp folder. */
|
|
213
|
+
export interface CommandSandbox {
|
|
214
|
+
/** Let commands reach the network. */
|
|
215
|
+
network: boolean;
|
|
216
|
+
/** More folders commands may write to (a package manager's cache, say). */
|
|
217
|
+
extraWritable?: string[];
|
|
218
|
+
/**
|
|
219
|
+
* With the network off: hosts Forge's own network tools (fetch_url, repo_clone, the browser, GitHub, web search with `*`) may still
|
|
220
|
+
* reach. Exact names, `*.example.com`, or `*`. Shell commands are all or nothing and ignore it (F13).
|
|
221
|
+
*/
|
|
222
|
+
allowedHosts?: string[];
|
|
223
|
+
/**
|
|
224
|
+
* The surface told the person this system cannot confine commands (Windows has no sandbox tool) and they go on anyway.
|
|
225
|
+
* Without it such a command is refused rather than run unconfined in silence.
|
|
226
|
+
*/
|
|
227
|
+
allowUnconfined?: boolean;
|
|
228
|
+
}
|
|
179
229
|
/**
|
|
180
230
|
* Executes one shell command on behalf of `run_command`. Return `null` to decline
|
|
181
231
|
* (e.g. no terminal available); the default `child_process` runner then handles it.
|
|
@@ -250,6 +300,11 @@ export interface ChildProgressEvent {
|
|
|
250
300
|
/** Any event the child emitted. */
|
|
251
301
|
event: AgentEvent;
|
|
252
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* How a tool call was let through: the person approved it, a permission rule allowed it, it is on the
|
|
305
|
+
* auto-approve list, memory writes are allowed, the command is on the "Always allow" list, or the run is Autonomous.
|
|
306
|
+
*/
|
|
307
|
+
export type ApprovalSource = 'person' | 'rule' | 'list' | 'memory' | 'always' | 'autonomous';
|
|
253
308
|
/** Streamed agent event: surfaced to the UI via the AgentRunOptions.onEvent callback. */
|
|
254
309
|
export type AgentEvent = {
|
|
255
310
|
type: 'text_delta';
|
|
@@ -263,11 +318,14 @@ export type AgentEvent = {
|
|
|
263
318
|
} | {
|
|
264
319
|
type: 'reasoning_done';
|
|
265
320
|
text: string;
|
|
266
|
-
}
|
|
321
|
+
}
|
|
322
|
+
/** `approval` says how the call was let through; absent for bookkeeping tools that never ask (the todo list). */
|
|
323
|
+
| {
|
|
267
324
|
type: 'tool_call';
|
|
268
325
|
name: string;
|
|
269
326
|
id: string;
|
|
270
327
|
args: unknown;
|
|
328
|
+
approval?: ApprovalSource;
|
|
271
329
|
} | {
|
|
272
330
|
type: 'tool_result';
|
|
273
331
|
name: string;
|
|
@@ -343,6 +401,22 @@ export type AgentEvent = {
|
|
|
343
401
|
} | {
|
|
344
402
|
type: 'continuation'; /** 1-based count of continuations so far in this run. */
|
|
345
403
|
count: number;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The step now running failed (a provider error worth retrying, or an empty reply) and is being asked again after
|
|
407
|
+
* `delayMs`. Everything it streamed is discarded: a UI drops the step's partial text.
|
|
408
|
+
*/
|
|
409
|
+
/** The model ended a turn with `open` items of its own todo list still open and was reminded once (RB-44). */
|
|
410
|
+
| {
|
|
411
|
+
type: 'todo_nudge';
|
|
412
|
+
open: number;
|
|
413
|
+
} | {
|
|
414
|
+
type: 'retry';
|
|
415
|
+
kind: 'provider_error' | 'empty_reply' | 'offline';
|
|
416
|
+
attempt: number;
|
|
417
|
+
maxRetries: number;
|
|
418
|
+
delayMs: number;
|
|
419
|
+
reason: string;
|
|
346
420
|
} | {
|
|
347
421
|
type: 'phase';
|
|
348
422
|
/** Architect/editor mode: which phase the following events belong to. */
|
|
@@ -630,6 +704,7 @@ export interface AgentConfig {
|
|
|
630
704
|
maskOldResults?: boolean | {
|
|
631
705
|
keepRecentTurns?: number;
|
|
632
706
|
minChars?: number;
|
|
707
|
+
protectTokens?: number;
|
|
633
708
|
};
|
|
634
709
|
/**
|
|
635
710
|
* Model to use for the summarizer. Defaults to the agent's own model.
|
|
@@ -682,6 +757,10 @@ export interface AgentConfig {
|
|
|
682
757
|
extensions?: string[];
|
|
683
758
|
minScore?: number;
|
|
684
759
|
defaultLimit?: number;
|
|
760
|
+
/** Asked before code is sent to a remote embedder (F12). Absent: no question. */
|
|
761
|
+
consent?: import('./semantic/semantic-index.js').SemanticOptions['consent'];
|
|
762
|
+
/** Progress of the index, for a status item. */
|
|
763
|
+
onStatus?: import('./semantic/semantic-index.js').SemanticOptions['onStatus'];
|
|
685
764
|
};
|
|
686
765
|
/**
|
|
687
766
|
* After the model finishes a run that edited files, run these commands and
|
|
@@ -719,6 +798,31 @@ export interface AgentConfig {
|
|
|
719
798
|
* linter commands) and append any problems to the tool result so the model fixes them.
|
|
720
799
|
*/
|
|
721
800
|
autoLint?: import('./lint/auto-lint.js').AutoLintConfig;
|
|
801
|
+
/**
|
|
802
|
+
* After a file is written, run the person's formatter for its type on it (`prettier --write`), before auto-lint, and tell
|
|
803
|
+
* the model when the file changed. The commands run on every edit without asking, so they come from the person only (RB-12).
|
|
804
|
+
*/
|
|
805
|
+
autoFormat?: import('./format/auto-format.js').AutoFormatConfig;
|
|
806
|
+
/** The surface shows a "plan first?" card after a run that called `suggest_plan`. Without it the tool says to carry on. */
|
|
807
|
+
suggestPlan?: boolean;
|
|
808
|
+
/** The person already answered "just do it": `suggest_plan` says to carry on instead of asking again. */
|
|
809
|
+
planDeclined?: boolean;
|
|
810
|
+
/** Folders outside the project that read tools may reach from the start of the run: what the person allowed earlier in this chat (G16). */
|
|
811
|
+
readRoots?: string[];
|
|
812
|
+
/** Asks the person whether a directory outside the project may be read; absent = such a path is refused. */
|
|
813
|
+
askReadOutside?: (dir: string) => boolean | Promise<boolean>;
|
|
814
|
+
/** Which shell Forge's own `run_command` runner uses on Windows. Default `auto`: pwsh, Windows PowerShell, Git Bash, cmd.exe. */
|
|
815
|
+
shell?: import('./tools/shell.js').ShellPreference;
|
|
816
|
+
/**
|
|
817
|
+
* The shell commands really run in when a surface supplies its own runner (VS Code's terminal), so the prompt can say so
|
|
818
|
+
* (RB-27). Without it a custom runner is described as the platform default.
|
|
819
|
+
*/
|
|
820
|
+
runnerShell?: import('./tools/shell.js').ShellKind;
|
|
821
|
+
/**
|
|
822
|
+
* The chat's scheduler for `schedule_wakeup` / `cancel_wakeup`. Lives longer than a run: the surface creates it once per
|
|
823
|
+
* chat, delivers a due wakeup as an automatic message, and disposes it. Absent = the tools are not offered.
|
|
824
|
+
*/
|
|
825
|
+
wakeups?: import('./wakeup/wakeup.js').WakeupScheduler;
|
|
722
826
|
/**
|
|
723
827
|
* Stop the run (`reason: 'budget_exceeded'`) once the running USD cost reaches
|
|
724
828
|
* this cap. Checked before each model call, so one in-flight turn can overshoot.
|
|
@@ -742,6 +846,23 @@ export interface AgentConfig {
|
|
|
742
846
|
* Each continuation is a normal iteration (counts toward every cap).
|
|
743
847
|
*/
|
|
744
848
|
maxContinuations?: number;
|
|
849
|
+
/**
|
|
850
|
+
* Retrying a step whose model call failed with something a retry can fix (429, 5xx, overload, dropped
|
|
851
|
+
* connection). Default 5 retries, back-off from 2 s (doubling, capped at 30 s) unless the provider sends
|
|
852
|
+
* retry-after. An empty or reasoning-only reply is asked for again twice, separately.
|
|
853
|
+
*/
|
|
854
|
+
providerRetry?: {
|
|
855
|
+
maxRetries?: number;
|
|
856
|
+
baseDelayMs?: number;
|
|
857
|
+
/**
|
|
858
|
+
* With no network at all (DNS or route gone) the run waits instead of spending retries: it probes every
|
|
859
|
+
* `offlinePollMs` (default 3 s) for up to `maxOfflineMs` in total (default 15 min), then reports the error.
|
|
860
|
+
*/
|
|
861
|
+
offlinePollMs?: number;
|
|
862
|
+
maxOfflineMs?: number;
|
|
863
|
+
/** True when the network is reachable; default dials a few public addresses over TCP. */
|
|
864
|
+
networkProbe?: () => Promise<boolean>;
|
|
865
|
+
};
|
|
745
866
|
/**
|
|
746
867
|
* Pick the model for each step (cheap for exploration, strong for planning…).
|
|
747
868
|
* Return `undefined` to keep the current model. Cost is priced per routed model.
|
|
@@ -749,6 +870,11 @@ export interface AgentConfig {
|
|
|
749
870
|
modelRouter?: ModelRouter;
|
|
750
871
|
/** Replace how `run_command` executes. See {@link CommandRunner}. */
|
|
751
872
|
commandRunner?: CommandRunner;
|
|
873
|
+
/**
|
|
874
|
+
* Confine `run_command` with the OS sandbox: bubblewrap on Linux, sandbox-exec on macOS. A command is refused when
|
|
875
|
+
* the tool is missing; Windows has no equivalent, so there commands run as before. Absent = off.
|
|
876
|
+
*/
|
|
877
|
+
commandSandbox?: CommandSandbox;
|
|
752
878
|
/**
|
|
753
879
|
* Answers `ask_user`. When absent, or when `auto` is on, the model is not offered the tool.
|
|
754
880
|
*/
|
|
@@ -887,6 +1013,11 @@ export interface AgentRunResult {
|
|
|
887
1013
|
* across turns (used by the REPL).
|
|
888
1014
|
*/
|
|
889
1015
|
messages: ModelMessage[];
|
|
1016
|
+
/**
|
|
1017
|
+
* This run's own messages: its user message and everything added after it, untouched by compaction (RB-58).
|
|
1018
|
+
* Append them to a display transcript; `messages` is what the next turn should send.
|
|
1019
|
+
*/
|
|
1020
|
+
turnMessages: ModelMessage[];
|
|
890
1021
|
}
|
|
891
1022
|
/** Public alias for the AI SDK language model handle. */
|
|
892
1023
|
export type AnyLanguageModel = LanguageModel;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolDefinition } from '../types.js';
|
|
3
|
+
declare const scheduleParams: z.ZodObject<{
|
|
4
|
+
reason: z.ZodString;
|
|
5
|
+
delay: z.ZodOptional<z.ZodString>;
|
|
6
|
+
when: z.ZodOptional<z.ZodString>;
|
|
7
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export declare const scheduleWakeupTool: ToolDefinition<z.infer<typeof scheduleParams>, {
|
|
10
|
+
id: string;
|
|
11
|
+
dueAt: string;
|
|
12
|
+
notice?: string;
|
|
13
|
+
note: string;
|
|
14
|
+
}>;
|
|
15
|
+
declare const cancelParams: z.ZodObject<{
|
|
16
|
+
id: z.ZodString;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export declare const cancelWakeupTool: ToolDefinition<z.infer<typeof cancelParams>, {
|
|
19
|
+
id: string;
|
|
20
|
+
cancelled: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
function scheduler(ctx) {
|
|
3
|
+
if (!ctx.wakeups)
|
|
4
|
+
throw new Error('Scheduled wakeups are not available here.');
|
|
5
|
+
return ctx.wakeups;
|
|
6
|
+
}
|
|
7
|
+
const scheduleParams = z.object({
|
|
8
|
+
reason: z.string().min(1).describe('Why you want to be woken, in one sentence. You will see it again when the time comes.'),
|
|
9
|
+
delay: z.string().optional().describe('How long to wait: a number of seconds, or 30s, 5m, 2h, 1d. Give this or when.'),
|
|
10
|
+
when: z.string().optional().describe('An exact time as ISO date and time, for example 2026-10-27T15:30:00. Give this or delay.'),
|
|
11
|
+
prompt: z.string().optional().describe('What you will do when woken, in your own words.'),
|
|
12
|
+
});
|
|
13
|
+
export const scheduleWakeupTool = {
|
|
14
|
+
name: 'schedule_wakeup',
|
|
15
|
+
description: 'Ask to be woken once, later, for example to check on a build or a background process. At the time you get an automatic message with your reason. ' +
|
|
16
|
+
'It works only while the editor is open (a closed editor wakes you when it is next opened), one time per call, at least 10 seconds and at most 7 days ahead, and a chat holds at most 10.',
|
|
17
|
+
parameters: scheduleParams,
|
|
18
|
+
execute: async (args, ctx) => {
|
|
19
|
+
const w = await scheduler(ctx).schedule(args);
|
|
20
|
+
return {
|
|
21
|
+
id: w.id,
|
|
22
|
+
dueAt: new Date(w.dueAt).toISOString(),
|
|
23
|
+
...(w.notice ? { notice: w.notice } : {}),
|
|
24
|
+
note: 'Scheduled. Finish what you are doing now; do not wait for it.',
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const cancelParams = z.object({ id: z.string().describe('The id returned by schedule_wakeup.') });
|
|
29
|
+
export const cancelWakeupTool = {
|
|
30
|
+
name: 'cancel_wakeup',
|
|
31
|
+
description: 'Cancel a wakeup you scheduled with schedule_wakeup, before it fires.',
|
|
32
|
+
parameters: cancelParams,
|
|
33
|
+
execute: async (args, ctx) => ({ id: args.id, cancelled: await scheduler(ctx).cancel(args.id) }),
|
|
34
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduled wakeups: the model asks to be woken later ("check the build in 10 minutes") and the chat delivers an
|
|
3
|
+
* automatic message when the time comes. The scheduler belongs to the chat, not to a run, and works only while the
|
|
4
|
+
* editor is open. Caps follow Kilo's wakeup/schema.ts; recurring (cron) wakeups are not offered.
|
|
5
|
+
*/
|
|
6
|
+
/** A delay under this is raised to it; an absolute time is honoured as given. */
|
|
7
|
+
export declare const MIN_DELAY_MS = 10000;
|
|
8
|
+
/** A wakeup never fires further out than seven days. */
|
|
9
|
+
export declare const MAX_HORIZON_MS: number;
|
|
10
|
+
/** One chat holds at most this many pending wakeups. */
|
|
11
|
+
export declare const MAX_PER_SESSION = 10;
|
|
12
|
+
export interface Wakeup {
|
|
13
|
+
id: string;
|
|
14
|
+
/** Why the model wants to be woken; shown to the model again and to the person. */
|
|
15
|
+
reason: string;
|
|
16
|
+
/** What to do when woken, in the model's own words. */
|
|
17
|
+
prompt?: string;
|
|
18
|
+
/** Epoch milliseconds. */
|
|
19
|
+
dueAt: number;
|
|
20
|
+
created: number;
|
|
21
|
+
}
|
|
22
|
+
/** Where pending wakeups are kept so they survive a reload of the editor. */
|
|
23
|
+
export interface WakeupStore {
|
|
24
|
+
load(): Promise<Wakeup[]>;
|
|
25
|
+
save(wakeups: Wakeup[]): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** When a wakeup is due, with the clamp that applied (as a sentence for the model), or an error naming the problem. */
|
|
28
|
+
export declare function resolveWakeupTime(input: {
|
|
29
|
+
delay?: string;
|
|
30
|
+
when?: string;
|
|
31
|
+
}, now?: number): {
|
|
32
|
+
dueAt: number;
|
|
33
|
+
notice?: string;
|
|
34
|
+
};
|
|
35
|
+
export interface WakeupSchedulerOptions {
|
|
36
|
+
/** Deliver a due wakeup. Return false when the chat is busy: it is offered again after `retryMs`. */
|
|
37
|
+
onDue: (wakeup: Wakeup) => Promise<boolean>;
|
|
38
|
+
store?: WakeupStore;
|
|
39
|
+
retryMs?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare class WakeupScheduler {
|
|
42
|
+
private readonly options;
|
|
43
|
+
private readonly pending;
|
|
44
|
+
private readonly timers;
|
|
45
|
+
private nextId;
|
|
46
|
+
private disposed;
|
|
47
|
+
private readonly retryMs;
|
|
48
|
+
constructor(options: WakeupSchedulerOptions);
|
|
49
|
+
list(): Wakeup[];
|
|
50
|
+
schedule(input: {
|
|
51
|
+
delay?: string;
|
|
52
|
+
when?: string;
|
|
53
|
+
reason: string;
|
|
54
|
+
prompt?: string;
|
|
55
|
+
}): Promise<Wakeup & {
|
|
56
|
+
notice?: string;
|
|
57
|
+
}>;
|
|
58
|
+
cancel(id: string): Promise<boolean>;
|
|
59
|
+
cancelAll(): Promise<void>;
|
|
60
|
+
/** Load what a previous window left; anything already due fires on the next tick. */
|
|
61
|
+
restore(): Promise<void>;
|
|
62
|
+
/** Stop every timer without forgetting what is stored (the editor is closing, or the chat was deleted elsewhere). */
|
|
63
|
+
dispose(): void;
|
|
64
|
+
private arm;
|
|
65
|
+
private disarm;
|
|
66
|
+
private fire;
|
|
67
|
+
private save;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The automatic message a due wakeup becomes. It is a user-role message, so it says what it is and that the person's
|
|
71
|
+
* own words win: the first wording of the todo nudge ("carry on now") overrode "wait for my go-ahead" in live runs (RB-44).
|
|
72
|
+
*/
|
|
73
|
+
export declare function wakeupMessage(w: Pick<Wakeup, 'reason' | 'prompt' | 'created' | 'dueAt'> & {
|
|
74
|
+
id?: string;
|
|
75
|
+
}): string;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduled wakeups: the model asks to be woken later ("check the build in 10 minutes") and the chat delivers an
|
|
3
|
+
* automatic message when the time comes. The scheduler belongs to the chat, not to a run, and works only while the
|
|
4
|
+
* editor is open. Caps follow Kilo's wakeup/schema.ts; recurring (cron) wakeups are not offered.
|
|
5
|
+
*/
|
|
6
|
+
/** A delay under this is raised to it; an absolute time is honoured as given. */
|
|
7
|
+
export const MIN_DELAY_MS = 10_000;
|
|
8
|
+
/** A wakeup never fires further out than seven days. */
|
|
9
|
+
export const MAX_HORIZON_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
|
+
/** One chat holds at most this many pending wakeups. */
|
|
11
|
+
export const MAX_PER_SESSION = 10;
|
|
12
|
+
/** How often a due wakeup is offered again while the chat is busy. */
|
|
13
|
+
const DEFAULT_RETRY_MS = 15_000;
|
|
14
|
+
// ISO-8601 date-time; without an offset the wall clock is read in the host's timezone.
|
|
15
|
+
const WHEN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:?\d{2})?$/;
|
|
16
|
+
// `30`, `30s`, `5m`, `2h`, `1d`; a bare number is seconds.
|
|
17
|
+
const DELAY = /^\s*(\d+)\s*([smhd])?\s*$/i;
|
|
18
|
+
const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
19
|
+
/** When a wakeup is due, with the clamp that applied (as a sentence for the model), or an error naming the problem. */
|
|
20
|
+
export function resolveWakeupTime(input, now = Date.now()) {
|
|
21
|
+
const delay = input.delay?.trim() || undefined;
|
|
22
|
+
const when = input.when?.trim() || undefined;
|
|
23
|
+
if ((delay !== undefined) === (when !== undefined))
|
|
24
|
+
throw new Error('Provide exactly one of delay or when.');
|
|
25
|
+
if (when !== undefined) {
|
|
26
|
+
const target = WHEN.test(when) ? Date.parse(when) : NaN;
|
|
27
|
+
if (!Number.isFinite(target))
|
|
28
|
+
throw new Error(`Invalid time: ${when}. Use an ISO date and time such as 2026-10-27T15:30:00.`);
|
|
29
|
+
if (target <= now)
|
|
30
|
+
throw new Error(`Wakeup time is not in the future: ${when}`);
|
|
31
|
+
if (target > now + MAX_HORIZON_MS)
|
|
32
|
+
return { dueAt: now + MAX_HORIZON_MS, notice: `Requested time "${when}" is beyond the 7-day limit and was pulled back to it.` };
|
|
33
|
+
return { dueAt: target };
|
|
34
|
+
}
|
|
35
|
+
const m = DELAY.exec(delay);
|
|
36
|
+
if (!m)
|
|
37
|
+
throw new Error(`Invalid delay: ${delay}. Use a number of seconds, or 30s, 5m, 2h, 1d.`);
|
|
38
|
+
const span = Number(m[1]) * UNIT_MS[(m[2]?.toLowerCase() ?? 's')];
|
|
39
|
+
if (span <= 0)
|
|
40
|
+
throw new Error(`Wakeup delay is not in the future: ${delay}`);
|
|
41
|
+
if (span < MIN_DELAY_MS)
|
|
42
|
+
return { dueAt: now + MIN_DELAY_MS, notice: `Requested delay "${delay}" is under the 10-second minimum and was raised to it.` };
|
|
43
|
+
if (span > MAX_HORIZON_MS)
|
|
44
|
+
return { dueAt: now + MAX_HORIZON_MS, notice: `Requested delay "${delay}" is beyond the 7-day limit and was pulled back to it.` };
|
|
45
|
+
return { dueAt: now + span };
|
|
46
|
+
}
|
|
47
|
+
export class WakeupScheduler {
|
|
48
|
+
options;
|
|
49
|
+
pending = new Map();
|
|
50
|
+
timers = new Map();
|
|
51
|
+
nextId = 1;
|
|
52
|
+
disposed = false;
|
|
53
|
+
retryMs;
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.options = options;
|
|
56
|
+
this.retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
|
|
57
|
+
}
|
|
58
|
+
list() {
|
|
59
|
+
return [...this.pending.values()].sort((a, b) => a.dueAt - b.dueAt);
|
|
60
|
+
}
|
|
61
|
+
async schedule(input) {
|
|
62
|
+
if (this.pending.size >= MAX_PER_SESSION)
|
|
63
|
+
throw new Error(`A chat can hold at most ${MAX_PER_SESSION} pending wakeups. Cancel one first.`);
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const { dueAt, notice } = resolveWakeupTime(input, now);
|
|
66
|
+
const wakeup = { id: `wk${this.nextId++}-${now.toString(36)}`, reason: input.reason, ...(input.prompt ? { prompt: input.prompt } : {}), dueAt, created: now };
|
|
67
|
+
this.pending.set(wakeup.id, wakeup);
|
|
68
|
+
this.arm(wakeup);
|
|
69
|
+
await this.save();
|
|
70
|
+
return notice ? { ...wakeup, notice } : wakeup;
|
|
71
|
+
}
|
|
72
|
+
async cancel(id) {
|
|
73
|
+
if (!this.pending.delete(id))
|
|
74
|
+
return false;
|
|
75
|
+
this.disarm(id);
|
|
76
|
+
await this.save();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
async cancelAll() {
|
|
80
|
+
for (const id of [...this.pending.keys()])
|
|
81
|
+
this.disarm(id);
|
|
82
|
+
this.pending.clear();
|
|
83
|
+
await this.save();
|
|
84
|
+
}
|
|
85
|
+
/** Load what a previous window left; anything already due fires on the next tick. */
|
|
86
|
+
async restore() {
|
|
87
|
+
const saved = (await this.options.store?.load().catch(() => [])) ?? [];
|
|
88
|
+
for (const w of saved) {
|
|
89
|
+
if (this.pending.has(w.id))
|
|
90
|
+
continue;
|
|
91
|
+
this.pending.set(w.id, w);
|
|
92
|
+
this.arm(w);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Stop every timer without forgetting what is stored (the editor is closing, or the chat was deleted elsewhere). */
|
|
96
|
+
dispose() {
|
|
97
|
+
this.disposed = true;
|
|
98
|
+
for (const id of [...this.timers.keys()])
|
|
99
|
+
this.disarm(id);
|
|
100
|
+
}
|
|
101
|
+
arm(w, delayMs = Math.max(0, w.dueAt - Date.now())) {
|
|
102
|
+
this.disarm(w.id);
|
|
103
|
+
if (this.disposed)
|
|
104
|
+
return;
|
|
105
|
+
const timer = setTimeout(() => void this.fire(w), delayMs);
|
|
106
|
+
timer.unref?.();
|
|
107
|
+
this.timers.set(w.id, timer);
|
|
108
|
+
}
|
|
109
|
+
disarm(id) {
|
|
110
|
+
const t = this.timers.get(id);
|
|
111
|
+
if (t)
|
|
112
|
+
clearTimeout(t);
|
|
113
|
+
this.timers.delete(id);
|
|
114
|
+
}
|
|
115
|
+
async fire(w) {
|
|
116
|
+
this.timers.delete(w.id);
|
|
117
|
+
if (!this.pending.has(w.id))
|
|
118
|
+
return;
|
|
119
|
+
let delivered = false;
|
|
120
|
+
try {
|
|
121
|
+
delivered = await this.options.onDue(w);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
delivered = false;
|
|
125
|
+
}
|
|
126
|
+
if (!this.pending.has(w.id))
|
|
127
|
+
return;
|
|
128
|
+
if (!delivered) {
|
|
129
|
+
this.arm(w, this.retryMs);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.pending.delete(w.id);
|
|
133
|
+
await this.save();
|
|
134
|
+
}
|
|
135
|
+
async save() {
|
|
136
|
+
await this.options.store?.save(this.list()).catch(() => undefined);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The automatic message a due wakeup becomes. It is a user-role message, so it says what it is and that the person's
|
|
141
|
+
* own words win: the first wording of the todo nudge ("carry on now") overrode "wait for my go-ahead" in live runs (RB-44).
|
|
142
|
+
*/
|
|
143
|
+
export function wakeupMessage(w) {
|
|
144
|
+
const minutes = Math.max(1, Math.round((w.dueAt - w.created) / 60_000));
|
|
145
|
+
return [
|
|
146
|
+
`This is an automatic message, not a message from the user. Earlier in this chat you asked to be woken after about ${minutes} minute${minutes === 1 ? '' : 's'}.`,
|
|
147
|
+
`Reason: ${w.reason}`,
|
|
148
|
+
...(w.prompt ? [`What you planned to do: ${w.prompt}`] : []),
|
|
149
|
+
'Carry on with that now, unless the user has asked you to stop or wait since then. If they have, leave it and say nothing about this message.',
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ListedWorktree {
|
|
2
|
+
path: string;
|
|
3
|
+
head: string;
|
|
4
|
+
/** Short branch name, absent when detached. */
|
|
5
|
+
branch?: string;
|
|
6
|
+
detached: boolean;
|
|
7
|
+
locked: boolean;
|
|
8
|
+
lockReason?: string;
|
|
9
|
+
/** Git says its folder is gone and it can be pruned. */
|
|
10
|
+
prunable: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** `git worktree list --porcelain` into entries. */
|
|
13
|
+
export declare function parseWorktreeList(text: string): ListedWorktree[];
|
|
14
|
+
export interface WorktreeInspection {
|
|
15
|
+
/** Worktrees git lists under `.forge/worktrees`. */
|
|
16
|
+
registered: ListedWorktree[];
|
|
17
|
+
/** Folders under `.forge/worktrees` that git does not list. */
|
|
18
|
+
strayFolders: string[];
|
|
19
|
+
/** Listed worktrees whose folder is gone. */
|
|
20
|
+
missing: ListedWorktree[];
|
|
21
|
+
}
|
|
22
|
+
export declare function inspectWorktrees(repoRoot: string): Promise<WorktreeInspection>;
|
|
23
|
+
export interface HealthFinding {
|
|
24
|
+
path: string;
|
|
25
|
+
/** What is wrong, in a sentence. */
|
|
26
|
+
problem: string;
|
|
27
|
+
/** What to do about it, in a sentence. */
|
|
28
|
+
advice?: string;
|
|
29
|
+
/** A fix Forge can make itself, after the person confirms: forget a listing, or delete a stray folder. */
|
|
30
|
+
fix?: 'prune' | 'delete-folder';
|
|
31
|
+
}
|
|
32
|
+
/** Read-only checks of every Agent Manager worktree. An empty list means all is well. */
|
|
33
|
+
export declare function doctorWorktrees(repoRoot: string): Promise<HealthFinding[]>;
|
|
34
|
+
export interface WorktreeStats {
|
|
35
|
+
/** Commits on the session branch that the base does not have. */
|
|
36
|
+
ahead: number;
|
|
37
|
+
/** Commits on the base that the session branch does not have. */
|
|
38
|
+
behind: number;
|
|
39
|
+
/** Files changed against the base, committed or not. */
|
|
40
|
+
changed: number;
|
|
41
|
+
/** Lines added against the base in tracked files, committed or not. A new file git has not been told about is not counted. */
|
|
42
|
+
added: number;
|
|
43
|
+
/** Lines removed against the base, counted the same way. */
|
|
44
|
+
removed: number;
|
|
45
|
+
}
|
|
46
|
+
/** Numbers for one session's row. Never throws; a folder or branch that cannot be read gives zeros. */
|
|
47
|
+
export declare function worktreeStats(repoRoot: string, worktreePath: string, branch: string, base: string): Promise<WorktreeStats>;
|
|
48
|
+
/** Forget listings whose folder is gone. Returns how many of the Agent Manager's were forgotten. */
|
|
49
|
+
export declare function pruneMissingWorktrees(repoRoot: string): Promise<number>;
|
|
50
|
+
/** Delete a folder under `.forge/worktrees` that git does not know. Refuses anything else. */
|
|
51
|
+
export declare function removeStrayFolder(repoRoot: string, dir: string): Promise<void>;
|