ai-runtime-engine 1.3.0 → 2.7.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.
Files changed (138) hide show
  1. package/CHANGELOG.md +578 -0
  2. package/dist/agents/admit.d.ts +69 -0
  3. package/dist/agents/admit.js +129 -0
  4. package/dist/agents/definition.d.ts +36 -0
  5. package/dist/agents/definition.js +9 -0
  6. package/dist/agents/envelope.d.ts +53 -0
  7. package/dist/agents/envelope.js +68 -0
  8. package/dist/agents/finding.d.ts +79 -0
  9. package/dist/agents/finding.js +80 -0
  10. package/dist/agents/task.d.ts +60 -0
  11. package/dist/agents/task.js +32 -0
  12. package/dist/agents/worker.d.ts +68 -0
  13. package/dist/agents/worker.js +256 -0
  14. package/dist/capabilities/capability.d.ts +117 -0
  15. package/dist/capabilities/capability.js +66 -0
  16. package/dist/capabilities/registry.d.ts +139 -0
  17. package/dist/capabilities/registry.js +413 -0
  18. package/dist/capabilities/vocabulary.d.ts +32 -0
  19. package/dist/capabilities/vocabulary.js +34 -0
  20. package/dist/cli/cli.js +55 -4
  21. package/dist/cli/commands/cleanup.js +29 -27
  22. package/dist/cli/commands/doctor.d.ts +14 -0
  23. package/dist/cli/commands/doctor.js +38 -8
  24. package/dist/cli/commands/executions.js +34 -25
  25. package/dist/cli/commands/info.d.ts +1 -0
  26. package/dist/cli/commands/info.js +11 -9
  27. package/dist/cli/commands/init.js +19 -0
  28. package/dist/cli/commands/inspect.d.ts +40 -1
  29. package/dist/cli/commands/inspect.js +157 -2
  30. package/dist/cli/commands/mcp.d.ts +45 -0
  31. package/dist/cli/commands/mcp.js +148 -0
  32. package/dist/cli/commands/route.js +21 -0
  33. package/dist/cli/commands/run.d.ts +1 -0
  34. package/dist/cli/commands/run.js +21 -2
  35. package/dist/cli/commands/skills.d.ts +2 -0
  36. package/dist/cli/commands/skills.js +29 -7
  37. package/dist/cli/interactive/ansi.d.ts +41 -0
  38. package/dist/cli/interactive/ansi.js +43 -0
  39. package/dist/cli/interactive/complete.d.ts +10 -0
  40. package/dist/cli/interactive/complete.js +19 -0
  41. package/dist/cli/interactive/repl.d.ts +3 -0
  42. package/dist/cli/interactive/repl.js +91 -13
  43. package/dist/cli/interactive/session.d.ts +8 -0
  44. package/dist/cli/interactive/session.js +73 -2
  45. package/dist/cli/render.d.ts +7 -0
  46. package/dist/cli/render.js +10 -0
  47. package/dist/cli/runtimeSession.d.ts +11 -0
  48. package/dist/cli/runtimeSession.js +17 -0
  49. package/dist/config/defaults.d.ts +3 -1
  50. package/dist/config/defaults.js +2 -0
  51. package/dist/config/schema.d.ts +1 -0
  52. package/dist/config/schema.js +2 -2
  53. package/dist/context/lossVerifier.d.ts +24 -0
  54. package/dist/context/lossVerifier.js +45 -0
  55. package/dist/context/summarize.d.ts +19 -0
  56. package/dist/context/summarize.js +53 -0
  57. package/dist/core/fallback/fallback.d.ts +5 -0
  58. package/dist/core/fallback/fallback.js +3 -1
  59. package/dist/core/router/router.d.ts +3 -0
  60. package/dist/core/router/router.js +1 -0
  61. package/dist/executions/execution.d.ts +13 -2
  62. package/dist/generation/generateAdapter.d.ts +14 -0
  63. package/dist/generation/generateAdapter.js +38 -0
  64. package/dist/generation/generateSkill.d.ts +26 -0
  65. package/dist/generation/generateSkill.js +51 -0
  66. package/dist/index.d.ts +43 -4
  67. package/dist/index.js +26 -2
  68. package/dist/mcp/client.d.ts +70 -0
  69. package/dist/mcp/client.js +221 -0
  70. package/dist/mcp/manager.d.ts +151 -0
  71. package/dist/mcp/manager.js +493 -0
  72. package/dist/mcp/protocol.d.ts +216 -0
  73. package/dist/mcp/protocol.js +149 -0
  74. package/dist/mcp/toolAdapter.d.ts +44 -0
  75. package/dist/mcp/toolAdapter.js +94 -0
  76. package/dist/mcp/transport.d.ts +109 -0
  77. package/dist/mcp/transport.js +383 -0
  78. package/dist/memory/embedders/hash.d.ts +12 -0
  79. package/dist/memory/embedders/hash.js +31 -0
  80. package/dist/memory/embedders/http.d.ts +25 -0
  81. package/dist/memory/embedders/http.js +48 -0
  82. package/dist/memory/memory.d.ts +19 -2
  83. package/dist/memory/memory.js +75 -11
  84. package/dist/memory/semantic.d.ts +17 -0
  85. package/dist/memory/semantic.js +29 -0
  86. package/dist/orchestration/budget.d.ts +30 -0
  87. package/dist/orchestration/budget.js +40 -0
  88. package/dist/orchestration/executor.d.ts +39 -1
  89. package/dist/orchestration/executor.js +64 -4
  90. package/dist/orchestration/orchestrator.d.ts +29 -1
  91. package/dist/orchestration/orchestrator.js +89 -8
  92. package/dist/orchestration/plan.d.ts +15 -1
  93. package/dist/orchestration/plan.js +23 -4
  94. package/dist/orchestration/planner.d.ts +19 -1
  95. package/dist/orchestration/planner.js +25 -5
  96. package/dist/plugin/ai.d.ts +4 -0
  97. package/dist/plugin/ai.js +9 -0
  98. package/dist/runtime/config.js +50 -6
  99. package/dist/runtime/intent/aiClassifier.d.ts +19 -0
  100. package/dist/runtime/intent/aiClassifier.js +74 -0
  101. package/dist/runtime/models/modelProfile.d.ts +61 -0
  102. package/dist/runtime/models/modelProfile.js +139 -0
  103. package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
  104. package/dist/runtime/planning/deriveCapabilities.js +146 -0
  105. package/dist/runtime/policy.d.ts +10 -0
  106. package/dist/runtime/policy.js +9 -2
  107. package/dist/runtime/runtime.d.ts +173 -0
  108. package/dist/runtime/runtime.js +705 -50
  109. package/dist/runtime/types.d.ts +88 -2
  110. package/dist/skills/manifest.d.ts +3 -0
  111. package/dist/skills/manifest.js +24 -0
  112. package/dist/skills/registry.d.ts +16 -1
  113. package/dist/skills/registry.js +21 -1
  114. package/dist/skills/skill.d.ts +6 -1
  115. package/dist/store/area.d.ts +15 -1
  116. package/dist/store/area.js +19 -8
  117. package/dist/store/crypto.d.ts +21 -0
  118. package/dist/store/crypto.js +49 -0
  119. package/dist/store/paths.d.ts +5 -1
  120. package/dist/store/paths.js +6 -0
  121. package/dist/store/store.d.ts +15 -3
  122. package/dist/store/store.js +28 -7
  123. package/dist/telemetry/sinks/otlp.d.ts +31 -0
  124. package/dist/telemetry/sinks/otlp.js +76 -0
  125. package/dist/tools/builtins/filesystem.js +1 -0
  126. package/dist/tools/builtins/git.js +1 -0
  127. package/dist/tools/builtins/shell.js +1 -0
  128. package/dist/tools/permissions.d.ts +28 -0
  129. package/dist/tools/permissions.js +72 -0
  130. package/dist/tools/registry.d.ts +18 -2
  131. package/dist/tools/registry.js +22 -2
  132. package/dist/tools/tool.d.ts +4 -0
  133. package/dist/types.d.ts +5 -1
  134. package/dist/util/flatten.d.ts +11 -0
  135. package/dist/util/flatten.js +18 -0
  136. package/dist/util/semaphore.d.ts +19 -0
  137. package/dist/util/semaphore.js +60 -0
  138. package/package.json +24 -9
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The Finding ADMISSION PIPELINE (Phase 3.4) - the only way a Finding enters persistent or inter-agent
3
+ * state. Arbitrary agent output NEVER becomes a Finding.
4
+ *
5
+ * FOUR STAGES, in order, each with one rejection reason: extract -> output-contract validate ->
6
+ * Finding validate -> persist. A rejected candidate becomes a DIAGNOSTIC on the task record and is
7
+ * never threaded: not onto the observation, not into a prompt, not into a RuntimeResult.
8
+ *
9
+ * Findings carry no secret by construction: every string is a clamped derivative of tool/skill output.
10
+ * That clamp is the ONLY defense for a persisted finding - `src/security/redact.ts` covers telemetry and
11
+ * CLI output, not the execution store.
12
+ */
13
+ import type { Finding, FindingEvidence } from './finding.js';
14
+ import type { AgentDefinition } from './definition.js';
15
+ import type { ArtifactRef } from '../runtime/types.js';
16
+ /** Clamp bounds. Each is a prompt- and record-facing string. */
17
+ export declare const TYPE_MAX = 40;
18
+ export declare const SUBJECT_MAX = 80;
19
+ export declare const CLAIM_MAX = 240;
20
+ export declare const VERDICT_MAX = 80;
21
+ /** Observations are persisted verbatim onto the Execution, so what rides along is bounded. */
22
+ export declare const FINDING_THREAD_MAX = 5;
23
+ /** The contract default when a definition declares one without a count. */
24
+ export declare const DEFAULT_MAX_FINDINGS = 10;
25
+ export interface AdmissionRejection {
26
+ index: number;
27
+ stage: 'extract' | 'output-contract' | 'finding-validate' | 'persist';
28
+ why: string;
29
+ }
30
+ export interface AdmissionResult {
31
+ admitted: Finding[];
32
+ rejected: AdmissionRejection[];
33
+ /** True when a DECLARED output contract was violated - the input to the task-failure rule. */
34
+ contractViolated: boolean;
35
+ /** Whether a contract was declared at all. Recorded HERE so `contractFailed` needs only this result:
36
+ * taking the contract twice would let a caller check the rule against a contract the pipeline never
37
+ * actually applied, and quietly get the wrong answer. */
38
+ contractDeclared: boolean;
39
+ }
40
+ export interface AdmitFindingsInput {
41
+ agentTaskId: string;
42
+ agentId: string;
43
+ /** Raw `data.findings` from the final inner step. Untrusted shape: anything at all. */
44
+ candidates: unknown;
45
+ contract?: AgentDefinition['outputContract'];
46
+ /** Deterministic execution coverage, injected - never computed from the candidates. */
47
+ coverage: number;
48
+ /** `SkillResult.data.confidence` from the final inner step, if it offered one. */
49
+ confidenceOverride?: unknown;
50
+ evidenceFor: (index: number) => FindingEvidence[];
51
+ artifactsFor: (index: number) => {
52
+ refs: ArtifactRef[];
53
+ unavailable: boolean;
54
+ };
55
+ sourceSteps: string[];
56
+ provenance: {
57
+ executionId?: string;
58
+ planVersion: number;
59
+ };
60
+ now: number;
61
+ }
62
+ export declare function admitFindings(input: AdmitFindingsInput): AdmissionResult;
63
+ /**
64
+ * THE TASK-FAILURE RULE. An agent task fails on its findings ONLY when the definition declared an
65
+ * output contract: then a violation, or admitting nothing at all, is a contract failure. With no
66
+ * declared contract findings are best-effort - rejections are diagnostics and the step's success is
67
+ * decided by its inner plan alone.
68
+ */
69
+ export declare function contractFailed(result: AdmissionResult): boolean;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The Finding ADMISSION PIPELINE (Phase 3.4) - the only way a Finding enters persistent or inter-agent
3
+ * state. Arbitrary agent output NEVER becomes a Finding.
4
+ *
5
+ * FOUR STAGES, in order, each with one rejection reason: extract -> output-contract validate ->
6
+ * Finding validate -> persist. A rejected candidate becomes a DIAGNOSTIC on the task record and is
7
+ * never threaded: not onto the observation, not into a prompt, not into a RuntimeResult.
8
+ *
9
+ * Findings carry no secret by construction: every string is a clamped derivative of tool/skill output.
10
+ * That clamp is the ONLY defense for a persisted finding - `src/security/redact.ts` covers telemetry and
11
+ * CLI output, not the execution store.
12
+ */
13
+ import { flattenClamp } from '../util/flatten.js';
14
+ import { confidenceOf } from './finding.js';
15
+ /** Clamp bounds. Each is a prompt- and record-facing string. */
16
+ export const TYPE_MAX = 40;
17
+ export const SUBJECT_MAX = 80;
18
+ export const CLAIM_MAX = 240;
19
+ export const VERDICT_MAX = 80;
20
+ /** Observations are persisted verbatim onto the Execution, so what rides along is bounded. */
21
+ export const FINDING_THREAD_MAX = 5;
22
+ /** The contract default when a definition declares one without a count. */
23
+ export const DEFAULT_MAX_FINDINGS = 10;
24
+ export function admitFindings(input) {
25
+ const admitted = [];
26
+ const rejected = [];
27
+ let contractViolated = false;
28
+ // STAGE 1 - EXTRACT. The shape is whatever a skill happened to return.
29
+ if (!Array.isArray(input.candidates)) {
30
+ return { admitted, rejected: [{ index: -1, stage: 'extract', why: 'data.findings is not an array' }], contractViolated: input.contract !== undefined, contractDeclared: input.contract !== undefined };
31
+ }
32
+ const maxFindings = input.contract ? input.contract.maxFindings ?? DEFAULT_MAX_FINDINGS : Number.POSITIVE_INFINITY;
33
+ input.candidates.forEach((raw, index) => {
34
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
35
+ rejected.push({ index, stage: 'extract', why: 'candidate is not an object' });
36
+ return;
37
+ }
38
+ const c = raw;
39
+ if (typeof c.type !== 'string' || !c.type.trim()) {
40
+ rejected.push({ index, stage: 'extract', why: 'missing type' });
41
+ return;
42
+ }
43
+ if (typeof c.claim !== 'string' || !c.claim.trim()) {
44
+ rejected.push({ index, stage: 'extract', why: 'missing claim' });
45
+ return;
46
+ }
47
+ // STAGE 2 - OUTPUT-CONTRACT VALIDATE. Only when the definition declared one.
48
+ if (input.contract) {
49
+ if (!input.contract.types.includes(c.type)) {
50
+ rejected.push({ index, stage: 'output-contract', why: `type '${flattenClamp(c.type, TYPE_MAX)}' is not in the declared output contract` });
51
+ contractViolated = true;
52
+ return;
53
+ }
54
+ if (admitted.length >= maxFindings) {
55
+ rejected.push({ index, stage: 'output-contract', why: 'exceeds maxFindings' });
56
+ contractViolated = true;
57
+ return;
58
+ }
59
+ if (input.contract.requireSubject && (typeof c.subject !== 'string' || !c.subject.trim())) {
60
+ rejected.push({ index, stage: 'output-contract', why: 'the contract requires a subject' });
61
+ contractViolated = true;
62
+ return;
63
+ }
64
+ }
65
+ // STAGE 3 - FINDING VALIDATE. Build it fully, clamp every string, then assert the invariants.
66
+ const evidence = input.evidenceFor(index);
67
+ const artifacts = input.artifactsFor(index);
68
+ const confidence = confidenceOf(evidence, input.confidenceOverride);
69
+ const finding = {
70
+ id: `${input.agentTaskId}_f${admitted.length + 1}`,
71
+ agentId: input.agentId,
72
+ agentTaskId: input.agentTaskId,
73
+ type: flattenClamp(c.type, TYPE_MAX),
74
+ ...(typeof c.subject === 'string' && c.subject.trim() ? { subject: flattenClamp(c.subject, SUBJECT_MAX) } : {}),
75
+ claim: flattenClamp(c.claim, CLAIM_MAX),
76
+ ...(typeof c.verdict === 'string' && c.verdict.trim() ? { verdict: flattenClamp(c.verdict, VERDICT_MAX) } : {}),
77
+ executionCoverage: input.coverage,
78
+ confidence,
79
+ evidence,
80
+ artifacts: artifacts.refs,
81
+ sourceSteps: [...input.sourceSteps],
82
+ provenance: { ...input.provenance },
83
+ status: 'active',
84
+ createdAt: input.now,
85
+ };
86
+ const invalid = validateFinding(finding);
87
+ if (invalid) {
88
+ rejected.push({ index, stage: 'finding-validate', why: invalid });
89
+ return;
90
+ }
91
+ // STAGE 4 - PERSIST/THREAD. Bounded, and an excess is a recorded rejection rather than a silent
92
+ // truncation: an observation rides on the persisted Execution, so unbounded data would grow the
93
+ // store without limit.
94
+ if (admitted.length >= FINDING_THREAD_MAX) {
95
+ rejected.push({ index, stage: 'persist', why: `exceeds the ${FINDING_THREAD_MAX}-finding thread bound` });
96
+ return;
97
+ }
98
+ admitted.push(finding);
99
+ });
100
+ return { admitted, rejected, contractViolated, contractDeclared: input.contract !== undefined };
101
+ }
102
+ /** The Finding-validate assertions. Returns the failing one's name, or undefined when it holds. */
103
+ function validateFinding(f) {
104
+ if (!(f.confidence >= 0 && f.confidence <= 1))
105
+ return 'confidence is not in [0,1]';
106
+ if (!(f.executionCoverage >= 0 && f.executionCoverage <= 1))
107
+ return 'executionCoverage is not in [0,1]';
108
+ if (f.sourceSteps.length === 0)
109
+ return 'sourceSteps is empty';
110
+ // A claim with no evidence may exist only if it also claims no belief.
111
+ if (f.evidence.length === 0 && f.confidence !== 0)
112
+ return 'evidence is empty but confidence is not 0';
113
+ if (!f.type)
114
+ return 'type is empty after clamping';
115
+ if (!f.claim)
116
+ return 'claim is empty after clamping';
117
+ return undefined;
118
+ }
119
+ /**
120
+ * THE TASK-FAILURE RULE. An agent task fails on its findings ONLY when the definition declared an
121
+ * output contract: then a violation, or admitting nothing at all, is a contract failure. With no
122
+ * declared contract findings are best-effort - rejections are diagnostics and the step's success is
123
+ * decided by its inner plan alone.
124
+ */
125
+ export function contractFailed(result) {
126
+ if (!result.contractDeclared)
127
+ return false;
128
+ return result.contractViolated || result.admitted.length === 0;
129
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Agent definitions (Phase 3.4) — the OPERATOR-authored shape of a delegable unit of work.
3
+ *
4
+ * A definition is a CEILING, never a grant: its `tools`/`skills` are intersected with the parent's
5
+ * catalog, its `permissions` may only narrow, its caps may only lower, and its `routing` may only add
6
+ * exclusions. Everything that turns a definition into something runnable happens at exactly one place —
7
+ * `narrowEnvelope` in ./envelope.ts. Nothing else may derive an inner catalog or a permission set.
8
+ */
9
+ import type { PermissionPolicy } from '../runtime/policy.js';
10
+ import type { CapabilityRequirement, RoutingPreferences } from '../types.js';
11
+ export interface AgentDefinition {
12
+ /** What this agent is for. Definition-authored, clamped before it reaches a prompt. */
13
+ objective: string;
14
+ /** Tool allowlist, INTERSECTED with the parent catalog — it can never add a tool. */
15
+ tools?: string[];
16
+ /** Skill allowlist, intersected with the parent catalog (and with what the envelope's tools support). */
17
+ skills?: string[];
18
+ /** May only NARROW the parent's permissions (see `clampPermissions`). */
19
+ permissions?: PermissionPolicy;
20
+ /** Per-task caps. A definition may only LOWER the configured default. */
21
+ maxToolCalls?: number;
22
+ maxDurationMs?: number;
23
+ /** The wave-gate reservation AND the worker's hard inner-model-call ceiling — one number, both jobs. */
24
+ maxInnerCalls?: number;
25
+ /** What the agent must produce for its findings to be admitted (see ./admit.ts). */
26
+ outputContract?: {
27
+ types: string[];
28
+ maxFindings?: number;
29
+ requireSubject?: boolean;
30
+ };
31
+ /** Model constraints. `routing` may only narrow the parent's — never re-admit an excluded provider. */
32
+ model?: {
33
+ requirements?: CapabilityRequirement[];
34
+ routing?: RoutingPreferences;
35
+ };
36
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Agent definitions (Phase 3.4) — the OPERATOR-authored shape of a delegable unit of work.
3
+ *
4
+ * A definition is a CEILING, never a grant: its `tools`/`skills` are intersected with the parent's
5
+ * catalog, its `permissions` may only narrow, its caps may only lower, and its `routing` may only add
6
+ * exclusions. Everything that turns a definition into something runnable happens at exactly one place —
7
+ * `narrowEnvelope` in ./envelope.ts. Nothing else may derive an inner catalog or a permission set.
8
+ */
9
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * THE ONE ENFORCEMENT POINT for agent execution (Phase 3.4).
3
+ *
4
+ * `narrowEnvelope` is the single place an inner catalog, a narrowed permission set, the per-task caps,
5
+ * the budget reservation, and the no-nested-agents rule are derived. NEVER re-derive any of them at
6
+ * another call site — the same standing discipline as `Runtime.resolvedPermissions` and
7
+ * `clampMcpPermissions`. It has exactly one caller (`Runtime.agentEnvelopes`) and is deliberately not
8
+ * exported from the public API, so that property is checkable by grep.
9
+ *
10
+ * Everything here NARROWS. A definition is a ceiling: it can remove a tool but never add one, lower a
11
+ * cap but never raise it, drop a permission but never grant one, and add a routing exclusion but never
12
+ * re-admit an excluded provider.
13
+ */
14
+ import type { PermissionPolicy } from '../runtime/policy.js';
15
+ import type { CapabilityRequirement, RoutingPreferences } from '../types.js';
16
+ import type { AgentDefinition } from './definition.js';
17
+ /** The definition-authored objective reaches a model prompt, so it is bounded like any other source text. */
18
+ export declare const OBJECTIVE_MAX = 240;
19
+ export interface AgentEnvelope {
20
+ agentId: string;
21
+ /** Definition-authored, already flattened and clamped. */
22
+ objective: string;
23
+ /** parent ∩ definition, sorted. Agents are NEVER a member — depth 1 is structural. */
24
+ tools: string[];
25
+ /** parent ∩ definition ∩ { skills whose declared tools are all inside `tools` }, sorted. */
26
+ skills: string[];
27
+ /** Fully explicit and ≤ the parent on every field, so a later merge cannot widen it. */
28
+ permissions: PermissionPolicy;
29
+ maxToolCalls: number;
30
+ maxDurationMs: number;
31
+ /** The wave-gate cost AND the worker's hard inner-model-call ceiling. One number, both jobs. */
32
+ reservation: number;
33
+ /** Only ever narrower than the parent's: exclusions union, preferences intersect. */
34
+ routing?: RoutingPreferences;
35
+ requirements?: CapabilityRequirement[];
36
+ }
37
+ export interface NarrowEnvelopeInput {
38
+ agentId: string;
39
+ definition: AgentDefinition;
40
+ parentTools: string[];
41
+ parentSkills: Array<{
42
+ id: string;
43
+ tools?: string[];
44
+ }>;
45
+ parentPermissions: PermissionPolicy;
46
+ parentRouting?: RoutingPreferences;
47
+ defaults: {
48
+ maxToolCalls: number;
49
+ maxDurationMs: number;
50
+ maxInnerCalls: number;
51
+ };
52
+ }
53
+ export declare function narrowEnvelope(input: NarrowEnvelopeInput): AgentEnvelope;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * THE ONE ENFORCEMENT POINT for agent execution (Phase 3.4).
3
+ *
4
+ * `narrowEnvelope` is the single place an inner catalog, a narrowed permission set, the per-task caps,
5
+ * the budget reservation, and the no-nested-agents rule are derived. NEVER re-derive any of them at
6
+ * another call site — the same standing discipline as `Runtime.resolvedPermissions` and
7
+ * `clampMcpPermissions`. It has exactly one caller (`Runtime.agentEnvelopes`) and is deliberately not
8
+ * exported from the public API, so that property is checkable by grep.
9
+ *
10
+ * Everything here NARROWS. A definition is a ceiling: it can remove a tool but never add one, lower a
11
+ * cap but never raise it, drop a permission but never grant one, and add a routing exclusion but never
12
+ * re-admit an excluded provider.
13
+ */
14
+ import { clampPermissions } from '../tools/permissions.js';
15
+ import { flattenClamp } from '../util/flatten.js';
16
+ /** The definition-authored objective reaches a model prompt, so it is bounded like any other source text. */
17
+ export const OBJECTIVE_MAX = 240;
18
+ /** A cap a definition may only LOWER, never raise, and never below 1. */
19
+ function lowerOnly(deflt, requested) {
20
+ return Math.max(1, Math.min(deflt, requested ?? deflt));
21
+ }
22
+ const uniqSorted = (xs) => [...new Set(xs)].sort();
23
+ export function narrowEnvelope(input) {
24
+ const { definition: def, defaults } = input;
25
+ // (1) Tools: intersect with the parent. A definition entry naming something the parent does not have
26
+ // is simply absent — it can never ADD a tool.
27
+ const parentTools = new Set(input.parentTools);
28
+ const tools = uniqSorted((def.tools ?? input.parentTools).filter((t) => parentTools.has(t)));
29
+ // (2) Skills: intersect with the parent, then drop any skill that needs a tool outside the envelope.
30
+ // That second clause is load-bearing, not tidiness: a skill's own `callTool` resolves straight off the
31
+ // Runtime's registry with no allowlist check, so admitting a skill whose declared tools escape the
32
+ // envelope would be a hole. Excluding it is the structural fix; the worker's membership check is
33
+ // defense in depth.
34
+ const inner = new Set(tools);
35
+ const allowedSkills = def.skills ? new Set(def.skills) : undefined;
36
+ const skills = uniqSorted(input.parentSkills.filter((s) => (!allowedSkills || allowedSkills.has(s.id)) && (s.tools ?? []).every((t) => inner.has(t))).map((s) => s.id));
37
+ // (3) Permissions: minimum-merged and fully explicit (see `clampPermissions`).
38
+ const permissions = clampPermissions(input.parentPermissions, def.permissions);
39
+ // (6) Routing: exclusions only ever GROW, preferences only ever shrink, so an agent can never
40
+ // re-admit a provider the parent excluded, nor reach past a privacy or policy decision.
41
+ const pr = input.parentRouting;
42
+ const dr = def.model?.routing;
43
+ const routing = pr || dr
44
+ ? {
45
+ ...(pr?.excludeProviders || dr?.excludeProviders ? { excludeProviders: uniqSorted([...(pr?.excludeProviders ?? []), ...(dr?.excludeProviders ?? [])]) } : {}),
46
+ ...(pr?.excludeModels || dr?.excludeModels ? { excludeModels: uniqSorted([...(pr?.excludeModels ?? []), ...(dr?.excludeModels ?? [])]) } : {}),
47
+ ...(pr?.preferProviders ? { preferProviders: dr?.preferProviders ? pr.preferProviders.filter((x) => dr.preferProviders.includes(x)) : [...pr.preferProviders] } : dr?.preferProviders ? { preferProviders: [...dr.preferProviders] } : {}),
48
+ ...(pr?.preferModels ? { preferModels: dr?.preferModels ? pr.preferModels.filter((x) => dr.preferModels.includes(x)) : [...pr.preferModels] } : dr?.preferModels ? { preferModels: [...dr.preferModels] } : {}),
49
+ }
50
+ : undefined;
51
+ return {
52
+ agentId: input.agentId,
53
+ objective: flattenClamp(def.objective, OBJECTIVE_MAX),
54
+ tools,
55
+ skills,
56
+ permissions,
57
+ // (4) Caps: a definition may only lower.
58
+ maxToolCalls: lowerOnly(defaults.maxToolCalls, def.maxToolCalls),
59
+ maxDurationMs: lowerOnly(defaults.maxDurationMs, def.maxDurationMs),
60
+ // (5) The reservation is the same shape of number, and doubles as the hard inner-call ceiling.
61
+ reservation: lowerOnly(defaults.maxInnerCalls, def.maxInnerCalls),
62
+ ...(routing && Object.keys(routing).length ? { routing } : {}),
63
+ // Requirements ADD to the parent's — more requirements is a narrower candidate set.
64
+ ...(def.model?.requirements?.length ? { requirements: [...def.model.requirements] } : {}),
65
+ };
66
+ // (7) There is no `agents` field, by construction: depth 1 is structural, not a runtime check. The
67
+ // worker plans with `agents: []`, so validatePlan rejects any nested agent step as `unknown agent`.
68
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The Finding contract (Phase 3.4) - what an agent is allowed to tell the rest of the system.
3
+ *
4
+ * TWO NUMBERS, NEVER CONFLATED:
5
+ * - `executionCoverage` is how much of the agent's plan actually ran. Deterministic, read from step
6
+ * statuses, and NOT a belief about the claim.
7
+ * - `confidence` is evidence-based belief in the claim. Conflict resolution weighs ONLY this;
8
+ * coverage may filter or break a tie, never contribute to the weight.
9
+ *
10
+ * NAMING: the entry type is `FindingEvidence`, not `Evidence` - that name is already the router's
11
+ * capability-evidence RANK union (src/types.ts), and `EvidenceResult` belongs to comparison. The same
12
+ * class of collision as `Tool.provides` and `agentTaskId`.
13
+ */
14
+ import type { ArtifactRef } from '../runtime/types.js';
15
+ import type { PlanStep } from '../orchestration/plan.js';
16
+ export type FindingStatus = 'active' | 'superseded' | 'contradicted';
17
+ export type FindingEvidenceKind = 'validation' | 'observation' | 'tool-output' | 'artifact';
18
+ export interface FindingEvidence {
19
+ kind: FindingEvidenceKind;
20
+ /** The INNER step id this entry came from. */
21
+ stepId: string;
22
+ /** Clamped at construction. Fenced again at render; never stored fenced. */
23
+ detail: string;
24
+ ok: boolean;
25
+ /** `validation` only: the check tally that backs the belief. */
26
+ checks?: {
27
+ passed: number;
28
+ total: number;
29
+ };
30
+ /** Set when the backing artifact could not be written - the DOWNGRADE marker. */
31
+ unavailable?: boolean;
32
+ }
33
+ export interface Finding {
34
+ id: string;
35
+ agentId: string;
36
+ /** `agentTaskId`, NEVER `taskId` - that name belongs to the router. */
37
+ agentTaskId: string;
38
+ type: string;
39
+ subject?: string;
40
+ claim: string;
41
+ verdict?: string;
42
+ /** Deterministic plan completion, NOT belief. */
43
+ executionCoverage: number;
44
+ /** Evidence-based belief in the claim. */
45
+ confidence: number;
46
+ evidence: FindingEvidence[];
47
+ artifacts: ArtifactRef[];
48
+ sourceSteps: string[];
49
+ provenance: {
50
+ executionId?: string;
51
+ planVersion: number;
52
+ };
53
+ status: FindingStatus;
54
+ supersededBy?: string;
55
+ createdAt: number;
56
+ }
57
+ /** Two decimals, so a JSON round-trip is stable and an equality assertion means something. */
58
+ export declare function round2(n: number): number;
59
+ /**
60
+ * How much of the agent's inner plan succeeded. Reads step STATUSES only - it never consults evidence
61
+ * and is never derived from confidence.
62
+ */
63
+ export declare function executionCoverage(steps: PlanStep[]): number;
64
+ /**
65
+ * Evidence-based belief. The rubric, in order:
66
+ * - no evidence => 0, always. This guard precedes the override, so a finding with nothing behind it
67
+ * can never claim belief.
68
+ * - at least one usable `validation` entry => up to 1.0, scaled by validated/total CHECKS.
69
+ * - observation / tool-output only => capped at 0.5.
70
+ * - a final inner step MAY supply a structured override (`SkillResult.data.confidence`), clamped
71
+ * 0..1. That is a skill-authored judgment, still not model prose.
72
+ */
73
+ export declare function confidenceOf(ev: FindingEvidence[], override?: unknown): number;
74
+ /**
75
+ * Resolve conflicts among active findings, weighing ONLY `confidence`. Two findings conflict when they
76
+ * share a `type` + `subject`: a differing verdict makes the loser `contradicted`, and otherwise the
77
+ * loser is `superseded`. `executionCoverage` breaks a tie and never enters the weight.
78
+ */
79
+ export declare function resolveConflicts(findings: Finding[]): Finding[];
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The Finding contract (Phase 3.4) - what an agent is allowed to tell the rest of the system.
3
+ *
4
+ * TWO NUMBERS, NEVER CONFLATED:
5
+ * - `executionCoverage` is how much of the agent's plan actually ran. Deterministic, read from step
6
+ * statuses, and NOT a belief about the claim.
7
+ * - `confidence` is evidence-based belief in the claim. Conflict resolution weighs ONLY this;
8
+ * coverage may filter or break a tie, never contribute to the weight.
9
+ *
10
+ * NAMING: the entry type is `FindingEvidence`, not `Evidence` - that name is already the router's
11
+ * capability-evidence RANK union (src/types.ts), and `EvidenceResult` belongs to comparison. The same
12
+ * class of collision as `Tool.provides` and `agentTaskId`.
13
+ */
14
+ /** Two decimals, so a JSON round-trip is stable and an equality assertion means something. */
15
+ export function round2(n) {
16
+ return Math.round(n * 100) / 100;
17
+ }
18
+ /**
19
+ * How much of the agent's inner plan succeeded. Reads step STATUSES only - it never consults evidence
20
+ * and is never derived from confidence.
21
+ */
22
+ export function executionCoverage(steps) {
23
+ if (steps.length === 0)
24
+ return 0;
25
+ return round2(steps.filter((s) => s.status === 'succeeded').length / steps.length);
26
+ }
27
+ /**
28
+ * Evidence-based belief. The rubric, in order:
29
+ * - no evidence => 0, always. This guard precedes the override, so a finding with nothing behind it
30
+ * can never claim belief.
31
+ * - at least one usable `validation` entry => up to 1.0, scaled by validated/total CHECKS.
32
+ * - observation / tool-output only => capped at 0.5.
33
+ * - a final inner step MAY supply a structured override (`SkillResult.data.confidence`), clamped
34
+ * 0..1. That is a skill-authored judgment, still not model prose.
35
+ */
36
+ export function confidenceOf(ev, override) {
37
+ if (ev.length === 0)
38
+ return 0;
39
+ const validations = ev.filter((e) => e.kind === 'validation' && !e.unavailable && (e.checks?.total ?? 0) > 0);
40
+ let c;
41
+ if (validations.length >= 1) {
42
+ const passed = validations.reduce((n, e) => n + e.checks.passed, 0);
43
+ const total = validations.reduce((n, e) => n + e.checks.total, 0);
44
+ c = Math.min(1, passed / total);
45
+ }
46
+ else {
47
+ c = (ev.filter((e) => e.ok && !e.unavailable).length / ev.length) * 0.5;
48
+ }
49
+ if (typeof override === 'number' && Number.isFinite(override))
50
+ c = Math.min(1, Math.max(0, override));
51
+ return round2(c);
52
+ }
53
+ /**
54
+ * Resolve conflicts among active findings, weighing ONLY `confidence`. Two findings conflict when they
55
+ * share a `type` + `subject`: a differing verdict makes the loser `contradicted`, and otherwise the
56
+ * loser is `superseded`. `executionCoverage` breaks a tie and never enters the weight.
57
+ */
58
+ export function resolveConflicts(findings) {
59
+ const groups = new Map();
60
+ for (const f of findings) {
61
+ if (!f.subject)
62
+ continue; // no conflict key => nothing to resolve against
63
+ const key = `${f.type}\u0000${f.subject}`;
64
+ groups.set(key, [...(groups.get(key) ?? []), f]);
65
+ }
66
+ const out = new Map(findings.map((f) => [f.id, { ...f }]));
67
+ for (const group of groups.values()) {
68
+ if (group.length < 2)
69
+ continue;
70
+ const ranked = [...group].sort((a, b) => b.confidence - a.confidence || b.executionCoverage - a.executionCoverage || (a.id < b.id ? -1 : 1));
71
+ const winner = ranked[0];
72
+ for (const loser of ranked.slice(1)) {
73
+ const row = out.get(loser.id);
74
+ const differingVerdict = loser.verdict !== undefined && winner.verdict !== undefined && loser.verdict !== winner.verdict;
75
+ row.status = differingVerdict ? 'contradicted' : 'superseded';
76
+ row.supersededBy = winner.id;
77
+ }
78
+ }
79
+ return [...out.values()];
80
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Agent task state (Phase 3.4) - the record, and THE PROJECTION TABLE.
3
+ *
4
+ * The state set is the PRD's; no state is invented here. Every one of them projects onto the EXISTING
5
+ * PlanStepStatus / ExecutionStatus vocabularies, and that is deliberate: 3.4 adds no new status
6
+ * anywhere, which is what stops a later phase from needing one.
7
+ *
8
+ * An INTERRUPTION IS NOT A STATE. A crash, a parent pause, or a parent cancel leaves the task in a
9
+ * normal state and records WHY in `interruption`, so the reason is auditable without growing the
10
+ * lifecycle.
11
+ */
12
+ import type { PlanStepStatus } from '../orchestration/plan.js';
13
+ import type { StepObservationCode } from '../orchestration/executor.js';
14
+ import type { ExecutionStatus } from '../executions/execution.js';
15
+ import type { AdmissionRejection } from './admit.js';
16
+ import type { Finding } from './finding.js';
17
+ export type AgentTaskState = 'created' | 'queued' | 'running' | 'completed' | 'failed' | 'cancelled' | 'waiting_for_input' | 'waiting_for_clarification' | 'paused';
18
+ export interface AgentTaskRecord {
19
+ /** `ag_<base36 now>_<counter>`. `agentTaskId`, NEVER `taskId` - that name belongs to the router. */
20
+ agentTaskId: string;
21
+ agentId: string;
22
+ stepId: string;
23
+ state: AgentTaskState;
24
+ createdAt: number;
25
+ startedAt?: number;
26
+ endedAt?: number;
27
+ /** Additive, and NOT a state. Carries why a non-terminal task stopped. */
28
+ interruption?: {
29
+ kind: 'crash' | 'pause' | 'parent-cancel';
30
+ at: number;
31
+ detail?: string;
32
+ };
33
+ innerPlanVersion?: number;
34
+ innerSteps: {
35
+ total: number;
36
+ succeeded: number;
37
+ };
38
+ callsUsed: number;
39
+ toolCallsUsed: number;
40
+ findings: Finding[];
41
+ /** Candidates the admission pipeline REFUSED. Auditable, and never threaded anywhere. */
42
+ diagnostics: AdmissionRejection[];
43
+ failure?: {
44
+ code: StepObservationCode;
45
+ message: string;
46
+ };
47
+ }
48
+ /** One row per agent-task state. Read this table; never re-derive a projection at a call site. */
49
+ export interface ProjectionRow {
50
+ step: PlanStepStatus;
51
+ exec: ExecutionStatus;
52
+ code?: StepObservationCode;
53
+ /** False for the states only a persisted, resumable task can reach (Phase 3.5). */
54
+ reachableIn34: boolean;
55
+ }
56
+ export declare const AGENT_TASK_PROJECTION: Readonly<Record<AgentTaskState, ProjectionRow>>;
57
+ /** The states an agent task can actually reach in 3.4. */
58
+ export declare const REACHABLE_IN_34: AgentTaskState[];
59
+ /** Mint an agent task id. Base36 clock + a counter, so ids are stable under a fake clock. */
60
+ export declare function nextAgentTaskId(now: number): string;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Agent task state (Phase 3.4) - the record, and THE PROJECTION TABLE.
3
+ *
4
+ * The state set is the PRD's; no state is invented here. Every one of them projects onto the EXISTING
5
+ * PlanStepStatus / ExecutionStatus vocabularies, and that is deliberate: 3.4 adds no new status
6
+ * anywhere, which is what stops a later phase from needing one.
7
+ *
8
+ * An INTERRUPTION IS NOT A STATE. A crash, a parent pause, or a parent cancel leaves the task in a
9
+ * normal state and records WHY in `interruption`, so the reason is auditable without growing the
10
+ * lifecycle.
11
+ */
12
+ export const AGENT_TASK_PROJECTION = {
13
+ created: { step: 'pending', exec: 'running', reachableIn34: true },
14
+ queued: { step: 'pending', exec: 'running', reachableIn34: true },
15
+ running: { step: 'running', exec: 'running', reachableIn34: true },
16
+ completed: { step: 'succeeded', exec: 'completed', reachableIn34: true },
17
+ failed: { step: 'failed', exec: 'failed', code: 'agent-failed', reachableIn34: true },
18
+ cancelled: { step: 'skipped', exec: 'cancelled', code: 'cancelled', reachableIn34: true },
19
+ // Declared, not reachable in 3.4: nothing persists an AgentTaskRecord yet, so nothing can be resumed
20
+ // into a waiting state. The rows exist so 3.5 inherits a mapping instead of inventing one.
21
+ waiting_for_input: { step: 'pending', exec: 'waiting_for_input', reachableIn34: false },
22
+ waiting_for_clarification: { step: 'pending', exec: 'waiting_for_clarification', reachableIn34: false },
23
+ paused: { step: 'pending', exec: 'paused', reachableIn34: false },
24
+ };
25
+ /** The states an agent task can actually reach in 3.4. */
26
+ export const REACHABLE_IN_34 = Object.keys(AGENT_TASK_PROJECTION).filter((s) => AGENT_TASK_PROJECTION[s].reachableIn34);
27
+ let counter = 0;
28
+ /** Mint an agent task id. Base36 clock + a counter, so ids are stable under a fake clock. */
29
+ export function nextAgentTaskId(now) {
30
+ counter += 1;
31
+ return `ag_${now.toString(36)}_${counter.toString(36)}`;
32
+ }