ai-runtime-engine 2.9.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 +45 -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/worker.d.ts +3 -0
- package/dist/agents/worker.js +4 -1
- package/dist/cli/cli.js +1 -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/runtime.d.ts +40 -3
- package/dist/runtime/runtime.js +116 -18
- package/dist/runtime/types.d.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,50 @@ 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
|
+
|
|
8
52
|
## [2.9.0] — 2026-09-05
|
|
9
53
|
|
|
10
54
|
Agent work becomes VISIBLE. Concurrent agent steps render as live lanes in the interactive terminal,
|
|
@@ -797,6 +841,7 @@ Initial release: the provider-agnostic AI **router** — capability-based routin
|
|
|
797
841
|
scoring, evidence validation, fallback, health tracking, learning-based scoring, multi-model verification,
|
|
798
842
|
budgets, MCP tools, OpenAPI-based adapter generation, and the `AI` class + CLI.
|
|
799
843
|
|
|
844
|
+
[3.0.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v3.0.0
|
|
800
845
|
[2.9.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.9.0
|
|
801
846
|
[2.8.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.8.0
|
|
802
847
|
[2.7.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.7.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/worker.d.ts
CHANGED
|
@@ -79,6 +79,9 @@ export interface AgentWorkerDeps {
|
|
|
79
79
|
* `finish()` — the inner plan, every completed inner step, every inner model call — is lost to a
|
|
80
80
|
* crash, and the resume has nothing to skip. Synchronous; must not throw. */
|
|
81
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;
|
|
82
85
|
/** Phase 3.5: a persisted record to CONTINUE instead of minting a fresh one. The caller proves it
|
|
83
86
|
* belongs to THIS step by step-input hash before passing it. */
|
|
84
87
|
resume?: AgentTaskRecord;
|
package/dist/agents/worker.js
CHANGED
|
@@ -372,7 +372,10 @@ export async function runAgentTask(step, envelope, definition, deps) {
|
|
|
372
372
|
// Bounded: diagnostics accumulate across attempts, and every one of them is rewritten to disk on
|
|
373
373
|
// every commit. Keeping the most recent is the useful half.
|
|
374
374
|
record.diagnostics = [...record.diagnostics, ...admission.rejected].slice(-DIAGNOSTICS_KEPT);
|
|
375
|
-
|
|
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 })) {
|
|
376
379
|
return finish('failed', { code: 'finding-contract', message: 'the agent did not satisfy its declared output contract' });
|
|
377
380
|
}
|
|
378
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.
|
|
@@ -43,7 +43,8 @@ export interface OrchestrateInput {
|
|
|
43
43
|
routing?: RoutingPreferences;
|
|
44
44
|
/** Phase 22: run the phases that fit the call budget and pause resumably (vs. the default notify-and-wait). */
|
|
45
45
|
partial?: boolean;
|
|
46
|
-
/** Phase 3.1: pre-rendered action-capability snapshot for the planner prompt
|
|
46
|
+
/** Phase 3.1: pre-rendered, fenced action-capability snapshot for the planner prompt. Present by
|
|
47
|
+
* default from 3.0.0; `runtime.capabilities.catalog: false` removes it. */
|
|
47
48
|
capabilityCatalog?: string;
|
|
48
49
|
/** Phase 3.3: pre-rendered "Required capabilities" block for the planner prompt (opt-in). */
|
|
49
50
|
requiredCapabilities?: string;
|
|
@@ -18,7 +18,8 @@ export interface PlannerInput {
|
|
|
18
18
|
reason?: string;
|
|
19
19
|
/** Observations from a prior attempt, to inform a replan. */
|
|
20
20
|
priorObservations?: string[];
|
|
21
|
-
/** Phase 3.1: a pre-rendered, capped, fenced action-capability snapshot
|
|
21
|
+
/** Phase 3.1: a pre-rendered, capped, fenced action-capability snapshot (fenced for real as of
|
|
22
|
+
* 3.0.0, and present by default from it). Absent ⇒ the prompt is
|
|
22
23
|
* byte-identical to 2.3.0 (the catalog flag is off by default). */
|
|
23
24
|
capabilityCatalog?: string;
|
|
24
25
|
/** Phase 3.3: a pre-rendered, clamped block naming the capabilities the goal was derived to need and
|
package/dist/runtime/config.js
CHANGED
|
@@ -40,7 +40,7 @@ const agentDefinition = z
|
|
|
40
40
|
.strict();
|
|
41
41
|
/** An agent definition id: the same prompt-safe shape an MCP server id must have. */
|
|
42
42
|
const AGENT_ID_RE = /^[a-z0-9][a-z0-9_-]{0,32}$/;
|
|
43
|
-
const runtimeSettings = z.object({ defaultMode: z.enum(RUNTIME_MODES).optional(), defaultStrategy: z.enum(STRATEGIES).optional(), context: z.object({ maxTokens: z.number().optional(), verifyLoss: z.boolean().optional(), summarize: z.boolean().optional() }).strict().optional(), skills: z.object({ paths: z.array(z.string()).optional(), packages: z.array(z.string()).optional(), autoload: z.boolean().optional() }).strict().optional(), embedding: z.object({ provider: z.enum(['local', 'openai-compatible']), baseUrl: z.string().optional(), apiKeyEnv: z.string().optional(), model: z.string().optional() }).strict().optional(), intent: z.object({ aiFallback: z.boolean().optional() }).strict().optional(), organization: z.string().optional(), storage: z.object({ encrypt: z.boolean(), keyEnv: z.string() }).strict().optional(), capabilities: z.object({ catalog: z.boolean().optional(), planning: z.boolean().optional(), aliases: z.record(z.string(), z.string()).optional(), pins: z.record(z.string(), z.string()).optional() }).strict().optional(), concurrency: z.object({ maxParallelSteps: z.number().optional(), perTool: z.record(z.string(), z.number()).optional(), perSkill: z.record(z.string(), z.number()).optional(), perProvider: z.record(z.string(), z.number()).optional(), perAgent: z.record(z.string(), z.number()).optional() }).strict().optional(), agents: z.object({ enabled: z.boolean().optional(), maxToolCalls: z.number().optional(), maxDurationMs: z.number().optional(), maxInnerCalls: z.number().optional(), definitions: z.record(z.string().regex(AGENT_ID_RE, 'an agent definition id must be lowercase kebab/snake (max 33 chars)'), agentDefinition).optional() }).strict().optional() }).strict();
|
|
43
|
+
const runtimeSettings = z.object({ defaultMode: z.enum(RUNTIME_MODES).optional(), defaultStrategy: z.enum(STRATEGIES).optional(), context: z.object({ maxTokens: z.number().optional(), verifyLoss: z.boolean().optional(), summarize: z.boolean().optional() }).strict().optional(), skills: z.object({ paths: z.array(z.string()).optional(), packages: z.array(z.string()).optional(), autoload: z.boolean().optional() }).strict().optional(), embedding: z.object({ provider: z.enum(['local', 'openai-compatible']), baseUrl: z.string().optional(), apiKeyEnv: z.string().optional(), model: z.string().optional() }).strict().optional(), intent: z.object({ aiFallback: z.boolean().optional() }).strict().optional(), organization: z.string().optional(), storage: z.object({ encrypt: z.boolean(), keyEnv: z.string() }).strict().optional(), capabilities: z.object({ catalog: z.boolean().optional(), planning: z.boolean().optional(), aliases: z.record(z.string(), z.string()).optional(), pins: z.record(z.string(), z.string()).optional() }).strict().optional(), concurrency: z.object({ maxParallelSteps: z.number().optional(), perTool: z.record(z.string(), z.number()).optional(), perSkill: z.record(z.string(), z.number()).optional(), perProvider: z.record(z.string(), z.number()).optional(), perAgent: z.record(z.string(), z.number()).optional() }).strict().optional(), agents: z.object({ enabled: z.boolean().optional(), decompose: z.boolean().optional(), maxToolCalls: z.number().optional(), maxDurationMs: z.number().optional(), maxInnerCalls: z.number().optional(), definitions: z.record(z.string().regex(AGENT_ID_RE, 'an agent definition id must be lowercase kebab/snake (max 33 chars)').refine((id) => !id.startsWith('auto_'), 'the `auto_` prefix is reserved for agents the runtime derives — pick another id'), agentDefinition).optional() }).strict().optional() }).strict();
|
|
44
44
|
const learning = z.object({ enabled: z.boolean().optional() }).strict();
|
|
45
45
|
const verification = z.object({ enabled: z.boolean().optional() }).strict();
|
|
46
46
|
const budget = z.object({ maxCostUsd: z.number().optional(), maxCalls: z.number().optional() }).strict();
|
|
@@ -178,6 +178,15 @@ export declare class Runtime {
|
|
|
178
178
|
* This run's agent envelopes. THE ONLY call site of `narrowEnvelope` — never re-derive an inner
|
|
179
179
|
* catalog, a permission clamp, or a reservation anywhere else (see the header of agents/envelope.ts).
|
|
180
180
|
*/
|
|
181
|
+
/**
|
|
182
|
+
* Agents synthesized from the registry for this goal (Phase 3.7). Empty unless
|
|
183
|
+
* `runtime.agents.decompose` is on — so with the flag off nothing about planning changes.
|
|
184
|
+
*
|
|
185
|
+
* Deterministic and offline: no model call, no clock, no randomness. That is a requirement, not a
|
|
186
|
+
* preference — a derived definition is hashed into `agentDefHash`, and a resume that synthesized
|
|
187
|
+
* even slightly differently would discard every persisted inner plan as stale.
|
|
188
|
+
*/
|
|
189
|
+
private derivedAgents;
|
|
181
190
|
private agentEnvelopes;
|
|
182
191
|
/** The MCP server manager: `list()`, `status(id)`, `test(id)`, `addServer`, `removeServer`, `setEnabled`. */
|
|
183
192
|
mcp(): McpManager;
|
|
@@ -299,9 +308,17 @@ export declare class Runtime {
|
|
|
299
308
|
* skipped — no skill ran, so there is no success/failure to learn (recording them would teach noise). */
|
|
300
309
|
private recordOrchestration;
|
|
301
310
|
/**
|
|
302
|
-
* A capped, FENCED action-capability snapshot for the planner prompt (Phase 3.1
|
|
303
|
-
*
|
|
304
|
-
*
|
|
311
|
+
* A capped, FENCED action-capability snapshot for the planner prompt (Phase 3.1; ON by default from
|
|
312
|
+
* 3.0.0 — set `runtime.capabilities.catalog: false` to remove it).
|
|
313
|
+
*
|
|
314
|
+
* The fence is real as of 3.0.0 and was not before: this block carries ids that come from MCP servers
|
|
315
|
+
* and third-party skills, and it renders them as trusted-looking prompt structure on every planning
|
|
316
|
+
* iteration of every run. Flattening (`promptSafe`) bounds their shape but says nothing about their
|
|
317
|
+
* provenance, so the whole block is wrapped as untrusted data. Three comments claimed "fenced" while
|
|
318
|
+
* no fence existed; shipping that ON by default would have made a false safety claim load-bearing.
|
|
319
|
+
*
|
|
320
|
+
* Both halves are bounded. The blocked-skill list had no cap at all — measured at ~24k characters
|
|
321
|
+
* with 300 blocked skills, silently, in every prompt.
|
|
305
322
|
*/
|
|
306
323
|
private capabilityCatalogText;
|
|
307
324
|
/** Any call/cost ceiling declared in the config file's `budget:` block (router-level, not policy). */
|
|
@@ -373,6 +390,26 @@ export declare class Runtime {
|
|
|
373
390
|
* an agent step would have failed every one of those steps.
|
|
374
391
|
*/
|
|
375
392
|
private orchestrateRunners;
|
|
393
|
+
/**
|
|
394
|
+
* Reconcile findings that contradict each other, across ALL of this execution's agent tasks
|
|
395
|
+
* (Phase 3.7).
|
|
396
|
+
*
|
|
397
|
+
* `resolveConflicts` has existed since 3.4 with no caller, so two agents reaching opposite
|
|
398
|
+
* conclusions about the same subject both stayed `active` — and both were rendered into the next
|
|
399
|
+
* planning prompt, as if the runtime had no opinion about which was better supported. It does: it
|
|
400
|
+
* weighs evidence-based `confidence`, with `executionCoverage` only as a tiebreak.
|
|
401
|
+
*
|
|
402
|
+
* Runs at the ONE point new findings can appear — a task reaching a terminal state — and writes the
|
|
403
|
+
* outcome back onto the owning records, so a supersession survives a restart rather than being
|
|
404
|
+
* recomputed (and possibly recomputed differently) on every read.
|
|
405
|
+
*
|
|
406
|
+
* EVERY finding is passed in, not just the active ones. Resolving over the active subset makes the
|
|
407
|
+
* result depend on the order tasks happen to finish: a finding that beat a weak rival in wave 1 can
|
|
408
|
+
* itself lose in wave 2, and the wave-1 loser is then left pointing at a superseded finding — a
|
|
409
|
+
* broken chain nothing heals. Re-resolving the whole set each round is order-independent and gives
|
|
410
|
+
* the same answer as one pass over the final set.
|
|
411
|
+
*/
|
|
412
|
+
private resolveFindingConflicts;
|
|
376
413
|
/**
|
|
377
414
|
* A bounded, fenced brief of what the agents have already established (Phase 3.5).
|
|
378
415
|
*
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -55,6 +55,9 @@ 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
57
|
import { AGENT_TERMINAL, AGENT_RESUMABLE, agentTaskView } from '../agents/task.js';
|
|
58
|
+
import { synthesizeAgents } from '../agents/synthesize.js';
|
|
59
|
+
import { isDerivedAgentId } from '../agents/roles.js';
|
|
60
|
+
import { resolveConflicts } from '../agents/finding.js';
|
|
58
61
|
import { parseAgentTasks } from '../executions/agentTasks.js';
|
|
59
62
|
import { stepIdentity } from '../agents/worker.js';
|
|
60
63
|
import { hashOf } from '../util/hash.js';
|
|
@@ -460,6 +463,12 @@ export class Runtime {
|
|
|
460
463
|
registerAgent(id, def) {
|
|
461
464
|
if (!/^[a-z0-9][a-z0-9_-]{0,32}$/.test(id))
|
|
462
465
|
throw new AIError(`invalid agent id '${id}' — use lowercase letters, digits, '_' or '-' (max 33 chars)`, { category: 'CONFIG' });
|
|
466
|
+
// The same reservation the config schema enforces. Without it here, a host could register
|
|
467
|
+
// `auto_investigate` and — with decompose on — produce two catalog rows with one id, where the
|
|
468
|
+
// derived one wins both lookups and the SAME step resolves to a different envelope depending on a
|
|
469
|
+
// flag. Reserving a namespace in only one of its two doors reserves nothing.
|
|
470
|
+
if (isDerivedAgentId(id))
|
|
471
|
+
throw new AIError(`agent id '${id}' uses the reserved 'auto_' prefix — that namespace belongs to agents the runtime derives`, { category: 'CONFIG' });
|
|
463
472
|
this.agentDefs.set(id, def);
|
|
464
473
|
return this;
|
|
465
474
|
}
|
|
@@ -471,13 +480,34 @@ export class Runtime {
|
|
|
471
480
|
* This run's agent envelopes. THE ONLY call site of `narrowEnvelope` — never re-derive an inner
|
|
472
481
|
* catalog, a permission clamp, or a reservation anywhere else (see the header of agents/envelope.ts).
|
|
473
482
|
*/
|
|
474
|
-
|
|
475
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Agents synthesized from the registry for this goal (Phase 3.7). Empty unless
|
|
485
|
+
* `runtime.agents.decompose` is on — so with the flag off nothing about planning changes.
|
|
486
|
+
*
|
|
487
|
+
* Deterministic and offline: no model call, no clock, no randomness. That is a requirement, not a
|
|
488
|
+
* preference — a derived definition is hashed into `agentDefHash`, and a resume that synthesized
|
|
489
|
+
* even slightly differently would discard every persisted inner plan as stale.
|
|
490
|
+
*/
|
|
491
|
+
derivedAgents() {
|
|
492
|
+
if (!this.agentsEnabled || this.settingsValue.agents?.decompose !== true)
|
|
493
|
+
return [];
|
|
494
|
+
const capabilities = this._capabilities.list().map((c) => ({ id: c.id, effects: c.effects, providers: this._capabilities.providersOf(c.id).map((p) => p.providerId) }));
|
|
495
|
+
return synthesizeAgents({ capabilities, parentTools: this._tools.ids() });
|
|
496
|
+
}
|
|
497
|
+
agentEnvelopes(policy, derived = []) {
|
|
498
|
+
// Derived definitions are passed IN and never stored: `agentDefs` lives for the process, so writing
|
|
499
|
+
// a per-goal agent into it would leak that agent into every later run on this Runtime.
|
|
500
|
+
const entries = [
|
|
501
|
+
...[...this.agentDefs].map(([id, d]) => [id, d, 'authored']),
|
|
502
|
+
...derived.map((d) => [d.id, d.definition, 'derived']),
|
|
503
|
+
];
|
|
504
|
+
if (!this.agentsEnabled || entries.length === 0)
|
|
476
505
|
return [];
|
|
477
506
|
const routing = this.effectiveRouting();
|
|
478
|
-
return
|
|
507
|
+
return entries.map(([id, definition, provenance]) => narrowEnvelope({
|
|
479
508
|
agentId: id,
|
|
480
509
|
definition,
|
|
510
|
+
provenance,
|
|
481
511
|
parentTools: this._tools.ids(),
|
|
482
512
|
parentSkills: this.skills().map((sk) => ({ id: sk.id, ...(sk.tools ? { tools: sk.tools } : {}) })),
|
|
483
513
|
parentPermissions: policy.permissions,
|
|
@@ -1028,6 +1058,10 @@ export class Runtime {
|
|
|
1028
1058
|
exec.completedSteps = [...exec.completedSteps, record.stepId];
|
|
1029
1059
|
checkpointIfMoved();
|
|
1030
1060
|
}
|
|
1061
|
+
// A finished task is the only moment new findings can appear, so it is the only moment two of
|
|
1062
|
+
// them can start disagreeing.
|
|
1063
|
+
if (AGENT_TERMINAL.has(record.state))
|
|
1064
|
+
this.resolveFindingConflicts(exec);
|
|
1031
1065
|
commit();
|
|
1032
1066
|
},
|
|
1033
1067
|
/**
|
|
@@ -1070,9 +1104,10 @@ export class Runtime {
|
|
|
1070
1104
|
...(routing ? { routing } : {}),
|
|
1071
1105
|
...(partial ? { partial: true } : {}),
|
|
1072
1106
|
...(this.approval ? { approval: this.approval } : {}),
|
|
1073
|
-
// Phase 3.1
|
|
1074
|
-
// always on but only fires on a validation failure, adding metadata to an unchanged error.
|
|
1075
|
-
|
|
1107
|
+
// Phase 3.1, ON by default from 3.0.0 (`runtime.capabilities.catalog: false` removes it); the gap
|
|
1108
|
+
// resolver is always on but only fires on a validation failure, adding metadata to an unchanged error.
|
|
1109
|
+
// 3.0.0: ON by default. `catalog: false` removes it — the only way to get the 2.9.0 prompt back.
|
|
1110
|
+
...(this.settingsValue.capabilities?.catalog !== false ? { capabilityCatalog: this.capabilityCatalogText() } : {}),
|
|
1076
1111
|
// Phase 3.3: the derived-requirement block (opt-in, pre-rendered + clamped). Absent ⇒ the
|
|
1077
1112
|
// OrchestrateInput/PlannerInput objects are key-identical to 2.5.1.
|
|
1078
1113
|
...(requiredCapabilities ? { requiredCapabilities } : {}),
|
|
@@ -1102,11 +1137,19 @@ export class Runtime {
|
|
|
1102
1137
|
this._learning.record({ goalType: this.goalType(goal), mode, ok: outcome.status === 'completed', skills: this.planSkillRefs(outcome.plan) });
|
|
1103
1138
|
}
|
|
1104
1139
|
/**
|
|
1105
|
-
* A capped, FENCED action-capability snapshot for the planner prompt (Phase 3.1
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1140
|
+
* A capped, FENCED action-capability snapshot for the planner prompt (Phase 3.1; ON by default from
|
|
1141
|
+
* 3.0.0 — set `runtime.capabilities.catalog: false` to remove it).
|
|
1142
|
+
*
|
|
1143
|
+
* The fence is real as of 3.0.0 and was not before: this block carries ids that come from MCP servers
|
|
1144
|
+
* and third-party skills, and it renders them as trusted-looking prompt structure on every planning
|
|
1145
|
+
* iteration of every run. Flattening (`promptSafe`) bounds their shape but says nothing about their
|
|
1146
|
+
* provenance, so the whole block is wrapped as untrusted data. Three comments claimed "fenced" while
|
|
1147
|
+
* no fence existed; shipping that ON by default would have made a false safety claim load-bearing.
|
|
1148
|
+
*
|
|
1149
|
+
* Both halves are bounded. The blocked-skill list had no cap at all — measured at ~24k characters
|
|
1150
|
+
* with 300 blocked skills, silently, in every prompt.
|
|
1108
1151
|
*/
|
|
1109
|
-
capabilityCatalogText(maxEntries = 40) {
|
|
1152
|
+
capabilityCatalogText(maxEntries = 40, maxBlocked = 15) {
|
|
1110
1153
|
const caps = this._capabilities.list();
|
|
1111
1154
|
if (caps.length === 0)
|
|
1112
1155
|
return '';
|
|
@@ -1116,17 +1159,21 @@ export class Runtime {
|
|
|
1116
1159
|
.providersOf(c.id)
|
|
1117
1160
|
.map((p) => `${promptSafe(p.providerId)}${p.availability === 'available' ? '' : ` (${p.availability})`}`)
|
|
1118
1161
|
.join(', ');
|
|
1119
|
-
|
|
1162
|
+
// Every segment is clamped, `effects` included: it is an array off a declaration, so a hostile or
|
|
1163
|
+
// simply careless source can make one row arbitrarily long.
|
|
1164
|
+
lines.push(` - capability "${promptSafe(c.id)}" [${promptSafe(c.effects.join('/'), 40)}] → ${promptSafe(providers, 200)}`);
|
|
1120
1165
|
}
|
|
1121
1166
|
const more = caps.length > maxEntries ? `\n …and ${caps.length - maxEntries} more (see /capabilities)` : '';
|
|
1122
1167
|
// Skills hidden by a missing tool — the "why can't you do this" answer the planner needs.
|
|
1123
1168
|
const usable = new Set(this.skills().map((sk) => sk.id));
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
.
|
|
1169
|
+
const blockedAll = this._skills.list().filter((sk) => !usable.has(sk.id));
|
|
1170
|
+
const blocked = blockedAll
|
|
1171
|
+
.slice(0, maxBlocked)
|
|
1127
1172
|
.map((sk) => ` - skill "${promptSafe(sk.id)}" needs tool(s) ${promptSafe((sk.tools ?? []).filter((t) => !this._tools.ids().includes(t)).join(', '), 200)} (not registered)`);
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1173
|
+
const blockedMore = blockedAll.length > maxBlocked ? `\n …and ${blockedAll.length - maxBlocked} more` : '';
|
|
1174
|
+
const unavailable = blocked.length ? `\nUnavailable (do not use):\n${blocked.join('\n')}${blockedMore}` : '';
|
|
1175
|
+
// The ids inside come from MCP servers and third-party skills. Fenced as data, not structure.
|
|
1176
|
+
return wrapUntrusted('capability-catalog', `Action capabilities:\n${lines.join('\n')}${more}${unavailable}`);
|
|
1130
1177
|
}
|
|
1131
1178
|
/** Any call/cost ceiling declared in the config file's `budget:` block (router-level, not policy). */
|
|
1132
1179
|
configBudget() {
|
|
@@ -1503,8 +1550,12 @@ export class Runtime {
|
|
|
1503
1550
|
*/
|
|
1504
1551
|
orchestrateRunners(policy, opts) {
|
|
1505
1552
|
const { runId, signal, provenance, onRecord, agentResume } = opts;
|
|
1506
|
-
const
|
|
1553
|
+
const derived = this.derivedAgents();
|
|
1554
|
+
const envelopes = policy ? this.agentEnvelopes(policy, derived) : [];
|
|
1507
1555
|
const byId = new Map(envelopes.map((e) => [e.agentId, e]));
|
|
1556
|
+
// Definitions come from a LOCAL map, not the process-lifetime field: a derived agent exists for
|
|
1557
|
+
// this run only, and must not be findable by any later one.
|
|
1558
|
+
const defsById = new Map([...this.agentDefs, ...derived.map((d) => [d.id, d.definition])]);
|
|
1508
1559
|
return {
|
|
1509
1560
|
runSkill: (skillId, input) => this.runSkill(skillId, input).then((o) => ({ result: o.result, validation: o.validation })),
|
|
1510
1561
|
runTool: (toolId, input) => this.runTool(toolId, input),
|
|
@@ -1514,13 +1565,14 @@ export class Runtime {
|
|
|
1514
1565
|
reserve: (step) => (step.agent ? byId.get(step.agent)?.reservation ?? 1 : 0),
|
|
1515
1566
|
runAgent: async (step, ctx) => {
|
|
1516
1567
|
const envelope = byId.get(step.agent ?? '');
|
|
1517
|
-
const definition =
|
|
1568
|
+
const definition = defsById.get(step.agent ?? '');
|
|
1518
1569
|
if (!envelope || !definition)
|
|
1519
1570
|
return { stepId: step.id, ok: false, code: 'agent-not-enabled', error: `no agent definition '${step.agent ?? ''}'` };
|
|
1520
1571
|
const innerSkills = this._skills.list().filter((sk) => envelope.skills.includes(sk.id));
|
|
1521
1572
|
// Phase 3.5: continue a persisted task for THIS step, if one exists.
|
|
1522
1573
|
const prior = agentResume?.(step);
|
|
1523
1574
|
const out = await runAgentTask(step, envelope, definition, {
|
|
1575
|
+
...(derived.some((d) => d.id === step.agent) ? { derived: true } : {}),
|
|
1524
1576
|
...(prior?.record ? { resume: prior.record } : {}),
|
|
1525
1577
|
...(prior?.answer ? { resumeAnswer: prior.answer } : {}),
|
|
1526
1578
|
ai: this._ai,
|
|
@@ -1579,6 +1631,52 @@ export class Runtime {
|
|
|
1579
1631
|
: {}),
|
|
1580
1632
|
};
|
|
1581
1633
|
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Reconcile findings that contradict each other, across ALL of this execution's agent tasks
|
|
1636
|
+
* (Phase 3.7).
|
|
1637
|
+
*
|
|
1638
|
+
* `resolveConflicts` has existed since 3.4 with no caller, so two agents reaching opposite
|
|
1639
|
+
* conclusions about the same subject both stayed `active` — and both were rendered into the next
|
|
1640
|
+
* planning prompt, as if the runtime had no opinion about which was better supported. It does: it
|
|
1641
|
+
* weighs evidence-based `confidence`, with `executionCoverage` only as a tiebreak.
|
|
1642
|
+
*
|
|
1643
|
+
* Runs at the ONE point new findings can appear — a task reaching a terminal state — and writes the
|
|
1644
|
+
* outcome back onto the owning records, so a supersession survives a restart rather than being
|
|
1645
|
+
* recomputed (and possibly recomputed differently) on every read.
|
|
1646
|
+
*
|
|
1647
|
+
* EVERY finding is passed in, not just the active ones. Resolving over the active subset makes the
|
|
1648
|
+
* result depend on the order tasks happen to finish: a finding that beat a weak rival in wave 1 can
|
|
1649
|
+
* itself lose in wave 2, and the wave-1 loser is then left pointing at a superseded finding — a
|
|
1650
|
+
* broken chain nothing heals. Re-resolving the whole set each round is order-independent and gives
|
|
1651
|
+
* the same answer as one pass over the final set.
|
|
1652
|
+
*/
|
|
1653
|
+
resolveFindingConflicts(exec) {
|
|
1654
|
+
const { tasks } = parseAgentTasks(exec);
|
|
1655
|
+
const all = tasks.flatMap((t) => t.findings);
|
|
1656
|
+
if (all.length < 2)
|
|
1657
|
+
return;
|
|
1658
|
+
const resolved = new Map(resolveConflicts(all).map((f) => [f.id, f]));
|
|
1659
|
+
let changed = false;
|
|
1660
|
+
const next = tasks.map((t) => {
|
|
1661
|
+
const findings = t.findings.map((f) => {
|
|
1662
|
+
const r = resolved.get(f.id);
|
|
1663
|
+
if (!r || (r.status === f.status && r.supersededBy === f.supersededBy))
|
|
1664
|
+
return f;
|
|
1665
|
+
changed = true;
|
|
1666
|
+
const next = { ...f, status: r.status };
|
|
1667
|
+
if (r.supersededBy)
|
|
1668
|
+
next.supersededBy = r.supersededBy;
|
|
1669
|
+
else
|
|
1670
|
+
delete next.supersededBy;
|
|
1671
|
+
return next;
|
|
1672
|
+
});
|
|
1673
|
+
return changed ? { ...t, findings } : t;
|
|
1674
|
+
});
|
|
1675
|
+
if (!changed)
|
|
1676
|
+
return;
|
|
1677
|
+
const byId = new Map(next.map((t) => [t.agentTaskId, t]));
|
|
1678
|
+
exec.agentTasks = (exec.agentTasks ?? []).map((entry) => byId.get(entry.agentTaskId ?? '') ?? entry);
|
|
1679
|
+
}
|
|
1582
1680
|
/**
|
|
1583
1681
|
* A bounded, fenced brief of what the agents have already established (Phase 3.5).
|
|
1584
1682
|
*
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -120,6 +120,12 @@ export interface RuntimeSettings {
|
|
|
120
120
|
* the worker's hard inner-model-call ceiling. */
|
|
121
121
|
agents?: {
|
|
122
122
|
enabled?: boolean;
|
|
123
|
+
/** Phase 3.7, default OFF: offer the goal a bounded, read-shaped agent per shipped ROLE, with no
|
|
124
|
+
* operator-authored definition. The roles and their objectives are in-tree; nothing the model
|
|
125
|
+
* writes becomes an objective, a tool id or a permission.
|
|
126
|
+
*
|
|
127
|
+
* REQUIRES `enabled: true` — on its own this does nothing, because agent execution is off. */
|
|
128
|
+
decompose?: boolean;
|
|
123
129
|
maxToolCalls?: number;
|
|
124
130
|
maxDurationMs?: number;
|
|
125
131
|
maxInnerCalls?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-runtime-engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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",
|