@abelxiaoxing/cadence 1.2.2 → 1.2.3
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 +7 -1
- package/agents/diagnosis-worker.md +6 -2
- package/config/routes.example.json +0 -1
- package/package.json +1 -1
- package/prompts/abel-design.md +15 -4
- package/skills/abel-workflow/SKILL.md +5 -3
- package/src/apply-transaction.ts +1 -5
- package/src/child-session.ts +37 -46
- package/src/contracts.ts +278 -32
- package/src/control-contracts.ts +62 -0
- package/src/delivery-compiler.ts +239 -124
- package/src/design-control.ts +72 -27
- package/src/design-journal.ts +2 -2
- package/src/file-snapshot.ts +1 -1
- package/src/index.ts +238 -49
- package/src/packet-runtime.ts +79 -21
- package/src/parent-provider.ts +63 -30
- package/src/route-policy.ts +58 -14
- package/src/run-store.ts +98 -23
- package/src/subagent-activity.ts +82 -6
- package/src/submit-tool.ts +182 -10
- package/src/task-ledger.ts +1 -2
- package/src/worker-broker.ts +72 -13
- package/src/workflow-engine.ts +219 -31
- package/src/workspace-store.ts +2 -10
package/src/subagent-activity.ts
CHANGED
|
@@ -87,7 +87,6 @@ export type ActivitySummary = EvidenceActivitySummary | DiffActivitySummary;
|
|
|
87
87
|
export type ActivityTone = "accent" | "success" | "warning" | "muted" | "error";
|
|
88
88
|
|
|
89
89
|
export interface ActivityDisplay {
|
|
90
|
-
version: 1;
|
|
91
90
|
kind: "activityDisplay";
|
|
92
91
|
requestId: string;
|
|
93
92
|
role: string;
|
|
@@ -188,6 +187,7 @@ export function sanitizeDisplayText(value: unknown, maxLength = 240): string {
|
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
const SAFE_WORKFLOW_CODE = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
190
|
+
const SAFE_DESIGN_DETAIL_VALUE = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
191
191
|
const SAFE_CONTROL_ACTIONS = new Set([
|
|
192
192
|
"start",
|
|
193
193
|
"status",
|
|
@@ -221,6 +221,82 @@ export function sanitizeFailureReason(value: unknown, maxLength = 240): string {
|
|
|
221
221
|
return safe.slice(0, Math.max(0, maxLength));
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
function safeDesignFailure(value: unknown): {
|
|
225
|
+
operation: string;
|
|
226
|
+
code: string;
|
|
227
|
+
diagnostics: Array<Record<string, string>>;
|
|
228
|
+
} | null {
|
|
229
|
+
const failure = asRecord(value);
|
|
230
|
+
if (
|
|
231
|
+
failure?.kind !== "design-control-failure" ||
|
|
232
|
+
typeof failure.operation !== "string" ||
|
|
233
|
+
!SAFE_DESIGN_DETAIL_VALUE.test(failure.operation) ||
|
|
234
|
+
typeof failure.code !== "string" ||
|
|
235
|
+
!SAFE_DESIGN_DETAIL_VALUE.test(failure.code) ||
|
|
236
|
+
!Array.isArray(failure.diagnostics)
|
|
237
|
+
) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const diagnostics = failure.diagnostics.flatMap((candidate) => {
|
|
241
|
+
const record = asRecord(candidate);
|
|
242
|
+
if (
|
|
243
|
+
!record ||
|
|
244
|
+
typeof record.code !== "string" ||
|
|
245
|
+
!SAFE_DESIGN_DETAIL_VALUE.test(record.code)
|
|
246
|
+
) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
return [
|
|
250
|
+
Object.fromEntries(
|
|
251
|
+
Object.entries(record).filter(
|
|
252
|
+
([, entry]) =>
|
|
253
|
+
typeof entry === "string" && SAFE_DESIGN_DETAIL_VALUE.test(entry),
|
|
254
|
+
),
|
|
255
|
+
) as Record<string, string>,
|
|
256
|
+
];
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
operation: failure.operation,
|
|
260
|
+
code: failure.code,
|
|
261
|
+
diagnostics,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderDesignFailure(
|
|
266
|
+
failure: ReturnType<typeof safeDesignFailure> & object,
|
|
267
|
+
expanded: boolean,
|
|
268
|
+
theme?: ActivityTheme,
|
|
269
|
+
): Component {
|
|
270
|
+
const title = styled(
|
|
271
|
+
theme,
|
|
272
|
+
"error",
|
|
273
|
+
`${dispatchLabel("design")} ${failure.operation} failed: ${failure.code}`,
|
|
274
|
+
);
|
|
275
|
+
if (!expanded || failure.diagnostics.length === 0) {
|
|
276
|
+
const suffix =
|
|
277
|
+
failure.diagnostics.length > 0
|
|
278
|
+
? styled(
|
|
279
|
+
theme,
|
|
280
|
+
"muted",
|
|
281
|
+
` (${failure.diagnostics.length} diagnostic${failure.diagnostics.length === 1 ? "" : "s"})`,
|
|
282
|
+
)
|
|
283
|
+
: "";
|
|
284
|
+
return new Text(`${title}${suffix}`, 0, 0);
|
|
285
|
+
}
|
|
286
|
+
const lines = failure.diagnostics.map((diagnostic) => {
|
|
287
|
+
const details = Object.entries(diagnostic)
|
|
288
|
+
.filter(([key]) => key !== "code")
|
|
289
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
290
|
+
.join(" · ");
|
|
291
|
+
return ` - ${diagnostic.code}${details ? ` · ${details}` : ""}`;
|
|
292
|
+
});
|
|
293
|
+
return new Text(
|
|
294
|
+
[title, ...lines.map((entry) => styled(theme, "muted", entry))].join("\n"),
|
|
295
|
+
0,
|
|
296
|
+
0,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
224
300
|
export function formatElapsed(milliseconds: number): string {
|
|
225
301
|
const value = Math.max(
|
|
226
302
|
0,
|
|
@@ -470,6 +546,7 @@ function workflowStateOf(
|
|
|
470
546
|
}
|
|
471
547
|
const state = result.state;
|
|
472
548
|
switch (state) {
|
|
549
|
+
case "not-started":
|
|
473
550
|
case "created":
|
|
474
551
|
case "ready":
|
|
475
552
|
case "queued":
|
|
@@ -659,7 +736,6 @@ export function projectWorkflowActivity(
|
|
|
659
736
|
? "completion-state-inconsistent"
|
|
660
737
|
: workflowCode(payload);
|
|
661
738
|
return {
|
|
662
|
-
version: 1,
|
|
663
739
|
kind: "activityDisplay",
|
|
664
740
|
requestId: sanitizeDisplayText(
|
|
665
741
|
runId ?? taskId ?? change ?? command.operationId ?? "pending",
|
|
@@ -792,7 +868,6 @@ function displayFromEvent(
|
|
|
792
868
|
): ActivityDisplay {
|
|
793
869
|
const reason = event.failureReason;
|
|
794
870
|
return {
|
|
795
|
-
version: 1,
|
|
796
871
|
kind: "activityDisplay",
|
|
797
872
|
requestId: entry.requestId,
|
|
798
873
|
role: entry.role,
|
|
@@ -989,7 +1064,6 @@ export class ActivityController {
|
|
|
989
1064
|
const entry = this.entries.get(toolCallId);
|
|
990
1065
|
if (!entry) return;
|
|
991
1066
|
const display: ActivityDisplay = {
|
|
992
|
-
version: 1,
|
|
993
1067
|
kind: "activityDisplay",
|
|
994
1068
|
requestId: entry.requestId,
|
|
995
1069
|
role: entry.role,
|
|
@@ -1230,7 +1304,6 @@ export function renderActivityResult(
|
|
|
1230
1304
|
if (failedRun) {
|
|
1231
1305
|
const snapshot = identityFromDispatchArgs(asRecord(context?.args)?.request);
|
|
1232
1306
|
const display: ActivityDisplay = {
|
|
1233
|
-
version: 1,
|
|
1234
1307
|
kind: "activityDisplay",
|
|
1235
1308
|
requestId: isActivityDisplay(activity)
|
|
1236
1309
|
? activity.requestId
|
|
@@ -1250,6 +1323,10 @@ export function renderActivityResult(
|
|
|
1250
1323
|
return new ActivityInlineComponent(display, theme);
|
|
1251
1324
|
}
|
|
1252
1325
|
if (failed) {
|
|
1326
|
+
const designFailure = safeDesignFailure(details?.designFailure);
|
|
1327
|
+
if (args?.action === "design" && designFailure) {
|
|
1328
|
+
return renderDesignFailure(designFailure, options.expanded, theme);
|
|
1329
|
+
}
|
|
1253
1330
|
return new Text(
|
|
1254
1331
|
`${dispatchLabel(args?.action ?? args?.command)} failed`,
|
|
1255
1332
|
0,
|
|
@@ -1275,7 +1352,6 @@ function isActivityDisplay(value: unknown): value is ActivityDisplay {
|
|
|
1275
1352
|
const record = asRecord(value);
|
|
1276
1353
|
return (
|
|
1277
1354
|
record?.kind === "activityDisplay" &&
|
|
1278
|
-
record.version === 1 &&
|
|
1279
1355
|
typeof record.requestId === "string" &&
|
|
1280
1356
|
typeof record.role === "string" &&
|
|
1281
1357
|
typeof record.phase === "string" &&
|
package/src/submit-tool.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type EvidenceResult,
|
|
12
12
|
isAgentsPath,
|
|
13
13
|
isValidRelativePath,
|
|
14
|
+
LIMITS,
|
|
14
15
|
type SubmitFinalCategory,
|
|
15
16
|
type SubmitSchemaState,
|
|
16
17
|
validateDiffResult,
|
|
@@ -427,7 +428,7 @@ export function classifyCandidateContextRequest(
|
|
|
427
428
|
classification = { kind: "paused", code: "agents-context-needed" };
|
|
428
429
|
} else {
|
|
429
430
|
classification = {
|
|
430
|
-
kind: "paused",
|
|
431
|
+
kind: request.code === "approved-context-needed" ? "retryable" : "paused",
|
|
431
432
|
code:
|
|
432
433
|
request.code === "task-split-needed"
|
|
433
434
|
? "task-split-needed"
|
|
@@ -477,10 +478,16 @@ export function createCandidateArtifactTool(
|
|
|
477
478
|
executionMode: "sequential",
|
|
478
479
|
parameters,
|
|
479
480
|
async execute(_toolCallId, params) {
|
|
481
|
+
attempts++;
|
|
480
482
|
if (result !== undefined) {
|
|
483
|
+
schema = "invalid";
|
|
484
|
+
failure = {
|
|
485
|
+
kind: "artifact",
|
|
486
|
+
code: "invalid-structural-result",
|
|
487
|
+
stage: "structural-submit",
|
|
488
|
+
};
|
|
481
489
|
throw new Error("candidate terminal result already submitted");
|
|
482
490
|
}
|
|
483
|
-
attempts++;
|
|
484
491
|
if (attempts > 2) {
|
|
485
492
|
schema = "invalid";
|
|
486
493
|
failure = {
|
|
@@ -654,6 +661,170 @@ export function createCandidateArtifactTool(
|
|
|
654
661
|
};
|
|
655
662
|
}
|
|
656
663
|
|
|
664
|
+
export function createStructuredPatchTool(input: {
|
|
665
|
+
requestId: string;
|
|
666
|
+
taskId?: string;
|
|
667
|
+
role: string;
|
|
668
|
+
phase: string;
|
|
669
|
+
workspaceRoot: string;
|
|
670
|
+
writePaths: string[];
|
|
671
|
+
deletePaths: string[];
|
|
672
|
+
}) {
|
|
673
|
+
let submitted: DiffResult | CandidateContextRequest | undefined;
|
|
674
|
+
let failure: CandidateFailure | undefined;
|
|
675
|
+
let attempts = 0;
|
|
676
|
+
let schema: SubmitSchemaState = "not-submitted";
|
|
677
|
+
let identity: IdentityOutcome = {
|
|
678
|
+
request: true,
|
|
679
|
+
role: true,
|
|
680
|
+
task: true,
|
|
681
|
+
phase: true,
|
|
682
|
+
};
|
|
683
|
+
const candidateId = input.requestId;
|
|
684
|
+
const tool = defineTool({
|
|
685
|
+
name: "abel_submit_result",
|
|
686
|
+
label: "Submit Abel Structured Patch",
|
|
687
|
+
description:
|
|
688
|
+
"Submit one complete structured patch. Unified-diff headers and hunks are generated by the trusted control plane.",
|
|
689
|
+
executionMode: "sequential",
|
|
690
|
+
parameters: candidateArtifactSchema(candidateId),
|
|
691
|
+
async execute(_toolCallId, params) {
|
|
692
|
+
attempts += 1;
|
|
693
|
+
if (submitted !== undefined) {
|
|
694
|
+
schema = "invalid";
|
|
695
|
+
failure = {
|
|
696
|
+
kind: "artifact",
|
|
697
|
+
code: "invalid-structural-result",
|
|
698
|
+
stage: "structural-submit",
|
|
699
|
+
};
|
|
700
|
+
throw new Error("structured patch terminal result already submitted");
|
|
701
|
+
}
|
|
702
|
+
if (attempts > 2) {
|
|
703
|
+
schema = "invalid";
|
|
704
|
+
failure = {
|
|
705
|
+
kind: "artifact",
|
|
706
|
+
code: "invalid-structural-result",
|
|
707
|
+
stage: "structural-submit",
|
|
708
|
+
};
|
|
709
|
+
throw new Error("structural submission correction limit exceeded");
|
|
710
|
+
}
|
|
711
|
+
identity = {
|
|
712
|
+
request: true,
|
|
713
|
+
role: true,
|
|
714
|
+
task: true,
|
|
715
|
+
phase: true,
|
|
716
|
+
};
|
|
717
|
+
failure = undefined;
|
|
718
|
+
const value = params as unknown as Record<string, unknown>;
|
|
719
|
+
if (value.candidateId !== candidateId) {
|
|
720
|
+
identity.request = false;
|
|
721
|
+
schema = "invalid";
|
|
722
|
+
failure = {
|
|
723
|
+
kind: "artifact",
|
|
724
|
+
code: "structural-identity-mismatch",
|
|
725
|
+
stage: "structural-submit",
|
|
726
|
+
};
|
|
727
|
+
throw new Error("diagnosis candidate identity mismatch");
|
|
728
|
+
}
|
|
729
|
+
if (value.kind === "context-request") {
|
|
730
|
+
let refs: CandidateContextRef[];
|
|
731
|
+
try {
|
|
732
|
+
refs = normalizeCandidateContextRefs(value.refs);
|
|
733
|
+
} catch (error) {
|
|
734
|
+
schema = "invalid";
|
|
735
|
+
failure = {
|
|
736
|
+
kind: "artifact",
|
|
737
|
+
code: "invalid-structural-result",
|
|
738
|
+
stage: "structural-submit",
|
|
739
|
+
};
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
submitted = {
|
|
743
|
+
kind: "context-request",
|
|
744
|
+
candidateId,
|
|
745
|
+
code: value.code as CandidateContextRequest["code"],
|
|
746
|
+
refs,
|
|
747
|
+
};
|
|
748
|
+
schema = "valid";
|
|
749
|
+
return {
|
|
750
|
+
content: [
|
|
751
|
+
{ type: "text" as const, text: "Context request accepted." },
|
|
752
|
+
],
|
|
753
|
+
details: { accepted: true },
|
|
754
|
+
terminate: true,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
if (value.kind !== "candidate-patch") {
|
|
758
|
+
schema = "invalid";
|
|
759
|
+
failure = {
|
|
760
|
+
kind: "artifact",
|
|
761
|
+
code: "invalid-structural-result",
|
|
762
|
+
stage: "structural-submit",
|
|
763
|
+
};
|
|
764
|
+
throw new Error("diagnosis result must be a patch or context request");
|
|
765
|
+
}
|
|
766
|
+
let compiled: ReturnType<typeof compileCandidatePatch>;
|
|
767
|
+
try {
|
|
768
|
+
compiled = compileCandidatePatch({
|
|
769
|
+
root: input.workspaceRoot,
|
|
770
|
+
operations: value.operations as CandidatePatchOperation[],
|
|
771
|
+
writePaths: input.writePaths,
|
|
772
|
+
deletePaths: input.deletePaths,
|
|
773
|
+
maxBytes: LIMITS.maxCompleteResultBytes,
|
|
774
|
+
});
|
|
775
|
+
} catch (error) {
|
|
776
|
+
schema = "invalid";
|
|
777
|
+
failure = {
|
|
778
|
+
kind: "artifact",
|
|
779
|
+
code: "invalid-diff",
|
|
780
|
+
stage: "candidate-diff",
|
|
781
|
+
};
|
|
782
|
+
throw error;
|
|
783
|
+
}
|
|
784
|
+
const result: DiffResult = {
|
|
785
|
+
id: input.requestId,
|
|
786
|
+
role: input.role,
|
|
787
|
+
kind: "diff",
|
|
788
|
+
taskId: input.taskId ?? input.requestId,
|
|
789
|
+
phase: input.phase,
|
|
790
|
+
summary:
|
|
791
|
+
"Trusted control plane generated this diff from structured operations.",
|
|
792
|
+
diff: compiled,
|
|
793
|
+
expectedVerification: "parent-owned verification contract",
|
|
794
|
+
risks: [],
|
|
795
|
+
contractCompliant: true,
|
|
796
|
+
};
|
|
797
|
+
const validation = validateDiffResult(result);
|
|
798
|
+
if (!validation.ok) {
|
|
799
|
+
schema = "invalid";
|
|
800
|
+
failure = validation.failure ?? {
|
|
801
|
+
kind: "artifact",
|
|
802
|
+
code: "invalid-structural-result",
|
|
803
|
+
stage: "structural-submit",
|
|
804
|
+
};
|
|
805
|
+
throw new Error(validation.reason ?? "structured patch is invalid");
|
|
806
|
+
}
|
|
807
|
+
submitted = result;
|
|
808
|
+
schema = "valid";
|
|
809
|
+
return {
|
|
810
|
+
content: [{ type: "text" as const, text: "Abel patch accepted." }],
|
|
811
|
+
details: { accepted: true },
|
|
812
|
+
terminate: true,
|
|
813
|
+
};
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
return {
|
|
817
|
+
tool,
|
|
818
|
+
getResult: () =>
|
|
819
|
+
submitted === undefined ? undefined : structuredClone(submitted),
|
|
820
|
+
getAttempts: () => attempts,
|
|
821
|
+
getSchema: () => schema,
|
|
822
|
+
getIdentity: () => ({ ...identity }),
|
|
823
|
+
getFailure: () =>
|
|
824
|
+
failure === undefined ? undefined : structuredClone(failure),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
657
828
|
export type FinalCategory = SubmitFinalCategory;
|
|
658
829
|
|
|
659
830
|
export interface IdentityOutcome {
|
|
@@ -702,6 +873,15 @@ export function createSubmitTool(input: {
|
|
|
702
873
|
: diffSchema,
|
|
703
874
|
async execute(_toolCallId, params) {
|
|
704
875
|
attempts++;
|
|
876
|
+
if (submitted !== undefined) {
|
|
877
|
+
schema = "invalid";
|
|
878
|
+
failure = {
|
|
879
|
+
kind: "artifact",
|
|
880
|
+
code: "invalid-structural-result",
|
|
881
|
+
stage: "structural-submit",
|
|
882
|
+
};
|
|
883
|
+
throw new Error("duplicate structural submission");
|
|
884
|
+
}
|
|
705
885
|
if (attempts > 2) {
|
|
706
886
|
schema = "invalid";
|
|
707
887
|
failure = {
|
|
@@ -768,14 +948,6 @@ export function createSubmitTool(input: {
|
|
|
768
948
|
validation.reason ?? "submitted result identity does not match",
|
|
769
949
|
);
|
|
770
950
|
}
|
|
771
|
-
if (submitted) {
|
|
772
|
-
failure = {
|
|
773
|
-
kind: "artifact",
|
|
774
|
-
code: "invalid-structural-result",
|
|
775
|
-
stage: "structural-submit",
|
|
776
|
-
};
|
|
777
|
-
throw new Error("duplicate structural submission");
|
|
778
|
-
}
|
|
779
951
|
submitted = structuredClone(value) as unknown as
|
|
780
952
|
| EvidenceResult
|
|
781
953
|
| DiffResult;
|
package/src/task-ledger.ts
CHANGED
|
@@ -230,7 +230,7 @@ function hashEvent(
|
|
|
230
230
|
eventJson: string,
|
|
231
231
|
): string {
|
|
232
232
|
return createHash("sha256")
|
|
233
|
-
.update("cadence-task-ledger-event
|
|
233
|
+
.update("cadence-task-ledger-event\0")
|
|
234
234
|
.update(priorHash)
|
|
235
235
|
.update("\0")
|
|
236
236
|
.update(String(ordinal))
|
|
@@ -1354,7 +1354,6 @@ export class TaskLedger {
|
|
|
1354
1354
|
return { ordinal: row.ordinal, ...fact };
|
|
1355
1355
|
});
|
|
1356
1356
|
const projection = {
|
|
1357
|
-
version: 2,
|
|
1358
1357
|
runId: input.runId,
|
|
1359
1358
|
taskId: input.taskId,
|
|
1360
1359
|
deliveryRevision: task.delivery_revision,
|
package/src/worker-broker.ts
CHANGED
|
@@ -7,12 +7,12 @@ import {
|
|
|
7
7
|
} from "./route-policy.ts";
|
|
8
8
|
|
|
9
9
|
export const ROUTE_ATTEMPT_BOUNDS = Object.freeze({
|
|
10
|
-
connectMs:
|
|
11
|
-
firstResponseMs:
|
|
12
|
-
idleMs: 60_000,
|
|
13
|
-
totalMs:
|
|
10
|
+
connectMs: 10_000,
|
|
11
|
+
firstResponseMs: 90_000,
|
|
12
|
+
idleMs: 3 * 60_000,
|
|
13
|
+
totalMs: 20 * 60_000,
|
|
14
14
|
cooldownMs: 30_000,
|
|
15
|
-
attemptsPerRoute:
|
|
15
|
+
attemptsPerRoute: 2,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
export type RouteHealthState = "healthy" | "open" | "half-open";
|
|
@@ -61,6 +61,7 @@ export interface RunWorkerAttemptInput<T> {
|
|
|
61
61
|
requirements?: RouteRequirements;
|
|
62
62
|
signal?: AbortSignal;
|
|
63
63
|
onActivity?: (event: BrokerActivityUpdate) => void;
|
|
64
|
+
classifyResult?: (value: T) => string | undefined;
|
|
64
65
|
execute(context: RouteAttemptContext): Promise<T>;
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -218,6 +219,16 @@ export class WorkerBroker {
|
|
|
218
219
|
this.#setRouteHealth(routeId, { state: "healthy" });
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
#releaseHalfOpenProbeForRetry(routeId: string, code: string): void {
|
|
223
|
+
const health = this.#routeHealth(routeId);
|
|
224
|
+
if (health.state !== "half-open" || !health.probeInFlight) return;
|
|
225
|
+
this.#setRouteHealth(routeId, {
|
|
226
|
+
state: "open",
|
|
227
|
+
retryAt: this.#now(),
|
|
228
|
+
lastCode: code,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
221
232
|
select(input: {
|
|
222
233
|
role: WorkerRole;
|
|
223
234
|
dialects?: RouteDialect[];
|
|
@@ -372,6 +383,7 @@ export class WorkerBroker {
|
|
|
372
383
|
requirements?: RouteRequirements;
|
|
373
384
|
signal?: AbortSignal;
|
|
374
385
|
onActivity?: (event: BrokerActivityUpdate) => void;
|
|
386
|
+
classifyResult?: (value: T) => string | undefined;
|
|
375
387
|
execute: (context: RouteAttemptContext) => Promise<T>;
|
|
376
388
|
}): Promise<
|
|
377
389
|
| { ok: true; routeId: string; value: T; attempts: BrokerAttemptEvidence[] }
|
|
@@ -397,12 +409,20 @@ export class WorkerBroker {
|
|
|
397
409
|
}
|
|
398
410
|
const attempted = new Set<string>();
|
|
399
411
|
const evidence: BrokerAttemptEvidence[] = [];
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
412
|
+
const capableRouteCount = this.#policy.roles[input.role].filter(
|
|
413
|
+
(routeId) => {
|
|
414
|
+
const route = this.#policy.routes[routeId];
|
|
415
|
+
return route
|
|
416
|
+
? capable(route, input.role, input.requirements ?? {})
|
|
417
|
+
: false;
|
|
418
|
+
},
|
|
419
|
+
).length;
|
|
420
|
+
const maxAttempts = Math.max(
|
|
421
|
+
1,
|
|
422
|
+
capableRouteCount > 1
|
|
423
|
+
? capableRouteCount
|
|
424
|
+
: ROUTE_ATTEMPT_BOUNDS.attemptsPerRoute,
|
|
425
|
+
);
|
|
406
426
|
for (;;) {
|
|
407
427
|
const selection = this.select({
|
|
408
428
|
role: input.role,
|
|
@@ -427,6 +447,9 @@ export class WorkerBroker {
|
|
|
427
447
|
probeExpiresAt: this.#now() + ROUTE_ATTEMPT_BOUNDS.totalMs,
|
|
428
448
|
});
|
|
429
449
|
}
|
|
450
|
+
const routeAttempt = evidence.filter(
|
|
451
|
+
(item) => item.routeId === routeId,
|
|
452
|
+
).length;
|
|
430
453
|
const attempt = evidence.length + 1;
|
|
431
454
|
emitBrokerActivity(input.onActivity, {
|
|
432
455
|
state: "connecting",
|
|
@@ -450,6 +473,31 @@ export class WorkerBroker {
|
|
|
450
473
|
: "worker-progress",
|
|
451
474
|
}),
|
|
452
475
|
);
|
|
476
|
+
const semanticCode = input.classifyResult?.(value);
|
|
477
|
+
if (semanticCode) {
|
|
478
|
+
evidence.push({ routeId, code: semanticCode });
|
|
479
|
+
const routeHasRetry =
|
|
480
|
+
capableRouteCount === 1 &&
|
|
481
|
+
routeAttempt + 1 < ROUTE_ATTEMPT_BOUNDS.attemptsPerRoute;
|
|
482
|
+
if (routeHasRetry) {
|
|
483
|
+
this.#releaseHalfOpenProbeForRetry(routeId, semanticCode);
|
|
484
|
+
attempted.delete(routeId);
|
|
485
|
+
} else {
|
|
486
|
+
this.markFailure(routeId, semanticCode);
|
|
487
|
+
}
|
|
488
|
+
if (attempt < maxAttempts) {
|
|
489
|
+
emitBrokerActivity(input.onActivity, {
|
|
490
|
+
state: "retrying",
|
|
491
|
+
attempt,
|
|
492
|
+
maxAttempts,
|
|
493
|
+
code: semanticCode,
|
|
494
|
+
wait: "bounded-policy",
|
|
495
|
+
});
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (routeHasRetry) this.markFailure(routeId, semanticCode);
|
|
499
|
+
return { ok: true, routeId, value, attempts: evidence };
|
|
500
|
+
}
|
|
453
501
|
this.markSuccess(routeId);
|
|
454
502
|
return { ok: true, routeId, value, attempts: evidence };
|
|
455
503
|
} catch (error) {
|
|
@@ -466,8 +514,16 @@ export class WorkerBroker {
|
|
|
466
514
|
}
|
|
467
515
|
return { ok: false, state: "cancelled", code, attempts: evidence };
|
|
468
516
|
}
|
|
469
|
-
|
|
470
|
-
|
|
517
|
+
const routeHasRetry =
|
|
518
|
+
capableRouteCount === 1 &&
|
|
519
|
+
routeAttempt + 1 < ROUTE_ATTEMPT_BOUNDS.attemptsPerRoute;
|
|
520
|
+
if (routeHasRetry) {
|
|
521
|
+
this.#releaseHalfOpenProbeForRetry(routeId, code);
|
|
522
|
+
attempted.delete(routeId);
|
|
523
|
+
} else {
|
|
524
|
+
this.markFailure(routeId, code);
|
|
525
|
+
}
|
|
526
|
+
if (attempt < maxAttempts) {
|
|
471
527
|
emitBrokerActivity(input.onActivity, {
|
|
472
528
|
state: "retrying",
|
|
473
529
|
attempt,
|
|
@@ -475,6 +531,8 @@ export class WorkerBroker {
|
|
|
475
531
|
code,
|
|
476
532
|
wait: "bounded-policy",
|
|
477
533
|
});
|
|
534
|
+
} else if (routeHasRetry) {
|
|
535
|
+
this.markFailure(routeId, code);
|
|
478
536
|
}
|
|
479
537
|
}
|
|
480
538
|
}
|
|
@@ -615,6 +673,7 @@ export class RunWorkerBroker {
|
|
|
615
673
|
...(input.requirements ? { requirements: input.requirements } : {}),
|
|
616
674
|
...(input.signal ? { signal: input.signal } : {}),
|
|
617
675
|
...(input.onActivity ? { onActivity: input.onActivity } : {}),
|
|
676
|
+
...(input.classifyResult ? { classifyResult: input.classifyResult } : {}),
|
|
618
677
|
execute: input.execute,
|
|
619
678
|
});
|
|
620
679
|
if (result.ok) {
|