klyro 1.0.1 → 1.0.3
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/anthropic-adapter.d.ts +13 -5
- package/dist/agent/anthropic-adapter.js +19 -2
- package/dist/agent/capabilities.js +7 -1
- package/dist/agent/orchestrator.d.ts +44 -4
- package/dist/agent/orchestrator.js +102 -8
- package/dist/agent/retry.js +52 -10
- package/dist/agent/runtime.d.ts +34 -0
- package/dist/agent/runtime.js +169 -15
- package/dist/agent/stream-budget.d.ts +36 -0
- package/dist/agent/stream-budget.js +121 -0
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +26 -0
- package/dist/cli/commit.d.ts +31 -0
- package/dist/cli/commit.js +142 -0
- package/dist/cli/config.d.ts +45 -0
- package/dist/cli/config.js +82 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +71 -6
- package/dist/cli/hooks.d.ts +47 -0
- package/dist/cli/hooks.js +181 -0
- package/dist/cli/markdown.js +29 -1
- package/dist/cli/repl.js +41 -1
- package/dist/cli/run.d.ts +6 -0
- package/dist/cli/run.js +76 -3
- package/dist/context/tokenizer.d.ts +18 -0
- package/dist/context/tokenizer.js +50 -1
- package/dist/events/bus.d.ts +5 -0
- package/dist/events/bus.js +10 -0
- package/dist/events/catalog.d.ts +9 -0
- package/dist/events/catalog.js +9 -0
- package/dist/index.js +89 -5
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/registry.d.ts +0 -18
- package/dist/mcp/registry.js +49 -2
- package/dist/policy/engine.d.ts +16 -0
- package/dist/policy/engine.js +74 -1
- package/dist/policy/path-guard.d.ts +24 -0
- package/dist/policy/path-guard.js +46 -0
- package/dist/providers/model-info.d.ts +6 -0
- package/dist/providers/model-info.js +8 -0
- package/dist/tools/fs/apply-patch.js +6 -1
- package/dist/tools/fs/edit-file.js +4 -1
- package/dist/tools/fs/multi-edit.js +4 -1
- package/dist/tools/fs/write-file.js +16 -6
- package/dist/tools/plan/todo-write.js +1 -1
- package/dist/tools/shell/sandbox.d.ts +4 -3
- package/dist/tools/shell/sandbox.js +23 -3
- package/dist/tools/shell/shell-exec.d.ts +28 -0
- package/dist/tools/shell/shell-exec.js +87 -1
- package/dist/trace/writer.d.ts +7 -0
- package/dist/trace/writer.js +7 -0
- package/dist/tui/app.js +20 -1
- package/dist/tui/app.test.js +18 -12
- package/dist/tui/approval.test.js +20 -3
- package/dist/tui/markdown.d.ts +13 -0
- package/dist/tui/markdown.js +169 -2
- package/dist/tui/scroll-flow.test.js +3 -1
- package/dist/verification/classify.js +4 -3
- package/dist/verification/engine.d.ts +8 -0
- package/dist/verification/engine.js +25 -0
- package/dist/verification/registry.js +16 -5
- package/dist/verification/scoped.js +36 -5
- package/package.json +1 -1
|
@@ -72,6 +72,9 @@ interface AnthropicRequest {
|
|
|
72
72
|
name: string;
|
|
73
73
|
description: string;
|
|
74
74
|
input_schema: unknown;
|
|
75
|
+
cache_control?: {
|
|
76
|
+
type: 'ephemeral';
|
|
77
|
+
};
|
|
75
78
|
}>;
|
|
76
79
|
max_tokens: number;
|
|
77
80
|
temperature?: number;
|
|
@@ -94,6 +97,14 @@ export declare function anthropicAdapter(opts: AnthropicAdapterOptions): Provide
|
|
|
94
97
|
* Exported via _internal for testing.
|
|
95
98
|
*/
|
|
96
99
|
export declare function buildAnthropicSystem(system: string | undefined, suffix: string | undefined, promptCache: boolean): AnthropicRequest['system'];
|
|
100
|
+
/**
|
|
101
|
+
* Build the Anthropic `tools` array. When prompt caching is enabled, the
|
|
102
|
+
* last tool carries a `cache_control: {type:'ephemeral'}` breakpoint so
|
|
103
|
+
* the (usually stable) tool definitions join the cacheable prefix —
|
|
104
|
+
* mirroring the system-text breakpoint. OpenAI path untouched.
|
|
105
|
+
* Exported via _internal for testing.
|
|
106
|
+
*/
|
|
107
|
+
export declare function buildAnthropicTools(tools: ToolDefinition[], promptCache: boolean): AnthropicRequest['tools'];
|
|
97
108
|
/**
|
|
98
109
|
* Mutable per-stream assembly state. Blocks are keyed by content_block
|
|
99
110
|
* index; the tool id is carried inside the block entry. There is no global
|
|
@@ -118,15 +129,12 @@ interface AnthropicStreamState {
|
|
|
118
129
|
}
|
|
119
130
|
declare function translateSse(event: string, parsed: AnthropicSseEvent, state: AnthropicStreamState): StreamEvent[];
|
|
120
131
|
declare function toAnthropicMessages(messages: Message[]): AnthropicMessage[];
|
|
121
|
-
declare function toAnthropicTool(t: ToolDefinition):
|
|
122
|
-
name: string;
|
|
123
|
-
description: string;
|
|
124
|
-
input_schema: unknown;
|
|
125
|
-
};
|
|
132
|
+
declare function toAnthropicTool(t: ToolDefinition): NonNullable<AnthropicRequest['tools']>[number];
|
|
126
133
|
export declare const _internal: {
|
|
127
134
|
toAnthropicMessages: typeof toAnthropicMessages;
|
|
128
135
|
toAnthropicTool: typeof toAnthropicTool;
|
|
129
136
|
translateSse: typeof translateSse;
|
|
130
137
|
buildAnthropicSystem: typeof buildAnthropicSystem;
|
|
138
|
+
buildAnthropicTools: typeof buildAnthropicTools;
|
|
131
139
|
};
|
|
132
140
|
export {};
|
|
@@ -75,12 +75,29 @@ export function buildAnthropicSystem(system, suffix, promptCache) {
|
|
|
75
75
|
return undefined;
|
|
76
76
|
return promptCache ? [{ type: 'text', text: system, ...breakpoint }] : system;
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Build the Anthropic `tools` array. When prompt caching is enabled, the
|
|
80
|
+
* last tool carries a `cache_control: {type:'ephemeral'}` breakpoint so
|
|
81
|
+
* the (usually stable) tool definitions join the cacheable prefix —
|
|
82
|
+
* mirroring the system-text breakpoint. OpenAI path untouched.
|
|
83
|
+
* Exported via _internal for testing.
|
|
84
|
+
*/
|
|
85
|
+
export function buildAnthropicTools(tools, promptCache) {
|
|
86
|
+
if (tools.length === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
const out = tools.map(toAnthropicTool);
|
|
89
|
+
if (promptCache) {
|
|
90
|
+
const last = out[out.length - 1];
|
|
91
|
+
last.cache_control = { type: 'ephemeral' };
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
78
95
|
async function* streamAnthropic(req, opts) {
|
|
79
96
|
const body = {
|
|
80
97
|
model: req.model,
|
|
81
98
|
system: buildAnthropicSystem(req.system, req.systemSuffix, opts.promptCache),
|
|
82
99
|
messages: toAnthropicMessages(req.messages),
|
|
83
|
-
tools: req.tools
|
|
100
|
+
tools: buildAnthropicTools(req.tools, opts.promptCache),
|
|
84
101
|
max_tokens: req.maxTokens ?? 4096,
|
|
85
102
|
temperature: req.temperature,
|
|
86
103
|
stream: true,
|
|
@@ -430,4 +447,4 @@ function toAnthropicTool(t) {
|
|
|
430
447
|
};
|
|
431
448
|
}
|
|
432
449
|
// Re-export for testability.
|
|
433
|
-
export const _internal = { toAnthropicMessages, toAnthropicTool, translateSse, buildAnthropicSystem };
|
|
450
|
+
export const _internal = { toAnthropicMessages, toAnthropicTool, translateSse, buildAnthropicSystem, buildAnthropicTools };
|
|
@@ -187,5 +187,11 @@ export const DEFAULT_SPAWN_TOOLS = new Set([
|
|
|
187
187
|
]);
|
|
188
188
|
/** Default deny-list — these are NEVER allowed, even if explicitly requested. */
|
|
189
189
|
export const DEFAULT_DENIED_TOOLS = new Set([
|
|
190
|
-
//
|
|
190
|
+
// Intentionally empty. Deny happens per-pattern (shellDenyRule /
|
|
191
|
+
// DANGEROUS_PATTERNS, .env guards, repair-guard), not per-tool: a
|
|
192
|
+
// tool-granularity deny-all entry (e.g. banning `shell_exec` outright)
|
|
193
|
+
// would break legitimate flows that rely on the allowlist + approval
|
|
194
|
+
// path. Seed candidates considered and rejected: `shell_exec` (needed
|
|
195
|
+
// for tests/builds via approval), `run_verify` (needed by tester/
|
|
196
|
+
// implementer agents), `write_file`/`edit_file` (core agent function).
|
|
191
197
|
]);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* The compact result is a `ChildSummary` — a `ToolResult` the parent model
|
|
13
13
|
* can act on — never the full child transcript.
|
|
14
14
|
*/
|
|
15
|
-
import type { RuntimeDeps } from './runtime.js';
|
|
15
|
+
import type { RuntimeDeps, RunOptions, RuntimeEvent } from './runtime.js';
|
|
16
16
|
import type { ToolResult } from '../tools/types.js';
|
|
17
17
|
import { TaskManager, type TaskRecord, type TaskStatus, type TaskSummary } from './task-manager.js';
|
|
18
18
|
import { WorkerSpawner } from './worker-spawner.js';
|
|
@@ -151,12 +151,31 @@ export interface OrchestratorOpts {
|
|
|
151
151
|
taskManager?: TaskManager;
|
|
152
152
|
workerSpawner?: WorkerSpawner;
|
|
153
153
|
/**
|
|
154
|
-
* True when the parent is the interactive TUI. TUI children
|
|
155
|
-
*
|
|
156
|
-
*
|
|
154
|
+
* True when the parent is the interactive TUI. TUI children run
|
|
155
|
+
* process-isolated *unless* they may need to surface an approval prompt to
|
|
156
|
+
* the operator (see childCanIsolate) — the Ink bridge is tied to the parent
|
|
157
|
+
* terminal, so a prompting child must stay in-process. Headless/CLI children
|
|
158
|
+
* always isolate. Defaults to false.
|
|
157
159
|
*/
|
|
158
160
|
isTui?: boolean;
|
|
159
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Build a `subtask.progress` note for one finished tool call.
|
|
164
|
+
* Pure — unit-tested directly (see agent-tools.test.ts).
|
|
165
|
+
*/
|
|
166
|
+
export declare function progressNote(step: number, tool: string, isError: boolean): string;
|
|
167
|
+
/**
|
|
168
|
+
* Build the `RunOptions.onEvent` handler the orchestrator passes into each
|
|
169
|
+
* child's run options. Emits at most one `subtask.progress` per tool call:
|
|
170
|
+
* a `tool_result` is only mirrored when its `tool_call_end` was observed
|
|
171
|
+
* first, so duplicate/late results can never double-emit. (The note needs
|
|
172
|
+
* the ok/ERR outcome, which only `tool_result` carries — `tool_call_end`
|
|
173
|
+
* alone cannot build it — hence the end-gated result throttle.)
|
|
174
|
+
*/
|
|
175
|
+
export declare function createSubtaskProgressEmitter(opts: {
|
|
176
|
+
taskId: string;
|
|
177
|
+
sessionId: string;
|
|
178
|
+
}): (ev: RuntimeEvent) => void;
|
|
160
179
|
export declare class AgentOrchestrator {
|
|
161
180
|
readonly sessionId: string;
|
|
162
181
|
readonly deps: RuntimeDeps;
|
|
@@ -172,6 +191,27 @@ export declare class AgentOrchestrator {
|
|
|
172
191
|
getAgent(id: string): AgentDefinition | undefined;
|
|
173
192
|
/** Build the bridge the parent's runtime hands to tools. */
|
|
174
193
|
bridgeFor(parent: ParentContextRef): AgentSpawnBridge;
|
|
194
|
+
/**
|
|
195
|
+
* R2 — decide whether a child may run process-isolated.
|
|
196
|
+
*
|
|
197
|
+
* The only thing that forces a child to stay in-process is the possibility
|
|
198
|
+
* of an interactive approval prompt: the Ink bridge lives in the parent
|
|
199
|
+
* terminal, so a subprocess could never ask. A child that cannot prompt is
|
|
200
|
+
* therefore free to isolate.
|
|
201
|
+
*
|
|
202
|
+
* A child cannot prompt when any of these hold:
|
|
203
|
+
* - it is readonly (no write/execute tools → no `ask` on those),
|
|
204
|
+
* - the parent is not a TUI (no bridge to reach in the first place), or
|
|
205
|
+
* - the child's toolset contains no tool the policy can put in `ask`.
|
|
206
|
+
*
|
|
207
|
+
* Isolation is deliberately conservative here: when in doubt we keep the
|
|
208
|
+
* child in-process, because a stranded prompt is a hang and a hang is worse
|
|
209
|
+
* than lost isolation. Public for direct unit testing.
|
|
210
|
+
*/
|
|
211
|
+
childCanIsolate(resolved: {
|
|
212
|
+
allowed: ReadonlySet<string>;
|
|
213
|
+
readonly: boolean;
|
|
214
|
+
}, def: AgentDefinition, childOptions: RunOptions): boolean;
|
|
175
215
|
/** Compute a child's effective capabilities from the parent's own. */
|
|
176
216
|
private resolveChild;
|
|
177
217
|
/**
|
|
@@ -87,6 +87,45 @@ function mapResultStatus(status) {
|
|
|
87
87
|
return 'failed';
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Build a `subtask.progress` note for one finished tool call.
|
|
92
|
+
* Pure — unit-tested directly (see agent-tools.test.ts).
|
|
93
|
+
*/
|
|
94
|
+
export function progressNote(step, tool, isError) {
|
|
95
|
+
return `step ${step}: ${tool} ${isError ? 'ERR' : 'ok'}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build the `RunOptions.onEvent` handler the orchestrator passes into each
|
|
99
|
+
* child's run options. Emits at most one `subtask.progress` per tool call:
|
|
100
|
+
* a `tool_result` is only mirrored when its `tool_call_end` was observed
|
|
101
|
+
* first, so duplicate/late results can never double-emit. (The note needs
|
|
102
|
+
* the ok/ERR outcome, which only `tool_result` carries — `tool_call_end`
|
|
103
|
+
* alone cannot build it — hence the end-gated result throttle.)
|
|
104
|
+
*/
|
|
105
|
+
export function createSubtaskProgressEmitter(opts) {
|
|
106
|
+
let step = 0;
|
|
107
|
+
const ended = new Set();
|
|
108
|
+
return (ev) => {
|
|
109
|
+
if (ev.kind === 'step_start') {
|
|
110
|
+
step = ev.step;
|
|
111
|
+
}
|
|
112
|
+
else if (ev.kind === 'tool_call_end') {
|
|
113
|
+
ended.add(ev.id);
|
|
114
|
+
}
|
|
115
|
+
else if (ev.kind === 'tool_result') {
|
|
116
|
+
if (!ended.has(ev.id))
|
|
117
|
+
return;
|
|
118
|
+
ended.delete(ev.id);
|
|
119
|
+
globalBus.emit({
|
|
120
|
+
type: 'subtask.progress',
|
|
121
|
+
ts: Date.now(),
|
|
122
|
+
sessionId: opts.sessionId,
|
|
123
|
+
taskId: opts.taskId,
|
|
124
|
+
note: progressNote(step, ev.name, ev.isError),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
90
129
|
export class AgentOrchestrator {
|
|
91
130
|
sessionId;
|
|
92
131
|
deps;
|
|
@@ -131,6 +170,45 @@ export class AgentOrchestrator {
|
|
|
131
170
|
applyTask: (taskId) => this.applyTask(taskId),
|
|
132
171
|
};
|
|
133
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* R2 — decide whether a child may run process-isolated.
|
|
175
|
+
*
|
|
176
|
+
* The only thing that forces a child to stay in-process is the possibility
|
|
177
|
+
* of an interactive approval prompt: the Ink bridge lives in the parent
|
|
178
|
+
* terminal, so a subprocess could never ask. A child that cannot prompt is
|
|
179
|
+
* therefore free to isolate.
|
|
180
|
+
*
|
|
181
|
+
* A child cannot prompt when any of these hold:
|
|
182
|
+
* - it is readonly (no write/execute tools → no `ask` on those),
|
|
183
|
+
* - the parent is not a TUI (no bridge to reach in the first place), or
|
|
184
|
+
* - the child's toolset contains no tool the policy can put in `ask`.
|
|
185
|
+
*
|
|
186
|
+
* Isolation is deliberately conservative here: when in doubt we keep the
|
|
187
|
+
* child in-process, because a stranded prompt is a hang and a hang is worse
|
|
188
|
+
* than lost isolation. Public for direct unit testing.
|
|
189
|
+
*/
|
|
190
|
+
childCanIsolate(resolved, def, childOptions) {
|
|
191
|
+
// Headless parents have no approval bridge — isolation is always safe.
|
|
192
|
+
if (!this.isTui)
|
|
193
|
+
return true;
|
|
194
|
+
// Readonly agents never write/execute, so never prompt.
|
|
195
|
+
if (resolved.readonly)
|
|
196
|
+
return true;
|
|
197
|
+
// A child with an inherited bridge (grandchildren possible) must stay
|
|
198
|
+
// in-process: its own children need the bridge chain.
|
|
199
|
+
if (childOptions.agentBridge)
|
|
200
|
+
return false;
|
|
201
|
+
// Any tool in the child's set that the policy can escalate to `ask`
|
|
202
|
+
// pins it in-process. `execute` is the class that most commonly prompts
|
|
203
|
+
// (shell_exec), so its presence is the deciding signal alongside writes.
|
|
204
|
+
const prompting = new Set(['shell_exec', 'write_file', 'edit_file', 'multi_edit', 'apply_patch', 'run_verify']);
|
|
205
|
+
for (const t of resolved.allowed) {
|
|
206
|
+
if (prompting.has(t))
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
void def;
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
134
212
|
/** Compute a child's effective capabilities from the parent's own. */
|
|
135
213
|
resolveChild(def, parent, registryTools) {
|
|
136
214
|
const input = {
|
|
@@ -212,14 +290,17 @@ export class AgentOrchestrator {
|
|
|
212
290
|
const registryTools = new Set(this.deps.registry.list().map((t) => t.name));
|
|
213
291
|
const resolved = this.resolveChild(def, parent, registryTools);
|
|
214
292
|
const childModel = input.model ?? resolved.model ?? parent.model;
|
|
215
|
-
// Worktree isolation: write-capable children
|
|
216
|
-
//
|
|
217
|
-
//
|
|
293
|
+
// Worktree isolation: write-capable children get their own worktree —
|
|
294
|
+
// including when the spawn carries an explicit cwd (the worktree is
|
|
295
|
+
// then rooted at the resolved explicit cwd, which containment above
|
|
296
|
+
// already pinned inside the parent). Readonly agents keep the resolved
|
|
297
|
+
// cwd with no worktree. A write-capable spawn outside a git repo is
|
|
298
|
+
// rejected outright.
|
|
218
299
|
const writeCapable = [...resolved.allowed].some((t) => DEFAULT_WRITE_TOOLS.has(t));
|
|
219
300
|
let childCwd = baseCwd;
|
|
220
301
|
let worktree;
|
|
221
302
|
let repoCwd;
|
|
222
|
-
if (!resolved.readonly && writeCapable
|
|
303
|
+
if (!resolved.readonly && writeCapable) {
|
|
223
304
|
const isRepo = await ensureGitRepo(baseCwd).catch(() => false);
|
|
224
305
|
if (!isRepo) {
|
|
225
306
|
return {
|
|
@@ -285,6 +366,11 @@ export class AgentOrchestrator {
|
|
|
285
366
|
maxTimeMs: def.maxTimeMs ?? input.timeoutMs,
|
|
286
367
|
signal: record.abortController.signal,
|
|
287
368
|
nonInteractive: true,
|
|
369
|
+
// Mid-life progress: mirror each finished tool call as one
|
|
370
|
+
// `subtask.progress` bus event (see createSubtaskProgressEmitter).
|
|
371
|
+
// Process-isolated children don't run this closure — only the
|
|
372
|
+
// in-process path reports mid-life progress.
|
|
373
|
+
onEvent: createSubtaskProgressEmitter({ taskId: record.id, sessionId: this.sessionId }),
|
|
288
374
|
// Grandchildren: only children that canSpawn receive the bridge —
|
|
289
375
|
// otherwise tools see NO_ORCHESTRATOR as before.
|
|
290
376
|
...(resolved.canSpawn ? { agentBridge: this.bridgeFor(childRef) } : {}),
|
|
@@ -301,10 +387,18 @@ export class AgentOrchestrator {
|
|
|
301
387
|
depth: childDepth,
|
|
302
388
|
...(typeof childModel === 'string' ? { model: childModel } : {}),
|
|
303
389
|
});
|
|
304
|
-
// G2 — process isolation for
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
390
|
+
// G2/R2 — process isolation for sub-agents. A child can be spawned as a
|
|
391
|
+
// real OS process whenever it will never need to surface an interactive
|
|
392
|
+
// approval prompt to the operator. Every child that might prompt must stay
|
|
393
|
+
// in-process so the TUI approval bridge (tied to the parent terminal) can
|
|
394
|
+
// reach the operator. Headless/readonly/DenyAll children — which is the
|
|
395
|
+
// ordinary case — isolate into a subprocess. The blanket exclusion of ALL
|
|
396
|
+
// TUI children (V1) is replaced by this capability-aware rule, so a TUI
|
|
397
|
+
// session with readonly or non-interactive children gets real isolation
|
|
398
|
+
// too. Explicitly opt out with KLYRO_WORKER=0.
|
|
399
|
+
// See orchestratorOpts.isTui, capabilities.resolveCapabilities, and
|
|
400
|
+
// child-worker.buildChildDeps (DenyAll approval).
|
|
401
|
+
const useProcessIsolation = process.env.KLYRO_WORKER !== '0' && this.childCanIsolate(resolved, def, childOptions);
|
|
308
402
|
this.workerSpawner.spawn(async (signal) => {
|
|
309
403
|
// Both the in-process path and the forked child resolve to the same
|
|
310
404
|
// minimal outcome shape the settle tail needs.
|
package/dist/agent/retry.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* The default policy matches the L6 plan: 5 attempts, 500ms base, 8s cap.
|
|
19
19
|
*/
|
|
20
|
+
import { acquireStreamSlot, noteRateLimited } from './stream-budget.js';
|
|
20
21
|
export const DEFAULT_RETRY = {
|
|
21
22
|
maxAttempts: 5,
|
|
22
23
|
baseMs: 500,
|
|
@@ -88,6 +89,28 @@ export function computeBackoff(attempt, baseMs, maxMs) {
|
|
|
88
89
|
const jitter = exp * 0.25 * (Math.random() * 2 - 1);
|
|
89
90
|
return Math.max(0, Math.floor(exp + jitter));
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* True when a retryable error event carries a 429 rate-limit signal.
|
|
94
|
+
* Checks the `status` field first, then `code`; accepts numeric values
|
|
95
|
+
* and `'429'` substrings (e.g. `'429'`, `'HTTP_429'`).
|
|
96
|
+
*/
|
|
97
|
+
function isRateLimitedError(ev) {
|
|
98
|
+
if (ev.kind !== 'error')
|
|
99
|
+
return false;
|
|
100
|
+
const rec = ev;
|
|
101
|
+
for (const key of ['status', 'code']) {
|
|
102
|
+
const value = rec[key];
|
|
103
|
+
if (typeof value === 'string' && value.includes('429'))
|
|
104
|
+
return true;
|
|
105
|
+
if (typeof value === 'number' && String(value).includes('429'))
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
function rateLimitDelayMs(ev) {
|
|
111
|
+
const raw = ev['retryAfterMs'];
|
|
112
|
+
return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : undefined;
|
|
113
|
+
}
|
|
91
114
|
export function retryingAdapter(inner, opts = {}) {
|
|
92
115
|
const cfg = { ...DEFAULT_RETRY, ...opts };
|
|
93
116
|
const sleep = opts.sleep ?? defaultSleep;
|
|
@@ -129,20 +152,39 @@ export function retryingAdapter(inner, opts = {}) {
|
|
|
129
152
|
opts.onAttempt?.(attempt);
|
|
130
153
|
if (effectiveSignal?.aborted)
|
|
131
154
|
return;
|
|
155
|
+
// Rate-limit scheduler: hold one stream slot for the duration of
|
|
156
|
+
// this attempt's inner.stream consumption. Abort while queued ends
|
|
157
|
+
// the stream promptly with no inner call.
|
|
158
|
+
let release;
|
|
132
159
|
let sawRetryable = false;
|
|
133
160
|
let lastError = null;
|
|
134
|
-
|
|
135
|
-
|
|
161
|
+
try {
|
|
162
|
+
try {
|
|
163
|
+
release = await acquireStreamSlot(effectiveSignal);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
136
166
|
return;
|
|
137
|
-
if (ev.kind === 'error' && ev.retryable) {
|
|
138
|
-
// Buffer the retryable error; don't yield it yet. We'll either
|
|
139
|
-
// re-issue (and the caller will never see the error) or, on
|
|
140
|
-
// final attempt, yield it as the terminal error.
|
|
141
|
-
sawRetryable = true;
|
|
142
|
-
lastError = ev;
|
|
143
|
-
break; // stop consuming; the stream is dead on retryable errors.
|
|
144
167
|
}
|
|
145
|
-
|
|
168
|
+
for await (const ev of streamWithAbort(inner.stream(attemptReq), effectiveSignal)) {
|
|
169
|
+
if (effectiveSignal?.aborted)
|
|
170
|
+
return;
|
|
171
|
+
if (ev.kind === 'error' && ev.retryable) {
|
|
172
|
+
// Adaptive throttling: a 429 collapses the global stream cap
|
|
173
|
+
// to 1 for retryAfterMs (or 60s) — see stream-budget.ts.
|
|
174
|
+
if (isRateLimitedError(ev))
|
|
175
|
+
noteRateLimited(rateLimitDelayMs(ev));
|
|
176
|
+
// Buffer the retryable error; don't yield it yet. We'll either
|
|
177
|
+
// re-issue (and the caller will never see the error) or, on
|
|
178
|
+
// final attempt, yield it as the terminal error.
|
|
179
|
+
sawRetryable = true;
|
|
180
|
+
lastError = ev;
|
|
181
|
+
break; // stop consuming; the stream is dead on retryable errors.
|
|
182
|
+
}
|
|
183
|
+
yield ev;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
release?.();
|
|
146
188
|
}
|
|
147
189
|
if (!sawRetryable)
|
|
148
190
|
return; // success or non-retryable error — done.
|
package/dist/agent/runtime.d.ts
CHANGED
|
@@ -31,6 +31,13 @@ export type VerifyMode = typeof import('../verification/engine.js') extends {
|
|
|
31
31
|
} ? V : 'strict' | 'advisory' | 'off';
|
|
32
32
|
export interface RuntimeDeps {
|
|
33
33
|
adapter: ProviderAdapter;
|
|
34
|
+
/**
|
|
35
|
+
* Ordered failover adapters (L15). When the active adapter ends a step
|
|
36
|
+
* with a terminal provider error, the runtime swaps to the next entry
|
|
37
|
+
* and re-issues the step (bounded by chain length, never loops).
|
|
38
|
+
* Optional — single-adapter callers behave exactly as before.
|
|
39
|
+
*/
|
|
40
|
+
failoverAdapters?: ProviderAdapter[];
|
|
34
41
|
registry: ToolRegistry;
|
|
35
42
|
policy: PolicyEngine;
|
|
36
43
|
approval: ApprovalPrompt;
|
|
@@ -226,6 +233,22 @@ export type RuntimeEvent = {
|
|
|
226
233
|
} | {
|
|
227
234
|
kind: 'checkpoint_saved';
|
|
228
235
|
sessionId: string;
|
|
236
|
+
} | {
|
|
237
|
+
kind: 'status';
|
|
238
|
+
message: string;
|
|
239
|
+
} | {
|
|
240
|
+
kind: 'budget_warning';
|
|
241
|
+
ratio: number;
|
|
242
|
+
threshold: number;
|
|
243
|
+
} | {
|
|
244
|
+
kind: 'provider_failover';
|
|
245
|
+
from: string;
|
|
246
|
+
to: string;
|
|
247
|
+
reason: string;
|
|
248
|
+
} | {
|
|
249
|
+
kind: 'model_override';
|
|
250
|
+
requested: string;
|
|
251
|
+
effective: string;
|
|
229
252
|
};
|
|
230
253
|
export interface RunResult {
|
|
231
254
|
status: 'complete' | 'max_steps' | 'aborted' | 'no_final' | 'verify_failed' | 'limit' | 'blocked' | 'stuck';
|
|
@@ -264,7 +287,18 @@ export declare function toolDefinitions(registry: ToolRegistry): ToolDefinition[
|
|
|
264
287
|
export declare function estimateCost(model: string, usage: {
|
|
265
288
|
input: number;
|
|
266
289
|
output: number;
|
|
290
|
+
cacheRead?: number;
|
|
291
|
+
cacheWrite?: number;
|
|
267
292
|
}): number;
|
|
293
|
+
/**
|
|
294
|
+
* Parallel fan-out cap: approved concurrencySafe tool calls execute in
|
|
295
|
+
* sequential chunks of at most this size. Commit order stays identical
|
|
296
|
+
* (commits run sequentially after execution), so the transcript reads as
|
|
297
|
+
* if the calls ran in order.
|
|
298
|
+
*/
|
|
299
|
+
export declare const MAX_PARALLEL_TOOLS = 8;
|
|
300
|
+
/** Progressive budget-warning thresholds (fraction of maxCost), fired once each per run. */
|
|
301
|
+
export declare const BUDGET_WARNING_THRESHOLDS: readonly [0.4, 0.7, 0.9];
|
|
268
302
|
/** Run the autonomous loop. */
|
|
269
303
|
export declare function run(opts: RunOptions, deps: RuntimeDeps): Promise<RunResult>;
|
|
270
304
|
export declare function defaultSystemPrompt(ctx: {
|