ai-runtime-engine 2.8.0 → 2.9.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/CHANGELOG.md +48 -0
- package/dist/agents/task.d.ts +37 -0
- package/dist/agents/task.js +22 -0
- package/dist/agents/worker.d.ts +9 -1
- package/dist/agents/worker.js +10 -0
- package/dist/cli/cli.js +1 -1
- package/dist/cli/interactive/repl.js +52 -0
- package/dist/cli/interactive/session.d.ts +8 -1
- package/dist/cli/interactive/session.js +62 -10
- package/dist/executions/agentTasks.d.ts +76 -75
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/orchestration/executor.js +5 -1
- package/dist/runtime/events.d.ts +44 -0
- package/dist/runtime/events.js +4 -0
- package/dist/runtime/runtime.d.ts +30 -0
- package/dist/runtime/runtime.js +172 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,53 @@ All notable changes to `ai-runtime` are documented here. The format follows
|
|
|
5
5
|
Versioning](https://semver.org/). Development history and rationale live in
|
|
6
6
|
[docs/DECISIONS.md](docs/DECISIONS.md) and [docs/PROGRESS.md](docs/PROGRESS.md).
|
|
7
7
|
|
|
8
|
+
## [2.9.0] — 2026-09-05
|
|
9
|
+
|
|
10
|
+
Agent work becomes VISIBLE. Concurrent agent steps render as live lanes in the interactive terminal,
|
|
11
|
+
`/agents` lists and stops them, `/status` surfaces work that is unfinished, and the lifecycle events
|
|
12
|
+
they all read from are finally declared rather than cast. Additive: with `runtime.agents` absent no
|
|
13
|
+
agent event is ever emitted, so the lane region never opens and run output is identical to 2.8.0.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Agent lifecycle event arms** — `agent.task.started`, `agent.task.progress`, `agent.task.completed`
|
|
18
|
+
on `RuntimeEvent`. Metadata only: ids, states and counts. `findings` is a COUNT, because a finding's
|
|
19
|
+
claim is agent-authored text and an event is the one surface a host may forward anywhere.
|
|
20
|
+
- **`agent.task.progress` per inner wave**, plus one at plan end — bounded at waves + 1 per task, never
|
|
21
|
+
per inner step.
|
|
22
|
+
- **A multi-lane display** (`src/cli/interactive/lanes.ts`) — one live row per concurrent agent, pure
|
|
23
|
+
and offline-testable: terminal width, colour and the animation tick are all parameters. Piped output,
|
|
24
|
+
`NO_COLOR` and dumb terminals get append-only transition lines instead of cursor movement.
|
|
25
|
+
- **`/agents`** — list agent tasks with progress, spend and finding counts; **`/agents stop <id>`**
|
|
26
|
+
stops one, and reports every outcome including the ones that change nothing.
|
|
27
|
+
- **`/status`** now names unfinished agent work, and says which task is waiting for an answer.
|
|
28
|
+
- **`AgentTaskView`** + `agentTaskView()` — the public read shape. The record's persistence internals
|
|
29
|
+
(inner plan, inner observations, raw findings, diagnostics) stay internal.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **The event union's evolution contract is now documented**: a new arm is additive at runtime but
|
|
34
|
+
breaks an exhaustive `switch` at COMPILE time. Consumers must carry a default case. Arms added this
|
|
35
|
+
way are announced here.
|
|
36
|
+
- `AgentTaskView` (2.8.0) — the read RESULT of `parseAgentTasks` — is renamed `AgentTasksRead`, freeing
|
|
37
|
+
the name for the per-task view above. 2.8.0 was never published, so no released consumer names it.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **The agent events were a type lie.** Removing the `as never` cast made them *look* typed, but a
|
|
42
|
+
single object literal with a union-typed `type` compiles while carrying properties from every
|
|
43
|
+
constituent — so `agent.task.started` shipped four fields it does not declare. The emit now narrows to
|
|
44
|
+
one arm per branch, and a key-set test covers what the type system still cannot.
|
|
45
|
+
- **`runId` was missing, then wrong.** The events carried none at all, and the fix left
|
|
46
|
+
`orchestrateRunners` with a positional tail whose resume call site stamped the execution id instead.
|
|
47
|
+
It is now a required field of an options object.
|
|
48
|
+
- **`/agents stop` was cosmetic.** `AGENT_RESUMABLE` excludes `cancelled`, so a resume offered no record
|
|
49
|
+
for the stopped step and simply re-ran the agent — a second paid planning call after a human asked it
|
|
50
|
+
to stop. It also wedged an execution permanently when the stopped task held the pending slot, and
|
|
51
|
+
overwrote a concurrent writer's work.
|
|
52
|
+
- `/quit` was reachable but absent from `/help`, alongside `/agents`. A test now asserts every command
|
|
53
|
+
is documented.
|
|
54
|
+
|
|
8
55
|
## [2.8.0] — 2026-09-05
|
|
9
56
|
|
|
10
57
|
Agent work now **survives a crash**. Until this release an agent's progress existed only in memory: the
|
|
@@ -750,6 +797,7 @@ Initial release: the provider-agnostic AI **router** — capability-based routin
|
|
|
750
797
|
scoring, evidence validation, fallback, health tracking, learning-based scoring, multi-model verification,
|
|
751
798
|
budgets, MCP tools, OpenAPI-based adapter generation, and the `AI` class + CLI.
|
|
752
799
|
|
|
800
|
+
[2.9.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.9.0
|
|
753
801
|
[2.8.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.8.0
|
|
754
802
|
[2.7.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.7.0
|
|
755
803
|
[2.6.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.6.0
|
package/dist/agents/task.d.ts
CHANGED
|
@@ -92,6 +92,43 @@ export interface AgentTaskRecord {
|
|
|
92
92
|
export declare const AGENT_TERMINAL: ReadonlySet<AgentTaskState>;
|
|
93
93
|
/** States a persisted task may be resumed from. Exactly the complement of AGENT_TERMINAL. */
|
|
94
94
|
export declare const AGENT_RESUMABLE: ReadonlySet<AgentTaskState>;
|
|
95
|
+
/**
|
|
96
|
+
* The PUBLIC read shape of an agent task (Phase 3.6) — what a host or a status line needs to render
|
|
97
|
+
* progress, and nothing else.
|
|
98
|
+
*
|
|
99
|
+
* It exists so the record's internals are not frozen into the public API. `innerPlan`,
|
|
100
|
+
* `innerObservations`, `innerCheckpoint`, raw `findings` and `diagnostics` are all persistence detail:
|
|
101
|
+
* they change as the phase evolves, they carry agent-authored content, and a host that reached into
|
|
102
|
+
* them would break on the next schema version. This shape is counts, ids and states — safe to freeze.
|
|
103
|
+
*/
|
|
104
|
+
export interface AgentTaskView {
|
|
105
|
+
agentTaskId: string;
|
|
106
|
+
agentId: string;
|
|
107
|
+
stepId: string;
|
|
108
|
+
state: AgentTaskState;
|
|
109
|
+
innerSteps: {
|
|
110
|
+
total: number;
|
|
111
|
+
succeeded: number;
|
|
112
|
+
};
|
|
113
|
+
/** What the step reserved from the run's call budget, and what it has actually spent. */
|
|
114
|
+
callsReserved: number;
|
|
115
|
+
callsUsed: number;
|
|
116
|
+
toolCallsUsed: number;
|
|
117
|
+
/** How many findings were ADMITTED — never the findings themselves. */
|
|
118
|
+
findings: number;
|
|
119
|
+
/** Why a non-terminal task stopped, when it was stopped by something outside itself. */
|
|
120
|
+
interruption?: {
|
|
121
|
+
kind: 'crash' | 'pause' | 'parent-cancel';
|
|
122
|
+
at: number;
|
|
123
|
+
};
|
|
124
|
+
/** The question this task is waiting on, clamped. Present only while it is waiting. */
|
|
125
|
+
question?: string;
|
|
126
|
+
startedAt?: number;
|
|
127
|
+
endedAt?: number;
|
|
128
|
+
updatedAt: number;
|
|
129
|
+
}
|
|
130
|
+
/** Project a record onto the public view. The ONE place that mapping lives. */
|
|
131
|
+
export declare function agentTaskView(record: AgentTaskRecord): AgentTaskView;
|
|
95
132
|
/** One row per agent-task state. Read this table; never re-derive a projection at a call site. */
|
|
96
133
|
export interface ProjectionRow {
|
|
97
134
|
step: PlanStepStatus;
|
package/dist/agents/task.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* normal state and records WHY in `interruption`, so the reason is auditable without growing the
|
|
10
10
|
* lifecycle.
|
|
11
11
|
*/
|
|
12
|
+
import { flattenClamp } from '../util/flatten.js';
|
|
12
13
|
/** Terminal agent-task states: reached once, never left. A commit may not move a task out of one. */
|
|
13
14
|
export const AGENT_TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
14
15
|
/** States a persisted task may be resumed from. Exactly the complement of AGENT_TERMINAL. */
|
|
@@ -20,6 +21,27 @@ export const AGENT_RESUMABLE = new Set([
|
|
|
20
21
|
'waiting_for_clarification',
|
|
21
22
|
'paused',
|
|
22
23
|
]);
|
|
24
|
+
/** Project a record onto the public view. The ONE place that mapping lives. */
|
|
25
|
+
export function agentTaskView(record) {
|
|
26
|
+
return {
|
|
27
|
+
agentTaskId: record.agentTaskId,
|
|
28
|
+
agentId: record.agentId,
|
|
29
|
+
stepId: record.stepId,
|
|
30
|
+
state: record.state,
|
|
31
|
+
innerSteps: record.innerSteps,
|
|
32
|
+
callsReserved: record.callsReserved,
|
|
33
|
+
callsUsed: record.callsUsed,
|
|
34
|
+
toolCallsUsed: record.toolCallsUsed,
|
|
35
|
+
findings: record.findings.length,
|
|
36
|
+
...(record.interruption ? { interruption: { kind: record.interruption.kind, at: record.interruption.at } } : {}),
|
|
37
|
+
// The ONE untrusted string in this shape: model-authored, read back from a file some other
|
|
38
|
+
// version wrote, and rendered in a terminal. `.slice()` truncates but strips nothing.
|
|
39
|
+
...(record.pendingInner ? { question: flattenClamp(record.pendingInner.question, 240) } : {}),
|
|
40
|
+
...(record.startedAt !== undefined ? { startedAt: record.startedAt } : {}),
|
|
41
|
+
...(record.endedAt !== undefined ? { endedAt: record.endedAt } : {}),
|
|
42
|
+
updatedAt: record.updatedAt,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
23
45
|
export const AGENT_TASK_PROJECTION = {
|
|
24
46
|
created: { step: 'pending', exec: 'running', reachableIn34: true },
|
|
25
47
|
queued: { step: 'pending', exec: 'running', reachableIn34: true },
|
package/dist/agents/worker.d.ts
CHANGED
|
@@ -55,8 +55,11 @@ export interface AgentWorkerDeps {
|
|
|
55
55
|
ref?: ArtifactRef;
|
|
56
56
|
unavailable: boolean;
|
|
57
57
|
};
|
|
58
|
+
/** Lifecycle notifications. `agent.task.progress` fires once per inner WAVE — never per inner step:
|
|
59
|
+
* the inner executor runs one step at a time, so per-step would emit one event per step and a chatty
|
|
60
|
+
* agent would push every other event out of the emitter's ring buffer. */
|
|
58
61
|
emit?: (event: {
|
|
59
|
-
type: 'agent.task.started' | 'agent.task.completed';
|
|
62
|
+
type: 'agent.task.started' | 'agent.task.progress' | 'agent.task.completed';
|
|
60
63
|
record: AgentTaskRecord;
|
|
61
64
|
}) => void;
|
|
62
65
|
parentSignal?: AbortSignal;
|
|
@@ -66,6 +69,11 @@ export interface AgentWorkerDeps {
|
|
|
66
69
|
executionId?: string;
|
|
67
70
|
planVersion: number;
|
|
68
71
|
};
|
|
72
|
+
/** Phase 3.6: hands the caller a way to stop THIS task specifically, for as long as it is running.
|
|
73
|
+
* Without it the only lever is the run's own controller, which stops every agent at once. */
|
|
74
|
+
registerAbort?: (agentTaskId: string, abort: () => void) => void;
|
|
75
|
+
/** Phase 3.6: called when the task is no longer running, so the abort handle is not kept forever. */
|
|
76
|
+
releaseAbort?: (agentTaskId: string) => void;
|
|
69
77
|
/** Phase 3.5: called whenever the record MATERIALLY changes, so inner progress reaches disk while the
|
|
70
78
|
* agent is still running. Without a seam inside the inner run, everything between `running` and
|
|
71
79
|
* `finish()` — the inner plan, every completed inner step, every inner model call — is lost to a
|
package/dist/agents/worker.js
CHANGED
|
@@ -120,6 +120,7 @@ export async function runAgentTask(step, envelope, definition, deps) {
|
|
|
120
120
|
const skillResults = [];
|
|
121
121
|
const finish = (state, failure) => {
|
|
122
122
|
deps.parentSignal?.removeEventListener('abort', onParentAbort);
|
|
123
|
+
deps.releaseAbort?.(record.agentTaskId);
|
|
123
124
|
record.state = state;
|
|
124
125
|
record.endedAt = deps.clock.now();
|
|
125
126
|
record.updatedAt = record.endedAt;
|
|
@@ -165,6 +166,7 @@ export async function runAgentTask(step, envelope, definition, deps) {
|
|
|
165
166
|
record.state = 'running';
|
|
166
167
|
record.startedAt = deps.clock.now();
|
|
167
168
|
record.updatedAt = record.startedAt;
|
|
169
|
+
deps.registerAbort?.(record.agentTaskId, () => child.abort());
|
|
168
170
|
deps.emit?.({ type: 'agent.task.started', record });
|
|
169
171
|
deps.onRecord?.(record);
|
|
170
172
|
// (6) THE TOOL SEAM - defense in depth behind narrowEnvelope. Always a structured denial, never a
|
|
@@ -261,7 +263,15 @@ export async function runAgentTask(step, envelope, definition, deps) {
|
|
|
261
263
|
record.callsUsed = priorCalls + calls;
|
|
262
264
|
record.toolCallsUsed = priorToolCalls + toolCalls;
|
|
263
265
|
record.updatedAt = deps.clock.now();
|
|
266
|
+
record.innerSteps = { total: snap.plan.steps.length, succeeded: record.innerCompletedSteps.length };
|
|
264
267
|
deps.onRecord?.(record);
|
|
268
|
+
// One event per WAVE, plus one at the end. `wave-partition` fires BEFORE the wave runs, so on
|
|
269
|
+
// its own it reports the count from before — for a single-wave inner plan (the common case)
|
|
270
|
+
// that means the only progress event says 0/N, which is exactly what `started` already said,
|
|
271
|
+
// and a display would read 0/N until the task simply finished. `plan-end` fires once and is the
|
|
272
|
+
// only snapshot carrying the final count. Still not per-step: waves + 1 events per task.
|
|
273
|
+
if (snap.at === 'wave-partition' || snap.at === 'plan-end')
|
|
274
|
+
deps.emit?.({ type: 'agent.task.progress', record });
|
|
265
275
|
},
|
|
266
276
|
runSkill: async (id, i) => {
|
|
267
277
|
const out = await deps.runSkill(id, i, { permissions: envelope.permissions, signal: child.signal, ai: metered });
|
package/dist/cli/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ import { mcpCommand, mcpAddCommand, mcpRemoveCommand, mcpEnableCommand, mcpTestC
|
|
|
21
21
|
import { startRepl } from './interactive/repl.js';
|
|
22
22
|
import { printError } from './render.js';
|
|
23
23
|
const program = new Command();
|
|
24
|
-
program.name('ai-runtime').description('Universal, provider-agnostic AI Runtime & Orchestration Platform').version('2.
|
|
24
|
+
program.name('ai-runtime').description('Universal, provider-agnostic AI Runtime & Orchestration Platform').version('2.9.0');
|
|
25
25
|
const configOpt = ['-c, --config <path>', 'path to an ai-runtime config file'];
|
|
26
26
|
// Bare `ai-runtime` (no subcommand) opens the interactive terminal. `allowExcessArguments(false)` keeps
|
|
27
27
|
// a mistyped subcommand (e.g. `ai-runtime porviders`) failing fast instead of silently opening the REPL.
|
|
@@ -13,6 +13,7 @@ import { print, printChunk, printError } from '../render.js';
|
|
|
13
13
|
import { summarizeWorkspace } from '../../runtime/workspace/workspace.js';
|
|
14
14
|
import { ReplSession, SLASH_COMMANDS } from './session.js';
|
|
15
15
|
import { makeCompleter } from './complete.js';
|
|
16
|
+
import { LaneSet, laneLines, frameDiff, frameRows } from './lanes.js';
|
|
16
17
|
import { colorEnabled, bold, cyan, dim, gray, SPINNER_FRAMES, statusLine, clearLine } from './ansi.js';
|
|
17
18
|
function banner(rt, colors) {
|
|
18
19
|
const ws = rt.workspaceInfo();
|
|
@@ -96,6 +97,22 @@ export async function startRepl(configPath) {
|
|
|
96
97
|
};
|
|
97
98
|
let streamedThisRun = false;
|
|
98
99
|
let spinner;
|
|
100
|
+
// Phase 3.6: concurrent agent steps render as live lanes. The region opens on the first agent event —
|
|
101
|
+
// long after the spinner has been stopped by the very first event of the run — so the two never own
|
|
102
|
+
// the cursor at the same time. With agents disabled no agent event is ever emitted, so the region
|
|
103
|
+
// never opens and output is byte-identical.
|
|
104
|
+
const lanes = new LaneSet();
|
|
105
|
+
let laneRows = 0;
|
|
106
|
+
let laneTick = 0;
|
|
107
|
+
// A late event — one arriving after the run has already returned — must not open a region on top of
|
|
108
|
+
// the prompt and leave `laneRows` set for the NEXT run's cursor arithmetic to walk into.
|
|
109
|
+
let runInFlight = false;
|
|
110
|
+
const closeLanes = () => {
|
|
111
|
+
if (laneRows > 0)
|
|
112
|
+
printChunk(frameDiff(laneRows, [], colors));
|
|
113
|
+
laneRows = 0;
|
|
114
|
+
lanes.clear();
|
|
115
|
+
};
|
|
99
116
|
// Live progress + token streaming. Events are already redacted; a throwing observer can't break a run.
|
|
100
117
|
rt.on((e) => {
|
|
101
118
|
spinner?.stop(); // any event means work has started producing output — drop the spinner first
|
|
@@ -104,6 +121,30 @@ export async function startRepl(configPath) {
|
|
|
104
121
|
streamedThisRun = true;
|
|
105
122
|
return;
|
|
106
123
|
}
|
|
124
|
+
const lane = lanes.observe(e);
|
|
125
|
+
if (lane) {
|
|
126
|
+
if (!runInFlight)
|
|
127
|
+
return; // see `runInFlight`
|
|
128
|
+
laneTick += 1;
|
|
129
|
+
if (colors) {
|
|
130
|
+
// The region must fit the viewport: `cursorUp` saturates at row 0, so a frame taller than the
|
|
131
|
+
// pane can never walk back to its own top and would redraw itself downward forever.
|
|
132
|
+
const budget = Math.max(1, (process.stdout.rows || 24) - 2);
|
|
133
|
+
const all = lanes.list();
|
|
134
|
+
const shown = all.length > budget ? all.slice(0, budget - 1) : all;
|
|
135
|
+
const lines = laneLines(shown, { cols: process.stdout.columns || 80, colors, tick: laneTick });
|
|
136
|
+
if (all.length > shown.length)
|
|
137
|
+
lines.push(dim(` … ${all.length - shown.length} more agent task(s)`, colors));
|
|
138
|
+
printChunk(frameDiff(laneRows, lines, colors));
|
|
139
|
+
laneRows = frameRows(lines);
|
|
140
|
+
}
|
|
141
|
+
else if (e.type !== 'agent.task.progress') {
|
|
142
|
+
// Piped, NO_COLOR or a dumb terminal: cursor games would be garbage in a log file, so report
|
|
143
|
+
// the transitions append-only instead. Progress ticks are dropped — in a log they are noise.
|
|
144
|
+
print(` · agent ${lane.agentId} @ ${lane.stepId}: ${lane.state}`);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
107
148
|
const line = progressLine(e, colors);
|
|
108
149
|
if (line)
|
|
109
150
|
print(line);
|
|
@@ -125,18 +166,29 @@ export async function startRepl(configPath) {
|
|
|
125
166
|
let result;
|
|
126
167
|
streamedThisRun = false;
|
|
127
168
|
spinner = startSpinner(colors);
|
|
169
|
+
runInFlight = true;
|
|
170
|
+
// Readline keeps echoing keypresses while the handler awaits, and the cursor sits inside the lane
|
|
171
|
+
// region — so a keystroke mid-run writes into the frame and its clear-to-end-of-screen erases the
|
|
172
|
+
// rows below it. Pausing buffers the input instead; it is resumed on both exits below.
|
|
173
|
+
rl.pause();
|
|
128
174
|
try {
|
|
129
175
|
result = await session.handle(line);
|
|
130
176
|
}
|
|
131
177
|
catch (err) {
|
|
178
|
+
runInFlight = false;
|
|
179
|
+
rl.resume();
|
|
132
180
|
spinner.stop();
|
|
181
|
+
closeLanes();
|
|
133
182
|
if (streamedThisRun)
|
|
134
183
|
process.stdout.write('\n');
|
|
135
184
|
printError(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
136
185
|
rl.prompt();
|
|
137
186
|
continue;
|
|
138
187
|
}
|
|
188
|
+
runInFlight = false;
|
|
189
|
+
rl.resume();
|
|
139
190
|
spinner.stop();
|
|
191
|
+
closeLanes(); // hand the cursor back before any result line is printed
|
|
140
192
|
if (streamedThisRun)
|
|
141
193
|
process.stdout.write('\n'); // close the streamed line before printing result lines
|
|
142
194
|
if (result.clear)
|
|
@@ -11,7 +11,10 @@ export interface HandleResult {
|
|
|
11
11
|
clear?: boolean;
|
|
12
12
|
}
|
|
13
13
|
/** Top-level slash commands, for REPL tab-completion (Phase 21b). Kept in sync with the `handle` dispatch. */
|
|
14
|
-
export declare const SLASH_COMMANDS: readonly ["help", "status", "info", "doctor", "cleanup", "mode", "compare", "models", "config", "providers", "tools", "capabilities", "mcp", "skills", "memory", "conversations", "executions", "resume", "resume-execution", "pause", "cancel", "approve", "deny", "learning", "feedback", "permissions", "budget", "stream", "dry-run", "clear", "exit", "quit"];
|
|
14
|
+
export declare const SLASH_COMMANDS: readonly ["help", "status", "info", "doctor", "cleanup", "mode", "compare", "models", "config", "providers", "tools", "capabilities", "mcp", "skills", "memory", "conversations", "executions", "agents", "resume", "resume-execution", "pause", "cancel", "approve", "deny", "learning", "feedback", "permissions", "budget", "stream", "dry-run", "clear", "exit", "quit"];
|
|
15
|
+
/** Exported so a test can prove every reachable command is documented — the three touch points below
|
|
16
|
+
* are synced by hand, and `/agents` shipped tab-completable but absent from this list. */
|
|
17
|
+
export declare const HELP: string[];
|
|
15
18
|
export declare class ReplSession {
|
|
16
19
|
private readonly runtime;
|
|
17
20
|
private mode;
|
|
@@ -36,6 +39,10 @@ export declare class ReplSession {
|
|
|
36
39
|
private learning;
|
|
37
40
|
private permissions;
|
|
38
41
|
private conversationsList;
|
|
42
|
+
/** Agent tasks across this project's executions, newest first. */
|
|
43
|
+
private agentsList;
|
|
44
|
+
/** Stop one agent task. Every outcome is reported — a stop that looks like nothing happened is a bug. */
|
|
45
|
+
private agentStop;
|
|
39
46
|
private executionsList;
|
|
40
47
|
private resumeExecution;
|
|
41
48
|
private resume;
|
|
@@ -13,13 +13,17 @@ import { actionCapabilityRows, renderActionCapabilities, renderCapabilityGaps }
|
|
|
13
13
|
import { renderMcpList, renderMcpDetail } from '../commands/mcp.js';
|
|
14
14
|
import { candidatesFrom, deriveCapabilitiesOffline } from '../../runtime/planning/deriveCapabilities.js';
|
|
15
15
|
import { displaySafe } from '../render.js';
|
|
16
|
+
import { AGENT_TERMINAL } from '../../agents/task.js';
|
|
17
|
+
import { TERMINAL as EXEC_TERMINAL } from '../../executions/execution.js';
|
|
16
18
|
/** Top-level slash commands, for REPL tab-completion (Phase 21b). Kept in sync with the `handle` dispatch. */
|
|
17
19
|
export const SLASH_COMMANDS = [
|
|
18
20
|
'help', 'status', 'info', 'doctor', 'cleanup', 'mode', 'compare', 'models', 'config', 'providers', 'tools', 'capabilities', 'mcp',
|
|
19
|
-
'skills', 'memory', 'conversations', 'executions', 'resume', 'resume-execution', 'pause', 'cancel', 'approve',
|
|
21
|
+
'skills', 'memory', 'conversations', 'executions', 'agents', 'resume', 'resume-execution', 'pause', 'cancel', 'approve',
|
|
20
22
|
'deny', 'learning', 'feedback', 'permissions', 'budget', 'stream', 'dry-run', 'clear', 'exit', 'quit',
|
|
21
23
|
];
|
|
22
|
-
|
|
24
|
+
/** Exported so a test can prove every reachable command is documented — the three touch points below
|
|
25
|
+
* are synced by hand, and `/agents` shipped tab-completable but absent from this list. */
|
|
26
|
+
export const HELP = [
|
|
23
27
|
'Commands:',
|
|
24
28
|
' /help show this help',
|
|
25
29
|
' /status workspace, mode, provider count',
|
|
@@ -49,6 +53,8 @@ const HELP = [
|
|
|
49
53
|
' /conversations list recent conversations',
|
|
50
54
|
' /resume <id> resume a conversation',
|
|
51
55
|
' /executions list persisted executions',
|
|
56
|
+
' /agents list agent tasks (state, progress, spend, findings)',
|
|
57
|
+
' /agents stop <id> stop one agent task',
|
|
52
58
|
' /resume-execution <id> resume an execution',
|
|
53
59
|
' /approve <id> approve an execution waiting for approval, then continue',
|
|
54
60
|
' /deny <id> deny an execution waiting for approval (cancels it)',
|
|
@@ -59,7 +65,7 @@ const HELP = [
|
|
|
59
65
|
' /stream toggle token-by-token streaming of answers',
|
|
60
66
|
' /budget show the call/cost budget (AI_MAX_CALLS / AI_MAX_COST_USD)',
|
|
61
67
|
' /clear clear the screen',
|
|
62
|
-
' /exit
|
|
68
|
+
' /exit, /quit leave the session',
|
|
63
69
|
'',
|
|
64
70
|
'Anything else is sent to the runtime as a request.',
|
|
65
71
|
];
|
|
@@ -209,6 +215,8 @@ export class ReplSession {
|
|
|
209
215
|
return this.resume(args[0]);
|
|
210
216
|
case 'executions':
|
|
211
217
|
return this.executionsList();
|
|
218
|
+
case 'agents':
|
|
219
|
+
return args[0] === 'stop' ? this.agentStop(args[1]) : this.agentsList();
|
|
212
220
|
case 'resume-execution':
|
|
213
221
|
return this.resumeExecution(args[0]);
|
|
214
222
|
case 'approve':
|
|
@@ -327,6 +335,32 @@ export class ReplSession {
|
|
|
327
335
|
return { lines: ['no conversations yet.'] };
|
|
328
336
|
return { lines: ['Recent conversations:', ...list.map((c, i) => ` ${i + 1}. ${c.id} ${c.title} (${c.turns} turns)`)] };
|
|
329
337
|
}
|
|
338
|
+
/** Agent tasks across this project's executions, newest first. */
|
|
339
|
+
agentsList() {
|
|
340
|
+
if (!this.runtime.executionStore.enabled)
|
|
341
|
+
return { lines: ['agent tasks are not persisted (stateless mode).'] };
|
|
342
|
+
const tasks = this.runtime.agentTasks().slice(0, 12);
|
|
343
|
+
if (!tasks.length)
|
|
344
|
+
return { lines: ['no agent tasks yet.', '(agents run when `runtime.agents.enabled` is set and a plan delegates to one)'] };
|
|
345
|
+
// A waiting task's question goes on its own line: it is the one thing the user must read to act.
|
|
346
|
+
const lines = tasks.flatMap((t) => {
|
|
347
|
+
const steps = `${t.innerSteps.succeeded}/${t.innerSteps.total}`;
|
|
348
|
+
const spend = `${t.callsUsed}/${t.callsReserved} call(s), ${t.toolCallsUsed} tool call(s)`;
|
|
349
|
+
const why = t.interruption ? ` (${t.interruption.kind})` : '';
|
|
350
|
+
// `stepId` is planner-authored and `question` is model-authored: both go through displaySafe,
|
|
351
|
+
// like every other untrusted string this file renders.
|
|
352
|
+
const row = ` ${t.agentTaskId} [${t.state}] ${t.agentId} @ ${displaySafe(t.stepId, 40)} ${steps} inner step(s), ${spend}, ${t.findings} finding(s)${why}`;
|
|
353
|
+
return t.question ? [row, ` ? ${displaySafe(t.question, 200)}`] : [row];
|
|
354
|
+
});
|
|
355
|
+
return { lines: ['Agent tasks:', ...lines, '', 'stop one with /agents stop <agent-task-id>'] };
|
|
356
|
+
}
|
|
357
|
+
/** Stop one agent task. Every outcome is reported — a stop that looks like nothing happened is a bug. */
|
|
358
|
+
agentStop(id) {
|
|
359
|
+
if (!id)
|
|
360
|
+
return { lines: ['usage: /agents stop <agent-task-id>'] };
|
|
361
|
+
const r = this.runtime.stopAgentTask(id);
|
|
362
|
+
return { lines: [`${r.ok ? 'stopped' : 'not stopped'} ${id}: ${r.reason}${r.state ? ` (state: ${r.state})` : ''}`] };
|
|
363
|
+
}
|
|
330
364
|
executionsList() {
|
|
331
365
|
if (!this.runtime.executionStore.enabled)
|
|
332
366
|
return { lines: ['executions are disabled (stateless mode).'] };
|
|
@@ -362,13 +396,31 @@ export class ReplSession {
|
|
|
362
396
|
status() {
|
|
363
397
|
const ws = this.runtime.workspaceInfo();
|
|
364
398
|
const providers = this.runtime.ai.providers();
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
399
|
+
const lines = [
|
|
400
|
+
`workspace: ${ws?.name ?? '(none)'}${ws?.git.branch ? ` @ ${ws.git.branch}` : ''}`,
|
|
401
|
+
`mode: ${this.mode}`,
|
|
402
|
+
`providers: ${providers.length} configured (${providers.filter((p) => p.enabled).length} enabled)`,
|
|
403
|
+
];
|
|
404
|
+
// Phase 3.6: agent work is the one thing that can be UNFINISHED and invisible — a task left waiting
|
|
405
|
+
// or interrupted holds its execution up, so /status names it rather than leaving the user to think
|
|
406
|
+
// to run /agents. Absent entirely when nothing has ever delegated, so the flag-off output is
|
|
407
|
+
// unchanged.
|
|
408
|
+
// Only executions that are themselves still going: a non-terminal task on a finished execution is
|
|
409
|
+
// stale bookkeeping, and reporting it as "unfinished work" forever would train the user to ignore
|
|
410
|
+
// this line — which is the one line that has to be trusted when something IS waiting.
|
|
411
|
+
const live = this.runtime.executionStore.enabled ? this.runtime.executions().filter((e) => !EXEC_TERMINAL.has(e.status)) : [];
|
|
412
|
+
const tasks = live.flatMap((e) => this.runtime.agentTasks(e.id));
|
|
413
|
+
if (tasks.length) {
|
|
414
|
+
const unfinished = tasks.filter((t) => !AGENT_TERMINAL.has(t.state));
|
|
415
|
+
const waiting = unfinished.filter((t) => t.state === 'waiting_for_clarification' || t.state === 'waiting_for_input');
|
|
416
|
+
lines.push(`agents: ${tasks.length} task(s), ${unfinished.length} unfinished${waiting.length ? `, ${waiting.length} waiting for an answer` : ''}`);
|
|
417
|
+
for (const t of unfinished.slice(0, 3)) {
|
|
418
|
+
lines.push(` ${t.agentTaskId} [${t.state}] ${displaySafe(t.agentId, 24)} @ ${displaySafe(t.stepId, 16)} ${t.innerSteps.succeeded}/${t.innerSteps.total} step(s)`);
|
|
419
|
+
}
|
|
420
|
+
if (unfinished.length > 3)
|
|
421
|
+
lines.push(` … ${unfinished.length - 3} more (/agents)`);
|
|
422
|
+
}
|
|
423
|
+
return { lines };
|
|
372
424
|
}
|
|
373
425
|
setOrShowMode(next) {
|
|
374
426
|
if (!next)
|
|
@@ -35,11 +35,11 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
35
35
|
detail: z.ZodOptional<z.ZodString>;
|
|
36
36
|
}, "strict", z.ZodTypeAny, {
|
|
37
37
|
at: number;
|
|
38
|
-
kind: "
|
|
38
|
+
kind: "crash" | "pause" | "parent-cancel";
|
|
39
39
|
detail?: string | undefined;
|
|
40
40
|
}, {
|
|
41
41
|
at: number;
|
|
42
|
-
kind: "
|
|
42
|
+
kind: "crash" | "pause" | "parent-cancel";
|
|
43
43
|
detail?: string | undefined;
|
|
44
44
|
}>>;
|
|
45
45
|
provenance: z.ZodObject<{
|
|
@@ -361,42 +361,14 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
361
361
|
}, "strict", z.ZodTypeAny, {
|
|
362
362
|
v: 1;
|
|
363
363
|
state: "running" | "failed" | "completed" | "waiting_for_clarification" | "cancelled" | "paused" | "waiting_for_input" | "created" | "queued";
|
|
364
|
-
createdAt: number;
|
|
365
|
-
updatedAt: number;
|
|
366
364
|
stepId: string;
|
|
367
365
|
agentTaskId: string;
|
|
368
366
|
callsUsed: number;
|
|
369
|
-
envelopeHash: string;
|
|
370
367
|
agentId: string;
|
|
371
|
-
provenance: {
|
|
372
|
-
planVersion: number;
|
|
373
|
-
executionId?: string | undefined;
|
|
374
|
-
};
|
|
375
|
-
agentDefHash: string;
|
|
376
|
-
stepInputHash: string;
|
|
377
|
-
attempt: number;
|
|
378
|
-
innerCompletedSteps: string[];
|
|
379
|
-
innerObservations: z.objectOutputType<{
|
|
380
|
-
stepId: z.ZodString;
|
|
381
|
-
skill: z.ZodOptional<z.ZodString>;
|
|
382
|
-
tool: z.ZodOptional<z.ZodString>;
|
|
383
|
-
agent: z.ZodOptional<z.ZodString>;
|
|
384
|
-
agentTaskId: z.ZodOptional<z.ZodString>;
|
|
385
|
-
ok: z.ZodBoolean;
|
|
386
|
-
output: z.ZodOptional<z.ZodString>;
|
|
387
|
-
error: z.ZodOptional<z.ZodString>;
|
|
388
|
-
code: z.ZodOptional<z.ZodString>;
|
|
389
|
-
data: z.ZodOptional<z.ZodUnknown>;
|
|
390
|
-
artifacts: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
|
|
391
|
-
callsUsed: z.ZodOptional<z.ZodNumber>;
|
|
392
|
-
}, z.ZodTypeAny, "passthrough">[];
|
|
393
|
-
innerObservationsOmitted: number;
|
|
394
368
|
innerSteps: {
|
|
395
369
|
succeeded: number;
|
|
396
370
|
total: number;
|
|
397
371
|
};
|
|
398
|
-
callsReserved: number;
|
|
399
|
-
callsRefunded: number;
|
|
400
372
|
toolCallsUsed: number;
|
|
401
373
|
findings: z.objectOutputType<{
|
|
402
374
|
id: z.ZodString;
|
|
@@ -440,7 +412,42 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
440
412
|
supersededBy: z.ZodOptional<z.ZodString>;
|
|
441
413
|
createdAt: z.ZodNumber;
|
|
442
414
|
}, z.ZodTypeAny, "passthrough">[];
|
|
415
|
+
createdAt: number;
|
|
416
|
+
updatedAt: number;
|
|
417
|
+
envelopeHash: string;
|
|
418
|
+
provenance: {
|
|
419
|
+
planVersion: number;
|
|
420
|
+
executionId?: string | undefined;
|
|
421
|
+
};
|
|
422
|
+
agentDefHash: string;
|
|
423
|
+
stepInputHash: string;
|
|
424
|
+
attempt: number;
|
|
425
|
+
innerCompletedSteps: string[];
|
|
426
|
+
innerObservations: z.objectOutputType<{
|
|
427
|
+
stepId: z.ZodString;
|
|
428
|
+
skill: z.ZodOptional<z.ZodString>;
|
|
429
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
430
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
431
|
+
agentTaskId: z.ZodOptional<z.ZodString>;
|
|
432
|
+
ok: z.ZodBoolean;
|
|
433
|
+
output: z.ZodOptional<z.ZodString>;
|
|
434
|
+
error: z.ZodOptional<z.ZodString>;
|
|
435
|
+
code: z.ZodOptional<z.ZodString>;
|
|
436
|
+
data: z.ZodOptional<z.ZodUnknown>;
|
|
437
|
+
artifacts: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
|
|
438
|
+
callsUsed: z.ZodOptional<z.ZodNumber>;
|
|
439
|
+
}, z.ZodTypeAny, "passthrough">[];
|
|
440
|
+
innerObservationsOmitted: number;
|
|
441
|
+
callsReserved: number;
|
|
442
|
+
callsRefunded: number;
|
|
443
443
|
diagnostics: unknown[];
|
|
444
|
+
interruption?: {
|
|
445
|
+
at: number;
|
|
446
|
+
kind: "crash" | "pause" | "parent-cancel";
|
|
447
|
+
detail?: string | undefined;
|
|
448
|
+
} | undefined;
|
|
449
|
+
startedAt?: number | undefined;
|
|
450
|
+
endedAt?: number | undefined;
|
|
444
451
|
innerPlan?: {
|
|
445
452
|
id: string;
|
|
446
453
|
version: number;
|
|
@@ -458,13 +465,6 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
458
465
|
goal: string;
|
|
459
466
|
reason?: string | undefined;
|
|
460
467
|
} | undefined;
|
|
461
|
-
startedAt?: number | undefined;
|
|
462
|
-
endedAt?: number | undefined;
|
|
463
|
-
interruption?: {
|
|
464
|
-
at: number;
|
|
465
|
-
kind: "pause" | "crash" | "parent-cancel";
|
|
466
|
-
detail?: string | undefined;
|
|
467
|
-
} | undefined;
|
|
468
468
|
innerPlanVersion?: number | undefined;
|
|
469
469
|
innerCheckpoint?: z.objectOutputType<{
|
|
470
470
|
at: z.ZodNumber;
|
|
@@ -488,42 +488,14 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
488
488
|
}, {
|
|
489
489
|
v: 1;
|
|
490
490
|
state: "running" | "failed" | "completed" | "waiting_for_clarification" | "cancelled" | "paused" | "waiting_for_input" | "created" | "queued";
|
|
491
|
-
createdAt: number;
|
|
492
|
-
updatedAt: number;
|
|
493
491
|
stepId: string;
|
|
494
492
|
agentTaskId: string;
|
|
495
493
|
callsUsed: number;
|
|
496
|
-
envelopeHash: string;
|
|
497
494
|
agentId: string;
|
|
498
|
-
provenance: {
|
|
499
|
-
planVersion: number;
|
|
500
|
-
executionId?: string | undefined;
|
|
501
|
-
};
|
|
502
|
-
agentDefHash: string;
|
|
503
|
-
stepInputHash: string;
|
|
504
|
-
attempt: number;
|
|
505
|
-
innerCompletedSteps: string[];
|
|
506
|
-
innerObservations: z.objectInputType<{
|
|
507
|
-
stepId: z.ZodString;
|
|
508
|
-
skill: z.ZodOptional<z.ZodString>;
|
|
509
|
-
tool: z.ZodOptional<z.ZodString>;
|
|
510
|
-
agent: z.ZodOptional<z.ZodString>;
|
|
511
|
-
agentTaskId: z.ZodOptional<z.ZodString>;
|
|
512
|
-
ok: z.ZodBoolean;
|
|
513
|
-
output: z.ZodOptional<z.ZodString>;
|
|
514
|
-
error: z.ZodOptional<z.ZodString>;
|
|
515
|
-
code: z.ZodOptional<z.ZodString>;
|
|
516
|
-
data: z.ZodOptional<z.ZodUnknown>;
|
|
517
|
-
artifacts: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
|
|
518
|
-
callsUsed: z.ZodOptional<z.ZodNumber>;
|
|
519
|
-
}, z.ZodTypeAny, "passthrough">[];
|
|
520
|
-
innerObservationsOmitted: number;
|
|
521
495
|
innerSteps: {
|
|
522
496
|
succeeded: number;
|
|
523
497
|
total: number;
|
|
524
498
|
};
|
|
525
|
-
callsReserved: number;
|
|
526
|
-
callsRefunded: number;
|
|
527
499
|
toolCallsUsed: number;
|
|
528
500
|
findings: z.objectInputType<{
|
|
529
501
|
id: z.ZodString;
|
|
@@ -567,7 +539,42 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
567
539
|
supersededBy: z.ZodOptional<z.ZodString>;
|
|
568
540
|
createdAt: z.ZodNumber;
|
|
569
541
|
}, z.ZodTypeAny, "passthrough">[];
|
|
542
|
+
createdAt: number;
|
|
543
|
+
updatedAt: number;
|
|
544
|
+
envelopeHash: string;
|
|
545
|
+
provenance: {
|
|
546
|
+
planVersion: number;
|
|
547
|
+
executionId?: string | undefined;
|
|
548
|
+
};
|
|
549
|
+
agentDefHash: string;
|
|
550
|
+
stepInputHash: string;
|
|
551
|
+
attempt: number;
|
|
552
|
+
innerCompletedSteps: string[];
|
|
553
|
+
innerObservations: z.objectInputType<{
|
|
554
|
+
stepId: z.ZodString;
|
|
555
|
+
skill: z.ZodOptional<z.ZodString>;
|
|
556
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
557
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
558
|
+
agentTaskId: z.ZodOptional<z.ZodString>;
|
|
559
|
+
ok: z.ZodBoolean;
|
|
560
|
+
output: z.ZodOptional<z.ZodString>;
|
|
561
|
+
error: z.ZodOptional<z.ZodString>;
|
|
562
|
+
code: z.ZodOptional<z.ZodString>;
|
|
563
|
+
data: z.ZodOptional<z.ZodUnknown>;
|
|
564
|
+
artifacts: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
|
|
565
|
+
callsUsed: z.ZodOptional<z.ZodNumber>;
|
|
566
|
+
}, z.ZodTypeAny, "passthrough">[];
|
|
567
|
+
innerObservationsOmitted: number;
|
|
568
|
+
callsReserved: number;
|
|
569
|
+
callsRefunded: number;
|
|
570
570
|
diagnostics: unknown[];
|
|
571
|
+
interruption?: {
|
|
572
|
+
at: number;
|
|
573
|
+
kind: "crash" | "pause" | "parent-cancel";
|
|
574
|
+
detail?: string | undefined;
|
|
575
|
+
} | undefined;
|
|
576
|
+
startedAt?: number | undefined;
|
|
577
|
+
endedAt?: number | undefined;
|
|
571
578
|
innerPlan?: {
|
|
572
579
|
id: string;
|
|
573
580
|
version: number;
|
|
@@ -585,13 +592,6 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
585
592
|
goal: string;
|
|
586
593
|
reason?: string | undefined;
|
|
587
594
|
} | undefined;
|
|
588
|
-
startedAt?: number | undefined;
|
|
589
|
-
endedAt?: number | undefined;
|
|
590
|
-
interruption?: {
|
|
591
|
-
at: number;
|
|
592
|
-
kind: "pause" | "crash" | "parent-cancel";
|
|
593
|
-
detail?: string | undefined;
|
|
594
|
-
} | undefined;
|
|
595
595
|
innerPlanVersion?: number | undefined;
|
|
596
596
|
innerCheckpoint?: z.objectInputType<{
|
|
597
597
|
at: z.ZodNumber;
|
|
@@ -613,15 +613,16 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
613
613
|
message: z.ZodString;
|
|
614
614
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
615
615
|
}>;
|
|
616
|
-
|
|
616
|
+
/** The result of reading agent tasks back: what parsed, and how much did not. */
|
|
617
|
+
export interface AgentTasksRead {
|
|
617
618
|
tasks: AgentTaskRecord[];
|
|
618
|
-
/** Records that failed validation and were left out
|
|
619
|
+
/** Records that failed validation and were left out (still on disk, untouched). */
|
|
619
620
|
dropped: number;
|
|
620
621
|
}
|
|
621
622
|
/**
|
|
622
623
|
* Validate the agent tasks on an execution. Pure: it reads, it never writes. Callers that then persist
|
|
623
624
|
* the execution are choosing to drop the unparseable records — reading alone never does.
|
|
624
625
|
*/
|
|
625
|
-
export declare function parseAgentTasks(exec: Pick<Execution, 'agentTasks'>):
|
|
626
|
+
export declare function parseAgentTasks(exec: Pick<Execution, 'agentTasks'>): AgentTasksRead;
|
|
626
627
|
/** One task by id, validated — the lookup every resume path uses. */
|
|
627
628
|
export declare function findAgentTask(exec: Pick<Execution, 'agentTasks'>, agentTaskId: string): AgentTaskRecord | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -102,8 +102,9 @@ export type { AdmissionRejection, AdmissionResult } from './agents/admit.js';
|
|
|
102
102
|
export type { StepObservationCode } from './orchestration/executor.js';
|
|
103
103
|
export { PLAN_STEP_STATUSES } from './orchestration/plan.js';
|
|
104
104
|
export { parseAgentTasks, findAgentTask } from './executions/agentTasks.js';
|
|
105
|
-
export type {
|
|
106
|
-
export { AGENT_TERMINAL, AGENT_RESUMABLE } from './agents/task.js';
|
|
105
|
+
export type { AgentTasksRead } from './executions/agentTasks.js';
|
|
106
|
+
export { AGENT_TERMINAL, AGENT_RESUMABLE, agentTaskView } from './agents/task.js';
|
|
107
|
+
export type { AgentTaskView } from './agents/task.js';
|
|
107
108
|
export type { ProgressSnapshot } from './orchestration/executor.js';
|
|
108
109
|
export { mcpToolId } from './mcp/toolAdapter.js';
|
|
109
110
|
export { MCP_PROTOCOL_VERSION } from './mcp/protocol.js';
|
package/dist/index.js
CHANGED
|
@@ -80,7 +80,7 @@ export { PLAN_STEP_STATUSES } from './orchestration/plan.js';
|
|
|
80
80
|
// trust records this version cannot parse. The state sets are exported with it because "is this task
|
|
81
81
|
// finished?" must have one answer, not one per caller.
|
|
82
82
|
export { parseAgentTasks, findAgentTask } from './executions/agentTasks.js';
|
|
83
|
-
export { AGENT_TERMINAL, AGENT_RESUMABLE } from './agents/task.js';
|
|
83
|
+
export { AGENT_TERMINAL, AGENT_RESUMABLE, agentTaskView } from './agents/task.js';
|
|
84
84
|
// ── MCP connectivity (Phase 3.2) — external servers as ordinary Runtime tools ──
|
|
85
85
|
// The CONFIG + STATUS surface is public; the client, transports, and manager internals are not, so the
|
|
86
86
|
// wire implementation stays free to change without a breaking release.
|
|
@@ -34,7 +34,11 @@ async function runStepInner(step, deps) {
|
|
|
34
34
|
if (step.agent) {
|
|
35
35
|
if (!deps.runAgent)
|
|
36
36
|
return { stepId: step.id, agent: step.agent, ok: false, code: 'agent-not-enabled', error: 'agent execution is not enabled' };
|
|
37
|
-
return
|
|
37
|
+
// `return await`, NOT a bare return. In an async function a returned promise is ADOPTED, not
|
|
38
|
+
// caught, so a bare return here escapes this try/catch entirely: a throwing agent rejected the
|
|
39
|
+
// whole executePlan instead of failing its own step, and left its wave-mates running unawaited.
|
|
40
|
+
// The skill and tool branches above both await, which is why only agents had this hole.
|
|
41
|
+
return await deps.runAgent(step, { reservation: stepCalls(step, Number.POSITIVE_INFINITY, deps.reserve), ...(deps.signal ? { signal: deps.signal } : {}) });
|
|
38
42
|
}
|
|
39
43
|
return { stepId: step.id, ok: false, error: 'step names neither a skill nor a tool' };
|
|
40
44
|
}
|
package/dist/runtime/events.d.ts
CHANGED
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
* Token-streaming extension point: `response.delta` is reserved here (content-bearing, redacted like
|
|
7
7
|
* everything else). Provider token streaming is NOT implemented in 1.0 — no delta is ever emitted yet —
|
|
8
8
|
* but declaring the arm keeps the host/emitter architecture ready for it without a breaking change.
|
|
9
|
+
*
|
|
10
|
+
* EVOLUTION CONTRACT: this union GROWS. A new arm is additive at runtime — an existing consumer keeps
|
|
11
|
+
* receiving the events it knows — but it breaks an exhaustive `switch` at COMPILE time. Consumers must
|
|
12
|
+
* carry a default case. Arms added this way are announced in the CHANGELOG.
|
|
9
13
|
*/
|
|
10
14
|
import type { Clock } from '../util/clock.js';
|
|
11
15
|
import type { ErrorCategory } from '../types.js';
|
|
12
16
|
import type { ExecutableMode, ModeSource, RuntimeMode, RuntimeStatus } from './types.js';
|
|
17
|
+
import type { AgentTaskState } from '../agents/task.js';
|
|
13
18
|
export type RuntimeEvent = {
|
|
14
19
|
type: 'runtime.started';
|
|
15
20
|
ts: number;
|
|
@@ -54,6 +59,45 @@ export type RuntimeEvent = {
|
|
|
54
59
|
ts: number;
|
|
55
60
|
runId: string;
|
|
56
61
|
text: string;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'agent.task.started';
|
|
64
|
+
ts: number;
|
|
65
|
+
runId: string;
|
|
66
|
+
agentTaskId: string;
|
|
67
|
+
agentId: string;
|
|
68
|
+
stepId: string;
|
|
69
|
+
state: AgentTaskState;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'agent.task.progress';
|
|
72
|
+
ts: number;
|
|
73
|
+
runId: string;
|
|
74
|
+
agentTaskId: string;
|
|
75
|
+
agentId: string;
|
|
76
|
+
stepId: string;
|
|
77
|
+
/** Per inner WAVE, never per inner step: a chatty agent would otherwise flood the ring buffer
|
|
78
|
+
* and push every other event out of it. */
|
|
79
|
+
innerSteps: {
|
|
80
|
+
total: number;
|
|
81
|
+
succeeded: number;
|
|
82
|
+
};
|
|
83
|
+
callsUsed: number;
|
|
84
|
+
toolCallsUsed: number;
|
|
85
|
+
} | {
|
|
86
|
+
type: 'agent.task.completed';
|
|
87
|
+
ts: number;
|
|
88
|
+
runId: string;
|
|
89
|
+
agentTaskId: string;
|
|
90
|
+
agentId: string;
|
|
91
|
+
stepId: string;
|
|
92
|
+
state: AgentTaskState;
|
|
93
|
+
innerSteps: {
|
|
94
|
+
total: number;
|
|
95
|
+
succeeded: number;
|
|
96
|
+
};
|
|
97
|
+
callsUsed: number;
|
|
98
|
+
toolCallsUsed: number;
|
|
99
|
+
/** How many findings were ADMITTED. Never the findings themselves. */
|
|
100
|
+
findings: number;
|
|
57
101
|
};
|
|
58
102
|
/** Distributive Omit so an event can be emitted without pre-stamping `ts`. */
|
|
59
103
|
type WithoutTs<T> = T extends unknown ? Omit<T, 'ts'> : never;
|
package/dist/runtime/events.js
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* Token-streaming extension point: `response.delta` is reserved here (content-bearing, redacted like
|
|
7
7
|
* everything else). Provider token streaming is NOT implemented in 1.0 — no delta is ever emitted yet —
|
|
8
8
|
* but declaring the arm keeps the host/emitter architecture ready for it without a breaking change.
|
|
9
|
+
*
|
|
10
|
+
* EVOLUTION CONTRACT: this union GROWS. A new arm is additive at runtime — an existing consumer keeps
|
|
11
|
+
* receiving the events it knows — but it breaks an exhaustive `switch` at COMPILE time. Consumers must
|
|
12
|
+
* carry a default case. Arms added this way are announced in the CHANGELOG.
|
|
9
13
|
*/
|
|
10
14
|
import { systemClock } from '../util/clock.js';
|
|
11
15
|
import { redact } from '../security/redact.js';
|
|
@@ -25,6 +25,7 @@ import type { SkillSource, LoadedSource } from '../skills/discovery.js';
|
|
|
25
25
|
import type { AgentDefinition } from '../agents/definition.js';
|
|
26
26
|
import { ExecutionStore } from '../executions/store.js';
|
|
27
27
|
import type { Execution } from '../executions/execution.js';
|
|
28
|
+
import type { AgentTaskState, AgentTaskView } from '../agents/task.js';
|
|
28
29
|
import { ArtifactStore } from '../artifacts/artifacts.js';
|
|
29
30
|
import type { CompareInput } from '../comparison/comparator.js';
|
|
30
31
|
import type { ComparisonResult } from '../comparison/comparison.js';
|
|
@@ -87,6 +88,8 @@ export declare class Runtime {
|
|
|
87
88
|
/** Live runs, so a pause/cancel can abort the agent tasks actually in flight. Only ever populated
|
|
88
89
|
* when agents are enabled, so pause/cancel are unchanged with the flag off. */
|
|
89
90
|
private readonly liveRuns;
|
|
91
|
+
/** Phase 3.6: agent tasks running RIGHT NOW in this process, and how to stop each one on its own. */
|
|
92
|
+
private readonly liveAgentTasks;
|
|
90
93
|
/** The config file's `budget:` ceilings, kept only so the 3.3 pre-pass can decline a model call. */
|
|
91
94
|
private readonly _configBudget?;
|
|
92
95
|
private readonly approval?;
|
|
@@ -285,6 +288,12 @@ export declare class Runtime {
|
|
|
285
288
|
private runPersistence;
|
|
286
289
|
/** Run `fn` while heartbeating the execution lease so a long run never lets the lease expire. */
|
|
287
290
|
private withHeartbeat;
|
|
291
|
+
/**
|
|
292
|
+
* Build the OrchestrateInput. The optional half is an OBJECT, not a positional tail: this function
|
|
293
|
+
* grew to ten parameters and a caller that passed five of them silently got no sink, no signal and no
|
|
294
|
+
* provenance — a replan that persisted nothing and could not be cancelled. Named fields cannot be
|
|
295
|
+
* short-counted.
|
|
296
|
+
*/
|
|
288
297
|
private orchestrateInput;
|
|
289
298
|
/** Record an EXECUTED orchestration outcome for learning. plan-only, dry-run, and waiting states are
|
|
290
299
|
* skipped — no skill ran, so there is no success/failure to learn (recording them would teach noise). */
|
|
@@ -409,6 +418,27 @@ export declare class Runtime {
|
|
|
409
418
|
* hand one step's completed inner work to a different step with the same id and a different input.
|
|
410
419
|
*/
|
|
411
420
|
private agentResumeLookup;
|
|
421
|
+
/**
|
|
422
|
+
* Every agent task this Runtime can see, newest execution first (Phase 3.6). Live ones (running in
|
|
423
|
+
* this process) and persisted ones are the same list: a task's record IS its status, so there is no
|
|
424
|
+
* second source to disagree with.
|
|
425
|
+
*/
|
|
426
|
+
agentTasks(executionId?: string): AgentTaskView[];
|
|
427
|
+
/**
|
|
428
|
+
* Stop one agent task. Four cases, and NONE of them is a silent no-op — a `stop` that appears to do
|
|
429
|
+
* nothing is indistinguishable from a bug:
|
|
430
|
+
*
|
|
431
|
+
* - running in this process: abort it, then let the worker record `cancelled` as it unwinds;
|
|
432
|
+
* - persisted and not finished (another process, or a dead one): write `cancelled` with a
|
|
433
|
+
* `parent-cancel` interruption, so the record stops claiming it is queued or running;
|
|
434
|
+
* - already finished: report that, and change nothing — a terminal state is sticky;
|
|
435
|
+
* - unknown id: say so.
|
|
436
|
+
*/
|
|
437
|
+
stopAgentTask(agentTaskId: string): {
|
|
438
|
+
ok: boolean;
|
|
439
|
+
state?: AgentTaskState;
|
|
440
|
+
reason: string;
|
|
441
|
+
};
|
|
412
442
|
/** Abort a run that is in flight, recording WHY so a task can tell a pause from a cancellation. */
|
|
413
443
|
private abortLiveRun;
|
|
414
444
|
/** Mark an execution paused (it can be resumed later). */
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -54,7 +54,7 @@ import { executePlan } from '../orchestration/executor.js';
|
|
|
54
54
|
import { ExecutionStore } from '../executions/store.js';
|
|
55
55
|
import { RESUMABLE, TERMINAL } from '../executions/execution.js';
|
|
56
56
|
import { captureCheckpoint, reconcile } from '../executions/checkpoint.js';
|
|
57
|
-
import { AGENT_TERMINAL, AGENT_RESUMABLE } from '../agents/task.js';
|
|
57
|
+
import { AGENT_TERMINAL, AGENT_RESUMABLE, agentTaskView } from '../agents/task.js';
|
|
58
58
|
import { parseAgentTasks } from '../executions/agentTasks.js';
|
|
59
59
|
import { stepIdentity } from '../agents/worker.js';
|
|
60
60
|
import { hashOf } from '../util/hash.js';
|
|
@@ -159,6 +159,8 @@ export class Runtime {
|
|
|
159
159
|
/** Live runs, so a pause/cancel can abort the agent tasks actually in flight. Only ever populated
|
|
160
160
|
* when agents are enabled, so pause/cancel are unchanged with the flag off. */
|
|
161
161
|
liveRuns = new Map();
|
|
162
|
+
/** Phase 3.6: agent tasks running RIGHT NOW in this process, and how to stop each one on its own. */
|
|
163
|
+
liveAgentTasks = new Map();
|
|
162
164
|
/** The config file's `budget:` ceilings, kept only so the 3.3 pre-pass can decline a model call. */
|
|
163
165
|
_configBudget;
|
|
164
166
|
approval;
|
|
@@ -854,7 +856,14 @@ export class Runtime {
|
|
|
854
856
|
// entire flag-off delta on this path.
|
|
855
857
|
const planning = await this.capabilityPlanning(effectiveGoal, policy, routing);
|
|
856
858
|
try {
|
|
857
|
-
const outcome = await orchestrate(this.orchestrateInput(mode, effectiveGoal, policy,
|
|
859
|
+
const outcome = await orchestrate(this.orchestrateInput(mode, effectiveGoal, policy, {
|
|
860
|
+
...(routing ? { routing } : {}),
|
|
861
|
+
...(partial ? { partial } : {}),
|
|
862
|
+
...(planning?.block ? { requiredCapabilities: planning.block } : {}),
|
|
863
|
+
...(this.agentsEnabled ? { signal: controller.signal } : {}),
|
|
864
|
+
provenance: { planVersion: 1 },
|
|
865
|
+
runId,
|
|
866
|
+
}));
|
|
858
867
|
this.recordOrchestration(mode, goal, outcome);
|
|
859
868
|
return this.mapOutcome(outcome, resolution, runId, undefined, planning);
|
|
860
869
|
}
|
|
@@ -887,7 +896,15 @@ export class Runtime {
|
|
|
887
896
|
this.liveRuns.set(exec.id, { controller });
|
|
888
897
|
// Phase 3.5: only when there is a store to write to — a stateless run keeps the 2.7.0 shape.
|
|
889
898
|
const sink = this._executions.enabled ? this.runPersistence(exec, controller) : undefined;
|
|
890
|
-
const outcome = await this.withHeartbeat(exec.id, () => orchestrate(this.orchestrateInput(mode, effectiveGoal, policy,
|
|
899
|
+
const outcome = await this.withHeartbeat(exec.id, () => orchestrate(this.orchestrateInput(mode, effectiveGoal, policy, {
|
|
900
|
+
...(routing ? { routing } : {}),
|
|
901
|
+
...(partial ? { partial } : {}),
|
|
902
|
+
...(planning?.block ? { requiredCapabilities: planning.block } : {}),
|
|
903
|
+
...(this.agentsEnabled ? { signal: controller.signal } : {}),
|
|
904
|
+
provenance: { executionId: exec.id, planVersion: exec.planVersion },
|
|
905
|
+
...(sink ? { sink } : {}),
|
|
906
|
+
runId,
|
|
907
|
+
})));
|
|
891
908
|
exec.status = this.execStatus(outcome.status);
|
|
892
909
|
exec.observations = outcome.observations.map(persistableObservation);
|
|
893
910
|
if (outcome.plan) {
|
|
@@ -1035,7 +1052,14 @@ export class Runtime {
|
|
|
1035
1052
|
clearInterval(timer);
|
|
1036
1053
|
}
|
|
1037
1054
|
}
|
|
1038
|
-
|
|
1055
|
+
/**
|
|
1056
|
+
* Build the OrchestrateInput. The optional half is an OBJECT, not a positional tail: this function
|
|
1057
|
+
* grew to ten parameters and a caller that passed five of them silently got no sink, no signal and no
|
|
1058
|
+
* provenance — a replan that persisted nothing and could not be cancelled. Named fields cannot be
|
|
1059
|
+
* short-counted.
|
|
1060
|
+
*/
|
|
1061
|
+
orchestrateInput(mode, goal, policy, opts = {}) {
|
|
1062
|
+
const { routing, partial, requiredCapabilities, signal, provenance, sink, agentResume, runId } = opts;
|
|
1039
1063
|
return {
|
|
1040
1064
|
goal,
|
|
1041
1065
|
mode,
|
|
@@ -1055,7 +1079,13 @@ export class Runtime {
|
|
|
1055
1079
|
resolveGaps: (missing) => this.resolveMissingRefs(missing, policy),
|
|
1056
1080
|
// Phase 3.4: ONE runner source. `agents`, `runAgent` and `reserve` ride along only when agents are
|
|
1057
1081
|
// enabled AND a definition exists, so with the flag off this object is KEY-identical to 2.6.0.
|
|
1058
|
-
...this.orchestrateRunners(policy,
|
|
1082
|
+
...this.orchestrateRunners(policy, {
|
|
1083
|
+
runId: runId ?? 'unknown',
|
|
1084
|
+
...(signal ? { signal } : {}),
|
|
1085
|
+
...(provenance ? { provenance } : {}),
|
|
1086
|
+
...(sink?.onRecord ? { onRecord: sink.onRecord } : {}),
|
|
1087
|
+
...(agentResume ? { agentResume } : {}),
|
|
1088
|
+
}),
|
|
1059
1089
|
// Phase 3.5: the commit points. Present ONLY when there is a store to commit to, so a stateless
|
|
1060
1090
|
// Runtime builds an OrchestrateInput key-identical to 2.7.0.
|
|
1061
1091
|
...(sink ? { onPlan: sink.onPlan, onProgress: sink.onProgress } : {}),
|
|
@@ -1392,7 +1422,7 @@ export class Runtime {
|
|
|
1392
1422
|
// path (whether the pause was approval, budget, or partial-progress) so an approved-but-over-budget
|
|
1393
1423
|
// plan pauses for budget rather than silently exceeding it; a raised budget re-applies here.
|
|
1394
1424
|
const resumeLookup = this.agentResumeLookup(exec, opts.clarificationAnswer);
|
|
1395
|
-
const exe = await this.withHeartbeat(exec.id, () => executePlan(exec.plan, { ...this.orchestrateRunners(policy, opts.signal ?? controller.signal, { executionId: exec.id, planVersion: exec.planVersion }, sink?.onRecord, resumeLookup), ...(sink ? { onProgress: sink.onProgress } : {}), maxParallelSteps: policy.maxParallelSteps ?? 2, ...(policy.limits ? { limits: policy.limits } : {}), skip: new Set(exec.completedSteps), ...(policy.maxCalls !== undefined ? { callBudget: policy.maxCalls } : {}), ...(opts.signal ? { signal: opts.signal } : {}) }));
|
|
1425
|
+
const exe = await this.withHeartbeat(exec.id, () => executePlan(exec.plan, { ...this.orchestrateRunners(policy, { runId, signal: opts.signal ?? controller.signal, provenance: { executionId: exec.id, planVersion: exec.planVersion }, ...(sink?.onRecord ? { onRecord: sink.onRecord } : {}), agentResume: resumeLookup }), ...(sink ? { onProgress: sink.onProgress } : {}), maxParallelSteps: policy.maxParallelSteps ?? 2, ...(policy.limits ? { limits: policy.limits } : {}), skip: new Set(exec.completedSteps), ...(policy.maxCalls !== undefined ? { callBudget: policy.maxCalls } : {}), ...(opts.signal ? { signal: opts.signal } : {}) }));
|
|
1396
1426
|
if (exe.stoppedForBudget) {
|
|
1397
1427
|
const done = exe.plan.steps.filter((s) => s.status === 'succeeded').length;
|
|
1398
1428
|
const total = exe.plan.steps.length;
|
|
@@ -1427,7 +1457,14 @@ export class Runtime {
|
|
|
1427
1457
|
// this arm with no signal, no provenance, no sink and no resume lookup — so a replan persisted
|
|
1428
1458
|
// no agent record AT ALL (onRecord is the only writer of `agentTasks`), could not be paused or
|
|
1429
1459
|
// cancelled, and elected `pending` from stale records.
|
|
1430
|
-
outcome = await this.withHeartbeat(exec.id, () => orchestrate(this.orchestrateInput('orchestrate', brief ? `${goal}\n\n${brief}` : goal, policy,
|
|
1460
|
+
outcome = await this.withHeartbeat(exec.id, () => orchestrate(this.orchestrateInput('orchestrate', brief ? `${goal}\n\n${brief}` : goal, policy, {
|
|
1461
|
+
...(this.effectiveRouting() ? { routing: this.effectiveRouting() } : {}),
|
|
1462
|
+
...(budgetPaused ? { partial: true } : {}),
|
|
1463
|
+
...(this.agentsEnabled ? { signal: controller.signal } : {}),
|
|
1464
|
+
provenance: { executionId: exec.id, planVersion: exec.planVersion },
|
|
1465
|
+
...(sink ? { sink } : {}),
|
|
1466
|
+
runId,
|
|
1467
|
+
})));
|
|
1431
1468
|
}
|
|
1432
1469
|
exec.status = this.execStatus(outcome.status);
|
|
1433
1470
|
exec.observations = [...observationsBefore, ...outcome.observations.map(persistableObservation)];
|
|
@@ -1464,7 +1501,8 @@ export class Runtime {
|
|
|
1464
1501
|
* They used to drift: resume built its own pair with no agent runner, so a persisted plan containing
|
|
1465
1502
|
* an agent step would have failed every one of those steps.
|
|
1466
1503
|
*/
|
|
1467
|
-
orchestrateRunners(policy,
|
|
1504
|
+
orchestrateRunners(policy, opts) {
|
|
1505
|
+
const { runId, signal, provenance, onRecord, agentResume } = opts;
|
|
1468
1506
|
const envelopes = policy ? this.agentEnvelopes(policy) : [];
|
|
1469
1507
|
const byId = new Map(envelopes.map((e) => [e.agentId, e]));
|
|
1470
1508
|
return {
|
|
@@ -1503,8 +1541,29 @@ export class Runtime {
|
|
|
1503
1541
|
return { unavailable: true };
|
|
1504
1542
|
}
|
|
1505
1543
|
},
|
|
1506
|
-
|
|
1544
|
+
// METADATA ONLY, and ONE BRANCH PER ARM. Building a single object with a union-typed
|
|
1545
|
+
// `type` compiles while carrying properties from every constituent — TypeScript's
|
|
1546
|
+
// excess-property check against a union admits anything present in SOME arm — so
|
|
1547
|
+
// `agent.task.started` was shipping innerSteps/callsUsed/findings it does not declare.
|
|
1548
|
+
// Narrowing to a literal first makes an extra property a compile error again; the
|
|
1549
|
+
// key-set test covers what the type system still cannot.
|
|
1550
|
+
emit: (e) => {
|
|
1551
|
+
const base = { runId, agentTaskId: e.record.agentTaskId, agentId: e.record.agentId, stepId: e.record.stepId };
|
|
1552
|
+
if (e.type === 'agent.task.started') {
|
|
1553
|
+
this.emitter.emit({ type: 'agent.task.started', ...base, state: e.record.state });
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (e.type === 'agent.task.progress') {
|
|
1557
|
+
this.emitter.emit({ type: 'agent.task.progress', ...base, innerSteps: e.record.innerSteps, callsUsed: e.record.callsUsed, toolCallsUsed: e.record.toolCallsUsed });
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
// `findings` is a COUNT: a claim is agent-authored text and an event is the one
|
|
1561
|
+
// surface a host may forward anywhere, so no content crosses it.
|
|
1562
|
+
this.emitter.emit({ type: 'agent.task.completed', ...base, state: e.record.state, innerSteps: e.record.innerSteps, callsUsed: e.record.callsUsed, toolCallsUsed: e.record.toolCallsUsed, findings: e.record.findings.length });
|
|
1563
|
+
},
|
|
1507
1564
|
...(onRecord ? { onRecord } : {}),
|
|
1565
|
+
registerAbort: (id, abort) => this.liveAgentTasks.set(id, { abort, agentId: envelope.agentId, stepId: step.id, ...(provenance?.executionId ? { executionId: provenance.executionId } : {}) }),
|
|
1566
|
+
releaseAbort: (id) => this.liveAgentTasks.delete(id),
|
|
1508
1567
|
...(ctx.signal ?? signal ? { parentSignal: ctx.signal ?? signal } : {}),
|
|
1509
1568
|
...(provenance ? { provenance } : { provenance: { planVersion: 1 } }),
|
|
1510
1569
|
abortReason: () => {
|
|
@@ -1631,6 +1690,110 @@ export class Runtime {
|
|
|
1631
1690
|
return answer && record.agentTaskId === answeringId ? { record, answer } : { record };
|
|
1632
1691
|
};
|
|
1633
1692
|
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Every agent task this Runtime can see, newest execution first (Phase 3.6). Live ones (running in
|
|
1695
|
+
* this process) and persisted ones are the same list: a task's record IS its status, so there is no
|
|
1696
|
+
* second source to disagree with.
|
|
1697
|
+
*/
|
|
1698
|
+
agentTasks(executionId) {
|
|
1699
|
+
const executions = executionId ? this._executions.list().filter((e) => e.id === executionId) : this._executions.list();
|
|
1700
|
+
return executions.flatMap((e) => parseAgentTasks(e).tasks.map(agentTaskView));
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Stop one agent task. Four cases, and NONE of them is a silent no-op — a `stop` that appears to do
|
|
1704
|
+
* nothing is indistinguishable from a bug:
|
|
1705
|
+
*
|
|
1706
|
+
* - running in this process: abort it, then let the worker record `cancelled` as it unwinds;
|
|
1707
|
+
* - persisted and not finished (another process, or a dead one): write `cancelled` with a
|
|
1708
|
+
* `parent-cancel` interruption, so the record stops claiming it is queued or running;
|
|
1709
|
+
* - already finished: report that, and change nothing — a terminal state is sticky;
|
|
1710
|
+
* - unknown id: say so.
|
|
1711
|
+
*/
|
|
1712
|
+
stopAgentTask(agentTaskId) {
|
|
1713
|
+
const live = this.liveAgentTasks.get(agentTaskId);
|
|
1714
|
+
if (live) {
|
|
1715
|
+
live.abort();
|
|
1716
|
+
// Deliberately no `state`: the abort is cooperative and the worker has not unwound yet, so
|
|
1717
|
+
// claiming `cancelled` here would report a state that has not happened.
|
|
1718
|
+
return { ok: true, reason: 'aborted a task running in this process' };
|
|
1719
|
+
}
|
|
1720
|
+
const owning = this._executions.list().find((e) => parseAgentTasks(e).tasks.some((t) => t.agentTaskId === agentTaskId));
|
|
1721
|
+
if (!owning)
|
|
1722
|
+
return { ok: false, reason: 'no such agent task' };
|
|
1723
|
+
// A run this process is executing owns its own record. Writing it from the side would fight the
|
|
1724
|
+
// run's sink, and TAKING ITS LEASE would be worse: the sink refuses a leaseless commit and treats
|
|
1725
|
+
// the refusal as a stop signal, so releasing here would abort the entire run to stop one task.
|
|
1726
|
+
if (this.liveRuns.has(owning.id)) {
|
|
1727
|
+
return { ok: false, reason: `that task belongs to a run in flight — /cancel ${owning.id} stops the whole run` };
|
|
1728
|
+
}
|
|
1729
|
+
// Claim the execution properly. A live lease held elsewhere means another process is mid-run: its
|
|
1730
|
+
// next commit would rewrite the file from its own memory and silently drop this edit, so refuse
|
|
1731
|
+
// rather than pretend.
|
|
1732
|
+
const acq = this._executions.acquire(owning.id);
|
|
1733
|
+
if (!acq.ok || !acq.execution)
|
|
1734
|
+
return { ok: false, reason: `the execution is busy elsewhere (${acq.reason ?? 'unavailable'})` };
|
|
1735
|
+
const exec = acq.execution;
|
|
1736
|
+
try {
|
|
1737
|
+
// Re-read EVERYTHING from the claimed copy. The listing was a snapshot: the task may have
|
|
1738
|
+
// finished since, and writing the snapshot back would revert its own counters and findings.
|
|
1739
|
+
const fresh = parseAgentTasks(exec).tasks.find((t) => t.agentTaskId === agentTaskId);
|
|
1740
|
+
if (!fresh)
|
|
1741
|
+
return { ok: false, reason: 'the task is no longer on the record' };
|
|
1742
|
+
if (AGENT_TERMINAL.has(fresh.state))
|
|
1743
|
+
return { ok: false, state: fresh.state, reason: `already ${fresh.state}` };
|
|
1744
|
+
const now = this.clock.now();
|
|
1745
|
+
const stopped = { ...fresh, state: 'cancelled', interruption: { kind: 'parent-cancel', at: now }, endedAt: now, updatedAt: now };
|
|
1746
|
+
// A cancelled task is not asking anything any more; leaving the question on it would keep
|
|
1747
|
+
// advertising a prompt nobody can answer.
|
|
1748
|
+
delete stopped.pendingInner;
|
|
1749
|
+
exec.agentTasks = (exec.agentTasks ?? []).map((entry) => (entry.agentTaskId === agentTaskId ? stopped : entry));
|
|
1750
|
+
// WITHOUT THIS THE COMMAND IS COSMETIC. `AGENT_RESUMABLE` excludes `cancelled`, so a resume
|
|
1751
|
+
// offers no record for this step and the executor runs the agent again from scratch — a second
|
|
1752
|
+
// paid planning call and the tools fired again, after a human asked it to stop.
|
|
1753
|
+
//
|
|
1754
|
+
// But mark the step done only when the CURRENT plan still contains that exact step. Step ids
|
|
1755
|
+
// recur across replans, so an id from a retired plan could name a completely different step,
|
|
1756
|
+
// and marking it done would silently skip work that was never even started.
|
|
1757
|
+
const stepStillThere = exec.plan?.steps.some((st) => st.id === stopped.stepId && stepIdentity(st) === stopped.stepInputHash);
|
|
1758
|
+
if (stepStillThere && !exec.completedSteps.includes(stopped.stepId))
|
|
1759
|
+
exec.completedSteps = [...exec.completedSteps, stopped.stepId];
|
|
1760
|
+
// A stopped task cannot answer, so a pending wait belonging to it would strand the execution:
|
|
1761
|
+
// resume early-returns on an unanswered inner clarification, and re-election only considers
|
|
1762
|
+
// tasks that are still waiting. Hand the slot to another waiter, or clear it.
|
|
1763
|
+
if (exec.pending?.kind === 'clarification' && exec.pending.agentTaskId === agentTaskId) {
|
|
1764
|
+
const next = this.electWaitingAgent(exec);
|
|
1765
|
+
if (next?.pendingInner)
|
|
1766
|
+
exec.pending = { kind: 'clarification', question: next.pendingInner.question, agentTaskId: next.agentTaskId };
|
|
1767
|
+
else
|
|
1768
|
+
delete exec.pending;
|
|
1769
|
+
}
|
|
1770
|
+
// `commit`, not `commitProgress`. The funnel's rules exist to stop an IN-FLIGHT RUN overwriting
|
|
1771
|
+
// a decision made while it was finishing — terminal is sticky, a pause is not the runner's to
|
|
1772
|
+
// erase. This is not a run: it holds the lease it just acquired, it changes one task record, and
|
|
1773
|
+
// it never touches `exec.status`, so a completed execution stays completed. Refusing here would
|
|
1774
|
+
// instead make a stale non-terminal task on a finished execution permanently unstoppable.
|
|
1775
|
+
if (!this._executions.commit(exec))
|
|
1776
|
+
return { ok: false, reason: 'another owner holds this execution' };
|
|
1777
|
+
this.emitter.emit({
|
|
1778
|
+
type: 'agent.task.completed',
|
|
1779
|
+
// A stop is out-of-band: it belongs to no run, so this id identifies the OPERATION. It is
|
|
1780
|
+
// deliberately fresh rather than an execution id borrowed to look like a run id.
|
|
1781
|
+
runId: nextRunId(),
|
|
1782
|
+
agentTaskId,
|
|
1783
|
+
agentId: stopped.agentId,
|
|
1784
|
+
stepId: stopped.stepId,
|
|
1785
|
+
state: 'cancelled',
|
|
1786
|
+
innerSteps: stopped.innerSteps,
|
|
1787
|
+
callsUsed: stopped.callsUsed,
|
|
1788
|
+
toolCallsUsed: stopped.toolCallsUsed,
|
|
1789
|
+
findings: stopped.findings.length,
|
|
1790
|
+
});
|
|
1791
|
+
return { ok: true, state: 'cancelled', reason: stepStillThere ? 'marked a persisted task cancelled' : 'marked a persisted task cancelled (its step is no longer in the plan)' };
|
|
1792
|
+
}
|
|
1793
|
+
finally {
|
|
1794
|
+
this._executions.release(owning.id);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1634
1797
|
/** Abort a run that is in flight, recording WHY so a task can tell a pause from a cancellation. */
|
|
1635
1798
|
abortLiveRun(id, reason) {
|
|
1636
1799
|
const live = this.liveRuns.get(id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-runtime-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "AI Runtime \u2014 a provider-agnostic AI runtime and orchestration platform. Point it at whatever AI providers you have; it routes each task to the best available model. Ships the `ai-runtime` CLI and the `Runtime`/`AI` library API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|