harnery 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +4 -0
- package/dist/commands/supervisor.d.ts +4 -0
- package/dist/commands/supervisor.d.ts.map +1 -0
- package/dist/commands/supervisor.js +238 -0
- package/dist/commands/work.d.ts +4 -0
- package/dist/commands/work.d.ts.map +1 -0
- package/dist/commands/work.js +281 -0
- package/dist/core/supervisor/index.d.ts +3 -0
- package/dist/core/supervisor/index.d.ts.map +1 -0
- package/dist/core/supervisor/index.js +2 -0
- package/dist/core/supervisor/read.d.ts +2 -0
- package/dist/core/supervisor/read.d.ts.map +1 -0
- package/dist/core/supervisor/read.js +1 -0
- package/dist/core/supervisor/runner.d.ts +34 -0
- package/dist/core/supervisor/runner.d.ts.map +1 -0
- package/dist/core/supervisor/runner.js +198 -0
- package/dist/core/supervisor/state.d.ts +69 -0
- package/dist/core/supervisor/state.d.ts.map +1 -0
- package/dist/core/supervisor/state.js +413 -0
- package/dist/core/work/index.d.ts +3 -0
- package/dist/core/work/index.d.ts.map +1 -0
- package/dist/core/work/index.js +2 -0
- package/dist/core/work/read.d.ts +2 -0
- package/dist/core/work/read.d.ts.map +1 -0
- package/dist/core/work/read.js +1 -0
- package/dist/core/work/runner.d.ts +10 -0
- package/dist/core/work/runner.d.ts.map +1 -0
- package/dist/core/work/runner.js +63 -0
- package/dist/core/work/state.d.ts +109 -0
- package/dist/core/work/state.d.ts.map +1 -0
- package/dist/core/work/state.js +698 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +41 -9
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +1 -0
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -0
- package/dist/core/workflow/run-state.d.ts +3 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +14 -0
- package/dist/core/workflow/specialists.d.ts +7 -0
- package/dist/core/workflow/specialists.d.ts.map +1 -0
- package/dist/core/workflow/specialists.js +78 -0
- package/dist/core/workflow/types.d.ts +24 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/package.json +21 -1
- package/src/commander.ts +4 -0
- package/src/commands/supervisor.ts +315 -0
- package/src/commands/work.ts +367 -0
- package/src/core/supervisor/index.ts +24 -0
- package/src/core/supervisor/read.ts +14 -0
- package/src/core/supervisor/runner.ts +278 -0
- package/src/core/supervisor/state.ts +552 -0
- package/src/core/work/index.ts +24 -0
- package/src/core/work/read.ts +15 -0
- package/src/core/work/runner.ts +81 -0
- package/src/core/work/state.ts +900 -0
- package/src/core/workflow/engine.ts +42 -9
- package/src/core/workflow/index.ts +1 -0
- package/src/core/workflow/proof.ts +3 -0
- package/src/core/workflow/run-state.ts +20 -0
- package/src/core/workflow/specialists.ts +97 -0
- package/src/core/workflow/types.ts +25 -0
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
policyDigest,
|
|
36
36
|
summarizePolicyRequest,
|
|
37
37
|
} from "../policy/index.ts";
|
|
38
|
-
import { createWorkflowApproval } from "./approvals.ts";
|
|
38
|
+
import { assertWorkflowRunId, createWorkflowApproval } from "./approvals.ts";
|
|
39
39
|
import { type BillingProbe, probeBilling } from "./billing.ts";
|
|
40
40
|
import {
|
|
41
41
|
buildWorkflowProof,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
workflowScriptDigest,
|
|
52
52
|
writeWorkflowRunManifest,
|
|
53
53
|
} from "./run-state.ts";
|
|
54
|
+
import { normalizeWorkflowSpecialists, resolveSpecialistAssignment } from "./specialists.ts";
|
|
54
55
|
import type {
|
|
55
56
|
AgentOpts,
|
|
56
57
|
EngineOpts,
|
|
@@ -101,12 +102,26 @@ export class WorkflowParkedError extends Error {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise<RunReport> {
|
|
105
|
+
if (opts.runId && opts.resumeRunId) {
|
|
106
|
+
throw new Error("runId and resumeRunId are mutually exclusive");
|
|
107
|
+
}
|
|
108
|
+
if (opts.runId) assertWorkflowRunId(opts.runId);
|
|
109
|
+
if (opts.workItemId && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(opts.workItemId)) {
|
|
110
|
+
throw new Error(`invalid work item id ${JSON.stringify(opts.workItemId)}`);
|
|
111
|
+
}
|
|
104
112
|
if (opts.resumeRunId && opts.resumeFrom) {
|
|
105
113
|
throw new Error("resumeRunId and resumeFrom are mutually exclusive");
|
|
106
114
|
}
|
|
107
115
|
const resumeState = opts.resumeRunId
|
|
108
116
|
? assertWorkflowRunResumable(opts.coordRoot, opts.resumeRunId)
|
|
109
117
|
: undefined;
|
|
118
|
+
if (
|
|
119
|
+
resumeState !== undefined &&
|
|
120
|
+
opts.workItemId !== undefined &&
|
|
121
|
+
resumeState.manifest.work_item_id !== opts.workItemId
|
|
122
|
+
) {
|
|
123
|
+
throw new Error(`workflow run ${opts.resumeRunId} belongs to a different work item`);
|
|
124
|
+
}
|
|
110
125
|
const absScript = isAbsolute(scriptPath) ? scriptPath : resolve(process.cwd(), scriptPath);
|
|
111
126
|
if (resumeState) {
|
|
112
127
|
if (resolve(resumeState.manifest.script.path) !== resolve(absScript)) {
|
|
@@ -145,6 +160,7 @@ async function executeWorkflow(
|
|
|
145
160
|
|
|
146
161
|
const runId =
|
|
147
162
|
opts.resumeRunId ??
|
|
163
|
+
opts.runId ??
|
|
148
164
|
`wf-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomBytes(3).toString("hex")}`;
|
|
149
165
|
const runDir = join(opts.coordRoot, ".harnery", "workflows", runId);
|
|
150
166
|
mkdirSync(runDir, { recursive: true });
|
|
@@ -160,6 +176,8 @@ async function executeWorkflow(
|
|
|
160
176
|
const log = opts.onLog ?? ((line: string) => process.stderr.write(`${line}\n`));
|
|
161
177
|
const defaultHarness: HarnessName =
|
|
162
178
|
frozen?.default_harness ?? opts.defaultHarness ?? "claude-code";
|
|
179
|
+
const specialists =
|
|
180
|
+
frozen?.specialists ?? (resumeState ? {} : normalizeWorkflowSpecialists(opts.specialists));
|
|
163
181
|
const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
|
|
164
182
|
const networkAccess = frozen?.network_access ?? opts.networkAccess ?? "unknown";
|
|
165
183
|
const policy =
|
|
@@ -178,6 +196,7 @@ async function executeWorkflow(
|
|
|
178
196
|
manifest: {
|
|
179
197
|
schema_version: 1,
|
|
180
198
|
run_id: runId,
|
|
199
|
+
work_item_id: opts.workItemId,
|
|
181
200
|
name,
|
|
182
201
|
started_at: startedAt,
|
|
183
202
|
script: { path: absScript, sha256: workflowScriptDigest(absScript) },
|
|
@@ -194,6 +213,7 @@ async function executeWorkflow(
|
|
|
194
213
|
isolation,
|
|
195
214
|
network_access: networkAccess,
|
|
196
215
|
policy: policy ? (policy as NormalizedPolicy) : undefined,
|
|
216
|
+
specialists,
|
|
197
217
|
},
|
|
198
218
|
},
|
|
199
219
|
});
|
|
@@ -399,7 +419,10 @@ async function executeWorkflow(
|
|
|
399
419
|
return decision;
|
|
400
420
|
};
|
|
401
421
|
|
|
402
|
-
const agent = async (prompt: string,
|
|
422
|
+
const agent = async (prompt: string, requestedOpts: AgentOpts = {}): Promise<unknown> => {
|
|
423
|
+
const assignment = resolveSpecialistAssignment(specialists, prompt, requestedOpts);
|
|
424
|
+
const agentOpts = assignment.opts;
|
|
425
|
+
const assignmentPrompt = assignment.prompt;
|
|
403
426
|
let reservedForDispatch = 0;
|
|
404
427
|
let spawnCountClaimed = false;
|
|
405
428
|
const harness = agentOpts.harness ?? defaultHarness;
|
|
@@ -417,6 +440,7 @@ async function executeWorkflow(
|
|
|
417
440
|
id,
|
|
418
441
|
label: proofLabel,
|
|
419
442
|
stage: currentStage || undefined,
|
|
443
|
+
specialist: agentOpts.specialist,
|
|
420
444
|
harness,
|
|
421
445
|
model: agentOpts.model,
|
|
422
446
|
status: "failed",
|
|
@@ -427,7 +451,7 @@ async function executeWorkflow(
|
|
|
427
451
|
|
|
428
452
|
// Call identity for resume: same stage + harness + model + effort + turns + schema
|
|
429
453
|
// + ORIGINAL prompt → same key. Retry-mutated prompts never enter the key.
|
|
430
|
-
const key = agentCallKey(currentStage, harness, agentOpts,
|
|
454
|
+
const key = agentCallKey(currentStage, harness, agentOpts, assignmentPrompt);
|
|
431
455
|
const cached = resumeCache.get(key);
|
|
432
456
|
if (cached) {
|
|
433
457
|
// Exact-run replay skips dispatch authorization because no dispatch
|
|
@@ -442,6 +466,7 @@ async function executeWorkflow(
|
|
|
442
466
|
label,
|
|
443
467
|
key,
|
|
444
468
|
harness,
|
|
469
|
+
specialist: agentOpts.specialist ?? null,
|
|
445
470
|
model: agentOpts.model ?? null,
|
|
446
471
|
kind: cached.kind,
|
|
447
472
|
});
|
|
@@ -462,7 +487,7 @@ async function executeWorkflow(
|
|
|
462
487
|
max_attempts: maxAttempts,
|
|
463
488
|
max_turns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
464
489
|
timeout_ms: agentOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
465
|
-
prompt_bytes: Buffer.byteLength(
|
|
490
|
+
prompt_bytes: Buffer.byteLength(assignmentPrompt),
|
|
466
491
|
isolation,
|
|
467
492
|
network_access: networkAccess,
|
|
468
493
|
current_cost_usd: round4(costUsd + reservedCostUsd),
|
|
@@ -578,12 +603,13 @@ async function executeWorkflow(
|
|
|
578
603
|
label,
|
|
579
604
|
key,
|
|
580
605
|
harness,
|
|
606
|
+
specialist: agentOpts.specialist ?? null,
|
|
581
607
|
model: agentOpts.model ?? null,
|
|
582
608
|
effort: agentOpts.effort ?? null,
|
|
583
609
|
});
|
|
584
610
|
log(`[${name}] ${currentStage || "(no stage)"} → ${id} [${harness}] ${label}`);
|
|
585
611
|
|
|
586
|
-
let attemptPrompt =
|
|
612
|
+
let attemptPrompt = assignmentPrompt;
|
|
587
613
|
let last: SpawnResult | null = null;
|
|
588
614
|
let agentCostUsd = 0;
|
|
589
615
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -655,7 +681,7 @@ async function executeWorkflow(
|
|
|
655
681
|
// Feed the validation failure back verbatim — the retry prompt carries
|
|
656
682
|
// exactly what was wrong, which is what makes bounded retry converge.
|
|
657
683
|
attemptPrompt =
|
|
658
|
-
`${
|
|
684
|
+
`${assignmentPrompt}\n\nYour previous reply failed validation:\n` +
|
|
659
685
|
`${problems.map((p) => ` - ${p}`).join("\n")}\n` +
|
|
660
686
|
`Reply with ONLY the corrected JSON object. No prose, no code fences.`;
|
|
661
687
|
}
|
|
@@ -739,11 +765,13 @@ async function executeWorkflow(
|
|
|
739
765
|
} else {
|
|
740
766
|
journal("run.start", {
|
|
741
767
|
name,
|
|
768
|
+
work_item_id: opts.workItemId ?? null,
|
|
742
769
|
script: absScript,
|
|
743
770
|
objective: meta.objective ?? null,
|
|
744
771
|
acceptance: meta.acceptance,
|
|
745
772
|
max_agents: maxAgents,
|
|
746
773
|
concurrency,
|
|
774
|
+
specialists: Object.keys(specialists),
|
|
747
775
|
policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
|
|
748
776
|
isolation,
|
|
749
777
|
network_access: networkAccess,
|
|
@@ -763,6 +791,7 @@ async function executeWorkflow(
|
|
|
763
791
|
try {
|
|
764
792
|
proof = buildWorkflowProof({
|
|
765
793
|
runId,
|
|
794
|
+
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
766
795
|
meta,
|
|
767
796
|
status: "succeeded",
|
|
768
797
|
startedAt,
|
|
@@ -795,6 +824,7 @@ async function executeWorkflow(
|
|
|
795
824
|
}
|
|
796
825
|
const report: RunReport = {
|
|
797
826
|
runId,
|
|
827
|
+
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
798
828
|
name,
|
|
799
829
|
result,
|
|
800
830
|
agentsSpawned,
|
|
@@ -827,6 +857,7 @@ async function executeWorkflow(
|
|
|
827
857
|
try {
|
|
828
858
|
const proof = buildWorkflowProof({
|
|
829
859
|
runId,
|
|
860
|
+
workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
|
|
830
861
|
meta,
|
|
831
862
|
status: "failed",
|
|
832
863
|
startedAt,
|
|
@@ -869,15 +900,17 @@ function agentCallKey(
|
|
|
869
900
|
agentOpts: AgentOpts,
|
|
870
901
|
prompt: string,
|
|
871
902
|
): string {
|
|
872
|
-
const
|
|
903
|
+
const parts: unknown[] = [
|
|
873
904
|
stage,
|
|
874
905
|
harness,
|
|
875
906
|
agentOpts.model ?? null,
|
|
876
907
|
agentOpts.effort ?? null,
|
|
877
908
|
agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
878
909
|
agentOpts.schema ?? null,
|
|
879
|
-
|
|
880
|
-
|
|
910
|
+
];
|
|
911
|
+
if (agentOpts.specialist) parts.push(agentOpts.specialist);
|
|
912
|
+
parts.push(prompt);
|
|
913
|
+
const basis = JSON.stringify(parts);
|
|
881
914
|
return createHash("sha256").update(basis).digest("hex").slice(0, 16);
|
|
882
915
|
}
|
|
883
916
|
|
|
@@ -58,6 +58,7 @@ export interface NormalizedWorkflowMeta {
|
|
|
58
58
|
|
|
59
59
|
export interface BuildWorkflowProofInput {
|
|
60
60
|
runId: string;
|
|
61
|
+
workItemId?: string;
|
|
61
62
|
meta: NormalizedWorkflowMeta;
|
|
62
63
|
status: "succeeded" | "failed";
|
|
63
64
|
startedAt: string;
|
|
@@ -202,6 +203,7 @@ export function buildWorkflowProof(input: BuildWorkflowProofInput): WorkflowProo
|
|
|
202
203
|
const agents = input.agents.map((agent) => ({
|
|
203
204
|
...agent,
|
|
204
205
|
label: clipped(agent.label, MAX_LABEL_CHARS),
|
|
206
|
+
specialist: clippedOptional(agent.specialist, MAX_LABEL_CHARS),
|
|
205
207
|
model: clippedOptional(agent.model, MAX_LABEL_CHARS),
|
|
206
208
|
session_id: clippedOptional(agent.session_id, MAX_REF_CHARS),
|
|
207
209
|
error: clippedOptional(agent.error, MAX_SUMMARY_CHARS),
|
|
@@ -213,6 +215,7 @@ export function buildWorkflowProof(input: BuildWorkflowProofInput): WorkflowProo
|
|
|
213
215
|
schema_version: WORKFLOW_PROOF_SCHEMA_VERSION,
|
|
214
216
|
run: {
|
|
215
217
|
id: input.runId,
|
|
218
|
+
work_item_id: input.workItemId,
|
|
216
219
|
name: input.meta.name,
|
|
217
220
|
status: input.status,
|
|
218
221
|
started_at: input.startedAt,
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
policyDigest,
|
|
23
23
|
} from "../policy/index.ts";
|
|
24
24
|
import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.ts";
|
|
25
|
+
import { normalizeWorkflowSpecialists } from "./specialists.ts";
|
|
26
|
+
import type { WorkflowSpecialistProfile } from "./types.ts";
|
|
25
27
|
|
|
26
28
|
export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 1 as const;
|
|
27
29
|
|
|
@@ -31,6 +33,7 @@ const FOREIGN_LEASE_STALE_MS = 24 * 60 * 60 * 1_000;
|
|
|
31
33
|
export interface WorkflowRunManifest {
|
|
32
34
|
schema_version: typeof WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION;
|
|
33
35
|
run_id: string;
|
|
36
|
+
work_item_id?: string;
|
|
34
37
|
name: string;
|
|
35
38
|
started_at: string;
|
|
36
39
|
script: { path: string; sha256: string };
|
|
@@ -47,6 +50,7 @@ export interface WorkflowRunManifest {
|
|
|
47
50
|
isolation: PolicyIsolation;
|
|
48
51
|
network_access: PolicyNetworkAccess;
|
|
49
52
|
policy?: NormalizedPolicy;
|
|
53
|
+
specialists?: Record<string, WorkflowSpecialistProfile>;
|
|
50
54
|
};
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -97,6 +101,8 @@ export function readWorkflowRunManifest(coordRoot: string, runId: string): Workf
|
|
|
97
101
|
if (
|
|
98
102
|
manifest.schema_version !== WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION ||
|
|
99
103
|
manifest.run_id !== runId ||
|
|
104
|
+
(manifest.work_item_id !== undefined &&
|
|
105
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(manifest.work_item_id)) ||
|
|
100
106
|
typeof manifest.name !== "string" ||
|
|
101
107
|
manifest.name.length === 0 ||
|
|
102
108
|
manifest.name.length > 200 ||
|
|
@@ -250,10 +256,24 @@ function validExecution(value: WorkflowRunManifest["execution"]): boolean {
|
|
|
250
256
|
value.approval_addressee.length <= 200 &&
|
|
251
257
|
["shared", "worktree", "sandbox", "remote"].includes(value.isolation) &&
|
|
252
258
|
["enabled", "disabled", "unknown"].includes(value.network_access) &&
|
|
259
|
+
validSpecialists(value.specialists) &&
|
|
253
260
|
validFrozenPolicy(value.policy, value.cwd)
|
|
254
261
|
);
|
|
255
262
|
}
|
|
256
263
|
|
|
264
|
+
function validSpecialists(
|
|
265
|
+
specialists: Record<string, WorkflowSpecialistProfile> | undefined,
|
|
266
|
+
): boolean {
|
|
267
|
+
if (specialists === undefined) return true;
|
|
268
|
+
try {
|
|
269
|
+
return (
|
|
270
|
+
JSON.stringify(normalizeWorkflowSpecialists(specialists)) === JSON.stringify(specialists)
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
257
277
|
function validFrozenPolicy(policy: NormalizedPolicy | undefined, cwd: string): boolean {
|
|
258
278
|
if (policy === undefined) return true;
|
|
259
279
|
try {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AgentOpts, WorkflowSpecialistProfile } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const SPECIALIST_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
4
|
+
const MAX_SPECIALISTS = 32;
|
|
5
|
+
const MAX_INSTRUCTIONS = 4_000;
|
|
6
|
+
const MAX_OPTION = 200;
|
|
7
|
+
|
|
8
|
+
export function normalizeWorkflowSpecialists(
|
|
9
|
+
input: Readonly<Record<string, WorkflowSpecialistProfile>> | undefined,
|
|
10
|
+
): Record<string, WorkflowSpecialistProfile> {
|
|
11
|
+
if (input === undefined) return {};
|
|
12
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
13
|
+
throw new Error("workflow specialists must be an object keyed by role id");
|
|
14
|
+
}
|
|
15
|
+
const entries = Object.entries(input);
|
|
16
|
+
if (entries.length > MAX_SPECIALISTS) {
|
|
17
|
+
throw new Error(`workflow specialists exceed ${MAX_SPECIALISTS} roles`);
|
|
18
|
+
}
|
|
19
|
+
const normalized: Record<string, WorkflowSpecialistProfile> = {};
|
|
20
|
+
for (const [id, profile] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
21
|
+
if (!SPECIALIST_ID.test(id))
|
|
22
|
+
throw new Error(`invalid workflow specialist id ${JSON.stringify(id)}`);
|
|
23
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
|
|
24
|
+
throw new Error(`workflow specialist ${id} must be an object`);
|
|
25
|
+
}
|
|
26
|
+
normalized[id] = {
|
|
27
|
+
instructions: bounded(
|
|
28
|
+
profile.instructions,
|
|
29
|
+
`workflow specialist ${id} instructions`,
|
|
30
|
+
MAX_INSTRUCTIONS,
|
|
31
|
+
),
|
|
32
|
+
harness: optional(profile.harness, `workflow specialist ${id} harness`),
|
|
33
|
+
model: optional(profile.model, `workflow specialist ${id} model`),
|
|
34
|
+
effort: optional(profile.effort, `workflow specialist ${id} effort`),
|
|
35
|
+
maxAttempts: positiveOptional(
|
|
36
|
+
profile.maxAttempts,
|
|
37
|
+
`workflow specialist ${id} maxAttempts`,
|
|
38
|
+
10,
|
|
39
|
+
),
|
|
40
|
+
timeoutMs: positiveOptional(
|
|
41
|
+
profile.timeoutMs,
|
|
42
|
+
`workflow specialist ${id} timeoutMs`,
|
|
43
|
+
24 * 60 * 60 * 1_000,
|
|
44
|
+
),
|
|
45
|
+
maxTurns: positiveOptional(profile.maxTurns, `workflow specialist ${id} maxTurns`, 1_000),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveSpecialistAssignment(
|
|
52
|
+
profiles: Readonly<Record<string, WorkflowSpecialistProfile>>,
|
|
53
|
+
prompt: string,
|
|
54
|
+
opts: AgentOpts,
|
|
55
|
+
): { prompt: string; opts: AgentOpts } {
|
|
56
|
+
if (!opts.specialist) return { prompt, opts };
|
|
57
|
+
if (!SPECIALIST_ID.test(opts.specialist)) {
|
|
58
|
+
throw new Error(`invalid workflow specialist id ${JSON.stringify(opts.specialist)}`);
|
|
59
|
+
}
|
|
60
|
+
const profile = profiles[opts.specialist];
|
|
61
|
+
if (!profile)
|
|
62
|
+
throw new Error(`workflow specialist ${JSON.stringify(opts.specialist)} is not configured`);
|
|
63
|
+
return {
|
|
64
|
+
prompt: `${profile.instructions}\n\nAssignment:\n${prompt}`,
|
|
65
|
+
opts: {
|
|
66
|
+
specialist: opts.specialist,
|
|
67
|
+
harness: opts.harness ?? profile.harness,
|
|
68
|
+
model: opts.model ?? profile.model,
|
|
69
|
+
effort: opts.effort ?? profile.effort,
|
|
70
|
+
maxAttempts: opts.maxAttempts ?? profile.maxAttempts,
|
|
71
|
+
timeoutMs: opts.timeoutMs ?? profile.timeoutMs,
|
|
72
|
+
maxTurns: opts.maxTurns ?? profile.maxTurns,
|
|
73
|
+
label: opts.label,
|
|
74
|
+
schema: opts.schema,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function bounded(value: unknown, field: string, max: number): string {
|
|
80
|
+
if (typeof value !== "string") throw new Error(`${field} must be a string`);
|
|
81
|
+
const normalized = value.trim();
|
|
82
|
+
if (!normalized) throw new Error(`${field} must not be empty`);
|
|
83
|
+
if (normalized.length > max) throw new Error(`${field} exceeds ${max} characters`);
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function optional(value: unknown, field: string): string | undefined {
|
|
88
|
+
return value === undefined ? undefined : bounded(value, field, MAX_OPTION);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function positiveOptional(value: unknown, field: string, max: number): number | undefined {
|
|
92
|
+
if (value === undefined) return undefined;
|
|
93
|
+
if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > max) {
|
|
94
|
+
throw new Error(`${field} must be an integer from 1 to ${max}`);
|
|
95
|
+
}
|
|
96
|
+
return value as number;
|
|
97
|
+
}
|
|
@@ -82,6 +82,7 @@ export interface WorkflowAgentProof {
|
|
|
82
82
|
id: string;
|
|
83
83
|
label: string;
|
|
84
84
|
stage?: string;
|
|
85
|
+
specialist?: string;
|
|
85
86
|
harness: HarnessName;
|
|
86
87
|
model?: string;
|
|
87
88
|
status: "succeeded" | "failed" | "cached";
|
|
@@ -147,6 +148,8 @@ export interface WorkflowProof {
|
|
|
147
148
|
schema_version: typeof WORKFLOW_PROOF_SCHEMA_VERSION;
|
|
148
149
|
run: {
|
|
149
150
|
id: string;
|
|
151
|
+
/** Durable objective this execution attempt belongs to, when linked. */
|
|
152
|
+
work_item_id?: string;
|
|
150
153
|
name: string;
|
|
151
154
|
status: WorkflowRunStatus;
|
|
152
155
|
started_at: string;
|
|
@@ -208,6 +211,9 @@ export interface StageSchema {
|
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
export interface AgentOpts {
|
|
214
|
+
/** Frozen specialist profile whose instructions and defaults wrap this
|
|
215
|
+
* assignment. The host supplies profiles through EngineOpts.specialists. */
|
|
216
|
+
specialist?: string;
|
|
211
217
|
/** Stage gate: when present, the agent's reply must strict-parse as JSON and
|
|
212
218
|
* validate; the engine retries with the validation error appended, up to
|
|
213
219
|
* `maxAttempts`. Without it, `agent()` resolves to the raw reply text. */
|
|
@@ -237,6 +243,18 @@ export interface AgentOpts {
|
|
|
237
243
|
* a package-owned union first. */
|
|
238
244
|
export type HarnessName = string;
|
|
239
245
|
|
|
246
|
+
/** Durable role defaults supplied by a goal supervisor or embedding host.
|
|
247
|
+
* Profiles are frozen into a workflow run manifest before the first spawn. */
|
|
248
|
+
export interface WorkflowSpecialistProfile {
|
|
249
|
+
instructions: string;
|
|
250
|
+
harness?: HarnessName;
|
|
251
|
+
model?: string;
|
|
252
|
+
effort?: string;
|
|
253
|
+
maxAttempts?: number;
|
|
254
|
+
timeoutMs?: number;
|
|
255
|
+
maxTurns?: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
240
258
|
/** What a spawn adapter returns for one subagent run. */
|
|
241
259
|
export interface SpawnResult {
|
|
242
260
|
ok: boolean;
|
|
@@ -307,6 +325,8 @@ export interface EngineOpts {
|
|
|
307
325
|
spawners: Readonly<Record<HarnessName, Spawner | undefined>>;
|
|
308
326
|
/** Harness used when an agent() call doesn't name one (default "claude-code"). */
|
|
309
327
|
defaultHarness?: HarnessName;
|
|
328
|
+
/** Named specialist roles available to agent(..., { specialist }). */
|
|
329
|
+
specialists?: Readonly<Record<string, WorkflowSpecialistProfile>>;
|
|
310
330
|
/** Resume: run id of a prior run whose journal supplies cached results.
|
|
311
331
|
* agent() calls whose (stage, prompt, model, maxTurns, schema) key matches a
|
|
312
332
|
* completed prior agent return the journaled result without spawning. */
|
|
@@ -315,6 +335,10 @@ export interface EngineOpts {
|
|
|
315
335
|
* approval has been resolved. The frozen run manifest supplies execution
|
|
316
336
|
* options and the original repository-before snapshot. */
|
|
317
337
|
resumeRunId?: string;
|
|
338
|
+
/** Stable id for a new run allocated by a durable-work host. */
|
|
339
|
+
runId?: string;
|
|
340
|
+
/** Durable objective this execution attempt belongs to. */
|
|
341
|
+
workItemId?: string;
|
|
318
342
|
/** Total-agent ceiling for the run (default 50): the runaway backstop. */
|
|
319
343
|
maxAgents?: number;
|
|
320
344
|
/** Concurrent-subagent cap for parallel() (default 4). */
|
|
@@ -357,6 +381,7 @@ export interface EngineOpts {
|
|
|
357
381
|
|
|
358
382
|
export interface RunReport {
|
|
359
383
|
runId: string;
|
|
384
|
+
workItemId?: string;
|
|
360
385
|
name: string;
|
|
361
386
|
/** What the script's default export returned. */
|
|
362
387
|
result: unknown;
|