@abelxiaoxing/cadence 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/agents/implementation-worker.md +17 -11
- package/config/routes.example.json +0 -2
- package/package.json +2 -2
- package/prompts/abel-design.md +20 -16
- package/prompts/abel-implement.md +4 -3
- package/skills/abel-workflow/SKILL.md +6 -3
- package/src/agent-registry.ts +0 -1
- package/src/candidate-patch.ts +250 -0
- package/src/child-session.ts +27 -24
- package/src/contracts.ts +1 -8
- package/src/control-contracts.ts +72 -48
- package/src/design-control.ts +224 -17
- package/src/design-journal.ts +69 -22
- package/src/index.ts +81 -23
- package/src/packet-runtime.ts +6 -6
- package/src/route-policy.ts +45 -17
- package/src/run-state.ts +4 -2
- package/src/run-store.ts +22 -6
- package/src/submit-tool.ts +149 -49
- package/src/worker-broker.ts +0 -1
- package/src/workflow-engine.ts +22 -171
- package/agents/contract-reviewer.md +0 -11
package/src/design-journal.ts
CHANGED
|
@@ -11,6 +11,7 @@ const SHA256 = /^[a-f0-9]{64}$/u;
|
|
|
11
11
|
const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
12
12
|
const ZERO_HASH = "0".repeat(64);
|
|
13
13
|
const MAX_PLAN_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
const MAX_CONTRACT_BYTES = 256 * 1024;
|
|
14
15
|
|
|
15
16
|
const SCHEMA = `
|
|
16
17
|
CREATE TABLE IF NOT EXISTS design_facts (
|
|
@@ -144,6 +145,7 @@ export interface DesignGateProjection {
|
|
|
144
145
|
| "behavior-decision-changed"
|
|
145
146
|
| "technical-decision-changed"
|
|
146
147
|
| "plan-recompiled"
|
|
148
|
+
| "gate-a-reapproved"
|
|
147
149
|
| "gate-a-stale"
|
|
148
150
|
| "plan-missing";
|
|
149
151
|
}
|
|
@@ -223,6 +225,21 @@ function sha256(value: Uint8Array | string): string {
|
|
|
223
225
|
return createHash("sha256").update(value).digest("hex");
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
function contractHash(domain: string, value: string): string {
|
|
229
|
+
if (
|
|
230
|
+
typeof value !== "string" ||
|
|
231
|
+
value.trim().length === 0 ||
|
|
232
|
+
Buffer.byteLength(value, "utf8") > MAX_CONTRACT_BYTES
|
|
233
|
+
) {
|
|
234
|
+
throw new Error("design-transient-input-invalid");
|
|
235
|
+
}
|
|
236
|
+
return createHash("sha256")
|
|
237
|
+
.update(domain)
|
|
238
|
+
.update("\0")
|
|
239
|
+
.update(value.replace(/\r\n?/gu, "\n").trim())
|
|
240
|
+
.digest("hex");
|
|
241
|
+
}
|
|
242
|
+
|
|
226
243
|
function requireIdentifier(value: string, label: string): void {
|
|
227
244
|
if (!IDENTIFIER.test(value)) throw new Error(`invalid-${label}`);
|
|
228
245
|
}
|
|
@@ -877,11 +894,14 @@ export class DesignJournal {
|
|
|
877
894
|
operationId: string;
|
|
878
895
|
decisionId: string;
|
|
879
896
|
category: DecisionCategory;
|
|
880
|
-
|
|
897
|
+
contract: string;
|
|
881
898
|
refs: string[];
|
|
882
899
|
}): Record<string, unknown> {
|
|
883
900
|
requireIdentifier(input.decisionId, "design-decision-id");
|
|
884
|
-
|
|
901
|
+
const decisionContractHash = contractHash(
|
|
902
|
+
"abel-design-decision-v1",
|
|
903
|
+
input.contract,
|
|
904
|
+
);
|
|
885
905
|
if (input.category !== "behavior" && input.category !== "technical") {
|
|
886
906
|
throw new Error("design-decision-category-invalid");
|
|
887
907
|
}
|
|
@@ -890,7 +910,7 @@ export class DesignJournal {
|
|
|
890
910
|
operation: "record-decision",
|
|
891
911
|
decisionId: input.decisionId,
|
|
892
912
|
category: input.category,
|
|
893
|
-
contractHash:
|
|
913
|
+
contractHash: decisionContractHash,
|
|
894
914
|
refs,
|
|
895
915
|
};
|
|
896
916
|
return this.#transaction(() => {
|
|
@@ -909,7 +929,7 @@ export class DesignJournal {
|
|
|
909
929
|
let decision: DesignDecisionProjection;
|
|
910
930
|
if (
|
|
911
931
|
prior &&
|
|
912
|
-
prior.contractHash ===
|
|
932
|
+
prior.contractHash === decisionContractHash &&
|
|
913
933
|
canonicalJson(prior.refs) === canonicalJson(refs)
|
|
914
934
|
) {
|
|
915
935
|
decision = prior;
|
|
@@ -919,7 +939,7 @@ export class DesignJournal {
|
|
|
919
939
|
decisionId: input.decisionId,
|
|
920
940
|
category: input.category,
|
|
921
941
|
revision,
|
|
922
|
-
contractHash:
|
|
942
|
+
contractHash: decisionContractHash,
|
|
923
943
|
refs,
|
|
924
944
|
};
|
|
925
945
|
const fact = this.#appendFact(
|
|
@@ -980,12 +1000,20 @@ export class DesignJournal {
|
|
|
980
1000
|
return undefined;
|
|
981
1001
|
}
|
|
982
1002
|
|
|
983
|
-
approveGate(
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1003
|
+
approveGate(
|
|
1004
|
+
input:
|
|
1005
|
+
| {
|
|
1006
|
+
runId: string;
|
|
1007
|
+
operationId: string;
|
|
1008
|
+
gate: "gate-a";
|
|
1009
|
+
contract: string;
|
|
1010
|
+
}
|
|
1011
|
+
| {
|
|
1012
|
+
runId: string;
|
|
1013
|
+
operationId: string;
|
|
1014
|
+
gate: "gate-b";
|
|
1015
|
+
},
|
|
1016
|
+
): {
|
|
989
1017
|
operation: "approve-gate";
|
|
990
1018
|
runId: string;
|
|
991
1019
|
gate: DesignGate;
|
|
@@ -994,14 +1022,17 @@ export class DesignJournal {
|
|
|
994
1022
|
if (input.gate !== "gate-a" && input.gate !== "gate-b") {
|
|
995
1023
|
throw new Error("design-gate-invalid");
|
|
996
1024
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
contractHash: input.contractHash,
|
|
1002
|
-
};
|
|
1025
|
+
const gateAContractHash =
|
|
1026
|
+
input.gate === "gate-a"
|
|
1027
|
+
? contractHash("abel-design-gate-a-v1", input.contract)
|
|
1028
|
+
: undefined;
|
|
1003
1029
|
return this.#transaction(() => {
|
|
1004
1030
|
this.#run(input.runId, true);
|
|
1031
|
+
const request = {
|
|
1032
|
+
operation: "approve-gate",
|
|
1033
|
+
gate: input.gate,
|
|
1034
|
+
...(gateAContractHash ? { contractHash: gateAContractHash } : {}),
|
|
1035
|
+
};
|
|
1005
1036
|
const replay = this.#operationReplay(
|
|
1006
1037
|
input.runId,
|
|
1007
1038
|
input.operationId,
|
|
@@ -1012,15 +1043,26 @@ export class DesignJournal {
|
|
|
1012
1043
|
return structuredClone(replay) as ReturnType<
|
|
1013
1044
|
DesignJournal["approveGate"]
|
|
1014
1045
|
>;
|
|
1046
|
+
let approvedContractHash = gateAContractHash;
|
|
1015
1047
|
if (input.gate === "gate-b") {
|
|
1016
1048
|
const current = this.#gateStatus(input.runId);
|
|
1017
1049
|
if (!current.gateA.current) throw new Error("design-gate-a-required");
|
|
1018
1050
|
const plan = this.#currentPlanProjection(input.runId);
|
|
1019
1051
|
if (!plan) throw new Error("design-plan-required");
|
|
1020
|
-
|
|
1021
|
-
|
|
1052
|
+
const latestDecisionSequence = this.#facts(input.runId)
|
|
1053
|
+
.filter((fact) => fact.kind === "decision")
|
|
1054
|
+
.at(-1)?.sequence;
|
|
1055
|
+
if (
|
|
1056
|
+
plan.sequence <= (current.gateA.approvedSequence ?? 0) ||
|
|
1057
|
+
(latestDecisionSequence !== undefined &&
|
|
1058
|
+
plan.sequence <= latestDecisionSequence)
|
|
1059
|
+
) {
|
|
1060
|
+
throw new Error("design-plan-stale");
|
|
1022
1061
|
}
|
|
1062
|
+
approvedContractHash = plan.canonicalHash;
|
|
1023
1063
|
}
|
|
1064
|
+
if (!approvedContractHash)
|
|
1065
|
+
throw new Error("design-transient-input-invalid");
|
|
1024
1066
|
const existing = this.#latestApproval(input.runId, input.gate);
|
|
1025
1067
|
const currentGate = this.#gateStatus(input.runId)[
|
|
1026
1068
|
input.gate === "gate-a" ? "gateA" : "gateB"
|
|
@@ -1029,7 +1071,7 @@ export class DesignJournal {
|
|
|
1029
1071
|
if (
|
|
1030
1072
|
existing &&
|
|
1031
1073
|
currentGate.current &&
|
|
1032
|
-
existing.proof.contractHash ===
|
|
1074
|
+
existing.proof.contractHash === approvedContractHash
|
|
1033
1075
|
) {
|
|
1034
1076
|
proof = existing.proof;
|
|
1035
1077
|
} else {
|
|
@@ -1037,7 +1079,7 @@ export class DesignJournal {
|
|
|
1037
1079
|
const payload = {
|
|
1038
1080
|
gate: input.gate,
|
|
1039
1081
|
revision,
|
|
1040
|
-
contractHash:
|
|
1082
|
+
contractHash: approvedContractHash,
|
|
1041
1083
|
};
|
|
1042
1084
|
const fact = this.#appendFact(
|
|
1043
1085
|
input.runId,
|
|
@@ -1047,7 +1089,7 @@ export class DesignJournal {
|
|
|
1047
1089
|
);
|
|
1048
1090
|
proof = {
|
|
1049
1091
|
revision,
|
|
1050
|
-
contractHash:
|
|
1092
|
+
contractHash: approvedContractHash,
|
|
1051
1093
|
recordHash: fact.record_hash,
|
|
1052
1094
|
};
|
|
1053
1095
|
}
|
|
@@ -1506,6 +1548,11 @@ export class DesignJournal {
|
|
|
1506
1548
|
latestTechnical > gateBApproval.sequence
|
|
1507
1549
|
) {
|
|
1508
1550
|
staleReason = "technical-decision-changed";
|
|
1551
|
+
} else if (
|
|
1552
|
+
gateAApproval !== undefined &&
|
|
1553
|
+
gateAApproval.sequence > gateBApproval.sequence
|
|
1554
|
+
) {
|
|
1555
|
+
staleReason = "gate-a-reapproved";
|
|
1509
1556
|
} else if (plan && plan.sequence > gateBApproval.sequence) {
|
|
1510
1557
|
staleReason = "plan-recompiled";
|
|
1511
1558
|
} else if (!plan) {
|
package/src/index.ts
CHANGED
|
@@ -37,7 +37,10 @@ import {
|
|
|
37
37
|
type StructuredVerificationContract,
|
|
38
38
|
verificationSteps,
|
|
39
39
|
} from "./contracts.ts";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
canonicalizeControlCommandToolInput,
|
|
42
|
+
validateControlCommand,
|
|
43
|
+
} from "./control-contracts.ts";
|
|
41
44
|
import {
|
|
42
45
|
assessDeliveryTraceability,
|
|
43
46
|
compileImplementPlan,
|
|
@@ -210,6 +213,42 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
|
|
|
210
213
|
description:
|
|
211
214
|
"One closed durable Design control operation. The operation body is strict and idempotent by operationId.",
|
|
212
215
|
oneOf: [
|
|
216
|
+
...(["requirement", "change"] as const).map((field) => ({
|
|
217
|
+
type: "object" as const,
|
|
218
|
+
properties: {
|
|
219
|
+
operation: { type: "string" as const, enum: ["start"] },
|
|
220
|
+
operationId: { type: "string" as const },
|
|
221
|
+
[field]: {
|
|
222
|
+
type: "string" as const,
|
|
223
|
+
description:
|
|
224
|
+
field === "requirement"
|
|
225
|
+
? "Raw Design requirement; code derives and stores only its stable hash."
|
|
226
|
+
: "Existing canonical change name to resume or revise.",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
required: ["operation", "operationId", field],
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
})),
|
|
232
|
+
{
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {
|
|
235
|
+
operation: { type: "string", enum: ["status"] },
|
|
236
|
+
runId: { type: "string" },
|
|
237
|
+
},
|
|
238
|
+
required: ["operation", "runId"],
|
|
239
|
+
additionalProperties: false,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
operation: { type: "string", enum: ["bind-change"] },
|
|
245
|
+
runId: { type: "string" },
|
|
246
|
+
operationId: { type: "string" },
|
|
247
|
+
change: { type: "string" },
|
|
248
|
+
},
|
|
249
|
+
required: ["operation", "runId", "operationId", "change"],
|
|
250
|
+
additionalProperties: false,
|
|
251
|
+
},
|
|
213
252
|
{
|
|
214
253
|
type: "object",
|
|
215
254
|
properties: {
|
|
@@ -218,7 +257,11 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
|
|
|
218
257
|
operationId: { type: "string" },
|
|
219
258
|
decisionId: { type: "string" },
|
|
220
259
|
category: { type: "string", enum: ["behavior", "technical"] },
|
|
221
|
-
|
|
260
|
+
contract: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description:
|
|
263
|
+
"Normalized decision contract; code hashes it and never persists the text.",
|
|
264
|
+
},
|
|
222
265
|
refs: { type: "array", items: { type: "string" } },
|
|
223
266
|
},
|
|
224
267
|
required: [
|
|
@@ -227,7 +270,7 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
|
|
|
227
270
|
"operationId",
|
|
228
271
|
"decisionId",
|
|
229
272
|
"category",
|
|
230
|
-
"
|
|
273
|
+
"contract",
|
|
231
274
|
"refs",
|
|
232
275
|
],
|
|
233
276
|
additionalProperties: false,
|
|
@@ -238,10 +281,25 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
|
|
|
238
281
|
operation: { type: "string", enum: ["approve-gate"] },
|
|
239
282
|
runId: { type: "string" },
|
|
240
283
|
operationId: { type: "string" },
|
|
241
|
-
gate: { type: "string", enum: ["gate-a"
|
|
242
|
-
|
|
284
|
+
gate: { type: "string", enum: ["gate-a"] },
|
|
285
|
+
contract: {
|
|
286
|
+
type: "string",
|
|
287
|
+
description:
|
|
288
|
+
"Complete approved WHAT contract; code hashes it and never persists the text.",
|
|
289
|
+
},
|
|
243
290
|
},
|
|
244
|
-
required: ["operation", "runId", "operationId", "gate", "
|
|
291
|
+
required: ["operation", "runId", "operationId", "gate", "contract"],
|
|
292
|
+
additionalProperties: false,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
operation: { type: "string", enum: ["approve-gate"] },
|
|
298
|
+
runId: { type: "string" },
|
|
299
|
+
operationId: { type: "string" },
|
|
300
|
+
gate: { type: "string", enum: ["gate-b"] },
|
|
301
|
+
},
|
|
302
|
+
required: ["operation", "runId", "operationId", "gate"],
|
|
245
303
|
additionalProperties: false,
|
|
246
304
|
},
|
|
247
305
|
{
|
|
@@ -417,7 +475,7 @@ export interface PackageDeliverySourceOptions {
|
|
|
417
475
|
|
|
418
476
|
export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
|
|
419
477
|
discoverLatest(input: {
|
|
420
|
-
stage: "abel-
|
|
478
|
+
stage: "abel-implement";
|
|
421
479
|
change: string;
|
|
422
480
|
}): Promise<WorkflowAvailableDelivery | undefined>;
|
|
423
481
|
}
|
|
@@ -1418,6 +1476,7 @@ export function openPackageWorkflowControlEngine(
|
|
|
1418
1476
|
const routeResolution = loadRoutePolicy({
|
|
1419
1477
|
cwd: consumerRoot,
|
|
1420
1478
|
home: homedir(),
|
|
1479
|
+
...(initialContext.model ? { parentModel: initialContext.model } : {}),
|
|
1421
1480
|
});
|
|
1422
1481
|
const stateRoot = resolveStateRoot({
|
|
1423
1482
|
consumerRoot,
|
|
@@ -1614,6 +1673,7 @@ export function openPackageWorkflowControlEngine(
|
|
|
1614
1673
|
const operationRouteResolution = loadRoutePolicy({
|
|
1615
1674
|
cwd: consumerRoot,
|
|
1616
1675
|
home: homedir(),
|
|
1676
|
+
...(context.model ? { parentModel: context.model } : {}),
|
|
1617
1677
|
});
|
|
1618
1678
|
engine.updateRoutePolicy(
|
|
1619
1679
|
operationRouteResolution.ok
|
|
@@ -1636,12 +1696,7 @@ export function openPackageWorkflowControlEngine(
|
|
|
1636
1696
|
operationRouteResolution,
|
|
1637
1697
|
engine.routePolicyStatus(),
|
|
1638
1698
|
);
|
|
1639
|
-
|
|
1640
|
-
validation.value.stage === "abel-design" &&
|
|
1641
|
-
typeof outcome.runId === "string"
|
|
1642
|
-
? { design: design.status(outcome.runId) }
|
|
1643
|
-
: {};
|
|
1644
|
-
return { ...outcome, ...designStatus, routePolicy };
|
|
1699
|
+
return { ...outcome, routePolicy };
|
|
1645
1700
|
});
|
|
1646
1701
|
},
|
|
1647
1702
|
executeDesign(request: unknown) {
|
|
@@ -1781,16 +1836,15 @@ export function registerWorkflowControl(
|
|
|
1781
1836
|
},
|
|
1782
1837
|
stage: {
|
|
1783
1838
|
type: "string",
|
|
1784
|
-
enum: ["abel-
|
|
1839
|
+
enum: ["abel-implement"],
|
|
1785
1840
|
},
|
|
1786
1841
|
change: { type: "string" },
|
|
1787
|
-
provisionalKey: { type: "string" },
|
|
1788
1842
|
operationId: { type: "string" },
|
|
1789
1843
|
deliveryRevision: { type: "integer", minimum: 1 },
|
|
1790
1844
|
receiptHash: { type: "string" },
|
|
1791
1845
|
routeId: { type: "string" },
|
|
1792
1846
|
},
|
|
1793
|
-
required: ["command", "stage"],
|
|
1847
|
+
required: ["command", "stage", "change"],
|
|
1794
1848
|
additionalProperties: false,
|
|
1795
1849
|
} as const;
|
|
1796
1850
|
const PACKET_PARAMETERS = {
|
|
@@ -1818,9 +1872,15 @@ export function registerWorkflowControl(
|
|
|
1818
1872
|
label: "Abel Control",
|
|
1819
1873
|
description:
|
|
1820
1874
|
kind === "command"
|
|
1821
|
-
? "Private stage-bound Abel workflow control. Accepts durable
|
|
1875
|
+
? "Private stage-bound Abel workflow control. Accepts durable Implement change commands."
|
|
1822
1876
|
: "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
|
|
1823
1877
|
executionMode: "parallel",
|
|
1878
|
+
...(kind === "command"
|
|
1879
|
+
? {
|
|
1880
|
+
prepareArguments: (args: unknown) =>
|
|
1881
|
+
canonicalizeControlCommandToolInput(args) as never,
|
|
1882
|
+
}
|
|
1883
|
+
: {}),
|
|
1824
1884
|
parameters:
|
|
1825
1885
|
kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
|
|
1826
1886
|
async execute(
|
|
@@ -1953,7 +2013,9 @@ export function registerWorkflowControl(
|
|
|
1953
2013
|
...(usage === undefined ? {} : { usage }),
|
|
1954
2014
|
};
|
|
1955
2015
|
}
|
|
1956
|
-
const validation = validateControlCommand(
|
|
2016
|
+
const validation = validateControlCommand(
|
|
2017
|
+
canonicalizeControlCommandToolInput(params),
|
|
2018
|
+
);
|
|
1957
2019
|
if (!validation.ok) {
|
|
1958
2020
|
const error = new Error(validation.code);
|
|
1959
2021
|
error.name = "ControlCommandError";
|
|
@@ -2007,10 +2069,6 @@ export function registerWorkflowControl(
|
|
|
2007
2069
|
["completed", "discarded", "rejected"].includes(String(payload.state))
|
|
2008
2070
|
) {
|
|
2009
2071
|
await deactivateStage();
|
|
2010
|
-
} else if (validation.value.stage === "abel-design") {
|
|
2011
|
-
// Design starts/statuses through the command surface, then switches to
|
|
2012
|
-
// its bounded evidence and artifact packet surface for the next turn.
|
|
2013
|
-
registerDispatchTool("packet");
|
|
2014
2072
|
}
|
|
2015
2073
|
let display: ReturnType<typeof activity.finalizeWorkflow>;
|
|
2016
2074
|
if (tuiControl) {
|
|
@@ -2068,7 +2126,7 @@ export function registerWorkflowControl(
|
|
|
2068
2126
|
if (activePrompt === "abel-design" && prompt !== "abel-design") {
|
|
2069
2127
|
restoreDesignTools();
|
|
2070
2128
|
}
|
|
2071
|
-
registerDispatchTool(prompt === "abel-
|
|
2129
|
+
registerDispatchTool(prompt === "abel-implement" ? "command" : "packet");
|
|
2072
2130
|
activePrompt = prompt;
|
|
2073
2131
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
2074
2132
|
if (typeof sessionId === "string") {
|
package/src/packet-runtime.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { runtimeForWorkerRoute } from "./parent-provider.ts";
|
|
|
20
20
|
import {
|
|
21
21
|
loadRoutePolicy,
|
|
22
22
|
type RoutePolicy,
|
|
23
|
-
unavailableRoutePolicy,
|
|
24
23
|
type WorkerRole,
|
|
25
24
|
} from "./route-policy.ts";
|
|
26
25
|
import { type BrokerActivityUpdate, RunWorkerBroker } from "./worker-broker.ts";
|
|
@@ -319,7 +318,7 @@ export class PacketRuntime {
|
|
|
319
318
|
if (!agent) {
|
|
320
319
|
throw new Error("packet agent definition is unavailable");
|
|
321
320
|
}
|
|
322
|
-
const broker = this.#brokerFor(context
|
|
321
|
+
const broker = this.#brokerFor(context);
|
|
323
322
|
const routed = await broker.run({
|
|
324
323
|
runId: `${packet.stage}:${packet.id}`,
|
|
325
324
|
operationId: packet.id,
|
|
@@ -405,8 +404,8 @@ export class PacketRuntime {
|
|
|
405
404
|
return routed.value.child;
|
|
406
405
|
}
|
|
407
406
|
|
|
408
|
-
#brokerFor(
|
|
409
|
-
const root = path.resolve(cwd);
|
|
407
|
+
#brokerFor(context: PacketContext): RunWorkerBroker {
|
|
408
|
+
const root = path.resolve(context.cwd);
|
|
410
409
|
const existing = this.#brokers.get(root);
|
|
411
410
|
let policy = this.#routePolicy;
|
|
412
411
|
if (policy && existing) return existing;
|
|
@@ -414,12 +413,13 @@ export class PacketRuntime {
|
|
|
414
413
|
const resolution = loadRoutePolicy({
|
|
415
414
|
cwd: root,
|
|
416
415
|
...(this.#routePolicyHome ? { home: this.#routePolicyHome } : {}),
|
|
416
|
+
...(context.model ? { parentModel: context.model } : {}),
|
|
417
417
|
});
|
|
418
|
-
if (!resolution.ok
|
|
418
|
+
if (!resolution.ok) {
|
|
419
419
|
const code = resolution.diagnostics[0]?.code ?? "policy-invalid";
|
|
420
420
|
throw new Error(`route-policy-unavailable:${code}`);
|
|
421
421
|
}
|
|
422
|
-
policy = resolution.
|
|
422
|
+
policy = resolution.policy;
|
|
423
423
|
}
|
|
424
424
|
const serializedPolicy = JSON.stringify(policy);
|
|
425
425
|
if (existing && this.#brokerPolicies.get(root) === serializedPolicy) {
|
package/src/route-policy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { ROLES } from "./contracts.ts";
|
|
@@ -21,6 +21,11 @@ export interface RouteCapabilities {
|
|
|
21
21
|
maxTokens: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface ParentModelCapabilities {
|
|
25
|
+
contextWindow: number;
|
|
26
|
+
maxTokens: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
interface RouteBase {
|
|
25
30
|
id: string;
|
|
26
31
|
kind: "inherited" | "custom";
|
|
@@ -50,10 +55,9 @@ export interface RoutePolicy {
|
|
|
50
55
|
|
|
51
56
|
export type RoutePolicySource =
|
|
52
57
|
| { kind: "project" | "user"; path: string }
|
|
53
|
-
| { kind: "
|
|
58
|
+
| { kind: "default" };
|
|
54
59
|
|
|
55
60
|
export type RoutePolicyDiagnosticCode =
|
|
56
|
-
| "policy-missing"
|
|
57
61
|
| "policy-unreadable"
|
|
58
62
|
| "policy-invalid-json"
|
|
59
63
|
| "policy-invalid"
|
|
@@ -77,7 +81,7 @@ export type ParsedRoutePolicy =
|
|
|
77
81
|
export type RoutePolicyResolution =
|
|
78
82
|
| {
|
|
79
83
|
ok: true;
|
|
80
|
-
source:
|
|
84
|
+
source: RoutePolicySource;
|
|
81
85
|
policy: RoutePolicy;
|
|
82
86
|
}
|
|
83
87
|
| {
|
|
@@ -87,8 +91,8 @@ export type RoutePolicyResolution =
|
|
|
87
91
|
};
|
|
88
92
|
|
|
89
93
|
/**
|
|
90
|
-
* Internal fail-closed policy used while
|
|
91
|
-
*
|
|
94
|
+
* Internal fail-closed policy used while a selected explicit policy is
|
|
95
|
+
* invalid. It deliberately contains no inherited-parent route:
|
|
92
96
|
* the broker therefore reports endpoint-unavailable without making a Worker
|
|
93
97
|
* request, while the local durable control plane remains available.
|
|
94
98
|
*
|
|
@@ -351,8 +355,16 @@ export function parseRoutePolicy(value: unknown): ParsedRoutePolicy {
|
|
|
351
355
|
};
|
|
352
356
|
}
|
|
353
357
|
|
|
354
|
-
/**
|
|
355
|
-
export function parentRoutePolicy(
|
|
358
|
+
/** Default parent-model route used when no explicit policy file exists. */
|
|
359
|
+
export function parentRoutePolicy(
|
|
360
|
+
parentModel?: ParentModelCapabilities,
|
|
361
|
+
): RoutePolicy {
|
|
362
|
+
const contextWindow = positiveInteger(parentModel?.contextWindow)
|
|
363
|
+
? parentModel.contextWindow
|
|
364
|
+
: 1;
|
|
365
|
+
const maxTokens = positiveInteger(parentModel?.maxTokens)
|
|
366
|
+
? Math.min(parentModel.maxTokens, contextWindow)
|
|
367
|
+
: 1;
|
|
356
368
|
const parsed = parseRoutePolicy({
|
|
357
369
|
version: ROUTE_POLICY_VERSION,
|
|
358
370
|
routes: {
|
|
@@ -361,8 +373,8 @@ export function parentRoutePolicy(): RoutePolicy {
|
|
|
361
373
|
capabilities: {
|
|
362
374
|
roles: [...ROLES],
|
|
363
375
|
dialects: [...ROUTE_DIALECTS],
|
|
364
|
-
contextWindow
|
|
365
|
-
maxTokens
|
|
376
|
+
contextWindow,
|
|
377
|
+
maxTokens,
|
|
366
378
|
},
|
|
367
379
|
},
|
|
368
380
|
},
|
|
@@ -372,25 +384,41 @@ export function parentRoutePolicy(): RoutePolicy {
|
|
|
372
384
|
return parsed.policy;
|
|
373
385
|
}
|
|
374
386
|
|
|
375
|
-
function selectedSource(
|
|
387
|
+
function selectedSource(
|
|
388
|
+
cwd: string,
|
|
389
|
+
home: string,
|
|
390
|
+
): { kind: "project" | "user"; path: string } | { kind: "none" } {
|
|
391
|
+
const mayExist = (candidate: string): boolean => {
|
|
392
|
+
try {
|
|
393
|
+
return lstatSync(candidate, { throwIfNoEntry: false }) !== undefined;
|
|
394
|
+
} catch {
|
|
395
|
+
// Permission and path-observation failures are explicit unreadable
|
|
396
|
+
// policy candidates, not proof that the policy is absent.
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
376
400
|
const projectPath = path.join(cwd, ".pi", "cadence", "routes.json");
|
|
377
|
-
if (
|
|
401
|
+
if (mayExist(projectPath)) return { kind: "project", path: projectPath };
|
|
378
402
|
const userPath = path.join(home, ".pi", "agent", "cadence", "routes.json");
|
|
379
|
-
if (
|
|
403
|
+
if (mayExist(userPath)) return { kind: "user", path: userPath };
|
|
380
404
|
return { kind: "none" };
|
|
381
405
|
}
|
|
382
406
|
|
|
383
407
|
export function loadRoutePolicy(
|
|
384
|
-
options: {
|
|
408
|
+
options: {
|
|
409
|
+
cwd?: string;
|
|
410
|
+
home?: string;
|
|
411
|
+
parentModel?: ParentModelCapabilities;
|
|
412
|
+
} = {},
|
|
385
413
|
): RoutePolicyResolution {
|
|
386
414
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
387
415
|
const home = path.resolve(options.home ?? homedir());
|
|
388
416
|
const source = selectedSource(cwd, home);
|
|
389
417
|
if (source.kind === "none") {
|
|
390
418
|
return {
|
|
391
|
-
ok:
|
|
392
|
-
source,
|
|
393
|
-
|
|
419
|
+
ok: true,
|
|
420
|
+
source: { kind: "default" },
|
|
421
|
+
policy: parentRoutePolicy(options.parentModel),
|
|
394
422
|
};
|
|
395
423
|
}
|
|
396
424
|
let bytes: string;
|
package/src/run-state.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
export const RUN_STAGES = ["abel-design", "abel-implement"] as const;
|
|
4
|
+
export type RunStage = (typeof RUN_STAGES)[number];
|
|
3
5
|
|
|
4
6
|
export const RUN_STATES = [
|
|
5
7
|
"created",
|
|
@@ -57,7 +59,7 @@ export interface DeliveryBindingProjection {
|
|
|
57
59
|
export interface RunProjection {
|
|
58
60
|
runId: string;
|
|
59
61
|
rootHash: string;
|
|
60
|
-
stage:
|
|
62
|
+
stage: RunStage;
|
|
61
63
|
change?: string;
|
|
62
64
|
provisionalKey?: string;
|
|
63
65
|
state: RunState;
|
package/src/run-store.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { chmodSync, closeSync, lstatSync, openSync } from "node:fs";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
|
-
import type { ControlStage } from "./control-contracts.ts";
|
|
5
4
|
import {
|
|
6
5
|
createRunEvent,
|
|
7
6
|
type DeliveryGate,
|
|
8
7
|
type RunEvent,
|
|
9
8
|
type RunProjection,
|
|
9
|
+
type RunStage,
|
|
10
10
|
type RunState,
|
|
11
11
|
reduceRunEvents,
|
|
12
12
|
ZERO_EVENT_HASH,
|
|
@@ -37,6 +37,7 @@ interface EventRow {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
interface OperationRow {
|
|
40
|
+
kind: string;
|
|
40
41
|
state: string;
|
|
41
42
|
outcome_json: string | null;
|
|
42
43
|
lease_token: string | null;
|
|
@@ -49,7 +50,7 @@ export interface RunStoreOptions {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface StartRunInput {
|
|
52
|
-
stage:
|
|
53
|
+
stage: RunStage;
|
|
53
54
|
change?: string;
|
|
54
55
|
provisionalKey?: string;
|
|
55
56
|
operationId: string;
|
|
@@ -338,7 +339,7 @@ export class RunStore {
|
|
|
338
339
|
#operation(runId: string, operationId: string): OperationRow | undefined {
|
|
339
340
|
return this.#database
|
|
340
341
|
.prepare(
|
|
341
|
-
`SELECT state, outcome_json, lease_token, lease_expires_at
|
|
342
|
+
`SELECT kind, state, outcome_json, lease_token, lease_expires_at
|
|
342
343
|
FROM operations WHERE run_id = ? AND operation_id = ?`,
|
|
343
344
|
)
|
|
344
345
|
.get(runId, operationId) as OperationRow | undefined;
|
|
@@ -347,8 +348,10 @@ export class RunStore {
|
|
|
347
348
|
#operationOutcome(
|
|
348
349
|
runId: string,
|
|
349
350
|
operationId: string,
|
|
351
|
+
kind: string,
|
|
350
352
|
): RunProjection | undefined {
|
|
351
353
|
const row = this.#operation(runId, operationId);
|
|
354
|
+
if (row && row.kind !== kind) throw new Error("operation-id-conflict");
|
|
352
355
|
if (row?.state !== "committed" || row.outcome_json === null)
|
|
353
356
|
return undefined;
|
|
354
357
|
return parseProjection(row.outcome_json);
|
|
@@ -465,6 +468,7 @@ export class RunStore {
|
|
|
465
468
|
const replay = this.#operationOutcome(
|
|
466
469
|
existing.run_id,
|
|
467
470
|
input.operationId,
|
|
471
|
+
"start",
|
|
468
472
|
);
|
|
469
473
|
if (replay) return replay;
|
|
470
474
|
const projection = parseProjection(existing.projection_json);
|
|
@@ -539,7 +543,11 @@ export class RunStore {
|
|
|
539
543
|
requireIdentifier(input.operationId, "operation-id");
|
|
540
544
|
if (!CHANGE_NAME.test(input.change)) throw new Error("invalid-change");
|
|
541
545
|
return this.#transaction(() => {
|
|
542
|
-
const replay = this.#operationOutcome(
|
|
546
|
+
const replay = this.#operationOutcome(
|
|
547
|
+
input.runId,
|
|
548
|
+
input.operationId,
|
|
549
|
+
"change-bound",
|
|
550
|
+
);
|
|
543
551
|
if (replay) return replay;
|
|
544
552
|
const current = this.status(input.runId);
|
|
545
553
|
if (current.stage !== "abel-design" || !current.provisionalKey) {
|
|
@@ -597,7 +605,11 @@ export class RunStore {
|
|
|
597
605
|
if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
|
|
598
606
|
this.#assertLease(input.lease);
|
|
599
607
|
}
|
|
600
|
-
const replay = this.#operationOutcome(
|
|
608
|
+
const replay = this.#operationOutcome(
|
|
609
|
+
input.runId,
|
|
610
|
+
input.operationId,
|
|
611
|
+
"delivery-bound",
|
|
612
|
+
);
|
|
601
613
|
if (replay) return replay;
|
|
602
614
|
const existing = this.#database
|
|
603
615
|
.prepare(
|
|
@@ -911,7 +923,11 @@ export class RunStore {
|
|
|
911
923
|
if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
|
|
912
924
|
this.#assertLease(input.lease);
|
|
913
925
|
}
|
|
914
|
-
const replay = this.#operationOutcome(
|
|
926
|
+
const replay = this.#operationOutcome(
|
|
927
|
+
input.runId,
|
|
928
|
+
input.operationId,
|
|
929
|
+
"state-transitioned",
|
|
930
|
+
);
|
|
915
931
|
if (replay) return replay;
|
|
916
932
|
const current = this.status(input.runId);
|
|
917
933
|
const projection = this.#appendEvent(input.runId, "state-transitioned", {
|