@aisy/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/agent-loop/index.d.ts +4 -0
- package/dist/agent-loop/index.d.ts.map +1 -0
- package/dist/agent-loop/index.js +352 -0
- package/dist/agent-loop/index.js.map +1 -0
- package/dist/agent-loop/types.d.ts +183 -0
- package/dist/agent-loop/types.d.ts.map +1 -0
- package/dist/agent-loop/types.js +3 -0
- package/dist/agent-loop/types.js.map +1 -0
- package/dist/bin/aisy.d.ts +3 -0
- package/dist/bin/aisy.d.ts.map +1 -0
- package/dist/bin/aisy.js +14 -0
- package/dist/bin/aisy.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +114 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-engine/index.d.ts +4 -0
- package/dist/context-engine/index.d.ts.map +1 -0
- package/dist/context-engine/index.js +126 -0
- package/dist/context-engine/index.js.map +1 -0
- package/dist/context-engine/types.d.ts +54 -0
- package/dist/context-engine/types.d.ts.map +1 -0
- package/dist/context-engine/types.js +4 -0
- package/dist/context-engine/types.js.map +1 -0
- package/dist/eval/index.d.ts +20 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +128 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/types.d.ts +62 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/eval/types.js +17 -0
- package/dist/eval/types.js.map +1 -0
- package/dist/gateway/index.d.ts +5 -0
- package/dist/gateway/index.d.ts.map +1 -0
- package/dist/gateway/index.js +288 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/types.d.ts +194 -0
- package/dist/gateway/types.d.ts.map +1 -0
- package/dist/gateway/types.js +94 -0
- package/dist/gateway/types.js.map +1 -0
- package/dist/goals/index.d.ts +11 -0
- package/dist/goals/index.d.ts.map +1 -0
- package/dist/goals/index.js +21 -0
- package/dist/goals/index.js.map +1 -0
- package/dist/goals/types.d.ts +47 -0
- package/dist/goals/types.d.ts.map +1 -0
- package/dist/goals/types.js +5 -0
- package/dist/goals/types.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +215 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/types.d.ts +148 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +4 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/memory/index.d.ts +6 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +419 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/types.d.ts +131 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +33 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/nightly/index.d.ts +4 -0
- package/dist/nightly/index.d.ts.map +1 -0
- package/dist/nightly/index.js +470 -0
- package/dist/nightly/index.js.map +1 -0
- package/dist/nightly/types.d.ts +326 -0
- package/dist/nightly/types.d.ts.map +1 -0
- package/dist/nightly/types.js +3 -0
- package/dist/nightly/types.js.map +1 -0
- package/dist/observability/index.d.ts +11 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +396 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/types.d.ts +139 -0
- package/dist/observability/types.d.ts.map +1 -0
- package/dist/observability/types.js +4 -0
- package/dist/observability/types.js.map +1 -0
- package/dist/onboarding/index.d.ts +16 -0
- package/dist/onboarding/index.d.ts.map +1 -0
- package/dist/onboarding/index.js +787 -0
- package/dist/onboarding/index.js.map +1 -0
- package/dist/onboarding/interactive.d.ts +23 -0
- package/dist/onboarding/interactive.d.ts.map +1 -0
- package/dist/onboarding/interactive.js +45 -0
- package/dist/onboarding/interactive.js.map +1 -0
- package/dist/onboarding/types.d.ts +388 -0
- package/dist/onboarding/types.d.ts.map +1 -0
- package/dist/onboarding/types.js +35 -0
- package/dist/onboarding/types.js.map +1 -0
- package/dist/orchestration/index.d.ts +8 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +706 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/types.d.ts +391 -0
- package/dist/orchestration/types.d.ts.map +1 -0
- package/dist/orchestration/types.js +30 -0
- package/dist/orchestration/types.js.map +1 -0
- package/dist/personality/index.d.ts +65 -0
- package/dist/personality/index.d.ts.map +1 -0
- package/dist/personality/index.js +339 -0
- package/dist/personality/index.js.map +1 -0
- package/dist/personality/types.d.ts +103 -0
- package/dist/personality/types.d.ts.map +1 -0
- package/dist/personality/types.js +15 -0
- package/dist/personality/types.js.map +1 -0
- package/dist/provider/index.d.ts +4 -0
- package/dist/provider/index.d.ts.map +1 -0
- package/dist/provider/index.js +236 -0
- package/dist/provider/index.js.map +1 -0
- package/dist/provider/types.d.ts +180 -0
- package/dist/provider/types.d.ts.map +1 -0
- package/dist/provider/types.js +4 -0
- package/dist/provider/types.js.map +1 -0
- package/dist/runtime/agent-cards.d.ts +14 -0
- package/dist/runtime/agent-cards.d.ts.map +1 -0
- package/dist/runtime/agent-cards.js +90 -0
- package/dist/runtime/agent-cards.js.map +1 -0
- package/dist/runtime/agent-runner.d.ts +30 -0
- package/dist/runtime/agent-runner.d.ts.map +1 -0
- package/dist/runtime/agent-runner.js +37 -0
- package/dist/runtime/agent-runner.js.map +1 -0
- package/dist/runtime/budget.d.ts +15 -0
- package/dist/runtime/budget.d.ts.map +1 -0
- package/dist/runtime/budget.js +24 -0
- package/dist/runtime/budget.js.map +1 -0
- package/dist/runtime/delegation-driver.d.ts +11 -0
- package/dist/runtime/delegation-driver.d.ts.map +1 -0
- package/dist/runtime/delegation-driver.js +132 -0
- package/dist/runtime/delegation-driver.js.map +1 -0
- package/dist/runtime/exact-cache.d.ts +10 -0
- package/dist/runtime/exact-cache.d.ts.map +1 -0
- package/dist/runtime/exact-cache.js +30 -0
- package/dist/runtime/exact-cache.js.map +1 -0
- package/dist/runtime/execute-tool.d.ts +29 -0
- package/dist/runtime/execute-tool.d.ts.map +1 -0
- package/dist/runtime/execute-tool.js +80 -0
- package/dist/runtime/execute-tool.js.map +1 -0
- package/dist/runtime/guardian.d.ts +9 -0
- package/dist/runtime/guardian.d.ts.map +1 -0
- package/dist/runtime/guardian.js +41 -0
- package/dist/runtime/guardian.js.map +1 -0
- package/dist/runtime/hook-gate.d.ts +17 -0
- package/dist/runtime/hook-gate.d.ts.map +1 -0
- package/dist/runtime/hook-gate.js +56 -0
- package/dist/runtime/hook-gate.js.map +1 -0
- package/dist/runtime/memory-adapter.d.ts +6 -0
- package/dist/runtime/memory-adapter.d.ts.map +1 -0
- package/dist/runtime/memory-adapter.js +38 -0
- package/dist/runtime/memory-adapter.js.map +1 -0
- package/dist/runtime/nightly-adapters.d.ts +48 -0
- package/dist/runtime/nightly-adapters.d.ts.map +1 -0
- package/dist/runtime/nightly-adapters.js +139 -0
- package/dist/runtime/nightly-adapters.js.map +1 -0
- package/dist/runtime/nightly-generator.d.ts +10 -0
- package/dist/runtime/nightly-generator.d.ts.map +1 -0
- package/dist/runtime/nightly-generator.js +335 -0
- package/dist/runtime/nightly-generator.js.map +1 -0
- package/dist/runtime/onboarding-node.d.ts +6 -0
- package/dist/runtime/onboarding-node.d.ts.map +1 -0
- package/dist/runtime/onboarding-node.js +356 -0
- package/dist/runtime/onboarding-node.js.map +1 -0
- package/dist/runtime/provider-anthropic.d.ts +43 -0
- package/dist/runtime/provider-anthropic.d.ts.map +1 -0
- package/dist/runtime/provider-anthropic.js +148 -0
- package/dist/runtime/provider-anthropic.js.map +1 -0
- package/dist/runtime/provider-cli.d.ts +18 -0
- package/dist/runtime/provider-cli.d.ts.map +1 -0
- package/dist/runtime/provider-cli.js +73 -0
- package/dist/runtime/provider-cli.js.map +1 -0
- package/dist/runtime/provider-openai.d.ts +30 -0
- package/dist/runtime/provider-openai.d.ts.map +1 -0
- package/dist/runtime/provider-openai.js +114 -0
- package/dist/runtime/provider-openai.js.map +1 -0
- package/dist/runtime/providers.d.ts +43 -0
- package/dist/runtime/providers.d.ts.map +1 -0
- package/dist/runtime/providers.js +72 -0
- package/dist/runtime/providers.js.map +1 -0
- package/dist/runtime/sandbox-bash.d.ts +21 -0
- package/dist/runtime/sandbox-bash.d.ts.map +1 -0
- package/dist/runtime/sandbox-bash.js +51 -0
- package/dist/runtime/sandbox-bash.js.map +1 -0
- package/dist/runtime/scoped-tool-executor.d.ts +10 -0
- package/dist/runtime/scoped-tool-executor.d.ts.map +1 -0
- package/dist/runtime/scoped-tool-executor.js +30 -0
- package/dist/runtime/scoped-tool-executor.js.map +1 -0
- package/dist/runtime/session-log.d.ts +6 -0
- package/dist/runtime/session-log.d.ts.map +1 -0
- package/dist/runtime/session-log.js +54 -0
- package/dist/runtime/session-log.js.map +1 -0
- package/dist/runtime/settings.d.ts +24 -0
- package/dist/runtime/settings.d.ts.map +1 -0
- package/dist/runtime/settings.js +29 -0
- package/dist/runtime/settings.js.map +1 -0
- package/dist/runtime/spawn-plan.d.ts +13 -0
- package/dist/runtime/spawn-plan.d.ts.map +1 -0
- package/dist/runtime/spawn-plan.js +107 -0
- package/dist/runtime/spawn-plan.js.map +1 -0
- package/dist/runtime/spend.d.ts +41 -0
- package/dist/runtime/spend.d.ts.map +1 -0
- package/dist/runtime/spend.js +0 -0
- package/dist/runtime/spend.js.map +1 -0
- package/dist/runtime/sub-agent-runner.d.ts +19 -0
- package/dist/runtime/sub-agent-runner.d.ts.map +1 -0
- package/dist/runtime/sub-agent-runner.js +47 -0
- package/dist/runtime/sub-agent-runner.js.map +1 -0
- package/dist/safety/grants.d.ts +7 -0
- package/dist/safety/grants.d.ts.map +1 -0
- package/dist/safety/grants.js +53 -0
- package/dist/safety/grants.js.map +1 -0
- package/dist/safety/index.d.ts +72 -0
- package/dist/safety/index.d.ts.map +1 -0
- package/dist/safety/index.js +464 -0
- package/dist/safety/index.js.map +1 -0
- package/dist/safety/types.d.ts +254 -0
- package/dist/safety/types.d.ts.map +1 -0
- package/dist/safety/types.js +3 -0
- package/dist/safety/types.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +463 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/types.d.ts +177 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +3 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/testing/clock.d.ts +8 -0
- package/dist/testing/clock.d.ts.map +1 -0
- package/dist/testing/clock.js +13 -0
- package/dist/testing/clock.js.map +1 -0
- package/dist/testing/effect-verifier.d.ts +15 -0
- package/dist/testing/effect-verifier.d.ts.map +1 -0
- package/dist/testing/effect-verifier.js +27 -0
- package/dist/testing/effect-verifier.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/provider-fake.d.ts +14 -0
- package/dist/testing/provider-fake.d.ts.map +1 -0
- package/dist/testing/provider-fake.js +18 -0
- package/dist/testing/provider-fake.js.map +1 -0
- package/dist/testing/sandbox-stub.d.ts +15 -0
- package/dist/testing/sandbox-stub.d.ts.map +1 -0
- package/dist/testing/sandbox-stub.js +15 -0
- package/dist/testing/sandbox-stub.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +0 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/types.d.ts +138 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/triggers/index.d.ts +4 -0
- package/dist/triggers/index.d.ts.map +1 -0
- package/dist/triggers/index.js +187 -0
- package/dist/triggers/index.js.map +1 -0
- package/dist/triggers/types.d.ts +74 -0
- package/dist/triggers/types.d.ts.map +1 -0
- package/dist/triggers/types.js +5 -0
- package/dist/triggers/types.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
// Orchestration — Component 11
|
|
2
|
+
// Deterministic control plane for multi-step work: coordinator-workers topology
|
|
3
|
+
// (ADR-0021), Loop Guardian retry cap (ADR-0020), generations (ADR-0005).
|
|
4
|
+
// See docs/specs/11-orchestration.md.
|
|
5
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
6
|
+
export { ScopeConflictError, ScopeViolationError } from './types.js';
|
|
7
|
+
import { ScopeConflictError, ScopeViolationError, } from './types.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Glob matching — minimal, deterministic. Worker scopes use glob paths
|
|
10
|
+
// ('src/api/**'); `touched` carries concrete paths checked against them
|
|
11
|
+
// (spec §3 WorkerScope, §5.1 "code checks touched ⊆ scope.owns").
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function globToRegExp(glob) {
|
|
14
|
+
let re = '';
|
|
15
|
+
for (let i = 0; i < glob.length; i++) {
|
|
16
|
+
const c = glob[i];
|
|
17
|
+
if (c === '*') {
|
|
18
|
+
if (glob[i + 1] === '*') {
|
|
19
|
+
re += '.*';
|
|
20
|
+
i++;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
re += '[^/]*';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (c === '?') {
|
|
27
|
+
re += '[^/]';
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
re += c.replace(/[.+^${}()|[\]\\]/, '\\$&');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return new RegExp(`^${re}$`);
|
|
34
|
+
}
|
|
35
|
+
export function globMatches(glob, path) {
|
|
36
|
+
return globToRegExp(glob).test(path);
|
|
37
|
+
}
|
|
38
|
+
/** Literal prefix of a glob — everything before the first wildcard. */
|
|
39
|
+
function globRoot(glob) {
|
|
40
|
+
const wildcardIdx = glob.search(/[*?]/);
|
|
41
|
+
return wildcardIdx === -1 ? glob : glob.slice(0, wildcardIdx);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Conservative may-overlap test between two scope patterns: equal patterns or
|
|
45
|
+
* one literal root truly *containing* the other are treated as overlapping.
|
|
46
|
+
* Containment only counts at a path-segment boundary — the character after the
|
|
47
|
+
* shorter root in the longer one must be a `/` (or the shorter root already
|
|
48
|
+
* ends in `/`, or the roots are equal). Otherwise a bare prefix like
|
|
49
|
+
* 'src/api' would wrongly match the disjoint 'src/apiv2/**'.
|
|
50
|
+
* Used for the pairwise write-disjointness assertion (ADR-0021, AC-11-1).
|
|
51
|
+
*/
|
|
52
|
+
function patternsMayOverlap(a, b) {
|
|
53
|
+
if (a === b)
|
|
54
|
+
return true;
|
|
55
|
+
const ra = globRoot(a);
|
|
56
|
+
const rb = globRoot(b);
|
|
57
|
+
const [shorter, longer] = ra.length <= rb.length ? [ra, rb] : [rb, ra];
|
|
58
|
+
if (!longer.startsWith(shorter))
|
|
59
|
+
return false;
|
|
60
|
+
return longer.length === shorter.length || shorter.endsWith('/') || longer[shorter.length] === '/';
|
|
61
|
+
}
|
|
62
|
+
function overlappingPaths(ownsA, ownsB) {
|
|
63
|
+
const overlap = [];
|
|
64
|
+
for (const a of ownsA) {
|
|
65
|
+
for (const b of ownsB) {
|
|
66
|
+
if (patternsMayOverlap(a, b))
|
|
67
|
+
overlap.push(a === b ? a : `${a} ~ ${b}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return overlap;
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Loop Guardian — sliding-window cycle detector for periods 1/2/3 (ADR-0020).
|
|
74
|
+
// A cycle repeating more than `maxRepeats` times latches a STOP; the verdict
|
|
75
|
+
// persists across further checks (no auto-resume, spec §7 / AC-11-7). Cycles
|
|
76
|
+
// of period ≥4 are by design invisible here and are caught by the global
|
|
77
|
+
// budget cap instead (spec §5.2 / AC-11-8).
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
const DEFAULT_MAX_REPEATS = 3;
|
|
80
|
+
const DEFAULT_WINDOW_SIZE = 32;
|
|
81
|
+
export function makeLoopGuardian(config = {}) {
|
|
82
|
+
const maxRepeats = config.maxRepeats ?? DEFAULT_MAX_REPEATS;
|
|
83
|
+
const windowSize = config.windowSize ?? DEFAULT_WINDOW_SIZE;
|
|
84
|
+
let window = [];
|
|
85
|
+
let latched;
|
|
86
|
+
return {
|
|
87
|
+
check(step) {
|
|
88
|
+
// STOP is latched — the run never auto-resumes (AC-11-7).
|
|
89
|
+
if (latched !== undefined)
|
|
90
|
+
return latched;
|
|
91
|
+
window.push(step);
|
|
92
|
+
if (window.length > windowSize)
|
|
93
|
+
window.shift();
|
|
94
|
+
for (const period of [1, 2, 3]) {
|
|
95
|
+
if (window.length < period * 2)
|
|
96
|
+
continue;
|
|
97
|
+
// The candidate cycle is the last `period` actions; count how many
|
|
98
|
+
// times it repeats consecutively at the tail of the window.
|
|
99
|
+
const block = window.slice(-period).map(s => s.actionId);
|
|
100
|
+
let repeats = 1;
|
|
101
|
+
let pos = window.length - 2 * period;
|
|
102
|
+
while (pos >= 0) {
|
|
103
|
+
let same = true;
|
|
104
|
+
for (let i = 0; i < period; i++) {
|
|
105
|
+
if (window[pos + i].actionId !== block[i]) {
|
|
106
|
+
same = false;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!same)
|
|
111
|
+
break;
|
|
112
|
+
repeats++;
|
|
113
|
+
pos -= period;
|
|
114
|
+
}
|
|
115
|
+
if (repeats > maxRepeats) {
|
|
116
|
+
latched = { stop: true, period, repeatCount: repeats, windowSnapshot: [...window] };
|
|
117
|
+
return latched;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { pass: true };
|
|
121
|
+
},
|
|
122
|
+
reset() {
|
|
123
|
+
window = [];
|
|
124
|
+
latched = undefined;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Coordinator — decomposition, scope-checked spawn, journal reconciliation
|
|
130
|
+
// (ADR-0021). Scope assignment and all invariants below are code (100%
|
|
131
|
+
// adherence); only the *wording* of intents/entries defers to the model.
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/** §7: with the Loop Guardian unavailable the iteration cap tightens to this floor. */
|
|
134
|
+
const CONSERVATIVE_ITERATION_FLOOR = 25;
|
|
135
|
+
/** §4 example scope contract — default per-worker budget slice. */
|
|
136
|
+
const DEFAULT_WORKER_ITERATIONS = 40;
|
|
137
|
+
const DEFAULT_WORKER_SPEND_USD = 0.5;
|
|
138
|
+
export function makeCoordinator(deps) {
|
|
139
|
+
const runId = `r-${randomUUID().slice(0, 8)}`;
|
|
140
|
+
let generationId = 'g-1';
|
|
141
|
+
const spawnedScopes = new Map();
|
|
142
|
+
/** Workers that attempted an out-of-scope write; excluded from the merge (§7). */
|
|
143
|
+
const faulted = new Set();
|
|
144
|
+
/** Mirror of budget.json.nested — proves which bound stopped the run (§5.2). */
|
|
145
|
+
const nested = { loopGuardianTrips: 0, planReplans: 0, skillFailures: {} };
|
|
146
|
+
let haltReason;
|
|
147
|
+
let degraded = false;
|
|
148
|
+
/**
|
|
149
|
+
* Coordinator-controlled monotonic seq. The journal `seq` is a tamper signal
|
|
150
|
+
* (§4), so a worker must never assign its own — a hostile/buggy worker could
|
|
151
|
+
* forge collisions or gaps. The coordinator alone advances it (§8 repudiation).
|
|
152
|
+
*/
|
|
153
|
+
let nextSeq = 1;
|
|
154
|
+
function emit(kind, payload) {
|
|
155
|
+
deps.emit({ kind, runId, ts: new Date().toISOString(), payload });
|
|
156
|
+
}
|
|
157
|
+
/** Halt fail-closed on the first cap reached; never proceeds past it (§5.2). */
|
|
158
|
+
function halt(reason, extra) {
|
|
159
|
+
if (haltReason !== undefined)
|
|
160
|
+
return;
|
|
161
|
+
haltReason = reason;
|
|
162
|
+
emit('run.terminated', { reason, nested: { ...nested }, ...extra });
|
|
163
|
+
}
|
|
164
|
+
// Cold start (§7 / AC-11-14): no run state exists yet — fail closed by
|
|
165
|
+
// construction: fresh g-1, empty journal, fresh ledger, zero workers, and
|
|
166
|
+
// no orphan-resumption capability at all.
|
|
167
|
+
//
|
|
168
|
+
// Guardian heartbeat probe (§7 / AC-11-15): if Observability's Loop
|
|
169
|
+
// Guardian is unreachable, do not run unattended at normal caps — flag the
|
|
170
|
+
// run degraded and tighten the iteration cap to the conservative floor.
|
|
171
|
+
let startupVerdict;
|
|
172
|
+
try {
|
|
173
|
+
startupVerdict = deps.loopGuardian.check({ actionId: '__heartbeat__', seq: 0 });
|
|
174
|
+
if (!('stop' in startupVerdict))
|
|
175
|
+
deps.loopGuardian.reset();
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
degraded = true;
|
|
179
|
+
}
|
|
180
|
+
emit('run.started', {
|
|
181
|
+
generationId,
|
|
182
|
+
degraded,
|
|
183
|
+
iterationCapFloor: degraded ? CONSERVATIVE_ITERATION_FLOOR : undefined,
|
|
184
|
+
});
|
|
185
|
+
// Precedence (§5.2 / AC-11-9): the Loop-Guardian STOP is the innermost
|
|
186
|
+
// bound and fires before any budget charge — a latched STOP halts here.
|
|
187
|
+
if (startupVerdict !== undefined && 'stop' in startupVerdict) {
|
|
188
|
+
nested.loopGuardianTrips++;
|
|
189
|
+
halt('loop-guardian', { reviewCard: { windowSnapshot: startupVerdict.windowSnapshot } });
|
|
190
|
+
}
|
|
191
|
+
function checkScope(touched, scope) {
|
|
192
|
+
// §5.1: touched ⊆ owns ∧ touched ∩ doNotTouch = ∅; doNotTouch overrides owns.
|
|
193
|
+
const insideDeny = touched.filter(p => scope.doNotTouch.some(g => globMatches(g, p)));
|
|
194
|
+
if (insideDeny.length > 0)
|
|
195
|
+
return { paths: insideDeny, reason: 'inside-doNotTouch' };
|
|
196
|
+
const outsideOwns = touched.filter(p => !scope.owns.some(g => globMatches(g, p)));
|
|
197
|
+
if (outsideOwns.length > 0)
|
|
198
|
+
return { paths: outsideOwns, reason: 'outside-owns' };
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
function abortReconcile(reason) {
|
|
202
|
+
// §7 seq gap / tamper: reconciliation aborts, run is halted as untrusted.
|
|
203
|
+
halt('untrusted-journal', { integrity: reason });
|
|
204
|
+
return { aborted: reason };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Dead-end fork (§5.3 / ADR-0005). The model would distill lessons (~70%); the
|
|
208
|
+
* fork itself is code. The new generation carries constitution + lessons only —
|
|
209
|
+
* the failed transcript is dropped — and per-generation counters reset, but the
|
|
210
|
+
* run-level spend cap is NOT reset (no budget-reset evasion, §8). Switching
|
|
211
|
+
* `generationId` is the deterministic transcript drop: subsequent entries are
|
|
212
|
+
* stamped with the fresh generation, never the dead-ended one.
|
|
213
|
+
*/
|
|
214
|
+
function forkOnDeadEnd(lessons) {
|
|
215
|
+
const newGen = deps.generationManager.fork(runId, lessons);
|
|
216
|
+
const parent = generationId;
|
|
217
|
+
generationId = newGen;
|
|
218
|
+
nested.loopGuardianTrips = 0;
|
|
219
|
+
nested.planReplans = 0;
|
|
220
|
+
nested.skillFailures = {};
|
|
221
|
+
emit('generation.forked', { generationId: newGen, parent });
|
|
222
|
+
return newGen;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
decompose(task, _gen) {
|
|
226
|
+
// Decomposition is a model call in production (~70%, spec §5.1); the
|
|
227
|
+
// deterministic carve here is one worker per affected path. Each
|
|
228
|
+
// worker's doNotTouch explicitly denies every peer's lane.
|
|
229
|
+
const paths = [...new Set(task.affectedPaths)];
|
|
230
|
+
const briefs = paths.map((path, i) => ({
|
|
231
|
+
workerId: `w-${i + 1}`,
|
|
232
|
+
intent: `Apply '${task.description}' within ${path}`,
|
|
233
|
+
scope: {
|
|
234
|
+
owns: [path],
|
|
235
|
+
doNotTouch: paths.filter(p => p !== path),
|
|
236
|
+
taskClass: 'reasoning',
|
|
237
|
+
budgetSlice: { iterations: DEFAULT_WORKER_ITERATIONS, spendUsd: DEFAULT_WORKER_SPEND_USD },
|
|
238
|
+
},
|
|
239
|
+
}));
|
|
240
|
+
// Code-enforced (ADR-0021 / AC-11-1): scopes must be pairwise
|
|
241
|
+
// write-disjoint before any spawn; an overlap halts the run instead.
|
|
242
|
+
for (let i = 0; i < briefs.length; i++) {
|
|
243
|
+
for (let j = i + 1; j < briefs.length; j++) {
|
|
244
|
+
const overlap = overlappingPaths(briefs[i].scope.owns, briefs[j].scope.owns);
|
|
245
|
+
if (overlap.length > 0) {
|
|
246
|
+
throw new ScopeConflictError(briefs[i].workerId, briefs[j].workerId, overlap);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return briefs;
|
|
251
|
+
},
|
|
252
|
+
async spawn(brief) {
|
|
253
|
+
if (haltReason !== undefined) {
|
|
254
|
+
throw new Error(`run halted (${haltReason}); refusing to spawn worker '${brief.workerId}'`);
|
|
255
|
+
}
|
|
256
|
+
// Reject any scope overlapping an already-spawned worker (AC-11-1).
|
|
257
|
+
for (const [otherId, otherScope] of spawnedScopes) {
|
|
258
|
+
const overlap = overlappingPaths(brief.scope.owns, otherScope.owns);
|
|
259
|
+
if (overlap.length > 0) {
|
|
260
|
+
throw new ScopeConflictError(otherId, brief.workerId, overlap);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
spawnedScopes.set(brief.workerId, brief.scope);
|
|
264
|
+
emit('worker.spawned', { workerId: brief.workerId, scope: brief.scope });
|
|
265
|
+
// The handle deliberately exposes NO peer channel — no sendToPeer /
|
|
266
|
+
// message / peers. The no-peer-to-peer invariant is enforced by
|
|
267
|
+
// absence of capability (ADR-0021 / AC-11-3); the only sink a worker
|
|
268
|
+
// has is appendDecision into the shared journal.
|
|
269
|
+
const handle = {
|
|
270
|
+
workerId: brief.workerId,
|
|
271
|
+
appendDecision: (partial) => {
|
|
272
|
+
// Fail closed (§7): a worker that already committed a scope violation
|
|
273
|
+
// is faulted and excluded from the merge — it must not be able to
|
|
274
|
+
// write any further entries into the shared journal (AC-11-2).
|
|
275
|
+
if (faulted.has(brief.workerId)) {
|
|
276
|
+
throw new ScopeViolationError(brief.workerId, partial.touched, 'outside-owns');
|
|
277
|
+
}
|
|
278
|
+
const violation = checkScope(partial.touched, brief.scope);
|
|
279
|
+
if (violation !== undefined) {
|
|
280
|
+
// Fail closed (§7): reject the append, mark the worker faulted
|
|
281
|
+
// so its output never enters the merge (AC-11-2).
|
|
282
|
+
faulted.add(brief.workerId);
|
|
283
|
+
emit('scope.violation', {
|
|
284
|
+
workerId: brief.workerId,
|
|
285
|
+
violatingPaths: violation.paths,
|
|
286
|
+
reason: violation.reason,
|
|
287
|
+
});
|
|
288
|
+
throw new ScopeViolationError(brief.workerId, violation.paths, violation.reason);
|
|
289
|
+
}
|
|
290
|
+
// Seq is coordinator-controlled (§4 / §8): override any worker-supplied
|
|
291
|
+
// value with a fresh monotonic seq so the journal stays tamper-evident.
|
|
292
|
+
const entry = {
|
|
293
|
+
runId,
|
|
294
|
+
generationId,
|
|
295
|
+
workerId: brief.workerId,
|
|
296
|
+
...partial,
|
|
297
|
+
seq: nextSeq++,
|
|
298
|
+
};
|
|
299
|
+
deps.journal.append(entry);
|
|
300
|
+
emit('journal.appended', { workerId: brief.workerId, seq: entry.seq });
|
|
301
|
+
},
|
|
302
|
+
done: Promise.resolve(),
|
|
303
|
+
};
|
|
304
|
+
return handle;
|
|
305
|
+
},
|
|
306
|
+
reconcile(targetRunId) {
|
|
307
|
+
const all = deps.journal.read(targetRunId);
|
|
308
|
+
// Integrity first (AC-11-5/6): `seq` must be strictly monotonic with no
|
|
309
|
+
// gaps. A gap means loss/tampering; a duplicate means mutation. Both
|
|
310
|
+
// fail closed — no merge is ever produced from an untrusted journal.
|
|
311
|
+
const seqs = all.map(e => e.seq).sort((a, b) => a - b);
|
|
312
|
+
for (let i = 1; i < seqs.length; i++) {
|
|
313
|
+
const delta = seqs[i] - seqs[i - 1];
|
|
314
|
+
if (delta === 0)
|
|
315
|
+
return abortReconcile('untrusted');
|
|
316
|
+
if (delta > 1)
|
|
317
|
+
return abortReconcile('seq-gap');
|
|
318
|
+
}
|
|
319
|
+
// Faulted workers' decisions never enter the merge (§7 scope violation).
|
|
320
|
+
const entries = all.filter(e => !faulted.has(e.workerId));
|
|
321
|
+
// Contradiction scan (AC-11-4 / ADR-0021): two entries conflict when they
|
|
322
|
+
// touch a shared resource and chose *different* options for it. Only one
|
|
323
|
+
// decision can hold per resource, so distinct `decidedFor` on a shared path
|
|
324
|
+
// is incompatible — regardless of whether either recorded a competing
|
|
325
|
+
// `decidedAgainst` (which is often '' when there was no rival option). The
|
|
326
|
+
// earlier FOR==AGAINST-only test silently glued together exactly this common
|
|
327
|
+
// case, the failure ADR-0021 exists to prevent.
|
|
328
|
+
const conflicts = [];
|
|
329
|
+
for (let i = 0; i < entries.length; i++) {
|
|
330
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
331
|
+
const a = entries[i];
|
|
332
|
+
const b = entries[j];
|
|
333
|
+
if (a.decidedFor === b.decidedFor)
|
|
334
|
+
continue;
|
|
335
|
+
const shared = a.touched.find(p => b.touched.includes(p));
|
|
336
|
+
if (shared === undefined)
|
|
337
|
+
continue;
|
|
338
|
+
conflicts.push({ entryA: a, entryB: b, resource: shared });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (conflicts.length === 0) {
|
|
342
|
+
emit('run.reconciled', { entries: entries.length, conflicts: 0 });
|
|
343
|
+
return { merged: entries };
|
|
344
|
+
}
|
|
345
|
+
// Deterministic resolution (§5.1): one merger, not dialogue — the
|
|
346
|
+
// earliest decision (lowest seq) wins and the coordinator records its
|
|
347
|
+
// own journal entry; the losing decision is excluded from the merge.
|
|
348
|
+
const excluded = new Set();
|
|
349
|
+
const resolutions = [];
|
|
350
|
+
let resolutionSeq = (seqs[seqs.length - 1] ?? 0) + 1;
|
|
351
|
+
for (const c of conflicts) {
|
|
352
|
+
const winner = c.entryA.seq <= c.entryB.seq ? c.entryA : c.entryB;
|
|
353
|
+
const loser = winner === c.entryA ? c.entryB : c.entryA;
|
|
354
|
+
excluded.add(loser);
|
|
355
|
+
const resolution = {
|
|
356
|
+
runId: targetRunId,
|
|
357
|
+
generationId,
|
|
358
|
+
workerId: 'coordinator',
|
|
359
|
+
seq: resolutionSeq++,
|
|
360
|
+
decidedFor: winner.decidedFor,
|
|
361
|
+
decidedAgainst: loser.decidedFor,
|
|
362
|
+
because: `coordinator resolution over '${c.resource}': ` +
|
|
363
|
+
`earliest decision (seq ${winner.seq}, ${winner.workerId}) is the single source of truth`,
|
|
364
|
+
touched: [],
|
|
365
|
+
ts: new Date().toISOString(),
|
|
366
|
+
};
|
|
367
|
+
deps.journal.append(resolution);
|
|
368
|
+
resolutions.push(resolution);
|
|
369
|
+
}
|
|
370
|
+
const merged = [...entries.filter(e => !excluded.has(e)), ...resolutions];
|
|
371
|
+
emit('run.reconciled', { entries: merged.length, conflicts: conflicts.length });
|
|
372
|
+
return { merged };
|
|
373
|
+
},
|
|
374
|
+
// §5.2 global-budget backstop (AC-11-8/9). Every iteration / tool dispatch
|
|
375
|
+
// is charged here; the run never proceeds past the first cap reached. A
|
|
376
|
+
// `global-budget` cap is a dead-end trigger (§5.3) → fork a fresh generation.
|
|
377
|
+
charge(cost) {
|
|
378
|
+
// Fail-closed: once halted, never charge or proceed again.
|
|
379
|
+
if (haltReason !== undefined) {
|
|
380
|
+
return { capped: true, reason: 'global-budget' };
|
|
381
|
+
}
|
|
382
|
+
const verdict = deps.budgetGuard.charge(runId, cost);
|
|
383
|
+
if ('capped' in verdict) {
|
|
384
|
+
emit('budget.capped', { reason: verdict.reason });
|
|
385
|
+
halt(verdict.reason);
|
|
386
|
+
if (verdict.reason === 'global-budget') {
|
|
387
|
+
// Dead-end: distillation is the model's job in production; the fork is code.
|
|
388
|
+
forkOnDeadEnd([{ summary: `run hit global budget cap` }]);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return verdict;
|
|
392
|
+
},
|
|
393
|
+
// ADR-0025 / AC-11-10: advisory only. N≥3 failures lower the strategy's
|
|
394
|
+
// priority in a worker's choice (recorded in the nested ledger); this
|
|
395
|
+
// never emits run.terminated and never blocks a spawn.
|
|
396
|
+
onSkillFailure(skill) {
|
|
397
|
+
nested.skillFailures[skill] = (nested.skillFailures[skill] ?? 0) + 1;
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// First-class sub-agent delegation (ADR-0039, spec §5.4/§5.5).
|
|
403
|
+
//
|
|
404
|
+
// A goal-DAG of DelegationTasks, each served by a sub-agent whose capabilities
|
|
405
|
+
// are fixed by an AgentCard (the model cannot self-widen). State hands off
|
|
406
|
+
// without loss: every delegation owns a hash-chained shard; the parent receives
|
|
407
|
+
// only a compact TaskObservation. Reuses the §5.1 scope-disjointness logic, the
|
|
408
|
+
// ADR-0020 Loop Guardian, and ScopeConflictError — nothing here is a new runtime.
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
const SHARD_GENESIS = '0'.repeat(64);
|
|
411
|
+
/** Deterministic JSON: object keys sorted recursively (stable hashing input). */
|
|
412
|
+
function stableStringify(value) {
|
|
413
|
+
return JSON.stringify(value, (_k, v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
414
|
+
? Object.fromEntries(Object.keys(v)
|
|
415
|
+
.sort()
|
|
416
|
+
.map(k => [k, v[k]]))
|
|
417
|
+
: v);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Deep clone a shard payload so neither the caller (who keeps a reference to
|
|
421
|
+
* what it passed to append) nor a post-mortem shard() reader can mutate an
|
|
422
|
+
* "immutable" audit entry after the fact. structuredClone handles cycles/Dates;
|
|
423
|
+
* the rare non-cloneable payload (e.g. a function) falls back to the original.
|
|
424
|
+
*/
|
|
425
|
+
function safeClone(v) {
|
|
426
|
+
try {
|
|
427
|
+
return structuredClone(v);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return v;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function cloneEntry(e) {
|
|
434
|
+
return { ...e, payload: safeClone(e.payload) };
|
|
435
|
+
}
|
|
436
|
+
function shardEntryHash(e) {
|
|
437
|
+
const payloadHash = createHash('sha256').update(stableStringify(e.payload), 'utf8').digest('hex');
|
|
438
|
+
return createHash('sha256')
|
|
439
|
+
.update(stableStringify({
|
|
440
|
+
delegationId: e.delegationId,
|
|
441
|
+
seq: e.seq,
|
|
442
|
+
prevHash: e.prevHash,
|
|
443
|
+
kind: e.kind,
|
|
444
|
+
ts: e.ts,
|
|
445
|
+
payloadHash,
|
|
446
|
+
}), 'utf8')
|
|
447
|
+
.digest('hex');
|
|
448
|
+
}
|
|
449
|
+
function isPlanDAG(plan) {
|
|
450
|
+
return Array.isArray(plan.nodes);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Normalize a Core (01) linear plan into a degenerate goal-DAG: each step
|
|
454
|
+
* becomes a node depending on the previous one, preserving sequential order
|
|
455
|
+
* (AC-11-16). A PlanDAG is returned unchanged. The Core `Plan` shape itself is
|
|
456
|
+
* never modified — the union lives here, at the orchestration layer.
|
|
457
|
+
*/
|
|
458
|
+
function normalizePlan(plan) {
|
|
459
|
+
if (isPlanDAG(plan))
|
|
460
|
+
return { nodes: [...plan.nodes], edges: [...plan.edges] };
|
|
461
|
+
const nodes = plan.steps.map((step, i) => ({
|
|
462
|
+
taskId: `s${i + 1}`,
|
|
463
|
+
intent: step.intent ?? '',
|
|
464
|
+
assignedTo: null,
|
|
465
|
+
dependsOn: i > 0 ? [`s${i}`] : [],
|
|
466
|
+
scope: { owns: [], doNotTouch: [], taskClass: 'reasoning' },
|
|
467
|
+
budgetSlice: { iterations: DEFAULT_WORKER_ITERATIONS, spendUsd: DEFAULT_WORKER_SPEND_USD },
|
|
468
|
+
outputContract: '',
|
|
469
|
+
retryPolicy: { maxReplans: 0, maxIterations: DEFAULT_WORKER_ITERATIONS },
|
|
470
|
+
}));
|
|
471
|
+
const edges = nodes.slice(1).map((n, i) => ({ from: `s${i + 1}`, to: n.taskId }));
|
|
472
|
+
return { nodes, edges };
|
|
473
|
+
}
|
|
474
|
+
export function makeDelegationManager(plan, deps) {
|
|
475
|
+
const runId = `r-${randomUUID().slice(0, 8)}`;
|
|
476
|
+
const dagPlan = normalizePlan(plan);
|
|
477
|
+
const tasksById = new Map(dagPlan.nodes.map(t => [t.taskId, t]));
|
|
478
|
+
const completed = new Set();
|
|
479
|
+
const failed = new Set();
|
|
480
|
+
const skipped = new Set();
|
|
481
|
+
const delegations = new Map();
|
|
482
|
+
const checkpoints = new Map();
|
|
483
|
+
const runBudget = { iterations: 0, spendUsd: 0, wallMs: 0 };
|
|
484
|
+
const delegationIdFor = (taskId) => `d-${taskId}`;
|
|
485
|
+
function emit(kind, payload) {
|
|
486
|
+
deps.emit({ kind, runId, ts: new Date().toISOString(), payload });
|
|
487
|
+
}
|
|
488
|
+
function isTerminal(taskId) {
|
|
489
|
+
return completed.has(taskId) || failed.has(taskId) || skipped.has(taskId);
|
|
490
|
+
}
|
|
491
|
+
/** Owns of every delegation still holding write scope (active or resumed). */
|
|
492
|
+
function activeOwns(exceptId) {
|
|
493
|
+
const out = [];
|
|
494
|
+
for (const [id, st] of delegations) {
|
|
495
|
+
if (id === exceptId)
|
|
496
|
+
continue;
|
|
497
|
+
if (st.status === 'active' || st.status === 'resumed')
|
|
498
|
+
out.push({ id, owns: st.owns });
|
|
499
|
+
}
|
|
500
|
+
return out;
|
|
501
|
+
}
|
|
502
|
+
function addCost(cost) {
|
|
503
|
+
runBudget.iterations += cost.iterations;
|
|
504
|
+
runBudget.spendUsd += cost.spendUsd;
|
|
505
|
+
runBudget.wallMs += cost.wallMs;
|
|
506
|
+
}
|
|
507
|
+
function appendShard(state, delegationId, kind, payload) {
|
|
508
|
+
const prev = state.entries[state.entries.length - 1];
|
|
509
|
+
const seq = (prev?.seq ?? 0) + 1;
|
|
510
|
+
const prevHash = prev?.hash ?? SHARD_GENESIS;
|
|
511
|
+
const ts = new Date().toISOString();
|
|
512
|
+
// Sever the caller's reference: store an independent deep copy so the
|
|
513
|
+
// append-only shard stays tamper-evident (a circular payload fails fast at
|
|
514
|
+
// the hash step below, never reaching storage).
|
|
515
|
+
const stored = safeClone(payload);
|
|
516
|
+
const base = { delegationId, seq, prevHash, kind, payload: stored, ts };
|
|
517
|
+
const entry = { ...base, hash: shardEntryHash(base) };
|
|
518
|
+
state.entries.push(entry);
|
|
519
|
+
return cloneEntry(entry);
|
|
520
|
+
}
|
|
521
|
+
function writeCheckpoint(delegationId, state) {
|
|
522
|
+
const last = state.entries[state.entries.length - 1];
|
|
523
|
+
checkpoints.set(delegationId, {
|
|
524
|
+
delegationId,
|
|
525
|
+
taskId: state.task.taskId,
|
|
526
|
+
scope: state.task.scope,
|
|
527
|
+
snapshotPrefixHash: last?.hash ?? SHARD_GENESIS,
|
|
528
|
+
lastSeq: last?.seq ?? 0,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function makeHandle(delegationId, state) {
|
|
532
|
+
return {
|
|
533
|
+
delegationId,
|
|
534
|
+
taskId: state.task.taskId,
|
|
535
|
+
card: state.card,
|
|
536
|
+
owns: [...state.owns],
|
|
537
|
+
writableMcp: [...state.writableMcp],
|
|
538
|
+
permitsTool: (name) => state.permittedTools.has(name),
|
|
539
|
+
permitsMcp: (server) => state.writableMcp.includes(server),
|
|
540
|
+
append: (kind, payload) => appendShard(state, delegationId, kind, payload),
|
|
541
|
+
shard: () => state.entries.map(cloneEntry),
|
|
542
|
+
get guardian() {
|
|
543
|
+
return state.guardian;
|
|
544
|
+
},
|
|
545
|
+
complete: (summary, result, cost) => {
|
|
546
|
+
state.status = 'completed';
|
|
547
|
+
completed.add(state.task.taskId);
|
|
548
|
+
addCost(cost);
|
|
549
|
+
emit('delegation.completed', { delegationId, taskId: state.task.taskId });
|
|
550
|
+
return { delegationId, status: 'completed', summary, touched: [...state.owns], result, cost };
|
|
551
|
+
},
|
|
552
|
+
fail: (summary, cost) => {
|
|
553
|
+
state.status = 'failed';
|
|
554
|
+
failed.add(state.task.taskId);
|
|
555
|
+
addCost(cost);
|
|
556
|
+
writeCheckpoint(delegationId, state);
|
|
557
|
+
emit('delegation.failed', { delegationId, taskId: state.task.taskId });
|
|
558
|
+
return { delegationId, status: 'failed', summary, touched: [...state.owns], result: undefined, cost };
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
dag() {
|
|
564
|
+
return { nodes: [...dagPlan.nodes], edges: [...dagPlan.edges] };
|
|
565
|
+
},
|
|
566
|
+
readySet() {
|
|
567
|
+
// Deterministic: input order, a task ready only when every dependency has
|
|
568
|
+
// COMPLETED and it has not itself run / been spawned (AC-11-16).
|
|
569
|
+
return dagPlan.nodes.filter(t => !isTerminal(t.taskId) &&
|
|
570
|
+
!delegations.has(delegationIdFor(t.taskId)) &&
|
|
571
|
+
t.dependsOn.every(dep => completed.has(dep)));
|
|
572
|
+
},
|
|
573
|
+
spawn(taskId, requested) {
|
|
574
|
+
const task = tasksById.get(taskId);
|
|
575
|
+
if (task === undefined)
|
|
576
|
+
throw new Error(`unknown delegation task '${taskId}'`);
|
|
577
|
+
const delegationId = delegationIdFor(taskId);
|
|
578
|
+
if (delegations.has(delegationId)) {
|
|
579
|
+
throw new Error(`delegation '${delegationId}' already spawned — use resume()`);
|
|
580
|
+
}
|
|
581
|
+
if (failed.has(taskId) || skipped.has(taskId)) {
|
|
582
|
+
throw new Error(`task '${taskId}' is terminal (failed/cascade-skipped) and not runnable`);
|
|
583
|
+
}
|
|
584
|
+
// Deterministic ready-set: refuse a task whose upstream has not completed.
|
|
585
|
+
const unmet = task.dependsOn.filter(d => !completed.has(d));
|
|
586
|
+
if (unmet.length > 0) {
|
|
587
|
+
throw new Error(`task '${taskId}' not ready: unmet dependencies [${unmet.join(', ')}]`);
|
|
588
|
+
}
|
|
589
|
+
if (task.assignedTo === null) {
|
|
590
|
+
throw new Error(`task '${taskId}' has no assigned AgentCard`);
|
|
591
|
+
}
|
|
592
|
+
const card = deps.resolveCard(task.assignedTo);
|
|
593
|
+
if (card === undefined)
|
|
594
|
+
throw new Error(`AgentCard '${task.assignedTo}' not found`);
|
|
595
|
+
// §5.5 scope composition. The granted lane is owns minus doNotTouch; a
|
|
596
|
+
// declared skill writing outside it is a ScopeConflictError and no
|
|
597
|
+
// sub-agent starts. owns = task.scope.owns ∪ skill-touched, which equals
|
|
598
|
+
// task.scope.owns precisely because skill-touched ⊆ the granted lane.
|
|
599
|
+
const inGrantedLane = (p) => task.scope.owns.some(g => globMatches(g, p)) &&
|
|
600
|
+
!task.scope.doNotTouch.some(g => globMatches(g, p));
|
|
601
|
+
const skillTouched = card.skills.flatMap(s => deps.skillTouchedPaths(s));
|
|
602
|
+
const outOfLane = skillTouched.filter(p => !inGrantedLane(p));
|
|
603
|
+
if (outOfLane.length > 0) {
|
|
604
|
+
emit('scope.violation', { delegationId, taskId, reason: 'skill-outside-lane', paths: outOfLane });
|
|
605
|
+
throw new ScopeConflictError(delegationId, `card:${card.name}`, outOfLane);
|
|
606
|
+
}
|
|
607
|
+
const owns = [...task.scope.owns];
|
|
608
|
+
// Pairwise write-disjointness across ALL active delegations (§5.5).
|
|
609
|
+
for (const other of activeOwns()) {
|
|
610
|
+
const overlap = overlappingPaths(owns, other.owns);
|
|
611
|
+
if (overlap.length > 0)
|
|
612
|
+
throw new ScopeConflictError(other.id, delegationId, overlap);
|
|
613
|
+
}
|
|
614
|
+
// Capabilities are the card's; any model-emitted request that exceeds the
|
|
615
|
+
// card is ignored — the card is the sole authority (AC-11-17).
|
|
616
|
+
void requested;
|
|
617
|
+
const permittedTools = new Set(Object.keys(card.toolTiers));
|
|
618
|
+
const writableMcp = card.mcpAllowlist.filter(s => deps.mcpWritable(s));
|
|
619
|
+
const state = {
|
|
620
|
+
task,
|
|
621
|
+
card,
|
|
622
|
+
owns,
|
|
623
|
+
writableMcp,
|
|
624
|
+
permittedTools,
|
|
625
|
+
entries: [],
|
|
626
|
+
guardian: makeLoopGuardian({}),
|
|
627
|
+
status: 'active',
|
|
628
|
+
};
|
|
629
|
+
delegations.set(delegationId, state);
|
|
630
|
+
emit('delegation.spawned', { delegationId, taskId, owns, card: card.name });
|
|
631
|
+
return makeHandle(delegationId, state);
|
|
632
|
+
},
|
|
633
|
+
resume(delegationId) {
|
|
634
|
+
const state = delegations.get(delegationId);
|
|
635
|
+
if (state === undefined)
|
|
636
|
+
throw new Error(`cannot resume unknown delegation '${delegationId}'`);
|
|
637
|
+
const cp = checkpoints.get(delegationId);
|
|
638
|
+
if (cp === undefined)
|
|
639
|
+
throw new Error(`no checkpoint for '${delegationId}' — nothing to resume`);
|
|
640
|
+
// Resume from checkpoint.lastSeq: the shard already holds entries up to
|
|
641
|
+
// lastSeq, so further appends continue the chain. Reset ONLY the local
|
|
642
|
+
// Loop Guardian; the run-level budget is inherited (no budget-reset
|
|
643
|
+
// evasion, §8) and downstream cascade state is untouched (AC-11-20).
|
|
644
|
+
failed.delete(state.task.taskId);
|
|
645
|
+
state.status = 'resumed';
|
|
646
|
+
state.guardian = makeLoopGuardian({});
|
|
647
|
+
emit('delegation.resumed', { delegationId, fromSeq: cp.lastSeq });
|
|
648
|
+
return makeHandle(delegationId, state);
|
|
649
|
+
},
|
|
650
|
+
schedule() {
|
|
651
|
+
// Cascade-skip: any non-terminal task with a failed or already-skipped
|
|
652
|
+
// ancestor is skipped, transitively, with an explicit journal entry — no
|
|
653
|
+
// silent drop (AC-11-20). Once skipped, a task is terminal and not re-emitted.
|
|
654
|
+
let changed = true;
|
|
655
|
+
while (changed) {
|
|
656
|
+
changed = false;
|
|
657
|
+
for (const t of dagPlan.nodes) {
|
|
658
|
+
if (isTerminal(t.taskId))
|
|
659
|
+
continue;
|
|
660
|
+
const blocked = t.dependsOn.some(d => failed.has(d) || skipped.has(d));
|
|
661
|
+
if (blocked) {
|
|
662
|
+
skipped.add(t.taskId);
|
|
663
|
+
emit('cascade-skip', { taskId: t.taskId, reason: 'upstream-failed' });
|
|
664
|
+
changed = true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const ready = dagPlan.nodes
|
|
669
|
+
.filter(t => !isTerminal(t.taskId) &&
|
|
670
|
+
!delegations.has(delegationIdFor(t.taskId)) &&
|
|
671
|
+
t.dependsOn.every(dep => completed.has(dep)))
|
|
672
|
+
.map(t => t.taskId);
|
|
673
|
+
const cascadeSkipped = dagPlan.nodes.filter(t => skipped.has(t.taskId)).map(t => t.taskId);
|
|
674
|
+
return { ready, cascadeSkipped };
|
|
675
|
+
},
|
|
676
|
+
runBudgetSpent() {
|
|
677
|
+
return { ...runBudget };
|
|
678
|
+
},
|
|
679
|
+
verifyShardChain(delegationId) {
|
|
680
|
+
const state = delegations.get(delegationId);
|
|
681
|
+
if (state === undefined)
|
|
682
|
+
return false;
|
|
683
|
+
let prev = SHARD_GENESIS;
|
|
684
|
+
for (let i = 0; i < state.entries.length; i++) {
|
|
685
|
+
const e = state.entries[i];
|
|
686
|
+
if (e.seq !== i + 1)
|
|
687
|
+
return false;
|
|
688
|
+
if (e.prevHash !== prev)
|
|
689
|
+
return false;
|
|
690
|
+
const recomputed = shardEntryHash({
|
|
691
|
+
delegationId: e.delegationId,
|
|
692
|
+
seq: e.seq,
|
|
693
|
+
prevHash: e.prevHash,
|
|
694
|
+
kind: e.kind,
|
|
695
|
+
payload: e.payload,
|
|
696
|
+
ts: e.ts,
|
|
697
|
+
});
|
|
698
|
+
if (recomputed !== e.hash)
|
|
699
|
+
return false;
|
|
700
|
+
prev = e.hash;
|
|
701
|
+
}
|
|
702
|
+
return true;
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
//# sourceMappingURL=index.js.map
|