@signalridge/pi-workflows 1.4.0 → 1.6.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 +249 -0
- package/README.md +193 -15
- package/package.json +3 -3
- package/skills/workflow-authoring/SKILL.md +23 -7
- package/skills/workflow-authoring/references/capabilities.md +6 -6
- package/skills/workflow-authoring/references/capability-details.md +16 -6
- package/skills/workflow-authoring/references/debugging.md +2 -2
- package/skills/workflow-authoring/references/quality-helpers.md +2 -2
- package/skills/workflow-authoring/references/registry-ownership.md +6 -4
- package/skills/workflow-authoring/references/review.md +6 -2
- package/skills/workflow-authoring/references/runtime.md +3 -3
- 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 +1 -1
- package/src/builtins.ts +23 -23
- package/src/capabilities.ts +28 -6
- package/src/engine.ts +248 -58
- package/src/index.ts +457 -173
- package/src/journal.ts +278 -253
- package/src/rpc-client.ts +37 -48
- package/src/runtime.ts +429 -54
- package/src/strengths.ts +93 -0
- package/src/ui.ts +29 -4
- package/src/workflow-settings.ts +95 -1
package/src/runtime.ts
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import vm from "node:vm";
|
|
24
|
+
import type { ManagedRoutingPolicy } from "@signalridge/pi-subagents-protocol";
|
|
25
|
+
import { agentTierPolicyIdentity, isManagedAgentTier } from "@signalridge/pi-subagents-protocol";
|
|
24
26
|
import { parse } from "acorn";
|
|
25
27
|
import { checkRuntimeBindings } from "./capabilities.js";
|
|
26
28
|
import { WorkflowError, WorkflowErrorCode, wrapError } from "./errors.js";
|
|
@@ -31,30 +33,25 @@ import {
|
|
|
31
33
|
type OrchestrationTaskContext,
|
|
32
34
|
} from "./orchestrator.js";
|
|
33
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";
|
|
34
43
|
|
|
35
44
|
// ── Meta contract ────────────────────────────────────────────────────────────
|
|
36
45
|
|
|
37
46
|
export interface WorkflowMetaPhase {
|
|
38
47
|
title: string;
|
|
39
48
|
detail?: string;
|
|
40
|
-
/**
|
|
41
|
-
* Phase-level model preference — currently validated but not routed.
|
|
42
|
-
* Per-call model routing requires a protocol extension (see README
|
|
43
|
-
* "Intentional divergences"); use agent({ tier }) instead.
|
|
44
|
-
*/
|
|
45
|
-
model?: string;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export interface WorkflowMeta {
|
|
49
52
|
name: string;
|
|
50
53
|
description: string;
|
|
51
54
|
phases?: WorkflowMetaPhase[];
|
|
52
|
-
/**
|
|
53
|
-
* Run-level model preference — currently validated but not routed.
|
|
54
|
-
* Per-call model routing requires a protocol extension (see README
|
|
55
|
-
* "Intentional divergences"); use agent({ tier }) instead.
|
|
56
|
-
*/
|
|
57
|
-
model?: string;
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
/** A resolved, journaled agent() call. */
|
|
@@ -65,6 +62,8 @@ export interface JournalEntry {
|
|
|
65
62
|
hash: string;
|
|
66
63
|
result: unknown;
|
|
67
64
|
storeDelta?: Record<string, unknown>;
|
|
65
|
+
/** Agent tier this call resolved to, as the host reported it. */
|
|
66
|
+
tier?: string;
|
|
68
67
|
/** Generation for a nested workflow replay boundary. */
|
|
69
68
|
generation?: number;
|
|
70
69
|
/** Logical agents represented by a nested workflow replay boundary. */
|
|
@@ -91,6 +90,24 @@ export interface SharedRuntime {
|
|
|
91
90
|
runFatalController: AbortController;
|
|
92
91
|
inFlight: Set<Promise<unknown>>;
|
|
93
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>;
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
/** Counting semaphore with a FIFO resolve queue. */
|
|
@@ -108,7 +125,6 @@ export interface AgentRunOptions {
|
|
|
108
125
|
schema?: unknown;
|
|
109
126
|
signal?: AbortSignal;
|
|
110
127
|
instructions?: string;
|
|
111
|
-
model?: string;
|
|
112
128
|
tier?: string;
|
|
113
129
|
cwd?: string;
|
|
114
130
|
/** Named subagent type; resolved by pi-subagents at dispatch. */
|
|
@@ -129,7 +145,14 @@ export interface AgentRunOptions {
|
|
|
129
145
|
/** Additional tool names denied for this call. */
|
|
130
146
|
excludeTools?: string[];
|
|
131
147
|
onModelResolved?: (id: string) => void;
|
|
132
|
-
|
|
148
|
+
/**
|
|
149
|
+
* The tier the host actually selected, which may be one this call did not
|
|
150
|
+
* name — an agent's own tier, or the host's default. A callback rather than a
|
|
151
|
+
* mutation of `tier`: the dispatcher does not own this object, and reading a
|
|
152
|
+
* reply back out of an input field would only work while every layer happened
|
|
153
|
+
* to preserve its identity.
|
|
154
|
+
*/
|
|
155
|
+
onTierResolved?: (tier: string) => void;
|
|
133
156
|
onUsage?: (usage: AgentUsage) => void;
|
|
134
157
|
onHistory?: (history: unknown[]) => void;
|
|
135
158
|
}
|
|
@@ -138,8 +161,25 @@ export interface AgentOptions {
|
|
|
138
161
|
label?: string;
|
|
139
162
|
phase?: string;
|
|
140
163
|
schema?: unknown;
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Workflow strength: how much effort this step deserves, `low`/`medium`/`high`.
|
|
166
|
+
*
|
|
167
|
+
* The only model control a workflow has and the only routing word it speaks.
|
|
168
|
+
* There is no per-call model, thinking, or Agent tier, so a script cannot pin
|
|
169
|
+
* a vendor or a catalogue key the host did not choose. Which Agent tier a
|
|
170
|
+
* strength runs on is the user's `strengths` table and nothing else — a
|
|
171
|
+
* strength that table does not define dispatches with no tier at all and
|
|
172
|
+
* takes the agent's ordinary default.
|
|
173
|
+
*
|
|
174
|
+
* There is no default. Omitting it dispatches with no tier at all, the same
|
|
175
|
+
* as an unmapped strength and the same as any ordinary Agent spawn: a script
|
|
176
|
+
* that named no strength stated no opinion, and the host's own precedence —
|
|
177
|
+
* the agent type's frontmatter tier before the configured default — is the
|
|
178
|
+
* right answer to that. A default would have outranked the agent's own tier
|
|
179
|
+
* on every unlabelled call, which is the one thing this indirection exists to
|
|
180
|
+
* avoid doing.
|
|
181
|
+
*/
|
|
182
|
+
strength?: WorkflowStrength;
|
|
143
183
|
isolation?: "worktree";
|
|
144
184
|
agentType?: string;
|
|
145
185
|
timeoutMs?: number | null;
|
|
@@ -167,6 +207,53 @@ export interface WorkflowRunOptions {
|
|
|
167
207
|
agent?: WorkflowAgentRunner;
|
|
168
208
|
/** The session's main model (provider/id), shown for default agents. */
|
|
169
209
|
mainModel?: string;
|
|
210
|
+
/** The host's Agent-tier catalogue, captured fresh for this start/resume. */
|
|
211
|
+
routingPolicy?: ManagedRoutingPolicy;
|
|
212
|
+
/**
|
|
213
|
+
* The user's strength → Agent tier table: the whole of workflow model routing.
|
|
214
|
+
*
|
|
215
|
+
* A script names a strength and nothing else. This table is the only thing
|
|
216
|
+
* that binds one to an Agent tier, so a strength it does not define reaches
|
|
217
|
+
* dispatch carrying no tier and takes the agent's ordinary default — the
|
|
218
|
+
* agent's own frontmatter tier, then the host's configured default.
|
|
219
|
+
*
|
|
220
|
+
* Omitted entirely is not "no mappings": it means {@link defaultStrengthTable},
|
|
221
|
+
* which starts each strength on the catalogue tier of the same name wherever
|
|
222
|
+
* this host defines one. That is a default *table* and not a fallback rule,
|
|
223
|
+
* and the difference is what keeps re-pricing possible. A rule would be
|
|
224
|
+
* unremovable, so making a 26-agent fan-out cheaper would still mean editing
|
|
225
|
+
* the tier itself — dragging the Explore agent and everything else that names
|
|
226
|
+
* it. A table is replaced by writing one, and a strength the written table
|
|
227
|
+
* omits stays unmapped even on a host that defines a tier of the same name.
|
|
228
|
+
*
|
|
229
|
+
* Values are keys in the host's one catalogue and nothing else: pi-subagents
|
|
230
|
+
* still owns every model, every thinking level, and the only
|
|
231
|
+
* `resolveAgentTier()`. No entry may ever grow a `model` or `thinking` field
|
|
232
|
+
* — that is the line between this and the retired `workflow.tiers` key, which
|
|
233
|
+
* carried both and so became a second model policy behind a second resolver.
|
|
234
|
+
*
|
|
235
|
+
* One hop: a strength resolves to a tier, and that tier is never looked up as
|
|
236
|
+
* a strength in turn.
|
|
237
|
+
*
|
|
238
|
+
* Read fresh for each start and resume, never frozen onto the run, so editing
|
|
239
|
+
* the table and resuming re-routes the calls that have not run.
|
|
240
|
+
*
|
|
241
|
+
* That is a re-spend, not a free redirect. A call's resume identity keys on
|
|
242
|
+
* the tier it requests, so every *finished* call whose strength moved misses
|
|
243
|
+
* its journal entry — and resume replays only the longest unchanged prefix, so
|
|
244
|
+
* the first such miss takes everything after it live as well. Re-routing a run
|
|
245
|
+
* that is mostly done costs roughly the run. Start a new one instead when the
|
|
246
|
+
* point of the edit was to spend less.
|
|
247
|
+
*/
|
|
248
|
+
strengths?: WorkflowStrengthTable;
|
|
249
|
+
/**
|
|
250
|
+
* Whole-catalogue identity, used wherever a per-tier one would be dishonest:
|
|
251
|
+
* a nested `workflow()` boundary, where the parent caches one value for a
|
|
252
|
+
* child whose per-call tiers it cannot see, and an `agent()` call that names
|
|
253
|
+
* no tier, whose real tier may come from the agent's frontmatter. A call that
|
|
254
|
+
* names its tier keys on that tier's policy instead.
|
|
255
|
+
*/
|
|
256
|
+
routingPolicyFingerprint?: string;
|
|
170
257
|
/** Engine-owned execution nonce used to isolate stale nested managed calls. */
|
|
171
258
|
executionNonce?: string;
|
|
172
259
|
/** Runtime frame depth; siblings inherit the same depth and may run concurrently. */
|
|
@@ -190,16 +277,25 @@ export interface WorkflowRunOptions {
|
|
|
190
277
|
sharedRuntime?: SharedRuntime;
|
|
191
278
|
initialTokenUsage?: AgentUsage;
|
|
192
279
|
sharedStore?: SharedStore;
|
|
280
|
+
/** Resolve a nested `workflow(name)` reference to the script it names. */
|
|
193
281
|
loadSavedWorkflow?: (name: string) => string | undefined;
|
|
194
282
|
confirm?: (promptText: string, options: CheckpointOptions) => Promise<unknown>;
|
|
195
283
|
onLog?: (message: string) => void;
|
|
196
284
|
onPhase?: (title: string) => void;
|
|
197
285
|
onRuntimeEvent?: (event: WorkflowRuntimeEvent) => void;
|
|
198
|
-
onAgentStart?: (event: {
|
|
286
|
+
onAgentStart?: (event: {
|
|
287
|
+
id: string;
|
|
288
|
+
label: string;
|
|
289
|
+
phase?: string;
|
|
290
|
+
prompt: string;
|
|
291
|
+
model?: string;
|
|
292
|
+
tier?: string;
|
|
293
|
+
}) => void;
|
|
199
294
|
onAgentEnd?: (event: {
|
|
200
295
|
id: string;
|
|
201
296
|
label: string;
|
|
202
297
|
phase?: string;
|
|
298
|
+
tier?: string;
|
|
203
299
|
result: unknown;
|
|
204
300
|
tokens?: number;
|
|
205
301
|
tokenUsage?: AgentUsage;
|
|
@@ -473,26 +569,29 @@ function propertyKey(node: unknown, path: string): string {
|
|
|
473
569
|
|
|
474
570
|
function validateMeta(meta: unknown): asserts meta is WorkflowMeta {
|
|
475
571
|
if (!meta || typeof meta !== "object") throw new Error("meta must be an object");
|
|
476
|
-
const value = meta as WorkflowMeta
|
|
572
|
+
const value = meta as WorkflowMeta & Record<string, unknown>;
|
|
477
573
|
if (typeof value.name !== "string" || !value.name.trim() || value.name.length > 512)
|
|
478
574
|
throw new Error("meta.name must be a non-empty string of at most 512 characters");
|
|
479
575
|
if (typeof value.description !== "string" || !value.description.trim() || value.description.length > 100_000)
|
|
480
576
|
throw new Error("meta.description must be a non-empty string of at most 100000 characters");
|
|
481
|
-
if (
|
|
577
|
+
if (Object.hasOwn(value, "model") || Object.hasOwn(value, "thinking")) {
|
|
578
|
+
throw new Error("workflow meta model/thinking fields are unsupported; use agent({ strength })");
|
|
579
|
+
}
|
|
482
580
|
if (value.phases !== undefined) {
|
|
483
581
|
if (!Array.isArray(value.phases)) throw new Error("meta.phases must be an array");
|
|
484
582
|
for (const phase of value.phases) {
|
|
485
583
|
if (
|
|
486
584
|
!phase ||
|
|
487
585
|
typeof phase !== "object" ||
|
|
488
|
-
typeof phase.title !== "string" ||
|
|
489
|
-
!phase.title.trim() ||
|
|
490
|
-
phase.title.length > 512
|
|
586
|
+
typeof (phase as { title?: unknown }).title !== "string" ||
|
|
587
|
+
!(phase as { title: string }).title.trim() ||
|
|
588
|
+
(phase as { title: string }).title.length > 512
|
|
491
589
|
) {
|
|
492
590
|
throw new Error("each meta phase must have a title string");
|
|
493
591
|
}
|
|
494
|
-
|
|
495
|
-
|
|
592
|
+
const phaseRecord = phase as unknown as Record<string, unknown>;
|
|
593
|
+
if (Object.hasOwn(phaseRecord, "model") || Object.hasOwn(phaseRecord, "thinking")) {
|
|
594
|
+
throw new Error("workflow phase model/thinking fields are unsupported; use agent({ strength })");
|
|
496
595
|
}
|
|
497
596
|
}
|
|
498
597
|
}
|
|
@@ -524,27 +623,62 @@ function hashIdentity(value: unknown): string {
|
|
|
524
623
|
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
525
624
|
}
|
|
526
625
|
|
|
626
|
+
/**
|
|
627
|
+
* The strength vocabulary as a stable value, for hashing.
|
|
628
|
+
*
|
|
629
|
+
* Sorted with a code-unit comparison rather than `localeCompare` for the reason
|
|
630
|
+
* `getRoutingPolicySnapshot` gives about `blockedProfiles`: this reaches an
|
|
631
|
+
* identity hash, and a locale-dependent order would let one table hash two ways
|
|
632
|
+
* on two machines, or on one machine after its locale changed.
|
|
633
|
+
*/
|
|
634
|
+
function canonicalStrengths(strengths: ReadonlyMap<string, string>): [string, string][] | null {
|
|
635
|
+
if (strengths.size === 0) return null;
|
|
636
|
+
return [...strengths].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
637
|
+
}
|
|
638
|
+
|
|
527
639
|
/**
|
|
528
640
|
* Stable identity hash for an agent() call. Must cover every input that can
|
|
529
641
|
* change the outcome so a cache-miss is never served on resume. The agent
|
|
530
|
-
* definition is represented by the agentType name
|
|
531
|
-
* in pi-subagents
|
|
642
|
+
* definition is represented by the agentType name — the definition itself lives
|
|
643
|
+
* in pi-subagents, so a name-only key is the honest observable, and there is no
|
|
644
|
+
* second agent-definition field to hash beside it.
|
|
532
645
|
*/
|
|
533
646
|
function hashAgentCall(
|
|
534
647
|
prompt: string,
|
|
535
|
-
model: string | undefined,
|
|
536
648
|
phase: string | undefined,
|
|
537
649
|
options: AgentOptions,
|
|
538
|
-
|
|
650
|
+
tier: string | undefined,
|
|
651
|
+
routingPolicy: ManagedRoutingPolicy | undefined,
|
|
652
|
+
routingPolicyFingerprint: string | undefined,
|
|
539
653
|
): string {
|
|
654
|
+
// Keyed on the tier this call will actually request, never on the strength
|
|
655
|
+
// that chose it. The strength is a name for a routing decision; the tier is
|
|
656
|
+
// the decision, so two runs whose tables differ but land on the same tier
|
|
657
|
+
// must replay, and one whose table moved this strength elsewhere must not.
|
|
658
|
+
//
|
|
659
|
+
// A call that resolved to a tier is keyed on that tier's policy alone, so
|
|
660
|
+
// defining or editing an unrelated tier cannot invalidate it.
|
|
661
|
+
//
|
|
662
|
+
// A call whose strength is unmapped resolves to no tier and cannot be keyed
|
|
663
|
+
// that way, and keying it on `defaultTier` would be worse than coarse — it
|
|
664
|
+
// would be wrong. The host resolves an untiered call against the agent's own
|
|
665
|
+
// frontmatter tier first and only then the default, and frontmatter is
|
|
666
|
+
// invisible here: the agent definition lives in pi-subagents and reaches this
|
|
667
|
+
// side as a name. Keyed on the default, editing the tier the agent actually
|
|
668
|
+
// declares would replay stale work, while editing the default would
|
|
669
|
+
// invalidate work that never touched it. The whole-catalogue fingerprint is
|
|
670
|
+
// the coarse answer that is at least sound, and it is stable across the
|
|
671
|
+
// original run and its resume, which the tier the host selected is not — that
|
|
672
|
+
// one is only known after dispatch, and this hash is computed before it.
|
|
673
|
+
const tierPolicy =
|
|
674
|
+
tier === undefined ? (routingPolicyFingerprint ?? null) : agentTierPolicyIdentity(routingPolicy, tier);
|
|
540
675
|
return hashIdentity({
|
|
541
676
|
prompt,
|
|
542
|
-
|
|
543
|
-
|
|
677
|
+
tier: tier ?? null,
|
|
678
|
+
tierPolicy,
|
|
544
679
|
phase: phase ?? null,
|
|
545
680
|
agentType: options.agentType ?? null,
|
|
546
681
|
thread: options.thread ?? null,
|
|
547
|
-
agentDef: agentDefKey,
|
|
548
682
|
schema: options.schema ?? null,
|
|
549
683
|
isolation: options.isolation ?? null,
|
|
550
684
|
toolset: options.toolset ?? null,
|
|
@@ -573,6 +707,104 @@ function hashCheckpoint(promptText: string, options: CheckpointOptions): string
|
|
|
573
707
|
|
|
574
708
|
// ── Small helpers ────────────────────────────────────────────────────────────
|
|
575
709
|
|
|
710
|
+
/**
|
|
711
|
+
* Every key {@link AgentOptions} accepts, and every key {@link CheckpointOptions}
|
|
712
|
+
* does.
|
|
713
|
+
*
|
|
714
|
+
* Spelled out because an interface leaves no runtime value to enumerate, and a
|
|
715
|
+
* script is JavaScript evaluated in a VM, so the compiler never sees the object
|
|
716
|
+
* these guard. The `satisfies` clause is what keeps the list honest anyway: a
|
|
717
|
+
* `Record` of the interface's keys is missing-key-checked, and the excess
|
|
718
|
+
* property check on the literal catches the other direction — so adding an
|
|
719
|
+
* option without listing it here, or listing one that no longer exists, is a
|
|
720
|
+
* type error rather than a silently unreachable option.
|
|
721
|
+
*/
|
|
722
|
+
const AGENT_OPTION_KEYS: ReadonlySet<string> = new Set(
|
|
723
|
+
Object.keys({
|
|
724
|
+
label: 0,
|
|
725
|
+
phase: 0,
|
|
726
|
+
schema: 0,
|
|
727
|
+
strength: 0,
|
|
728
|
+
isolation: 0,
|
|
729
|
+
agentType: 0,
|
|
730
|
+
timeoutMs: 0,
|
|
731
|
+
retries: 0,
|
|
732
|
+
thread: 0,
|
|
733
|
+
toolset: 0,
|
|
734
|
+
excludeTools: 0,
|
|
735
|
+
} satisfies Record<keyof AgentOptions, unknown>),
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
const CHECKPOINT_OPTION_KEYS: ReadonlySet<string> = new Set(
|
|
739
|
+
Object.keys({
|
|
740
|
+
default: 0,
|
|
741
|
+
headless: 0,
|
|
742
|
+
kind: 0,
|
|
743
|
+
choices: 0,
|
|
744
|
+
timeoutMs: 0,
|
|
745
|
+
} satisfies Record<keyof CheckpointOptions, unknown>),
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Keys that used to mean something, or that a script author reaches for out of
|
|
750
|
+
* habit, answered by name instead of by the generic list.
|
|
751
|
+
*
|
|
752
|
+
* `tier` was this package's own option before a strength stood between a script
|
|
753
|
+
* and the host catalogue. `model`/`thinking` never were — `meta` and phase
|
|
754
|
+
* profiles have always rejected them — but an author who tries them on `meta`
|
|
755
|
+
* and gets a clear refusal will try them here next, and the two surfaces should
|
|
756
|
+
* answer the same way.
|
|
757
|
+
*/
|
|
758
|
+
const RETIRED_AGENT_OPTIONS: Readonly<Record<string, string>> = {
|
|
759
|
+
tier: `name a strength instead: one of ${WORKFLOW_STRENGTH_LIST}`,
|
|
760
|
+
model: "a strength is the only model policy a workflow can express",
|
|
761
|
+
thinking: "a strength is the only model policy a workflow can express",
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Refuse an options object carrying a key this runtime does not read.
|
|
766
|
+
*
|
|
767
|
+
* Fails closed because the alternative is not "harmless": an unknown key is
|
|
768
|
+
* dropped before the call hash is built, so it changes neither the dispatch nor
|
|
769
|
+
* the resume identity — the run simply spends at a policy nobody chose, and a
|
|
770
|
+
* resume replays it without ever noticing. `agent({ strenght: "low" })` would
|
|
771
|
+
* quietly run every step untiered; `checkpoint(p, { headles: "abort" })` would
|
|
772
|
+
* quietly auto-approve in headless mode instead of aborting. Neither shows up
|
|
773
|
+
* in a log.
|
|
774
|
+
*
|
|
775
|
+
* A script is authored against this contract and evaluated as plain JavaScript,
|
|
776
|
+
* so a typo has no other gate to catch it. The closed vocabulary made a
|
|
777
|
+
* misspelled *strength* fail; this is the same argument one level up, for the
|
|
778
|
+
* option name itself.
|
|
779
|
+
*/
|
|
780
|
+
function validateCallOptions(
|
|
781
|
+
callee: "agent" | "checkpoint",
|
|
782
|
+
options: unknown,
|
|
783
|
+
allowed: ReadonlySet<string>,
|
|
784
|
+
retired: Readonly<Record<string, string>> = {},
|
|
785
|
+
): void {
|
|
786
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
787
|
+
throw new WorkflowError(
|
|
788
|
+
`${callee}() options must be a plain object (received ${
|
|
789
|
+
Array.isArray(options) ? "an array" : JSON.stringify(options)
|
|
790
|
+
})`,
|
|
791
|
+
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
792
|
+
{ recoverable: false },
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
for (const key of Object.keys(options)) {
|
|
796
|
+
if (allowed.has(key)) continue;
|
|
797
|
+
const hint = retired[key];
|
|
798
|
+
throw new WorkflowError(
|
|
799
|
+
`${callee}() received an unknown option ${JSON.stringify(key)}; ${
|
|
800
|
+
hint ?? `valid options are: ${[...allowed].join(", ")}`
|
|
801
|
+
}`,
|
|
802
|
+
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
803
|
+
{ recoverable: false },
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
576
808
|
function defaultAgentLabel(phase: string | undefined, index: number): string {
|
|
577
809
|
return phase ? `${phase} agent ${index}` : `agent ${index}`;
|
|
578
810
|
}
|
|
@@ -930,6 +1162,14 @@ export async function runWorkflow<T = unknown>(
|
|
|
930
1162
|
const agentTimeoutMs = normalizeAgentTimeout(options.agentTimeoutMs);
|
|
931
1163
|
const runId = options.runId ?? `run-${started.toString(36)}`;
|
|
932
1164
|
const baseCwd = options.cwd ?? process.cwd();
|
|
1165
|
+
// The host's catalogue, known before the first call. A script cannot name a
|
|
1166
|
+
// tier, so this is not a typo check: it is what the strength table is resolved
|
|
1167
|
+
// against at run start — an entry pointing at a tier this host does not define
|
|
1168
|
+
// is dropped there, and an absent table becomes the same-named default only
|
|
1169
|
+
// for the tiers listed here. Undefined when no policy was supplied (in-process
|
|
1170
|
+
// tests), which leaves a configured table unchecked and the default empty.
|
|
1171
|
+
// Only the top frame reads it; a nested one inherits the resolved table.
|
|
1172
|
+
const knownTiers = options.routingPolicy ? new Set(Object.keys(options.routingPolicy.profiles)) : undefined;
|
|
933
1173
|
|
|
934
1174
|
const state: RuntimeState = {
|
|
935
1175
|
logs: [],
|
|
@@ -953,6 +1193,58 @@ export async function runWorkflow<T = unknown>(
|
|
|
953
1193
|
) - 2,
|
|
954
1194
|
);
|
|
955
1195
|
|
|
1196
|
+
const log = (message: unknown) => {
|
|
1197
|
+
const text = String(message);
|
|
1198
|
+
state.logs.push(text);
|
|
1199
|
+
options.onLog?.(text);
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* The strength table this run will honor, target-checked once at start.
|
|
1204
|
+
*
|
|
1205
|
+
* Checked here rather than per call so a stale entry is reported once, at the
|
|
1206
|
+
* point where the fix is obvious, instead of once per dispatch. An entry whose
|
|
1207
|
+
* tier this host does not define is dropped rather than fatal: the table is a
|
|
1208
|
+
* routing preference, and a name that went stale should cost the redirect, not
|
|
1209
|
+
* every workflow on the machine. A dropped entry leaves its strength unmapped,
|
|
1210
|
+
* which is the ordinary unconfigured case — the call takes the agent's own
|
|
1211
|
+
* default, exactly as it would on a machine that configured nothing.
|
|
1212
|
+
*
|
|
1213
|
+
* An entry keyed on a word outside the vocabulary is dropped in silence. The
|
|
1214
|
+
* settings layer already refuses to store one, so reaching here means a
|
|
1215
|
+
* hand-edited file, and the run is the wrong place to teach the vocabulary.
|
|
1216
|
+
*
|
|
1217
|
+
* No configured table at all is not "no mappings": it takes
|
|
1218
|
+
* {@link defaultStrengthTable}, whose entries are drawn from this host's own
|
|
1219
|
+
* catalogue and so can never be stale — which is why only a configured table
|
|
1220
|
+
* ever reaches the complaint above. An explicit `{}` is the way to say "no
|
|
1221
|
+
* mappings"; the settings layer stores that one and drops only a table whose
|
|
1222
|
+
* every entry was invalid, so a broken file cannot pass for that statement.
|
|
1223
|
+
*
|
|
1224
|
+
* Resolved once at the top frame and shared, so a nested `workflow()` cannot
|
|
1225
|
+
* re-derive the same answer and repeat the same complaint at its boundary.
|
|
1226
|
+
*/
|
|
1227
|
+
const resolveStrengths = (): ReadonlyMap<string, string> => {
|
|
1228
|
+
const configured = options.strengths;
|
|
1229
|
+
if (configured === undefined) return new Map(Object.entries(defaultStrengthTable(knownTiers)));
|
|
1230
|
+
const active = new Map<string, string>();
|
|
1231
|
+
// Re-checked despite the parameter type: this is a public entry point, and
|
|
1232
|
+
// a JS caller reaches it without one. The settings layer runs the same two
|
|
1233
|
+
// guards at read, so a table coming from there passes both twice.
|
|
1234
|
+
for (const [strength, tier] of Object.entries(configured)) {
|
|
1235
|
+
if (!isWorkflowStrength(strength) || !isManagedAgentTier(tier)) continue;
|
|
1236
|
+
if (knownTiers && !knownTiers.has(tier)) {
|
|
1237
|
+
log(
|
|
1238
|
+
`workflow strength ${JSON.stringify(strength)} → tier ${JSON.stringify(tier)} ignored; ` +
|
|
1239
|
+
`this host defines: ${[...knownTiers].sort().join(", ") || "(none)"}`,
|
|
1240
|
+
);
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
active.set(strength, tier);
|
|
1244
|
+
}
|
|
1245
|
+
return active;
|
|
1246
|
+
};
|
|
1247
|
+
|
|
956
1248
|
// Global caps + budget shared with any nested workflow() so they hold across nesting.
|
|
957
1249
|
const shared: SharedRuntime = options.sharedRuntime ?? {
|
|
958
1250
|
limiter: createLimiter(concurrency),
|
|
@@ -965,6 +1257,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
965
1257
|
runFatalController: new AbortController(),
|
|
966
1258
|
inFlight: new Set<Promise<unknown>>(),
|
|
967
1259
|
activeThreads: new Set<string>(),
|
|
1260
|
+
routedStrengths: new Set<string>(),
|
|
1261
|
+
strengths: resolveStrengths(),
|
|
968
1262
|
};
|
|
969
1263
|
const limiter = shared.limiter;
|
|
970
1264
|
const isTopLevelRun = !options.sharedRuntime;
|
|
@@ -973,10 +1267,22 @@ export async function runWorkflow<T = unknown>(
|
|
|
973
1267
|
/** agent() promises owned by this runWorkflow frame only. */
|
|
974
1268
|
const frameInFlight = new Set<Promise<unknown>>();
|
|
975
1269
|
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1270
|
+
const strengths = shared.strengths;
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Report each strength's routing once per run.
|
|
1274
|
+
*
|
|
1275
|
+
* A fan-out asks for the same strength dozens of times and the answer is the
|
|
1276
|
+
* same every time; a line per dispatch would bury the run's own log under it.
|
|
1277
|
+
*/
|
|
1278
|
+
const logStrengthRouting = (strength: WorkflowStrength, tier: string | undefined): void => {
|
|
1279
|
+
if (shared.routedStrengths.has(strength)) return;
|
|
1280
|
+
shared.routedStrengths.add(strength);
|
|
1281
|
+
log(
|
|
1282
|
+
tier === undefined
|
|
1283
|
+
? `workflow strength ${JSON.stringify(strength)} is unmapped; using the agent's own default`
|
|
1284
|
+
: `workflow strength ${JSON.stringify(strength)} runs on agent tier ${JSON.stringify(tier)}`,
|
|
1285
|
+
);
|
|
980
1286
|
};
|
|
981
1287
|
|
|
982
1288
|
const emitRuntimeEvent = (event: WorkflowRuntimeEvent): void => {
|
|
@@ -1036,6 +1342,9 @@ export async function runWorkflow<T = unknown>(
|
|
|
1036
1342
|
};
|
|
1037
1343
|
|
|
1038
1344
|
const agent = (promptText: string, agentOptions: AgentOptions = {}): Promise<unknown> => {
|
|
1345
|
+
// First, ahead of the thread bookkeeping below: a malformed options object
|
|
1346
|
+
// must not be able to register a thread it will never release.
|
|
1347
|
+
validateCallOptions("agent", agentOptions, AGENT_OPTION_KEYS, RETIRED_AGENT_OPTIONS);
|
|
1039
1348
|
const rawThread = agentOptions.thread;
|
|
1040
1349
|
const thread = rawThread === undefined ? undefined : typeof rawThread === "string" ? rawThread.trim() : "";
|
|
1041
1350
|
if (rawThread !== undefined && !thread) {
|
|
@@ -1090,16 +1399,41 @@ export async function runWorkflow<T = unknown>(
|
|
|
1090
1399
|
log(`agentType "${agentOptions.agentType}" resolves in pi-subagents at dispatch`);
|
|
1091
1400
|
}
|
|
1092
1401
|
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
// the
|
|
1097
|
-
|
|
1098
|
-
|
|
1402
|
+
// The vocabulary is closed, so a word outside it is a typo rather than a
|
|
1403
|
+
// strength nobody configured, and saying so here beats dispatching in
|
|
1404
|
+
// silence. This is the whole of the check: unlike a tier key, a strength is
|
|
1405
|
+
// ours, so we can answer for it without asking the host anything.
|
|
1406
|
+
if (agentOptions.strength !== undefined && !isWorkflowStrength(agentOptions.strength)) {
|
|
1407
|
+
throw new WorkflowError(
|
|
1408
|
+
`agent strength must be one of ${WORKFLOW_STRENGTH_LIST} (received ${JSON.stringify(agentOptions.strength)})`,
|
|
1409
|
+
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
1410
|
+
{ recoverable: false },
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
// The strength becomes an Agent tier here and only here, through the user's
|
|
1414
|
+
// table and nothing else. Unmapped resolves to no tier — the call takes the
|
|
1415
|
+
// agent's own default, and no catalogue key is ever inferred from a matching
|
|
1416
|
+
// spelling. One hop: the tier this yields is never looked up as a strength
|
|
1417
|
+
// in turn. There is no catalogue check left to do, because the only tier
|
|
1418
|
+
// that can appear here already passed one at run start.
|
|
1419
|
+
//
|
|
1420
|
+
// A call that named no strength takes that same untiered path rather than a
|
|
1421
|
+
// default rung: see the note in strengths.ts on why a default would have
|
|
1422
|
+
// outranked the agent type's own tier on every unlabelled dispatch.
|
|
1423
|
+
const strength = agentOptions.strength;
|
|
1424
|
+
const effectiveTier = strength === undefined ? undefined : strengths.get(strength);
|
|
1425
|
+
if (strength !== undefined) logStrengthRouting(strength, effectiveTier);
|
|
1099
1426
|
|
|
1100
1427
|
// Deterministic resume key: assigned at lexical call time, before the limiter.
|
|
1101
1428
|
const callIndex = nextCallIndex();
|
|
1102
|
-
const callHash = hashAgentCall(
|
|
1429
|
+
const callHash = hashAgentCall(
|
|
1430
|
+
promptText,
|
|
1431
|
+
assignedPhase,
|
|
1432
|
+
agentOptions,
|
|
1433
|
+
effectiveTier,
|
|
1434
|
+
options.routingPolicy,
|
|
1435
|
+
options.routingPolicyFingerprint,
|
|
1436
|
+
);
|
|
1103
1437
|
// Store delta key: runId-namespaced so a nested workflow() call's
|
|
1104
1438
|
// callIndex-0 can never collide with the parent's (see SharedStore doc).
|
|
1105
1439
|
const deltaKey = `${runId}:${callIndex}`;
|
|
@@ -1117,11 +1451,22 @@ export async function runWorkflow<T = unknown>(
|
|
|
1117
1451
|
if (hashMatches && !cachedEmptyOutput && callIndex < state.firstMiss) {
|
|
1118
1452
|
shared.agentCount++;
|
|
1119
1453
|
frameAgentCount++;
|
|
1120
|
-
|
|
1454
|
+
// The journal holds the tier the host actually selected, which may be a
|
|
1455
|
+
// default the script never named. Replay reports that durable identity
|
|
1456
|
+
// rather than re-deriving one from the script.
|
|
1457
|
+
const replayTier = cached.tier ?? effectiveTier;
|
|
1458
|
+
options.onAgentStart?.({
|
|
1459
|
+
id: deltaKey,
|
|
1460
|
+
label,
|
|
1461
|
+
phase: assignedPhase,
|
|
1462
|
+
prompt: promptText,
|
|
1463
|
+
...(replayTier === undefined ? {} : { tier: replayTier }),
|
|
1464
|
+
});
|
|
1121
1465
|
options.onAgentEnd?.({
|
|
1122
1466
|
id: deltaKey,
|
|
1123
1467
|
label,
|
|
1124
1468
|
phase: assignedPhase,
|
|
1469
|
+
tier: replayTier,
|
|
1125
1470
|
result: cached.result,
|
|
1126
1471
|
tokens: 0,
|
|
1127
1472
|
});
|
|
@@ -1166,7 +1511,13 @@ export async function runWorkflow<T = unknown>(
|
|
|
1166
1511
|
const retryAttempts = normalizeAgentRetries(agentOptions.retries ?? options.agentRetries ?? 0);
|
|
1167
1512
|
const maxAttempts = retryAttempts + 1;
|
|
1168
1513
|
|
|
1169
|
-
options.onAgentStart?.({
|
|
1514
|
+
options.onAgentStart?.({
|
|
1515
|
+
id: deltaKey,
|
|
1516
|
+
label,
|
|
1517
|
+
phase: assignedPhase,
|
|
1518
|
+
prompt: promptText,
|
|
1519
|
+
...(effectiveTier === undefined ? {} : { tier: effectiveTier }),
|
|
1520
|
+
});
|
|
1170
1521
|
|
|
1171
1522
|
// Worktree creation and cleanup remain owned by pi-subagents; the request
|
|
1172
1523
|
// carries the explicit per-call policy without touching the host filesystem here.
|
|
@@ -1206,6 +1557,9 @@ export async function runWorkflow<T = unknown>(
|
|
|
1206
1557
|
const externalSignal = options.signal;
|
|
1207
1558
|
let onExternalAbort: (() => void) | undefined;
|
|
1208
1559
|
let onRunFatal: (() => void) | undefined;
|
|
1560
|
+
// Filled in from the host's reply, which may name a tier this call
|
|
1561
|
+
// did not: an agent's own tier, or the configured default.
|
|
1562
|
+
let resolvedTier = effectiveTier;
|
|
1209
1563
|
try {
|
|
1210
1564
|
throwIfAborted();
|
|
1211
1565
|
if (batch?.cancelled) throw agentLimitError();
|
|
@@ -1238,8 +1592,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1238
1592
|
schema,
|
|
1239
1593
|
signal: agentController.signal,
|
|
1240
1594
|
instructions: buildAgentInstructions(assignedPhase, agentOptions, resolvedIsolation),
|
|
1241
|
-
|
|
1242
|
-
tier: agentOptions.tier,
|
|
1595
|
+
tier: effectiveTier,
|
|
1243
1596
|
agentType: agentOptions.agentType,
|
|
1244
1597
|
isolation: resolvedIsolation,
|
|
1245
1598
|
toolset: options.toolset ?? agentOptions.toolset,
|
|
@@ -1252,6 +1605,9 @@ export async function runWorkflow<T = unknown>(
|
|
|
1252
1605
|
onModelResolved: (id: string) => {
|
|
1253
1606
|
void id; // display model tracking is engine-side
|
|
1254
1607
|
},
|
|
1608
|
+
onTierResolved: (tier: string) => {
|
|
1609
|
+
resolvedTier = tier;
|
|
1610
|
+
},
|
|
1255
1611
|
onUsage: (u: AgentUsage) => {
|
|
1256
1612
|
usage = u;
|
|
1257
1613
|
},
|
|
@@ -1313,6 +1669,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1313
1669
|
runId,
|
|
1314
1670
|
hash: callHash,
|
|
1315
1671
|
result,
|
|
1672
|
+
...(resolvedTier === undefined ? {} : { tier: resolvedTier }),
|
|
1316
1673
|
storeDelta: store.commitDelta(deltaKey),
|
|
1317
1674
|
});
|
|
1318
1675
|
options.onAgentEnd?.({
|
|
@@ -1320,9 +1677,9 @@ export async function runWorkflow<T = unknown>(
|
|
|
1320
1677
|
label,
|
|
1321
1678
|
phase: assignedPhase,
|
|
1322
1679
|
result,
|
|
1680
|
+
tier: resolvedTier,
|
|
1323
1681
|
tokens,
|
|
1324
1682
|
tokenUsage: usage,
|
|
1325
|
-
model: modelSpec,
|
|
1326
1683
|
});
|
|
1327
1684
|
return result;
|
|
1328
1685
|
} catch (error) {
|
|
@@ -1350,9 +1707,9 @@ export async function runWorkflow<T = unknown>(
|
|
|
1350
1707
|
label,
|
|
1351
1708
|
phase: assignedPhase,
|
|
1352
1709
|
result: null,
|
|
1710
|
+
tier: resolvedTier,
|
|
1353
1711
|
tokens,
|
|
1354
1712
|
tokenUsage: usage,
|
|
1355
|
-
model: modelSpec,
|
|
1356
1713
|
error: workflowError.message,
|
|
1357
1714
|
errorCode: workflowError.code,
|
|
1358
1715
|
recoverable: workflowError.recoverable,
|
|
@@ -1511,8 +1868,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1511
1868
|
});
|
|
1512
1869
|
}
|
|
1513
1870
|
const callIndex = nextCallIndex();
|
|
1514
|
-
const
|
|
1515
|
-
const childScript = resolved ?? String(nameOrScript);
|
|
1871
|
+
const childScript = options.loadSavedWorkflow?.(String(nameOrScript)) ?? String(nameOrScript);
|
|
1516
1872
|
const workflowName = String(nameOrScript);
|
|
1517
1873
|
const argsPresent = childArgs !== undefined;
|
|
1518
1874
|
const callHash = hashIdentity({
|
|
@@ -1521,6 +1877,15 @@ export async function runWorkflow<T = unknown>(
|
|
|
1521
1877
|
script: childScript,
|
|
1522
1878
|
argsPresent,
|
|
1523
1879
|
args: argsPresent ? childArgs : null,
|
|
1880
|
+
// The table decides which tier the child's calls actually request. A
|
|
1881
|
+
// strength resolved inside *this* frame is already covered per call,
|
|
1882
|
+
// because the agent hash keys on the tier that will be requested. The
|
|
1883
|
+
// child's calls are not: the parent caches one value for the whole child
|
|
1884
|
+
// frame, keyed on the whole-catalogue fingerprint — which does not move
|
|
1885
|
+
// when only this table changed. Without it, editing the table and
|
|
1886
|
+
// resuming would replay a child that had been routed the old way.
|
|
1887
|
+
strengths: canonicalStrengths(strengths),
|
|
1888
|
+
routingPolicyFingerprint: options.routingPolicyFingerprint ?? null,
|
|
1524
1889
|
});
|
|
1525
1890
|
const cached = options.resumeJournal?.get(`${runId}:${callIndex}`);
|
|
1526
1891
|
if (cached != null && cached.hash === callHash && callIndex < state.firstMiss) {
|
|
@@ -1592,7 +1957,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1592
1957
|
};
|
|
1593
1958
|
const verify = async (
|
|
1594
1959
|
item: unknown,
|
|
1595
|
-
opts: { reviewers?: number; threshold?: number; lens?: string | string[] } = {},
|
|
1960
|
+
opts: { reviewers?: number; threshold?: number; lens?: string | string[]; strength?: WorkflowStrength } = {},
|
|
1596
1961
|
): Promise<{
|
|
1597
1962
|
real: boolean;
|
|
1598
1963
|
realCount: number;
|
|
@@ -1611,7 +1976,10 @@ export async function runWorkflow<T = unknown>(
|
|
|
1611
1976
|
(_v, i) => () =>
|
|
1612
1977
|
agent(
|
|
1613
1978
|
`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}`,
|
|
1614
|
-
|
|
1979
|
+
// Defaults low: numerous, narrow, and aggregated by the vote
|
|
1980
|
+
// below, which is what carries the gate rather than any single
|
|
1981
|
+
// reviewer's depth. A caller who wants a deeper gate says so.
|
|
1982
|
+
{ label: `verify ${i + 1}`, strength: opts.strength ?? "low", schema: VERIFY_SCHEMA },
|
|
1615
1983
|
),
|
|
1616
1984
|
),
|
|
1617
1985
|
)
|
|
@@ -1632,7 +2000,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1632
2000
|
};
|
|
1633
2001
|
const judgePanel = async (
|
|
1634
2002
|
attempts: unknown[],
|
|
1635
|
-
opts: { judges?: number; rubric?: string } = {},
|
|
2003
|
+
opts: { judges?: number; rubric?: string; strength?: WorkflowStrength } = {},
|
|
1636
2004
|
): Promise<
|
|
1637
2005
|
{ index: number; attempt: unknown; score: number; judgments: Array<{ score: number; reason?: string }> } | undefined
|
|
1638
2006
|
> => {
|
|
@@ -1656,6 +2024,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
1656
2024
|
`Score this candidate from 0 to 1 on: ${rubric}. Reply with the score.\n\nCandidate:\n${text}`,
|
|
1657
2025
|
{
|
|
1658
2026
|
label: `judge ${idx + 1}.${j + 1}`,
|
|
2027
|
+
// Same shape as verify: many narrow scores, aggregated.
|
|
2028
|
+
strength: opts.strength ?? "low",
|
|
1659
2029
|
schema: JUDGE_SCHEMA,
|
|
1660
2030
|
},
|
|
1661
2031
|
),
|
|
@@ -1735,11 +2105,15 @@ export async function runWorkflow<T = unknown>(
|
|
|
1735
2105
|
const completenessCheck = async (
|
|
1736
2106
|
taskArgs: unknown,
|
|
1737
2107
|
results: unknown,
|
|
2108
|
+
opts: { strength?: WorkflowStrength } = {},
|
|
1738
2109
|
): Promise<{ complete: boolean; missing?: string[] } | null> => {
|
|
1739
2110
|
emitRuntimeEvent({ type: "quality", stage: "start", helper: "completenessCheck" });
|
|
1740
2111
|
const verdict = await agent(
|
|
1741
2112
|
`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)}`,
|
|
1742
|
-
|
|
2113
|
+
// Defaults medium, not low like the other two gates: one call, no vote to
|
|
2114
|
+
// average over, and an open-ended read of the whole task rather than one
|
|
2115
|
+
// claim, so there is no redundancy here to carry a shallow verdict.
|
|
2116
|
+
{ label: "completeness critic", strength: opts.strength ?? "medium", schema: COMPLETENESS_SCHEMA },
|
|
1743
2117
|
);
|
|
1744
2118
|
emitRuntimeEvent({ type: "quality", stage: "end", helper: "completenessCheck" });
|
|
1745
2119
|
return verdict as { complete: boolean; missing?: string[] } | null;
|
|
@@ -1785,6 +2159,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1785
2159
|
// it is gated on the agent counter + abort (not budget). On resume the
|
|
1786
2160
|
// human's reply replays by callIndex exactly like a cached agent().
|
|
1787
2161
|
const checkpoint = async (promptText: string, checkpointOptions: CheckpointOptions = {}) => {
|
|
2162
|
+
validateCallOptions("checkpoint", checkpointOptions, CHECKPOINT_OPTION_KEYS);
|
|
1788
2163
|
throwIfAborted();
|
|
1789
2164
|
if (typeof promptText !== "string") throw new TypeError("checkpoint(promptText, options?) needs a prompt string");
|
|
1790
2165
|
const callIndex = nextCallIndex();
|