@parcha/agentrun-pi 0.1.0-beta.2 → 0.1.0-beta.21

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 (114) hide show
  1. package/README.md +123 -32
  2. package/dist/cli.js +15 -4
  3. package/dist/demo-cli.d.ts +2 -0
  4. package/dist/demo-cli.js +25 -0
  5. package/dist/demo.js +1 -0
  6. package/dist/durable/decision-recovery.d.ts +23 -0
  7. package/dist/durable/decision-recovery.js +57 -0
  8. package/dist/durable/decisions.d.ts +113 -0
  9. package/dist/durable/decisions.js +71 -0
  10. package/dist/durable/index.d.ts +20 -0
  11. package/dist/durable/index.js +14 -0
  12. package/dist/durable/node-runner.d.ts +106 -0
  13. package/dist/durable/node-runner.js +253 -0
  14. package/dist/durable/node.d.ts +88 -0
  15. package/dist/durable/node.js +98 -0
  16. package/dist/durable/pi-versions.d.ts +34 -0
  17. package/dist/durable/pi-versions.js +66 -0
  18. package/dist/durable/readers.d.ts +28 -0
  19. package/dist/durable/readers.js +103 -0
  20. package/dist/durable/receipts.d.ts +54 -0
  21. package/dist/durable/receipts.js +92 -0
  22. package/dist/durable/record-file.d.ts +10 -0
  23. package/dist/durable/record-file.js +56 -0
  24. package/dist/durable/record-tool.d.ts +76 -0
  25. package/dist/durable/record-tool.js +123 -0
  26. package/dist/durable/record.d.ts +157 -0
  27. package/dist/durable/record.js +402 -0
  28. package/dist/durable/recording.d.ts +90 -0
  29. package/dist/durable/recording.js +292 -0
  30. package/dist/durable/store.d.ts +23 -0
  31. package/dist/durable/store.js +242 -0
  32. package/dist/durable/task.d.ts +133 -0
  33. package/dist/durable/task.js +411 -0
  34. package/dist/durable/workflow-tool.d.ts +32 -0
  35. package/dist/durable/workflow-tool.js +188 -0
  36. package/dist/extension-service.js +63 -22
  37. package/dist/extension.d.ts +2 -0
  38. package/dist/extension.js +463 -80
  39. package/dist/host-addendum.d.ts +4 -0
  40. package/dist/host-addendum.js +20 -0
  41. package/dist/host-session.js +5 -2
  42. package/dist/index.d.ts +1 -2
  43. package/dist/index.js +1 -2
  44. package/dist/pi-host.d.ts +7 -0
  45. package/dist/pi-host.js +22 -0
  46. package/dist/presentation.js +2 -2
  47. package/dist/run-error.d.ts +6 -0
  48. package/dist/run-error.js +13 -0
  49. package/dist/runner.d.ts +1 -6
  50. package/dist/runner.js +7 -13
  51. package/dist/signin/index.d.ts +9 -0
  52. package/dist/signin/index.js +7 -0
  53. package/dist/signin/limit.d.ts +10 -0
  54. package/dist/signin/limit.js +23 -0
  55. package/dist/signin/resolve.d.ts +47 -0
  56. package/dist/signin/resolve.js +163 -0
  57. package/dist/signin/signin.d.ts +44 -0
  58. package/dist/signin/signin.js +126 -0
  59. package/dist/signin/spec.d.ts +116 -0
  60. package/dist/signin/spec.js +181 -0
  61. package/dist/signin/store.d.ts +40 -0
  62. package/dist/signin/store.js +206 -0
  63. package/dist/skills/author/SKILL.md +138 -0
  64. package/{skills → dist/skills}/author/references/jev-decisions.md +27 -14
  65. package/dist/skills/author/references/language.md +134 -0
  66. package/dist/skills/author/references/sdk-authoring.md +11 -0
  67. package/{skills → dist/skills}/author/references/workflow-format.md +7 -15
  68. package/dist/sop.d.ts +12 -0
  69. package/dist/sop.js +29 -0
  70. package/dist/thinking.d.ts +3 -0
  71. package/dist/thinking.js +7 -0
  72. package/dist/tool-context.d.ts +21 -0
  73. package/dist/tool-context.js +13 -0
  74. package/dist/triage-demo.d.ts +62 -0
  75. package/dist/triage-demo.js +153 -0
  76. package/dist/types.d.ts +7 -0
  77. package/dist/workflow-inspector.d.ts +47 -0
  78. package/dist/workflow-inspector.js +235 -0
  79. package/dist/workflow-session.d.ts +23 -0
  80. package/dist/workflow-session.js +168 -0
  81. package/dist/workflow-store.d.ts +36 -0
  82. package/dist/workflow-store.js +255 -0
  83. package/dist/workflow-tool.d.ts +81 -0
  84. package/dist/workflow-tool.js +94 -0
  85. package/dist/workflow-view.d.ts +64 -0
  86. package/dist/workflow-view.js +273 -0
  87. package/dist/workflows.d.ts +1 -0
  88. package/dist/workflows.js +3 -0
  89. package/package.json +57 -11
  90. package/dist/admission.d.ts +0 -6
  91. package/dist/admission.js +0 -51
  92. package/dist/author.d.ts +0 -21
  93. package/dist/author.js +0 -72
  94. package/dist/benchmark-contract.d.ts +0 -88
  95. package/dist/benchmark-contract.js +0 -50
  96. package/dist/benchmark-freeze.d.ts +0 -9
  97. package/dist/benchmark-freeze.js +0 -9
  98. package/dist/benchmark.d.ts +0 -127
  99. package/dist/benchmark.js +0 -260
  100. package/dist/comparison.d.ts +0 -69
  101. package/dist/comparison.js +0 -229
  102. package/dist/skill-bundle.d.ts +0 -5
  103. package/dist/skill-bundle.js +0 -15
  104. package/skills/author/SKILL.md +0 -102
  105. package/skills/author/references/sdk-authoring.md +0 -16
  106. /package/{skills → dist/skills}/author/examples/evidence-gate.input.json +0 -0
  107. /package/{skills → dist/skills}/author/examples/evidence-gate.json +0 -0
  108. /package/{skills → dist/skills}/author/examples/read-file.input.json +0 -0
  109. /package/{skills → dist/skills}/author/examples/read-file.json +0 -0
  110. /package/{skills → dist/skills}/author/examples/read-source-decision.input.json +0 -0
  111. /package/{skills → dist/skills}/author/examples/read-source-decision.json +0 -0
  112. /package/{skills → dist/skills}/author/examples/release-note.txt +0 -0
  113. /package/{skills → dist/skills}/author/examples/review-release-notes.input.json +0 -0
  114. /package/{skills → dist/skills}/author/examples/review-release-notes.json +0 -0
@@ -0,0 +1,36 @@
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
+ * Public through `@parcha/agentrun-pi/workflows`. The constructor takes a project directory, never the
20
+ * workflows directory itself: records live in `<projectDirectory>/.pi/agentrun/workflows/<name>/`, the same
21
+ * place the extension keeps them for its `ctx.cwd`, so the extension and a host that passes that directory
22
+ * read each other's records. The directory is resolved once when it is used; everything under it must be
23
+ * real directories and regular files.
24
+ */
25
+ export declare class WorkflowStore {
26
+ private readonly projectDirectory;
27
+ constructor(projectDirectory: string);
28
+ private directory;
29
+ private read;
30
+ private latest;
31
+ /** All revisions, newest first; the active revision leads within a timestamp tie. */
32
+ list(): Promise<WorkflowRevision[]>;
33
+ load(name: string, digest?: string): Promise<StoredWorkflow>;
34
+ /** Publish a complete immutable revision atomically; identical saves are idempotent. */
35
+ save(name: string, value: unknown): Promise<StoredWorkflow>;
36
+ }
@@ -0,0 +1,255 @@
1
+ import { constants } from 'node:fs';
2
+ import { link, lstat, mkdir, open, readdir, realpath, rename, unlink } from 'node:fs/promises';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { join, resolve } from 'node:path';
5
+ import { inspectWorkflow, validateWorkflow } from '@parcha/agentrun-dsl';
6
+ export class WorkflowStoreError extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = 'WorkflowStoreError';
12
+ }
13
+ }
14
+ const assertValidName = (name) => {
15
+ if (typeof name !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name))
16
+ throw new WorkflowStoreError('invalid_name', 'Use a workflow name of 1–64 lowercase letters, digits, hyphens or underscores, starting with a letter or digit.');
17
+ };
18
+ const assertValidDigest = (digest) => {
19
+ if (typeof digest !== 'string' || !/^[a-f0-9]{64}$/.test(digest))
20
+ throw new WorkflowStoreError('invalid_revision', 'A workflow revision must be its complete SHA-256 digest.');
21
+ };
22
+ const missing = (error) => error?.code === 'ENOENT';
23
+ const unsafe = () => new WorkflowStoreError('unsafe_path', 'Workflow storage requires real directories and regular files, without symbolic links.');
24
+ function checkedWorkflow(value) {
25
+ try {
26
+ // Inspection rejects non-JSON values before cloning. Neither check executes code.
27
+ const inspection = inspectWorkflow(value);
28
+ const workflow = structuredClone(value);
29
+ const validation = validateWorkflow(workflow, { executeCode: false });
30
+ if (!validation.ok)
31
+ throw new Error('invalid workflow');
32
+ return { workflow, digest: inspection.sha256 };
33
+ }
34
+ catch {
35
+ throw new WorkflowStoreError('invalid_workflow', 'Workflow failed nonexecuting structure, syntax or schema validation. Inspect it before saving.');
36
+ }
37
+ }
38
+ /** Local procedure revisions, not execution state or authority. The host still
39
+ * preflights loaded workflows against current capabilities and approves each run.
40
+ * This is not a sandbox against a process concurrently replacing project directories.
41
+ *
42
+ * Public through `@parcha/agentrun-pi/workflows`. The constructor takes a project directory, never the
43
+ * workflows directory itself: records live in `<projectDirectory>/.pi/agentrun/workflows/<name>/`, the same
44
+ * place the extension keeps them for its `ctx.cwd`, so the extension and a host that passes that directory
45
+ * read each other's records. The directory is resolved once when it is used; everything under it must be
46
+ * real directories and regular files.
47
+ */
48
+ export class WorkflowStore {
49
+ projectDirectory;
50
+ constructor(projectDirectory) { this.projectDirectory = resolve(projectDirectory); }
51
+ async directory(name, create = false) {
52
+ if (name !== undefined)
53
+ assertValidName(name);
54
+ let directory = await realpath(this.projectDirectory);
55
+ for (const component of ['.pi', 'agentrun', 'workflows', ...(name === undefined ? [] : [name])]) {
56
+ directory = join(directory, component);
57
+ if (create) {
58
+ try {
59
+ await mkdir(directory, { mode: 0o700 });
60
+ }
61
+ catch (error) {
62
+ if (error.code !== 'EEXIST')
63
+ throw error;
64
+ }
65
+ }
66
+ let stat;
67
+ try {
68
+ stat = await lstat(directory);
69
+ }
70
+ catch (error) {
71
+ if (missing(error) && !create)
72
+ return undefined;
73
+ throw error;
74
+ }
75
+ if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(directory) !== directory)
76
+ throw unsafe();
77
+ }
78
+ return directory;
79
+ }
80
+ async read(name, digest) {
81
+ assertValidName(name);
82
+ assertValidDigest(digest);
83
+ const directory = await this.directory(name);
84
+ if (!directory)
85
+ throw new WorkflowStoreError('not_found', 'Saved workflow revision not found.');
86
+ const path = join(directory, `${digest}.json`);
87
+ let handle;
88
+ try {
89
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
90
+ }
91
+ catch (error) {
92
+ if (missing(error))
93
+ throw new WorkflowStoreError('not_found', 'Saved workflow revision not found.');
94
+ if (error.code === 'ELOOP')
95
+ throw unsafe();
96
+ throw error;
97
+ }
98
+ let value;
99
+ try {
100
+ if (!(await handle.stat()).isFile() || await realpath(path) !== path)
101
+ throw unsafe();
102
+ try {
103
+ value = JSON.parse(await handle.readFile('utf8'));
104
+ }
105
+ catch {
106
+ throw new WorkflowStoreError('invalid_record', 'Saved workflow record is not valid JSON.');
107
+ }
108
+ }
109
+ finally {
110
+ await handle.close();
111
+ }
112
+ if (!value || typeof value !== 'object' || Array.isArray(value)
113
+ || Object.keys(value).sort().join(',') !== 'createdAt,digest,name,version,workflow'
114
+ || value.version !== 1 || value.name !== name || value.digest !== digest
115
+ || typeof value.createdAt !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value.createdAt)
116
+ || !Number.isFinite(Date.parse(value.createdAt)) || new Date(value.createdAt).toISOString() !== value.createdAt)
117
+ throw new WorkflowStoreError('invalid_record', 'Saved workflow metadata does not match its revision.');
118
+ const checked = checkedWorkflow(value.workflow);
119
+ if (checked.digest !== digest)
120
+ throw new WorkflowStoreError('invalid_record', 'Saved workflow content does not match its digest.');
121
+ return { name, digest, createdAt: value.createdAt, workflowName: checked.workflow.name, workflow: checked.workflow };
122
+ }
123
+ async latest(name) {
124
+ const directory = await this.directory(name);
125
+ if (!directory)
126
+ return undefined;
127
+ const path = join(directory, 'latest');
128
+ let handle;
129
+ try {
130
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
131
+ }
132
+ catch (error) {
133
+ if (missing(error))
134
+ return undefined;
135
+ if (error.code === 'ELOOP')
136
+ throw unsafe();
137
+ throw error;
138
+ }
139
+ try {
140
+ const stat = await handle.stat();
141
+ if (!stat.isFile() || await realpath(path) !== path)
142
+ throw unsafe();
143
+ if (stat.size !== 65)
144
+ throw new WorkflowStoreError('invalid_record', 'Saved workflow latest revision is invalid.');
145
+ const value = await handle.readFile('utf8');
146
+ if (!/^[a-f0-9]{64}\n$/.test(value))
147
+ throw new WorkflowStoreError('invalid_record', 'Saved workflow latest revision is invalid.');
148
+ return value.slice(0, -1);
149
+ }
150
+ finally {
151
+ await handle.close();
152
+ }
153
+ }
154
+ /** All revisions, newest first; the active revision leads within a timestamp tie. */
155
+ async list() {
156
+ const directory = await this.directory();
157
+ if (!directory)
158
+ return [];
159
+ const revisions = [];
160
+ const latest = new Map();
161
+ for (const name of await readdir(directory)) {
162
+ assertValidName(name);
163
+ const child = await this.directory(name);
164
+ if (!child)
165
+ continue;
166
+ const active = await this.latest(name);
167
+ if (active)
168
+ latest.set(name, active);
169
+ for (const file of await readdir(child)) {
170
+ // Unpublished files survive an interrupted write and never become revisions.
171
+ if (/^\.pending-[a-f0-9-]+$/.test(file) || file === 'latest')
172
+ continue;
173
+ if (!/^[a-f0-9]{64}\.json$/.test(file))
174
+ throw new WorkflowStoreError('invalid_record', 'Unexpected file in workflow revision storage.');
175
+ const { workflow: _workflow, ...revision } = await this.read(name, file.slice(0, -5));
176
+ revisions.push(revision);
177
+ }
178
+ }
179
+ return revisions.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || a.name.localeCompare(b.name)
180
+ || Number(b.digest === latest.get(b.name)) - Number(a.digest === latest.get(a.name)) || a.digest.localeCompare(b.digest));
181
+ }
182
+ async load(name, digest) {
183
+ assertValidName(name);
184
+ if (digest !== undefined)
185
+ return this.read(name, digest);
186
+ const latest = await this.latest(name);
187
+ if (latest)
188
+ return this.read(name, latest);
189
+ // Revisions saved before the latest pointer was introduced remain readable.
190
+ const revisions = (await this.list()).filter(item => item.name === name);
191
+ const revision = revisions[0];
192
+ if (!revision)
193
+ throw new WorkflowStoreError('not_found', 'Saved workflow not found.');
194
+ if (revisions[1]?.createdAt === revision.createdAt)
195
+ throw new WorkflowStoreError('ambiguous_revision', 'Saved workflow revisions share a timestamp. Load by complete digest or save the intended revision again.');
196
+ return this.read(name, revision.digest);
197
+ }
198
+ /** Publish a complete immutable revision atomically; identical saves are idempotent. */
199
+ async save(name, value) {
200
+ assertValidName(name);
201
+ const { workflow, digest } = checkedWorkflow(value);
202
+ const directory = (await this.directory(name, true));
203
+ const temporary = join(directory, `.pending-${randomUUID()}`);
204
+ const target = join(directory, `${digest}.json`);
205
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
206
+ try {
207
+ await handle.writeFile(JSON.stringify({ version: 1, name, digest, createdAt: new Date().toISOString(), workflow }, null, 2) + '\n');
208
+ await handle.sync();
209
+ }
210
+ finally {
211
+ await handle.close();
212
+ }
213
+ // Hard-link publication is atomic and, unlike rename, never overwrites a revision.
214
+ if (await this.directory(name) !== directory)
215
+ throw unsafe();
216
+ try {
217
+ await link(temporary, target);
218
+ }
219
+ catch (error) {
220
+ if (error.code !== 'EEXIST')
221
+ throw error;
222
+ }
223
+ const saved = await this.read(name, digest);
224
+ await unlink(temporary);
225
+ const parent = await open(directory, constants.O_RDONLY | constants.O_NOFOLLOW);
226
+ try {
227
+ await parent.sync();
228
+ }
229
+ finally {
230
+ await parent.close();
231
+ }
232
+ // This rename is the linearization point for "load by name", even when
233
+ // different revisions have identical millisecond timestamps.
234
+ const pointerTemporary = join(directory, `.pending-${randomUUID()}`);
235
+ const pointer = await open(pointerTemporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
236
+ try {
237
+ await pointer.writeFile(`${digest}\n`);
238
+ await pointer.sync();
239
+ }
240
+ finally {
241
+ await pointer.close();
242
+ }
243
+ if (await this.directory(name) !== directory)
244
+ throw unsafe();
245
+ await rename(pointerTemporary, join(directory, 'latest'));
246
+ const updatedParent = await open(directory, constants.O_RDONLY | constants.O_NOFOLLOW);
247
+ try {
248
+ await updatedParent.sync();
249
+ }
250
+ finally {
251
+ await updatedParent.close();
252
+ }
253
+ return saved;
254
+ }
255
+ }
@@ -0,0 +1,81 @@
1
+ import { Type } from 'typebox';
2
+ import { type AuthorHostAddendum, type DryRunResult, type WorkflowDeps } from '@parcha/agentrun-dsl';
3
+ import type { WorkflowExtensionService } from './extension-service.js';
4
+ export declare const WORKFLOW_TOOL_ACTIONS: readonly ["describe", "inspect", "dry-run", "run", "save", "load", "list"];
5
+ /** The tool's fields: one action, and the fields an action reads. A host's tool may add its own. */
6
+ export declare const workflowToolProperties: () => {
7
+ action: Type.TUnion<Type.TLiteral<"run" | "dry-run" | "save" | "load" | "describe" | "inspect" | "list">[]>;
8
+ name: Type.TOptional<Type.TString>;
9
+ digest: Type.TOptional<Type.TString>;
10
+ workflow: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
11
+ input: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
12
+ };
13
+ /** The tool's parameters. */
14
+ export declare const workflowToolParameters: () => Type.TObject<{
15
+ action: Type.TUnion<Type.TLiteral<"run" | "dry-run" | "save" | "load" | "describe" | "inspect" | "list">[]>;
16
+ name: Type.TOptional<Type.TString>;
17
+ digest: Type.TOptional<Type.TString>;
18
+ workflow: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
19
+ input: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
20
+ }>;
21
+ /** What describe says of each tool a workflow may call: its schemas, and the result contract it is called under. */
22
+ export declare const describeTools: (tools: readonly {
23
+ name: string;
24
+ description?: string;
25
+ parameters: unknown;
26
+ resultSchema?: unknown;
27
+ }[]) => {
28
+ name: string;
29
+ description: string | undefined;
30
+ parameters: unknown;
31
+ resultSchema: {};
32
+ resultContract: string;
33
+ }[];
34
+ /** The author references describe returns, with the host's addendum (the coding-agent extension's by default). */
35
+ export declare const authoringReferences: (host?: AuthorHostAddendum) => {
36
+ language: string;
37
+ workflow: string;
38
+ jev: string;
39
+ host: string;
40
+ };
41
+ /** The checks that run no code (admission by `checker`, structure, a supplied input), and the workflow's tree. A
42
+ * refusal throws, naming why. */
43
+ export declare function inspectCandidate(checker: WorkflowExtensionService, candidate: unknown, input?: Record<string, unknown>): {
44
+ inspection: import("@parcha/agentrun-dsl").WorkflowInspection;
45
+ tree: string;
46
+ };
47
+ /** A dry run's answer. `status` says whether every check ran: `passed`, `not_run` (a check could not run; `skipped`
48
+ * says which, so it is no proof the workflow works) or `failed` (`problems` say why). */
49
+ export type WorkflowDryRun = {
50
+ status: 'passed' | 'not_run';
51
+ ok: true;
52
+ skipped?: string[];
53
+ } | {
54
+ status: 'failed';
55
+ ok: false;
56
+ problems: string[];
57
+ stage?: string;
58
+ };
59
+ export type DryRunDraftOptions = {
60
+ /** The input to run on; without one the dry run synthesizes it from the workflow's input schema. */
61
+ input?: Record<string, unknown>;
62
+ /** The SOP text for a workflow that names sections. */
63
+ sop?: string;
64
+ /** Why no SOP text is available, said when the workflow names sections. */
65
+ sopUnavailable?: string;
66
+ /** The operator authorized code for this dry run: code nodes run in this process. A host's `runCode` runs them instead. */
67
+ trusted?: boolean;
68
+ runCode?: WorkflowDeps['runCode'];
69
+ /** How code is authorized in this host, named when a code node does not run. */
70
+ trustedCommand?: string;
71
+ /** Ends the dry run at its next step, as not run. */
72
+ signal?: AbortSignal;
73
+ };
74
+ /** The status of a dry run's result: its own once the DSL gives one, else read from `ok` and `skipped`. */
75
+ export declare function dryRunStatus(result: DryRunResult): WorkflowDryRun['status'];
76
+ /** Dry-runs an inspected draft on synthetic values, or `options.input`: model, Jev and tool calls are synthesized, so
77
+ * nothing is paid and nothing outside this process is touched. Code runs only when authorized, and a code failure is
78
+ * reported without what it threw. */
79
+ export declare function dryRunDraft(draft: unknown, options?: DryRunDraftOptions): Promise<WorkflowDryRun>;
80
+ /** A dry run as a person reads it. */
81
+ export declare function formatDryRun(report: WorkflowDryRun): string;
@@ -0,0 +1,94 @@
1
+ // The agentrun tool's own work, for any host that offers it to an agent: its actions and parameters, describe, inspect
2
+ // and the dry run. Nothing here imports pi's coding agent, its agent core or its TUI, so a pi-durable host can use it.
3
+ import { Type } from 'typebox';
4
+ import { compileTransform, dryRunWorkflow, formatWorkflowTree, inspectWorkflow, loadAuthorReference, renderAuthorHostAddendum, } from '@parcha/agentrun-dsl';
5
+ import { PI_HOST_ADDENDUM } from './host-addendum.js';
6
+ export const WORKFLOW_TOOL_ACTIONS = ['describe', 'inspect', 'dry-run', 'run', 'save', 'load', 'list'];
7
+ /** The tool's fields: one action, and the fields an action reads. A host's tool may add its own. */
8
+ export const workflowToolProperties = () => ({ action: Type.Union(WORKFLOW_TOOL_ACTIONS.map(value => Type.Literal(value))),
9
+ name: Type.Optional(Type.String({ description: 'Saved procedure name: lowercase letters, digits, hyphens or underscores.' })),
10
+ digest: Type.Optional(Type.String({ description: 'Optional exact saved SHA-256 revision to load.' })),
11
+ workflow: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
12
+ description: 'The complete AgentRun workflow JSON object. Pass the object directly, not a JSON-encoded string. Inspection validates its DSL structure.',
13
+ })), input: Type.Optional(Type.Record(Type.String(), Type.Unknown())) });
14
+ /** The tool's parameters. */
15
+ export const workflowToolParameters = () => Type.Object(workflowToolProperties());
16
+ /** What describe says of each tool a workflow may call: its schemas, and the result contract it is called under. */
17
+ export const describeTools = (tools) => tools.map(t => ({
18
+ name: t.name, description: t.description, parameters: t.parameters,
19
+ resultSchema: t.resultSchema ?? { type: 'object', properties: { content: { type: 'array', items: { type: 'object' } }, details: {} }, required: ['content'] },
20
+ resultContract: t.resultSchema ? 'Host-declared direct workflow call result.' : 'Pi result envelope; tool-specific details are not declared. Do not invent fields inside details.',
21
+ }));
22
+ /** The author references describe returns, with the host's addendum (the coding-agent extension's by default). */
23
+ export const authoringReferences = (host = PI_HOST_ADDENDUM) => ({
24
+ language: loadAuthorReference('language'), workflow: loadAuthorReference('workflow-format'), jev: loadAuthorReference('jev-decisions'),
25
+ host: renderAuthorHostAddendum(host),
26
+ });
27
+ /** The checks that run no code (admission by `checker`, structure, a supplied input), and the workflow's tree. A
28
+ * refusal throws, naming why. */
29
+ export function inspectCandidate(checker, candidate, input) {
30
+ if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) {
31
+ throw new Error('Cannot inspect workflow: workflow must be a JSON object, not a JSON-encoded string or array. Pass the complete object directly.');
32
+ }
33
+ let inspection;
34
+ try {
35
+ inspection = checker.preflight(candidate, input);
36
+ }
37
+ catch (error) {
38
+ throw new Error(`Cannot inspect workflow: ${error instanceof Error ? error.message : 'Invalid workflow'}`);
39
+ }
40
+ return { inspection, tree: formatWorkflowTree(inspection) };
41
+ }
42
+ /** The status of a dry run's result: its own once the DSL gives one, else read from `ok` and `skipped`. */
43
+ export function dryRunStatus(result) {
44
+ const given = result.status;
45
+ if (given === 'passed' || given === 'not_run' || given === 'failed')
46
+ return given;
47
+ return !result.ok ? 'failed' : result.skipped?.length ? 'not_run' : 'passed';
48
+ }
49
+ /** A code node run in this process, as the interpreter runs one; a failure names the node and not what it threw. */
50
+ const inProcessCode = async (node, state, ctx) => {
51
+ let transform;
52
+ try {
53
+ transform = compileTransform(node.code);
54
+ }
55
+ catch {
56
+ throw new Error(`code node "${ctx.label}" did not compile`);
57
+ }
58
+ try {
59
+ return transform(state, { sop: ctx.sop });
60
+ }
61
+ catch {
62
+ throw new Error(`code node "${ctx.label}" threw; what it threw is not shown`);
63
+ }
64
+ };
65
+ /** Never reached: a draft with code runs it only when authorized, and this keeps validation from running any. */
66
+ const noCode = async (_node, _state, ctx) => { throw new Error(`code node "${ctx.label}" did not run`); };
67
+ /** Dry-runs an inspected draft on synthetic values, or `options.input`: model, Jev and tool calls are synthesized, so
68
+ * nothing is paid and nothing outside this process is touched. Code runs only when authorized, and a code failure is
69
+ * reported without what it threw. */
70
+ export async function dryRunDraft(draft, options = {}) {
71
+ const inspection = inspectWorkflow(draft);
72
+ const code = inspection.nodes.filter(node => node.kind === 'code').map(node => JSON.stringify(node.label));
73
+ if (code.length && !options.trusted && !options.runCode) {
74
+ return { status: 'not_run', ok: true, skipped: [`Code node${code.length === 1 ? '' : 's'} ${code.join(', ')} did not run, and nothing else ran: code runs only ${options.trustedCommand ? `with the user command ${options.trustedCommand}` : 'with the host\'s code executor'}.`] };
75
+ }
76
+ const sections = inspection.requires.sopSections;
77
+ if (sections.length && options.sop === undefined) {
78
+ return { status: 'not_run', ok: true, skipped: [options.sopUnavailable ?? `The workflow needs SOP text for ${sections.map(name => JSON.stringify(name)).join(', ')}, and none was supplied.`] };
79
+ }
80
+ const result = await dryRunWorkflow(draft, {
81
+ ...(options.input === undefined ? {} : { input: options.input }), ...(options.sop === undefined ? {} : { sop: options.sop }),
82
+ ...(options.signal ? { signal: options.signal } : {}),
83
+ runCode: options.runCode ?? (code.length ? inProcessCode : noCode),
84
+ });
85
+ return { ...result, status: dryRunStatus(result) };
86
+ }
87
+ /** A dry run as a person reads it. */
88
+ export function formatDryRun(report) {
89
+ const head = !report.ok ? `Dry run failed${report.stage ? ` at "${report.stage}"` : ''}.`
90
+ : report.status === 'passed' ? 'Dry run passed: every step ran on synthetic values. It checks the wiring, not the answers.'
91
+ : 'Dry run not run: a check could not run, so this is not proof the workflow works.';
92
+ const lines = report.ok ? report.skipped ?? [] : report.problems;
93
+ return [`${head} No model, Jev or tool call was made.`, ...lines.map(line => `- ${line}`)].join('\n');
94
+ }
@@ -0,0 +1,64 @@
1
+ import { type WorkflowDeps } from '@parcha/agentrun-dsl';
2
+ import type { ExtensionRunReport } from './extension-service.js';
3
+ type Event = Parameters<NonNullable<WorkflowDeps['onEvent']>>[0];
4
+ type JudgeRequest = Parameters<NonNullable<WorkflowDeps['runJudge']>>[0];
5
+ export interface WorkflowViewNode {
6
+ path: string;
7
+ label: string;
8
+ kind: string;
9
+ status: string;
10
+ summary: string;
11
+ details: string[];
12
+ }
13
+ export interface WorkflowView {
14
+ title: string;
15
+ digest: string;
16
+ status: string;
17
+ summary: string[];
18
+ nodes: WorkflowViewNode[];
19
+ output?: string[];
20
+ }
21
+ export interface DecisionEvidence {
22
+ path: string;
23
+ label: string;
24
+ questions: string;
25
+ evidence: string;
26
+ answer?: string;
27
+ accepted?: boolean;
28
+ }
29
+ export interface RunObservation {
30
+ steps: Record<string, {
31
+ label: string;
32
+ kind: string;
33
+ status: string;
34
+ }>;
35
+ decisions: DecisionEvidence[];
36
+ notes: Record<string, string>;
37
+ omitted: number;
38
+ routes?: Record<string, string>;
39
+ }
40
+ /** Display limits never admit, cancel, or modify execution. */
41
+ export declare function readable(value: unknown, max?: number): string;
42
+ export declare class WorkflowObservation {
43
+ readonly data: RunObservation;
44
+ private decisionBytes;
45
+ record(event: Event): void;
46
+ private recordSafe;
47
+ /** Capture the exact supplied questions/state, not an explanation invented afterward. */
48
+ requested(request: JudgeRequest): DecisionEvidence | undefined;
49
+ observe(deps: WorkflowDeps): WorkflowDeps;
50
+ }
51
+ export declare function progressLines(observation: RunObservation, terminalStatus?: string): string[];
52
+ export declare function workflowView(workflow: unknown, options?: {
53
+ input?: Record<string, unknown>;
54
+ observation?: RunObservation;
55
+ report?: ExtensionRunReport;
56
+ running?: boolean;
57
+ mode?: string;
58
+ model?: string;
59
+ limits?: Record<string, number | null>;
60
+ availableTools?: string[];
61
+ savedName?: string;
62
+ }): WorkflowView;
63
+ export declare function formatWorkflowView(view: WorkflowView): string;
64
+ export {};