pi-extensible-workflows 1.0.1 → 3.0.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.
Files changed (47) hide show
  1. package/README.md +9 -2
  2. package/dist/src/agent-execution.d.ts +13 -14
  3. package/dist/src/agent-execution.js +110 -197
  4. package/dist/src/budget.d.ts +38 -0
  5. package/dist/src/budget.js +160 -0
  6. package/dist/src/cli.d.ts +9 -0
  7. package/dist/src/cli.js +536 -6
  8. package/dist/src/doctor.d.ts +4 -4
  9. package/dist/src/doctor.js +9 -29
  10. package/dist/src/execution.d.ts +17 -0
  11. package/dist/src/execution.js +630 -0
  12. package/dist/src/herdr.d.ts +12 -0
  13. package/dist/src/herdr.js +74 -0
  14. package/dist/src/host.d.ts +62 -0
  15. package/dist/src/host.js +2696 -0
  16. package/dist/src/index.d.ts +11 -557
  17. package/dist/src/index.js +8 -3974
  18. package/dist/src/persistence.d.ts +32 -19
  19. package/dist/src/persistence.js +310 -70
  20. package/dist/src/registry.d.ts +31 -0
  21. package/dist/src/registry.js +169 -0
  22. package/dist/src/session-inspector.d.ts +1 -0
  23. package/dist/src/session-inspector.js +4 -1
  24. package/dist/src/types.d.ts +565 -0
  25. package/dist/src/types.js +27 -0
  26. package/dist/src/utils.d.ts +26 -0
  27. package/dist/src/utils.js +128 -0
  28. package/dist/src/validation.d.ts +24 -0
  29. package/dist/src/validation.js +740 -0
  30. package/dist/src/workflow-evals.js +2 -1
  31. package/package.json +4 -3
  32. package/skills/pi-extensible-workflows/SKILL.md +52 -67
  33. package/src/agent-execution.ts +84 -147
  34. package/src/budget.ts +75 -0
  35. package/src/cli.ts +427 -6
  36. package/src/doctor.ts +13 -32
  37. package/src/execution.ts +540 -0
  38. package/src/herdr.ts +73 -0
  39. package/src/host.ts +2265 -0
  40. package/src/index.ts +11 -3453
  41. package/src/persistence.ts +255 -47
  42. package/src/registry.ts +157 -0
  43. package/src/session-inspector.ts +5 -1
  44. package/src/types.ts +113 -0
  45. package/src/utils.ts +108 -0
  46. package/src/validation.ts +616 -0
  47. package/src/workflow-evals.ts +2 -1
@@ -0,0 +1,160 @@
1
+ import { WorkflowError } from "./types.js";
2
+ import { fail, object } from "./utils.js";
3
+ function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0; }
4
+ function nonNegativeFinite(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
5
+ export function validateBudget(value) {
6
+ if (value === undefined)
7
+ return undefined;
8
+ if (!object(value))
9
+ fail("INVALID_METADATA", "budget must be an object");
10
+ const result = {};
11
+ for (const [dimension, raw] of Object.entries(value)) {
12
+ if (!["tokens", "costUsd", "durationMs", "agentLaunches"].includes(dimension))
13
+ fail("INVALID_METADATA", `Unknown budget dimension: ${dimension}`);
14
+ if (!object(raw))
15
+ fail("INVALID_METADATA", `${dimension} budget must be an object`);
16
+ if (Object.keys(raw).some((key) => key !== "soft" && key !== "hard"))
17
+ fail("INVALID_METADATA", `${dimension} budget has an unknown limit`);
18
+ const isCost = dimension === "costUsd";
19
+ for (const key of ["soft", "hard"])
20
+ if (raw[key] !== undefined && !(isCost ? nonNegativeFinite(raw[key]) : nonNegativeInteger(raw[key])))
21
+ fail("INVALID_METADATA", `${dimension}.${key} must be a non-negative ${isCost ? "finite number" : "integer"}`);
22
+ if (raw.soft !== undefined && raw.soft !== null && raw.hard !== undefined && raw.hard !== null && raw.soft >= raw.hard)
23
+ fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
24
+ const limits = {};
25
+ if (raw.soft !== undefined)
26
+ limits.soft = raw.soft;
27
+ if (raw.hard !== undefined)
28
+ limits.hard = raw.hard;
29
+ if (Object.keys(limits).length)
30
+ result[dimension] = limits;
31
+ }
32
+ return Object.freeze(result);
33
+ }
34
+ export function validateBudgetPatch(value) {
35
+ if (!object(value))
36
+ fail("INVALID_METADATA", "budget patch must be an object");
37
+ const result = {};
38
+ for (const [dimension, raw] of Object.entries(value)) {
39
+ if (!["tokens", "costUsd", "durationMs", "agentLaunches"].includes(dimension))
40
+ fail("INVALID_METADATA", `Unknown budget dimension: ${dimension}`);
41
+ if (raw === null) {
42
+ result[dimension] = null;
43
+ continue;
44
+ }
45
+ if (!object(raw) || Object.keys(raw).some((key) => key !== "soft" && key !== "hard"))
46
+ fail("INVALID_METADATA", `${dimension} budget patch must contain only soft and hard`);
47
+ const limits = {};
48
+ for (const key of ["soft", "hard"])
49
+ if (Object.prototype.hasOwnProperty.call(raw, key)) {
50
+ if (raw[key] === null)
51
+ limits[key] = null;
52
+ else {
53
+ const checked = validateBudget({ [dimension]: { [key]: raw[key] } })?.[dimension];
54
+ if (checked?.[key] !== undefined)
55
+ limits[key] = checked[key];
56
+ }
57
+ }
58
+ if (limits.soft !== null && limits.hard !== null && limits.soft !== undefined && limits.hard !== undefined && limits.soft >= limits.hard)
59
+ fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
60
+ result[dimension] = limits;
61
+ }
62
+ return result;
63
+ }
64
+ export function budgetUsage(value) { return { tokens: value?.tokens ?? 0, costUsd: value?.costUsd ?? 0, durationMs: value?.durationMs ?? 0, agentLaunches: value?.agentLaunches ?? 0 }; }
65
+ export class WorkflowBudgetRuntime {
66
+ budget;
67
+ version;
68
+ #now;
69
+ #onChange;
70
+ #injected = new Set();
71
+ #seen = new Set();
72
+ #active;
73
+ #activeSince;
74
+ #usage;
75
+ #events;
76
+ #turnAccounting;
77
+ constructor(budget, version = 1, usage, events = [], options = {}) {
78
+ this.budget = budget;
79
+ this.version = version;
80
+ this.#now = options.now ?? (() => Date.now());
81
+ this.#onChange = options.onChange;
82
+ this.#active = options.active ?? true;
83
+ this.#activeSince = this.#active ? this.#now() : undefined;
84
+ this.#usage = budgetUsage(usage);
85
+ this.#events = [...events];
86
+ for (const event of events)
87
+ if (event.budgetVersion === version)
88
+ this.#seen.add(event.type);
89
+ }
90
+ get usage() { this.#syncDuration(); return { ...this.#usage }; }
91
+ get events() { return this.#events; }
92
+ get hardExhausted() { return this.#events.some((event) => event.type === "hard_exhausted" && event.budgetVersion === this.version); }
93
+ checkAgentLaunch() { this.#checkHard(["agentLaunches"]); }
94
+ beforeAttempt() { this.#checkHard(["agentLaunches"]); this.#usage.agentLaunches += 1; this.#evaluate(); }
95
+ beforeTurn() { this.#syncDuration(); this.#evaluate(); this.#checkHard(["tokens", "costUsd", "durationMs"]); }
96
+ afterTurn(accounting, final) { this.#syncDuration(); this.#applyTurn(accounting, final, this.#turnAccounting); this.#turnAccounting = { input: accounting.input, output: accounting.output, cost: accounting.cost }; }
97
+ #applyTurn(accounting, final, previous = { input: 0, output: 0, cost: 0 }) { this.#usage.tokens += Math.max(0, accounting.input - previous.input) + Math.max(0, accounting.output - previous.output); this.#usage.costUsd += Math.max(0, accounting.cost - previous.cost); this.#evaluate(); if (!final)
98
+ this.#checkHard(["tokens", "costUsd", "durationMs"]); }
99
+ instruction(agentId = "agent") { if (!this.#hasSoftCrossed() || this.#injected.has(agentId))
100
+ return undefined; this.#injected.add(agentId); return `The workflow budget soft limit has been reached. Finish the requested output now, preserving any required output schema. Current usage: ${JSON.stringify(this.usage)}. Do not start additional model work unless it is required to produce the final requested result.`; }
101
+ forAgent(agentId) { let attempt = 0; let previous; return { beforeAttempt: () => { attempt += 1; previous = undefined; this.beforeAttempt(); }, beforeTurn: () => { this.beforeTurn(); }, afterTurn: (accounting, final) => { this.#applyTurn(accounting, final, previous); previous = { input: accounting.input, output: accounting.output, cost: accounting.cost }; }, instruction: () => this.instruction(`${agentId}:${String(attempt + 1)}`) }; }
102
+ transition(state) { const active = state === "running"; if (active === this.#active)
103
+ return; if (active) {
104
+ this.#active = true;
105
+ this.#activeSince = this.#now();
106
+ }
107
+ else {
108
+ this.#syncDuration();
109
+ this.#evaluate();
110
+ this.#active = false;
111
+ this.#activeSince = undefined;
112
+ } this.#onChange?.(); }
113
+ #syncDuration() { if (this.#active && this.#activeSince !== undefined) {
114
+ const now = this.#now();
115
+ this.#usage.durationMs += Math.max(0, now - this.#activeSince);
116
+ this.#activeSince = now;
117
+ } }
118
+ #hasSoftCrossed() { return !!this.budget && Object.entries(this.budget).some(([dimension, limits]) => limits.soft !== undefined && this.#usage[dimension] >= limits.soft); }
119
+ #checkHard(dimensions) { const exhausted = dimensions.filter((dimension) => { const hard = this.budget?.[dimension]?.hard; return hard !== undefined && this.#usage[dimension] >= hard; }); if (!exhausted.length)
120
+ return; this.#record("hard_exhausted", exhausted); const detail = exhausted.map((dimension) => `${dimension} usage=${String(this.#usage[dimension])} hard=${String(this.budget?.[dimension]?.hard)}`).join(", "); throw new WorkflowError("BUDGET_EXHAUSTED", `Budget version ${String(this.version)} exhausted: ${detail}`); }
121
+ #evaluate() { const budget = this.budget; if (!budget)
122
+ return; const soft = Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.soft !== undefined && this.#usage[dimension] >= limits.soft; }); if (soft.length)
123
+ this.#record("soft_crossed", soft); const overrun = Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.hard !== undefined && this.#usage[dimension] > limits.hard; }); if (overrun.length)
124
+ this.#record("hard_overrun", overrun); }
125
+ #record(type, dimensions) { if (this.#seen.has(type))
126
+ return; this.#seen.add(type); this.#events.push({ type, budgetVersion: this.version, dimensions: [...dimensions], usage: this.usage, limits: structuredClone(this.budget ?? {}), at: this.#now() }); this.#onChange?.(); }
127
+ recordEvent(event) { this.#events.push(structuredClone(event)); }
128
+ snapshot() { return { usage: this.usage, budgetEvents: [...this.#events] }; }
129
+ }
130
+ export function mergeBudget(budget, patch) { const merged = structuredClone(budget ?? {}); for (const dimension of ["tokens", "costUsd", "durationMs", "agentLaunches"])
131
+ if (Object.prototype.hasOwnProperty.call(patch, dimension)) {
132
+ const value = patch[dimension];
133
+ if (value === null) {
134
+ Reflect.deleteProperty(merged, dimension);
135
+ continue;
136
+ }
137
+ const next = { ...(merged[dimension] ?? {}) };
138
+ for (const key of ["soft", "hard"])
139
+ if (value && Object.prototype.hasOwnProperty.call(value, key)) {
140
+ const limit = value[key];
141
+ if (limit === null)
142
+ Reflect.deleteProperty(next, key);
143
+ else if (limit !== undefined)
144
+ next[key] = limit;
145
+ }
146
+ if (Object.keys(next).length)
147
+ merged[dimension] = next;
148
+ else
149
+ Reflect.deleteProperty(merged, dimension);
150
+ } return validateBudget(merged); }
151
+ export function budgetRelaxed(previous, next) { for (const dimension of ["tokens", "costUsd", "durationMs", "agentLaunches"]) {
152
+ const oldLimit = previous?.[dimension];
153
+ const newLimit = next?.[dimension];
154
+ for (const key of ["soft", "hard"])
155
+ if ((oldLimit?.[key] !== undefined && newLimit?.[key] === undefined) || (oldLimit?.[key] !== undefined && newLimit?.[key] !== undefined && newLimit[key] > oldLimit[key]))
156
+ return true;
157
+ } return false; }
158
+ export function exhaustedBudgetDimensions(budget, usage) { if (!budget)
159
+ return []; return Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.hard !== undefined && usage[dimension] >= limits.hard; }); }
160
+ export function resumeBudgetAllowed(budget, usage) { return exhaustedBudgetDimensions(budget, usage).length === 0; }
package/dist/src/cli.d.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { type DoctorOptions } from "./doctor.js";
3
+ import { type JsonSchema, type JsonValue } from "./index.js";
4
+ import type { WorkflowCatalogFunction } from "./index.js";
3
5
  export interface CliOptions extends DoctorOptions {
4
6
  inspect?: (sessionId?: string) => Promise<void>;
7
+ transcript?: (sessionFile: string) => Promise<void>;
8
+ stderr?: (text: string) => void;
9
+ signal?: AbortSignal;
10
+ trustOverride?: boolean;
11
+ isTTY?: boolean;
5
12
  }
13
+ export declare function formatWorkflowCliHelp(fn: WorkflowCatalogFunction, command?: string): string;
14
+ export declare function parseWorkflowCliArgs(schema: JsonSchema, rawArgs: readonly string[]): Record<string, JsonValue>;
6
15
  export declare function runCli(args: readonly string[], options?: CliOptions, write?: (text: string) => void): Promise<number>;