ai-runtime-engine 2.8.0 → 3.0.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 +93 -0
- package/README.md +30 -0
- package/dist/agents/admit.d.ts +9 -1
- package/dist/agents/admit.js +10 -2
- package/dist/agents/envelope.d.ts +21 -0
- package/dist/agents/envelope.js +39 -5
- package/dist/agents/finding.d.ts +9 -3
- package/dist/agents/finding.js +14 -3
- package/dist/agents/task.d.ts +37 -0
- package/dist/agents/task.js +22 -0
- package/dist/agents/worker.d.ts +12 -1
- package/dist/agents/worker.js +14 -1
- 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/orchestration/orchestrator.d.ts +2 -1
- package/dist/orchestration/planner.d.ts +2 -1
- package/dist/runtime/config.js +1 -1
- package/dist/runtime/events.d.ts +44 -0
- package/dist/runtime/events.js +4 -0
- package/dist/runtime/runtime.d.ts +70 -3
- package/dist/runtime/runtime.js +288 -27
- package/dist/runtime/types.d.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,97 @@ 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
|
+
## [3.0.0] — 2026-09-05
|
|
9
|
+
|
|
10
|
+
**AI Runtime 3.0.** The 3.x arc set out to make the runtime reason about *what it can do*, talk to tools
|
|
11
|
+
it did not ship with, delegate bounded work to agents, and survive being killed in the middle of it.
|
|
12
|
+
That is now true end to end, and `tests/integration/runtime-3-demo.test.ts` runs the whole of it —
|
|
13
|
+
derive, decompose, delegate, crash, resume — offline, inside a call budget, with a leak scan.
|
|
14
|
+
|
|
15
|
+
Upgrading from 2.9.0 requires no config change. One documented default changes; see below.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Auto-decomposition** (`runtime.agents.decompose`, default OFF) — a goal can delegate to a bounded,
|
|
20
|
+
read-shaped agent with no operator-authored definition. Three roles ship in-tree; no model-authored
|
|
21
|
+
string ever becomes an objective, a tool id, or a permission, and `auto_` is a reserved config
|
|
22
|
+
namespace so a derived id can never shadow an authored one.
|
|
23
|
+
- **Conflict supersession is wired.** `resolveConflicts` shipped in 3.4 with no caller, so two agents
|
|
24
|
+
reaching opposite conclusions about the same subject both stayed active and both reached the next
|
|
25
|
+
planning prompt. Resolution now runs when a task finishes, weighs evidence-based confidence, and
|
|
26
|
+
persists — and it is order-independent, so the answer does not depend on which agent happens to
|
|
27
|
+
finish first.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **`runtime.capabilities.catalog` now defaults to ON.** Set `catalog: false` to remove the block.
|
|
32
|
+
Before flipping it, the block was made to match its own documentation: three comments across three
|
|
33
|
+
files described it as fenced when nothing fenced it, and its blocked-skill half had no cap at all
|
|
34
|
+
(~24k characters with 300 blocked skills, silently, in every prompt). Both are fixed.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **A throwing agent took down the whole run.** `executor.ts` returned the agent runner's promise from
|
|
39
|
+
inside a `try` — and an async function *adopts* a returned promise rather than awaiting it, so the
|
|
40
|
+
rejection escaped the executor's own catch. The plan failed instead of the step, and wave-mates were
|
|
41
|
+
left running unawaited. Present since 2.7.0.
|
|
42
|
+
- Derived permissions are a ceiling rather than a default, so a synthesized definition cannot request
|
|
43
|
+
write access even when the parent has it.
|
|
44
|
+
|
|
45
|
+
### Notes for consumers
|
|
46
|
+
|
|
47
|
+
- The `RuntimeEvent` union gained agent arms in 2.9.0 and may gain more. New arms are additive at
|
|
48
|
+
runtime but break an exhaustive `switch` at compile time — carry a default case.
|
|
49
|
+
- `capabilities.planning` is skipped whenever any call or cost budget is set, and a resumed run is
|
|
50
|
+
never re-derived. Both are deliberate and now asserted by tests rather than only documented.
|
|
51
|
+
|
|
52
|
+
## [2.9.0] — 2026-09-05
|
|
53
|
+
|
|
54
|
+
Agent work becomes VISIBLE. Concurrent agent steps render as live lanes in the interactive terminal,
|
|
55
|
+
`/agents` lists and stops them, `/status` surfaces work that is unfinished, and the lifecycle events
|
|
56
|
+
they all read from are finally declared rather than cast. Additive: with `runtime.agents` absent no
|
|
57
|
+
agent event is ever emitted, so the lane region never opens and run output is identical to 2.8.0.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **Agent lifecycle event arms** — `agent.task.started`, `agent.task.progress`, `agent.task.completed`
|
|
62
|
+
on `RuntimeEvent`. Metadata only: ids, states and counts. `findings` is a COUNT, because a finding's
|
|
63
|
+
claim is agent-authored text and an event is the one surface a host may forward anywhere.
|
|
64
|
+
- **`agent.task.progress` per inner wave**, plus one at plan end — bounded at waves + 1 per task, never
|
|
65
|
+
per inner step.
|
|
66
|
+
- **A multi-lane display** (`src/cli/interactive/lanes.ts`) — one live row per concurrent agent, pure
|
|
67
|
+
and offline-testable: terminal width, colour and the animation tick are all parameters. Piped output,
|
|
68
|
+
`NO_COLOR` and dumb terminals get append-only transition lines instead of cursor movement.
|
|
69
|
+
- **`/agents`** — list agent tasks with progress, spend and finding counts; **`/agents stop <id>`**
|
|
70
|
+
stops one, and reports every outcome including the ones that change nothing.
|
|
71
|
+
- **`/status`** now names unfinished agent work, and says which task is waiting for an answer.
|
|
72
|
+
- **`AgentTaskView`** + `agentTaskView()` — the public read shape. The record's persistence internals
|
|
73
|
+
(inner plan, inner observations, raw findings, diagnostics) stay internal.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **The event union's evolution contract is now documented**: a new arm is additive at runtime but
|
|
78
|
+
breaks an exhaustive `switch` at COMPILE time. Consumers must carry a default case. Arms added this
|
|
79
|
+
way are announced here.
|
|
80
|
+
- `AgentTaskView` (2.8.0) — the read RESULT of `parseAgentTasks` — is renamed `AgentTasksRead`, freeing
|
|
81
|
+
the name for the per-task view above. 2.8.0 was never published, so no released consumer names it.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **The agent events were a type lie.** Removing the `as never` cast made them *look* typed, but a
|
|
86
|
+
single object literal with a union-typed `type` compiles while carrying properties from every
|
|
87
|
+
constituent — so `agent.task.started` shipped four fields it does not declare. The emit now narrows to
|
|
88
|
+
one arm per branch, and a key-set test covers what the type system still cannot.
|
|
89
|
+
- **`runId` was missing, then wrong.** The events carried none at all, and the fix left
|
|
90
|
+
`orchestrateRunners` with a positional tail whose resume call site stamped the execution id instead.
|
|
91
|
+
It is now a required field of an options object.
|
|
92
|
+
- **`/agents stop` was cosmetic.** `AGENT_RESUMABLE` excludes `cancelled`, so a resume offered no record
|
|
93
|
+
for the stopped step and simply re-ran the agent — a second paid planning call after a human asked it
|
|
94
|
+
to stop. It also wedged an execution permanently when the stopped task held the pending slot, and
|
|
95
|
+
overwrote a concurrent writer's work.
|
|
96
|
+
- `/quit` was reachable but absent from `/help`, alongside `/agents`. A test now asserts every command
|
|
97
|
+
is documented.
|
|
98
|
+
|
|
8
99
|
## [2.8.0] — 2026-09-05
|
|
9
100
|
|
|
10
101
|
Agent work now **survives a crash**. Until this release an agent's progress existed only in memory: the
|
|
@@ -750,6 +841,8 @@ Initial release: the provider-agnostic AI **router** — capability-based routin
|
|
|
750
841
|
scoring, evidence validation, fallback, health tracking, learning-based scoring, multi-model verification,
|
|
751
842
|
budgets, MCP tools, OpenAPI-based adapter generation, and the `AI` class + CLI.
|
|
752
843
|
|
|
844
|
+
[3.0.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v3.0.0
|
|
845
|
+
[2.9.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.9.0
|
|
753
846
|
[2.8.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.8.0
|
|
754
847
|
[2.7.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.7.0
|
|
755
848
|
[2.6.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.6.0
|
package/README.md
CHANGED
|
@@ -80,10 +80,40 @@ You describe *what you want*; the runtime picks *which model* runs it. A few ide
|
|
|
80
80
|
stateless mode).
|
|
81
81
|
- **Add your own skills** — drop a manifest into `.ai-runtime/skills/`, or install a skill pack from npm.
|
|
82
82
|
- **Steer routing** — hard-`exclude` or soft-`prefer` providers/models via config, per-run, or env vars.
|
|
83
|
+
- **Connect MCP servers** — `mcp:` in the config brings a server's tools in as ordinary permission-gated
|
|
84
|
+
tools, deny-by-default per server. `ai-runtime mcp` and `/mcp` show what is connected.
|
|
85
|
+
- **Delegate to agents** — a plan step can hand bounded work to an agent with its own narrowed catalog,
|
|
86
|
+
permissions and budget, which reports back structured findings. `/agents` lists them; `/agents stop`
|
|
87
|
+
stops one. Turn it on with `runtime.agents.enabled`.
|
|
88
|
+
- **Survive a crash mid-run** — agent progress is committed as it happens, so resuming finishes the job
|
|
89
|
+
instead of redoing it. `ai-runtime resume-execution <id>`.
|
|
83
90
|
- Everything that acts is **permission-gated and workspace-jailed** (see [Safety](#safety)).
|
|
84
91
|
|
|
85
92
|
---
|
|
86
93
|
|
|
94
|
+
## What's new in 3.0
|
|
95
|
+
|
|
96
|
+
The 3.x arc made the runtime reason about *what it can do*, work with tools it didn't ship with, delegate
|
|
97
|
+
bounded work, and survive being killed in the middle of it.
|
|
98
|
+
|
|
99
|
+
- **Action capabilities** — the runtime knows it can `read_file` or `run_tests` independently of which
|
|
100
|
+
tool or skill provides it, and tells you what's missing when a goal needs something it hasn't got.
|
|
101
|
+
- **MCP** — a zero-dependency client over stdio and streamable HTTP. Server tools become ordinary tools,
|
|
102
|
+
with per-server grants that are off until you say otherwise.
|
|
103
|
+
- **Agents** — a third kind of plan step. An agent gets an envelope narrowed from the parent's catalog and
|
|
104
|
+
permissions, a call budget it cannot exceed, and an output contract its findings must satisfy. Approving
|
|
105
|
+
a plan shows you that envelope, because approving a delegation blind is approving a blank cheque.
|
|
106
|
+
- **Durable agent work** — plan, inner steps, findings and spend are on disk before the next wave starts,
|
|
107
|
+
so a `kill -9` costs you the step in flight and nothing else.
|
|
108
|
+
- **Auto-decomposition** (opt-in) — a goal can delegate to read-shaped agents nobody configured. The roles
|
|
109
|
+
and their objectives ship in-tree; nothing the model writes becomes an objective, a tool, or a
|
|
110
|
+
permission.
|
|
111
|
+
|
|
112
|
+
Upgrading from 2.x needs no config change. One default moved: the capability catalog is now included in
|
|
113
|
+
planning prompts — set `runtime.capabilities.catalog: false` to remove it.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
87
117
|
## Configuration
|
|
88
118
|
|
|
89
119
|
Three places, and only these:
|
package/dist/agents/admit.d.ts
CHANGED
|
@@ -65,5 +65,13 @@ export declare function admitFindings(input: AdmitFindingsInput): AdmissionResul
|
|
|
65
65
|
* output contract: then a violation, or admitting nothing at all, is a contract failure. With no
|
|
66
66
|
* declared contract findings are best-effort - rejections are diagnostics and the step's success is
|
|
67
67
|
* decided by its inner plan alone.
|
|
68
|
+
*
|
|
69
|
+
* `emptyIsFailure: false` keeps the contract's SHAPE rules (types, cap, subject) while making "found
|
|
70
|
+
* nothing worth reporting" an ordinary outcome rather than a failure. Derived agents pass it: their
|
|
71
|
+
* contract is mandatory precisely so their output stays bounded, but none of the shipped tools emit
|
|
72
|
+
* `data.findings`, so requiring at least one finding would fail every derived task against the
|
|
73
|
+
* runtime's own toolset — a feature that cannot succeed out of the box.
|
|
68
74
|
*/
|
|
69
|
-
export declare function contractFailed(result: AdmissionResult
|
|
75
|
+
export declare function contractFailed(result: AdmissionResult, opts?: {
|
|
76
|
+
emptyIsFailure?: boolean;
|
|
77
|
+
}): boolean;
|
package/dist/agents/admit.js
CHANGED
|
@@ -121,9 +121,17 @@ function validateFinding(f) {
|
|
|
121
121
|
* output contract: then a violation, or admitting nothing at all, is a contract failure. With no
|
|
122
122
|
* declared contract findings are best-effort - rejections are diagnostics and the step's success is
|
|
123
123
|
* decided by its inner plan alone.
|
|
124
|
+
*
|
|
125
|
+
* `emptyIsFailure: false` keeps the contract's SHAPE rules (types, cap, subject) while making "found
|
|
126
|
+
* nothing worth reporting" an ordinary outcome rather than a failure. Derived agents pass it: their
|
|
127
|
+
* contract is mandatory precisely so their output stays bounded, but none of the shipped tools emit
|
|
128
|
+
* `data.findings`, so requiring at least one finding would fail every derived task against the
|
|
129
|
+
* runtime's own toolset — a feature that cannot succeed out of the box.
|
|
124
130
|
*/
|
|
125
|
-
export function contractFailed(result) {
|
|
131
|
+
export function contractFailed(result, opts = {}) {
|
|
126
132
|
if (!result.contractDeclared)
|
|
127
133
|
return false;
|
|
128
|
-
|
|
134
|
+
if (result.contractViolated)
|
|
135
|
+
return true;
|
|
136
|
+
return opts.emptyIsFailure !== false && result.admitted.length === 0;
|
|
129
137
|
}
|
|
@@ -49,5 +49,26 @@ export interface NarrowEnvelopeInput {
|
|
|
49
49
|
maxDurationMs: number;
|
|
50
50
|
maxInnerCalls: number;
|
|
51
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Where the definition came from (Phase 3.7). `authored` keeps the 3.4 rule that an omitted field
|
|
54
|
+
* INHERITS the parent's reach — an operator wrote that definition, and omission is their choice.
|
|
55
|
+
* `derived` INVERTS it: an omitted field means nothing at all.
|
|
56
|
+
*
|
|
57
|
+
* The inversion lives here rather than in the synthesizer on purpose. If the synthesizer did the
|
|
58
|
+
* bounding, a forgotten field there would silently hand a machine-generated agent the parent's whole
|
|
59
|
+
* catalog and every permission — it would fail OPEN. Here, a synthesizer bug produces an agent that
|
|
60
|
+
* can do nothing: loud, and safe. There is still exactly one enforcement point.
|
|
61
|
+
*/
|
|
62
|
+
provenance?: 'authored' | 'derived';
|
|
52
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* What a DERIVED agent may do: read, and reason about what it read. Every other dimension is explicitly
|
|
66
|
+
* `false` rather than absent, because `clampPermissions` treats an absent field as "inherit" — so an
|
|
67
|
+
* explicit denial is the only thing that actually denies. Writes remain the province of a definition a
|
|
68
|
+
* human wrote and an operator configured.
|
|
69
|
+
*/
|
|
70
|
+
export declare const DERIVED_PERMISSIONS: PermissionPolicy;
|
|
71
|
+
/** Tighter ceilings for an agent nobody authored. A definition may still only lower them. */
|
|
72
|
+
export declare const DERIVED_MAX_TOOL_CALLS = 8;
|
|
73
|
+
export declare const DERIVED_MAX_INNER_CALLS = 2;
|
|
53
74
|
export declare function narrowEnvelope(input: NarrowEnvelopeInput): AgentEnvelope;
|
package/dist/agents/envelope.js
CHANGED
|
@@ -15,6 +15,26 @@ import { clampPermissions } from '../tools/permissions.js';
|
|
|
15
15
|
import { flattenClamp } from '../util/flatten.js';
|
|
16
16
|
/** The definition-authored objective reaches a model prompt, so it is bounded like any other source text. */
|
|
17
17
|
export const OBJECTIVE_MAX = 240;
|
|
18
|
+
/**
|
|
19
|
+
* What a DERIVED agent may do: read, and reason about what it read. Every other dimension is explicitly
|
|
20
|
+
* `false` rather than absent, because `clampPermissions` treats an absent field as "inherit" — so an
|
|
21
|
+
* explicit denial is the only thing that actually denies. Writes remain the province of a definition a
|
|
22
|
+
* human wrote and an operator configured.
|
|
23
|
+
*/
|
|
24
|
+
export const DERIVED_PERMISSIONS = {
|
|
25
|
+
fsRead: true,
|
|
26
|
+
fsWrite: false,
|
|
27
|
+
shell: false,
|
|
28
|
+
shellAllowedCommands: [],
|
|
29
|
+
gitWrite: false,
|
|
30
|
+
gitCommit: false,
|
|
31
|
+
gitPush: false,
|
|
32
|
+
network: false,
|
|
33
|
+
mcp: { servers: {} },
|
|
34
|
+
};
|
|
35
|
+
/** Tighter ceilings for an agent nobody authored. A definition may still only lower them. */
|
|
36
|
+
export const DERIVED_MAX_TOOL_CALLS = 8;
|
|
37
|
+
export const DERIVED_MAX_INNER_CALLS = 2;
|
|
18
38
|
/** A cap a definition may only LOWER, never raise, and never below 1. */
|
|
19
39
|
function lowerOnly(deflt, requested) {
|
|
20
40
|
return Math.max(1, Math.min(deflt, requested ?? deflt));
|
|
@@ -25,17 +45,31 @@ export function narrowEnvelope(input) {
|
|
|
25
45
|
// (1) Tools: intersect with the parent. A definition entry naming something the parent does not have
|
|
26
46
|
// is simply absent — it can never ADD a tool.
|
|
27
47
|
const parentTools = new Set(input.parentTools);
|
|
28
|
-
const
|
|
48
|
+
const derived = input.provenance === 'derived';
|
|
49
|
+
// A derived definition that names no tools gets NONE. An authored one inherits the parent's, which is
|
|
50
|
+
// the 3.4 behaviour and stays unchanged.
|
|
51
|
+
const tools = uniqSorted((def.tools ?? (derived ? [] : input.parentTools)).filter((t) => parentTools.has(t)));
|
|
29
52
|
// (2) Skills: intersect with the parent, then drop any skill that needs a tool outside the envelope.
|
|
30
53
|
// That second clause is load-bearing, not tidiness: a skill's own `callTool` resolves straight off the
|
|
31
54
|
// Runtime's registry with no allowlist check, so admitting a skill whose declared tools escape the
|
|
32
55
|
// envelope would be a hole. Excluding it is the structural fix; the worker's membership check is
|
|
33
56
|
// defense in depth.
|
|
34
57
|
const inner = new Set(tools);
|
|
35
|
-
const allowedSkills = def.skills ? new Set(def.skills) : undefined;
|
|
58
|
+
const allowedSkills = def.skills ? new Set(def.skills) : derived ? new Set() : undefined;
|
|
36
59
|
const skills = uniqSorted(input.parentSkills.filter((s) => (!allowedSkills || allowedSkills.has(s.id)) && (s.tools ?? []).every((t) => inner.has(t))).map((s) => s.id));
|
|
37
60
|
// (3) Permissions: minimum-merged and fully explicit (see `clampPermissions`).
|
|
38
|
-
|
|
61
|
+
// For a derived agent DERIVED_PERMISSIONS is a CEILING, not a default: the definition is clamped
|
|
62
|
+
// against it first, so even an explicit `fsWrite: true` in a synthesized definition cannot grant
|
|
63
|
+
// writing. Using it as a mere default would leave the guarantee resting on the synthesizer never
|
|
64
|
+
// setting the field — which is true today, and is exactly the kind of thing that stops being true.
|
|
65
|
+
const permissions = derived
|
|
66
|
+
? clampPermissions(input.parentPermissions, clampPermissions(DERIVED_PERMISSIONS, def.permissions))
|
|
67
|
+
: clampPermissions(input.parentPermissions, def.permissions);
|
|
68
|
+
// MCP is the one non-boolean permission dimension, and its clamp is a PER-KEY minimum: an empty
|
|
69
|
+
// override map means "no opinion", i.e. inherit every server grant — the opposite of what an empty
|
|
70
|
+
// map reads like. So a derived agent's MCP access is set explicitly rather than clamped to nothing.
|
|
71
|
+
if (derived)
|
|
72
|
+
permissions.mcp = { servers: {} };
|
|
39
73
|
// (6) Routing: exclusions only ever GROW, preferences only ever shrink, so an agent can never
|
|
40
74
|
// re-admit a provider the parent excluded, nor reach past a privacy or policy decision.
|
|
41
75
|
const pr = input.parentRouting;
|
|
@@ -55,10 +89,10 @@ export function narrowEnvelope(input) {
|
|
|
55
89
|
skills,
|
|
56
90
|
permissions,
|
|
57
91
|
// (4) Caps: a definition may only lower.
|
|
58
|
-
maxToolCalls: lowerOnly(defaults.maxToolCalls, def.maxToolCalls),
|
|
92
|
+
maxToolCalls: lowerOnly(derived ? Math.min(defaults.maxToolCalls, DERIVED_MAX_TOOL_CALLS) : defaults.maxToolCalls, def.maxToolCalls),
|
|
59
93
|
maxDurationMs: lowerOnly(defaults.maxDurationMs, def.maxDurationMs),
|
|
60
94
|
// (5) The reservation is the same shape of number, and doubles as the hard inner-call ceiling.
|
|
61
|
-
reservation: lowerOnly(defaults.maxInnerCalls, def.maxInnerCalls),
|
|
95
|
+
reservation: lowerOnly(derived ? Math.min(defaults.maxInnerCalls, DERIVED_MAX_INNER_CALLS) : defaults.maxInnerCalls, def.maxInnerCalls),
|
|
62
96
|
...(routing && Object.keys(routing).length ? { routing } : {}),
|
|
63
97
|
// Requirements ADD to the parent's — more requirements is a narrower candidate set.
|
|
64
98
|
...(def.model?.requirements?.length ? { requirements: [...def.model.requirements] } : {}),
|
package/dist/agents/finding.d.ts
CHANGED
|
@@ -72,8 +72,14 @@ export declare function executionCoverage(steps: PlanStep[]): number;
|
|
|
72
72
|
*/
|
|
73
73
|
export declare function confidenceOf(ev: FindingEvidence[], override?: unknown): number;
|
|
74
74
|
/**
|
|
75
|
-
* Resolve conflicts among
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* Resolve conflicts among findings, weighing ONLY `confidence`. Two findings conflict when they share a
|
|
76
|
+
* `type` + `subject`: a differing verdict makes the loser `contradicted`, and otherwise the loser is
|
|
77
|
+
* `superseded`. `executionCoverage` breaks a tie and never enters the weight.
|
|
78
|
+
*
|
|
79
|
+
* IDEMPOTENT AND TOTAL: pass the whole set every time, including findings already marked. The winner of
|
|
80
|
+
* each group is restored to `active`, so re-running over a set whose membership grew produces the same
|
|
81
|
+
* answer as running once over the final set. Resolving only the currently-`active` subset instead makes
|
|
82
|
+
* the outcome depend on the order results ARRIVE — and leaves `supersededBy` pointing at a finding that
|
|
83
|
+
* was itself later superseded, a chain nothing heals.
|
|
78
84
|
*/
|
|
79
85
|
export declare function resolveConflicts(findings: Finding[]): Finding[];
|
package/dist/agents/finding.js
CHANGED
|
@@ -51,9 +51,15 @@ export function confidenceOf(ev, override) {
|
|
|
51
51
|
return round2(c);
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
* Resolve conflicts among
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* Resolve conflicts among findings, weighing ONLY `confidence`. Two findings conflict when they share a
|
|
55
|
+
* `type` + `subject`: a differing verdict makes the loser `contradicted`, and otherwise the loser is
|
|
56
|
+
* `superseded`. `executionCoverage` breaks a tie and never enters the weight.
|
|
57
|
+
*
|
|
58
|
+
* IDEMPOTENT AND TOTAL: pass the whole set every time, including findings already marked. The winner of
|
|
59
|
+
* each group is restored to `active`, so re-running over a set whose membership grew produces the same
|
|
60
|
+
* answer as running once over the final set. Resolving only the currently-`active` subset instead makes
|
|
61
|
+
* the outcome depend on the order results ARRIVE — and leaves `supersededBy` pointing at a finding that
|
|
62
|
+
* was itself later superseded, a chain nothing heals.
|
|
57
63
|
*/
|
|
58
64
|
export function resolveConflicts(findings) {
|
|
59
65
|
const groups = new Map();
|
|
@@ -69,6 +75,11 @@ export function resolveConflicts(findings) {
|
|
|
69
75
|
continue;
|
|
70
76
|
const ranked = [...group].sort((a, b) => b.confidence - a.confidence || b.executionCoverage - a.executionCoverage || (a.id < b.id ? -1 : 1));
|
|
71
77
|
const winner = ranked[0];
|
|
78
|
+
// The winner is `active` by definition of having won — even if an earlier, smaller round had
|
|
79
|
+
// marked it a loser. This is what makes the function idempotent.
|
|
80
|
+
const top = out.get(winner.id);
|
|
81
|
+
top.status = 'active';
|
|
82
|
+
delete top.supersededBy;
|
|
72
83
|
for (const loser of ranked.slice(1)) {
|
|
73
84
|
const row = out.get(loser.id);
|
|
74
85
|
const differingVerdict = loser.verdict !== undefined && winner.verdict !== undefined && loser.verdict !== winner.verdict;
|
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,11 +69,19 @@ 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
|
|
72
80
|
* crash, and the resume has nothing to skip. Synchronous; must not throw. */
|
|
73
81
|
onRecord?: (record: AgentTaskRecord) => void;
|
|
82
|
+
/** Phase 3.7: this agent was SYNTHESIZED, not authored. Its output contract bounds what it may
|
|
83
|
+
* report without obliging it to report anything. */
|
|
84
|
+
derived?: boolean;
|
|
74
85
|
/** Phase 3.5: a persisted record to CONTINUE instead of minting a fresh one. The caller proves it
|
|
75
86
|
* belongs to THIS step by step-input hash before passing it. */
|
|
76
87
|
resume?: AgentTaskRecord;
|
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 });
|
|
@@ -362,7 +372,10 @@ export async function runAgentTask(step, envelope, definition, deps) {
|
|
|
362
372
|
// Bounded: diagnostics accumulate across attempts, and every one of them is rewritten to disk on
|
|
363
373
|
// every commit. Keeping the most recent is the useful half.
|
|
364
374
|
record.diagnostics = [...record.diagnostics, ...admission.rejected].slice(-DIAGNOSTICS_KEPT);
|
|
365
|
-
|
|
375
|
+
// A DERIVED agent's contract bounds what it may report; it does not oblige it to report. Nothing in
|
|
376
|
+
// the shipped toolset emits `data.findings`, so demanding at least one would fail every derived task
|
|
377
|
+
// against the runtime's own tools — the feature would be unusable without a bespoke tool.
|
|
378
|
+
if (contractFailed(admission, { emptyIsFailure: !deps.derived })) {
|
|
366
379
|
return finish('failed', { code: 'finding-contract', message: 'the agent did not satisfy its declared output contract' });
|
|
367
380
|
}
|
|
368
381
|
if (expired())
|
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('
|
|
24
|
+
program.name('ai-runtime').description('Universal, provider-agnostic AI Runtime & Orchestration Platform').version('3.0.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;
|