ai-runtime-engine 1.2.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 (153) hide show
  1. package/CHANGELOG.md +602 -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 +56 -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 +2 -0
  34. package/dist/cli/commands/run.js +36 -4
  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 +105 -16
  43. package/dist/cli/interactive/session.d.ts +12 -1
  44. package/dist/cli/interactive/session.js +83 -5
  45. package/dist/cli/render.d.ts +13 -0
  46. package/dist/cli/render.js +18 -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 +8 -0
  58. package/dist/core/fallback/fallback.js +3 -1
  59. package/dist/core/router/executor.d.ts +6 -1
  60. package/dist/core/router/executor.js +9 -2
  61. package/dist/core/router/normalize.d.ts +2 -0
  62. package/dist/core/router/request.js +2 -0
  63. package/dist/core/router/router.d.ts +3 -0
  64. package/dist/core/router/router.js +7 -0
  65. package/dist/executions/execution.d.ts +13 -2
  66. package/dist/generation/generateAdapter.d.ts +14 -0
  67. package/dist/generation/generateAdapter.js +38 -0
  68. package/dist/generation/generateSkill.d.ts +26 -0
  69. package/dist/generation/generateSkill.js +51 -0
  70. package/dist/index.d.ts +44 -5
  71. package/dist/index.js +26 -2
  72. package/dist/mcp/client.d.ts +70 -0
  73. package/dist/mcp/client.js +221 -0
  74. package/dist/mcp/manager.d.ts +151 -0
  75. package/dist/mcp/manager.js +493 -0
  76. package/dist/mcp/protocol.d.ts +216 -0
  77. package/dist/mcp/protocol.js +149 -0
  78. package/dist/mcp/toolAdapter.d.ts +44 -0
  79. package/dist/mcp/toolAdapter.js +94 -0
  80. package/dist/mcp/transport.d.ts +109 -0
  81. package/dist/mcp/transport.js +383 -0
  82. package/dist/memory/embedders/hash.d.ts +12 -0
  83. package/dist/memory/embedders/hash.js +31 -0
  84. package/dist/memory/embedders/http.d.ts +25 -0
  85. package/dist/memory/embedders/http.js +48 -0
  86. package/dist/memory/memory.d.ts +19 -2
  87. package/dist/memory/memory.js +75 -11
  88. package/dist/memory/semantic.d.ts +17 -0
  89. package/dist/memory/semantic.js +29 -0
  90. package/dist/orchestration/budget.d.ts +30 -0
  91. package/dist/orchestration/budget.js +40 -0
  92. package/dist/orchestration/executor.d.ts +39 -1
  93. package/dist/orchestration/executor.js +64 -4
  94. package/dist/orchestration/orchestrator.d.ts +29 -1
  95. package/dist/orchestration/orchestrator.js +89 -8
  96. package/dist/orchestration/plan.d.ts +15 -1
  97. package/dist/orchestration/plan.js +23 -4
  98. package/dist/orchestration/planner.d.ts +19 -1
  99. package/dist/orchestration/planner.js +25 -5
  100. package/dist/plugin/ai.d.ts +4 -0
  101. package/dist/plugin/ai.js +9 -0
  102. package/dist/providers/httpClient.d.ts +25 -1
  103. package/dist/providers/httpClient.js +93 -0
  104. package/dist/providers/httpProvider.d.ts +1 -0
  105. package/dist/providers/httpProvider.js +67 -1
  106. package/dist/providers/mock/mockProvider.d.ts +3 -0
  107. package/dist/providers/mock/mockProvider.js +54 -0
  108. package/dist/providers/mock/scenarios.d.ts +7 -0
  109. package/dist/providers/provider.d.ts +6 -0
  110. package/dist/providers/wire/anthropicWire.js +34 -0
  111. package/dist/providers/wire/openaiWire.js +30 -0
  112. package/dist/providers/wire/types.d.ts +16 -0
  113. package/dist/runtime/config.js +50 -6
  114. package/dist/runtime/intent/aiClassifier.d.ts +19 -0
  115. package/dist/runtime/intent/aiClassifier.js +74 -0
  116. package/dist/runtime/models/modelProfile.d.ts +61 -0
  117. package/dist/runtime/models/modelProfile.js +139 -0
  118. package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
  119. package/dist/runtime/planning/deriveCapabilities.js +146 -0
  120. package/dist/runtime/policy.d.ts +10 -0
  121. package/dist/runtime/policy.js +9 -2
  122. package/dist/runtime/runtime.d.ts +173 -0
  123. package/dist/runtime/runtime.js +723 -50
  124. package/dist/runtime/types.d.ts +94 -2
  125. package/dist/skills/manifest.d.ts +3 -0
  126. package/dist/skills/manifest.js +24 -0
  127. package/dist/skills/registry.d.ts +16 -1
  128. package/dist/skills/registry.js +21 -1
  129. package/dist/skills/skill.d.ts +6 -1
  130. package/dist/store/area.d.ts +15 -1
  131. package/dist/store/area.js +19 -8
  132. package/dist/store/crypto.d.ts +21 -0
  133. package/dist/store/crypto.js +49 -0
  134. package/dist/store/paths.d.ts +5 -1
  135. package/dist/store/paths.js +6 -0
  136. package/dist/store/store.d.ts +15 -3
  137. package/dist/store/store.js +28 -7
  138. package/dist/telemetry/sinks/otlp.d.ts +31 -0
  139. package/dist/telemetry/sinks/otlp.js +76 -0
  140. package/dist/tools/builtins/filesystem.js +1 -0
  141. package/dist/tools/builtins/git.js +1 -0
  142. package/dist/tools/builtins/shell.js +1 -0
  143. package/dist/tools/permissions.d.ts +28 -0
  144. package/dist/tools/permissions.js +72 -0
  145. package/dist/tools/registry.d.ts +18 -2
  146. package/dist/tools/registry.js +22 -2
  147. package/dist/tools/tool.d.ts +4 -0
  148. package/dist/types.d.ts +11 -1
  149. package/dist/util/flatten.d.ts +11 -0
  150. package/dist/util/flatten.js +18 -0
  151. package/dist/util/semaphore.d.ts +19 -0
  152. package/dist/util/semaphore.js +60 -0
  153. package/package.json +24 -9
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The agent worker (Phase 3.4) - runs ONE agent step.
3
+ *
4
+ * IT NEVER CALLS `orchestrate()`. It composes `generatePlan` + `executePlan` directly, because
5
+ * re-entering the orchestrator inside a step would re-run the parent's gates per step: re-asking for
6
+ * approval, re-applying the parent call budget, and re-checking plan/dry-run mode. A useful consequence
7
+ * of that same structure is that a dry run can never reach a worker at all - `orchestrate` returns
8
+ * before `executePlan` is ever called.
9
+ *
10
+ * IT NEVER CONSTRUCTS AN `AI`. One router: the inner planning call goes through a METERED FACADE over
11
+ * the parent's AI, so every inner model call is counted against the envelope's reservation - including
12
+ * the ones a skill makes through `SkillContext.ai`, which the parent budget cannot see today.
13
+ *
14
+ * DEPTH 1 IS STRUCTURAL: the inner plan is generated with `agents: []`, so `validatePlan` rejects a
15
+ * nested agent step as `unknown agent`. There is no runtime depth counter to get wrong.
16
+ */
17
+ import { AI } from '../plugin/ai.js';
18
+ import type { AgentTaskRecord } from './task.js';
19
+ import type { AgentEnvelope } from './envelope.js';
20
+ import type { AgentDefinition } from './definition.js';
21
+ import type { PlanStep } from '../orchestration/plan.js';
22
+ import type { StepObservation } from '../orchestration/executor.js';
23
+ import type { Skill, SkillResult, SkillValidation } from '../skills/skill.js';
24
+ import type { ToolResult } from '../tools/tool.js';
25
+ import type { ArtifactRef } from '../runtime/types.js';
26
+ import type { PermissionPolicy } from '../runtime/policy.js';
27
+ import type { Clock } from '../util/clock.js';
28
+ /** How much of a step input may reach the inner prompt. It is model-authored, and it is fenced. */
29
+ export declare const AGENT_INPUT_MAX = 1000;
30
+ export interface AgentWorkerDeps {
31
+ ai: AI;
32
+ clock: Clock;
33
+ /** Narrowed to the envelope by the caller; the worker does not re-derive a catalog. */
34
+ skills: Skill[];
35
+ runSkill: (id: string, input: unknown, overrides: {
36
+ permissions: PermissionPolicy;
37
+ signal?: AbortSignal;
38
+ ai?: AI;
39
+ }) => Promise<{
40
+ result: SkillResult;
41
+ validation: SkillValidation;
42
+ }>;
43
+ runTool: (id: string, input: unknown, overrides: {
44
+ permissions: PermissionPolicy;
45
+ signal?: AbortSignal;
46
+ }) => Promise<ToolResult>;
47
+ /** Store evidence content. Returns `unavailable` rather than a dangling ref when the store is off. */
48
+ putArtifact: (content: string, source: string) => {
49
+ ref?: ArtifactRef;
50
+ unavailable: boolean;
51
+ };
52
+ emit?: (event: {
53
+ type: 'agent.task.started' | 'agent.task.completed';
54
+ record: AgentTaskRecord;
55
+ }) => void;
56
+ parentSignal?: AbortSignal;
57
+ /** Why the parent aborted, when it knows. Distinguishes a cancel from a pause on the record. */
58
+ abortReason?: () => 'pause' | 'parent-cancel' | undefined;
59
+ provenance: {
60
+ executionId?: string;
61
+ planVersion: number;
62
+ };
63
+ }
64
+ export interface RunAgentTaskResult {
65
+ observation: StepObservation;
66
+ record: AgentTaskRecord;
67
+ }
68
+ export declare function runAgentTask(step: PlanStep, envelope: AgentEnvelope, definition: AgentDefinition, deps: AgentWorkerDeps): Promise<RunAgentTaskResult>;
@@ -0,0 +1,256 @@
1
+ /**
2
+ * The agent worker (Phase 3.4) - runs ONE agent step.
3
+ *
4
+ * IT NEVER CALLS `orchestrate()`. It composes `generatePlan` + `executePlan` directly, because
5
+ * re-entering the orchestrator inside a step would re-run the parent's gates per step: re-asking for
6
+ * approval, re-applying the parent call budget, and re-checking plan/dry-run mode. A useful consequence
7
+ * of that same structure is that a dry run can never reach a worker at all - `orchestrate` returns
8
+ * before `executePlan` is ever called.
9
+ *
10
+ * IT NEVER CONSTRUCTS AN `AI`. One router: the inner planning call goes through a METERED FACADE over
11
+ * the parent's AI, so every inner model call is counted against the envelope's reservation - including
12
+ * the ones a skill makes through `SkillContext.ai`, which the parent budget cannot see today.
13
+ *
14
+ * DEPTH 1 IS STRUCTURAL: the inner plan is generated with `agents: []`, so `validatePlan` rejects a
15
+ * nested agent step as `unknown agent`. There is no runtime depth counter to get wrong.
16
+ */
17
+ import { generatePlan } from '../orchestration/planner.js';
18
+ import { executePlan } from '../orchestration/executor.js';
19
+ import { wrapUntrusted } from '../tools/untrusted.js';
20
+ import { flattenClamp } from '../util/flatten.js';
21
+ import { admitFindings, contractFailed } from './admit.js';
22
+ import { executionCoverage } from './finding.js';
23
+ import { AGENT_TASK_PROJECTION, nextAgentTaskId } from './task.js';
24
+ /** How much of a step input may reach the inner prompt. It is model-authored, and it is fenced. */
25
+ export const AGENT_INPUT_MAX = 1000;
26
+ /** Raised by the metered facade when an agent tries to exceed its reservation. */
27
+ class AgentCallBudgetError extends Error {
28
+ constructor() {
29
+ super('agent inner model-call reservation exhausted');
30
+ this.name = 'AgentCallBudgetError';
31
+ }
32
+ }
33
+ export async function runAgentTask(step, envelope, definition, deps) {
34
+ const now = deps.clock.now();
35
+ const record = {
36
+ agentTaskId: nextAgentTaskId(now),
37
+ agentId: envelope.agentId,
38
+ stepId: step.id,
39
+ state: 'created',
40
+ createdAt: now,
41
+ innerSteps: { total: 0, succeeded: 0 },
42
+ callsUsed: 0,
43
+ toolCallsUsed: 0,
44
+ findings: [],
45
+ diagnostics: [],
46
+ };
47
+ record.state = 'queued';
48
+ // (2) A child controller, so a parent abort reaches the inner run and nothing else does.
49
+ const child = new AbortController();
50
+ const onParentAbort = () => child.abort();
51
+ if (deps.parentSignal?.aborted)
52
+ child.abort();
53
+ else
54
+ deps.parentSignal?.addEventListener('abort', onParentAbort, { once: true });
55
+ const deadline = deps.clock.now() + envelope.maxDurationMs;
56
+ const expired = () => deps.clock.now() > deadline;
57
+ // (3) THE METERED FACADE. Prototype-delegating, so it IS an AI (assignable to SkillContext.ai) while
58
+ // owning `run`. The count increments BEFORE the await, so a failed inner call is still charged.
59
+ let calls = 0;
60
+ const metered = Object.create(deps.ai);
61
+ metered.run = async (req) => {
62
+ if (calls >= envelope.reservation)
63
+ throw new AgentCallBudgetError();
64
+ calls += 1;
65
+ return deps.ai.run(req);
66
+ };
67
+ let toolCalls = 0;
68
+ const toolResults = [];
69
+ const skillResults = [];
70
+ const finish = (state, failure) => {
71
+ deps.parentSignal?.removeEventListener('abort', onParentAbort);
72
+ record.state = state;
73
+ record.endedAt = deps.clock.now();
74
+ record.callsUsed = calls;
75
+ record.toolCallsUsed = toolCalls;
76
+ if (failure)
77
+ record.failure = failure;
78
+ const row = AGENT_TASK_PROJECTION[state];
79
+ const observation = {
80
+ stepId: step.id,
81
+ agent: envelope.agentId,
82
+ agentTaskId: record.agentTaskId,
83
+ ok: state === 'completed',
84
+ callsUsed: calls,
85
+ ...(failure ? { code: failure.code, error: failure.message } : row.code ? { code: row.code } : {}),
86
+ ...(record.findings.length ? { data: { findings: record.findings } } : {}),
87
+ ...(record.findings.some((f) => f.artifacts.length) ? { artifacts: record.findings.flatMap((f) => f.artifacts) } : {}),
88
+ ...(state === 'completed' ? { output: `${envelope.agentId}: ${record.innerSteps.succeeded}/${record.innerSteps.total} inner step(s), ${record.findings.length} finding(s)` } : {}),
89
+ };
90
+ deps.emit?.({ type: 'agent.task.completed', record });
91
+ return { observation, record };
92
+ };
93
+ // (12) A parent abort is a cancel or a pause; both project to the same observation, and the record
94
+ // carries which so a later phase can re-queue a paused task rather than treat it as cancelled.
95
+ const abortedNow = () => {
96
+ if (!child.signal.aborted)
97
+ return undefined;
98
+ const kind = deps.abortReason?.() ?? 'parent-cancel';
99
+ if (kind === 'pause') {
100
+ record.interruption = { kind: 'pause', at: deps.clock.now() };
101
+ return finish('queued', { code: 'cancelled', message: 'cancelled: the run was paused' });
102
+ }
103
+ record.interruption = { kind: 'parent-cancel', at: deps.clock.now() };
104
+ return finish('cancelled', { code: 'cancelled', message: 'cancelled: the run was aborted' });
105
+ };
106
+ const preempted = abortedNow();
107
+ if (preempted)
108
+ return preempted;
109
+ record.state = 'running';
110
+ record.startedAt = deps.clock.now();
111
+ deps.emit?.({ type: 'agent.task.started', record });
112
+ // (6) THE TOOL SEAM - defense in depth behind narrowEnvelope. Always a structured denial, never a
113
+ // throw: the tool contract is that a refusal is visible.
114
+ const innerCallTool = async (id, input) => {
115
+ if (!envelope.tools.includes(id))
116
+ return { ok: false, error: { code: 'PERMISSION', message: `'${flattenClamp(id, 40)}' is not in this agent's catalog` } };
117
+ if (toolCalls + 1 > envelope.maxToolCalls)
118
+ return { ok: false, error: { code: 'PERMISSION', message: `agent tool-call budget exhausted (${envelope.maxToolCalls})` } };
119
+ if (expired())
120
+ return { ok: false, error: { code: 'TIMEOUT', message: 'agent time budget exhausted' } };
121
+ toolCalls += 1;
122
+ const result = await deps.runTool(id, input, { permissions: envelope.permissions, signal: child.signal });
123
+ return result;
124
+ };
125
+ // (4) PROMPT BOUNDARY #1: the objective is definition-authored and clamped; the step input is
126
+ // MODEL-authored and therefore fenced as untrusted data.
127
+ const innerGoal = [
128
+ envelope.objective,
129
+ wrapUntrusted(`agent-input:${envelope.agentId}`, flattenClamp(JSON.stringify(step.input ?? null), AGENT_INPUT_MAX)),
130
+ ].join('\n\n');
131
+ let planned;
132
+ try {
133
+ planned = await generatePlan({ goal: innerGoal, ai: metered, skills: deps.skills, tools: envelope.tools, agents: [], version: 1, ...(envelope.routing ? { routing: envelope.routing } : {}) });
134
+ }
135
+ catch (err) {
136
+ if (err instanceof AgentCallBudgetError)
137
+ return finish('failed', { code: 'agent-call-budget', message: err.message });
138
+ return finish('failed', { code: 'agent-failed', message: flattenClamp(err instanceof Error ? err.message : String(err), 160) });
139
+ }
140
+ if (!planned.plan) {
141
+ return finish('failed', { code: 'agent-failed', message: flattenClamp(planned.error ?? planned.clarification ?? 'the agent could not form a plan', 160) });
142
+ }
143
+ record.innerPlanVersion = planned.plan.version;
144
+ const aborted = abortedNow();
145
+ if (aborted)
146
+ return aborted;
147
+ if (expired())
148
+ return finish('failed', { code: 'agent-timeout', message: `agent exceeded ${envelope.maxDurationMs}ms` });
149
+ // (5) Inner execution. NO callBudget: the facade in (3) is a strictly tighter ceiling and also covers
150
+ // a skill's own model calls, which a wave gate cannot see.
151
+ let inner;
152
+ try {
153
+ inner = await executePlan(planned.plan, {
154
+ runSkill: async (id, i) => {
155
+ const out = await deps.runSkill(id, i, { permissions: envelope.permissions, signal: child.signal, ai: metered });
156
+ skillResults.push({ stepId: id, result: out.result, validation: out.validation });
157
+ return out;
158
+ },
159
+ runTool: async (id, i) => {
160
+ const result = await innerCallTool(id, i);
161
+ toolResults.push({ stepId: id, result });
162
+ return result;
163
+ },
164
+ maxParallelSteps: 1,
165
+ signal: child.signal,
166
+ });
167
+ }
168
+ catch (err) {
169
+ if (err instanceof AgentCallBudgetError)
170
+ return finish('failed', { code: 'agent-call-budget', message: err.message });
171
+ return finish('failed', { code: 'agent-failed', message: flattenClamp(err instanceof Error ? err.message : String(err), 160) });
172
+ }
173
+ record.innerSteps = { total: inner.plan.steps.length, succeeded: inner.plan.steps.filter((s) => s.status === 'succeeded').length };
174
+ const afterRun = abortedNow();
175
+ if (afterRun)
176
+ return afterRun;
177
+ // (8) Deterministic coverage, from statuses only.
178
+ const coverage = executionCoverage(inner.plan.steps);
179
+ // (10) Evidence: one entry per inner step, keyed STRUCTURALLY by what the step produced. A skill's
180
+ // validation checks are free-form names, so there is nothing to parse - the kind comes from the fact
181
+ // that a SkillValidation exists at all.
182
+ const evidence = [
183
+ ...skillResults.map((s) => ({
184
+ kind: 'validation',
185
+ stepId: s.stepId,
186
+ detail: flattenClamp(s.result.output ?? s.result.error?.message ?? 'skill completed', 160),
187
+ ok: s.result.ok && s.validation.ok,
188
+ checks: { passed: s.validation.checks.filter((c) => c.ok).length, total: s.validation.checks.length },
189
+ })),
190
+ ...toolResults.map((t) => ({
191
+ kind: 'tool-output',
192
+ stepId: t.stepId,
193
+ detail: flattenClamp(t.result.output ?? t.result.error?.message ?? 'tool completed', 160),
194
+ ok: t.result.ok,
195
+ })),
196
+ ];
197
+ // (11) HEADLESS DENIAL => FINDING + FAIL-BRANCH. There is no inner approval wait at 3.0: a denial is
198
+ // reported as a first-class finding and the branch fails. The worker can see `error.code` because it
199
+ // holds the ToolResult directly; the executor flattens it away.
200
+ const denials = toolResults
201
+ .filter((t) => !t.result.ok && t.result.error?.code === 'PERMISSION')
202
+ .map((t, i) => ({
203
+ id: `${record.agentTaskId}_d${i + 1}`,
204
+ agentId: envelope.agentId,
205
+ agentTaskId: record.agentTaskId,
206
+ type: 'permission-denied',
207
+ subject: flattenClamp(t.stepId, 80),
208
+ claim: flattenClamp(`the agent was denied a permitted action: ${t.result.error?.message ?? 'permission denied'}`, 240),
209
+ executionCoverage: coverage,
210
+ confidence: 0.5,
211
+ evidence: [{ kind: 'tool-output', stepId: t.stepId, detail: flattenClamp(t.result.error?.message ?? 'permission denied', 160), ok: false }],
212
+ artifacts: [],
213
+ sourceSteps: [t.stepId],
214
+ provenance: { ...deps.provenance },
215
+ status: 'active',
216
+ createdAt: deps.clock.now(),
217
+ }));
218
+ // (9) Candidates come from the FINAL inner step's structured data - never from model prose.
219
+ const last = [...skillResults].pop();
220
+ const lastTool = [...toolResults].pop();
221
+ const finalData = (last?.result.data ?? lastTool?.result.data);
222
+ const admission = admitFindings({
223
+ agentTaskId: record.agentTaskId,
224
+ agentId: envelope.agentId,
225
+ candidates: finalData?.findings ?? [],
226
+ ...(definition.outputContract ? { contract: definition.outputContract } : {}),
227
+ coverage,
228
+ ...(finalData?.confidence !== undefined ? { confidenceOverride: finalData.confidence } : {}),
229
+ evidenceFor: () => evidence,
230
+ artifactsFor: (index) => {
231
+ const source = `agent:${envelope.agentId}#${index}`;
232
+ const content = evidence.map((e) => `${e.kind} ${e.stepId}: ${e.detail}`).join('\n');
233
+ if (!content)
234
+ return { refs: [], unavailable: false };
235
+ const put = deps.putArtifact(content, source);
236
+ return put.ref ? { refs: [put.ref], unavailable: false } : { refs: [], unavailable: true };
237
+ },
238
+ sourceSteps: inner.plan.steps.map((s) => s.id),
239
+ provenance: deps.provenance,
240
+ now: deps.clock.now(),
241
+ });
242
+ record.findings = [...denials, ...admission.admitted];
243
+ record.diagnostics = admission.rejected;
244
+ if (contractFailed(admission)) {
245
+ return finish('failed', { code: 'finding-contract', message: 'the agent did not satisfy its declared output contract' });
246
+ }
247
+ if (expired())
248
+ return finish('failed', { code: 'agent-timeout', message: `agent exceeded ${envelope.maxDurationMs}ms` });
249
+ if (toolCalls > envelope.maxToolCalls)
250
+ return finish('failed', { code: 'agent-tool-budget', message: `agent exceeded ${envelope.maxToolCalls} tool call(s)` });
251
+ if (!inner.ok) {
252
+ const why = inner.observations.filter((o) => !o.ok).map((o) => `${o.stepId}: ${o.error ?? 'failed'}`).join('; ');
253
+ return finish('failed', { code: 'agent-failed', message: flattenClamp(why || 'the agent plan did not complete', 160) });
254
+ }
255
+ return finish('completed');
256
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * ACTION capabilities (Phase 3.1) — what the system can DO (`read_file`, `run_tests`, `read_issue`),
3
+ * independent of which tool / skill / MCP server provides it. Deliberately distinct from the router's
4
+ * MODEL-capability system (`src/core/capabilities/**`, `CapabilityProfile`/`CapabilityRequirement`):
5
+ * every type here is `Action*`-prefixed, nothing under `src/core/` imports this module, and the router
6
+ * never sees an action capability.
7
+ *
8
+ * SECURITY: `effects` is advisory metadata only and NEVER authorization. It orders resolution, drives
9
+ * permission-gap suggestions, and labels the catalog; the actual operation permission is always enforced
10
+ * by the selected Tool/Skill execution path (`runTool`/`runSkill` → `ToolContext.permissions`).
11
+ */
12
+ import type { Evidence } from '../types.js';
13
+ /** Side-effect class. Two values only: anything not provably read-only is `write` (fail-closed). */
14
+ export type ActionCapabilityEffect = 'read' | 'write';
15
+ /** Where a provider came from. `plugin` is reserved for a future plugin system. */
16
+ export type ActionCapabilitySource = 'native' | 'skill' | 'mcp' | 'openapi' | 'plugin';
17
+ /** Whether a provider can be used right now. `disabled` is operator-driven (MCP, Phase 3.2). */
18
+ export type ProviderAvailability = 'available' | 'unavailable' | 'disabled' | 'unknown';
19
+ /** What a tool / skill / manifest declares it provides. Effects absent ⇒ ['write'] (fail-closed). */
20
+ export interface ActionCapabilityDecl {
21
+ id: string;
22
+ effects?: ActionCapabilityEffect[];
23
+ description?: string;
24
+ }
25
+ /** A capability entity — a DERIVED view keyed by id (curated metadata wins; see vocabulary.ts). */
26
+ export interface ActionCapability {
27
+ id: string;
28
+ name: string;
29
+ description: string;
30
+ effects: ActionCapabilityEffect[];
31
+ /** True when the id belongs to the shipped curated vocabulary. */
32
+ curated: boolean;
33
+ }
34
+ /** One concrete provider of one capability. Bindings always execute through an EXISTING registry. */
35
+ export interface ActionCapabilityProvider {
36
+ capabilityId: string;
37
+ /** `tool:<id>` | `skill:<id>` | `mcp:<server>.<tool>` — the one grammar pins/aliases/tie-breaks use. */
38
+ providerId: string;
39
+ source: ActionCapabilitySource;
40
+ binding: {
41
+ kind: 'tool';
42
+ toolId: string;
43
+ } | {
44
+ kind: 'skill';
45
+ skillId: string;
46
+ };
47
+ effects: ActionCapabilityEffect[];
48
+ /** `inferred` (undeclared), `documented` (declared or config-aliased), `verified` (future probes). */
49
+ verification: Evidence;
50
+ availability: ProviderAvailability;
51
+ note?: string;
52
+ }
53
+ /** Why a required capability could not be satisfied. */
54
+ export type CapabilityGapReason = 'unknown' | 'unavailable' | 'disabled' | 'permission';
55
+ export type GapSuggestion = {
56
+ kind: 'register';
57
+ hint: string;
58
+ } | {
59
+ kind: 'enable-server';
60
+ serverId: string;
61
+ } | {
62
+ kind: 'install-skill';
63
+ hint: string;
64
+ } | {
65
+ kind: 'grant-permission';
66
+ policyKey: string;
67
+ };
68
+ export interface CapabilityGap {
69
+ capabilityId: string;
70
+ reason: CapabilityGapReason;
71
+ /** Every known provider (may be unavailable/disabled/denied); empty ⇒ reason 'unknown'. */
72
+ providers: ActionCapabilityProvider[];
73
+ suggestions: GapSuggestion[];
74
+ }
75
+ /** The user-facing report: what a goal needed, what is available, and what to do about the rest. */
76
+ export interface MissingCapabilityReport {
77
+ required: Array<{
78
+ capabilityId: string;
79
+ status: 'available' | CapabilityGapReason;
80
+ providers: string[];
81
+ }>;
82
+ gaps: CapabilityGap[];
83
+ suggestions: GapSuggestion[];
84
+ }
85
+ export interface CapabilityResolution {
86
+ ok: boolean;
87
+ satisfied: Array<{
88
+ capabilityId: string;
89
+ chosen: ActionCapabilityProvider;
90
+ alternates: ActionCapabilityProvider[];
91
+ }>;
92
+ gaps: CapabilityGap[];
93
+ }
94
+ /** A bare curated id: `^[a-z][a-z0-9_]*$`. */
95
+ export declare const CURATED_ID_RE: RegExp;
96
+ /** A namespaced id: `tool:`/`skill:`/`mcp:` + a segment containing no further colon. */
97
+ export declare const NAMESPACED_ID_RE: RegExp;
98
+ /** Sanitize an arbitrary source id into a namespace segment (never rejects — existing callers keep working). */
99
+ export declare function sanitizeSegment(raw: string): string;
100
+ /** Build the namespaced id for a source-owned capability: `tool:filesystem`, `skill:code-review`. */
101
+ export declare function namespacedId(kind: 'tool' | 'skill' | 'mcp', segment: string): string;
102
+ /**
103
+ * Resolve the id a declaration actually registers under (the claiming rule, D2). A declared bare id is
104
+ * kept ONLY when the declaration is trusted (in-tree builtins) and the id is curated; a declared
105
+ * NAMESPACED id is kept only when it is the declarer's OWN id (`tool:<own-segment>`) — otherwise the
106
+ * declaration is namespaced to its source, so no source can ever claim a curated name or impersonate
107
+ * another provider's namespaced capability.
108
+ */
109
+ export declare function effectiveDeclId(decl: ActionCapabilityDecl, kind: 'tool' | 'skill' | 'mcp', segment: string, trusted: boolean): string;
110
+ /** Effects for a declaration: declared values, else fail-closed `['write']`. Curated ids use vocabulary. */
111
+ export declare function effectsOf(decl: ActionCapabilityDecl | undefined, id: string): ActionCapabilityEffect[];
112
+ /** Build the derived entity view for an id (curated metadata wins; else the declaring source's text). */
113
+ export declare function entityFor(id: string, fallback: {
114
+ name?: string;
115
+ description?: string;
116
+ effects?: ActionCapabilityEffect[];
117
+ }): ActionCapability;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ACTION capabilities (Phase 3.1) — what the system can DO (`read_file`, `run_tests`, `read_issue`),
3
+ * independent of which tool / skill / MCP server provides it. Deliberately distinct from the router's
4
+ * MODEL-capability system (`src/core/capabilities/**`, `CapabilityProfile`/`CapabilityRequirement`):
5
+ * every type here is `Action*`-prefixed, nothing under `src/core/` imports this module, and the router
6
+ * never sees an action capability.
7
+ *
8
+ * SECURITY: `effects` is advisory metadata only and NEVER authorization. It orders resolution, drives
9
+ * permission-gap suggestions, and labels the catalog; the actual operation permission is always enforced
10
+ * by the selected Tool/Skill execution path (`runTool`/`runSkill` → `ToolContext.permissions`).
11
+ */
12
+ import { curated, isCurated } from './vocabulary.js';
13
+ // ── id grammar (D2/D3) ───────────────────────────────────────────────────────
14
+ /** A bare curated id: `^[a-z][a-z0-9_]*$`. */
15
+ export const CURATED_ID_RE = /^[a-z][a-z0-9_]*$/;
16
+ /** A namespaced id: `tool:`/`skill:`/`mcp:` + a segment containing no further colon. */
17
+ export const NAMESPACED_ID_RE = /^(tool|skill|mcp):[^:\s][^:]*$/;
18
+ /** Sanitize an arbitrary source id into a namespace segment (never rejects — existing callers keep working). */
19
+ export function sanitizeSegment(raw) {
20
+ const s = String(raw).trim().toLowerCase().replace(/:/g, '-').replace(/\s+/g, '-').replace(/[^a-z0-9._/-]/g, '');
21
+ return s || 'unnamed';
22
+ }
23
+ /** Build the namespaced id for a source-owned capability: `tool:filesystem`, `skill:code-review`. */
24
+ export function namespacedId(kind, segment) {
25
+ return `${kind}:${sanitizeSegment(segment)}`;
26
+ }
27
+ /**
28
+ * Resolve the id a declaration actually registers under (the claiming rule, D2). A declared bare id is
29
+ * kept ONLY when the declaration is trusted (in-tree builtins) and the id is curated; a declared
30
+ * NAMESPACED id is kept only when it is the declarer's OWN id (`tool:<own-segment>`) — otherwise the
31
+ * declaration is namespaced to its source, so no source can ever claim a curated name or impersonate
32
+ * another provider's namespaced capability.
33
+ */
34
+ export function effectiveDeclId(decl, kind, segment, trusted) {
35
+ const id = String(decl.id ?? '').trim();
36
+ const own = namespacedId(kind, segment);
37
+ // The declarer's own id, or a sub-namespace of it (`tool:acme.fetch_issue`) — expressive but unforgeable.
38
+ if (NAMESPACED_ID_RE.test(id) && (id === own || id.startsWith(`${own}.`)))
39
+ return id;
40
+ if (trusted && isCurated(id))
41
+ return id; // builtin claiming a curated id
42
+ return own;
43
+ }
44
+ /** Effects for a declaration: declared values, else fail-closed `['write']`. Curated ids use vocabulary. */
45
+ export function effectsOf(decl, id) {
46
+ const c = curated(id);
47
+ if (c)
48
+ return [...c.effects]; // curated metadata is canonical
49
+ const declared = decl?.effects;
50
+ if (Array.isArray(declared) && declared.length > 0 && declared.every((e) => e === 'read' || e === 'write'))
51
+ return [...declared];
52
+ return ['write']; // absent or unrecognized ⇒ fail-closed
53
+ }
54
+ /** Build the derived entity view for an id (curated metadata wins; else the declaring source's text). */
55
+ export function entityFor(id, fallback) {
56
+ const c = curated(id);
57
+ if (c)
58
+ return { id, name: c.name, description: c.description, effects: [...c.effects], curated: true };
59
+ return {
60
+ id,
61
+ name: fallback.name ?? id,
62
+ description: fallback.description ?? id,
63
+ effects: fallback.effects && fallback.effects.length ? [...fallback.effects] : ['write'],
64
+ curated: false,
65
+ };
66
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The ACTION-capability registry (Phase 3.1) — capability → provider(s), with a fully deterministic
3
+ * resolution algorithm and structured gaps for what cannot be satisfied.
4
+ *
5
+ * Population is driven by the tool/skill registries (their optional change listeners), so builtins,
6
+ * autoloaded manifest skills, and npm skill packs are all covered by one funnel. Availability is
7
+ * DERIVED at query time from injected read-only accessors, so it is never a stale snapshot.
8
+ *
9
+ * SECURITY: resolution NEVER grants permission. It drops policy-denied providers (so it doesn't propose
10
+ * something the run cannot do) and returns bindings that still execute through `runTool`/`runSkill`,
11
+ * where the real permission check lives.
12
+ */
13
+ import type { ResolvedPermissions } from '../tools/permissions.js';
14
+ import { type ActionCapability, type ActionCapabilityDecl, type ActionCapabilityEffect, type ActionCapabilityProvider, type ActionCapabilitySource, type CapabilityGap, type CapabilityResolution, type MissingCapabilityReport, type ProviderAvailability } from './capability.js';
15
+ export interface ActionCapabilityQuery {
16
+ text?: string;
17
+ source?: ActionCapabilitySource;
18
+ effects?: ActionCapabilityEffect;
19
+ availableOnly?: boolean;
20
+ }
21
+ /** Read-only views the registry needs to derive availability (lifts SkillRegistry.match one level). */
22
+ export interface RegistryAccessors {
23
+ toolIds?: () => string[];
24
+ skills?: () => Array<{
25
+ id: string;
26
+ tools?: string[];
27
+ }>;
28
+ /**
29
+ * MCP availability, DRIVEN by the McpManager (registration alone cannot answer this): connected ⇒
30
+ * available, disabled ⇒ disabled, unreachable/auth-failed ⇒ unavailable, cache-only ⇒ unknown.
31
+ */
32
+ mcpAvailability?: (serverId: string) => ProviderAvailability;
33
+ }
34
+ export interface CapabilityConfig {
35
+ /** `aliases.<curatedId> = <providerId>` — CLAIMS: injects that provider into the curated id's set. */
36
+ aliases?: Record<string, string>;
37
+ /** `pins.<capabilityId> = <providerId>` — SELECTS among already-eligible providers. */
38
+ pins?: Record<string, string>;
39
+ }
40
+ export declare class ActionCapabilityRegistry {
41
+ /** providerId → its entries (so a re-registration can drop exactly that provider's contributions). */
42
+ private readonly byProvider;
43
+ private accessors;
44
+ private config;
45
+ /** Warnings recorded during ingestion (sanitized ids, unknown alias/pin targets). */
46
+ private readonly warnings;
47
+ /** Wire the live registries (call once, before any registration). */
48
+ attachAccessors(a: RegistryAccessors): this;
49
+ /** Apply `runtime.capabilities` config (aliases claim; pins select). */
50
+ configure(config: CapabilityConfig): this;
51
+ configWarnings(): string[];
52
+ /**
53
+ * Ingest a tool's declarations (or infer one). `trusted` marks in-tree builtins, the only code allowed
54
+ * to claim a bare curated id. Idempotent: prior entries for this provider are replaced.
55
+ */
56
+ ingestTool(tool: {
57
+ id: string;
58
+ description?: string;
59
+ provides?: ActionCapabilityDecl[];
60
+ }, trusted?: boolean): void;
61
+ /**
62
+ * Ingest an MCP-backed Tool. Its OWN namespace is derived from the Runtime-generated tool id
63
+ * (`<serverId>.<toolName>`) — never from the server's own declaration — so a server can claim a
64
+ * capability inside its own `mcp:` namespace and nowhere else. MCP is never a trusted claimant, so a
65
+ * curated id in its declaration is namespaced away like any other untrusted source.
66
+ */
67
+ ingestMcpTool(tool: {
68
+ id: string;
69
+ description?: string;
70
+ provides?: ActionCapabilityDecl[];
71
+ }): void;
72
+ /** Ingest a skill's declarations (or infer one). Skills are never trusted to claim curated ids. */
73
+ ingestSkill(skill: {
74
+ id: string;
75
+ description?: string;
76
+ provides?: ActionCapabilityDecl[];
77
+ }): void;
78
+ /**
79
+ * Ownership guard: `providerId` is a SANITIZED view of a source id, and sanitization is not injective
80
+ * (`Filesystem`, `FILESYSTEM`, `file@system` all fold to `filesystem`). Re-registering the SAME raw id
81
+ * replaces its entries (idempotent, matching the tool/skill registries); a DIFFERENT raw id that folds
82
+ * onto an already-owned providerId is refused with a warning — it can neither erase the incumbent's
83
+ * capabilities nor inherit an operator alias/pin that names that providerId.
84
+ */
85
+ private claimProviderId;
86
+ /** Remove everything a provider contributed (tool/skill unregistration, MCP server removal). */
87
+ removeProvider(providerId: string): void;
88
+ /**
89
+ * Remove a source's entries by its EXACT id (what the tool/skill registries emit on unregister).
90
+ * Sanitization is applied here, and only the owning raw id may remove — so a colliding id can never
91
+ * unregister the incumbent's capabilities.
92
+ */
93
+ removeSource(kind: 'tool' | 'skill' | 'mcp', rawId: string): void;
94
+ /**
95
+ * Drop every capability contributed by one MCP server's tools. Scoped by the `mcp:<server>.` prefix
96
+ * INCLUDING the dot — without it, removing `demo` would also strip `demo2`.
97
+ */
98
+ removeMcpServer(serverId: string): void;
99
+ /**
100
+ * Remove every provider whose providerId starts with the given LITERAL prefix. A low-level primitive:
101
+ * prefer `removeSource`/`removeMcpServer`, which scope correctly — a bare `mcp:demo` prefix also
102
+ * matches `mcp:demo2.x`.
103
+ */
104
+ removeBySourcePrefix(prefix: string): void;
105
+ private replaceProvider;
106
+ private declsToEntries;
107
+ /** Every provider entry, including alias-claimed ones, with availability derived. */
108
+ private allProviders;
109
+ private findEntry;
110
+ private toProvider;
111
+ /** Availability is DERIVED: a skill whose tools are not all registered is unavailable. */
112
+ private availabilityOf;
113
+ /**
114
+ * The capability ids ONE provider contributes: its own stored entries plus any config alias that
115
+ * CLAIMS a curated id for it. A pure lookup over `byProvider` — no inference — so the registry stays
116
+ * offline and property-testable. Phase 3.3's post-plan check uses it to ask "what does this step's
117
+ * provider actually do?" instead of guessing from the step input.
118
+ */
119
+ capabilitiesOf(providerId: string, rawId?: string): string[];
120
+ /** All capability entities (derived views), sorted by id for stable output. */
121
+ list(): ActionCapability[];
122
+ get(id: string): ActionCapability | undefined;
123
+ providersOf(capabilityId: string): ActionCapabilityProvider[];
124
+ query(q?: ActionCapabilityQuery): ActionCapability[];
125
+ resolve(required: string[], opts?: {
126
+ permissions?: ResolvedPermissions;
127
+ }): CapabilityResolution;
128
+ /** Build the user-facing report from a resolution (one shape; gaps carry the actionable suggestions). */
129
+ report(required: string[], opts?: {
130
+ permissions?: ResolvedPermissions;
131
+ }): MissingCapabilityReport;
132
+ }
133
+ /**
134
+ * Build the ONE `MissingCapabilityReport` shape from an already-resolved gap set. Factored out of
135
+ * `report()` (Phase 3.3) so the run path and the public reporter cannot diverge: before this, the
136
+ * Runtime hand-built the same shape without `dedupeSuggestions` and listed only gapped ids in
137
+ * `required`. Every construction of this type now goes through here.
138
+ */
139
+ export declare function capabilityReportFrom(required: string[], gaps: CapabilityGap[], providersOf: (id: string) => ActionCapabilityProvider[]): MissingCapabilityReport;