omk-agent-core 0.99.0 → 1.2.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.
@@ -0,0 +1,190 @@
1
+ /**
2
+ * ECRAF — resource-aware ready queue admission planner.
3
+ *
4
+ * Given the current ready frontier, a free-slot count, and multi-dimensional
5
+ * resource capacities, choose which ready nodes to admit next using the
6
+ * deterministic greedy baseline the deep-research report prescribes (PR 8).
7
+ *
8
+ * legacy-v1: D_i = P_i / (epsilon + sum_r weight_r * a_ir)
9
+ * normalized-v2: D_i = P_i / (epsilon + slotCost + sum_r weight_r * a_ir / s_r)
10
+ * Normalization is opt-in, not a live scheduler or a measured improvement.
11
+ *
12
+ * then sorted by density descending, readySeq ascending, sourceIndex ascending.
13
+ * A candidate is admitted only if admitting it does not push cumulative
14
+ * running usage over any capacity and it does not semantically conflict with a
15
+ * node already admitted in this pass. Admission is pure: it computes the plan;
16
+ * the caller launches.
17
+ */
18
+ /**
19
+ * Numeric contract for one admission pass (audit §5.3): every resource
20
+ * demand, running usage, and capacity is a finite non-negative number; the
21
+ * slot budget is a non-negative integer; epsilon is finite and positive;
22
+ * ranking weights are finite and non-negative; candidate source indices and
23
+ * ready sequences are unique, finite integers. Violating these invariants
24
+ * silently poisons the plan — NaN makes every capacity check pass, negative
25
+ * demand manufactures capacity, a fractional slot admits ⌈slots⌉ nodes, and
26
+ * duplicate source indices make the admit list ambiguous — so malformed input
27
+ * is rejected up front rather than coerced into "unbounded".
28
+ */
29
+ function assertFiniteNonNegative(value, label) {
30
+ if (!Number.isFinite(value) || value < 0) {
31
+ throw new RangeError(`${label} must be a finite non-negative number, got ${String(value)}`);
32
+ }
33
+ }
34
+ function assertInputContract(options) {
35
+ const { candidates, runningUsage, capacities, slots, epsilon = 1e-6, resourceWeights = {}, referenceScales, slotCost = 1, } = options;
36
+ if (!Number.isInteger(slots) || slots < 0) {
37
+ throw new RangeError(`slots must be a non-negative integer, got ${String(slots)}`);
38
+ }
39
+ if (!Number.isFinite(epsilon) || epsilon <= 0) {
40
+ throw new RangeError(`epsilon must be a finite positive number, got ${String(epsilon)}`);
41
+ }
42
+ const seenSourceIndices = new Set();
43
+ const seenReadySeqs = new Set();
44
+ for (const [position, node] of candidates.entries()) {
45
+ if (!Number.isInteger(node.sourceIndex) || node.sourceIndex < 0) {
46
+ throw new RangeError(`candidates[${position}].sourceIndex must be a non-negative integer, got ${String(node.sourceIndex)}`);
47
+ }
48
+ if (seenSourceIndices.has(node.sourceIndex)) {
49
+ throw new RangeError(`duplicate candidates[].sourceIndex ${node.sourceIndex} — admit results must be unique per node`);
50
+ }
51
+ seenSourceIndices.add(node.sourceIndex);
52
+ if (!Number.isInteger(node.readySeq) || node.readySeq < 0) {
53
+ throw new RangeError(`candidates[${position}].readySeq must be a non-negative integer, got ${String(node.readySeq)}`);
54
+ }
55
+ if (seenReadySeqs.has(node.readySeq)) {
56
+ throw new RangeError(`duplicate candidates[].readySeq ${node.readySeq} — ready ordering must be unambiguous`);
57
+ }
58
+ seenReadySeqs.add(node.readySeq);
59
+ assertFiniteNonNegative(node.priority, `candidates[${position}].priority`);
60
+ for (const [name, demand] of Object.entries(node.resources)) {
61
+ assertFiniteNonNegative(demand, `candidates[${position}].resources.${name}`);
62
+ }
63
+ }
64
+ for (const [name, value] of Object.entries(runningUsage)) {
65
+ assertFiniteNonNegative(value, `runningUsage.${name}`);
66
+ }
67
+ for (const [name, value] of Object.entries(capacities)) {
68
+ assertFiniteNonNegative(value, `capacities.${name}`);
69
+ }
70
+ for (const [name, value] of Object.entries(resourceWeights)) {
71
+ assertFiniteNonNegative(value, `resourceWeights.${name}`);
72
+ }
73
+ if (referenceScales !== undefined) {
74
+ if (!Number.isFinite(slotCost) || slotCost <= 0) {
75
+ throw new RangeError(`slotCost must be a finite positive number, got ${String(slotCost)}`);
76
+ }
77
+ for (const [name, scale] of Object.entries(referenceScales)) {
78
+ if (!Number.isFinite(scale) || scale <= 0) {
79
+ throw new RangeError(`referenceScales.${name} must be a finite positive number, got ${String(scale)}`);
80
+ }
81
+ }
82
+ for (const [position, node] of candidates.entries()) {
83
+ for (const [name, demand] of Object.entries(node.resources)) {
84
+ if (demand > 0 && capacities[name] !== 0 && referenceScales[name] === undefined) {
85
+ throw new RangeError(`referenceScales.${name} is required for a nonzero ${name} demand (candidates[${position}])`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ function resourceCost(node, weights) {
92
+ let cost = 0;
93
+ for (const name of Object.keys(node.resources)) {
94
+ const weight = weights[name] ?? 1;
95
+ cost += weight * (node.resources[name] ?? 0);
96
+ }
97
+ return cost;
98
+ }
99
+ function density(node, weights, epsilon, referenceScales, slotCost = 1) {
100
+ // Normalized mode divides each demand by its reference scale, so per-resource
101
+ // unit changes that scale demand and reference together cancel exactly.
102
+ const raw = referenceScales === undefined
103
+ ? resourceCost(node, weights)
104
+ : [...Object.entries(node.resources)].reduce((cost, [name, demand]) => demand === 0 ? cost : cost + (weights[name] ?? 1) * (demand / referenceScales[name]), 0);
105
+ const denominator = referenceScales === undefined ? epsilon + raw : epsilon + slotCost + raw;
106
+ assertFiniteNonNegative(denominator, `candidate ${node.sourceIndex} density denominator`);
107
+ const score = node.priority / denominator;
108
+ assertFiniteNonNegative(score, `candidate ${node.sourceIndex} density`);
109
+ return score;
110
+ }
111
+ function fits(node, used, capacities, normalized) {
112
+ for (const name of Object.keys(node.resources)) {
113
+ const capacity = capacities[name];
114
+ if (capacity === undefined)
115
+ continue; // unbounded resource
116
+ const needed = node.resources[name] ?? 0;
117
+ if (normalized && capacity === 0 && needed === 0)
118
+ continue;
119
+ const running = used.get(name) ?? 0;
120
+ if (running + needed > capacity)
121
+ return false;
122
+ }
123
+ return true;
124
+ }
125
+ function reserve(node, used) {
126
+ for (const name of Object.keys(node.resources)) {
127
+ const total = (used.get(name) ?? 0) + (node.resources[name] ?? 0);
128
+ assertFiniteNonNegative(total, `candidate ${node.sourceIndex} reserved usage.${name}`);
129
+ used.set(name, total);
130
+ }
131
+ }
132
+ /**
133
+ * Greedy admission pass. Deterministic: identical inputs always produce the
134
+ * identical plan. `admit` is ordered by launch preference (density, then
135
+ * readySeq, then sourceIndex); `deferred` preserves source order.
136
+ */
137
+ export function planEcrafAdmissions(options) {
138
+ const version = options.algorithmVersion ?? (options.referenceScales === undefined ? "legacy-v1" : "normalized-v2");
139
+ if (version !== "legacy-v1" && version !== "normalized-v2") {
140
+ throw new RangeError(`Unknown ECRAF algorithmVersion: ${String(version)}`);
141
+ }
142
+ if (version === "legacy-v1" && (options.referenceScales !== undefined || options.slotCost !== undefined)) {
143
+ throw new RangeError("referenceScales and slotCost require normalized-v2");
144
+ }
145
+ if (version === "normalized-v2") {
146
+ options = {
147
+ ...options,
148
+ referenceScales: {
149
+ ...Object.fromEntries(Object.entries(options.capacities).filter(([, capacity]) => capacity > 0)),
150
+ ...options.referenceScales,
151
+ },
152
+ };
153
+ }
154
+ assertInputContract(options);
155
+ const { candidates, runningUsage, capacities, slots, epsilon = 1e-6, resourceWeights = {}, conflicts, referenceScales, slotCost = 1, } = options;
156
+ // Seed running usage so newly admitted nodes consume from the same budget.
157
+ const used = new Map(Object.entries(runningUsage));
158
+ // Zero-capacity infeasibility precedes scoring in v2. Validate all other
159
+ // scores even for singleton and zero-slot batches, before any callbacks.
160
+ const infeasible = new Set(version === "normalized-v2"
161
+ ? candidates.filter((node) => Object.entries(node.resources).some(([name, demand]) => capacities[name] === 0 && demand > 0))
162
+ : []);
163
+ const sorted = candidates
164
+ .filter((node) => !infeasible.has(node))
165
+ .map((node) => ({ node, score: density(node, resourceWeights, epsilon, referenceScales, slotCost) }))
166
+ .sort((a, b) => b.score - a.score || a.node.readySeq - b.node.readySeq || a.node.sourceIndex - b.node.sourceIndex);
167
+ const admit = [];
168
+ const deferred = [...infeasible].map((node) => node.sourceIndex);
169
+ const admittedNodes = [];
170
+ for (const { node } of sorted) {
171
+ if (admit.length >= slots) {
172
+ deferred.push(node.sourceIndex);
173
+ continue;
174
+ }
175
+ if (conflicts && admittedNodes.some((admitted) => conflicts(node, admitted))) {
176
+ deferred.push(node.sourceIndex);
177
+ continue;
178
+ }
179
+ if (!fits(node, used, capacities, version === "normalized-v2")) {
180
+ deferred.push(node.sourceIndex);
181
+ continue;
182
+ }
183
+ reserve(node, used);
184
+ admittedNodes.push(node);
185
+ admit.push(node.sourceIndex);
186
+ }
187
+ deferred.sort((a, b) => a - b);
188
+ return { admit, deferred };
189
+ }
190
+ //# sourceMappingURL=tool-dag-ecraf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-dag-ecraf.js","sourceRoot":"","sources":["../src/tool-dag-ecraf.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ECRAF — resource-aware ready queue admission planner.\n *\n * Given the current ready frontier, a free-slot count, and multi-dimensional\n * resource capacities, choose which ready nodes to admit next using the\n * deterministic greedy baseline the deep-research report prescribes (PR 8).\n *\n * legacy-v1: D_i = P_i / (epsilon + sum_r weight_r * a_ir)\n * normalized-v2: D_i = P_i / (epsilon + slotCost + sum_r weight_r * a_ir / s_r)\n * Normalization is opt-in, not a live scheduler or a measured improvement.\n *\n * then sorted by density descending, readySeq ascending, sourceIndex ascending.\n * A candidate is admitted only if admitting it does not push cumulative\n * running usage over any capacity and it does not semantically conflict with a\n * node already admitted in this pass. Admission is pure: it computes the plan;\n * the caller launches.\n */\n\nexport interface EcrafCandidate {\n\t/** Source order of the call within the batch — the stable tiebreak. */\n\treadonly sourceIndex: number;\n\t/** Monotonic order in which the node first entered the ready frontier. */\n\treadonly readySeq: number;\n\t/** Resource vector: resource name -> units consumed while running. */\n\treadonly resources: Readonly<Record<string, number>>;\n\t/** Scalar priority P_i already folded (rank, aging, evidence, risk...). */\n\treadonly priority: number;\n}\n\nexport interface EcrafAdmissionsOptions {\n\t/** Omitted: legacy-v1, or normalized-v2 when referenceScales is supplied. */\n\treadonly algorithmVersion?: \"legacy-v1\" | \"normalized-v2\";\n\treadonly candidates: readonly EcrafCandidate[];\n\t/** Resource units already held by running nodes: name -> units. */\n\treadonly runningUsage: Readonly<Record<string, number>>;\n\t/** Hard capacity per resource: name -> max units. Missing = unbounded. */\n\treadonly capacities: Readonly<Record<string, number>>;\n\t/** Free admission slots (concurrency budget). */\n\treadonly slots: number;\n\t/** Density epsilon guarding division-by-zero. Default 1e-6. */\n\treadonly epsilon?: number;\n\t/** Optional weight per resource in the density denominator. Default 1. */\n\treadonly resourceWeights?: Readonly<Record<string, number>>;\n\t/**\n\t * Opt-in dimensionless normalization (audit §13.2): resource name -> positive\n\t * reference scale s_r. When provided, the density denominator uses unitless\n\t * demands a_ir/s_r, making ranking invariant to per-resource unit changes\n\t * (a'_ir = c_r·a_ir with s'_ir = c_r·s_r preserves the mathematical ratio).\n\t * In normalized-v2, omitted entries use positive total capacities, never\n\t * remaining headroom. Positive unbounded demands require explicit scales.\n\t * Zero capacity is a feasibility gate, not a scale. All supplied scales must\n\t * be finite and positive. Supplying this map without a version opts into v2.\n\t */\n\treadonly referenceScales?: Readonly<Record<string, number>>;\n\t/** Finite positive slot term λ_slot in normalized-v2. Default 1; invalid in v1. */\n\treadonly slotCost?: number;\n\t/**\n\t * Semantic-conflict predicate. Return true when the candidate may not run\n\t * concurrently with an already-admitted node. Defaults to no conflicts.\n\t */\n\treadonly conflicts?: (candidate: EcrafCandidate, running: EcrafCandidate) => boolean;\n}\n\nexport interface EcrafAdmissionPlan {\n\t/** Source indices admitted, in the order they should be launched. */\n\treadonly admit: readonly number[];\n\t/** Source indices deferred this pass (capacity or conflict), in source order. */\n\treadonly deferred: readonly number[];\n}\n\n/**\n * Numeric contract for one admission pass (audit §5.3): every resource\n * demand, running usage, and capacity is a finite non-negative number; the\n * slot budget is a non-negative integer; epsilon is finite and positive;\n * ranking weights are finite and non-negative; candidate source indices and\n * ready sequences are unique, finite integers. Violating these invariants\n * silently poisons the plan — NaN makes every capacity check pass, negative\n * demand manufactures capacity, a fractional slot admits ⌈slots⌉ nodes, and\n * duplicate source indices make the admit list ambiguous — so malformed input\n * is rejected up front rather than coerced into \"unbounded\".\n */\nfunction assertFiniteNonNegative(value: number, label: string): void {\n\tif (!Number.isFinite(value) || value < 0) {\n\t\tthrow new RangeError(`${label} must be a finite non-negative number, got ${String(value)}`);\n\t}\n}\n\nfunction assertInputContract(options: EcrafAdmissionsOptions): void {\n\tconst {\n\t\tcandidates,\n\t\trunningUsage,\n\t\tcapacities,\n\t\tslots,\n\t\tepsilon = 1e-6,\n\t\tresourceWeights = {},\n\t\treferenceScales,\n\t\tslotCost = 1,\n\t} = options;\n\n\tif (!Number.isInteger(slots) || slots < 0) {\n\t\tthrow new RangeError(`slots must be a non-negative integer, got ${String(slots)}`);\n\t}\n\tif (!Number.isFinite(epsilon) || epsilon <= 0) {\n\t\tthrow new RangeError(`epsilon must be a finite positive number, got ${String(epsilon)}`);\n\t}\n\n\tconst seenSourceIndices = new Set<number>();\n\tconst seenReadySeqs = new Set<number>();\n\tfor (const [position, node] of candidates.entries()) {\n\t\tif (!Number.isInteger(node.sourceIndex) || node.sourceIndex < 0) {\n\t\t\tthrow new RangeError(\n\t\t\t\t`candidates[${position}].sourceIndex must be a non-negative integer, got ${String(node.sourceIndex)}`,\n\t\t\t);\n\t\t}\n\t\tif (seenSourceIndices.has(node.sourceIndex)) {\n\t\t\tthrow new RangeError(\n\t\t\t\t`duplicate candidates[].sourceIndex ${node.sourceIndex} — admit results must be unique per node`,\n\t\t\t);\n\t\t}\n\t\tseenSourceIndices.add(node.sourceIndex);\n\t\tif (!Number.isInteger(node.readySeq) || node.readySeq < 0) {\n\t\t\tthrow new RangeError(\n\t\t\t\t`candidates[${position}].readySeq must be a non-negative integer, got ${String(node.readySeq)}`,\n\t\t\t);\n\t\t}\n\t\tif (seenReadySeqs.has(node.readySeq)) {\n\t\t\tthrow new RangeError(`duplicate candidates[].readySeq ${node.readySeq} — ready ordering must be unambiguous`);\n\t\t}\n\t\tseenReadySeqs.add(node.readySeq);\n\t\tassertFiniteNonNegative(node.priority, `candidates[${position}].priority`);\n\t\tfor (const [name, demand] of Object.entries(node.resources)) {\n\t\t\tassertFiniteNonNegative(demand, `candidates[${position}].resources.${name}`);\n\t\t}\n\t}\n\n\tfor (const [name, value] of Object.entries(runningUsage)) {\n\t\tassertFiniteNonNegative(value, `runningUsage.${name}`);\n\t}\n\tfor (const [name, value] of Object.entries(capacities)) {\n\t\tassertFiniteNonNegative(value, `capacities.${name}`);\n\t}\n\tfor (const [name, value] of Object.entries(resourceWeights)) {\n\t\tassertFiniteNonNegative(value, `resourceWeights.${name}`);\n\t}\n\tif (referenceScales !== undefined) {\n\t\tif (!Number.isFinite(slotCost) || slotCost <= 0) {\n\t\t\tthrow new RangeError(`slotCost must be a finite positive number, got ${String(slotCost)}`);\n\t\t}\n\t\tfor (const [name, scale] of Object.entries(referenceScales)) {\n\t\t\tif (!Number.isFinite(scale) || scale <= 0) {\n\t\t\t\tthrow new RangeError(`referenceScales.${name} must be a finite positive number, got ${String(scale)}`);\n\t\t\t}\n\t\t}\n\t\tfor (const [position, node] of candidates.entries()) {\n\t\t\tfor (const [name, demand] of Object.entries(node.resources)) {\n\t\t\t\tif (demand > 0 && capacities[name] !== 0 && referenceScales[name] === undefined) {\n\t\t\t\t\tthrow new RangeError(\n\t\t\t\t\t\t`referenceScales.${name} is required for a nonzero ${name} demand (candidates[${position}])`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction resourceCost(node: EcrafCandidate, weights: Readonly<Record<string, number>>): number {\n\tlet cost = 0;\n\tfor (const name of Object.keys(node.resources)) {\n\t\tconst weight = weights[name] ?? 1;\n\t\tcost += weight * (node.resources[name] ?? 0);\n\t}\n\treturn cost;\n}\n\nfunction density(\n\tnode: EcrafCandidate,\n\tweights: Readonly<Record<string, number>>,\n\tepsilon: number,\n\treferenceScales?: Readonly<Record<string, number>>,\n\tslotCost = 1,\n): number {\n\t// Normalized mode divides each demand by its reference scale, so per-resource\n\t// unit changes that scale demand and reference together cancel exactly.\n\tconst raw =\n\t\treferenceScales === undefined\n\t\t\t? resourceCost(node, weights)\n\t\t\t: [...Object.entries(node.resources)].reduce(\n\t\t\t\t\t(cost, [name, demand]) =>\n\t\t\t\t\t\tdemand === 0 ? cost : cost + (weights[name] ?? 1) * (demand / referenceScales[name]),\n\t\t\t\t\t0,\n\t\t\t\t);\n\tconst denominator = referenceScales === undefined ? epsilon + raw : epsilon + slotCost + raw;\n\tassertFiniteNonNegative(denominator, `candidate ${node.sourceIndex} density denominator`);\n\tconst score = node.priority / denominator;\n\tassertFiniteNonNegative(score, `candidate ${node.sourceIndex} density`);\n\treturn score;\n}\n\nfunction fits(\n\tnode: EcrafCandidate,\n\tused: Map<string, number>,\n\tcapacities: Readonly<Record<string, number>>,\n\tnormalized: boolean,\n): boolean {\n\tfor (const name of Object.keys(node.resources)) {\n\t\tconst capacity = capacities[name];\n\t\tif (capacity === undefined) continue; // unbounded resource\n\t\tconst needed = node.resources[name] ?? 0;\n\t\tif (normalized && capacity === 0 && needed === 0) continue;\n\t\tconst running = used.get(name) ?? 0;\n\t\tif (running + needed > capacity) return false;\n\t}\n\treturn true;\n}\n\nfunction reserve(node: EcrafCandidate, used: Map<string, number>): void {\n\tfor (const name of Object.keys(node.resources)) {\n\t\tconst total = (used.get(name) ?? 0) + (node.resources[name] ?? 0);\n\t\tassertFiniteNonNegative(total, `candidate ${node.sourceIndex} reserved usage.${name}`);\n\t\tused.set(name, total);\n\t}\n}\n\n/**\n * Greedy admission pass. Deterministic: identical inputs always produce the\n * identical plan. `admit` is ordered by launch preference (density, then\n * readySeq, then sourceIndex); `deferred` preserves source order.\n */\nexport function planEcrafAdmissions(options: EcrafAdmissionsOptions): EcrafAdmissionPlan {\n\tconst version = options.algorithmVersion ?? (options.referenceScales === undefined ? \"legacy-v1\" : \"normalized-v2\");\n\tif (version !== \"legacy-v1\" && version !== \"normalized-v2\") {\n\t\tthrow new RangeError(`Unknown ECRAF algorithmVersion: ${String(version)}`);\n\t}\n\tif (version === \"legacy-v1\" && (options.referenceScales !== undefined || options.slotCost !== undefined)) {\n\t\tthrow new RangeError(\"referenceScales and slotCost require normalized-v2\");\n\t}\n\tif (version === \"normalized-v2\") {\n\t\toptions = {\n\t\t\t...options,\n\t\t\treferenceScales: {\n\t\t\t\t...Object.fromEntries(Object.entries(options.capacities).filter(([, capacity]) => capacity > 0)),\n\t\t\t\t...options.referenceScales,\n\t\t\t},\n\t\t};\n\t}\n\tassertInputContract(options);\n\tconst {\n\t\tcandidates,\n\t\trunningUsage,\n\t\tcapacities,\n\t\tslots,\n\t\tepsilon = 1e-6,\n\t\tresourceWeights = {},\n\t\tconflicts,\n\t\treferenceScales,\n\t\tslotCost = 1,\n\t} = options;\n\n\t// Seed running usage so newly admitted nodes consume from the same budget.\n\tconst used = new Map<string, number>(Object.entries(runningUsage));\n\n\t// Zero-capacity infeasibility precedes scoring in v2. Validate all other\n\t// scores even for singleton and zero-slot batches, before any callbacks.\n\tconst infeasible = new Set(\n\t\tversion === \"normalized-v2\"\n\t\t\t? candidates.filter((node) =>\n\t\t\t\t\tObject.entries(node.resources).some(([name, demand]) => capacities[name] === 0 && demand > 0),\n\t\t\t\t)\n\t\t\t: [],\n\t);\n\tconst sorted = candidates\n\t\t.filter((node) => !infeasible.has(node))\n\t\t.map((node) => ({ node, score: density(node, resourceWeights, epsilon, referenceScales, slotCost) }))\n\t\t.sort(\n\t\t\t(a, b) => b.score - a.score || a.node.readySeq - b.node.readySeq || a.node.sourceIndex - b.node.sourceIndex,\n\t\t);\n\n\tconst admit: number[] = [];\n\tconst deferred: number[] = [...infeasible].map((node) => node.sourceIndex);\n\tconst admittedNodes: EcrafCandidate[] = [];\n\n\tfor (const { node } of sorted) {\n\t\tif (admit.length >= slots) {\n\t\t\tdeferred.push(node.sourceIndex);\n\t\t\tcontinue;\n\t\t}\n\t\tif (conflicts && admittedNodes.some((admitted) => conflicts(node, admitted))) {\n\t\t\tdeferred.push(node.sourceIndex);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!fits(node, used, capacities, version === \"normalized-v2\")) {\n\t\t\tdeferred.push(node.sourceIndex);\n\t\t\tcontinue;\n\t\t}\n\t\treserve(node, used);\n\t\tadmittedNodes.push(node);\n\t\tadmit.push(node.sourceIndex);\n\t}\n\n\tdeferred.sort((a, b) => a - b);\n\treturn { admit, deferred };\n}\n"]}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Per-run memo for DAG schedules. A plan is a pure function of the keyed
3
+ * inputs, so replaying it is safe — but only while every input the key cannot
4
+ * fingerprint stays stable. A custom `resourceKeyResolver` or any tool whose
5
+ * `resourceClaims` is a function closure can answer differently on identical
6
+ * call shapes, so those batches bypass the memo entirely (audit §9).
7
+ */
8
+ import { type DagSchedulePlan, type ScheduleDagLevelsOptions } from "./tool-dag-scheduler.ts";
9
+ import type { ClaimableToolCall } from "./tool-resource-claims.ts";
10
+ /** Bounded per-run memo for DAG schedules. */
11
+ export type DagScheduleCache = Map<string, DagSchedulePlan>;
12
+ export declare const DAG_SCHEDULE_CACHE_LIMIT = 64;
13
+ /**
14
+ * Schedule with a per-run memo. Identical batches (provider retries, stubborn
15
+ * re-emissions) re-resolve path identities and custom claims; the plan is a
16
+ * pure function of the canonical inputs, so replaying it is safe. Returns
17
+ * `null` when the underlying schedule was aborted. Cached levels are handed
18
+ * out as copies because callers append to and reorder them.
19
+ */
20
+ export declare function scheduleDagLevelsMemo(toolCalls: readonly ClaimableToolCall[], options: ScheduleDagLevelsOptions, signal: AbortSignal | undefined, cache: DagScheduleCache): Promise<DagSchedulePlan | null>;
21
+ //# sourceMappingURL=tool-dag-memo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-dag-memo.d.ts","sourceRoot":"","sources":["../src/tool-dag-memo.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,wBAAwB,EAAqB,MAAM,yBAAyB,CAAC;AAEjH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,8CAA8C;AAC9C,MAAM,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAE5D,eAAO,MAAM,wBAAwB,KAAK,CAAC;AA4B3C;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAC1C,SAAS,EAAE,SAAS,iBAAiB,EAAE,EACvC,OAAO,EAAE,wBAAwB,EACjC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,KAAK,EAAE,gBAAgB,GACrB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAmCjC","sourcesContent":["/**\n * Per-run memo for DAG schedules. A plan is a pure function of the keyed\n * inputs, so replaying it is safe — but only while every input the key cannot\n * fingerprint stays stable. A custom `resourceKeyResolver` or any tool whose\n * `resourceClaims` is a function closure can answer differently on identical\n * call shapes, so those batches bypass the memo entirely (audit §9).\n */\n\nimport { type DagSchedulePlan, type ScheduleDagLevelsOptions, scheduleDagLevels } from \"./tool-dag-scheduler.ts\";\nimport { awaitWithAbort } from \"./tool-execution-boundary.ts\";\nimport type { ClaimableToolCall } from \"./tool-resource-claims.ts\";\n\n/** Bounded per-run memo for DAG schedules. */\nexport type DagScheduleCache = Map<string, DagSchedulePlan>;\n\nexport const DAG_SCHEDULE_CACHE_LIMIT = 64;\n\n/**\n * Canonical key covering every input claim resolution depends on. A custom\n * `resourceKeyResolver` function cannot be fingerprinted, so callers skip the\n * memo entirely when one is configured. Within a run, tool definitions (and\n * their `resourceClaims` closures) are stable, so name/mode/claims-presence\n * fingerprints are sufficient.\n */\nfunction dagScheduleCacheKey(toolCalls: readonly ClaimableToolCall[], options: ScheduleDagLevelsOptions): string {\n\tconst policies = [...(options.toolPolicies?.entries() ?? [])].sort(([left], [right]) =>\n\t\tleft < right ? -1 : left > right ? 1 : 0,\n\t);\n\tconst registered = (options.registeredTools ?? []).map((tool) => [\n\t\ttool.name,\n\t\ttool.executionMode ?? \"\",\n\t\ttypeof tool.resourceClaims === \"function\" ? \"1\" : \"0\",\n\t]);\n\treturn JSON.stringify([\n\t\ttoolCalls.map((call) => [call.name, call.arguments ?? null]),\n\t\toptions.cwd,\n\t\toptions.strictExtensionClaims === true,\n\t\toptions.maxConcurrency ?? null,\n\t\tpolicies,\n\t\tregistered,\n\t]);\n}\n\n/**\n * Schedule with a per-run memo. Identical batches (provider retries, stubborn\n * re-emissions) re-resolve path identities and custom claims; the plan is a\n * pure function of the canonical inputs, so replaying it is safe. Returns\n * `null` when the underlying schedule was aborted. Cached levels are handed\n * out as copies because callers append to and reorder them.\n */\nexport async function scheduleDagLevelsMemo(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ScheduleDagLevelsOptions,\n\tsignal: AbortSignal | undefined,\n\tcache: DagScheduleCache,\n): Promise<DagSchedulePlan | null> {\n\t// Skip the memo whenever resolution can depend on state the key cannot\n\t// fingerprint: a custom resourceKeyResolver, or any tool whose resourceClaims\n\t// is a function closure — its return value may change between identical\n\t// calls, and a stale cached plan would silently reuse its old claims.\n\tif (\n\t\toptions.resourceKeyResolver ||\n\t\toptions.registeredTools?.some((tool) => typeof tool.resourceClaims === \"function\")\n\t) {\n\t\tconst scheduled = await awaitWithAbort(() => scheduleDagLevels(toolCalls, options), signal);\n\t\treturn scheduled.kind === \"aborted\" ? null : scheduled.value;\n\t}\n\tconst key = dagScheduleCacheKey(toolCalls, options);\n\tconst cached = cache.get(key);\n\tif (cached) {\n\t\tcache.delete(key);\n\t\tcache.set(key, cached);\n\t\treturn { levels: cached.levels.map((level) => level.slice()), planKey: cached.planKey, entries: cached.entries };\n\t}\n\tconst scheduled = await awaitWithAbort(() => scheduleDagLevels(toolCalls, options), signal);\n\tif (scheduled.kind === \"aborted\") {\n\t\treturn null;\n\t}\n\tif (cache.size >= DAG_SCHEDULE_CACHE_LIMIT) {\n\t\tconst oldest = cache.keys().next();\n\t\tif (!oldest.done) {\n\t\t\tcache.delete(oldest.value);\n\t\t}\n\t}\n\tcache.set(key, {\n\t\tlevels: scheduled.value.levels.map((level) => level.slice()),\n\t\tplanKey: scheduled.value.planKey,\n\t\tentries: scheduled.value.entries,\n\t});\n\treturn scheduled.value;\n}\n"]}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Per-run memo for DAG schedules. A plan is a pure function of the keyed
3
+ * inputs, so replaying it is safe — but only while every input the key cannot
4
+ * fingerprint stays stable. A custom `resourceKeyResolver` or any tool whose
5
+ * `resourceClaims` is a function closure can answer differently on identical
6
+ * call shapes, so those batches bypass the memo entirely (audit §9).
7
+ */
8
+ import { scheduleDagLevels } from "./tool-dag-scheduler.js";
9
+ import { awaitWithAbort } from "./tool-execution-boundary.js";
10
+ export const DAG_SCHEDULE_CACHE_LIMIT = 64;
11
+ /**
12
+ * Canonical key covering every input claim resolution depends on. A custom
13
+ * `resourceKeyResolver` function cannot be fingerprinted, so callers skip the
14
+ * memo entirely when one is configured. Within a run, tool definitions (and
15
+ * their `resourceClaims` closures) are stable, so name/mode/claims-presence
16
+ * fingerprints are sufficient.
17
+ */
18
+ function dagScheduleCacheKey(toolCalls, options) {
19
+ const policies = [...(options.toolPolicies?.entries() ?? [])].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
20
+ const registered = (options.registeredTools ?? []).map((tool) => [
21
+ tool.name,
22
+ tool.executionMode ?? "",
23
+ typeof tool.resourceClaims === "function" ? "1" : "0",
24
+ ]);
25
+ return JSON.stringify([
26
+ toolCalls.map((call) => [call.name, call.arguments ?? null]),
27
+ options.cwd,
28
+ options.strictExtensionClaims === true,
29
+ options.maxConcurrency ?? null,
30
+ policies,
31
+ registered,
32
+ ]);
33
+ }
34
+ /**
35
+ * Schedule with a per-run memo. Identical batches (provider retries, stubborn
36
+ * re-emissions) re-resolve path identities and custom claims; the plan is a
37
+ * pure function of the canonical inputs, so replaying it is safe. Returns
38
+ * `null` when the underlying schedule was aborted. Cached levels are handed
39
+ * out as copies because callers append to and reorder them.
40
+ */
41
+ export async function scheduleDagLevelsMemo(toolCalls, options, signal, cache) {
42
+ // Skip the memo whenever resolution can depend on state the key cannot
43
+ // fingerprint: a custom resourceKeyResolver, or any tool whose resourceClaims
44
+ // is a function closure — its return value may change between identical
45
+ // calls, and a stale cached plan would silently reuse its old claims.
46
+ if (options.resourceKeyResolver ||
47
+ options.registeredTools?.some((tool) => typeof tool.resourceClaims === "function")) {
48
+ const scheduled = await awaitWithAbort(() => scheduleDagLevels(toolCalls, options), signal);
49
+ return scheduled.kind === "aborted" ? null : scheduled.value;
50
+ }
51
+ const key = dagScheduleCacheKey(toolCalls, options);
52
+ const cached = cache.get(key);
53
+ if (cached) {
54
+ cache.delete(key);
55
+ cache.set(key, cached);
56
+ return { levels: cached.levels.map((level) => level.slice()), planKey: cached.planKey, entries: cached.entries };
57
+ }
58
+ const scheduled = await awaitWithAbort(() => scheduleDagLevels(toolCalls, options), signal);
59
+ if (scheduled.kind === "aborted") {
60
+ return null;
61
+ }
62
+ if (cache.size >= DAG_SCHEDULE_CACHE_LIMIT) {
63
+ const oldest = cache.keys().next();
64
+ if (!oldest.done) {
65
+ cache.delete(oldest.value);
66
+ }
67
+ }
68
+ cache.set(key, {
69
+ levels: scheduled.value.levels.map((level) => level.slice()),
70
+ planKey: scheduled.value.planKey,
71
+ entries: scheduled.value.entries,
72
+ });
73
+ return scheduled.value;
74
+ }
75
+ //# sourceMappingURL=tool-dag-memo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-dag-memo.js","sourceRoot":"","sources":["../src/tool-dag-memo.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-run memo for DAG schedules. A plan is a pure function of the keyed\n * inputs, so replaying it is safe — but only while every input the key cannot\n * fingerprint stays stable. A custom `resourceKeyResolver` or any tool whose\n * `resourceClaims` is a function closure can answer differently on identical\n * call shapes, so those batches bypass the memo entirely (audit §9).\n */\n\nimport { type DagSchedulePlan, type ScheduleDagLevelsOptions, scheduleDagLevels } from \"./tool-dag-scheduler.ts\";\nimport { awaitWithAbort } from \"./tool-execution-boundary.ts\";\nimport type { ClaimableToolCall } from \"./tool-resource-claims.ts\";\n\n/** Bounded per-run memo for DAG schedules. */\nexport type DagScheduleCache = Map<string, DagSchedulePlan>;\n\nexport const DAG_SCHEDULE_CACHE_LIMIT = 64;\n\n/**\n * Canonical key covering every input claim resolution depends on. A custom\n * `resourceKeyResolver` function cannot be fingerprinted, so callers skip the\n * memo entirely when one is configured. Within a run, tool definitions (and\n * their `resourceClaims` closures) are stable, so name/mode/claims-presence\n * fingerprints are sufficient.\n */\nfunction dagScheduleCacheKey(toolCalls: readonly ClaimableToolCall[], options: ScheduleDagLevelsOptions): string {\n\tconst policies = [...(options.toolPolicies?.entries() ?? [])].sort(([left], [right]) =>\n\t\tleft < right ? -1 : left > right ? 1 : 0,\n\t);\n\tconst registered = (options.registeredTools ?? []).map((tool) => [\n\t\ttool.name,\n\t\ttool.executionMode ?? \"\",\n\t\ttypeof tool.resourceClaims === \"function\" ? \"1\" : \"0\",\n\t]);\n\treturn JSON.stringify([\n\t\ttoolCalls.map((call) => [call.name, call.arguments ?? null]),\n\t\toptions.cwd,\n\t\toptions.strictExtensionClaims === true,\n\t\toptions.maxConcurrency ?? null,\n\t\tpolicies,\n\t\tregistered,\n\t]);\n}\n\n/**\n * Schedule with a per-run memo. Identical batches (provider retries, stubborn\n * re-emissions) re-resolve path identities and custom claims; the plan is a\n * pure function of the canonical inputs, so replaying it is safe. Returns\n * `null` when the underlying schedule was aborted. Cached levels are handed\n * out as copies because callers append to and reorder them.\n */\nexport async function scheduleDagLevelsMemo(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ScheduleDagLevelsOptions,\n\tsignal: AbortSignal | undefined,\n\tcache: DagScheduleCache,\n): Promise<DagSchedulePlan | null> {\n\t// Skip the memo whenever resolution can depend on state the key cannot\n\t// fingerprint: a custom resourceKeyResolver, or any tool whose resourceClaims\n\t// is a function closure — its return value may change between identical\n\t// calls, and a stale cached plan would silently reuse its old claims.\n\tif (\n\t\toptions.resourceKeyResolver ||\n\t\toptions.registeredTools?.some((tool) => typeof tool.resourceClaims === \"function\")\n\t) {\n\t\tconst scheduled = await awaitWithAbort(() => scheduleDagLevels(toolCalls, options), signal);\n\t\treturn scheduled.kind === \"aborted\" ? null : scheduled.value;\n\t}\n\tconst key = dagScheduleCacheKey(toolCalls, options);\n\tconst cached = cache.get(key);\n\tif (cached) {\n\t\tcache.delete(key);\n\t\tcache.set(key, cached);\n\t\treturn { levels: cached.levels.map((level) => level.slice()), planKey: cached.planKey, entries: cached.entries };\n\t}\n\tconst scheduled = await awaitWithAbort(() => scheduleDagLevels(toolCalls, options), signal);\n\tif (scheduled.kind === \"aborted\") {\n\t\treturn null;\n\t}\n\tif (cache.size >= DAG_SCHEDULE_CACHE_LIMIT) {\n\t\tconst oldest = cache.keys().next();\n\t\tif (!oldest.done) {\n\t\t\tcache.delete(oldest.value);\n\t\t}\n\t}\n\tcache.set(key, {\n\t\tlevels: scheduled.value.levels.map((level) => level.slice()),\n\t\tplanKey: scheduled.value.planKey,\n\t\tentries: scheduled.value.entries,\n\t});\n\treturn scheduled.value;\n}\n"]}
@@ -43,6 +43,12 @@ export interface DagSchedulePlan {
43
43
  * execution timing/outcomes). Stable under claim reordering within a call.
44
44
  */
45
45
  planKey: string;
46
+ /**
47
+ * The resolved, canonicalized claim entries the plan was built from. Callers
48
+ * that re-resolve claims after argument mutation (e.g. authorization hooks)
49
+ * compare against these to detect scope drift. Read-only for consumers.
50
+ */
51
+ entries: readonly ResolvedClaimEntry[];
46
52
  }
47
53
  /** One tool call's resolved claim data, canonicalized for stable planning. */
48
54
  export interface ResolvedClaimEntry {
@@ -103,4 +109,21 @@ export declare function computePlanKey(entries: readonly ResolvedClaimEntry[]):
103
109
  * deterministic.
104
110
  */
105
111
  export declare function scheduleDagLevels(toolCalls: readonly ClaimableToolCall[], options: ScheduleDagLevelsOptions): Promise<DagSchedulePlan>;
112
+ /**
113
+ * True when executing `sourceIndex` now would overlap an unsettled call whose
114
+ * known claims conflict with `finalResolution`.
115
+ *
116
+ * Two distinct violations are covered:
117
+ * - Earlier-source unsettled calls: the initial plan already ordered that
118
+ * pair, so only post-hook scope drift can create the situation; the caller
119
+ * defers the call instead of silently reversing the conflict pair.
120
+ * - Later-source calls that are already running (present in `running`): a
121
+ * hook that expands this call's scope onto their claims must defer — the
122
+ * running call cannot be un-started. Later-source calls that are merely
123
+ * pending keep source order: they may not overlap this call once it runs.
124
+ *
125
+ * Earlier-source calls inside `ready` (this same re-plan) are excluded — the
126
+ * sub-level packing orders them.
127
+ */
128
+ export declare function conflictsWithUnsettledClaim(sourceIndex: number, finalResolution: ToolClaimResolution, ready: ReadonlySet<number>, settled: ReadonlySet<number>, running: ReadonlySet<number>, resolutions: ReadonlyMap<number, ToolClaimResolution>): boolean;
106
129
  //# sourceMappingURL=tool-dag-scheduler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-dag-scheduler.d.ts","sourceRoot":"","sources":["../src/tool-dag-scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACN,KAAK,iBAAiB,EAGtB,KAAK,wBAAwB,EAG7B,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,MAAM,2BAA2B,CAAC;AAEnC,sDAAsD;AACtD,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACzE;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC/B,4FAA4F;IAC5F,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC;IACnB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,mBAAmB,CAAC;IAChC,eAAe,EAAE,iBAAiB,EAAE,CAAC;CACrC;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACvC,SAAS,EAAE,SAAS,iBAAiB,EAAE,EACvC,OAAO,EAAE,wBAAwB,GAC/B,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAQ/B;AAqDD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,EAAE,EAAE,CA6BlF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,EAAE,EAAE,CAYxF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,EAAE,CAgBpG;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,CAU7E;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACtC,SAAS,EAAE,SAAS,iBAAiB,EAAE,EACvC,OAAO,EAAE,wBAAwB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAK1B","sourcesContent":["/**\n * Pure, browser-safe deterministic resource-claim DAG scheduler.\n *\n * Given a source-ordered batch of tool calls and their resolved resource\n * claims, this module assigns each call after every earlier call it conflicts\n * with. Levels run in source order; calls inside one level are mutually\n * conflict-free and may execute concurrently. A positive `maxConcurrency`\n * width cap splits each level into deterministic contiguous source-ordered\n * chunks.\n *\n * Determinism contract:\n * - The level assignment is a pure function of the source-ordered input\n * claims. The canonical example is the head-of-line example\n * `write x, write x, write y`, which schedules as `[[0, 2], [1]]`: the second\n * write to `x` is pushed to level 1, but the independent write to `y` is not\n * blocked and joins level 0.\n * - Reordering claims *within* a single call does not change the plan: claims\n * are canonicalized (sorted, fixed property order) before comparison.\n * - {@link DagSchedulePlan.planKey} is a canonical serialization of the\n * resolved claim sequence (canonical claim data only — never execution\n * timing or outcomes). It is collision-free for distinct canonical claim\n * sequences.\n *\n * This module uses no platform APIs (no `process`, fs, `node:path`, or timers).\n */\n\nimport {\n\ttype ClaimableToolCall,\n\tcanonicalizeClaims,\n\tclaimsConflict,\n\ttype ResolveToolClaimsOptions,\n\tresolutionsConflict,\n\tresolveToolClaimsForCall,\n\ttype ToolClaimResolution,\n\ttype ToolResourceClaim,\n} from \"./tool-resource-claims.ts\";\n\n/** Options for scheduling a batch into DAG levels. */\nexport interface ScheduleDagLevelsOptions extends ResolveToolClaimsOptions {\n\t/**\n\t * Optional positive width cap. When set, each level is split into contiguous\n\t * source-ordered chunks of at most this many calls so a wide conflict-free\n\t * level does not fan out unbounded. Absent, non-finite, or non-positive\n\t * values leave each level whole. Does not affect `planKey`.\n\t */\n\tmaxConcurrency?: number;\n}\n\n/** A scheduled plan: ordered levels of source indices plus a canonical key. */\nexport interface DagSchedulePlan {\n\t/** Levels in execution order; each level holds source indices that may run concurrently. */\n\tlevels: number[][];\n\t/**\n\t * Canonical deterministic key over the resolved claim sequence only (never\n\t * execution timing/outcomes). Stable under claim reordering within a call.\n\t */\n\tplanKey: string;\n}\n\n/** One tool call's resolved claim data, canonicalized for stable planning. */\nexport interface ResolvedClaimEntry {\n\tsourceIndex: number;\n\tresolution: ToolClaimResolution;\n\tcanonicalClaims: ToolResourceClaim[];\n}\n\n/**\n * Resolve and canonicalize claims for a whole batch, preserving source order.\n * Registered custom resolvers are awaited one call at a time in source order,\n * and every resolution completes before a schedule is constructed.\n */\nexport async function resolveBatchClaims(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ResolveToolClaimsOptions,\n): Promise<ResolvedClaimEntry[]> {\n\tconst entries: ResolvedClaimEntry[] = [];\n\tfor (let index = 0; index < toolCalls.length; index++) {\n\t\tconst resolution = await resolveToolClaimsForCall(toolCalls[index], options);\n\t\tconst canonicalClaims = resolution.kind === \"claims\" ? canonicalizeClaims(resolution.claims) : [];\n\t\tentries.push({ sourceIndex: index, resolution, canonicalClaims });\n\t}\n\treturn entries;\n}\n\ninterface DagLevel {\n\tindices: number[];\n\t/** Write claims currently placed in this level (for fast read-vs-write checks). */\n\twriteClaims: ToolResourceClaim[];\n\t/** Read claims currently placed in this level (for fast write-vs-read checks). */\n\treadClaims: ToolResourceClaim[];\n\t/** True once an exclusive call is placed here; such a level accepts no more. */\n\thasExclusive: boolean;\n}\n\nfunction claimConflictsAny(claim: ToolResourceClaim, candidates: readonly ToolResourceClaim[]): boolean {\n\tfor (const candidate of candidates) {\n\t\tif (claimsConflict(claim, candidate)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * True when `resolution` cannot join `level`. Equivalent to the full pairwise\n * `resolutionsConflict` check, optimized so read/read never triggers a scan:\n * a read claim only needs to scan the level's writes, and a write claim scans\n * both writes and reads. An exclusive resolution, or a level that already holds\n * an exclusive call, conflicts unconditionally.\n */\nfunction resolutionConflictsLevel(resolution: ToolClaimResolution, level: DagLevel): boolean {\n\tif (level.hasExclusive) {\n\t\treturn true;\n\t}\n\tif (resolution.kind === \"exclusive\") {\n\t\treturn true;\n\t}\n\tfor (const claim of resolution.claims) {\n\t\tif (claim.access === \"exclusive\") {\n\t\t\treturn true;\n\t\t}\n\t\tif (claim.access === \"write\") {\n\t\t\tif (claimConflictsAny(claim, level.writeClaims)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (claimConflictsAny(claim, level.readClaims)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (claimConflictsAny(claim, level.writeClaims)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Assign each source-ordered claim entry one level after its latest earlier\n * conflict. Deterministic and stable: equal inputs (including claim reordering\n * within a call, which is canonicalized away) always produce equal levels, and\n * every directed conflict edge advances at least one level.\n */\nexport function assignDagLevels(entries: readonly ResolvedClaimEntry[]): number[][] {\n\tconst levels: DagLevel[] = [];\n\tfor (const entry of entries) {\n\t\tconst resolution = entry.resolution;\n\t\tlet targetIndex = 0;\n\t\tfor (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {\n\t\t\tif (resolutionConflictsLevel(resolution, levels[levelIndex])) {\n\t\t\t\ttargetIndex = levelIndex + 1;\n\t\t\t}\n\t\t}\n\t\tlet target = levels[targetIndex];\n\t\tif (!target) {\n\t\t\ttarget = { indices: [], writeClaims: [], readClaims: [], hasExclusive: false };\n\t\t\tlevels.push(target);\n\t\t}\n\t\ttarget.indices.push(entry.sourceIndex);\n\t\tif (resolution.kind === \"exclusive\" || entry.canonicalClaims.some((claim) => claim.access === \"exclusive\")) {\n\t\t\ttarget.hasExclusive = true;\n\t\t} else {\n\t\t\tfor (const claim of entry.canonicalClaims) {\n\t\t\t\tif (claim.access === \"write\") {\n\t\t\t\t\ttarget.writeClaims.push(claim);\n\t\t\t\t} else {\n\t\t\t\t\ttarget.readClaims.push(claim);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn levels.map((level) => level.indices);\n}\n\n/**\n * Direct conflicting predecessors for each source-ordered entry: `result[i]`\n * lists every earlier position `i` must wait for.\n *\n * This is the precedence graph that {@link assignDagLevels} approximates with a\n * barrier schedule. Levels force every call in level N+1 to wait for ALL of\n * level N, so one slow unrelated call in a level delays the whole next level.\n * Dependency edges only connect calls that actually conflict, so a consumer can\n * start each call as soon as its own predecessors settle.\n *\n * Deterministic and stable: the result is a pure function of the source-ordered\n * canonicalized claims, and every list is ascending and strictly earlier-only.\n * Transitively-implied edges are kept rather than reduced — waiting on an\n * already-finished ancestor costs nothing and keeps this a simple pure scan.\n *\n * The graph is never deeper than the barrier schedule: for any `j` in\n * `result[i]`, `i` conflicts with an entry in level `j`, so\n * `level(i) >= level(j) + 1`, and depth follows by induction.\n */\nexport function assignDagDependencies(entries: readonly ResolvedClaimEntry[]): number[][] {\n\tconst dependencies: number[][] = [];\n\tfor (let index = 0; index < entries.length; index++) {\n\t\tconst blockers: number[] = [];\n\t\tfor (let earlier = 0; earlier < index; earlier++) {\n\t\t\tif (resolutionsConflict(entries[earlier].resolution, entries[index].resolution)) {\n\t\t\t\tblockers.push(earlier);\n\t\t\t}\n\t\t}\n\t\tdependencies.push(blockers);\n\t}\n\treturn dependencies;\n}\n\n/**\n * Split each level into contiguous source-ordered chunks of at most `cap` calls.\n * Absent/non-finite/non-positive `cap` returns the levels unchanged.\n */\nexport function applyConcurrencyCap(levels: readonly number[][], cap: number | undefined): number[][] {\n\tif (typeof cap !== \"number\" || !Number.isFinite(cap) || cap <= 0) {\n\t\treturn levels.map((level) => level.slice());\n\t}\n\tconst integerCap = Math.max(1, Math.floor(cap));\n\tconst chunked: number[][] = [];\n\tfor (const level of levels) {\n\t\tif (level.length <= integerCap) {\n\t\t\tchunked.push(level.slice());\n\t\t\tcontinue;\n\t\t}\n\t\tfor (let start = 0; start < level.length; start += integerCap) {\n\t\t\tchunked.push(level.slice(start, start + integerCap));\n\t\t}\n\t}\n\treturn chunked;\n}\n\n/**\n * Canonical deterministic key over the resolved claim sequence. Uses\n * `JSON.stringify` of each call's canonicalized claims (fixed property order,\n * sorted) with an `\"E\"`/`\"C\"` discriminator, so it is collision-free for\n * distinct canonical claim sequences and stable under claim reordering within a\n * call. Contains no execution timing or outcomes.\n */\nexport function computePlanKey(entries: readonly ResolvedClaimEntry[]): string {\n\tlet key = \"\";\n\tfor (const entry of entries) {\n\t\tif (entry.resolution.kind === \"exclusive\") {\n\t\t\tkey += \"E,\";\n\t\t} else {\n\t\t\tkey += `C${JSON.stringify(entry.canonicalClaims)},`;\n\t\t}\n\t}\n\treturn key;\n}\n\n/**\n * Schedule a source-ordered tool-call batch into deterministic DAG levels.\n * Resolves default claims, computes source-directed dependency levels, applies\n * the optional width cap, and computes the canonical plan key. Pure and\n * deterministic.\n */\nexport async function scheduleDagLevels(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ScheduleDagLevelsOptions,\n): Promise<DagSchedulePlan> {\n\tconst entries = await resolveBatchClaims(toolCalls, options);\n\tconst baseLevels = assignDagLevels(entries);\n\tconst levels = applyConcurrencyCap(baseLevels, options.maxConcurrency);\n\treturn { levels, planKey: computePlanKey(entries) };\n}\n"]}
1
+ {"version":3,"file":"tool-dag-scheduler.d.ts","sourceRoot":"","sources":["../src/tool-dag-scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACN,KAAK,iBAAiB,EAGtB,KAAK,wBAAwB,EAG7B,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,MAAM,2BAA2B,CAAC;AAEnC,sDAAsD;AACtD,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACzE;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC/B,4FAA4F;IAC5F,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC;IACnB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACvC;AAED,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,mBAAmB,CAAC;IAChC,eAAe,EAAE,iBAAiB,EAAE,CAAC;CACrC;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACvC,SAAS,EAAE,SAAS,iBAAiB,EAAE,EACvC,OAAO,EAAE,wBAAwB,GAC/B,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAQ/B;AAqDD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,EAAE,EAAE,CA6BlF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,EAAE,EAAE,CAYxF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,EAAE,CAgBpG;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,CAU7E;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACtC,SAAS,EAAE,SAAS,iBAAiB,EAAE,EACvC,OAAO,EAAE,wBAAwB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAK1B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CAC1C,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,mBAAmB,EACpC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,EAC1B,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAC5B,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAC5B,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,mBAAmB,CAAC,GACnD,OAAO,CAYT","sourcesContent":["/**\n * Pure, browser-safe deterministic resource-claim DAG scheduler.\n *\n * Given a source-ordered batch of tool calls and their resolved resource\n * claims, this module assigns each call after every earlier call it conflicts\n * with. Levels run in source order; calls inside one level are mutually\n * conflict-free and may execute concurrently. A positive `maxConcurrency`\n * width cap splits each level into deterministic contiguous source-ordered\n * chunks.\n *\n * Determinism contract:\n * - The level assignment is a pure function of the source-ordered input\n * claims. The canonical example is the head-of-line example\n * `write x, write x, write y`, which schedules as `[[0, 2], [1]]`: the second\n * write to `x` is pushed to level 1, but the independent write to `y` is not\n * blocked and joins level 0.\n * - Reordering claims *within* a single call does not change the plan: claims\n * are canonicalized (sorted, fixed property order) before comparison.\n * - {@link DagSchedulePlan.planKey} is a canonical serialization of the\n * resolved claim sequence (canonical claim data only — never execution\n * timing or outcomes). It is collision-free for distinct canonical claim\n * sequences.\n *\n * This module uses no platform APIs (no `process`, fs, `node:path`, or timers).\n */\n\nimport {\n\ttype ClaimableToolCall,\n\tcanonicalizeClaims,\n\tclaimsConflict,\n\ttype ResolveToolClaimsOptions,\n\tresolutionsConflict,\n\tresolveToolClaimsForCall,\n\ttype ToolClaimResolution,\n\ttype ToolResourceClaim,\n} from \"./tool-resource-claims.ts\";\n\n/** Options for scheduling a batch into DAG levels. */\nexport interface ScheduleDagLevelsOptions extends ResolveToolClaimsOptions {\n\t/**\n\t * Optional positive width cap. When set, each level is split into contiguous\n\t * source-ordered chunks of at most this many calls so a wide conflict-free\n\t * level does not fan out unbounded. Absent, non-finite, or non-positive\n\t * values leave each level whole. Does not affect `planKey`.\n\t */\n\tmaxConcurrency?: number;\n}\n\n/** A scheduled plan: ordered levels of source indices plus a canonical key. */\nexport interface DagSchedulePlan {\n\t/** Levels in execution order; each level holds source indices that may run concurrently. */\n\tlevels: number[][];\n\t/**\n\t * Canonical deterministic key over the resolved claim sequence only (never\n\t * execution timing/outcomes). Stable under claim reordering within a call.\n\t */\n\tplanKey: string;\n\t/**\n\t * The resolved, canonicalized claim entries the plan was built from. Callers\n\t * that re-resolve claims after argument mutation (e.g. authorization hooks)\n\t * compare against these to detect scope drift. Read-only for consumers.\n\t */\n\tentries: readonly ResolvedClaimEntry[];\n}\n\n/** One tool call's resolved claim data, canonicalized for stable planning. */\nexport interface ResolvedClaimEntry {\n\tsourceIndex: number;\n\tresolution: ToolClaimResolution;\n\tcanonicalClaims: ToolResourceClaim[];\n}\n\n/**\n * Resolve and canonicalize claims for a whole batch, preserving source order.\n * Registered custom resolvers are awaited one call at a time in source order,\n * and every resolution completes before a schedule is constructed.\n */\nexport async function resolveBatchClaims(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ResolveToolClaimsOptions,\n): Promise<ResolvedClaimEntry[]> {\n\tconst entries: ResolvedClaimEntry[] = [];\n\tfor (let index = 0; index < toolCalls.length; index++) {\n\t\tconst resolution = await resolveToolClaimsForCall(toolCalls[index], options);\n\t\tconst canonicalClaims = resolution.kind === \"claims\" ? canonicalizeClaims(resolution.claims) : [];\n\t\tentries.push({ sourceIndex: index, resolution, canonicalClaims });\n\t}\n\treturn entries;\n}\n\ninterface DagLevel {\n\tindices: number[];\n\t/** Write claims currently placed in this level (for fast read-vs-write checks). */\n\twriteClaims: ToolResourceClaim[];\n\t/** Read claims currently placed in this level (for fast write-vs-read checks). */\n\treadClaims: ToolResourceClaim[];\n\t/** True once an exclusive call is placed here; such a level accepts no more. */\n\thasExclusive: boolean;\n}\n\nfunction claimConflictsAny(claim: ToolResourceClaim, candidates: readonly ToolResourceClaim[]): boolean {\n\tfor (const candidate of candidates) {\n\t\tif (claimsConflict(claim, candidate)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * True when `resolution` cannot join `level`. Equivalent to the full pairwise\n * `resolutionsConflict` check, optimized so read/read never triggers a scan:\n * a read claim only needs to scan the level's writes, and a write claim scans\n * both writes and reads. An exclusive resolution, or a level that already holds\n * an exclusive call, conflicts unconditionally.\n */\nfunction resolutionConflictsLevel(resolution: ToolClaimResolution, level: DagLevel): boolean {\n\tif (level.hasExclusive) {\n\t\treturn true;\n\t}\n\tif (resolution.kind === \"exclusive\") {\n\t\treturn true;\n\t}\n\tfor (const claim of resolution.claims) {\n\t\tif (claim.access === \"exclusive\") {\n\t\t\treturn true;\n\t\t}\n\t\tif (claim.access === \"write\") {\n\t\t\tif (claimConflictsAny(claim, level.writeClaims)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (claimConflictsAny(claim, level.readClaims)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (claimConflictsAny(claim, level.writeClaims)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Assign each source-ordered claim entry one level after its latest earlier\n * conflict. Deterministic and stable: equal inputs (including claim reordering\n * within a call, which is canonicalized away) always produce equal levels, and\n * every directed conflict edge advances at least one level.\n */\nexport function assignDagLevels(entries: readonly ResolvedClaimEntry[]): number[][] {\n\tconst levels: DagLevel[] = [];\n\tfor (const entry of entries) {\n\t\tconst resolution = entry.resolution;\n\t\tlet targetIndex = 0;\n\t\tfor (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {\n\t\t\tif (resolutionConflictsLevel(resolution, levels[levelIndex])) {\n\t\t\t\ttargetIndex = levelIndex + 1;\n\t\t\t}\n\t\t}\n\t\tlet target = levels[targetIndex];\n\t\tif (!target) {\n\t\t\ttarget = { indices: [], writeClaims: [], readClaims: [], hasExclusive: false };\n\t\t\tlevels.push(target);\n\t\t}\n\t\ttarget.indices.push(entry.sourceIndex);\n\t\tif (resolution.kind === \"exclusive\" || entry.canonicalClaims.some((claim) => claim.access === \"exclusive\")) {\n\t\t\ttarget.hasExclusive = true;\n\t\t} else {\n\t\t\tfor (const claim of entry.canonicalClaims) {\n\t\t\t\tif (claim.access === \"write\") {\n\t\t\t\t\ttarget.writeClaims.push(claim);\n\t\t\t\t} else {\n\t\t\t\t\ttarget.readClaims.push(claim);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn levels.map((level) => level.indices);\n}\n\n/**\n * Direct conflicting predecessors for each source-ordered entry: `result[i]`\n * lists every earlier position `i` must wait for.\n *\n * This is the precedence graph that {@link assignDagLevels} approximates with a\n * barrier schedule. Levels force every call in level N+1 to wait for ALL of\n * level N, so one slow unrelated call in a level delays the whole next level.\n * Dependency edges only connect calls that actually conflict, so a consumer can\n * start each call as soon as its own predecessors settle.\n *\n * Deterministic and stable: the result is a pure function of the source-ordered\n * canonicalized claims, and every list is ascending and strictly earlier-only.\n * Transitively-implied edges are kept rather than reduced — waiting on an\n * already-finished ancestor costs nothing and keeps this a simple pure scan.\n *\n * The graph is never deeper than the barrier schedule: for any `j` in\n * `result[i]`, `i` conflicts with an entry in level `j`, so\n * `level(i) >= level(j) + 1`, and depth follows by induction.\n */\nexport function assignDagDependencies(entries: readonly ResolvedClaimEntry[]): number[][] {\n\tconst dependencies: number[][] = [];\n\tfor (let index = 0; index < entries.length; index++) {\n\t\tconst blockers: number[] = [];\n\t\tfor (let earlier = 0; earlier < index; earlier++) {\n\t\t\tif (resolutionsConflict(entries[earlier].resolution, entries[index].resolution)) {\n\t\t\t\tblockers.push(earlier);\n\t\t\t}\n\t\t}\n\t\tdependencies.push(blockers);\n\t}\n\treturn dependencies;\n}\n\n/**\n * Split each level into contiguous source-ordered chunks of at most `cap` calls.\n * Absent/non-finite/non-positive `cap` returns the levels unchanged.\n */\nexport function applyConcurrencyCap(levels: readonly number[][], cap: number | undefined): number[][] {\n\tif (typeof cap !== \"number\" || !Number.isFinite(cap) || cap <= 0) {\n\t\treturn levels.map((level) => level.slice());\n\t}\n\tconst integerCap = Math.max(1, Math.floor(cap));\n\tconst chunked: number[][] = [];\n\tfor (const level of levels) {\n\t\tif (level.length <= integerCap) {\n\t\t\tchunked.push(level.slice());\n\t\t\tcontinue;\n\t\t}\n\t\tfor (let start = 0; start < level.length; start += integerCap) {\n\t\t\tchunked.push(level.slice(start, start + integerCap));\n\t\t}\n\t}\n\treturn chunked;\n}\n\n/**\n * Canonical deterministic key over the resolved claim sequence. Uses\n * `JSON.stringify` of each call's canonicalized claims (fixed property order,\n * sorted) with an `\"E\"`/`\"C\"` discriminator, so it is collision-free for\n * distinct canonical claim sequences and stable under claim reordering within a\n * call. Contains no execution timing or outcomes.\n */\nexport function computePlanKey(entries: readonly ResolvedClaimEntry[]): string {\n\tlet key = \"\";\n\tfor (const entry of entries) {\n\t\tif (entry.resolution.kind === \"exclusive\") {\n\t\t\tkey += \"E,\";\n\t\t} else {\n\t\t\tkey += `C${JSON.stringify(entry.canonicalClaims)},`;\n\t\t}\n\t}\n\treturn key;\n}\n\n/**\n * Schedule a source-ordered tool-call batch into deterministic DAG levels.\n * Resolves default claims, computes source-directed dependency levels, applies\n * the optional width cap, and computes the canonical plan key. Pure and\n * deterministic.\n */\nexport async function scheduleDagLevels(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ScheduleDagLevelsOptions,\n): Promise<DagSchedulePlan> {\n\tconst entries = await resolveBatchClaims(toolCalls, options);\n\tconst baseLevels = assignDagLevels(entries);\n\tconst levels = applyConcurrencyCap(baseLevels, options.maxConcurrency);\n\treturn { levels, planKey: computePlanKey(entries), entries };\n}\n\n/**\n * True when executing `sourceIndex` now would overlap an unsettled call whose\n * known claims conflict with `finalResolution`.\n *\n * Two distinct violations are covered:\n * - Earlier-source unsettled calls: the initial plan already ordered that\n * pair, so only post-hook scope drift can create the situation; the caller\n * defers the call instead of silently reversing the conflict pair.\n * - Later-source calls that are already running (present in `running`): a\n * hook that expands this call's scope onto their claims must defer — the\n * running call cannot be un-started. Later-source calls that are merely\n * pending keep source order: they may not overlap this call once it runs.\n *\n * Earlier-source calls inside `ready` (this same re-plan) are excluded — the\n * sub-level packing orders them.\n */\nexport function conflictsWithUnsettledClaim(\n\tsourceIndex: number,\n\tfinalResolution: ToolClaimResolution,\n\tready: ReadonlySet<number>,\n\tsettled: ReadonlySet<number>,\n\trunning: ReadonlySet<number>,\n\tresolutions: ReadonlyMap<number, ToolClaimResolution>,\n): boolean {\n\tfor (const [otherIndex, resolution] of resolutions) {\n\t\tif (otherIndex === sourceIndex || settled.has(otherIndex)) continue;\n\t\tif (otherIndex < sourceIndex) {\n\t\t\tif (ready.has(otherIndex)) continue;\n\t\t} else if (!running.has(otherIndex)) {\n\t\t\t// Later-source and not running: source order wins, no constraint.\n\t\t\tcontinue;\n\t\t}\n\t\tif (resolutionsConflict(resolution, finalResolution)) return true;\n\t}\n\treturn false;\n}\n"]}
@@ -198,6 +198,39 @@ export async function scheduleDagLevels(toolCalls, options) {
198
198
  const entries = await resolveBatchClaims(toolCalls, options);
199
199
  const baseLevels = assignDagLevels(entries);
200
200
  const levels = applyConcurrencyCap(baseLevels, options.maxConcurrency);
201
- return { levels, planKey: computePlanKey(entries) };
201
+ return { levels, planKey: computePlanKey(entries), entries };
202
+ }
203
+ /**
204
+ * True when executing `sourceIndex` now would overlap an unsettled call whose
205
+ * known claims conflict with `finalResolution`.
206
+ *
207
+ * Two distinct violations are covered:
208
+ * - Earlier-source unsettled calls: the initial plan already ordered that
209
+ * pair, so only post-hook scope drift can create the situation; the caller
210
+ * defers the call instead of silently reversing the conflict pair.
211
+ * - Later-source calls that are already running (present in `running`): a
212
+ * hook that expands this call's scope onto their claims must defer — the
213
+ * running call cannot be un-started. Later-source calls that are merely
214
+ * pending keep source order: they may not overlap this call once it runs.
215
+ *
216
+ * Earlier-source calls inside `ready` (this same re-plan) are excluded — the
217
+ * sub-level packing orders them.
218
+ */
219
+ export function conflictsWithUnsettledClaim(sourceIndex, finalResolution, ready, settled, running, resolutions) {
220
+ for (const [otherIndex, resolution] of resolutions) {
221
+ if (otherIndex === sourceIndex || settled.has(otherIndex))
222
+ continue;
223
+ if (otherIndex < sourceIndex) {
224
+ if (ready.has(otherIndex))
225
+ continue;
226
+ }
227
+ else if (!running.has(otherIndex)) {
228
+ // Later-source and not running: source order wins, no constraint.
229
+ continue;
230
+ }
231
+ if (resolutionsConflict(resolution, finalResolution))
232
+ return true;
233
+ }
234
+ return false;
202
235
  }
203
236
  //# sourceMappingURL=tool-dag-scheduler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-dag-scheduler.js","sourceRoot":"","sources":["../src/tool-dag-scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure, browser-safe deterministic resource-claim DAG scheduler.\n *\n * Given a source-ordered batch of tool calls and their resolved resource\n * claims, this module assigns each call after every earlier call it conflicts\n * with. Levels run in source order; calls inside one level are mutually\n * conflict-free and may execute concurrently. A positive `maxConcurrency`\n * width cap splits each level into deterministic contiguous source-ordered\n * chunks.\n *\n * Determinism contract:\n * - The level assignment is a pure function of the source-ordered input\n * claims. The canonical example is the head-of-line example\n * `write x, write x, write y`, which schedules as `[[0, 2], [1]]`: the second\n * write to `x` is pushed to level 1, but the independent write to `y` is not\n * blocked and joins level 0.\n * - Reordering claims *within* a single call does not change the plan: claims\n * are canonicalized (sorted, fixed property order) before comparison.\n * - {@link DagSchedulePlan.planKey} is a canonical serialization of the\n * resolved claim sequence (canonical claim data only — never execution\n * timing or outcomes). It is collision-free for distinct canonical claim\n * sequences.\n *\n * This module uses no platform APIs (no `process`, fs, `node:path`, or timers).\n */\n\nimport {\n\ttype ClaimableToolCall,\n\tcanonicalizeClaims,\n\tclaimsConflict,\n\ttype ResolveToolClaimsOptions,\n\tresolutionsConflict,\n\tresolveToolClaimsForCall,\n\ttype ToolClaimResolution,\n\ttype ToolResourceClaim,\n} from \"./tool-resource-claims.ts\";\n\n/** Options for scheduling a batch into DAG levels. */\nexport interface ScheduleDagLevelsOptions extends ResolveToolClaimsOptions {\n\t/**\n\t * Optional positive width cap. When set, each level is split into contiguous\n\t * source-ordered chunks of at most this many calls so a wide conflict-free\n\t * level does not fan out unbounded. Absent, non-finite, or non-positive\n\t * values leave each level whole. Does not affect `planKey`.\n\t */\n\tmaxConcurrency?: number;\n}\n\n/** A scheduled plan: ordered levels of source indices plus a canonical key. */\nexport interface DagSchedulePlan {\n\t/** Levels in execution order; each level holds source indices that may run concurrently. */\n\tlevels: number[][];\n\t/**\n\t * Canonical deterministic key over the resolved claim sequence only (never\n\t * execution timing/outcomes). Stable under claim reordering within a call.\n\t */\n\tplanKey: string;\n}\n\n/** One tool call's resolved claim data, canonicalized for stable planning. */\nexport interface ResolvedClaimEntry {\n\tsourceIndex: number;\n\tresolution: ToolClaimResolution;\n\tcanonicalClaims: ToolResourceClaim[];\n}\n\n/**\n * Resolve and canonicalize claims for a whole batch, preserving source order.\n * Registered custom resolvers are awaited one call at a time in source order,\n * and every resolution completes before a schedule is constructed.\n */\nexport async function resolveBatchClaims(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ResolveToolClaimsOptions,\n): Promise<ResolvedClaimEntry[]> {\n\tconst entries: ResolvedClaimEntry[] = [];\n\tfor (let index = 0; index < toolCalls.length; index++) {\n\t\tconst resolution = await resolveToolClaimsForCall(toolCalls[index], options);\n\t\tconst canonicalClaims = resolution.kind === \"claims\" ? canonicalizeClaims(resolution.claims) : [];\n\t\tentries.push({ sourceIndex: index, resolution, canonicalClaims });\n\t}\n\treturn entries;\n}\n\ninterface DagLevel {\n\tindices: number[];\n\t/** Write claims currently placed in this level (for fast read-vs-write checks). */\n\twriteClaims: ToolResourceClaim[];\n\t/** Read claims currently placed in this level (for fast write-vs-read checks). */\n\treadClaims: ToolResourceClaim[];\n\t/** True once an exclusive call is placed here; such a level accepts no more. */\n\thasExclusive: boolean;\n}\n\nfunction claimConflictsAny(claim: ToolResourceClaim, candidates: readonly ToolResourceClaim[]): boolean {\n\tfor (const candidate of candidates) {\n\t\tif (claimsConflict(claim, candidate)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * True when `resolution` cannot join `level`. Equivalent to the full pairwise\n * `resolutionsConflict` check, optimized so read/read never triggers a scan:\n * a read claim only needs to scan the level's writes, and a write claim scans\n * both writes and reads. An exclusive resolution, or a level that already holds\n * an exclusive call, conflicts unconditionally.\n */\nfunction resolutionConflictsLevel(resolution: ToolClaimResolution, level: DagLevel): boolean {\n\tif (level.hasExclusive) {\n\t\treturn true;\n\t}\n\tif (resolution.kind === \"exclusive\") {\n\t\treturn true;\n\t}\n\tfor (const claim of resolution.claims) {\n\t\tif (claim.access === \"exclusive\") {\n\t\t\treturn true;\n\t\t}\n\t\tif (claim.access === \"write\") {\n\t\t\tif (claimConflictsAny(claim, level.writeClaims)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (claimConflictsAny(claim, level.readClaims)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (claimConflictsAny(claim, level.writeClaims)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Assign each source-ordered claim entry one level after its latest earlier\n * conflict. Deterministic and stable: equal inputs (including claim reordering\n * within a call, which is canonicalized away) always produce equal levels, and\n * every directed conflict edge advances at least one level.\n */\nexport function assignDagLevels(entries: readonly ResolvedClaimEntry[]): number[][] {\n\tconst levels: DagLevel[] = [];\n\tfor (const entry of entries) {\n\t\tconst resolution = entry.resolution;\n\t\tlet targetIndex = 0;\n\t\tfor (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {\n\t\t\tif (resolutionConflictsLevel(resolution, levels[levelIndex])) {\n\t\t\t\ttargetIndex = levelIndex + 1;\n\t\t\t}\n\t\t}\n\t\tlet target = levels[targetIndex];\n\t\tif (!target) {\n\t\t\ttarget = { indices: [], writeClaims: [], readClaims: [], hasExclusive: false };\n\t\t\tlevels.push(target);\n\t\t}\n\t\ttarget.indices.push(entry.sourceIndex);\n\t\tif (resolution.kind === \"exclusive\" || entry.canonicalClaims.some((claim) => claim.access === \"exclusive\")) {\n\t\t\ttarget.hasExclusive = true;\n\t\t} else {\n\t\t\tfor (const claim of entry.canonicalClaims) {\n\t\t\t\tif (claim.access === \"write\") {\n\t\t\t\t\ttarget.writeClaims.push(claim);\n\t\t\t\t} else {\n\t\t\t\t\ttarget.readClaims.push(claim);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn levels.map((level) => level.indices);\n}\n\n/**\n * Direct conflicting predecessors for each source-ordered entry: `result[i]`\n * lists every earlier position `i` must wait for.\n *\n * This is the precedence graph that {@link assignDagLevels} approximates with a\n * barrier schedule. Levels force every call in level N+1 to wait for ALL of\n * level N, so one slow unrelated call in a level delays the whole next level.\n * Dependency edges only connect calls that actually conflict, so a consumer can\n * start each call as soon as its own predecessors settle.\n *\n * Deterministic and stable: the result is a pure function of the source-ordered\n * canonicalized claims, and every list is ascending and strictly earlier-only.\n * Transitively-implied edges are kept rather than reduced — waiting on an\n * already-finished ancestor costs nothing and keeps this a simple pure scan.\n *\n * The graph is never deeper than the barrier schedule: for any `j` in\n * `result[i]`, `i` conflicts with an entry in level `j`, so\n * `level(i) >= level(j) + 1`, and depth follows by induction.\n */\nexport function assignDagDependencies(entries: readonly ResolvedClaimEntry[]): number[][] {\n\tconst dependencies: number[][] = [];\n\tfor (let index = 0; index < entries.length; index++) {\n\t\tconst blockers: number[] = [];\n\t\tfor (let earlier = 0; earlier < index; earlier++) {\n\t\t\tif (resolutionsConflict(entries[earlier].resolution, entries[index].resolution)) {\n\t\t\t\tblockers.push(earlier);\n\t\t\t}\n\t\t}\n\t\tdependencies.push(blockers);\n\t}\n\treturn dependencies;\n}\n\n/**\n * Split each level into contiguous source-ordered chunks of at most `cap` calls.\n * Absent/non-finite/non-positive `cap` returns the levels unchanged.\n */\nexport function applyConcurrencyCap(levels: readonly number[][], cap: number | undefined): number[][] {\n\tif (typeof cap !== \"number\" || !Number.isFinite(cap) || cap <= 0) {\n\t\treturn levels.map((level) => level.slice());\n\t}\n\tconst integerCap = Math.max(1, Math.floor(cap));\n\tconst chunked: number[][] = [];\n\tfor (const level of levels) {\n\t\tif (level.length <= integerCap) {\n\t\t\tchunked.push(level.slice());\n\t\t\tcontinue;\n\t\t}\n\t\tfor (let start = 0; start < level.length; start += integerCap) {\n\t\t\tchunked.push(level.slice(start, start + integerCap));\n\t\t}\n\t}\n\treturn chunked;\n}\n\n/**\n * Canonical deterministic key over the resolved claim sequence. Uses\n * `JSON.stringify` of each call's canonicalized claims (fixed property order,\n * sorted) with an `\"E\"`/`\"C\"` discriminator, so it is collision-free for\n * distinct canonical claim sequences and stable under claim reordering within a\n * call. Contains no execution timing or outcomes.\n */\nexport function computePlanKey(entries: readonly ResolvedClaimEntry[]): string {\n\tlet key = \"\";\n\tfor (const entry of entries) {\n\t\tif (entry.resolution.kind === \"exclusive\") {\n\t\t\tkey += \"E,\";\n\t\t} else {\n\t\t\tkey += `C${JSON.stringify(entry.canonicalClaims)},`;\n\t\t}\n\t}\n\treturn key;\n}\n\n/**\n * Schedule a source-ordered tool-call batch into deterministic DAG levels.\n * Resolves default claims, computes source-directed dependency levels, applies\n * the optional width cap, and computes the canonical plan key. Pure and\n * deterministic.\n */\nexport async function scheduleDagLevels(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ScheduleDagLevelsOptions,\n): Promise<DagSchedulePlan> {\n\tconst entries = await resolveBatchClaims(toolCalls, options);\n\tconst baseLevels = assignDagLevels(entries);\n\tconst levels = applyConcurrencyCap(baseLevels, options.maxConcurrency);\n\treturn { levels, planKey: computePlanKey(entries) };\n}\n"]}
1
+ {"version":3,"file":"tool-dag-scheduler.js","sourceRoot":"","sources":["../src/tool-dag-scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAEN,kBAAkB,EAClB,cAAc,EAEd,mBAAmB,EACnB,wBAAwB,GAGxB,MAAM,2BAA2B,CAAC;AAqCnC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,SAAuC,EACvC,OAAiC,EACD;IAChC,MAAM,OAAO,GAAyB,EAAE,CAAC;IACzC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QACvD,MAAM,UAAU,GAAG,MAAM,wBAAwB,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;QAC7E,MAAM,eAAe,GAAG,UAAU,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClG,OAAO,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,OAAO,CAAC;AAAA,CACf;AAYD,SAAS,iBAAiB,CAAC,KAAwB,EAAE,UAAwC,EAAW;IACvG,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,cAAc,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;YACtC,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED;;;;;;GAMG;AACH,SAAS,wBAAwB,CAAC,UAA+B,EAAE,KAAe,EAAW;IAC5F,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IACb,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACrC,OAAO,IAAI,CAAC;IACb,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;QACvC,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC;QACb,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YAC9B,IAAI,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;gBACjD,OAAO,IAAI,CAAC;YACb,CAAC;YACD,IAAI,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;gBAChD,OAAO,IAAI,CAAC;YACb,CAAC;QACF,CAAC;aAAM,IAAI,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YACxD,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAAsC,EAAc;IACnF,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;QACpC,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,KAAK,IAAI,UAAU,GAAG,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,CAAC;YACnE,IAAI,wBAAwB,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;gBAC9D,WAAW,GAAG,UAAU,GAAG,CAAC,CAAC;YAC9B,CAAC;QACF,CAAC;QACD,IAAI,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,MAAM,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;YAC/E,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;QACD,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACvC,IAAI,UAAU,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,EAAE,CAAC;YAC5G,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC;QAC5B,CAAC;aAAM,CAAC;YACP,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,eAAe,EAAE,CAAC;gBAC3C,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;oBAC9B,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAChC,CAAC;qBAAM,CAAC;oBACP,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC/B,CAAC;YACF,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AAAA,CAC5C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAsC,EAAc;IACzF,MAAM,YAAY,GAAe,EAAE,CAAC;IACpC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;YAClD,IAAI,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC;gBACjF,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACxB,CAAC;QACF,CAAC;QACD,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,YAAY,CAAC;AAAA,CACpB;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAA2B,EAAE,GAAuB,EAAc;IACrG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QAClE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,MAAM,OAAO,GAAe,EAAE,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,MAAM,IAAI,UAAU,EAAE,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;YAC5B,SAAS;QACV,CAAC;QACD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,UAAU,EAAE,CAAC;YAC/D,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC;QACtD,CAAC;IACF,CAAC;IACD,OAAO,OAAO,CAAC;AAAA,CACf;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAsC,EAAU;IAC9E,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC3C,GAAG,IAAI,IAAI,CAAC;QACb,CAAC;aAAM,CAAC;YACP,GAAG,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC;QACrD,CAAC;IACF,CAAC;IACD,OAAO,GAAG,CAAC;AAAA,CACX;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,SAAuC,EACvC,OAAiC,EACN;IAC3B,MAAM,OAAO,GAAG,MAAM,kBAAkB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC7D,MAAM,UAAU,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IACvE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;AAAA,CAC7D;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,2BAA2B,CAC1C,WAAmB,EACnB,eAAoC,EACpC,KAA0B,EAC1B,OAA4B,EAC5B,OAA4B,EAC5B,WAAqD,EAC3C;IACV,KAAK,MAAM,CAAC,UAAU,EAAE,UAAU,CAAC,IAAI,WAAW,EAAE,CAAC;QACpD,IAAI,UAAU,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;YAAE,SAAS;QACpE,IAAI,UAAU,GAAG,WAAW,EAAE,CAAC;YAC9B,IAAI,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC;gBAAE,SAAS;QACrC,CAAC;aAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YACrC,kEAAkE;YAClE,SAAS;QACV,CAAC;QACD,IAAI,mBAAmB,CAAC,UAAU,EAAE,eAAe,CAAC;YAAE,OAAO,IAAI,CAAC;IACnE,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb","sourcesContent":["/**\n * Pure, browser-safe deterministic resource-claim DAG scheduler.\n *\n * Given a source-ordered batch of tool calls and their resolved resource\n * claims, this module assigns each call after every earlier call it conflicts\n * with. Levels run in source order; calls inside one level are mutually\n * conflict-free and may execute concurrently. A positive `maxConcurrency`\n * width cap splits each level into deterministic contiguous source-ordered\n * chunks.\n *\n * Determinism contract:\n * - The level assignment is a pure function of the source-ordered input\n * claims. The canonical example is the head-of-line example\n * `write x, write x, write y`, which schedules as `[[0, 2], [1]]`: the second\n * write to `x` is pushed to level 1, but the independent write to `y` is not\n * blocked and joins level 0.\n * - Reordering claims *within* a single call does not change the plan: claims\n * are canonicalized (sorted, fixed property order) before comparison.\n * - {@link DagSchedulePlan.planKey} is a canonical serialization of the\n * resolved claim sequence (canonical claim data only — never execution\n * timing or outcomes). It is collision-free for distinct canonical claim\n * sequences.\n *\n * This module uses no platform APIs (no `process`, fs, `node:path`, or timers).\n */\n\nimport {\n\ttype ClaimableToolCall,\n\tcanonicalizeClaims,\n\tclaimsConflict,\n\ttype ResolveToolClaimsOptions,\n\tresolutionsConflict,\n\tresolveToolClaimsForCall,\n\ttype ToolClaimResolution,\n\ttype ToolResourceClaim,\n} from \"./tool-resource-claims.ts\";\n\n/** Options for scheduling a batch into DAG levels. */\nexport interface ScheduleDagLevelsOptions extends ResolveToolClaimsOptions {\n\t/**\n\t * Optional positive width cap. When set, each level is split into contiguous\n\t * source-ordered chunks of at most this many calls so a wide conflict-free\n\t * level does not fan out unbounded. Absent, non-finite, or non-positive\n\t * values leave each level whole. Does not affect `planKey`.\n\t */\n\tmaxConcurrency?: number;\n}\n\n/** A scheduled plan: ordered levels of source indices plus a canonical key. */\nexport interface DagSchedulePlan {\n\t/** Levels in execution order; each level holds source indices that may run concurrently. */\n\tlevels: number[][];\n\t/**\n\t * Canonical deterministic key over the resolved claim sequence only (never\n\t * execution timing/outcomes). Stable under claim reordering within a call.\n\t */\n\tplanKey: string;\n\t/**\n\t * The resolved, canonicalized claim entries the plan was built from. Callers\n\t * that re-resolve claims after argument mutation (e.g. authorization hooks)\n\t * compare against these to detect scope drift. Read-only for consumers.\n\t */\n\tentries: readonly ResolvedClaimEntry[];\n}\n\n/** One tool call's resolved claim data, canonicalized for stable planning. */\nexport interface ResolvedClaimEntry {\n\tsourceIndex: number;\n\tresolution: ToolClaimResolution;\n\tcanonicalClaims: ToolResourceClaim[];\n}\n\n/**\n * Resolve and canonicalize claims for a whole batch, preserving source order.\n * Registered custom resolvers are awaited one call at a time in source order,\n * and every resolution completes before a schedule is constructed.\n */\nexport async function resolveBatchClaims(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ResolveToolClaimsOptions,\n): Promise<ResolvedClaimEntry[]> {\n\tconst entries: ResolvedClaimEntry[] = [];\n\tfor (let index = 0; index < toolCalls.length; index++) {\n\t\tconst resolution = await resolveToolClaimsForCall(toolCalls[index], options);\n\t\tconst canonicalClaims = resolution.kind === \"claims\" ? canonicalizeClaims(resolution.claims) : [];\n\t\tentries.push({ sourceIndex: index, resolution, canonicalClaims });\n\t}\n\treturn entries;\n}\n\ninterface DagLevel {\n\tindices: number[];\n\t/** Write claims currently placed in this level (for fast read-vs-write checks). */\n\twriteClaims: ToolResourceClaim[];\n\t/** Read claims currently placed in this level (for fast write-vs-read checks). */\n\treadClaims: ToolResourceClaim[];\n\t/** True once an exclusive call is placed here; such a level accepts no more. */\n\thasExclusive: boolean;\n}\n\nfunction claimConflictsAny(claim: ToolResourceClaim, candidates: readonly ToolResourceClaim[]): boolean {\n\tfor (const candidate of candidates) {\n\t\tif (claimsConflict(claim, candidate)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * True when `resolution` cannot join `level`. Equivalent to the full pairwise\n * `resolutionsConflict` check, optimized so read/read never triggers a scan:\n * a read claim only needs to scan the level's writes, and a write claim scans\n * both writes and reads. An exclusive resolution, or a level that already holds\n * an exclusive call, conflicts unconditionally.\n */\nfunction resolutionConflictsLevel(resolution: ToolClaimResolution, level: DagLevel): boolean {\n\tif (level.hasExclusive) {\n\t\treturn true;\n\t}\n\tif (resolution.kind === \"exclusive\") {\n\t\treturn true;\n\t}\n\tfor (const claim of resolution.claims) {\n\t\tif (claim.access === \"exclusive\") {\n\t\t\treturn true;\n\t\t}\n\t\tif (claim.access === \"write\") {\n\t\t\tif (claimConflictsAny(claim, level.writeClaims)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (claimConflictsAny(claim, level.readClaims)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (claimConflictsAny(claim, level.writeClaims)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Assign each source-ordered claim entry one level after its latest earlier\n * conflict. Deterministic and stable: equal inputs (including claim reordering\n * within a call, which is canonicalized away) always produce equal levels, and\n * every directed conflict edge advances at least one level.\n */\nexport function assignDagLevels(entries: readonly ResolvedClaimEntry[]): number[][] {\n\tconst levels: DagLevel[] = [];\n\tfor (const entry of entries) {\n\t\tconst resolution = entry.resolution;\n\t\tlet targetIndex = 0;\n\t\tfor (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {\n\t\t\tif (resolutionConflictsLevel(resolution, levels[levelIndex])) {\n\t\t\t\ttargetIndex = levelIndex + 1;\n\t\t\t}\n\t\t}\n\t\tlet target = levels[targetIndex];\n\t\tif (!target) {\n\t\t\ttarget = { indices: [], writeClaims: [], readClaims: [], hasExclusive: false };\n\t\t\tlevels.push(target);\n\t\t}\n\t\ttarget.indices.push(entry.sourceIndex);\n\t\tif (resolution.kind === \"exclusive\" || entry.canonicalClaims.some((claim) => claim.access === \"exclusive\")) {\n\t\t\ttarget.hasExclusive = true;\n\t\t} else {\n\t\t\tfor (const claim of entry.canonicalClaims) {\n\t\t\t\tif (claim.access === \"write\") {\n\t\t\t\t\ttarget.writeClaims.push(claim);\n\t\t\t\t} else {\n\t\t\t\t\ttarget.readClaims.push(claim);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn levels.map((level) => level.indices);\n}\n\n/**\n * Direct conflicting predecessors for each source-ordered entry: `result[i]`\n * lists every earlier position `i` must wait for.\n *\n * This is the precedence graph that {@link assignDagLevels} approximates with a\n * barrier schedule. Levels force every call in level N+1 to wait for ALL of\n * level N, so one slow unrelated call in a level delays the whole next level.\n * Dependency edges only connect calls that actually conflict, so a consumer can\n * start each call as soon as its own predecessors settle.\n *\n * Deterministic and stable: the result is a pure function of the source-ordered\n * canonicalized claims, and every list is ascending and strictly earlier-only.\n * Transitively-implied edges are kept rather than reduced — waiting on an\n * already-finished ancestor costs nothing and keeps this a simple pure scan.\n *\n * The graph is never deeper than the barrier schedule: for any `j` in\n * `result[i]`, `i` conflicts with an entry in level `j`, so\n * `level(i) >= level(j) + 1`, and depth follows by induction.\n */\nexport function assignDagDependencies(entries: readonly ResolvedClaimEntry[]): number[][] {\n\tconst dependencies: number[][] = [];\n\tfor (let index = 0; index < entries.length; index++) {\n\t\tconst blockers: number[] = [];\n\t\tfor (let earlier = 0; earlier < index; earlier++) {\n\t\t\tif (resolutionsConflict(entries[earlier].resolution, entries[index].resolution)) {\n\t\t\t\tblockers.push(earlier);\n\t\t\t}\n\t\t}\n\t\tdependencies.push(blockers);\n\t}\n\treturn dependencies;\n}\n\n/**\n * Split each level into contiguous source-ordered chunks of at most `cap` calls.\n * Absent/non-finite/non-positive `cap` returns the levels unchanged.\n */\nexport function applyConcurrencyCap(levels: readonly number[][], cap: number | undefined): number[][] {\n\tif (typeof cap !== \"number\" || !Number.isFinite(cap) || cap <= 0) {\n\t\treturn levels.map((level) => level.slice());\n\t}\n\tconst integerCap = Math.max(1, Math.floor(cap));\n\tconst chunked: number[][] = [];\n\tfor (const level of levels) {\n\t\tif (level.length <= integerCap) {\n\t\t\tchunked.push(level.slice());\n\t\t\tcontinue;\n\t\t}\n\t\tfor (let start = 0; start < level.length; start += integerCap) {\n\t\t\tchunked.push(level.slice(start, start + integerCap));\n\t\t}\n\t}\n\treturn chunked;\n}\n\n/**\n * Canonical deterministic key over the resolved claim sequence. Uses\n * `JSON.stringify` of each call's canonicalized claims (fixed property order,\n * sorted) with an `\"E\"`/`\"C\"` discriminator, so it is collision-free for\n * distinct canonical claim sequences and stable under claim reordering within a\n * call. Contains no execution timing or outcomes.\n */\nexport function computePlanKey(entries: readonly ResolvedClaimEntry[]): string {\n\tlet key = \"\";\n\tfor (const entry of entries) {\n\t\tif (entry.resolution.kind === \"exclusive\") {\n\t\t\tkey += \"E,\";\n\t\t} else {\n\t\t\tkey += `C${JSON.stringify(entry.canonicalClaims)},`;\n\t\t}\n\t}\n\treturn key;\n}\n\n/**\n * Schedule a source-ordered tool-call batch into deterministic DAG levels.\n * Resolves default claims, computes source-directed dependency levels, applies\n * the optional width cap, and computes the canonical plan key. Pure and\n * deterministic.\n */\nexport async function scheduleDagLevels(\n\ttoolCalls: readonly ClaimableToolCall[],\n\toptions: ScheduleDagLevelsOptions,\n): Promise<DagSchedulePlan> {\n\tconst entries = await resolveBatchClaims(toolCalls, options);\n\tconst baseLevels = assignDagLevels(entries);\n\tconst levels = applyConcurrencyCap(baseLevels, options.maxConcurrency);\n\treturn { levels, planKey: computePlanKey(entries), entries };\n}\n\n/**\n * True when executing `sourceIndex` now would overlap an unsettled call whose\n * known claims conflict with `finalResolution`.\n *\n * Two distinct violations are covered:\n * - Earlier-source unsettled calls: the initial plan already ordered that\n * pair, so only post-hook scope drift can create the situation; the caller\n * defers the call instead of silently reversing the conflict pair.\n * - Later-source calls that are already running (present in `running`): a\n * hook that expands this call's scope onto their claims must defer — the\n * running call cannot be un-started. Later-source calls that are merely\n * pending keep source order: they may not overlap this call once it runs.\n *\n * Earlier-source calls inside `ready` (this same re-plan) are excluded — the\n * sub-level packing orders them.\n */\nexport function conflictsWithUnsettledClaim(\n\tsourceIndex: number,\n\tfinalResolution: ToolClaimResolution,\n\tready: ReadonlySet<number>,\n\tsettled: ReadonlySet<number>,\n\trunning: ReadonlySet<number>,\n\tresolutions: ReadonlyMap<number, ToolClaimResolution>,\n): boolean {\n\tfor (const [otherIndex, resolution] of resolutions) {\n\t\tif (otherIndex === sourceIndex || settled.has(otherIndex)) continue;\n\t\tif (otherIndex < sourceIndex) {\n\t\t\tif (ready.has(otherIndex)) continue;\n\t\t} else if (!running.has(otherIndex)) {\n\t\t\t// Later-source and not running: source order wins, no constraint.\n\t\t\tcontinue;\n\t\t}\n\t\tif (resolutionsConflict(resolution, finalResolution)) return true;\n\t}\n\treturn false;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omk-agent-core",
3
- "version": "0.99.0",
3
+ "version": "1.2.0",
4
4
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,7 +30,7 @@
30
30
  "prepublishOnly": "npm run clean && npm run build"
31
31
  },
32
32
  "dependencies": {
33
- "omk-ai": "^0.99.0",
33
+ "omk-ai": "^1.2.0",
34
34
  "ignore": "7.0.6",
35
35
  "typebox": "1.3.11",
36
36
  "yaml": "2.9.0"