ai-runtime-engine 2.9.0 → 3.0.1
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 +108 -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 +8 -1
- package/dist/cli/commands/cleanup.js +11 -3
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/run.js +6 -0
- package/dist/cli/commands/skills.js +9 -2
- package/dist/cli/interactive/repl.js +12 -2
- package/dist/cli/interactive/session.d.ts +2 -0
- package/dist/cli/interactive/session.js +6 -2
- package/dist/config/schema.js +19 -1
- package/dist/conversations/conversations.d.ts +6 -1
- package/dist/conversations/conversations.js +15 -8
- package/dist/core/fallback/fallback.d.ts +7 -0
- package/dist/core/fallback/fallback.js +15 -2
- package/dist/core/health/monitor.d.ts +6 -0
- package/dist/core/health/monitor.js +15 -2
- package/dist/core/router/confidence.js +10 -5
- package/dist/core/router/dimensions.d.ts +3 -1
- package/dist/core/router/dimensions.js +15 -5
- package/dist/core/router/filter.js +25 -6
- package/dist/core/router/normalize.js +2 -0
- package/dist/core/router/router.js +16 -2
- package/dist/core/router/scorer.d.ts +3 -0
- package/dist/core/router/scorer.js +17 -2
- package/dist/discovery/openapi.js +3 -2
- package/dist/executions/agentTasks.d.ts +4 -4
- package/dist/generation/generateAdapter.js +3 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/mcp/protocol.js +4 -1
- package/dist/memory/bm25.d.ts +7 -0
- package/dist/memory/bm25.js +17 -1
- package/dist/memory/memory.d.ts +7 -1
- package/dist/memory/memory.js +18 -4
- package/dist/orchestration/orchestrator.d.ts +2 -1
- package/dist/orchestration/planner.d.ts +2 -1
- package/dist/plugin/ai.d.ts +6 -0
- package/dist/plugin/ai.js +17 -2
- package/dist/providers/estimate.d.ts +25 -0
- package/dist/providers/estimate.js +55 -0
- package/dist/providers/factory.d.ts +3 -0
- package/dist/providers/factory.js +26 -5
- package/dist/providers/httpClient.js +4 -0
- package/dist/providers/httpProvider.js +4 -3
- package/dist/providers/mock/mockProvider.js +4 -3
- package/dist/runtime/config.d.ts +4 -3
- package/dist/runtime/config.js +14 -23
- package/dist/runtime/events.d.ts +6 -0
- package/dist/runtime/runtime.d.ts +43 -5
- package/dist/runtime/runtime.js +133 -25
- package/dist/runtime/types.d.ts +8 -1
- package/dist/store/area.d.ts +1 -1
- package/dist/store/area.js +34 -10
- package/dist/store/crypto.d.ts +27 -13
- package/dist/store/crypto.js +101 -23
- package/dist/store/errors.d.ts +11 -0
- package/dist/store/errors.js +14 -0
- package/dist/store/store.d.ts +21 -1
- package/dist/store/store.js +74 -19
- package/dist/telemetry/sinks/file.js +4 -2
- package/dist/telemetry/sinks/otlp.d.ts +12 -2
- package/dist/telemetry/sinks/otlp.js +39 -24
- package/dist/telemetry/telemetry.d.ts +5 -0
- package/dist/telemetry/telemetry.js +4 -0
- package/dist/tools/builtins/shell.d.ts +30 -3
- package/dist/tools/builtins/shell.js +218 -7
- package/dist/tools/untrusted.d.ts +1 -1
- package/dist/tools/untrusted.js +5 -3
- package/dist/types.d.ts +14 -0
- package/dist/verification/verify.js +10 -3
- package/docs/GUIDE.md +66 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +5 -1
- package/docs/router.md +1 -1
- package/docs/security.md +26 -7
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,112 @@ 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.1] — 2026-09-07
|
|
9
|
+
|
|
10
|
+
**Hardening pass.** A correctness, security, test, documentation, and release-cleanup pass over a
|
|
11
|
+
full-repo audit of 3.0. The architecture is unchanged — every change extends an existing seam and is
|
|
12
|
+
backward compatible (with one documented downgrade caveat on the encryption format). The test suite grew
|
|
13
|
+
from 892 to 971 and CI now runs a Node 22 + 24 matrix with a tarball-contents audit, a doc-link/anchor
|
|
14
|
+
check, and a fail-closed test-count integrity floor.
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- **Shell tool: eval-capable invocations now require approval, even when allowlisted.** Allowlisting a
|
|
19
|
+
binary like `npm` or `node` no longer silently permits arbitrary code through its arguments —
|
|
20
|
+
interpreters run with inline-code/preload flags (`node -e`/`-r`, `python -c/-m`, `perl -M`, the `awk`
|
|
21
|
+
family, …), package/script runners (`npx`, `npm exec`/`run`/`init`, `deno task`, …), container
|
|
22
|
+
`run`/`exec`, argv-indirection wrappers (`env`, `xargs`, `nice`, `timeout`, `find -exec`, …), and a
|
|
23
|
+
non-default `make -f` are escalated to interactive approval (headless runs fail closed). The allowlist
|
|
24
|
+
matches an absolute path by basename but never a relative or workspace-internal one (realpath-checked).
|
|
25
|
+
This backstop is best-effort; `docs/security.md` documents its limits and accepted residuals.
|
|
26
|
+
- **Store files are created owner-only** (`0600` files, `0700` directories) on POSIX.
|
|
27
|
+
- **Encryption at rest v2:** the AES-256-GCM key is now derived with **scrypt** over a per-record salt
|
|
28
|
+
(the `aienc2` envelope) instead of a bare SHA-256, so a passphrase key resists brute force. Legacy
|
|
29
|
+
`aienc1` and plaintext records still read and upgrade on rewrite. A decryption failure is a typed
|
|
30
|
+
`StoreDecryptError` — honestly "wrong key OR tampering OR corruption" — and now surfaces instead of
|
|
31
|
+
reading as a silently empty store. *Downgrade caveat: a pre-3.0.1 runtime cannot read `aienc2` records.*
|
|
32
|
+
- Untrusted-content fencing neutralizes any fence label (not just its own); the verifier's model-authored
|
|
33
|
+
`reason` is flattened/clamped and its prompt inputs are fenced; the MCP schema sanitizer uses a
|
|
34
|
+
null-prototype object so a `__proto__` key cannot pollute.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **Config schema accepted four documented keys it used to reject.** `parseConfig` (the public export) now
|
|
39
|
+
accepts `learning`, `verification`, `budget`, and `policy` at the root; before this a config using any of
|
|
40
|
+
them failed to load.
|
|
41
|
+
- **Routing now always enforces context-fit**, not only when a cost/latency constraint is set; and
|
|
42
|
+
cost/context **estimates account for multimodal input parts** (an image is no longer estimated as zero).
|
|
43
|
+
- **`models: 'auto'`** resolves deterministically offline (defaultModel → kind catalog); a zero-model
|
|
44
|
+
result is a loud CONFIG error, not a silently unroutable provider.
|
|
45
|
+
- **Streaming fallback no longer garbles output**: a failed streamed attempt is explicitly abandoned via a
|
|
46
|
+
new `response.stream_abandoned` runtime event, and `response.streamed` is accurate under fallback.
|
|
47
|
+
- **OTLP telemetry is flushed on close**, so short-lived runs no longer drop sub-batch events.
|
|
48
|
+
- **Store lock** stale-steal is atomic (ownership token); **conversation metadata** writes serialize
|
|
49
|
+
through it; the **HTTP client** no longer sleeps out a backoff after the final retry; a stale provider
|
|
50
|
+
**cooldown** now reads as routable again; provider `listModels()`/`discover()` return copies.
|
|
51
|
+
- **CLI `--json --yes`** on `cleanup` and `skills --scaffold` now applies the operation and reports the
|
|
52
|
+
result, instead of printing a preview and ignoring `--yes`. `ai-runtime mcp show <id>` was added.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **The four validated-but-ignored config fields are now enforced:** `ProviderConfig.weightOverrides`
|
|
57
|
+
(per-provider score weights; keys restricted to the scoring dimensions), `TaskDefinition.qualityFloor`
|
|
58
|
+
(excludes lower-tier models; a model with no tier is not excluded), `CapabilityRequirement.weight`
|
|
59
|
+
(weights capability fit and confidence alike), and `constraints.minimumConfidence` (raises the effective
|
|
60
|
+
confidence floor; it flags, it does not fail the run).
|
|
61
|
+
- **The memory tokenizer is Unicode-aware** — accented Latin, Cyrillic, Arabic, and CJK are now
|
|
62
|
+
searchable; pure-ASCII tokenization is byte-identical. *Note: learned goal keys for non-ASCII goals
|
|
63
|
+
re-key (old degenerate records are orphaned, never mixed); session-cached embedding vectors shift.*
|
|
64
|
+
|
|
65
|
+
### Notes for consumers
|
|
66
|
+
|
|
67
|
+
- The `RuntimeEvent` union gained a `response.stream_abandoned` arm. New arms are additive at runtime but
|
|
68
|
+
break an exhaustive `switch` at compile time — carry a default case.
|
|
69
|
+
|
|
70
|
+
## [3.0.0] — 2026-09-05
|
|
71
|
+
|
|
72
|
+
**AI Runtime 3.0.** The 3.x arc set out to make the runtime reason about *what it can do*, talk to tools
|
|
73
|
+
it did not ship with, delegate bounded work to agents, and survive being killed in the middle of it.
|
|
74
|
+
That is now true end to end, and `tests/integration/runtime-3-demo.test.ts` runs the whole of it —
|
|
75
|
+
derive, decompose, delegate, crash, resume — offline, inside a call budget, with a leak scan.
|
|
76
|
+
|
|
77
|
+
Upgrading from 2.9.0 requires no config change. One documented default changes; see below.
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- **Auto-decomposition** (`runtime.agents.decompose`, default OFF) — a goal can delegate to a bounded,
|
|
82
|
+
read-shaped agent with no operator-authored definition. Three roles ship in-tree; no model-authored
|
|
83
|
+
string ever becomes an objective, a tool id, or a permission, and `auto_` is a reserved config
|
|
84
|
+
namespace so a derived id can never shadow an authored one.
|
|
85
|
+
- **Conflict supersession is wired.** `resolveConflicts` shipped in 3.4 with no caller, so two agents
|
|
86
|
+
reaching opposite conclusions about the same subject both stayed active and both reached the next
|
|
87
|
+
planning prompt. Resolution now runs when a task finishes, weighs evidence-based confidence, and
|
|
88
|
+
persists — and it is order-independent, so the answer does not depend on which agent happens to
|
|
89
|
+
finish first.
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
|
|
93
|
+
- **`runtime.capabilities.catalog` now defaults to ON.** Set `catalog: false` to remove the block.
|
|
94
|
+
Before flipping it, the block was made to match its own documentation: three comments across three
|
|
95
|
+
files described it as fenced when nothing fenced it, and its blocked-skill half had no cap at all
|
|
96
|
+
(~24k characters with 300 blocked skills, silently, in every prompt). Both are fixed.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- **A throwing agent took down the whole run.** `executor.ts` returned the agent runner's promise from
|
|
101
|
+
inside a `try` — and an async function *adopts* a returned promise rather than awaiting it, so the
|
|
102
|
+
rejection escaped the executor's own catch. The plan failed instead of the step, and wave-mates were
|
|
103
|
+
left running unawaited. Present since 2.7.0.
|
|
104
|
+
- Derived permissions are a ceiling rather than a default, so a synthesized definition cannot request
|
|
105
|
+
write access even when the parent has it.
|
|
106
|
+
|
|
107
|
+
### Notes for consumers
|
|
108
|
+
|
|
109
|
+
- The `RuntimeEvent` union gained agent arms in 2.9.0 and may gain more. New arms are additive at
|
|
110
|
+
runtime but break an exhaustive `switch` at compile time — carry a default case.
|
|
111
|
+
- `capabilities.planning` is skipped whenever any call or cost budget is set, and a resumed run is
|
|
112
|
+
never re-derived. Both are deliberate and now asserted by tests rather than only documented.
|
|
113
|
+
|
|
8
114
|
## [2.9.0] — 2026-09-05
|
|
9
115
|
|
|
10
116
|
Agent work becomes VISIBLE. Concurrent agent steps render as live lanes in the interactive terminal,
|
|
@@ -797,6 +903,8 @@ Initial release: the provider-agnostic AI **router** — capability-based routin
|
|
|
797
903
|
scoring, evidence validation, fallback, health tracking, learning-based scoring, multi-model verification,
|
|
798
904
|
budgets, MCP tools, OpenAPI-based adapter generation, and the `AI` class + CLI.
|
|
799
905
|
|
|
906
|
+
[3.0.1]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v3.0.1
|
|
907
|
+
[3.0.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v3.0.0
|
|
800
908
|
[2.9.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.9.0
|
|
801
909
|
[2.8.0]: https://github.com/pavankhandelwal21/ai-runtime/releases/tag/v2.8.0
|
|
802
910
|
[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.1');
|
|
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.
|
|
@@ -77,6 +77,13 @@ mcp
|
|
|
77
77
|
.option(...configOpt)
|
|
78
78
|
.option('--json', 'print as JSON')
|
|
79
79
|
.action((id, o) => mcpCommand(id, o));
|
|
80
|
+
mcp
|
|
81
|
+
.command('show')
|
|
82
|
+
.argument('<id>', 'server id')
|
|
83
|
+
.description('Show one server in detail — the unambiguous form for an id that collides with a subcommand name (add/remove/enable/disable/test)')
|
|
84
|
+
.option(...configOpt)
|
|
85
|
+
.option('--json', 'print as JSON')
|
|
86
|
+
.action((id, o) => mcpCommand(id, o));
|
|
80
87
|
mcp
|
|
81
88
|
.command('add')
|
|
82
89
|
.argument('<id>', 'a short id for the server (lowercase, kebab/snake)')
|
|
@@ -74,11 +74,19 @@ export async function cleanupCommand(opts) {
|
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
const plan = planCleanup(rt, Date.now());
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
const removable = toRemove(plan);
|
|
78
|
+
// `--json` changes the OUTPUT FORMAT only; it does not suppress `--yes`. Without `--yes` (or with
|
|
79
|
+
// `--dry-run`) it emits a plan preview; with `--yes` it applies and reports the result. It never
|
|
80
|
+
// prompts (machine-facing). The JSON always carries `mode` so a caller can tell plan from applied.
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
if (opts.yes && !opts.dryRun && removable > 0) {
|
|
83
|
+
const applied = applyCleanup(rt, plan);
|
|
84
|
+
return print(JSON.stringify({ mode: 'applied', plan, applied }, null, 2));
|
|
85
|
+
}
|
|
86
|
+
return print(JSON.stringify({ mode: 'plan', plan }, null, 2));
|
|
87
|
+
}
|
|
79
88
|
for (const line of renderPlan(plan))
|
|
80
89
|
print(line);
|
|
81
|
-
const removable = toRemove(plan);
|
|
82
90
|
if (removable === 0) {
|
|
83
91
|
print('\nNothing to remove.');
|
|
84
92
|
return;
|
|
@@ -75,7 +75,7 @@ export function renderDoctor(r) {
|
|
|
75
75
|
if (!r.providers.length)
|
|
76
76
|
lines.push(' (none configured — run `ai-runtime setup`)');
|
|
77
77
|
for (const p of r.providers)
|
|
78
|
-
lines.push(` ${p.authenticated ? '✓' : '✗'} ${p.id.padEnd(14)} ${p.authenticated ? 'authenticated' : 'not configured'} [${p.state}] ${p.models} models`);
|
|
78
|
+
lines.push(` ${p.authenticated ? '✓' : '✗'} ${p.id.padEnd(14)} ${p.authenticated ? 'authenticated' : 'not configured'} [${p.state}] ${p.models} models${p.models === 0 ? ' ⚠ no routable models — list models explicitly or set defaultModel' : ''}`);
|
|
79
79
|
lines.push(`Models: ${r.models.total} known, ${r.models.usableProviders} provider(s) usable`);
|
|
80
80
|
lines.push('', 'Store:');
|
|
81
81
|
if (!r.store.enabled)
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -32,6 +32,12 @@ async function runWith(rt, input, options) {
|
|
|
32
32
|
printChunk(e.text);
|
|
33
33
|
streamedAny = true;
|
|
34
34
|
}
|
|
35
|
+
else if (e.type === 'response.stream_abandoned') {
|
|
36
|
+
// Mark a boundary so the discarded partial is not read as part of what follows. Do not promise a
|
|
37
|
+
// retry — abandonment also fires when the failed attempt was the last candidate.
|
|
38
|
+
printChunk('\n↩ discarded incomplete response\n');
|
|
39
|
+
streamedAny = false;
|
|
40
|
+
}
|
|
35
41
|
});
|
|
36
42
|
}
|
|
37
43
|
if (options.mode) {
|
|
@@ -51,8 +51,15 @@ export async function skillsCommand(opts) {
|
|
|
51
51
|
return print(JSON.stringify({ ok: false, error: draft.error }, null, 2));
|
|
52
52
|
return print(`Could not scaffold a skill: ${draft.error}`);
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// `--json` changes the OUTPUT FORMAT only; with `--yes` it SAVES and reports the path, without it
|
|
55
|
+
// returns the draft. It never prompts (machine-facing). `mode` distinguishes draft from saved.
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
if (opts.yes) {
|
|
58
|
+
const savedPath = rt.saveScaffoldedSkill(draft.manifest);
|
|
59
|
+
return print(JSON.stringify({ ok: true, mode: 'saved', path: savedPath, manifest: draft.manifest }, null, 2));
|
|
60
|
+
}
|
|
61
|
+
return print(JSON.stringify({ ok: true, mode: 'draft', manifest: draft.manifest, yaml: draft.yaml }, null, 2));
|
|
62
|
+
}
|
|
56
63
|
print(`Drafted skill "${draft.manifest.id}":\n`);
|
|
57
64
|
print(draft.yaml);
|
|
58
65
|
const proceed = opts.yes || (await confirm(`Save to .ai-runtime/skills/${draft.manifest.id}.skill.yaml?`));
|
|
@@ -88,8 +88,10 @@ export async function startRepl(configPath) {
|
|
|
88
88
|
if (!line.trim())
|
|
89
89
|
return;
|
|
90
90
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// Owner-only: history holds raw prompt lines under the store home (0700 dir / 0600 file, matching
|
|
92
|
+
// the store's own record hardening). Mode applies only on creation; POSIX-only, a no-op on Windows.
|
|
93
|
+
mkdirSync(dirname(historyPath), { recursive: true, mode: 0o700 });
|
|
94
|
+
appendFileSync(historyPath, `${line}\n`, { mode: 0o600 });
|
|
93
95
|
}
|
|
94
96
|
catch {
|
|
95
97
|
/* history is a convenience; a write failure must never break the REPL */
|
|
@@ -121,6 +123,14 @@ export async function startRepl(configPath) {
|
|
|
121
123
|
streamedThisRun = true;
|
|
122
124
|
return;
|
|
123
125
|
}
|
|
126
|
+
if (e.type === 'response.stream_abandoned') {
|
|
127
|
+
// The partial answer just streamed from this provider is being discarded; mark a clear boundary so
|
|
128
|
+
// whatever follows is not read as a seamless continuation of it. Not necessarily a retry — this also
|
|
129
|
+
// fires when the failed attempt was the last candidate.
|
|
130
|
+
printChunk('\n↩ discarded incomplete response\n');
|
|
131
|
+
streamedThisRun = false;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
124
134
|
const lane = lanes.observe(e);
|
|
125
135
|
if (lane) {
|
|
126
136
|
if (!runInFlight)
|
|
@@ -22,8 +22,10 @@ export declare class ReplSession {
|
|
|
22
22
|
private conversationId?;
|
|
23
23
|
private dryRunMode;
|
|
24
24
|
private streaming;
|
|
25
|
+
private readonly env;
|
|
25
26
|
constructor(runtime: Runtime, opts?: {
|
|
26
27
|
streaming?: boolean;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
27
29
|
});
|
|
28
30
|
currentMode(): RuntimeMode;
|
|
29
31
|
private views;
|
|
@@ -76,9 +76,13 @@ export class ReplSession {
|
|
|
76
76
|
conversationId;
|
|
77
77
|
dryRunMode = false;
|
|
78
78
|
streaming;
|
|
79
|
+
env;
|
|
79
80
|
constructor(runtime, opts = {}) {
|
|
80
81
|
this.runtime = runtime;
|
|
81
82
|
this.streaming = opts.streaming ?? false;
|
|
83
|
+
// The env the /budget display reads. Injectable so tests need not mutate the real process.env; the
|
|
84
|
+
// enforcement path already resolves budgets from the runtime's injected env.
|
|
85
|
+
this.env = opts.env ?? process.env;
|
|
82
86
|
}
|
|
83
87
|
currentMode() {
|
|
84
88
|
return this.mode;
|
|
@@ -202,8 +206,8 @@ export class ReplSession {
|
|
|
202
206
|
case 'budget':
|
|
203
207
|
return {
|
|
204
208
|
lines: [
|
|
205
|
-
`call budget (AI_MAX_CALLS): ${
|
|
206
|
-
`cost budget (AI_MAX_COST_USD): ${
|
|
209
|
+
`call budget (AI_MAX_CALLS): ${this.env.AI_MAX_CALLS ?? '(unset — no limit)'}`,
|
|
210
|
+
`cost budget (AI_MAX_COST_USD): ${this.env.AI_MAX_COST_USD ?? '(unset — no limit)'}`,
|
|
207
211
|
'over budget: notify-and-wait by default; add --partial (one-shot) to run the phases that fit and pause.',
|
|
208
212
|
],
|
|
209
213
|
};
|
package/dist/config/schema.js
CHANGED
|
@@ -11,6 +11,8 @@ const EVIDENCE = ['unsupported', 'unknown', 'inferred', 'documented', 'verified'
|
|
|
11
11
|
const GROUPS = ['input', 'output', 'intelligence', 'agent'];
|
|
12
12
|
const KINDS = ['openai-compatible', 'gemini', 'groq', 'anthropic', 'ollama', 'custom', 'mock'];
|
|
13
13
|
export const KEY_LIKE = /^(sk-|gsk_|Bearer\s|[A-Za-z0-9_-]{40,}$)/;
|
|
14
|
+
/** The seven scoring-weight dimensions. A `weightOverrides` block may only name these keys. */
|
|
15
|
+
const SCORE_WEIGHT_KEYS = ['capabilityFit', 'quality', 'reliability', 'historicalSuccess', 'latency', 'cost', 'userPreference'];
|
|
14
16
|
const capabilityRequirement = z.object({
|
|
15
17
|
group: z.enum(GROUPS),
|
|
16
18
|
key: z.string().min(1),
|
|
@@ -50,10 +52,22 @@ const providerConfig = z
|
|
|
50
52
|
privacyClass: z.enum(['local', 'cloud']).optional(),
|
|
51
53
|
wireShape: z.enum(['openai', 'anthropic']).optional(),
|
|
52
54
|
headers: z.record(z.string()).optional(),
|
|
53
|
-
weightOverrides: z.record(z.number()).optional(),
|
|
55
|
+
weightOverrides: z.record(z.enum(SCORE_WEIGHT_KEYS), z.number()).optional(),
|
|
54
56
|
})
|
|
55
57
|
.strict()
|
|
56
58
|
.refine((p) => !(['openai-compatible', 'custom'].includes(p.kind) && !p.baseUrl), { message: 'openai-compatible/custom providers require a baseUrl' });
|
|
59
|
+
/**
|
|
60
|
+
* The four router-level blocks that Runtime settings also accept. Defined once here — the single source
|
|
61
|
+
* of truth used by `routerConfig` below — so `parseConfig` (the public export) accepts them. Before 3.0.1
|
|
62
|
+
* the root schema rejected them and they were only tolerated via parseRuntimeConfig's fold-back; that
|
|
63
|
+
* fold-back is gone, and the runtime-config layer now receives them transitively through parseConfig.
|
|
64
|
+
*/
|
|
65
|
+
const learningConfig = z.object({ enabled: z.boolean().optional() }).strict();
|
|
66
|
+
const verificationConfig = z.object({ enabled: z.boolean().optional() }).strict();
|
|
67
|
+
const budgetConfig = z.object({ maxCostUsd: z.number().optional(), maxCalls: z.number().optional() }).strict();
|
|
68
|
+
const policyConfig = z
|
|
69
|
+
.object({ allowProviders: z.array(z.string()).optional(), denyProviders: z.array(z.string()).optional(), requireLocal: z.boolean().optional(), maxCostUsd: z.number().optional(), strategy: z.enum(STRATEGIES).optional() })
|
|
70
|
+
.strict();
|
|
57
71
|
const routerConfig = z
|
|
58
72
|
.object({
|
|
59
73
|
providers: z.array(providerConfig),
|
|
@@ -65,6 +79,10 @@ const routerConfig = z
|
|
|
65
79
|
.strict()
|
|
66
80
|
.optional(),
|
|
67
81
|
telemetry: z.object({ enabled: z.boolean().optional(), sink: z.enum(['memory', 'file', 'otlp']).optional(), storePrompts: z.literal(false).optional(), path: z.string().optional(), endpoint: z.string().optional(), headersEnv: z.string().refine((v) => v === undefined || !KEY_LIKE.test(v), { message: 'headersEnv must be an env-var NAME, not a header/token value' }).optional() }).strict().optional(),
|
|
82
|
+
learning: learningConfig.optional(),
|
|
83
|
+
verification: verificationConfig.optional(),
|
|
84
|
+
budget: budgetConfig.optional(),
|
|
85
|
+
policy: policyConfig.optional(),
|
|
68
86
|
tasks: z.array(taskDefinition).optional(),
|
|
69
87
|
})
|
|
70
88
|
.strict();
|
|
@@ -20,11 +20,16 @@ export interface ConversationMeta {
|
|
|
20
20
|
updatedAt: number;
|
|
21
21
|
turns: number;
|
|
22
22
|
}
|
|
23
|
+
/** Serializes a critical section. Injected from the RuntimeStore's advisory lock; defaults to a no-op. */
|
|
24
|
+
export type WithLock = <T>(fn: () => T) => T;
|
|
23
25
|
export declare class ConversationStore {
|
|
24
26
|
private readonly area;
|
|
25
27
|
private readonly clock;
|
|
28
|
+
private readonly withLock;
|
|
26
29
|
private counter;
|
|
27
|
-
constructor(area: Area, clock?: Clock
|
|
30
|
+
constructor(area: Area, clock?: Clock, opts?: {
|
|
31
|
+
withLock?: WithLock;
|
|
32
|
+
});
|
|
28
33
|
get enabled(): boolean;
|
|
29
34
|
/** Start a conversation; returns its id. */
|
|
30
35
|
start(id?: string): string;
|