@marcoscale98/pi-extensible-workflows 5.14.1-fork.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 +536 -0
- package/README.md +64 -0
- package/dist/src/agent-execution.d.ts +338 -0
- package/dist/src/agent-execution.js +1633 -0
- package/dist/src/background-widget.d.ts +74 -0
- package/dist/src/background-widget.js +1340 -0
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +188 -0
- package/dist/src/changelog.d.ts +6 -0
- package/dist/src/changelog.js +112 -0
- package/dist/src/decoders.d.ts +96 -0
- package/dist/src/decoders.js +629 -0
- package/dist/src/execution.d.ts +19 -0
- package/dist/src/execution.js +749 -0
- package/dist/src/herdr.d.ts +53 -0
- package/dist/src/herdr.js +270 -0
- package/dist/src/host-delivery.d.ts +95 -0
- package/dist/src/host-delivery.js +376 -0
- package/dist/src/host-navigator.d.ts +83 -0
- package/dist/src/host-navigator.js +1087 -0
- package/dist/src/host-phases.d.ts +89 -0
- package/dist/src/host-phases.js +320 -0
- package/dist/src/host-recovery.d.ts +141 -0
- package/dist/src/host-recovery.js +356 -0
- package/dist/src/host-runtime.d.ts +78 -0
- package/dist/src/host-runtime.js +378 -0
- package/dist/src/host-view.d.ts +116 -0
- package/dist/src/host-view.js +868 -0
- package/dist/src/host.d.ts +40 -0
- package/dist/src/host.js +1641 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.js +13725 -0
- package/dist/src/index.js.map +6 -0
- package/dist/src/io.d.ts +14 -0
- package/dist/src/io.js +82 -0
- package/dist/src/package-identity.d.ts +4 -0
- package/dist/src/package-identity.js +6 -0
- package/dist/src/paths.d.ts +14 -0
- package/dist/src/paths.js +51 -0
- package/dist/src/persistence.d.ts +5 -0
- package/dist/src/persistence.js +5 -0
- package/dist/src/pi-runtime-adapter.d.ts +27 -0
- package/dist/src/pi-runtime-adapter.js +243 -0
- package/dist/src/pi-runtime-runner.d.ts +42 -0
- package/dist/src/pi-runtime-runner.js +505 -0
- package/dist/src/registry.d.ts +48 -0
- package/dist/src/registry.js +379 -0
- package/dist/src/retention.d.ts +17 -0
- package/dist/src/retention.js +195 -0
- package/dist/src/runtime/agent-runner.d.ts +143 -0
- package/dist/src/runtime/agent-runner.js +12 -0
- package/dist/src/runtime/index.d.ts +2 -0
- package/dist/src/runtime/index.js +2 -0
- package/dist/src/runtime/namespace.d.ts +14 -0
- package/dist/src/runtime/namespace.js +1 -0
- package/dist/src/runtime/workflow-result.d.ts +2 -0
- package/dist/src/runtime/workflow-result.js +6 -0
- package/dist/src/session-handoff.d.ts +2 -0
- package/dist/src/session-handoff.js +82 -0
- package/dist/src/session-lease.d.ts +12 -0
- package/dist/src/session-lease.js +180 -0
- package/dist/src/store.d.ts +95 -0
- package/dist/src/store.js +951 -0
- package/dist/src/subagent-manager-handle.d.ts +4 -0
- package/dist/src/subagent-manager-handle.js +6 -0
- package/dist/src/tool-timing.d.ts +12 -0
- package/dist/src/tool-timing.js +33 -0
- package/dist/src/trajectory-contracts.d.ts +8 -0
- package/dist/src/trajectory-contracts.js +9 -0
- package/dist/src/trajectory-host-handle.d.ts +12 -0
- package/dist/src/trajectory-host-handle.js +6 -0
- package/dist/src/trajectory.d.ts +128 -0
- package/dist/src/trajectory.js +594 -0
- package/dist/src/types.d.ts +960 -0
- package/dist/src/types.js +60 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +225 -0
- package/dist/src/validation.d.ts +29 -0
- package/dist/src/validation.js +1100 -0
- package/dist/src/workflow-artifacts.d.ts +14 -0
- package/dist/src/workflow-artifacts.js +40 -0
- package/dist/starter/index.d.ts +1 -0
- package/dist/starter/index.js +103 -0
- package/dist/starter/index.js.map +6 -0
- package/dist/starter/review-loop.d.ts +72 -0
- package/dist/starter/review-loop.js +43 -0
- package/dist/starter/roles/developer.md +6 -0
- package/dist/starter/roles/oracle.md +18 -0
- package/dist/starter/roles/researcher.md +20 -0
- package/dist/starter/roles/reviewer.md +19 -0
- package/dist/starter/roles/scout.md +18 -0
- package/dist/subagents/index.d.ts +8 -0
- package/dist/subagents/index.js +8021 -0
- package/dist/subagents/index.js.map +6 -0
- package/dist/subagents/src/contracts.d.ts +173 -0
- package/dist/subagents/src/contracts.js +68 -0
- package/dist/subagents/src/decode.d.ts +15 -0
- package/dist/subagents/src/decode.js +182 -0
- package/dist/subagents/src/index.d.ts +12 -0
- package/dist/subagents/src/index.js +180 -0
- package/dist/subagents/src/manager.d.ts +3 -0
- package/dist/subagents/src/manager.js +1636 -0
- package/dist/subagents/src/navigator.d.ts +5 -0
- package/dist/subagents/src/navigator.js +627 -0
- package/dist/subagents/src/view.d.ts +70 -0
- package/dist/subagents/src/view.js +387 -0
- package/dist/subagents/src/worktree.d.ts +34 -0
- package/dist/subagents/src/worktree.js +68 -0
- package/dist/trajectory/assets/favicon.png +0 -0
- package/dist/trajectory/assets/index.html +772 -0
- package/dist/trajectory/assets/marked.min.js +6 -0
- package/dist/trajectory/assets/morphdom.min.js +1 -0
- package/dist/trajectory/index.d.ts +5 -0
- package/dist/trajectory/index.js +3459 -0
- package/dist/trajectory/index.js.map +6 -0
- package/dist/trajectory/src/assets/favicon.png +0 -0
- package/dist/trajectory/src/assets/index.html +772 -0
- package/dist/trajectory/src/assets/marked.min.js +6 -0
- package/dist/trajectory/src/assets/morphdom.min.js +1 -0
- package/dist/trajectory/src/export.d.ts +25 -0
- package/dist/trajectory/src/export.js +80 -0
- package/dist/trajectory/src/index.d.ts +26 -0
- package/dist/trajectory/src/index.js +732 -0
- package/dist/trajectory/src/server.d.ts +7 -0
- package/dist/trajectory/src/server.js +2534 -0
- package/dist/trajectory/src/server.js.map +6 -0
- package/examples/advanced-patterns/auto-model-router.mjs +78 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +105 -0
- package/skills/pi-extensible-workflows/SKILL.md +160 -0
- package/src/agent-execution.ts +1533 -0
- package/src/background-widget.ts +1496 -0
- package/src/budget.ts +109 -0
- package/src/changelog.ts +114 -0
- package/src/decoders.ts +567 -0
- package/src/execution.ts +652 -0
- package/src/herdr.ts +240 -0
- package/src/host-delivery.ts +346 -0
- package/src/host-navigator.ts +871 -0
- package/src/host-phases.ts +264 -0
- package/src/host-recovery.ts +333 -0
- package/src/host-runtime.ts +373 -0
- package/src/host-view.ts +828 -0
- package/src/host.ts +1502 -0
- package/src/index.ts +29 -0
- package/src/io.ts +73 -0
- package/src/package-identity.ts +7 -0
- package/src/paths.ts +49 -0
- package/src/persistence.ts +11 -0
- package/src/pi-runtime-adapter.ts +247 -0
- package/src/pi-runtime-runner.ts +436 -0
- package/src/registry.ts +321 -0
- package/src/retention.ts +178 -0
- package/src/runtime/agent-runner.ts +158 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/namespace.ts +16 -0
- package/src/runtime/workflow-result.ts +8 -0
- package/src/session-handoff.ts +78 -0
- package/src/session-lease.ts +125 -0
- package/src/store.ts +832 -0
- package/src/subagent-manager-handle.ts +8 -0
- package/src/tool-timing.ts +47 -0
- package/src/trajectory-contracts.ts +13 -0
- package/src/trajectory-host-handle.ts +16 -0
- package/src/trajectory.ts +543 -0
- package/src/types.ts +374 -0
- package/src/utils.ts +192 -0
- package/src/validation.ts +910 -0
- package/src/workflow-artifacts.ts +35 -0
- package/starter/index.ts +35 -0
- package/starter/prompts/council.md +16 -0
- package/starter/prompts/deep-research.md +16 -0
- package/starter/prompts/oracle.md +14 -0
- package/starter/prompts/parallel-review.md +22 -0
- package/starter/prompts/parallel-scout.md +16 -0
- package/starter/prompts/review-loop.md +14 -0
- package/starter/prompts/review.md +14 -0
- package/starter/prompts/scout.md +14 -0
- package/starter/review-loop.ts +73 -0
- package/starter/roles/developer.md +6 -0
- package/starter/roles/oracle.md +18 -0
- package/starter/roles/researcher.md +20 -0
- package/starter/roles/reviewer.md +19 -0
- package/starter/roles/scout.md +18 -0
- package/subagents/README.md +145 -0
- package/subagents/index.ts +8 -0
- package/subagents/src/contracts.ts +176 -0
- package/subagents/src/decode.ts +158 -0
- package/subagents/src/index.ts +194 -0
- package/subagents/src/manager.ts +1560 -0
- package/subagents/src/navigator.ts +488 -0
- package/subagents/src/view.ts +376 -0
- package/subagents/src/worktree.ts +99 -0
- package/trajectory/index.ts +5 -0
- package/trajectory/src/assets/favicon.png +0 -0
- package/trajectory/src/assets/index.html +772 -0
- package/trajectory/src/assets/marked.min.js +6 -0
- package/trajectory/src/assets/morphdom.min.js +1 -0
- package/trajectory/src/export.ts +85 -0
- package/trajectory/src/index.ts +604 -0
- package/trajectory/src/server.ts +447 -0
package/src/budget.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { BUDGET_DIMENSIONS, WorkflowError, type BudgetDimension, type BudgetEvent, type BudgetLimits, type WorkflowBudget, type WorkflowBudgetPatch, type WorkflowBudgetUsage, type AgentAccounting, type RunState } from "./types.js";
|
|
2
|
+
import { fail, object } from "./utils.js";
|
|
3
|
+
const BUDGET_LIMITS = ["soft", "hard"] as const;
|
|
4
|
+
type BudgetLimit = (typeof BUDGET_LIMITS)[number];
|
|
5
|
+
type BudgetPatchLimits = { soft?: number | null; hard?: number | null };
|
|
6
|
+
|
|
7
|
+
function isBudgetDimension(value: string): value is BudgetDimension { return BUDGET_DIMENSIONS.some((dimension) => dimension === value); }
|
|
8
|
+
function isBudgetLimit(value: string): value is BudgetLimit { return BUDGET_LIMITS.some((limit) => limit === value); }
|
|
9
|
+
function dimensionEntries(value: Record<string, unknown>): Array<[BudgetDimension, unknown]> {
|
|
10
|
+
const entries: Array<[BudgetDimension, unknown]> = [];
|
|
11
|
+
for (const [dimension, raw] of Object.entries(value)) {
|
|
12
|
+
if (!isBudgetDimension(dimension)) fail("INVALID_METADATA", `Unknown budget dimension: ${dimension}`);
|
|
13
|
+
entries.push([dimension, raw]);
|
|
14
|
+
}
|
|
15
|
+
return entries;
|
|
16
|
+
}
|
|
17
|
+
function limitEntries(value: Record<string, unknown>, message: string, ownOnly = false): Array<[BudgetLimit, unknown]> {
|
|
18
|
+
if (Object.keys(value).some((key) => !isBudgetLimit(key))) fail("INVALID_METADATA", message);
|
|
19
|
+
const entries: Array<[BudgetLimit, unknown]> = [];
|
|
20
|
+
for (const key of BUDGET_LIMITS) {
|
|
21
|
+
if (!ownOnly || Object.prototype.hasOwnProperty.call(value, key)) entries.push([key, value[key]]);
|
|
22
|
+
}
|
|
23
|
+
return entries;
|
|
24
|
+
}
|
|
25
|
+
function budgetEntries(value: WorkflowBudget): Array<[BudgetDimension, BudgetLimits]> {
|
|
26
|
+
const entries: Array<[BudgetDimension, BudgetLimits]> = [];
|
|
27
|
+
for (const [dimension, limits] of Object.entries(value)) {
|
|
28
|
+
if (!isBudgetDimension(dimension)) continue;
|
|
29
|
+
entries.push([dimension, limits]);
|
|
30
|
+
}
|
|
31
|
+
return entries;
|
|
32
|
+
}
|
|
33
|
+
function nonNegativeInteger(value: unknown): value is number { return typeof value === "number" && Number.isInteger(value) && value >= 0; }
|
|
34
|
+
function nonNegativeFinite(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
|
|
35
|
+
function validatedLimit(value: unknown, dimension: BudgetDimension, key: BudgetLimit): number {
|
|
36
|
+
const kind = dimension === "costUsd" ? "finite number" : "integer";
|
|
37
|
+
if (dimension === "costUsd") {
|
|
38
|
+
if (!nonNegativeFinite(value)) fail("INVALID_METADATA", `${dimension}.${key} must be a non-negative ${kind}`);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
if (!nonNegativeInteger(value)) fail("INVALID_METADATA", `${dimension}.${key} must be a non-negative ${kind}`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
export function validateBudget(value: unknown): WorkflowBudget | undefined {
|
|
45
|
+
if (value === undefined) return undefined;
|
|
46
|
+
if (!object(value)) fail("INVALID_METADATA", "budget must be an object");
|
|
47
|
+
const result: WorkflowBudget = {};
|
|
48
|
+
for (const [dimension, raw] of dimensionEntries(value)) {
|
|
49
|
+
if (!object(raw)) fail("INVALID_METADATA", `${dimension} budget must be an object`);
|
|
50
|
+
const limits: BudgetLimits = {};
|
|
51
|
+
for (const [key, rawLimit] of limitEntries(raw, `${dimension} budget has an unknown limit`)) {
|
|
52
|
+
if (rawLimit !== undefined) limits[key] = validatedLimit(rawLimit, dimension, key);
|
|
53
|
+
}
|
|
54
|
+
if (limits.soft !== undefined && limits.hard !== undefined && limits.soft >= limits.hard) fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
|
|
55
|
+
if (Object.keys(limits).length) result[dimension] = limits;
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze(result);
|
|
58
|
+
}
|
|
59
|
+
export function validateBudgetPatch(value: unknown): WorkflowBudgetPatch {
|
|
60
|
+
if (!object(value)) fail("INVALID_METADATA", "budget patch must be an object");
|
|
61
|
+
const result: WorkflowBudgetPatch = {};
|
|
62
|
+
for (const [dimension, raw] of dimensionEntries(value)) {
|
|
63
|
+
if (raw === null) { result[dimension] = null; continue; }
|
|
64
|
+
if (!object(raw)) fail("INVALID_METADATA", `${dimension} budget patch must contain only soft and hard`);
|
|
65
|
+
const limits: BudgetPatchLimits = {};
|
|
66
|
+
for (const [key, rawLimit] of limitEntries(raw, `${dimension} budget patch must contain only soft and hard`, true)) {
|
|
67
|
+
if (rawLimit === null) limits[key] = null;
|
|
68
|
+
else if (rawLimit !== undefined) limits[key] = validatedLimit(rawLimit, dimension, key);
|
|
69
|
+
}
|
|
70
|
+
if (limits.soft !== null && limits.hard !== null && limits.soft !== undefined && limits.hard !== undefined && limits.soft >= limits.hard) fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
|
|
71
|
+
result[dimension] = limits;
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
export function budgetUsage(value?: Partial<WorkflowBudgetUsage>): WorkflowBudgetUsage { return { tokens: value?.tokens ?? 0, costUsd: value?.costUsd ?? 0, durationMs: value?.durationMs ?? 0, agentLaunches: value?.agentLaunches ?? 0 }; }
|
|
76
|
+
export class WorkflowBudgetRuntime {
|
|
77
|
+
readonly #now: () => number;
|
|
78
|
+
readonly #onChange: (() => void) | undefined;
|
|
79
|
+
readonly #injected = new Set<string>();
|
|
80
|
+
readonly #seen = new Set<string>();
|
|
81
|
+
#active: boolean;
|
|
82
|
+
#activeSince: number | undefined;
|
|
83
|
+
#usage: WorkflowBudgetUsage;
|
|
84
|
+
#events: BudgetEvent[];
|
|
85
|
+
#turnAccounting?: { input: number; output: number; cost: number };
|
|
86
|
+
constructor(readonly budget: WorkflowBudget | undefined, readonly version = 1, usage?: Partial<WorkflowBudgetUsage>, events: readonly BudgetEvent[] = [], options: { now?: () => number; onChange?: () => void; active?: boolean } = {}) { this.#now = options.now ?? (() => Date.now()); this.#onChange = options.onChange; this.#active = options.active ?? true; this.#activeSince = this.#active ? this.#now() : undefined; this.#usage = budgetUsage(usage); this.#events = [...events]; for (const event of events) if (event.budgetVersion === version) this.#seen.add(event.type); }
|
|
87
|
+
get usage(): WorkflowBudgetUsage { this.#syncDuration(); return { ...this.#usage }; }
|
|
88
|
+
get events(): readonly BudgetEvent[] { return this.#events; }
|
|
89
|
+
get hardExhausted(): boolean { return this.#events.some((event) => event.type === "hard_exhausted" && event.budgetVersion === this.version); }
|
|
90
|
+
checkAgentLaunch(): void { this.#checkHard(["agentLaunches"]); }
|
|
91
|
+
beforeAttempt(): void { this.#checkHard(["agentLaunches"]); this.#usage.agentLaunches += 1; this.#evaluate(); }
|
|
92
|
+
beforeTurn(): void { this.#syncDuration(); this.#evaluate(); this.#checkHard(["tokens", "costUsd", "durationMs"]); }
|
|
93
|
+
afterTurn(accounting: AgentAccounting, final: boolean): void { this.#syncDuration(); this.#applyTurn(accounting, final, this.#turnAccounting); this.#turnAccounting = { input: accounting.input, output: accounting.output, cost: accounting.cost }; }
|
|
94
|
+
#applyTurn(accounting: AgentAccounting, final: boolean, previous = { input: 0, output: 0, cost: 0 }): void { 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) this.#checkHard(["tokens", "costUsd", "durationMs"]); }
|
|
95
|
+
instruction(agentId = "agent"): string | undefined { if (!this.#hasSoftCrossed() || this.#injected.has(agentId)) 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.`; }
|
|
96
|
+
forAgent(agentId: string) { let attempt = 0; let previous: { input: number; output: number; cost: number } | undefined; return { beforeAttempt: () => { attempt += 1; previous = undefined; this.beforeAttempt(); }, beforeTurn: () => { this.beforeTurn(); }, afterTurn: (accounting: AgentAccounting, final: boolean) => { this.#applyTurn(accounting, final, previous); previous = { input: accounting.input, output: accounting.output, cost: accounting.cost }; }, instruction: () => this.instruction(`${agentId}:${String(attempt + 1)}`) }; }
|
|
97
|
+
transition(state: RunState): void { const active = state === "running"; if (active === this.#active) return; if (active) { this.#active = true; this.#activeSince = this.#now(); } else { this.#syncDuration(); this.#evaluate(); this.#active = false; this.#activeSince = undefined; } this.#onChange?.(); }
|
|
98
|
+
#syncDuration(): void { if (this.#active && this.#activeSince !== undefined) { const now = this.#now(); this.#usage.durationMs += Math.max(0, now - this.#activeSince); this.#activeSince = now; } }
|
|
99
|
+
#hasSoftCrossed(): boolean { return !!this.budget && budgetEntries(this.budget).some(([dimension, limits]) => limits.soft !== undefined && this.#usage[dimension] >= limits.soft); }
|
|
100
|
+
#checkHard(dimensions: readonly BudgetDimension[]): void { const exhausted = dimensions.filter((dimension) => { const hard = this.budget?.[dimension]?.hard; return hard !== undefined && this.#usage[dimension] >= hard; }); if (!exhausted.length) 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}`); }
|
|
101
|
+
#evaluate(): void { const budget = this.budget; if (!budget) return; const entries = budgetEntries(budget); const soft = entries.filter(([dimension, limits]) => limits.soft !== undefined && this.#usage[dimension] >= limits.soft).map(([dimension]) => dimension); if (soft.length) this.#record("soft_crossed", soft); const overrun = entries.filter(([dimension, limits]) => limits.hard !== undefined && this.#usage[dimension] > limits.hard).map(([dimension]) => dimension); if (overrun.length) this.#record("hard_overrun", overrun); }
|
|
102
|
+
#record(type: BudgetEvent["type"], dimensions: readonly BudgetDimension[]): void { if (this.#seen.has(type)) 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?.(); }
|
|
103
|
+
recordEvent(event: BudgetEvent): void { this.#events.push(structuredClone(event)); }
|
|
104
|
+
snapshot(): { usage: WorkflowBudgetUsage; budgetEvents: readonly BudgetEvent[] } { return { usage: this.usage, budgetEvents: [...this.#events] }; }
|
|
105
|
+
}
|
|
106
|
+
export function mergeBudget(budget: WorkflowBudget | undefined, patch: WorkflowBudgetPatch): WorkflowBudget | undefined { const merged: WorkflowBudget = structuredClone(budget ?? {}); for (const dimension of BUDGET_DIMENSIONS) if (Object.prototype.hasOwnProperty.call(patch, dimension)) { const value = patch[dimension]; if (value === null) { Reflect.deleteProperty(merged, dimension); continue; } const next: BudgetLimits = { ...(merged[dimension] ?? {}) }; for (const key of BUDGET_LIMITS) if (value && Object.prototype.hasOwnProperty.call(value, key)) { const limit = value[key]; if (limit === null) Reflect.deleteProperty(next, key); else if (limit !== undefined) next[key] = limit; } if (Object.keys(next).length) merged[dimension] = next; else Reflect.deleteProperty(merged, dimension); } return validateBudget(merged); }
|
|
107
|
+
export function budgetRelaxed(previous: WorkflowBudget | undefined, next: WorkflowBudget | undefined): boolean { for (const dimension of BUDGET_DIMENSIONS) { const oldLimit = previous?.[dimension]; const newLimit = next?.[dimension]; for (const key of BUDGET_LIMITS) if ((oldLimit?.[key] !== undefined && newLimit?.[key] === undefined) || (oldLimit?.[key] !== undefined && newLimit?.[key] !== undefined && newLimit[key] > oldLimit[key])) return true; } return false; }
|
|
108
|
+
export function exhaustedBudgetDimensions(budget: WorkflowBudget | undefined, usage: WorkflowBudgetUsage): BudgetDimension[] { if (!budget) return []; return budgetEntries(budget).filter(([dimension, limits]) => limits.hard !== undefined && usage[dimension] >= limits.hard).map(([dimension]) => dimension); }
|
|
109
|
+
export function resumeBudgetAllowed(budget: WorkflowBudget | undefined, usage: WorkflowBudgetUsage): boolean { return exhaustedBudgetDimensions(budget, usage).length === 0; }
|
package/src/changelog.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { atomicJson, json } from "./io.js";
|
|
6
|
+
import { object } from "./utils.js";
|
|
7
|
+
import { isCorePackageName } from "./package-identity.js";
|
|
8
|
+
const STATE_DIRECTORY = "pi-extensible-workflows";
|
|
9
|
+
const STATE_FILE = "changelog-state.json";
|
|
10
|
+
const MAX_NOTICE_CHARACTERS = 6_000;
|
|
11
|
+
|
|
12
|
+
type ChangelogEntry = { version: string; body: string };
|
|
13
|
+
type PackageMetadata = { directory: string; version: string };
|
|
14
|
+
|
|
15
|
+
type ChangelogContext = Pick<ExtensionContext, "hasUI" | "mode"> & { ui: Pick<ExtensionContext["ui"], "notify"> };
|
|
16
|
+
|
|
17
|
+
function packageMetadata(value: unknown, directory: string): PackageMetadata | undefined {
|
|
18
|
+
if (!object(value) || !isCorePackageName(value.name) || typeof value.version !== "string" || !value.version.trim()) return undefined;
|
|
19
|
+
return { directory, version: value.version.trim() };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function packageMetadataAt(directory: string): Promise<PackageMetadata | undefined> {
|
|
23
|
+
try {
|
|
24
|
+
const value: unknown = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
25
|
+
return packageMetadata(value, directory);
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function installedPackageMetadata(): Promise<PackageMetadata | undefined> {
|
|
32
|
+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
for (const directory of [join(moduleDirectory, ".."), join(moduleDirectory, "../..")]) {
|
|
34
|
+
const metadata = await packageMetadataAt(directory);
|
|
35
|
+
if (metadata) return metadata;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseChangelog(markdown: string): ChangelogEntry[] {
|
|
41
|
+
const entries: ChangelogEntry[] = [];
|
|
42
|
+
let current: ChangelogEntry | undefined;
|
|
43
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
44
|
+
const heading = /^##\s+(?:\[([^\]\r\n]+)\]|([^\s\r\n]+))(?:\s+-.*)?\s*$/.exec(line);
|
|
45
|
+
if (heading) {
|
|
46
|
+
const version = heading[1]?.trim() ?? heading[2]?.trim();
|
|
47
|
+
current = undefined;
|
|
48
|
+
if (version && version.toLowerCase() !== "unreleased") {
|
|
49
|
+
current = { version, body: "" };
|
|
50
|
+
entries.push(current);
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (current) current.body = current.body ? `${current.body}\n${line}` : line;
|
|
55
|
+
}
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readChangelog(directory: string): Promise<ChangelogEntry[]> {
|
|
60
|
+
try {
|
|
61
|
+
return parseChangelog(await readFile(join(directory, "CHANGELOG.md"), "utf8"));
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function releaseEntries(entries: readonly ChangelogEntry[], currentVersion: string, previousVersion: string | undefined): ChangelogEntry[] {
|
|
68
|
+
const currentIndex = entries.findIndex((entry) => entry.version === currentVersion);
|
|
69
|
+
const current = entries[currentIndex];
|
|
70
|
+
if (current === undefined || current.body.trim() === "") return [];
|
|
71
|
+
if (previousVersion === currentVersion) return [];
|
|
72
|
+
const previousIndex = previousVersion === undefined ? -1 : entries.findIndex((entry) => entry.version === previousVersion);
|
|
73
|
+
const selected = previousIndex > currentIndex ? entries.slice(currentIndex, previousIndex) : [current];
|
|
74
|
+
return selected.filter((entry) => entry.body.trim() !== "");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function noticeText(version: string, entries: readonly ChangelogEntry[]): string {
|
|
78
|
+
const notes = entries.map((entry) => `## ${entry.version}\n${entry.body.trim()}`).join("\n\n");
|
|
79
|
+
const bounded = notes.length > MAX_NOTICE_CHARACTERS ? `${notes.slice(0, MAX_NOTICE_CHARACTERS).trimEnd()}\n...` : notes;
|
|
80
|
+
return `pi-extensible-workflows updated to ${version}\n\n${bounded}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async function lastNotifiedVersion(path: string): Promise<string | undefined> {
|
|
85
|
+
try {
|
|
86
|
+
const value: unknown = await json(path);
|
|
87
|
+
return object(value) && typeof value.lastNotifiedVersion === "string" && value.lastNotifiedVersion.trim() ? value.lastNotifiedVersion : undefined;
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function showChangelogNotice(context: ChangelogContext, agentDir: string, packageDirectory?: string): Promise<void> {
|
|
94
|
+
if (!context.hasUI || (context.mode !== "tui" && context.mode !== "rpc")) return;
|
|
95
|
+
const { ui } = context;
|
|
96
|
+
try {
|
|
97
|
+
const metadata = packageDirectory === undefined ? await installedPackageMetadata() : await packageMetadataAt(packageDirectory);
|
|
98
|
+
if (!metadata) return;
|
|
99
|
+
const statePath = join(agentDir, STATE_DIRECTORY, STATE_FILE);
|
|
100
|
+
const previousVersion = await lastNotifiedVersion(statePath);
|
|
101
|
+
if (previousVersion === metadata.version) return;
|
|
102
|
+
const entries = releaseEntries(await readChangelog(metadata.directory), metadata.version, previousVersion);
|
|
103
|
+
if (!entries.length) return;
|
|
104
|
+
ui.notify(noticeText(metadata.version, entries), "info");
|
|
105
|
+
try {
|
|
106
|
+
await mkdir(dirname(statePath), { recursive: true, mode: 0o700 });
|
|
107
|
+
await atomicJson(statePath, { lastNotifiedVersion: metadata.version });
|
|
108
|
+
} catch {
|
|
109
|
+
// The notice is advisory; a failed state write may repeat it on the next start.
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Changelog data is optional and must never prevent Pi startup.
|
|
113
|
+
}
|
|
114
|
+
}
|