@signalridge/pi-workflows 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +142 -17
- package/package.json +14 -8
- package/skills/workflow-authoring/SKILL.md +30 -0
- package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
- package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
- package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
- package/skills/workflow-authoring/examples/defensive-json-parsing.js +63 -0
- package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
- package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
- package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
- package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
- package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
- package/skills/workflow-authoring/examples/structured-output.js +37 -0
- package/skills/workflow-authoring/examples/tournament.js +81 -0
- package/skills/workflow-authoring/examples/validated-gate.js +63 -0
- package/skills/workflow-authoring/references/capabilities.md +44 -0
- package/skills/workflow-authoring/references/capability-details.md +294 -0
- package/skills/workflow-authoring/references/common-helpers.md +4 -0
- package/skills/workflow-authoring/references/debugging.md +29 -0
- package/skills/workflow-authoring/references/focused-recipes.md +12 -0
- package/skills/workflow-authoring/references/helpers.md +9 -0
- package/skills/workflow-authoring/references/lifecycle.md +37 -0
- package/skills/workflow-authoring/references/pattern-selection.md +15 -0
- package/skills/workflow-authoring/references/quality-helpers.md +8 -0
- package/skills/workflow-authoring/references/registry-ownership.md +17 -0
- package/skills/workflow-authoring/references/retry-helper.md +5 -0
- package/skills/workflow-authoring/references/review.md +43 -0
- package/skills/workflow-authoring/references/runtime.md +29 -0
- package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
- package/skills/workflow-authoring/references/versions.md +13 -0
- package/skills/workflow-patterns/SKILL.md +51 -0
- package/src/arming.ts +69 -0
- package/src/builtins.ts +331 -0
- package/src/capabilities.ts +318 -0
- package/src/code-review-scope.ts +144 -0
- package/src/engine.ts +1158 -2561
- package/src/errors.ts +136 -0
- package/src/index.ts +750 -103
- package/src/journal.ts +594 -1001
- package/src/rpc-client.ts +95 -24
- package/src/runtime.ts +1708 -0
- package/src/saved-workflows.ts +121 -0
- package/src/shared-store.ts +132 -0
- package/src/ui.ts +77 -40
- package/src/workflow-settings.ts +100 -0
- package/src/schema.ts +0 -215
- package/src/state-machine.ts +0 -79
- package/src/synthesis.ts +0 -140
package/src/schema.ts
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
export const WORKFLOW_SCHEMA_VERSION = 1;
|
|
2
|
-
|
|
3
|
-
import type { WorkflowTier } from "@signalridge/pi-subagents-protocol";
|
|
4
|
-
export const MAX_TASKS = 128;
|
|
5
|
-
export const MAX_PHASES = 32;
|
|
6
|
-
export const MAX_TEXT = 100_000;
|
|
7
|
-
export const MAX_SHORT_TEXT = 512;
|
|
8
|
-
|
|
9
|
-
export interface WorkflowPhase {
|
|
10
|
-
id: string;
|
|
11
|
-
title: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface WorkflowTask {
|
|
15
|
-
id: string;
|
|
16
|
-
phase?: string;
|
|
17
|
-
subagent_type: string;
|
|
18
|
-
description: string;
|
|
19
|
-
prompt: string;
|
|
20
|
-
tier?: WorkflowTier;
|
|
21
|
-
depends_on: string[];
|
|
22
|
-
/**
|
|
23
|
-
* Subset of `depends_on` whose journaled results are appended to this task's dispatch prompt.
|
|
24
|
-
* Opt-in: absent means `depends_on` stays a pure ordering barrier, as it always has been.
|
|
25
|
-
*/
|
|
26
|
-
inputs?: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface WorkflowSynthesis {
|
|
30
|
-
subagent_type: string;
|
|
31
|
-
prompt: string;
|
|
32
|
-
tier?: WorkflowTier;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface WorkflowDefinition {
|
|
36
|
-
name: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
/** Default semantic tier for tasks and synthesis in this run. */
|
|
39
|
-
tier?: WorkflowTier;
|
|
40
|
-
phases: WorkflowPhase[];
|
|
41
|
-
tasks: WorkflowTask[];
|
|
42
|
-
synthesis?: WorkflowSynthesis;
|
|
43
|
-
background: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function stringField(value: unknown, label: string, max: number): string {
|
|
51
|
-
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
52
|
-
const normalized = value.trim();
|
|
53
|
-
if (normalized.length === 0) throw new Error(`${label} must not be empty`);
|
|
54
|
-
if (normalized.length > max) throw new Error(`${label} exceeds ${max} characters`);
|
|
55
|
-
return normalized;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function optionalString(value: unknown, label: string, max: number): string | undefined {
|
|
59
|
-
if (value === undefined) return undefined;
|
|
60
|
-
return stringField(value, label, max);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function optionalTier(value: unknown, label: string): WorkflowTier | undefined {
|
|
64
|
-
if (value === undefined) return undefined;
|
|
65
|
-
if (value !== "small" && value !== "medium" && value !== "large") {
|
|
66
|
-
throw new Error(`${label} must be one of small, medium, or large`);
|
|
67
|
-
}
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function assertObjectKeys(value: Record<string, unknown>, allowed: readonly string[], label: string): void {
|
|
72
|
-
const allowedSet = new Set(allowed);
|
|
73
|
-
for (const key of Object.keys(value)) {
|
|
74
|
-
if (!allowedSet.has(key)) throw new Error(`${label} contains unsupported field "${key}"`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function stringArray(value: unknown, label: string, maxItems: number, maxText: number): string[] {
|
|
79
|
-
if (value === undefined) return [];
|
|
80
|
-
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
81
|
-
if (value.length > maxItems) throw new Error(`${label} has too many entries`);
|
|
82
|
-
return value.map((item, index) => stringField(item, `${label}[${index}]`, maxText));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* `inputs` must be a duplicate-free subset of the same task's `depends_on`, so it can never
|
|
87
|
-
* introduce an edge the cycle checker has not already seen.
|
|
88
|
-
*/
|
|
89
|
-
function validateTaskInputs(tasks: WorkflowTask[]): void {
|
|
90
|
-
for (const task of tasks) {
|
|
91
|
-
if (!task.inputs) continue;
|
|
92
|
-
const dependencies = new Set(task.depends_on);
|
|
93
|
-
const seen = new Set<string>();
|
|
94
|
-
for (const input of task.inputs) {
|
|
95
|
-
if (!dependencies.has(input)) {
|
|
96
|
-
throw new Error(`task "${task.id}" input "${input}" is not one of its depends_on`);
|
|
97
|
-
}
|
|
98
|
-
if (seen.has(input)) throw new Error(`task "${task.id}" lists duplicate input "${input}"`);
|
|
99
|
-
seen.add(input);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function validateDag(tasks: WorkflowTask[]): void {
|
|
105
|
-
const ids = new Set(tasks.map((task) => task.id));
|
|
106
|
-
const visiting = new Set<string>();
|
|
107
|
-
const visited = new Set<string>();
|
|
108
|
-
const visit = (id: string): void => {
|
|
109
|
-
if (visiting.has(id)) throw new Error(`workflow dependencies contain a cycle at "${id}"`);
|
|
110
|
-
if (visited.has(id)) return;
|
|
111
|
-
visiting.add(id);
|
|
112
|
-
const task = tasks.find((candidate) => candidate.id === id);
|
|
113
|
-
if (!task) throw new Error(`unknown dependency "${id}"`);
|
|
114
|
-
for (const dependency of task.depends_on) visit(dependency);
|
|
115
|
-
visiting.delete(id);
|
|
116
|
-
visited.add(id);
|
|
117
|
-
};
|
|
118
|
-
for (const task of tasks) {
|
|
119
|
-
for (const dependency of task.depends_on) {
|
|
120
|
-
if (!ids.has(dependency)) throw new Error(`task "${task.id}" depends on unknown task "${dependency}"`);
|
|
121
|
-
}
|
|
122
|
-
visit(task.id);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Validate the strict declarative workflow input and return a normalized copy. */
|
|
127
|
-
export function validateWorkflow(input: unknown): WorkflowDefinition {
|
|
128
|
-
if (!isRecord(input)) throw new Error("workflow must be an object");
|
|
129
|
-
assertObjectKeys(input, ["name", "description", "tier", "phases", "tasks", "synthesis", "background"], "workflow");
|
|
130
|
-
const name = stringField(input.name, "name", MAX_SHORT_TEXT);
|
|
131
|
-
const description = optionalString(input.description, "description", MAX_TEXT);
|
|
132
|
-
const tier = optionalTier(input.tier, "tier");
|
|
133
|
-
|
|
134
|
-
const rawPhases = input.phases === undefined ? [] : input.phases;
|
|
135
|
-
if (!Array.isArray(rawPhases)) throw new Error("phases must be an array");
|
|
136
|
-
if (rawPhases.length > MAX_PHASES) throw new Error(`phases may contain at most ${MAX_PHASES} entries`);
|
|
137
|
-
const phases = rawPhases.map((raw, index) => {
|
|
138
|
-
if (!isRecord(raw)) throw new Error(`phases[${index}] must be an object`);
|
|
139
|
-
assertObjectKeys(raw, ["id", "title"], `phases[${index}]`);
|
|
140
|
-
return {
|
|
141
|
-
id: stringField(raw.id, `phases[${index}].id`, 128),
|
|
142
|
-
title: stringField(raw.title, `phases[${index}].title`, MAX_SHORT_TEXT),
|
|
143
|
-
};
|
|
144
|
-
});
|
|
145
|
-
const phaseIds = new Set<string>();
|
|
146
|
-
for (const phase of phases) {
|
|
147
|
-
if (phaseIds.has(phase.id)) throw new Error(`duplicate phase id "${phase.id}"`);
|
|
148
|
-
phaseIds.add(phase.id);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!Array.isArray(input.tasks)) throw new Error("tasks must be an array");
|
|
152
|
-
if (input.tasks.length === 0) throw new Error("tasks must contain at least one task");
|
|
153
|
-
if (input.tasks.length > MAX_TASKS) throw new Error(`tasks may contain at most ${MAX_TASKS} entries`);
|
|
154
|
-
const taskIds = new Set<string>();
|
|
155
|
-
const tasks = input.tasks.map((raw, index) => {
|
|
156
|
-
if (!isRecord(raw)) throw new Error(`tasks[${index}] must be an object`);
|
|
157
|
-
assertObjectKeys(
|
|
158
|
-
raw,
|
|
159
|
-
["id", "phase", "tier", "subagent_type", "description", "prompt", "depends_on", "inputs"],
|
|
160
|
-
`tasks[${index}]`,
|
|
161
|
-
);
|
|
162
|
-
const task: WorkflowTask = {
|
|
163
|
-
id: stringField(raw.id, `tasks[${index}].id`, 128),
|
|
164
|
-
phase: optionalString(raw.phase, `tasks[${index}].phase`, 128),
|
|
165
|
-
tier: optionalTier(raw.tier, `tasks[${index}].tier`),
|
|
166
|
-
subagent_type: stringField(raw.subagent_type, `tasks[${index}].subagent_type`, 128),
|
|
167
|
-
description: stringField(raw.description, `tasks[${index}].description`, MAX_SHORT_TEXT),
|
|
168
|
-
prompt: stringField(raw.prompt, `tasks[${index}].prompt`, MAX_TEXT),
|
|
169
|
-
depends_on: stringArray(raw.depends_on, `tasks[${index}].depends_on`, MAX_TASKS, 128),
|
|
170
|
-
// Spread so an absent field stays absent and journaled definitions remain byte-stable.
|
|
171
|
-
...(raw.inputs === undefined
|
|
172
|
-
? {}
|
|
173
|
-
: { inputs: stringArray(raw.inputs, `tasks[${index}].inputs`, MAX_TASKS, 128) }),
|
|
174
|
-
};
|
|
175
|
-
if (taskIds.has(task.id)) throw new Error(`duplicate task id "${task.id}"`);
|
|
176
|
-
taskIds.add(task.id);
|
|
177
|
-
if (task.phase !== undefined && !phaseIds.has(task.phase)) {
|
|
178
|
-
throw new Error(`task "${task.id}" references unknown phase "${task.phase}"`);
|
|
179
|
-
}
|
|
180
|
-
return task;
|
|
181
|
-
});
|
|
182
|
-
validateTaskInputs(tasks);
|
|
183
|
-
validateDag(tasks);
|
|
184
|
-
|
|
185
|
-
let synthesis: WorkflowSynthesis | undefined;
|
|
186
|
-
if (input.synthesis !== undefined) {
|
|
187
|
-
if (!isRecord(input.synthesis)) throw new Error("synthesis must be an object");
|
|
188
|
-
assertObjectKeys(input.synthesis, ["subagent_type", "prompt", "tier"], "synthesis");
|
|
189
|
-
const tier = optionalTier(input.synthesis.tier, "synthesis.tier");
|
|
190
|
-
synthesis = {
|
|
191
|
-
...(tier === undefined ? {} : { tier }),
|
|
192
|
-
subagent_type: stringField(input.synthesis.subagent_type, "synthesis.subagent_type", 128),
|
|
193
|
-
prompt: stringField(input.synthesis.prompt, "synthesis.prompt", MAX_TEXT),
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (input.background !== undefined && typeof input.background !== "boolean") {
|
|
198
|
-
throw new Error("background must be a boolean");
|
|
199
|
-
}
|
|
200
|
-
return {
|
|
201
|
-
name,
|
|
202
|
-
...(description === undefined ? {} : { description }),
|
|
203
|
-
...(tier === undefined ? {} : { tier }),
|
|
204
|
-
phases,
|
|
205
|
-
tasks,
|
|
206
|
-
...(synthesis === undefined ? {} : { synthesis }),
|
|
207
|
-
background: input.background ?? false,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function readyTaskIds(definition: WorkflowDefinition, completed: ReadonlySet<string>): string[] {
|
|
212
|
-
return definition.tasks
|
|
213
|
-
.filter((task) => !completed.has(task.id) && task.depends_on.every((dependency) => completed.has(dependency)))
|
|
214
|
-
.map((task) => task.id);
|
|
215
|
-
}
|
package/src/state-machine.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
export type WorkflowStatus =
|
|
2
|
-
| "pending"
|
|
3
|
-
| "running"
|
|
4
|
-
| "pausing"
|
|
5
|
-
| "paused"
|
|
6
|
-
| "synthesizing"
|
|
7
|
-
| "stopping"
|
|
8
|
-
| "completed"
|
|
9
|
-
| "failed"
|
|
10
|
-
| "stopped"
|
|
11
|
-
| "interrupted";
|
|
12
|
-
|
|
13
|
-
export type TaskStatus =
|
|
14
|
-
| "pending"
|
|
15
|
-
| "ready"
|
|
16
|
-
| "dispatching"
|
|
17
|
-
| "queued"
|
|
18
|
-
| "running"
|
|
19
|
-
| "completed"
|
|
20
|
-
| "failed"
|
|
21
|
-
| "stopped"
|
|
22
|
-
| "blocked";
|
|
23
|
-
|
|
24
|
-
const workflowTransitions: Record<WorkflowStatus, readonly WorkflowStatus[]> = {
|
|
25
|
-
pending: ["running", "stopping", "stopped"],
|
|
26
|
-
running: ["pausing", "synthesizing", "stopping", "completed", "failed", "stopped", "interrupted"],
|
|
27
|
-
pausing: ["paused", "stopping", "failed", "stopped", "interrupted"],
|
|
28
|
-
paused: ["running", "stopping", "stopped", "interrupted"],
|
|
29
|
-
synthesizing: ["stopping", "completed", "failed", "stopped", "interrupted"],
|
|
30
|
-
stopping: ["completed", "failed", "stopped"],
|
|
31
|
-
completed: [],
|
|
32
|
-
failed: [],
|
|
33
|
-
stopped: [],
|
|
34
|
-
interrupted: ["running", "stopping", "stopped"],
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Every `WorkflowStatus`, derived from the exhaustive transition table rather than re-listed,
|
|
39
|
-
* so a new union member cannot silently be omitted from status-driven enumerations.
|
|
40
|
-
*/
|
|
41
|
-
export const WORKFLOW_STATUSES: readonly WorkflowStatus[] = Object.keys(workflowTransitions) as WorkflowStatus[];
|
|
42
|
-
|
|
43
|
-
const taskTransitions: Record<TaskStatus, readonly TaskStatus[]> = {
|
|
44
|
-
pending: ["ready", "blocked", "stopped"],
|
|
45
|
-
ready: ["dispatching", "blocked", "stopped"],
|
|
46
|
-
dispatching: ["ready", "queued", "running", "completed", "failed", "stopped", "blocked"],
|
|
47
|
-
queued: ["ready", "running", "completed", "failed", "stopped"],
|
|
48
|
-
running: ["ready", "completed", "failed", "stopped"],
|
|
49
|
-
completed: [],
|
|
50
|
-
failed: [],
|
|
51
|
-
stopped: [],
|
|
52
|
-
blocked: [],
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export function canTransitionWorkflow(from: WorkflowStatus, to: WorkflowStatus): boolean {
|
|
56
|
-
return from === to || workflowTransitions[from].includes(to);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function canTransitionTask(from: TaskStatus, to: TaskStatus): boolean {
|
|
60
|
-
return from === to || taskTransitions[from].includes(to);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function transitionWorkflow(from: WorkflowStatus, to: WorkflowStatus): WorkflowStatus {
|
|
64
|
-
if (!canTransitionWorkflow(from, to)) throw new Error(`invalid workflow transition: ${from} -> ${to}`);
|
|
65
|
-
return to;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function transitionTask(from: TaskStatus, to: TaskStatus): TaskStatus {
|
|
69
|
-
if (!canTransitionTask(from, to)) throw new Error(`invalid task transition: ${from} -> ${to}`);
|
|
70
|
-
return to;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function isTerminalWorkflow(status: WorkflowStatus): boolean {
|
|
74
|
-
return status === "completed" || status === "failed" || status === "stopped";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function isTerminalTask(status: TaskStatus): boolean {
|
|
78
|
-
return status === "completed" || status === "failed" || status === "stopped" || status === "blocked";
|
|
79
|
-
}
|
package/src/synthesis.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type { WorkflowRun, WorkflowTaskResult } from "./journal.js";
|
|
2
|
-
import type { WorkflowDefinition, WorkflowTask } from "./schema.js";
|
|
3
|
-
|
|
4
|
-
export const MAX_TASK_RESULT_CHARS = 6_000;
|
|
5
|
-
export const MAX_SYNTHESIS_INSTRUCTION_CHARS = 8_000;
|
|
6
|
-
export const MAX_SYNTHESIS_INPUT_CHARS = 48_000;
|
|
7
|
-
/** Aggregate budget for a task's `inputs` section; deliberately smaller than synthesis's. */
|
|
8
|
-
export const MAX_TASK_INPUT_CHARS = 24_000;
|
|
9
|
-
/** Must equal `boundedString`'s prompt cap in pi-subagents-protocol, which throws rather than truncating. */
|
|
10
|
-
export const MAX_DISPATCH_PROMPT_CHARS = 100_000;
|
|
11
|
-
/** Below this much headroom an input section carries no useful content, so none is injected. */
|
|
12
|
-
export const MIN_TASK_INPUT_CHARS = 512;
|
|
13
|
-
export const TASK_INPUT_HEADER = "Dependency results (pi-workflows inputs v1):";
|
|
14
|
-
|
|
15
|
-
export function cap(value: string, limit: number): { text: string; truncated: boolean } {
|
|
16
|
-
if (value.length <= limit) return { text: value, truncated: false };
|
|
17
|
-
const marker = "\n…[truncated]";
|
|
18
|
-
const contentLimit = Math.max(0, limit - marker.length);
|
|
19
|
-
return { text: `${value.slice(0, contentLimit)}${marker}`, truncated: true };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ResultEntry {
|
|
23
|
-
task: Pick<WorkflowTask, "id" | "description">;
|
|
24
|
-
result: WorkflowTaskResult | undefined;
|
|
25
|
-
/** Reported as `status=` only when no journaled result exists. */
|
|
26
|
-
fallbackStatus: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface BoundedSectionOptions {
|
|
30
|
-
maxPerResult: number;
|
|
31
|
-
maxTotal: number;
|
|
32
|
-
overflowMarker: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** One labeled, bounded result block. Reads only journaled fields so replays reproduce it exactly. */
|
|
36
|
-
export function buildResultBlock(entry: ResultEntry, maxResultChars = MAX_TASK_RESULT_CHARS): string {
|
|
37
|
-
const { task, result } = entry;
|
|
38
|
-
const bounded = cap(result?.text ?? "(no result)", maxResultChars);
|
|
39
|
-
const pointers = [
|
|
40
|
-
result?.agentId ? `agent_id=${result.agentId}` : "agent_id=unknown",
|
|
41
|
-
result?.outputFile ? `transcript=${result.outputFile}` : undefined,
|
|
42
|
-
result?.tokenCount !== undefined ? `token_count=${result.tokenCount}` : undefined,
|
|
43
|
-
]
|
|
44
|
-
.filter((value): value is string => value !== undefined)
|
|
45
|
-
.join(" ");
|
|
46
|
-
// `result.truncated` matters because `resultFromLifecycle` already caps text to
|
|
47
|
-
// MAX_TASK_RESULT_CHARS, so a re-cap here can never observe the original overflow.
|
|
48
|
-
const truncated = bounded.truncated || result?.truncated === true;
|
|
49
|
-
return [
|
|
50
|
-
`### ${task.id} — ${task.description}`,
|
|
51
|
-
`status=${result?.status ?? entry.fallbackStatus} ${pointers}`,
|
|
52
|
-
truncated ? "result_truncated=true" : "result_truncated=false",
|
|
53
|
-
bounded.text,
|
|
54
|
-
].join("\n");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Append result blocks after `preamble` until the aggregate budget is reached. */
|
|
58
|
-
export function buildBoundedResultSection(
|
|
59
|
-
preamble: readonly string[],
|
|
60
|
-
entries: readonly ResultEntry[],
|
|
61
|
-
options: BoundedSectionOptions,
|
|
62
|
-
): string {
|
|
63
|
-
const chunks: string[] = [...preamble];
|
|
64
|
-
let total = chunks.join("\n").length;
|
|
65
|
-
let aggregateTruncated = false;
|
|
66
|
-
|
|
67
|
-
for (const entry of entries) {
|
|
68
|
-
const block = buildResultBlock(entry, options.maxPerResult);
|
|
69
|
-
if (total + block.length + 2 > options.maxTotal) {
|
|
70
|
-
aggregateTruncated = true;
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
chunks.push("", block);
|
|
74
|
-
total += block.length + 2;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let output = chunks.join("\n");
|
|
78
|
-
if (aggregateTruncated) {
|
|
79
|
-
if (output.length + options.overflowMarker.length <= options.maxTotal) {
|
|
80
|
-
output += options.overflowMarker;
|
|
81
|
-
} else {
|
|
82
|
-
output = `${output.slice(0, options.maxTotal - options.overflowMarker.length)}${options.overflowMarker}`;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return output;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Build bounded, labeled synthesis context without concatenating full transcripts. */
|
|
89
|
-
export function buildSynthesisPrompt(
|
|
90
|
-
definition: WorkflowDefinition,
|
|
91
|
-
run: WorkflowRun,
|
|
92
|
-
tasks: readonly WorkflowTask[] = definition.tasks,
|
|
93
|
-
): string {
|
|
94
|
-
if (!definition.synthesis) throw new Error("workflow has no synthesis definition");
|
|
95
|
-
|
|
96
|
-
const instruction = cap(definition.synthesis.prompt, MAX_SYNTHESIS_INSTRUCTION_CHARS);
|
|
97
|
-
const entries = tasks.map((task) => ({
|
|
98
|
-
task,
|
|
99
|
-
result: run.taskResults[task.id],
|
|
100
|
-
fallbackStatus: String(run.taskStatus[task.id]),
|
|
101
|
-
}));
|
|
102
|
-
const output = buildBoundedResultSection([instruction.text, "", "Workflow task results:"], entries, {
|
|
103
|
-
maxPerResult: MAX_TASK_RESULT_CHARS,
|
|
104
|
-
maxTotal: MAX_SYNTHESIS_INPUT_CHARS,
|
|
105
|
-
overflowMarker: "\n\n[aggregate synthesis input truncated: additional task results omitted]",
|
|
106
|
-
});
|
|
107
|
-
return cap(output, MAX_SYNTHESIS_INPUT_CHARS).text;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function resultFromLifecycle(
|
|
111
|
-
status: "completed" | "failed" | "stopped",
|
|
112
|
-
agentId: string | undefined,
|
|
113
|
-
result: unknown,
|
|
114
|
-
error: unknown,
|
|
115
|
-
compactionCount: number,
|
|
116
|
-
updatedAt = Date.now(),
|
|
117
|
-
outputFile: unknown = undefined,
|
|
118
|
-
tokenCount: unknown = undefined,
|
|
119
|
-
): WorkflowTaskResult {
|
|
120
|
-
const raw = typeof result === "string" ? result : "";
|
|
121
|
-
const bounded = cap(raw, MAX_TASK_RESULT_CHARS);
|
|
122
|
-
const errorText = typeof error === "string" && error.length > 0 ? error.slice(0, 2_000) : undefined;
|
|
123
|
-
const boundedOutputFile =
|
|
124
|
-
typeof outputFile === "string" && outputFile.length > 0 ? outputFile.slice(0, 2_000) : undefined;
|
|
125
|
-
const boundedTokenCount =
|
|
126
|
-
typeof tokenCount === "number" && Number.isFinite(tokenCount) && tokenCount > 0
|
|
127
|
-
? Math.floor(tokenCount)
|
|
128
|
-
: undefined;
|
|
129
|
-
return {
|
|
130
|
-
status,
|
|
131
|
-
...(agentId ? { agentId } : {}),
|
|
132
|
-
...(bounded.text ? { text: bounded.text } : {}),
|
|
133
|
-
...(errorText ? { error: errorText } : {}),
|
|
134
|
-
...(boundedOutputFile ? { outputFile: boundedOutputFile } : {}),
|
|
135
|
-
...(boundedTokenCount === undefined ? {} : { tokenCount: boundedTokenCount }),
|
|
136
|
-
compactionCount,
|
|
137
|
-
...(bounded.truncated ? { truncated: true } : {}),
|
|
138
|
-
updatedAt,
|
|
139
|
-
};
|
|
140
|
-
}
|