@signalridge/pi-workflows 1.5.0 → 1.7.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/CHANGELOG.md +227 -0
- package/README.md +207 -48
- package/package.json +2 -2
- package/skills/workflow-authoring/SKILL.md +44 -14
- package/skills/workflow-authoring/examples/defensive-json-parsing.js +1 -1
- package/skills/workflow-authoring/examples/phased-budgets.js +1 -1
- package/skills/workflow-authoring/examples/saved-nested-workflows.js +1 -1
- package/skills/workflow-authoring/examples/structured-output.js +1 -1
- package/skills/workflow-authoring/references/capabilities.md +7 -7
- package/skills/workflow-authoring/references/capability-details.md +24 -10
- package/skills/workflow-authoring/references/debugging.md +1 -1
- package/skills/workflow-authoring/references/lifecycle.md +5 -1
- package/skills/workflow-authoring/references/quality-helpers.md +2 -2
- package/skills/workflow-authoring/references/registry-ownership.md +4 -4
- package/skills/workflow-authoring/references/review.md +8 -4
- package/skills/workflow-authoring/references/runtime.md +2 -2
- package/skills/workflow-authoring/references/specialized-helpers.md +1 -1
- package/skills/workflow-authoring/references/versions.md +1 -1
- package/skills/workflow-patterns/SKILL.md +1 -1
- package/skills/workflow-review/SKILL.md +6 -4
- package/src/builtins.ts +23 -23
- package/src/capabilities.ts +37 -12
- package/src/engine.ts +70 -26
- package/src/index.ts +243 -70
- package/src/journal.ts +0 -14
- package/src/rpc-client.ts +14 -1
- package/src/runtime.ts +452 -103
- package/src/strengths.ts +93 -0
- package/src/workflow-settings.ts +95 -1
package/src/runtime.ts
CHANGED
|
@@ -33,6 +33,13 @@ import {
|
|
|
33
33
|
type OrchestrationTaskContext,
|
|
34
34
|
} from "./orchestrator.js";
|
|
35
35
|
import { SharedStore } from "./shared-store.js";
|
|
36
|
+
import {
|
|
37
|
+
defaultStrengthTable,
|
|
38
|
+
isWorkflowStrength,
|
|
39
|
+
WORKFLOW_STRENGTH_LIST,
|
|
40
|
+
type WorkflowStrength,
|
|
41
|
+
type WorkflowStrengthTable,
|
|
42
|
+
} from "./strengths.js";
|
|
36
43
|
|
|
37
44
|
// ── Meta contract ────────────────────────────────────────────────────────────
|
|
38
45
|
|
|
@@ -83,6 +90,24 @@ export interface SharedRuntime {
|
|
|
83
90
|
runFatalController: AbortController;
|
|
84
91
|
inFlight: Set<Promise<unknown>>;
|
|
85
92
|
activeThreads: Set<string>;
|
|
93
|
+
/**
|
|
94
|
+
* Strengths whose routing this run has already reported.
|
|
95
|
+
*
|
|
96
|
+
* Shared with nested frames rather than kept per frame: the answer is the
|
|
97
|
+
* same in a child, and a run that says "reported once" has to mean once for
|
|
98
|
+
* the run and not once per `workflow()` boundary.
|
|
99
|
+
*/
|
|
100
|
+
routedStrengths: Set<string>;
|
|
101
|
+
/**
|
|
102
|
+
* The strength → tier table this run honors, resolved once at the top frame.
|
|
103
|
+
*
|
|
104
|
+
* Shared rather than recomputed per frame for the same reason the set above
|
|
105
|
+
* is: a nested frame inherits the same table and the same catalogue, so it
|
|
106
|
+
* would reach an identical answer and log an identical complaint about any
|
|
107
|
+
* stale entry — once per `workflow()` boundary. Resolving once also makes it
|
|
108
|
+
* structural, rather than merely likely, that one run honors one table.
|
|
109
|
+
*/
|
|
110
|
+
strengths: ReadonlyMap<string, string>;
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
/** Counting semaphore with a FIFO resolve queue. */
|
|
@@ -130,6 +155,16 @@ export interface AgentRunOptions {
|
|
|
130
155
|
onTierResolved?: (tier: string) => void;
|
|
131
156
|
onUsage?: (usage: AgentUsage) => void;
|
|
132
157
|
onHistory?: (history: unknown[]) => void;
|
|
158
|
+
/**
|
|
159
|
+
* The agent left the host's queue and is now running.
|
|
160
|
+
*
|
|
161
|
+
* A dispatched agent does not necessarily start: it takes a background slot
|
|
162
|
+
* from pi-subagents, and a fan-out wider than that pool waits. The wall clock
|
|
163
|
+
* is restarted from this point so queue time is not charged against the work,
|
|
164
|
+
* which is what let a batch dispatched behind a busy pool fail as timeouts
|
|
165
|
+
* without any of it having run.
|
|
166
|
+
*/
|
|
167
|
+
onRunning?: () => void;
|
|
133
168
|
}
|
|
134
169
|
|
|
135
170
|
export interface AgentOptions {
|
|
@@ -137,12 +172,24 @@ export interface AgentOptions {
|
|
|
137
172
|
phase?: string;
|
|
138
173
|
schema?: unknown;
|
|
139
174
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
175
|
+
* Workflow strength: how much effort this step deserves, `low`/`medium`/`high`.
|
|
176
|
+
*
|
|
177
|
+
* The only model control a workflow has and the only routing word it speaks.
|
|
178
|
+
* There is no per-call model, thinking, or Agent tier, so a script cannot pin
|
|
179
|
+
* a vendor or a catalogue key the host did not choose. Which Agent tier a
|
|
180
|
+
* strength runs on is the user's `strengths` table and nothing else — a
|
|
181
|
+
* strength that table does not define dispatches with no tier at all and
|
|
182
|
+
* takes the agent's ordinary default.
|
|
183
|
+
*
|
|
184
|
+
* There is no default. Omitting it dispatches with no tier at all, the same
|
|
185
|
+
* as an unmapped strength and the same as any ordinary Agent spawn: a script
|
|
186
|
+
* that named no strength stated no opinion, and the host's own precedence —
|
|
187
|
+
* the agent type's frontmatter tier before the configured default — is the
|
|
188
|
+
* right answer to that. A default would have outranked the agent's own tier
|
|
189
|
+
* on every unlabelled call, which is the one thing this indirection exists to
|
|
190
|
+
* avoid doing.
|
|
144
191
|
*/
|
|
145
|
-
|
|
192
|
+
strength?: WorkflowStrength;
|
|
146
193
|
isolation?: "worktree";
|
|
147
194
|
agentType?: string;
|
|
148
195
|
timeoutMs?: number | null;
|
|
@@ -173,16 +220,42 @@ export interface WorkflowRunOptions {
|
|
|
173
220
|
/** The host's Agent-tier catalogue, captured fresh for this start/resume. */
|
|
174
221
|
routingPolicy?: ManagedRoutingPolicy;
|
|
175
222
|
/**
|
|
176
|
-
*
|
|
223
|
+
* The user's strength → Agent tier table: the whole of workflow model routing.
|
|
177
224
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
225
|
+
* A script names a strength and nothing else. This table is the only thing
|
|
226
|
+
* that binds one to an Agent tier, so a strength it does not define reaches
|
|
227
|
+
* dispatch carrying no tier and takes the agent's ordinary default — the
|
|
228
|
+
* agent's own frontmatter tier, then the host's configured default.
|
|
229
|
+
*
|
|
230
|
+
* Omitted entirely is not "no mappings": it means {@link defaultStrengthTable},
|
|
231
|
+
* which starts each strength on the catalogue tier of the same name wherever
|
|
232
|
+
* this host defines one. That is a default *table* and not a fallback rule,
|
|
233
|
+
* and the difference is what keeps re-pricing possible. A rule would be
|
|
234
|
+
* unremovable, so making a 26-agent fan-out cheaper would still mean editing
|
|
235
|
+
* the tier itself — dragging the Explore agent and everything else that names
|
|
236
|
+
* it. A table is replaced by writing one, and a strength the written table
|
|
237
|
+
* omits stays unmapped even on a host that defines a tier of the same name.
|
|
238
|
+
*
|
|
239
|
+
* Values are keys in the host's one catalogue and nothing else: pi-subagents
|
|
240
|
+
* still owns every model, every thinking level, and the only
|
|
241
|
+
* `resolveAgentTier()`. No entry may ever grow a `model` or `thinking` field
|
|
242
|
+
* — that is the line between this and the retired `workflow.tiers` key, which
|
|
243
|
+
* carried both and so became a second model policy behind a second resolver.
|
|
244
|
+
*
|
|
245
|
+
* One hop: a strength resolves to a tier, and that tier is never looked up as
|
|
246
|
+
* a strength in turn.
|
|
247
|
+
*
|
|
248
|
+
* Read fresh for each start and resume, never frozen onto the run, so editing
|
|
249
|
+
* the table and resuming re-routes the calls that have not run.
|
|
250
|
+
*
|
|
251
|
+
* That is a re-spend, not a free redirect. A call's resume identity keys on
|
|
252
|
+
* the tier it requests, so every *finished* call whose strength moved misses
|
|
253
|
+
* its journal entry — and resume replays only the longest unchanged prefix, so
|
|
254
|
+
* the first such miss takes everything after it live as well. Re-routing a run
|
|
255
|
+
* that is mostly done costs roughly the run. Start a new one instead when the
|
|
256
|
+
* point of the edit was to spend less.
|
|
184
257
|
*/
|
|
185
|
-
|
|
258
|
+
strengths?: WorkflowStrengthTable;
|
|
186
259
|
/**
|
|
187
260
|
* Whole-catalogue identity, used wherever a per-tier one would be dishonest:
|
|
188
261
|
* a nested `workflow()` boundary, where the parent caches one value for a
|
|
@@ -197,6 +270,17 @@ export interface WorkflowRunOptions {
|
|
|
197
270
|
workflowDepth?: number;
|
|
198
271
|
cwd?: string;
|
|
199
272
|
concurrency?: number;
|
|
273
|
+
/**
|
|
274
|
+
* The host's background-agent pool size, read live from the peer at each
|
|
275
|
+
* start and resume.
|
|
276
|
+
*
|
|
277
|
+
* It is both the default width and the ceiling: a workflow agent occupies one
|
|
278
|
+
* of those slots like any other background agent, so dispatching past the pool
|
|
279
|
+
* does not run wider — it fills the host's single queue ahead of the host's own
|
|
280
|
+
* spawns. One setting therefore governs the fleet, and a run neither has to
|
|
281
|
+
* guess it nor can exceed it.
|
|
282
|
+
*/
|
|
283
|
+
hostMaxConcurrent?: number;
|
|
200
284
|
agentRetries?: number;
|
|
201
285
|
tokenBudget?: number | null;
|
|
202
286
|
signal?: AbortSignal;
|
|
@@ -214,12 +298,8 @@ export interface WorkflowRunOptions {
|
|
|
214
298
|
sharedRuntime?: SharedRuntime;
|
|
215
299
|
initialTokenUsage?: AgentUsage;
|
|
216
300
|
sharedStore?: SharedStore;
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
* it returned ships with this package. Shipped-ness travels with the script,
|
|
220
|
-
* not with the frame that called it — see `shippedScript`.
|
|
221
|
-
*/
|
|
222
|
-
loadSavedWorkflow?: (name: string) => { script: string; shippedScript?: boolean } | undefined;
|
|
301
|
+
/** Resolve a nested `workflow(name)` reference to the script it names. */
|
|
302
|
+
loadSavedWorkflow?: (name: string) => string | undefined;
|
|
223
303
|
confirm?: (promptText: string, options: CheckpointOptions) => Promise<unknown>;
|
|
224
304
|
onLog?: (message: string) => void;
|
|
225
305
|
onPhase?: (title: string) => void;
|
|
@@ -278,9 +358,36 @@ export const MAX_AGENTS_PER_RUN = 1000;
|
|
|
278
358
|
export const MAX_CALLS_PER_WORKFLOW = 1_000;
|
|
279
359
|
/** Per-call item cap, matching the bounded batch contract used by reference engines. */
|
|
280
360
|
export const MAX_FANOUT_ITEMS = 4_096;
|
|
281
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Upper bound on agents in flight at once.
|
|
363
|
+
*
|
|
364
|
+
* A dispatched agent is a remote call this process waits on, not work it
|
|
365
|
+
* performs, so the ceiling guards against a runaway script rather than against
|
|
366
|
+
* machine capacity. The real throttle belongs to pi-subagents: every managed
|
|
367
|
+
* spawn takes one of its `maxConcurrent` background slots, so a host that wants
|
|
368
|
+
* a narrower fleet narrows it there, once, for every agent it runs.
|
|
369
|
+
*/
|
|
370
|
+
export const MAX_CONCURRENCY = 64;
|
|
371
|
+
/**
|
|
372
|
+
* In-flight agents when neither the run nor the caller names a number.
|
|
373
|
+
*
|
|
374
|
+
* Deliberately a constant rather than a function of `hardwareConcurrency`. Core
|
|
375
|
+
* count measures what this process can compute, and dispatching an agent
|
|
376
|
+
* computes nothing here; deriving the default from it fanned a 14-core host out
|
|
377
|
+
* 12 ways and a 4-core host 2 ways for identical, entirely remote work.
|
|
378
|
+
*/
|
|
379
|
+
export const DEFAULT_CONCURRENCY = 16;
|
|
282
380
|
export const MAX_AGENT_RETRIES = 3;
|
|
283
|
-
|
|
381
|
+
/**
|
|
382
|
+
* Wall clock for one agent when neither the call nor the run names one.
|
|
383
|
+
*
|
|
384
|
+
* It bounds a hang and nothing else. An agent's real budget is the turn, tool
|
|
385
|
+
* and token ceiling pi-subagents applies to it, which a deep call legitimately
|
|
386
|
+
* spends over many minutes; a default an order of magnitude shorter than that
|
|
387
|
+
* budget turned every wide fan-out of real work into a batch of timeouts. Pass
|
|
388
|
+
* `null` to remove the wall clock and leave the host's budgets in sole charge.
|
|
389
|
+
*/
|
|
390
|
+
export const DEFAULT_AGENT_TIMEOUT_MS: number | null = 1_800_000;
|
|
284
391
|
export const MAX_SCRIPT_SYNC_MS = 1_000;
|
|
285
392
|
|
|
286
393
|
// ── Determinism guardrail ────────────────────────────────────────────────────
|
|
@@ -516,7 +623,7 @@ function validateMeta(meta: unknown): asserts meta is WorkflowMeta {
|
|
|
516
623
|
if (typeof value.description !== "string" || !value.description.trim() || value.description.length > 100_000)
|
|
517
624
|
throw new Error("meta.description must be a non-empty string of at most 100000 characters");
|
|
518
625
|
if (Object.hasOwn(value, "model") || Object.hasOwn(value, "thinking")) {
|
|
519
|
-
throw new Error("workflow meta model/thinking fields are unsupported; use agent({
|
|
626
|
+
throw new Error("workflow meta model/thinking fields are unsupported; use agent({ strength })");
|
|
520
627
|
}
|
|
521
628
|
if (value.phases !== undefined) {
|
|
522
629
|
if (!Array.isArray(value.phases)) throw new Error("meta.phases must be an array");
|
|
@@ -532,7 +639,7 @@ function validateMeta(meta: unknown): asserts meta is WorkflowMeta {
|
|
|
532
639
|
}
|
|
533
640
|
const phaseRecord = phase as unknown as Record<string, unknown>;
|
|
534
641
|
if (Object.hasOwn(phaseRecord, "model") || Object.hasOwn(phaseRecord, "thinking")) {
|
|
535
|
-
throw new Error("workflow phase model/thinking fields are unsupported; use agent({
|
|
642
|
+
throw new Error("workflow phase model/thinking fields are unsupported; use agent({ strength })");
|
|
536
643
|
}
|
|
537
644
|
}
|
|
538
645
|
}
|
|
@@ -564,6 +671,19 @@ function hashIdentity(value: unknown): string {
|
|
|
564
671
|
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
565
672
|
}
|
|
566
673
|
|
|
674
|
+
/**
|
|
675
|
+
* The strength vocabulary as a stable value, for hashing.
|
|
676
|
+
*
|
|
677
|
+
* Sorted with a code-unit comparison rather than `localeCompare` for the reason
|
|
678
|
+
* `getRoutingPolicySnapshot` gives about `blockedProfiles`: this reaches an
|
|
679
|
+
* identity hash, and a locale-dependent order would let one table hash two ways
|
|
680
|
+
* on two machines, or on one machine after its locale changed.
|
|
681
|
+
*/
|
|
682
|
+
function canonicalStrengths(strengths: ReadonlyMap<string, string>): [string, string][] | null {
|
|
683
|
+
if (strengths.size === 0) return null;
|
|
684
|
+
return [...strengths].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
685
|
+
}
|
|
686
|
+
|
|
567
687
|
/**
|
|
568
688
|
* Stable identity hash for an agent() call. Must cover every input that can
|
|
569
689
|
* change the outcome so a cache-miss is never served on resume. The agent
|
|
@@ -575,30 +695,34 @@ function hashAgentCall(
|
|
|
575
695
|
prompt: string,
|
|
576
696
|
phase: string | undefined,
|
|
577
697
|
options: AgentOptions,
|
|
698
|
+
tier: string | undefined,
|
|
578
699
|
routingPolicy: ManagedRoutingPolicy | undefined,
|
|
579
700
|
routingPolicyFingerprint: string | undefined,
|
|
580
701
|
): string {
|
|
581
|
-
//
|
|
702
|
+
// Keyed on the tier this call will actually request, never on the strength
|
|
703
|
+
// that chose it. The strength is a name for a routing decision; the tier is
|
|
704
|
+
// the decision, so two runs whose tables differ but land on the same tier
|
|
705
|
+
// must replay, and one whose table moved this strength elsewhere must not.
|
|
706
|
+
//
|
|
707
|
+
// A call that resolved to a tier is keyed on that tier's policy alone, so
|
|
582
708
|
// defining or editing an unrelated tier cannot invalidate it.
|
|
583
709
|
//
|
|
584
|
-
// A call
|
|
585
|
-
// `defaultTier` would be worse than coarse — it
|
|
586
|
-
// resolves an
|
|
587
|
-
// only then the default, and frontmatter is
|
|
588
|
-
// definition lives in pi-subagents and reaches this
|
|
589
|
-
// the default, editing the tier the agent actually
|
|
590
|
-
// stale work, while editing the default would
|
|
591
|
-
// touched it. The whole-catalogue fingerprint is
|
|
592
|
-
// least sound, and it is stable across the
|
|
593
|
-
// the tier the host selected is not — that
|
|
594
|
-
// and this hash is computed before it.
|
|
710
|
+
// A call whose strength is unmapped resolves to no tier and cannot be keyed
|
|
711
|
+
// that way, and keying it on `defaultTier` would be worse than coarse — it
|
|
712
|
+
// would be wrong. The host resolves an untiered call against the agent's own
|
|
713
|
+
// frontmatter tier first and only then the default, and frontmatter is
|
|
714
|
+
// invisible here: the agent definition lives in pi-subagents and reaches this
|
|
715
|
+
// side as a name. Keyed on the default, editing the tier the agent actually
|
|
716
|
+
// declares would replay stale work, while editing the default would
|
|
717
|
+
// invalidate work that never touched it. The whole-catalogue fingerprint is
|
|
718
|
+
// the coarse answer that is at least sound, and it is stable across the
|
|
719
|
+
// original run and its resume, which the tier the host selected is not — that
|
|
720
|
+
// one is only known after dispatch, and this hash is computed before it.
|
|
595
721
|
const tierPolicy =
|
|
596
|
-
|
|
597
|
-
? (routingPolicyFingerprint ?? null)
|
|
598
|
-
: agentTierPolicyIdentity(routingPolicy, options.tier);
|
|
722
|
+
tier === undefined ? (routingPolicyFingerprint ?? null) : agentTierPolicyIdentity(routingPolicy, tier);
|
|
599
723
|
return hashIdentity({
|
|
600
724
|
prompt,
|
|
601
|
-
tier:
|
|
725
|
+
tier: tier ?? null,
|
|
602
726
|
tierPolicy,
|
|
603
727
|
phase: phase ?? null,
|
|
604
728
|
agentType: options.agentType ?? null,
|
|
@@ -631,6 +755,104 @@ function hashCheckpoint(promptText: string, options: CheckpointOptions): string
|
|
|
631
755
|
|
|
632
756
|
// ── Small helpers ────────────────────────────────────────────────────────────
|
|
633
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Every key {@link AgentOptions} accepts, and every key {@link CheckpointOptions}
|
|
760
|
+
* does.
|
|
761
|
+
*
|
|
762
|
+
* Spelled out because an interface leaves no runtime value to enumerate, and a
|
|
763
|
+
* script is JavaScript evaluated in a VM, so the compiler never sees the object
|
|
764
|
+
* these guard. The `satisfies` clause is what keeps the list honest anyway: a
|
|
765
|
+
* `Record` of the interface's keys is missing-key-checked, and the excess
|
|
766
|
+
* property check on the literal catches the other direction — so adding an
|
|
767
|
+
* option without listing it here, or listing one that no longer exists, is a
|
|
768
|
+
* type error rather than a silently unreachable option.
|
|
769
|
+
*/
|
|
770
|
+
const AGENT_OPTION_KEYS: ReadonlySet<string> = new Set(
|
|
771
|
+
Object.keys({
|
|
772
|
+
label: 0,
|
|
773
|
+
phase: 0,
|
|
774
|
+
schema: 0,
|
|
775
|
+
strength: 0,
|
|
776
|
+
isolation: 0,
|
|
777
|
+
agentType: 0,
|
|
778
|
+
timeoutMs: 0,
|
|
779
|
+
retries: 0,
|
|
780
|
+
thread: 0,
|
|
781
|
+
toolset: 0,
|
|
782
|
+
excludeTools: 0,
|
|
783
|
+
} satisfies Record<keyof AgentOptions, unknown>),
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
const CHECKPOINT_OPTION_KEYS: ReadonlySet<string> = new Set(
|
|
787
|
+
Object.keys({
|
|
788
|
+
default: 0,
|
|
789
|
+
headless: 0,
|
|
790
|
+
kind: 0,
|
|
791
|
+
choices: 0,
|
|
792
|
+
timeoutMs: 0,
|
|
793
|
+
} satisfies Record<keyof CheckpointOptions, unknown>),
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Keys that used to mean something, or that a script author reaches for out of
|
|
798
|
+
* habit, answered by name instead of by the generic list.
|
|
799
|
+
*
|
|
800
|
+
* `tier` was this package's own option before a strength stood between a script
|
|
801
|
+
* and the host catalogue. `model`/`thinking` never were — `meta` and phase
|
|
802
|
+
* profiles have always rejected them — but an author who tries them on `meta`
|
|
803
|
+
* and gets a clear refusal will try them here next, and the two surfaces should
|
|
804
|
+
* answer the same way.
|
|
805
|
+
*/
|
|
806
|
+
const RETIRED_AGENT_OPTIONS: Readonly<Record<string, string>> = {
|
|
807
|
+
tier: `name a strength instead: one of ${WORKFLOW_STRENGTH_LIST}`,
|
|
808
|
+
model: "a strength is the only model policy a workflow can express",
|
|
809
|
+
thinking: "a strength is the only model policy a workflow can express",
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Refuse an options object carrying a key this runtime does not read.
|
|
814
|
+
*
|
|
815
|
+
* Fails closed because the alternative is not "harmless": an unknown key is
|
|
816
|
+
* dropped before the call hash is built, so it changes neither the dispatch nor
|
|
817
|
+
* the resume identity — the run simply spends at a policy nobody chose, and a
|
|
818
|
+
* resume replays it without ever noticing. `agent({ strenght: "low" })` would
|
|
819
|
+
* quietly run every step untiered; `checkpoint(p, { headles: "abort" })` would
|
|
820
|
+
* quietly auto-approve in headless mode instead of aborting. Neither shows up
|
|
821
|
+
* in a log.
|
|
822
|
+
*
|
|
823
|
+
* A script is authored against this contract and evaluated as plain JavaScript,
|
|
824
|
+
* so a typo has no other gate to catch it. The closed vocabulary made a
|
|
825
|
+
* misspelled *strength* fail; this is the same argument one level up, for the
|
|
826
|
+
* option name itself.
|
|
827
|
+
*/
|
|
828
|
+
function validateCallOptions(
|
|
829
|
+
callee: "agent" | "checkpoint",
|
|
830
|
+
options: unknown,
|
|
831
|
+
allowed: ReadonlySet<string>,
|
|
832
|
+
retired: Readonly<Record<string, string>> = {},
|
|
833
|
+
): void {
|
|
834
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
835
|
+
throw new WorkflowError(
|
|
836
|
+
`${callee}() options must be a plain object (received ${
|
|
837
|
+
Array.isArray(options) ? "an array" : JSON.stringify(options)
|
|
838
|
+
})`,
|
|
839
|
+
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
840
|
+
{ recoverable: false },
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
for (const key of Object.keys(options)) {
|
|
844
|
+
if (allowed.has(key)) continue;
|
|
845
|
+
const hint = retired[key];
|
|
846
|
+
throw new WorkflowError(
|
|
847
|
+
`${callee}() received an unknown option ${JSON.stringify(key)}; ${
|
|
848
|
+
hint ?? `valid options are: ${[...allowed].join(", ")}`
|
|
849
|
+
}`,
|
|
850
|
+
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
851
|
+
{ recoverable: false },
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
634
856
|
function defaultAgentLabel(phase: string | undefined, index: number): string {
|
|
635
857
|
return phase ? `${phase} agent ${index}` : `agent ${index}`;
|
|
636
858
|
}
|
|
@@ -691,13 +913,30 @@ async function withTimeout<T>(
|
|
|
691
913
|
ms: number | null,
|
|
692
914
|
label: string,
|
|
693
915
|
onTimeout?: () => void,
|
|
916
|
+
/**
|
|
917
|
+
* Receives a `restart()` that re-arms the clock from now.
|
|
918
|
+
*
|
|
919
|
+
* The clock is armed at dispatch and restarted when the agent reports that it
|
|
920
|
+
* started, rather than only armed on that report: a report that never arrives
|
|
921
|
+
* must degrade to the old behaviour, not to an agent that can hang forever.
|
|
922
|
+
*/
|
|
923
|
+
onArm?: (restart: () => void) => void,
|
|
694
924
|
): Promise<T> {
|
|
695
|
-
if (ms === null)
|
|
925
|
+
if (ms === null) {
|
|
926
|
+
onArm?.(() => {});
|
|
927
|
+
return promise;
|
|
928
|
+
}
|
|
696
929
|
|
|
697
930
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
931
|
+
// The `restart` handed out below outlives this call: the holder keeps it in a
|
|
932
|
+
// closure that the caller may still reach after the race settles. Re-arming
|
|
933
|
+
// then would leave a live timer nothing clears, firing `onTimeout` on a
|
|
934
|
+
// finished agent long after the fact, so settlement makes it inert here
|
|
935
|
+
// rather than relying on every holder to guard its own call.
|
|
936
|
+
let settled = false;
|
|
698
937
|
|
|
699
938
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
700
|
-
|
|
939
|
+
const fire = () => {
|
|
701
940
|
try {
|
|
702
941
|
onTimeout?.();
|
|
703
942
|
} catch {
|
|
@@ -710,12 +949,19 @@ async function withTimeout<T>(
|
|
|
710
949
|
{ recoverable: true },
|
|
711
950
|
),
|
|
712
951
|
);
|
|
713
|
-
}
|
|
952
|
+
};
|
|
953
|
+
timeoutId = setTimeout(fire, ms);
|
|
954
|
+
onArm?.(() => {
|
|
955
|
+
if (settled) return;
|
|
956
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
957
|
+
timeoutId = setTimeout(fire, ms);
|
|
958
|
+
});
|
|
714
959
|
});
|
|
715
960
|
|
|
716
961
|
try {
|
|
717
962
|
return await Promise.race([promise, timeoutPromise]);
|
|
718
963
|
} finally {
|
|
964
|
+
settled = true;
|
|
719
965
|
if (timeoutId) clearTimeout(timeoutId);
|
|
720
966
|
}
|
|
721
967
|
}
|
|
@@ -988,10 +1234,13 @@ export async function runWorkflow<T = unknown>(
|
|
|
988
1234
|
const agentTimeoutMs = normalizeAgentTimeout(options.agentTimeoutMs);
|
|
989
1235
|
const runId = options.runId ?? `run-${started.toString(36)}`;
|
|
990
1236
|
const baseCwd = options.cwd ?? process.cwd();
|
|
991
|
-
// The host's catalogue, known before the first call. A
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
//
|
|
1237
|
+
// The host's catalogue, known before the first call. A script cannot name a
|
|
1238
|
+
// tier, so this is not a typo check: it is what the strength table is resolved
|
|
1239
|
+
// against at run start — an entry pointing at a tier this host does not define
|
|
1240
|
+
// is dropped there, and an absent table becomes the same-named default only
|
|
1241
|
+
// for the tiers listed here. Undefined when no policy was supplied (in-process
|
|
1242
|
+
// tests), which leaves a configured table unchecked and the default empty.
|
|
1243
|
+
// Only the top frame reads it; a nested one inherits the resolved table.
|
|
995
1244
|
const knownTiers = options.routingPolicy ? new Set(Object.keys(options.routingPolicy.profiles)) : undefined;
|
|
996
1245
|
|
|
997
1246
|
const state: RuntimeState = {
|
|
@@ -1008,13 +1257,84 @@ export async function runWorkflow<T = unknown>(
|
|
|
1008
1257
|
};
|
|
1009
1258
|
|
|
1010
1259
|
const agentRunner = options.agent;
|
|
1260
|
+
// The host's pool is the width that can actually be achieved, so it is the
|
|
1261
|
+
// default when it is known and the ceiling when it is not the default.
|
|
1262
|
+
// DEFAULT_CONCURRENCY only covers a peer that published no pool size.
|
|
1263
|
+
const requestedConcurrency = options.concurrency ?? options.hostMaxConcurrent ?? DEFAULT_CONCURRENCY;
|
|
1011
1264
|
const concurrency = normalizeConcurrency(
|
|
1012
|
-
options.
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
(globalThis as { navigator?: { hardwareConcurrency?: number } }).navigator?.hardwareConcurrency ?? 8,
|
|
1016
|
-
) - 2,
|
|
1265
|
+
options.hostMaxConcurrent === undefined
|
|
1266
|
+
? requestedConcurrency
|
|
1267
|
+
: Math.min(requestedConcurrency, options.hostMaxConcurrent),
|
|
1017
1268
|
);
|
|
1269
|
+
// Only a caller who asked for a specific width can be surprised by the clamp,
|
|
1270
|
+
// and the guidance now tells everything else to omit the field — so the one
|
|
1271
|
+
// caller that set it is exactly the one owed an explanation for the number it
|
|
1272
|
+
// did not get.
|
|
1273
|
+
// Floored, not raw: normalizeConcurrency floors, and reporting 12.5 -> 12 as a
|
|
1274
|
+
// clamp would blame a bound that did not bite.
|
|
1275
|
+
const askedConcurrency = options.concurrency === undefined ? undefined : Math.floor(options.concurrency);
|
|
1276
|
+
const concurrencyClampNotice =
|
|
1277
|
+
askedConcurrency !== undefined && askedConcurrency > concurrency
|
|
1278
|
+
? `concurrency ${askedConcurrency} clamped to ${concurrency}` +
|
|
1279
|
+
// Name the bound that actually bit. A pool above MAX_CONCURRENCY is not
|
|
1280
|
+
// the reason for the clamp even though it is below what was requested.
|
|
1281
|
+
(concurrency === options.hostMaxConcurrent
|
|
1282
|
+
? `: the host's subagent pool is ${concurrency}, and dispatching past it only queues`
|
|
1283
|
+
: `: the runtime ceiling is ${MAX_CONCURRENCY}`)
|
|
1284
|
+
: undefined;
|
|
1285
|
+
|
|
1286
|
+
const log = (message: unknown) => {
|
|
1287
|
+
const text = String(message);
|
|
1288
|
+
state.logs.push(text);
|
|
1289
|
+
options.onLog?.(text);
|
|
1290
|
+
};
|
|
1291
|
+
if (concurrencyClampNotice !== undefined) log(concurrencyClampNotice);
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* The strength table this run will honor, target-checked once at start.
|
|
1295
|
+
*
|
|
1296
|
+
* Checked here rather than per call so a stale entry is reported once, at the
|
|
1297
|
+
* point where the fix is obvious, instead of once per dispatch. An entry whose
|
|
1298
|
+
* tier this host does not define is dropped rather than fatal: the table is a
|
|
1299
|
+
* routing preference, and a name that went stale should cost the redirect, not
|
|
1300
|
+
* every workflow on the machine. A dropped entry leaves its strength unmapped,
|
|
1301
|
+
* which is the ordinary unconfigured case — the call takes the agent's own
|
|
1302
|
+
* default, exactly as it would on a machine that configured nothing.
|
|
1303
|
+
*
|
|
1304
|
+
* An entry keyed on a word outside the vocabulary is dropped in silence. The
|
|
1305
|
+
* settings layer already refuses to store one, so reaching here means a
|
|
1306
|
+
* hand-edited file, and the run is the wrong place to teach the vocabulary.
|
|
1307
|
+
*
|
|
1308
|
+
* No configured table at all is not "no mappings": it takes
|
|
1309
|
+
* {@link defaultStrengthTable}, whose entries are drawn from this host's own
|
|
1310
|
+
* catalogue and so can never be stale — which is why only a configured table
|
|
1311
|
+
* ever reaches the complaint above. An explicit `{}` is the way to say "no
|
|
1312
|
+
* mappings"; the settings layer stores that one and drops only a table whose
|
|
1313
|
+
* every entry was invalid, so a broken file cannot pass for that statement.
|
|
1314
|
+
*
|
|
1315
|
+
* Resolved once at the top frame and shared, so a nested `workflow()` cannot
|
|
1316
|
+
* re-derive the same answer and repeat the same complaint at its boundary.
|
|
1317
|
+
*/
|
|
1318
|
+
const resolveStrengths = (): ReadonlyMap<string, string> => {
|
|
1319
|
+
const configured = options.strengths;
|
|
1320
|
+
if (configured === undefined) return new Map(Object.entries(defaultStrengthTable(knownTiers)));
|
|
1321
|
+
const active = new Map<string, string>();
|
|
1322
|
+
// Re-checked despite the parameter type: this is a public entry point, and
|
|
1323
|
+
// a JS caller reaches it without one. The settings layer runs the same two
|
|
1324
|
+
// guards at read, so a table coming from there passes both twice.
|
|
1325
|
+
for (const [strength, tier] of Object.entries(configured)) {
|
|
1326
|
+
if (!isWorkflowStrength(strength) || !isManagedAgentTier(tier)) continue;
|
|
1327
|
+
if (knownTiers && !knownTiers.has(tier)) {
|
|
1328
|
+
log(
|
|
1329
|
+
`workflow strength ${JSON.stringify(strength)} → tier ${JSON.stringify(tier)} ignored; ` +
|
|
1330
|
+
`this host defines: ${[...knownTiers].sort().join(", ") || "(none)"}`,
|
|
1331
|
+
);
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
active.set(strength, tier);
|
|
1335
|
+
}
|
|
1336
|
+
return active;
|
|
1337
|
+
};
|
|
1018
1338
|
|
|
1019
1339
|
// Global caps + budget shared with any nested workflow() so they hold across nesting.
|
|
1020
1340
|
const shared: SharedRuntime = options.sharedRuntime ?? {
|
|
@@ -1028,6 +1348,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
1028
1348
|
runFatalController: new AbortController(),
|
|
1029
1349
|
inFlight: new Set<Promise<unknown>>(),
|
|
1030
1350
|
activeThreads: new Set<string>(),
|
|
1351
|
+
routedStrengths: new Set<string>(),
|
|
1352
|
+
strengths: resolveStrengths(),
|
|
1031
1353
|
};
|
|
1032
1354
|
const limiter = shared.limiter;
|
|
1033
1355
|
const isTopLevelRun = !options.sharedRuntime;
|
|
@@ -1036,10 +1358,22 @@ export async function runWorkflow<T = unknown>(
|
|
|
1036
1358
|
/** agent() promises owned by this runWorkflow frame only. */
|
|
1037
1359
|
const frameInFlight = new Set<Promise<unknown>>();
|
|
1038
1360
|
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1361
|
+
const strengths = shared.strengths;
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Report each strength's routing once per run.
|
|
1365
|
+
*
|
|
1366
|
+
* A fan-out asks for the same strength dozens of times and the answer is the
|
|
1367
|
+
* same every time; a line per dispatch would bury the run's own log under it.
|
|
1368
|
+
*/
|
|
1369
|
+
const logStrengthRouting = (strength: WorkflowStrength, tier: string | undefined): void => {
|
|
1370
|
+
if (shared.routedStrengths.has(strength)) return;
|
|
1371
|
+
shared.routedStrengths.add(strength);
|
|
1372
|
+
log(
|
|
1373
|
+
tier === undefined
|
|
1374
|
+
? `workflow strength ${JSON.stringify(strength)} is unmapped; using the agent's own default`
|
|
1375
|
+
: `workflow strength ${JSON.stringify(strength)} runs on agent tier ${JSON.stringify(tier)}`,
|
|
1376
|
+
);
|
|
1043
1377
|
};
|
|
1044
1378
|
|
|
1045
1379
|
const emitRuntimeEvent = (event: WorkflowRuntimeEvent): void => {
|
|
@@ -1099,6 +1433,9 @@ export async function runWorkflow<T = unknown>(
|
|
|
1099
1433
|
};
|
|
1100
1434
|
|
|
1101
1435
|
const agent = (promptText: string, agentOptions: AgentOptions = {}): Promise<unknown> => {
|
|
1436
|
+
// First, ahead of the thread bookkeeping below: a malformed options object
|
|
1437
|
+
// must not be able to register a thread it will never release.
|
|
1438
|
+
validateCallOptions("agent", agentOptions, AGENT_OPTION_KEYS, RETIRED_AGENT_OPTIONS);
|
|
1102
1439
|
const rawThread = agentOptions.thread;
|
|
1103
1440
|
const thread = rawThread === undefined ? undefined : typeof rawThread === "string" ? rawThread.trim() : "";
|
|
1104
1441
|
if (rawThread !== undefined && !thread) {
|
|
@@ -1153,49 +1490,38 @@ export async function runWorkflow<T = unknown>(
|
|
|
1153
1490
|
log(`agentType "${agentOptions.agentType}" resolves in pi-subagents at dispatch`);
|
|
1154
1491
|
}
|
|
1155
1492
|
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
// the
|
|
1160
|
-
if (agentOptions.
|
|
1493
|
+
// The vocabulary is closed, so a word outside it is a typo rather than a
|
|
1494
|
+
// strength nobody configured, and saying so here beats dispatching in
|
|
1495
|
+
// silence. This is the whole of the check: unlike a tier key, a strength is
|
|
1496
|
+
// ours, so we can answer for it without asking the host anything.
|
|
1497
|
+
if (agentOptions.strength !== undefined && !isWorkflowStrength(agentOptions.strength)) {
|
|
1161
1498
|
throw new WorkflowError(
|
|
1162
|
-
`agent
|
|
1499
|
+
`agent strength must be one of ${WORKFLOW_STRENGTH_LIST} (received ${JSON.stringify(agentOptions.strength)})`,
|
|
1163
1500
|
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
1164
1501
|
{ recoverable: false },
|
|
1165
1502
|
);
|
|
1166
1503
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}`,
|
|
1181
|
-
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
1182
|
-
{ recoverable: false },
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
// Everything downstream — the resume hash, the dispatch, the journal —
|
|
1187
|
-
// works from the tier that will actually be requested, not the one the
|
|
1188
|
-
// script wrote, so a dropped name cannot make a cached call look different
|
|
1189
|
-
// from the live one.
|
|
1190
|
-
const tieredOptions: AgentOptions =
|
|
1191
|
-
effectiveTier === agentOptions.tier ? agentOptions : { ...agentOptions, tier: effectiveTier };
|
|
1504
|
+
// The strength becomes an Agent tier here and only here, through the user's
|
|
1505
|
+
// table and nothing else. Unmapped resolves to no tier — the call takes the
|
|
1506
|
+
// agent's own default, and no catalogue key is ever inferred from a matching
|
|
1507
|
+
// spelling. One hop: the tier this yields is never looked up as a strength
|
|
1508
|
+
// in turn. There is no catalogue check left to do, because the only tier
|
|
1509
|
+
// that can appear here already passed one at run start.
|
|
1510
|
+
//
|
|
1511
|
+
// A call that named no strength takes that same untiered path rather than a
|
|
1512
|
+
// default rung: see the note in strengths.ts on why a default would have
|
|
1513
|
+
// outranked the agent type's own tier on every unlabelled dispatch.
|
|
1514
|
+
const strength = agentOptions.strength;
|
|
1515
|
+
const effectiveTier = strength === undefined ? undefined : strengths.get(strength);
|
|
1516
|
+
if (strength !== undefined) logStrengthRouting(strength, effectiveTier);
|
|
1192
1517
|
|
|
1193
1518
|
// Deterministic resume key: assigned at lexical call time, before the limiter.
|
|
1194
1519
|
const callIndex = nextCallIndex();
|
|
1195
1520
|
const callHash = hashAgentCall(
|
|
1196
1521
|
promptText,
|
|
1197
1522
|
assignedPhase,
|
|
1198
|
-
|
|
1523
|
+
agentOptions,
|
|
1524
|
+
effectiveTier,
|
|
1199
1525
|
options.routingPolicy,
|
|
1200
1526
|
options.routingPolicyFingerprint,
|
|
1201
1527
|
);
|
|
@@ -1349,6 +1675,10 @@ export async function runWorkflow<T = unknown>(
|
|
|
1349
1675
|
);
|
|
1350
1676
|
}
|
|
1351
1677
|
|
|
1678
|
+
// Set by withTimeout below, and called by the dispatcher when the
|
|
1679
|
+
// agent leaves the host queue. Undefined until then: a start that
|
|
1680
|
+
// beats the arming simply leaves the full clock ahead of it.
|
|
1681
|
+
let restartTimeout: (() => void) | undefined;
|
|
1352
1682
|
const runPromise = agentRunner.run(promptText + schemaInstruction + repairFeedback, {
|
|
1353
1683
|
label,
|
|
1354
1684
|
sessionName: agentOptions.thread
|
|
@@ -1379,9 +1709,20 @@ export async function runWorkflow<T = unknown>(
|
|
|
1379
1709
|
onHistory: (history: unknown[]) => {
|
|
1380
1710
|
options.onAgentHistory?.({ id: deltaKey, label, phase: assignedPhase, history });
|
|
1381
1711
|
},
|
|
1712
|
+
onRunning: () => {
|
|
1713
|
+
restartTimeout?.();
|
|
1714
|
+
},
|
|
1382
1715
|
});
|
|
1383
1716
|
runPromise.catch(() => {});
|
|
1384
|
-
let result = await withTimeout(
|
|
1717
|
+
let result = await withTimeout(
|
|
1718
|
+
runPromise,
|
|
1719
|
+
timeout,
|
|
1720
|
+
label,
|
|
1721
|
+
() => agentController.abort(),
|
|
1722
|
+
(restart) => {
|
|
1723
|
+
restartTimeout = restart;
|
|
1724
|
+
},
|
|
1725
|
+
);
|
|
1385
1726
|
|
|
1386
1727
|
throwIfAborted();
|
|
1387
1728
|
if (isEmptyTextAgentResult(result, schema)) {
|
|
@@ -1633,13 +1974,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1633
1974
|
});
|
|
1634
1975
|
}
|
|
1635
1976
|
const callIndex = nextCallIndex();
|
|
1636
|
-
const
|
|
1637
|
-
const childScript = resolved?.script ?? String(nameOrScript);
|
|
1638
|
-
// A named script carries its own shipped-ness; a raw script literal is the
|
|
1639
|
-
// caller's own text and shares the caller's. Inheriting the frame's flag for
|
|
1640
|
-
// a resolved script would let a user script run a built-in under the strict
|
|
1641
|
-
// tier check — a hard failure on any host that renamed the shipped tiers.
|
|
1642
|
-
const childShippedScript = resolved ? resolved.shippedScript === true : options.shippedScript === true;
|
|
1977
|
+
const childScript = options.loadSavedWorkflow?.(String(nameOrScript)) ?? String(nameOrScript);
|
|
1643
1978
|
const workflowName = String(nameOrScript);
|
|
1644
1979
|
const argsPresent = childArgs !== undefined;
|
|
1645
1980
|
const callHash = hashIdentity({
|
|
@@ -1648,9 +1983,14 @@ export async function runWorkflow<T = unknown>(
|
|
|
1648
1983
|
script: childScript,
|
|
1649
1984
|
argsPresent,
|
|
1650
1985
|
args: argsPresent ? childArgs : null,
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1653
|
-
|
|
1986
|
+
// The table decides which tier the child's calls actually request. A
|
|
1987
|
+
// strength resolved inside *this* frame is already covered per call,
|
|
1988
|
+
// because the agent hash keys on the tier that will be requested. The
|
|
1989
|
+
// child's calls are not: the parent caches one value for the whole child
|
|
1990
|
+
// frame, keyed on the whole-catalogue fingerprint — which does not move
|
|
1991
|
+
// when only this table changed. Without it, editing the table and
|
|
1992
|
+
// resuming would replay a child that had been routed the old way.
|
|
1993
|
+
strengths: canonicalStrengths(strengths),
|
|
1654
1994
|
routingPolicyFingerprint: options.routingPolicyFingerprint ?? null,
|
|
1655
1995
|
});
|
|
1656
1996
|
const cached = options.resumeJournal?.get(`${runId}:${callIndex}`);
|
|
@@ -1671,7 +2011,6 @@ export async function runWorkflow<T = unknown>(
|
|
|
1671
2011
|
const child = await runWorkflow(childScript, {
|
|
1672
2012
|
...options,
|
|
1673
2013
|
args: childArgs,
|
|
1674
|
-
shippedScript: childShippedScript,
|
|
1675
2014
|
sharedRuntime: shared,
|
|
1676
2015
|
sharedStore: store,
|
|
1677
2016
|
workflowDepth: workflowDepth + 1,
|
|
@@ -1724,7 +2063,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1724
2063
|
};
|
|
1725
2064
|
const verify = async (
|
|
1726
2065
|
item: unknown,
|
|
1727
|
-
opts: { reviewers?: number; threshold?: number; lens?: string | string[] } = {},
|
|
2066
|
+
opts: { reviewers?: number; threshold?: number; lens?: string | string[]; strength?: WorkflowStrength } = {},
|
|
1728
2067
|
): Promise<{
|
|
1729
2068
|
real: boolean;
|
|
1730
2069
|
realCount: number;
|
|
@@ -1743,7 +2082,10 @@ export async function runWorkflow<T = unknown>(
|
|
|
1743
2082
|
(_v, i) => () =>
|
|
1744
2083
|
agent(
|
|
1745
2084
|
`Adversarially review whether the following is REAL/correct. Try to refute it; default to real=false if unsure.${lenses.length ? ` Focus lens: ${lenses[i % lenses.length]}.` : ""}\n\n${claim}`,
|
|
1746
|
-
|
|
2085
|
+
// Defaults low: numerous, narrow, and aggregated by the vote
|
|
2086
|
+
// below, which is what carries the gate rather than any single
|
|
2087
|
+
// reviewer's depth. A caller who wants a deeper gate says so.
|
|
2088
|
+
{ label: `verify ${i + 1}`, strength: opts.strength ?? "low", schema: VERIFY_SCHEMA },
|
|
1747
2089
|
),
|
|
1748
2090
|
),
|
|
1749
2091
|
)
|
|
@@ -1764,7 +2106,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1764
2106
|
};
|
|
1765
2107
|
const judgePanel = async (
|
|
1766
2108
|
attempts: unknown[],
|
|
1767
|
-
opts: { judges?: number; rubric?: string } = {},
|
|
2109
|
+
opts: { judges?: number; rubric?: string; strength?: WorkflowStrength } = {},
|
|
1768
2110
|
): Promise<
|
|
1769
2111
|
{ index: number; attempt: unknown; score: number; judgments: Array<{ score: number; reason?: string }> } | undefined
|
|
1770
2112
|
> => {
|
|
@@ -1788,6 +2130,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
1788
2130
|
`Score this candidate from 0 to 1 on: ${rubric}. Reply with the score.\n\nCandidate:\n${text}`,
|
|
1789
2131
|
{
|
|
1790
2132
|
label: `judge ${idx + 1}.${j + 1}`,
|
|
2133
|
+
// Same shape as verify: many narrow scores, aggregated.
|
|
2134
|
+
strength: opts.strength ?? "low",
|
|
1791
2135
|
schema: JUDGE_SCHEMA,
|
|
1792
2136
|
},
|
|
1793
2137
|
),
|
|
@@ -1867,11 +2211,15 @@ export async function runWorkflow<T = unknown>(
|
|
|
1867
2211
|
const completenessCheck = async (
|
|
1868
2212
|
taskArgs: unknown,
|
|
1869
2213
|
results: unknown,
|
|
2214
|
+
opts: { strength?: WorkflowStrength } = {},
|
|
1870
2215
|
): Promise<{ complete: boolean; missing?: string[] } | null> => {
|
|
1871
2216
|
emitRuntimeEvent({ type: "quality", stage: "start", helper: "completenessCheck" });
|
|
1872
2217
|
const verdict = await agent(
|
|
1873
2218
|
`Given the task and the results gathered so far, list what is still MISSING (modalities not covered, claims unverified, gaps). Be specific and concise.\n\nTask:\n${JSON.stringify(taskArgs)}\n\nResults so far:\n${JSON.stringify(results).slice(0, 4000)}`,
|
|
1874
|
-
|
|
2219
|
+
// Defaults medium, not low like the other two gates: one call, no vote to
|
|
2220
|
+
// average over, and an open-ended read of the whole task rather than one
|
|
2221
|
+
// claim, so there is no redundancy here to carry a shallow verdict.
|
|
2222
|
+
{ label: "completeness critic", strength: opts.strength ?? "medium", schema: COMPLETENESS_SCHEMA },
|
|
1875
2223
|
);
|
|
1876
2224
|
emitRuntimeEvent({ type: "quality", stage: "end", helper: "completenessCheck" });
|
|
1877
2225
|
return verdict as { complete: boolean; missing?: string[] } | null;
|
|
@@ -1917,6 +2265,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1917
2265
|
// it is gated on the agent counter + abort (not budget). On resume the
|
|
1918
2266
|
// human's reply replays by callIndex exactly like a cached agent().
|
|
1919
2267
|
const checkpoint = async (promptText: string, checkpointOptions: CheckpointOptions = {}) => {
|
|
2268
|
+
validateCallOptions("checkpoint", checkpointOptions, CHECKPOINT_OPTION_KEYS);
|
|
1920
2269
|
throwIfAborted();
|
|
1921
2270
|
if (typeof promptText !== "string") throw new TypeError("checkpoint(promptText, options?) needs a prompt string");
|
|
1922
2271
|
const callIndex = nextCallIndex();
|