@parcha/agentrun-pi 0.1.0-beta.1 → 0.1.0-beta.3

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 (44) hide show
  1. package/README.md +69 -27
  2. package/dist/cli.js +5 -3
  3. package/dist/demo.js +1 -0
  4. package/dist/extension-service.js +12 -4
  5. package/dist/extension.js +329 -57
  6. package/dist/host-addendum.d.ts +4 -0
  7. package/dist/host-addendum.js +19 -0
  8. package/dist/index.d.ts +1 -2
  9. package/dist/index.js +1 -2
  10. package/dist/presentation.js +2 -2
  11. package/dist/skills/author/SKILL.md +135 -0
  12. package/{skills → dist/skills}/author/references/jev-decisions.md +9 -8
  13. package/dist/skills/author/references/language.md +128 -0
  14. package/dist/skills/author/references/sdk-authoring.md +9 -0
  15. package/{skills → dist/skills}/author/references/workflow-format.md +7 -15
  16. package/dist/triage-demo.d.ts +62 -0
  17. package/dist/triage-demo.js +153 -0
  18. package/dist/types.d.ts +3 -0
  19. package/dist/workflow-inspector.d.ts +47 -0
  20. package/dist/workflow-inspector.js +235 -0
  21. package/dist/workflow-session.d.ts +19 -0
  22. package/dist/workflow-session.js +167 -0
  23. package/dist/workflow-store.d.ts +30 -0
  24. package/dist/workflow-store.js +249 -0
  25. package/dist/workflow-view.d.ts +64 -0
  26. package/dist/workflow-view.js +268 -0
  27. package/package.json +5 -6
  28. package/dist/admission.d.ts +0 -6
  29. package/dist/admission.js +0 -51
  30. package/dist/author.d.ts +0 -21
  31. package/dist/author.js +0 -72
  32. package/dist/skill-bundle.d.ts +0 -5
  33. package/dist/skill-bundle.js +0 -15
  34. package/skills/author/SKILL.md +0 -102
  35. package/skills/author/references/sdk-authoring.md +0 -16
  36. /package/{skills → dist/skills}/author/examples/evidence-gate.input.json +0 -0
  37. /package/{skills → dist/skills}/author/examples/evidence-gate.json +0 -0
  38. /package/{skills → dist/skills}/author/examples/read-file.input.json +0 -0
  39. /package/{skills → dist/skills}/author/examples/read-file.json +0 -0
  40. /package/{skills → dist/skills}/author/examples/read-source-decision.input.json +0 -0
  41. /package/{skills → dist/skills}/author/examples/read-source-decision.json +0 -0
  42. /package/{skills → dist/skills}/author/examples/release-note.txt +0 -0
  43. /package/{skills → dist/skills}/author/examples/review-release-notes.input.json +0 -0
  44. /package/{skills → dist/skills}/author/examples/review-release-notes.json +0 -0
package/dist/types.d.ts CHANGED
@@ -48,6 +48,9 @@ export interface PiToolDefinition<TArgs = unknown, TDetails = unknown> {
48
48
  label: string;
49
49
  description: string;
50
50
  parameters: object;
51
+ /** Optional JSON Schema for a direct workflow call's return value. Host tools
52
+ * normally return {content, details}; this describes that whole envelope. */
53
+ resultSchema?: Record<string, unknown>;
51
54
  executionMode?: "sequential" | "parallel";
52
55
  execute(toolCallId: string, params: TArgs, signal: AbortSignal | undefined, onUpdate: ((result: PiToolResult<TDetails>) => void) | undefined, context: unknown): Promise<PiToolResult<TDetails>>;
53
56
  }
@@ -0,0 +1,47 @@
1
+ import type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';
2
+ import { type Component } from '@earendil-works/pi-tui';
3
+ /** Structural view contract: the inspector never executes or changes a workflow. */
4
+ export interface InspectorView {
5
+ title: string;
6
+ digest: string;
7
+ status: string;
8
+ summary: string[];
9
+ nodes: {
10
+ path: string;
11
+ label: string;
12
+ kind: string;
13
+ status: string;
14
+ summary: string;
15
+ details: string[];
16
+ }[];
17
+ output?: string[];
18
+ readOnly?: boolean;
19
+ }
20
+ export type InspectorAction = 'run' | 'save' | 'load' | 'input' | 'history' | 'stop' | 'edit';
21
+ /** Exported for deterministic rendering/navigation tests, without a terminal or model. */
22
+ export declare class WorkflowInspector implements Component {
23
+ private readonly getView;
24
+ private readonly theme;
25
+ private readonly done;
26
+ private readonly redraw;
27
+ private readonly rows;
28
+ private page;
29
+ private selected;
30
+ private selectedPath?;
31
+ private actionIndex;
32
+ private scroll;
33
+ private viewport;
34
+ private detailLength;
35
+ private timer?;
36
+ constructor(getView: () => InspectorView, theme: Pick<Theme, 'fg' | 'bold'>, done: (action: InspectorAction | undefined) => void, redraw?: () => void, rows?: () => number);
37
+ startRefresh(): void;
38
+ dispose(): void;
39
+ invalidate(): void;
40
+ private view;
41
+ private actions;
42
+ private fingerprint;
43
+ handleInput(data: string): void;
44
+ private wrapped;
45
+ render(width: number): string[];
46
+ }
47
+ export declare function showWorkflowInspector(ctx: ExtensionContext, getView: () => InspectorView): Promise<InspectorAction | undefined>;
@@ -0,0 +1,235 @@
1
+ import { matchesKey, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
2
+ import { cleanText } from './presentation.js';
3
+ const MAX_CHARS = 24_000, MAX_LINES = 600;
4
+ const safeLine = (value) => cleanText(value).replace(/[\n\t]/g, ' ');
5
+ /** Exported for deterministic rendering/navigation tests, without a terminal or model. */
6
+ export class WorkflowInspector {
7
+ getView;
8
+ theme;
9
+ done;
10
+ redraw;
11
+ rows;
12
+ page = 'overview';
13
+ selected = 0;
14
+ selectedPath;
15
+ actionIndex = 0;
16
+ scroll = 0;
17
+ viewport = 12;
18
+ detailLength = 0;
19
+ timer;
20
+ constructor(getView, theme, done, redraw = () => { }, rows = () => 24) {
21
+ this.getView = getView;
22
+ this.theme = theme;
23
+ this.done = done;
24
+ this.redraw = redraw;
25
+ this.rows = rows;
26
+ }
27
+ startRefresh() {
28
+ if (this.timer)
29
+ return;
30
+ let previous = this.fingerprint();
31
+ this.timer = setInterval(() => {
32
+ const next = this.fingerprint();
33
+ if (next !== previous) {
34
+ previous = next;
35
+ this.redraw();
36
+ }
37
+ }, 250);
38
+ }
39
+ dispose() { if (this.timer)
40
+ clearInterval(this.timer); this.timer = undefined; }
41
+ invalidate() { }
42
+ view() {
43
+ const view = this.getView();
44
+ const index = this.selectedPath ? view.nodes.findIndex(node => node.path === this.selectedPath) : -1;
45
+ this.selected = Math.max(0, Math.min(index >= 0 ? index : this.selected, view.nodes.length - 1));
46
+ this.selectedPath = view.nodes[this.selected]?.path;
47
+ return view;
48
+ }
49
+ actions(view) {
50
+ if (view.readOnly)
51
+ return [];
52
+ return view.status === 'running' ? ['stop', 'history'] : ['run', 'input', 'edit', 'save', 'load', 'history'];
53
+ }
54
+ fingerprint() {
55
+ const view = this.view();
56
+ // Track the visible projection, not raw receipts or the whole workflow state.
57
+ const visible = this.page === 'detail' ? view.nodes[this.selected]?.details ?? []
58
+ : this.page === 'output' ? view.output ?? [] : view.summary;
59
+ let budget = MAX_CHARS;
60
+ const text = [];
61
+ for (const line of visible) {
62
+ if (budget <= 0 || text.length >= MAX_LINES)
63
+ break;
64
+ text.push(line.slice(0, budget));
65
+ budget -= line.length;
66
+ }
67
+ return JSON.stringify([view.title, view.digest, view.status, view.readOnly, view.output !== undefined,
68
+ view.nodes.map(node => [node.path, node.label, node.kind, node.status, node.summary]), text]);
69
+ }
70
+ handleInput(data) {
71
+ const view = this.view();
72
+ if (matchesKey(data, 'escape')) {
73
+ if (this.page === 'overview') {
74
+ this.dispose();
75
+ this.done(undefined);
76
+ return;
77
+ }
78
+ this.page = this.page === 'detail' ? 'stages' : 'overview';
79
+ this.scroll = 0;
80
+ }
81
+ else if (matchesKey(data, 'ctrl+c')) {
82
+ // Close the view, never silently cancel a workflow.
83
+ this.dispose();
84
+ this.done(undefined);
85
+ return;
86
+ }
87
+ else if (data === 'a' && !view.readOnly && this.page !== 'actions') {
88
+ this.page = 'actions';
89
+ this.actionIndex = 0;
90
+ this.scroll = 0;
91
+ }
92
+ else if (data === 'g') {
93
+ this.page = 'stages';
94
+ this.scroll = 0;
95
+ }
96
+ else if (data === 'v') {
97
+ this.page = 'overview';
98
+ this.scroll = 0;
99
+ }
100
+ else if (data === 'o' && view.output) {
101
+ this.page = 'output';
102
+ this.scroll = 0;
103
+ }
104
+ else if (matchesKey(data, 'enter')) {
105
+ if (this.page === 'actions') {
106
+ const action = this.actions(view)[this.actionIndex];
107
+ if (action) {
108
+ this.dispose();
109
+ this.done(action);
110
+ return;
111
+ }
112
+ }
113
+ else if (this.page === 'overview') {
114
+ this.page = 'stages';
115
+ this.scroll = 0;
116
+ }
117
+ else if (this.page === 'stages' && view.nodes.length) {
118
+ this.page = 'detail';
119
+ this.scroll = 0;
120
+ }
121
+ }
122
+ else {
123
+ const direction = matchesKey(data, 'up') ? -1 : matchesKey(data, 'down') ? 1 : 0;
124
+ const pageDirection = matchesKey(data, 'pageUp') ? -1 : matchesKey(data, 'pageDown') ? 1 : 0;
125
+ const amount = direction || pageDirection * this.viewport;
126
+ if (this.page === 'stages') {
127
+ this.selected = Math.max(0, Math.min(view.nodes.length - 1, this.selected + amount));
128
+ this.selectedPath = view.nodes[this.selected]?.path;
129
+ }
130
+ else if (this.page === 'actions') {
131
+ this.actionIndex = Math.max(0, Math.min(this.actions(view).length - 1, this.actionIndex + amount));
132
+ }
133
+ else
134
+ this.scroll = Math.max(0, Math.min(Math.max(0, this.detailLength - this.viewport), this.scroll + amount));
135
+ }
136
+ this.redraw();
137
+ }
138
+ wrapped(values, width) {
139
+ const lines = [];
140
+ let remaining = MAX_CHARS, clipped = false;
141
+ for (const value of values) {
142
+ if (!remaining || lines.length >= MAX_LINES) {
143
+ clipped = true;
144
+ break;
145
+ }
146
+ // Slice before sanitizing/wrapping so a huge individual field stays bounded.
147
+ const portion = value.slice(0, remaining);
148
+ remaining -= portion.length;
149
+ if (portion.length < value.length)
150
+ clipped = true;
151
+ const clean = cleanText(portion).replace(/\t/g, ' ');
152
+ const paragraphs = clean.split('\n');
153
+ for (const [index, line] of paragraphs.entries()) {
154
+ const wrapped = wrapTextWithAnsi(line, width);
155
+ const available = MAX_LINES - lines.length;
156
+ if (wrapped.length > available)
157
+ clipped = true;
158
+ lines.push(...wrapped.slice(0, available));
159
+ if (lines.length >= MAX_LINES) {
160
+ if (index < paragraphs.length - 1)
161
+ clipped = true;
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ return { lines, clipped };
167
+ }
168
+ render(width) {
169
+ const view = this.view();
170
+ const columns = Math.max(1, width);
171
+ // Cover the viewport except Pi's two status rows, including on tall terminals.
172
+ const height = Math.max(8, this.rows() - 2);
173
+ this.viewport = height - 6;
174
+ const cut = (value) => truncateToWidth(safeLine(value.slice(0, MAX_CHARS)), columns);
175
+ const title = this.theme.bold(cut(view.title));
176
+ const subtitle = cut(`${view.status} · ${view.digest.slice(0, 12)}`);
177
+ let body = [], position = '', clipped = false;
178
+ if (this.page === 'stages') {
179
+ const first = Math.max(0, Math.min(this.selected - Math.floor(this.viewport / 2), view.nodes.length - this.viewport));
180
+ body = view.nodes.slice(first, first + this.viewport).map((node, index) => {
181
+ const selected = first + index === this.selected;
182
+ const depth = Math.min(5, (node.path.match(/\/(?:steps|body|branches|workflow)(?:\/|$)/g) ?? []).length);
183
+ const text = cut(`${selected ? '›' : ' '} ${' '.repeat(depth)}${node.label} · ${node.kind} · ${node.status}`);
184
+ return selected ? this.theme.fg('accent', text) : text;
185
+ });
186
+ if (!body.length)
187
+ body = [cut('No stages yet. Use Actions to load a workflow.')];
188
+ position = view.nodes.length ? `Stage ${this.selected + 1}/${view.nodes.length}` : 'No stages';
189
+ }
190
+ else if (this.page === 'actions') {
191
+ const actions = this.actions(view);
192
+ this.actionIndex = Math.min(this.actionIndex, actions.length - 1);
193
+ const labels = { run: 'Run this workflow', save: 'Save procedure', load: 'Load procedure', input: 'Change input', history: 'Inspect run history', stop: 'Stop running workflow', edit: 'Edit procedure' };
194
+ const first = Math.max(0, this.actionIndex - this.viewport + 1);
195
+ body = actions.slice(first, first + this.viewport).map((action, index) => cut(`${first + index === this.actionIndex ? '›' : ' '} ${labels[action]}`));
196
+ position = 'Choose an action; execution stays with Pi.';
197
+ }
198
+ else {
199
+ const node = view.nodes[this.selected];
200
+ const details = function* () {
201
+ if (!node) {
202
+ yield 'This stage is no longer available.';
203
+ return;
204
+ }
205
+ yield node.label;
206
+ yield `${node.kind} · ${node.status}`;
207
+ yield node.path;
208
+ yield node.summary;
209
+ yield* node.details;
210
+ };
211
+ const values = this.page === 'overview' ? view.summary : this.page === 'output' ? view.output ?? ['No output recorded.'] : details();
212
+ const wrapped = this.wrapped(values, columns);
213
+ this.detailLength = wrapped.lines.length;
214
+ clipped = wrapped.clipped;
215
+ this.scroll = Math.max(0, Math.min(this.scroll, this.detailLength - this.viewport));
216
+ body = wrapped.lines.slice(this.scroll, this.scroll + this.viewport);
217
+ position = `${this.page === 'overview' ? 'Overview' : this.page === 'output' ? 'Output' : 'Stage details'} · lines ${this.detailLength ? this.scroll + 1 : 0}–${Math.min(this.scroll + this.viewport, this.detailLength)}/${this.detailLength}`;
218
+ }
219
+ while (body.length < this.viewport)
220
+ body.push('');
221
+ const guidance = this.page === 'stages' ? '↑↓ stages · Enter inspect · v overview' : this.page === 'actions' ? '↑↓ actions · Enter choose · Esc back' : '↑↓ scroll · g stages · v overview';
222
+ const footer = clipped ? 'Display clipped; see full run record.' : `${view.readOnly ? 'Read-only · ' : 'a actions · '}${view.output ? 'o output · ' : ''}${this.page === 'overview' ? 'Esc close' : 'Esc back'}`;
223
+ return [title, subtitle, cut(position), ...body.slice(0, this.viewport),
224
+ this.theme.fg('muted', cut(guidance)), this.theme.fg(clipped ? 'warning' : 'muted', cut(footer)), ''];
225
+ }
226
+ }
227
+ export async function showWorkflowInspector(ctx, getView) {
228
+ if (ctx.mode !== 'tui')
229
+ return undefined;
230
+ return ctx.ui.custom((tui, theme, _keys, done) => {
231
+ const component = new WorkflowInspector(getView, theme, done, () => tui.requestRender(), () => tui.terminal.rows);
232
+ component.startRefresh();
233
+ return component;
234
+ }, { overlay: true, overlayOptions: { width: '100%', maxHeight: '100%', margin: 0, anchor: 'top-left' } });
235
+ }
@@ -0,0 +1,19 @@
1
+ import { type Workflow } from '@parcha/agentrun-dsl';
2
+ import type { ExtensionRunReport } from './extension-service.js';
3
+ import type { RunObservation } from './workflow-view.js';
4
+ export interface WorkflowSessionSnapshot {
5
+ version: 1;
6
+ workflow: Workflow;
7
+ input: Record<string, unknown>;
8
+ savedName?: string;
9
+ demo?: 'scripted' | 'empty' | 'triage' | 'triage-failure';
10
+ report?: ExtensionRunReport;
11
+ observation?: RunObservation;
12
+ running?: boolean;
13
+ runId?: string;
14
+ createdAt: string;
15
+ }
16
+ /** Caller supplies Pi's active branch, not the complete session tree. */
17
+ export declare function restoreWorkflowSession(branch: readonly unknown[]): WorkflowSessionSnapshot | undefined;
18
+ /** Newest first, one terminal record per run; unmatched starts are interruptions. */
19
+ export declare function workflowRunHistory(branch: readonly unknown[]): WorkflowSessionSnapshot[];
@@ -0,0 +1,167 @@
1
+ import { types } from 'node:util';
2
+ import { inspectWorkflow, validateWorkflow } from '@parcha/agentrun-dsl';
3
+ import { demoWorkflow } from './demo.js';
4
+ import { supportTriageWorkflow } from './triage-demo.js';
5
+ const MAX_BYTES = 8 * 1024 * 1024;
6
+ const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
7
+ const only = (v, keys) => Object.keys(v).every(key => keys.includes(key));
8
+ const count = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
9
+ const textFields = (v, required, optional = []) => required.every(key => typeof v[key] === 'string') && optional.every(key => v[key] === undefined || typeof v[key] === 'string');
10
+ /** Copy data without invoking accessors, proxy traps, toJSON or authored code. */
11
+ function jsonSnapshot(value) {
12
+ const ancestors = new Set();
13
+ let values = 0, bytes = 0;
14
+ const visit = (item, depth) => {
15
+ if (++values > 200_000 || depth > 128 || bytes > MAX_BYTES)
16
+ throw new Error('snapshot bounds');
17
+ if (item === null || typeof item === 'boolean')
18
+ return item;
19
+ if (typeof item === 'number' && Number.isFinite(item))
20
+ return item;
21
+ if (typeof item === 'string') {
22
+ bytes += Buffer.byteLength(item);
23
+ if (bytes > MAX_BYTES)
24
+ throw new Error('snapshot bounds');
25
+ return item;
26
+ }
27
+ if (typeof item !== 'object' || types.isProxy(item) || ancestors.has(item))
28
+ throw new Error('snapshot data');
29
+ const prototype = Object.getPrototypeOf(item);
30
+ if (prototype !== (Array.isArray(item) ? Array.prototype : Object.prototype) && prototype !== null)
31
+ throw new Error('snapshot prototype');
32
+ const descriptors = Object.getOwnPropertyDescriptors(item);
33
+ if (Object.getOwnPropertySymbols(item).length || Object.values(descriptors).some(d => !('value' in d)))
34
+ throw new Error('snapshot descriptors');
35
+ ancestors.add(item);
36
+ try {
37
+ if (Array.isArray(item)) {
38
+ if (Object.keys(descriptors).some(key => key !== 'length' && !/^(0|[1-9]\d*)$/.test(key)) || item.length > 200_000)
39
+ throw new Error('snapshot array');
40
+ return Array.from({ length: item.length }, (_, index) => visit(descriptors[index]?.value, depth + 1));
41
+ }
42
+ return Object.fromEntries(Object.entries(descriptors).map(([key, descriptor]) => {
43
+ if (!descriptor.enumerable)
44
+ throw new Error('snapshot hidden data');
45
+ bytes += Buffer.byteLength(key);
46
+ return [key, visit(descriptor.value, depth + 1)];
47
+ }));
48
+ }
49
+ finally {
50
+ ancestors.delete(item);
51
+ }
52
+ };
53
+ const result = visit(value, 0);
54
+ if (Buffer.byteLength(JSON.stringify(result)) > MAX_BYTES)
55
+ throw new Error('snapshot bounds');
56
+ return result;
57
+ }
58
+ function validReport(r, digest) {
59
+ if (!object(r) || !only(r, ['digest', 'status', 'calls', 'events', 'output', 'escalation', 'error', 'uncertainEffects', 'traceTruncated', 'trace'])
60
+ || r.digest !== digest || !['complete', 'escalated', 'failed', 'interrupted'].includes(r.status)
61
+ || !object(r.calls) || !only(r.calls, ['agent', 'judge', 'tool']) || !['agent', 'judge', 'tool'].every(k => count(r.calls[k]))
62
+ || !Array.isArray(r.events) || !r.events.every((e) => object(e) && textFields(e, ['type', 'label'], ['executionPath']))
63
+ || r.traceTruncated !== undefined && typeof r.traceTruncated !== 'boolean')
64
+ return false;
65
+ if (r.error !== undefined && (!object(r.error) || !only(r.error, ['code', 'message', 'stage', 'path', 'reason', 'status', 'problems', 'stateBytes', 'maxStateBytes'])
66
+ || !textFields(r.error, ['code', 'message'], ['stage', 'path', 'reason'])
67
+ || ['status', 'stateBytes', 'maxStateBytes'].some(k => r.error[k] !== undefined && !count(r.error[k]))
68
+ || r.error.problems !== undefined && (!Array.isArray(r.error.problems) || !r.error.problems.every((p) => typeof p === 'string'))))
69
+ return false;
70
+ if (r.escalation !== undefined && (!object(r.escalation) || !only(r.escalation, ['kind', 'stage', 'summary', 'label', 'executionPath'])
71
+ || !textFields(r.escalation, ['kind', 'stage', 'summary'], ['label', 'executionPath'])))
72
+ return false;
73
+ if (r.uncertainEffects !== undefined && (!Array.isArray(r.uncertainEffects) || !r.uncertainEffects.every((e) => object(e)
74
+ && only(e, ['idempotencyKey', 'executionPath', 'outcome']) && textFields(e, ['idempotencyKey'], ['executionPath']) && e.outcome === 'unknown')))
75
+ return false;
76
+ if (r.trace !== undefined) {
77
+ const fields = ['receivedEvents', 'receivedBytes', 'rejectedEvents', 'retainedEvents', 'retainedBytes', 'droppedEvents', 'droppedBytes'];
78
+ if (!object(r.trace) || !only(r.trace, ['policy', ...fields]) || r.trace.policy !== 'tail' || !fields.every(k => count(r.trace[k])))
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+ function validObservation(o) {
84
+ return object(o) && only(o, ['steps', 'decisions', 'notes', 'routes', 'omitted']) && count(o.omitted)
85
+ && object(o.steps) && Object.values(o.steps).every(s => object(s) && only(s, ['label', 'kind', 'status']) && textFields(s, ['label', 'kind', 'status']))
86
+ && object(o.notes) && Object.values(o.notes).every(n => typeof n === 'string')
87
+ && (o.routes === undefined || object(o.routes) && Object.values(o.routes).every(n => typeof n === 'string'))
88
+ && Array.isArray(o.decisions) && o.decisions.every((d) => object(d) && only(d, ['path', 'label', 'questions', 'evidence', 'answer', 'accepted'])
89
+ && textFields(d, ['path', 'label', 'questions', 'evidence'], ['answer']) && (d.accepted === undefined || typeof d.accepted === 'boolean'));
90
+ }
91
+ function snapshotEntry(entry) {
92
+ try {
93
+ if (!object(entry) || types.isProxy(entry) || ![Object.prototype, null].includes(Object.getPrototypeOf(entry)))
94
+ return;
95
+ const descriptors = Object.getOwnPropertyDescriptors(entry);
96
+ if (descriptors.type?.value !== 'custom' || descriptors.customType?.value !== 'agentrun:snapshot' || !('value' in (descriptors.data ?? {})))
97
+ return;
98
+ const s = jsonSnapshot(descriptors.data.value);
99
+ if (!object(s) || !only(s, ['version', 'workflow', 'input', 'savedName', 'demo', 'report', 'observation', 'running', 'runId', 'createdAt'])
100
+ || s.version !== 1 || !object(s.input) || typeof s.createdAt !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(s.createdAt)
101
+ || !Number.isFinite(Date.parse(s.createdAt)) || new Date(s.createdAt).toISOString() !== s.createdAt
102
+ || s.savedName !== undefined && (typeof s.savedName !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(s.savedName))
103
+ || s.runId !== undefined && (typeof s.runId !== 'string' || !s.runId.length || s.runId.length > 200)
104
+ || s.running !== undefined && typeof s.running !== 'boolean')
105
+ return;
106
+ const inspection = inspectWorkflow(s.workflow);
107
+ // Input remains an exact draft, including incomplete form values; run preflight owns it.
108
+ if (!validateWorkflow(s.workflow, { executeCode: false }).ok)
109
+ return;
110
+ if (s.report !== undefined && !validReport(s.report, inspection.sha256))
111
+ return;
112
+ if (s.observation !== undefined && !validObservation(s.observation))
113
+ return;
114
+ if (s.demo !== undefined) {
115
+ const bundled = s.demo === 'scripted' || s.demo === 'empty' ? demoWorkflow
116
+ : s.demo === 'triage' || s.demo === 'triage-failure' ? supportTriageWorkflow : undefined;
117
+ if (!bundled || inspectWorkflow(bundled).sha256 !== inspection.sha256)
118
+ delete s.demo;
119
+ }
120
+ return s;
121
+ }
122
+ catch {
123
+ return undefined;
124
+ }
125
+ }
126
+ function interrupted(s) {
127
+ if (!s.running)
128
+ return s;
129
+ s.running = false;
130
+ s.report = { digest: inspectWorkflow(s.workflow).sha256, status: 'interrupted',
131
+ calls: s.report?.calls ?? { agent: 0, judge: 0, tool: 0 }, events: s.report?.events ?? [],
132
+ ...(s.report?.uncertainEffects ? { uncertainEffects: s.report.uncertainEffects } : {}),
133
+ error: { code: 'session_interrupted', message: 'This run has no retained completion record. It was not restarted. Recorded counts may be incomplete; reconcile any admitted effects before starting a new run.' } };
134
+ if (s.observation)
135
+ for (const step of Object.values(s.observation.steps))
136
+ if (step.status === 'running')
137
+ step.status = 'interrupted';
138
+ return s;
139
+ }
140
+ /** Caller supplies Pi's active branch, not the complete session tree. */
141
+ export function restoreWorkflowSession(branch) {
142
+ if (!Array.isArray(branch) || types.isProxy(branch))
143
+ return undefined;
144
+ for (let i = branch.length - 1; i >= 0; i--) {
145
+ const snapshot = snapshotEntry(Object.getOwnPropertyDescriptor(branch, String(i))?.value);
146
+ if (snapshot)
147
+ return interrupted(snapshot);
148
+ }
149
+ return undefined;
150
+ }
151
+ /** Newest first, one terminal record per run; unmatched starts are interruptions. */
152
+ export function workflowRunHistory(branch) {
153
+ const results = [], seen = new Set();
154
+ if (!Array.isArray(branch) || types.isProxy(branch))
155
+ return results;
156
+ for (let i = branch.length - 1; i >= 0 && results.length < 20; i--) {
157
+ const snapshot = snapshotEntry(Object.getOwnPropertyDescriptor(branch, String(i))?.value);
158
+ if (!snapshot || !snapshot.report && !snapshot.running)
159
+ continue;
160
+ const key = snapshot.runId ?? `entry:${i}`;
161
+ if (seen.has(key))
162
+ continue;
163
+ seen.add(key);
164
+ results.push(interrupted(snapshot));
165
+ }
166
+ return results;
167
+ }
@@ -0,0 +1,30 @@
1
+ import { type Workflow } from '@parcha/agentrun-dsl';
2
+ export interface WorkflowRevision {
3
+ name: string;
4
+ digest: string;
5
+ createdAt: string;
6
+ workflowName: string;
7
+ }
8
+ export interface StoredWorkflow extends WorkflowRevision {
9
+ workflow: Workflow;
10
+ }
11
+ export declare class WorkflowStoreError extends Error {
12
+ readonly code: 'invalid_name' | 'invalid_revision' | 'invalid_workflow' | 'unsafe_path' | 'not_found' | 'invalid_record' | 'ambiguous_revision';
13
+ constructor(code: 'invalid_name' | 'invalid_revision' | 'invalid_workflow' | 'unsafe_path' | 'not_found' | 'invalid_record' | 'ambiguous_revision', message: string);
14
+ }
15
+ /** Local procedure revisions, not execution state or authority. The host still
16
+ * preflights loaded workflows against current capabilities and approves each run.
17
+ * This is not a sandbox against a process concurrently replacing project directories.
18
+ */
19
+ export declare class WorkflowStore {
20
+ private readonly projectDirectory;
21
+ constructor(projectDirectory: string);
22
+ private directory;
23
+ private read;
24
+ private latest;
25
+ /** All revisions, newest first; the active revision leads within a timestamp tie. */
26
+ list(): Promise<WorkflowRevision[]>;
27
+ load(name: string, digest?: string): Promise<StoredWorkflow>;
28
+ /** Publish a complete immutable revision atomically; identical saves are idempotent. */
29
+ save(name: string, value: unknown): Promise<StoredWorkflow>;
30
+ }