@yycholla/pi-dynamic-workflows 3.3.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/LICENSE +22 -0
- package/README.md +325 -0
- package/assets/readme/hero.png +0 -0
- package/assets/readme/hero.svg +70 -0
- package/assets/readme/package-cover.png +0 -0
- package/assets/readme/package-cover.svg +66 -0
- package/assets/readme/workflow.png +0 -0
- package/assets/readme/workflow.svg +77 -0
- package/dist/accept-workflow-guidance.d.ts +14 -0
- package/dist/accept-workflow-guidance.js +53 -0
- package/dist/adversarial-review.d.ts +30 -0
- package/dist/adversarial-review.js +107 -0
- package/dist/agent-history.d.ts +20 -0
- package/dist/agent-history.js +122 -0
- package/dist/agent-registry.d.ts +83 -0
- package/dist/agent-registry.js +190 -0
- package/dist/agent.d.ts +364 -0
- package/dist/agent.js +714 -0
- package/dist/builtin-commands.d.ts +19 -0
- package/dist/builtin-commands.js +251 -0
- package/dist/builtin-workflows.d.ts +45 -0
- package/dist/builtin-workflows.js +121 -0
- package/dist/code-review.d.ts +26 -0
- package/dist/code-review.js +181 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +44 -0
- package/dist/deep-research.d.ts +30 -0
- package/dist/deep-research.js +124 -0
- package/dist/display.d.ts +134 -0
- package/dist/display.js +248 -0
- package/dist/effort-command.d.ts +28 -0
- package/dist/effort-command.js +68 -0
- package/dist/enums.d.ts +69 -0
- package/dist/enums.js +78 -0
- package/dist/errors.d.ts +113 -0
- package/dist/errors.js +140 -0
- package/dist/extension-reload.d.ts +37 -0
- package/dist/extension-reload.js +78 -0
- package/dist/fs-persistence.d.ts +63 -0
- package/dist/fs-persistence.js +102 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +35 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.js +66 -0
- package/dist/model-routing.d.ts +30 -0
- package/dist/model-routing.js +50 -0
- package/dist/model-spec.d.ts +29 -0
- package/dist/model-spec.js +252 -0
- package/dist/model-tier-config.d.ts +133 -0
- package/dist/model-tier-config.js +249 -0
- package/dist/run-persistence.d.ts +180 -0
- package/dist/run-persistence.js +294 -0
- package/dist/saved-commands.d.ts +28 -0
- package/dist/saved-commands.js +100 -0
- package/dist/shared-store.d.ts +98 -0
- package/dist/shared-store.js +212 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/task-panel.d.ts +61 -0
- package/dist/task-panel.js +422 -0
- package/dist/usage-limit-scheduler.d.ts +145 -0
- package/dist/usage-limit-scheduler.js +368 -0
- package/dist/web-tools.d.ts +20 -0
- package/dist/web-tools.js +120 -0
- package/dist/workflow-authoring-coverage.d.ts +70 -0
- package/dist/workflow-authoring-coverage.js +421 -0
- package/dist/workflow-authoring-reference.d.ts +20 -0
- package/dist/workflow-authoring-reference.js +156 -0
- package/dist/workflow-capability-contract.d.ts +131 -0
- package/dist/workflow-capability-contract.js +604 -0
- package/dist/workflow-commands.d.ts +18 -0
- package/dist/workflow-commands.js +260 -0
- package/dist/workflow-comprehension.d.ts +133 -0
- package/dist/workflow-comprehension.js +1321 -0
- package/dist/workflow-context-measurement.d.ts +72 -0
- package/dist/workflow-context-measurement.js +213 -0
- package/dist/workflow-control-tool.d.ts +30 -0
- package/dist/workflow-control-tool.js +176 -0
- package/dist/workflow-delivery-choice.d.ts +20 -0
- package/dist/workflow-delivery-choice.js +48 -0
- package/dist/workflow-editor.d.ts +93 -0
- package/dist/workflow-editor.js +363 -0
- package/dist/workflow-manager.d.ts +492 -0
- package/dist/workflow-manager.js +1124 -0
- package/dist/workflow-paths.d.ts +22 -0
- package/dist/workflow-paths.js +46 -0
- package/dist/workflow-release-gate.d.ts +39 -0
- package/dist/workflow-release-gate.js +309 -0
- package/dist/workflow-saved.d.ts +38 -0
- package/dist/workflow-saved.js +126 -0
- package/dist/workflow-settings.d.ts +70 -0
- package/dist/workflow-settings.js +131 -0
- package/dist/workflow-tool.d.ts +71 -0
- package/dist/workflow-tool.js +367 -0
- package/dist/workflow-ui.d.ts +182 -0
- package/dist/workflow-ui.js +1587 -0
- package/dist/workflow.d.ts +333 -0
- package/dist/workflow.js +1151 -0
- package/dist/workflows-models-command.d.ts +31 -0
- package/dist/workflows-models-command.js +156 -0
- package/dist/worktree.d.ts +25 -0
- package/dist/worktree.js +61 -0
- package/extensions/workflow.ts +151 -0
- package/package.json +104 -0
- 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/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 +41 -0
- package/skills/workflow-authoring/references/capability-details.md +357 -0
- package/skills/workflow-authoring/references/common-helpers.md +4 -0
- package/skills/workflow-authoring/references/debugging.md +27 -0
- package/skills/workflow-authoring/references/focused-recipes.md +11 -0
- package/skills/workflow-authoring/references/helpers.md +9 -0
- package/skills/workflow-authoring/references/lifecycle.md +33 -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 +15 -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 +27 -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/accept-workflow-guidance.ts +71 -0
- package/src/adversarial-review.ts +120 -0
- package/src/agent-history.ts +157 -0
- package/src/agent-registry.ts +221 -0
- package/src/agent.ts +929 -0
- package/src/builtin-commands.ts +286 -0
- package/src/builtin-workflows.ts +155 -0
- package/src/code-review.ts +183 -0
- package/src/config.ts +55 -0
- package/src/deep-research.ts +135 -0
- package/src/display.ts +367 -0
- package/src/effort-command.ts +87 -0
- package/src/enums.ts +77 -0
- package/src/errors.ts +199 -0
- package/src/extension-reload.ts +100 -0
- package/src/fs-persistence.ts +124 -0
- package/src/index.ts +176 -0
- package/src/logger.ts +88 -0
- package/src/model-routing.ts +73 -0
- package/src/model-spec.ts +309 -0
- package/src/model-tier-config.ts +296 -0
- package/src/run-persistence.ts +484 -0
- package/src/saved-commands.ts +115 -0
- package/src/shared-store.ts +228 -0
- package/src/structured-output.ts +47 -0
- package/src/task-panel.ts +490 -0
- package/src/usage-limit-scheduler.ts +432 -0
- package/src/web-tools.ts +124 -0
- package/src/workflow-authoring-coverage.ts +486 -0
- package/src/workflow-authoring-reference.ts +186 -0
- package/src/workflow-capability-contract.ts +806 -0
- package/src/workflow-commands.ts +287 -0
- package/src/workflow-comprehension.ts +1673 -0
- package/src/workflow-context-measurement.ts +262 -0
- package/src/workflow-control-tool.ts +238 -0
- package/src/workflow-delivery-choice.ts +69 -0
- package/src/workflow-editor.ts +444 -0
- package/src/workflow-manager.ts +1405 -0
- package/src/workflow-paths.ts +63 -0
- package/src/workflow-release-gate.ts +529 -0
- package/src/workflow-saved.ts +180 -0
- package/src/workflow-settings.ts +194 -0
- package/src/workflow-tool.ts +464 -0
- package/src/workflow-ui.ts +1789 -0
- package/src/workflow.ts +1615 -0
- package/src/workflows-models-command.ts +211 -0
- package/src/worktree.ts +76 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { createWorkflowTool } from "./workflow-tool.js";
|
|
4
|
+
|
|
5
|
+
/** Package-relative generated context-measurement artifact. */
|
|
6
|
+
export const WORKFLOW_CONTEXT_MEASUREMENT_PATH = "docs/workflow-context-surfaces.json";
|
|
7
|
+
const ROOT = join(import.meta.dirname, "..");
|
|
8
|
+
const SKILL_ROOT = "skills/workflow-authoring";
|
|
9
|
+
const SKILL_PATH = `${SKILL_ROOT}/SKILL.md`;
|
|
10
|
+
|
|
11
|
+
/** Canonical task profiles used for stable byte-based on-demand context measurements. */
|
|
12
|
+
export const WORKFLOW_AUTHORING_PROFILES = [
|
|
13
|
+
{
|
|
14
|
+
name: "write",
|
|
15
|
+
files: [
|
|
16
|
+
SKILL_PATH,
|
|
17
|
+
`${SKILL_ROOT}/references/runtime.md`,
|
|
18
|
+
`${SKILL_ROOT}/references/pattern-selection.md`,
|
|
19
|
+
`${SKILL_ROOT}/references/focused-recipes.md`,
|
|
20
|
+
`${SKILL_ROOT}/examples/fan-out-and-synthesize.js`,
|
|
21
|
+
`${SKILL_ROOT}/examples/structured-output.js`,
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "edit",
|
|
26
|
+
files: [
|
|
27
|
+
SKILL_PATH,
|
|
28
|
+
`${SKILL_ROOT}/references/runtime.md`,
|
|
29
|
+
`${SKILL_ROOT}/references/lifecycle.md`,
|
|
30
|
+
`${SKILL_ROOT}/references/focused-recipes.md`,
|
|
31
|
+
`${SKILL_ROOT}/examples/phased-budgets.js`,
|
|
32
|
+
`${SKILL_ROOT}/examples/saved-nested-workflows.js`,
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "review",
|
|
37
|
+
files: [
|
|
38
|
+
SKILL_PATH,
|
|
39
|
+
`${SKILL_ROOT}/references/runtime.md`,
|
|
40
|
+
`${SKILL_ROOT}/references/review.md`,
|
|
41
|
+
`${SKILL_ROOT}/references/quality-helpers.md`,
|
|
42
|
+
`${SKILL_ROOT}/examples/adversarial-verification.js`,
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "debug",
|
|
47
|
+
files: [
|
|
48
|
+
SKILL_PATH,
|
|
49
|
+
`${SKILL_ROOT}/references/runtime.md`,
|
|
50
|
+
`${SKILL_ROOT}/references/debugging.md`,
|
|
51
|
+
`${SKILL_ROOT}/references/specialized-helpers.md`,
|
|
52
|
+
`${SKILL_ROOT}/examples/validated-gate.js`,
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "loop",
|
|
57
|
+
files: [
|
|
58
|
+
SKILL_PATH,
|
|
59
|
+
`${SKILL_ROOT}/references/runtime.md`,
|
|
60
|
+
`${SKILL_ROOT}/references/pattern-selection.md`,
|
|
61
|
+
`${SKILL_ROOT}/references/lifecycle.md`,
|
|
62
|
+
`${SKILL_ROOT}/references/focused-recipes.md`,
|
|
63
|
+
`${SKILL_ROOT}/examples/loop-until-done.js`,
|
|
64
|
+
`${SKILL_ROOT}/examples/structured-output.js`,
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "retry",
|
|
69
|
+
files: [
|
|
70
|
+
SKILL_PATH,
|
|
71
|
+
`${SKILL_ROOT}/references/runtime.md`,
|
|
72
|
+
`${SKILL_ROOT}/references/retry-helper.md`,
|
|
73
|
+
`${SKILL_ROOT}/references/focused-recipes.md`,
|
|
74
|
+
`${SKILL_ROOT}/examples/bounded-semantic-retry.js`,
|
|
75
|
+
`${SKILL_ROOT}/examples/structured-output.js`,
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
] as const;
|
|
79
|
+
|
|
80
|
+
interface ByteSurface {
|
|
81
|
+
serialization: string;
|
|
82
|
+
bytes: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** One registered skill's always-on discovery-entry byte cost. */
|
|
86
|
+
export interface SkillDiscoverySurface extends ByteSurface {
|
|
87
|
+
root: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Versioned byte measurements for always-on, discovery, corpus, and representative authoring surfaces. */
|
|
91
|
+
export interface WorkflowContextMeasurement {
|
|
92
|
+
formatVersion: 3;
|
|
93
|
+
encoding: "utf8";
|
|
94
|
+
sources: ["src/workflow-tool.ts", "skills/workflow-authoring", "package.json#pi.skills"];
|
|
95
|
+
surfaces: {
|
|
96
|
+
permanentWorkflowPrompt: ByteSurface;
|
|
97
|
+
providerVisibleWorkflowToolDefinition: ByteSurface;
|
|
98
|
+
/**
|
|
99
|
+
* Every skill this package registers (package.json's `pi.skills` — read
|
|
100
|
+
* from disk, not hardcoded here) contributes an always-on discovery entry
|
|
101
|
+
* (name + description) the model sees regardless of whether the skill is
|
|
102
|
+
* ever loaded. This sums all of them, not just workflow-authoring, so a
|
|
103
|
+
* new skill's always-on cost can't silently go untracked.
|
|
104
|
+
*/
|
|
105
|
+
registeredSkillsDiscovery: ByteSurface & { skills: SkillDiscoverySurface[] };
|
|
106
|
+
ordinaryWorkflowOwnedAlwaysOn: ByteSurface;
|
|
107
|
+
workflowAuthoringSkillCorpus: ByteSurface & { files: number };
|
|
108
|
+
representativeAuthoringProfiles: {
|
|
109
|
+
serialization: "sum of UTF-8 bytes for each profile's declared package-relative files";
|
|
110
|
+
medianBytes: number;
|
|
111
|
+
profiles: Array<{ name: string; files: string[]; bytes: number }>;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function bytes(value: string): number {
|
|
117
|
+
return Buffer.byteLength(value, "utf8");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function fileBytes(root: string, path: string): number {
|
|
121
|
+
return bytes(readFileSync(join(root, path), "utf8"));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function skillFiles(root: string): string[] {
|
|
125
|
+
const absoluteRoot = join(root, SKILL_ROOT);
|
|
126
|
+
const pending = [absoluteRoot];
|
|
127
|
+
const files: string[] = [];
|
|
128
|
+
while (pending.length > 0) {
|
|
129
|
+
const directory = pending.pop();
|
|
130
|
+
if (!directory) continue;
|
|
131
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
132
|
+
const absolute = join(directory, entry.name);
|
|
133
|
+
if (entry.isDirectory()) pending.push(absolute);
|
|
134
|
+
else if (entry.isFile()) files.push(relative(root, absolute).replaceAll("\\", "/"));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return files.sort();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Every skill root this package registers, read from package.json's
|
|
142
|
+
* `pi.skills` array rather than hardcoded — so a newly added skill is picked
|
|
143
|
+
* up automatically instead of silently missing from the always-on tally.
|
|
144
|
+
*/
|
|
145
|
+
function registeredSkillRoots(root: string): string[] {
|
|
146
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
|
|
147
|
+
pi?: { skills?: unknown };
|
|
148
|
+
};
|
|
149
|
+
const skills = manifest.pi?.skills;
|
|
150
|
+
if (!Array.isArray(skills) || skills.length === 0 || !skills.every((s) => typeof s === "string")) {
|
|
151
|
+
throw new Error("package.json must declare a non-empty pi.skills array of skill root paths");
|
|
152
|
+
}
|
|
153
|
+
return skills;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function skillDiscoveryEntry(root: string, skillRoot: string): string {
|
|
157
|
+
const skillPath = `${skillRoot}/SKILL.md`;
|
|
158
|
+
const skill = readFileSync(join(root, skillPath), "utf8");
|
|
159
|
+
const name = /^name:\s*(.+)$/m.exec(skill)?.[1]?.trim();
|
|
160
|
+
const description = /^description:\s*(.+)$/m.exec(skill)?.[1]?.trim();
|
|
161
|
+
if (!name || !description) throw new Error(`${skillPath} must declare name and description`);
|
|
162
|
+
return [
|
|
163
|
+
"<skill>",
|
|
164
|
+
` <name>${name}</name>`,
|
|
165
|
+
` <description>${description}</description>`,
|
|
166
|
+
` <location>${skillPath}</location>`,
|
|
167
|
+
"</skill>",
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function median(values: number[]): number {
|
|
172
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
173
|
+
const middle = sorted.length / 2;
|
|
174
|
+
return sorted.length % 2 === 0
|
|
175
|
+
? ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2
|
|
176
|
+
: (sorted[Math.floor(middle)] ?? 0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Measures permanent, discovery, corpus, and canonical on-demand workflow context surfaces. */
|
|
180
|
+
export function measureWorkflowContextSurfaces(root: string = ROOT): WorkflowContextMeasurement {
|
|
181
|
+
const tool = createWorkflowTool();
|
|
182
|
+
const permanentWorkflowPrompt = [
|
|
183
|
+
`- workflow: ${tool.promptSnippet}`,
|
|
184
|
+
...(tool.promptGuidelines ?? []).map((guideline) => `- ${guideline}`),
|
|
185
|
+
].join("\n");
|
|
186
|
+
const providerVisibleWorkflowToolDefinition = JSON.stringify({
|
|
187
|
+
name: tool.name,
|
|
188
|
+
description: tool.description,
|
|
189
|
+
parameters: tool.parameters,
|
|
190
|
+
});
|
|
191
|
+
const skillRoots = registeredSkillRoots(root);
|
|
192
|
+
const skillDiscoverySurfaces: SkillDiscoverySurface[] = skillRoots.map((skillRoot) => ({
|
|
193
|
+
root: skillRoot,
|
|
194
|
+
serialization: "UTF-8 bytes of normalized Pi skill XML with package-relative location",
|
|
195
|
+
bytes: bytes(skillDiscoveryEntry(root, skillRoot)),
|
|
196
|
+
}));
|
|
197
|
+
const registeredSkillsDiscoveryBytes = skillDiscoverySurfaces.reduce((sum, surface) => sum + surface.bytes, 0);
|
|
198
|
+
const corpusFiles = skillFiles(root);
|
|
199
|
+
const corpusBytes = corpusFiles.reduce((sum, path) => sum + fileBytes(root, path), 0);
|
|
200
|
+
const profiles = WORKFLOW_AUTHORING_PROFILES.map((profile) => ({
|
|
201
|
+
name: profile.name,
|
|
202
|
+
files: [...profile.files],
|
|
203
|
+
bytes: profile.files.reduce((sum, path) => sum + fileBytes(root, path), 0),
|
|
204
|
+
}));
|
|
205
|
+
const promptBytes = bytes(permanentWorkflowPrompt);
|
|
206
|
+
const toolBytes = bytes(providerVisibleWorkflowToolDefinition);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
formatVersion: 3,
|
|
210
|
+
encoding: "utf8",
|
|
211
|
+
sources: ["src/workflow-tool.ts", "skills/workflow-authoring", "package.json#pi.skills"],
|
|
212
|
+
surfaces: {
|
|
213
|
+
permanentWorkflowPrompt: {
|
|
214
|
+
serialization: "UTF-8 bytes of LF-joined Pi prompt lines",
|
|
215
|
+
bytes: promptBytes,
|
|
216
|
+
},
|
|
217
|
+
providerVisibleWorkflowToolDefinition: {
|
|
218
|
+
serialization: "UTF-8 bytes of JSON.stringify({ name, description, parameters })",
|
|
219
|
+
bytes: toolBytes,
|
|
220
|
+
},
|
|
221
|
+
registeredSkillsDiscovery: {
|
|
222
|
+
serialization:
|
|
223
|
+
"sum of UTF-8 bytes of normalized Pi skill XML (name + description + location) across every root in package.json's pi.skills",
|
|
224
|
+
bytes: registeredSkillsDiscoveryBytes,
|
|
225
|
+
skills: skillDiscoverySurfaces,
|
|
226
|
+
},
|
|
227
|
+
ordinaryWorkflowOwnedAlwaysOn: {
|
|
228
|
+
serialization:
|
|
229
|
+
"sum of permanent prompt, provider-visible tool definition, and every registered skill's discovery bytes",
|
|
230
|
+
bytes: promptBytes + toolBytes + registeredSkillsDiscoveryBytes,
|
|
231
|
+
},
|
|
232
|
+
workflowAuthoringSkillCorpus: {
|
|
233
|
+
serialization: "sum of UTF-8 bytes for every file under skills/workflow-authoring",
|
|
234
|
+
files: corpusFiles.length,
|
|
235
|
+
bytes: corpusBytes,
|
|
236
|
+
},
|
|
237
|
+
representativeAuthoringProfiles: {
|
|
238
|
+
serialization: "sum of UTF-8 bytes for each profile's declared package-relative files",
|
|
239
|
+
medianBytes: median(profiles.map(({ bytes: profileBytes }) => profileBytes)),
|
|
240
|
+
profiles,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Render the current measurement as deterministic formatted JSON. */
|
|
247
|
+
export function renderWorkflowContextMeasurement(): string {
|
|
248
|
+
return `${JSON.stringify(measureWorkflowContextSurfaces(), null, 2)}\n`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Write the generated measurement under root and return the measured values. */
|
|
252
|
+
export function writeWorkflowContextMeasurement(root: string): WorkflowContextMeasurement {
|
|
253
|
+
const measurement = measureWorkflowContextSurfaces(root);
|
|
254
|
+
writeFileSync(join(root, WORKFLOW_CONTEXT_MEASUREMENT_PATH), `${JSON.stringify(measurement, null, 2)}\n`);
|
|
255
|
+
return measurement;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Report whether committed or supplied measurement JSON matches current package bytes. */
|
|
259
|
+
export function checkWorkflowContextMeasurement(root: string, actual?: string): boolean {
|
|
260
|
+
const committed = actual ?? readFileSync(join(root, WORKFLOW_CONTEXT_MEASUREMENT_PATH), "utf8");
|
|
261
|
+
return committed === `${JSON.stringify(measureWorkflowContextSurfaces(root), null, 2)}\n`;
|
|
262
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import { aggregateAgentUsage, tokenFigures, type WorkflowAgentSnapshot, type WorkflowSnapshot } from "./display.js";
|
|
4
|
+
import type { PersistedRunState, RunStatus } from "./run-persistence.js";
|
|
5
|
+
import type { WorkflowManager } from "./workflow-manager.js";
|
|
6
|
+
|
|
7
|
+
// A tool's top-level parameter schema must be a JSON Schema object (`type:
|
|
8
|
+
// "object"`). A discriminated Type.Union of two objects serializes to a
|
|
9
|
+
// top-level `anyOf` with no `type`, which strict providers (e.g. DeepSeek)
|
|
10
|
+
// reject with "schema must be type object, got type: null". So the schema is a
|
|
11
|
+
// single object: `action` is the full set of verbs and `runId` is optional at
|
|
12
|
+
// the schema level. The per-action requirement (runId is mandatory for every
|
|
13
|
+
// action except `list`, and `list` takes no runId) is enforced at runtime in
|
|
14
|
+
// normalizeInput() and guarded again in execute().
|
|
15
|
+
const workflowControlSchema = Type.Object(
|
|
16
|
+
{
|
|
17
|
+
action: Type.Union(
|
|
18
|
+
[
|
|
19
|
+
Type.Literal("list"),
|
|
20
|
+
Type.Literal("status"),
|
|
21
|
+
Type.Literal("pause"),
|
|
22
|
+
Type.Literal("resume"),
|
|
23
|
+
Type.Literal("stop"),
|
|
24
|
+
],
|
|
25
|
+
{ description: "list = all runs (no runId); status/pause/resume/stop act on one run and require runId." },
|
|
26
|
+
),
|
|
27
|
+
runId: Type.Optional(
|
|
28
|
+
Type.String({
|
|
29
|
+
minLength: 1,
|
|
30
|
+
description: "Canonical workflow run ID. Required for status, pause, resume, and stop; omit for list.",
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
{ additionalProperties: false },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export type WorkflowControlInput = Static<typeof workflowControlSchema>;
|
|
38
|
+
|
|
39
|
+
export interface WorkflowControlToolOptions {
|
|
40
|
+
manager: WorkflowManager;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WorkflowControlRunDetails {
|
|
44
|
+
runId: string;
|
|
45
|
+
workflowName: string;
|
|
46
|
+
status: RunStatus;
|
|
47
|
+
phase: string | null;
|
|
48
|
+
counts: {
|
|
49
|
+
total: number;
|
|
50
|
+
done: number;
|
|
51
|
+
running: number;
|
|
52
|
+
queued: number;
|
|
53
|
+
error: number;
|
|
54
|
+
skipped: number;
|
|
55
|
+
};
|
|
56
|
+
activeLabels: string[];
|
|
57
|
+
tokenTotal: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type ControlResult = {
|
|
61
|
+
content: Array<{ type: "text"; text: string }>;
|
|
62
|
+
details: Record<string, unknown>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function createWorkflowControlTool(
|
|
66
|
+
options: WorkflowControlToolOptions,
|
|
67
|
+
): ToolDefinition<typeof workflowControlSchema, Record<string, unknown>> {
|
|
68
|
+
const manager = options.manager;
|
|
69
|
+
return defineTool({
|
|
70
|
+
name: "workflow_control",
|
|
71
|
+
label: "Workflow Control",
|
|
72
|
+
description:
|
|
73
|
+
"List and inspect workflow runs, or pause, resume, and stop them without asking the user to run slash commands.",
|
|
74
|
+
promptSnippet: "Inspect and manage workflow runs directly by canonical run ID.",
|
|
75
|
+
promptGuidelines: [
|
|
76
|
+
"Use workflow_control for workflow lifecycle management; do not ask the user to type /workflows when this tool can perform the action.",
|
|
77
|
+
"Use stop to terminate or quit a run. Closing the navigator does not stop a run.",
|
|
78
|
+
],
|
|
79
|
+
parameters: workflowControlSchema,
|
|
80
|
+
prepareArguments: normalizeInput,
|
|
81
|
+
async execute(_toolCallId, params) {
|
|
82
|
+
if (params.action === "list") {
|
|
83
|
+
const runs = manager.listRuns();
|
|
84
|
+
const summaries = runs.map((run) => summarizeRun(run, manager.getSnapshot(run.runId)));
|
|
85
|
+
return result(
|
|
86
|
+
summaries.length
|
|
87
|
+
? `action=list result=ok runs=${summaries.length}\n${summaries.map(formatRun).join("\n")}`
|
|
88
|
+
: "action=list result=ok runs=0",
|
|
89
|
+
{ action: "list", result: "ok", runs: summaries },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// runId is optional in the schema (see workflowControlSchema) but required
|
|
94
|
+
// for every non-list action; normalizeInput already enforces this, and this
|
|
95
|
+
// guard both narrows the type and returns a structured error if a model
|
|
96
|
+
// somehow calls a run action without one.
|
|
97
|
+
if (!params.runId) return controlError(params.action, "", "runId is required for this action", ["list"]);
|
|
98
|
+
const run = findRun(manager, params.runId);
|
|
99
|
+
if (!run) return controlError(params.action, params.runId, "run not found", ["list"]);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
switch (params.action) {
|
|
103
|
+
case "status": {
|
|
104
|
+
const summary = summarizeRun(run, manager.getSnapshot(run.runId));
|
|
105
|
+
return result(`action=status result=ok ${formatRun(summary)}`, {
|
|
106
|
+
action: "status",
|
|
107
|
+
result: "ok",
|
|
108
|
+
run: summary,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
case "pause":
|
|
112
|
+
if (!manager.pause(run.runId)) return invalidTransition("pause", run);
|
|
113
|
+
return actionSuccess("pause", "paused", currentSummary(manager, run));
|
|
114
|
+
case "resume":
|
|
115
|
+
if (!(await manager.resume(run.runId))) return invalidTransition("resume", run);
|
|
116
|
+
return actionSuccess("resume", "resumed", currentSummary(manager, run));
|
|
117
|
+
case "stop":
|
|
118
|
+
if (!manager.stop(run.runId)) return invalidTransition("stop", run);
|
|
119
|
+
return actionSuccess("stop", "stopped", currentSummary(manager, run));
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
// A transient persistence I/O error (or any unexpected throw from the
|
|
123
|
+
// manager) shouldn't surface as a raw stack trace to the model — report
|
|
124
|
+
// it via the tool's normal structured error shape instead.
|
|
125
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
126
|
+
return controlError(params.action, run.runId, message, allowedActions(run.status));
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeInput(value: unknown): WorkflowControlInput {
|
|
133
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
134
|
+
throw new Error("workflow_control requires an object argument");
|
|
135
|
+
}
|
|
136
|
+
const input = value as Record<string, unknown>;
|
|
137
|
+
const actions = new Set(["list", "status", "pause", "resume", "stop"]);
|
|
138
|
+
if (typeof input.action !== "string" || !actions.has(input.action)) {
|
|
139
|
+
throw new Error("workflow_control requires action: list|status|pause|resume|stop");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const allowedKeys = input.action === "list" ? new Set(["action"]) : new Set(["action", "runId"]);
|
|
143
|
+
const extraKey = Object.keys(input).find((key) => !allowedKeys.has(key));
|
|
144
|
+
if (extraKey) throw new Error(`workflow_control action "${input.action}" does not accept ${extraKey}`);
|
|
145
|
+
|
|
146
|
+
if (input.action !== "list" && (typeof input.runId !== "string" || !input.runId.trim())) {
|
|
147
|
+
throw new Error(`workflow_control action "${input.action}" requires runId`);
|
|
148
|
+
}
|
|
149
|
+
return input as WorkflowControlInput;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function result(text: string, details: Record<string, unknown>): ControlResult {
|
|
153
|
+
return { content: [{ type: "text", text }], details };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function findRun(manager: WorkflowManager, runId: string): PersistedRunState | undefined {
|
|
157
|
+
return manager.listRuns().find((candidate) => candidate.runId === runId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function currentSummary(manager: WorkflowManager, fallback: PersistedRunState): WorkflowControlRunDetails {
|
|
161
|
+
const current = findRun(manager, fallback.runId) ?? fallback;
|
|
162
|
+
return summarizeRun(current, manager.getSnapshot(current.runId));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function actionSuccess(action: string, actionResult: string, run: WorkflowControlRunDetails): ControlResult {
|
|
166
|
+
return result(`action=${action} result=${actionResult} ${formatRun(run)}`, {
|
|
167
|
+
action,
|
|
168
|
+
result: actionResult,
|
|
169
|
+
run,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function invalidTransition(action: string, run: PersistedRunState): ControlResult {
|
|
174
|
+
return controlError(action, run.runId, `cannot ${action} run with status ${run.status}`, allowedActions(run.status));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function controlError(action: string, runId: string, message: string, allowed: string[]): ControlResult {
|
|
178
|
+
return result(
|
|
179
|
+
`action=${action} result=error runId=${runId} error=${message} allowed=${allowed.join(",") || "none"}`,
|
|
180
|
+
{ action, result: "error", runId, error: message, allowedActions: allowed },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function allowedActions(status: RunStatus): string[] {
|
|
185
|
+
switch (status) {
|
|
186
|
+
case "running":
|
|
187
|
+
return ["status", "pause", "stop"];
|
|
188
|
+
case "paused":
|
|
189
|
+
return ["status", "resume", "stop"];
|
|
190
|
+
case "failed":
|
|
191
|
+
case "pending":
|
|
192
|
+
return ["status", "resume"];
|
|
193
|
+
case "completed":
|
|
194
|
+
case "aborted":
|
|
195
|
+
return ["status"];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function summarizeRun(run: PersistedRunState, live?: WorkflowSnapshot | null): WorkflowControlRunDetails {
|
|
200
|
+
const agents = live?.agents ?? run.agents;
|
|
201
|
+
const counts = countAgents(agents);
|
|
202
|
+
const liveUsage = tokenFigures(live?.tokenUsage);
|
|
203
|
+
const persistedUsage = tokenFigures(run.tokenUsage);
|
|
204
|
+
const agentUsage = aggregateAgentUsage(agents);
|
|
205
|
+
return {
|
|
206
|
+
runId: run.runId,
|
|
207
|
+
workflowName: live?.name ?? run.workflowName,
|
|
208
|
+
status: run.status,
|
|
209
|
+
phase: live?.currentPhase ?? run.currentPhase ?? null,
|
|
210
|
+
counts,
|
|
211
|
+
activeLabels: agents.filter((agent) => agent.status === "running").map((agent) => agent.label),
|
|
212
|
+
tokenTotal: Math.max(
|
|
213
|
+
liveUsage.fresh + liveUsage.cacheRead,
|
|
214
|
+
persistedUsage.fresh + persistedUsage.cacheRead,
|
|
215
|
+
agentUsage.fresh + agentUsage.cacheRead,
|
|
216
|
+
),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function countAgents(agents: Array<Pick<WorkflowAgentSnapshot, "status">>): WorkflowControlRunDetails["counts"] {
|
|
221
|
+
return {
|
|
222
|
+
total: agents.length,
|
|
223
|
+
done: agents.filter((agent) => agent.status === "done").length,
|
|
224
|
+
running: agents.filter((agent) => agent.status === "running").length,
|
|
225
|
+
queued: agents.filter((agent) => agent.status === "queued").length,
|
|
226
|
+
error: agents.filter((agent) => agent.status === "error").length,
|
|
227
|
+
skipped: agents.filter((agent) => agent.status === "skipped").length,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function formatRun(run: WorkflowControlRunDetails): string {
|
|
232
|
+
const active = run.activeLabels.join(",") || "-";
|
|
233
|
+
return `runId=${run.runId} name=${quote(run.workflowName)} status=${run.status} phase=${quote(run.phase ?? "-")} total=${run.counts.total} done=${run.counts.done} running=${run.counts.running} queued=${run.counts.queued} error=${run.counts.error} skipped=${run.counts.skipped} active=${quote(active)} tokens=${run.tokenTotal}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function quote(value: string): string {
|
|
237
|
+
return JSON.stringify(value);
|
|
238
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** One model-facing timing decision for invoking the workflow tool. */
|
|
2
|
+
export interface WorkflowDeliveryChoiceScenario {
|
|
3
|
+
id: string;
|
|
4
|
+
prompt: string;
|
|
5
|
+
expectedBackground: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Pure scoring result for one captured workflow tool invocation. */
|
|
9
|
+
export interface WorkflowDeliveryChoiceEvaluation {
|
|
10
|
+
passed: boolean;
|
|
11
|
+
resolvedBackground: boolean | null;
|
|
12
|
+
assertions: Array<{ name: string; passed: boolean; details: string }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Focused #89 scenarios for default background delivery versus same-turn use. */
|
|
16
|
+
export const WORKFLOW_DELIVERY_CHOICE_SCENARIOS: readonly WorkflowDeliveryChoiceScenario[] = [
|
|
17
|
+
{
|
|
18
|
+
id: "background-delivery",
|
|
19
|
+
prompt:
|
|
20
|
+
"Run a workflow to audit the repository from several independent angles. I do not need the result in this turn; let it be delivered back later so I can keep working.",
|
|
21
|
+
expectedBackground: true,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "inline-result",
|
|
25
|
+
prompt:
|
|
26
|
+
"Run a workflow to compare the two proposed designs, then use its result in your answer in this same turn. I am waiting for the result before you respond.",
|
|
27
|
+
expectedBackground: false,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** Score captured workflow arguments without executing the submitted workflow. */
|
|
32
|
+
export function evaluateWorkflowDeliveryChoice(
|
|
33
|
+
scenario: WorkflowDeliveryChoiceScenario,
|
|
34
|
+
value: unknown,
|
|
35
|
+
): WorkflowDeliveryChoiceEvaluation {
|
|
36
|
+
const input = isRecord(value) ? value : null;
|
|
37
|
+
const hasScript = typeof input?.script === "string" && input.script.trim().length > 0;
|
|
38
|
+
const validBackground = input !== null && (input.background === undefined || typeof input.background === "boolean");
|
|
39
|
+
const resolvedBackground = validBackground ? (typeof input.background === "boolean" ? input.background : true) : null;
|
|
40
|
+
const timingMatches = resolvedBackground === scenario.expectedBackground;
|
|
41
|
+
const assertions = [
|
|
42
|
+
{
|
|
43
|
+
name: "workflow:called-with-script",
|
|
44
|
+
passed: hasScript,
|
|
45
|
+
details: "the model must invoke the real workflow tool shape with a nonblank script",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "background:valid-type",
|
|
49
|
+
passed: validBackground,
|
|
50
|
+
details: "background must be omitted for its true default or supplied as a boolean",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "background:matches-user-timing",
|
|
54
|
+
passed: timingMatches,
|
|
55
|
+
details: scenario.expectedBackground
|
|
56
|
+
? "later delivery should use the default background run"
|
|
57
|
+
: "same-turn use should pass background: false",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
return {
|
|
61
|
+
passed: assertions.every(({ passed }) => passed),
|
|
62
|
+
resolvedBackground,
|
|
63
|
+
assertions,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
68
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
69
|
+
}
|