@parcha/agentrun-pi 0.1.0-beta.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.
Files changed (45) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +209 -0
  4. package/dist/admission.d.ts +6 -0
  5. package/dist/admission.js +51 -0
  6. package/dist/author.d.ts +21 -0
  7. package/dist/author.js +72 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +68 -0
  10. package/dist/demo-data.d.ts +361 -0
  11. package/dist/demo-data.js +386 -0
  12. package/dist/demo.d.ts +13 -0
  13. package/dist/demo.js +44 -0
  14. package/dist/extension-service.d.ts +110 -0
  15. package/dist/extension-service.js +477 -0
  16. package/dist/extension.d.ts +41 -0
  17. package/dist/extension.js +419 -0
  18. package/dist/host-session.d.ts +5 -0
  19. package/dist/host-session.js +80 -0
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +6 -0
  22. package/dist/presentation.d.ts +4 -0
  23. package/dist/presentation.js +34 -0
  24. package/dist/runner.d.ts +12 -0
  25. package/dist/runner.js +256 -0
  26. package/dist/skill-bundle.d.ts +5 -0
  27. package/dist/skill-bundle.js +15 -0
  28. package/dist/tool-input-error.d.ts +10 -0
  29. package/dist/tool-input-error.js +40 -0
  30. package/dist/types.d.ts +119 -0
  31. package/dist/types.js +1 -0
  32. package/package.json +73 -0
  33. package/skills/author/SKILL.md +102 -0
  34. package/skills/author/examples/evidence-gate.input.json +6 -0
  35. package/skills/author/examples/evidence-gate.json +125 -0
  36. package/skills/author/examples/read-file.input.json +1 -0
  37. package/skills/author/examples/read-file.json +44 -0
  38. package/skills/author/examples/read-source-decision.input.json +4 -0
  39. package/skills/author/examples/read-source-decision.json +37 -0
  40. package/skills/author/examples/release-note.txt +10 -0
  41. package/skills/author/examples/review-release-notes.input.json +6 -0
  42. package/skills/author/examples/review-release-notes.json +123 -0
  43. package/skills/author/references/jev-decisions.md +112 -0
  44. package/skills/author/references/sdk-authoring.md +16 -0
  45. package/skills/author/references/workflow-format.md +145 -0
@@ -0,0 +1,477 @@
1
+ import { types as utilTypes } from 'node:util';
2
+ import { JevError, isJevResponseReason } from '@parcha/agentrun-jev';
3
+ import { PiRunError } from './runner.js';
4
+ import { ToolInputValidationError } from './tool-input-error.js';
5
+ import { inspectWorkflow, runWorkflow, validateWorkflow, EffectOutcomeUnknownError, WorkflowInvalidError, WorkflowInputInvalidError, WorkflowOutputInvalidError, WorkflowCodeError, WorkflowStateError, EffectDeadlineExceededError, } from '@parcha/agentrun-dsl';
6
+ import { workflowPolicyErrors } from './admission.js';
7
+ export class ExtensionServiceError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ this.name = 'ExtensionServiceError';
13
+ }
14
+ }
15
+ const ceilings = {
16
+ deadlineMs: 600_000, maxAgentCalls: 50, maxJudgeCalls: 100, maxToolCalls: 100,
17
+ maxConcurrency: 8, maxTraceBytes: 2 * 1024 * 1024, maxEventBytes: 2 * 1024 * 1024,
18
+ };
19
+ const defaults = { ...ceilings, deadlineMs: 120_000, maxConcurrency: 4, maxTraceBytes: 1024 * 1024, maxEventBytes: 1024 * 1024 };
20
+ export const extensionStructuralLimits = Object.freeze({ maxNodes: 200, maxMapConcurrency: 8, maxParallelBranches: 8 });
21
+ // Never expose arbitrary error.message/cause, provider bodies or submitted values.
22
+ class AdapterDiagnosticError extends Error {
23
+ diagnostic;
24
+ constructor(diagnostic) {
25
+ super(diagnostic.message);
26
+ this.diagnostic = diagnostic;
27
+ this.name = 'AdapterDiagnosticError';
28
+ }
29
+ }
30
+ async function withAdapterDiagnostic(stage, run) {
31
+ try {
32
+ return await run();
33
+ }
34
+ catch (error) {
35
+ let diagnostic;
36
+ if (error instanceof JevError) {
37
+ switch (error.code) {
38
+ case 'invalid_response':
39
+ diagnostic = { code: 'jev_invalid_response', message: 'Jev returned a response that failed the adapter contract. No judgment was accepted; provider details are omitted.' };
40
+ break;
41
+ case 'http':
42
+ diagnostic = { code: 'jev_http', message: 'Jev returned an HTTP failure. No judgment was accepted; provider details are omitted.' };
43
+ break;
44
+ case 'connection':
45
+ diagnostic = { code: 'jev_connection', message: 'Jev transport failed before an accepted judgment. Provider details are omitted.' };
46
+ break;
47
+ case 'timeout':
48
+ diagnostic = { code: 'jev_timeout', message: 'The Jev adapter deadline expired before an accepted judgment. Provider details are omitted.' };
49
+ break;
50
+ }
51
+ if (diagnostic) {
52
+ const reason = error.responseDiagnostic?.reason;
53
+ if (error.code === 'invalid_response' && isJevResponseReason(reason) && reason !== 'max_tokens_exceeded')
54
+ diagnostic.reason = reason;
55
+ if (error.code === 'http') {
56
+ if (Number.isInteger(error.status) && error.status >= 100 && error.status <= 599)
57
+ diagnostic.status = error.status;
58
+ if (reason === 'max_tokens_exceeded')
59
+ diagnostic.reason = reason;
60
+ }
61
+ }
62
+ }
63
+ else if (error instanceof PiRunError) {
64
+ switch (error.reason) {
65
+ case 'model_error':
66
+ diagnostic = { code: 'model_response_failed', message: 'The model session reported a response failure before an accepted submission. Provider details are omitted.' };
67
+ break;
68
+ case 'timeout':
69
+ diagnostic = { code: 'pi_timeout', message: 'The Pi node deadline expired before an accepted submission. Check slow tools or increase the host node timeout.' };
70
+ break;
71
+ case 'turn_limit':
72
+ diagnostic = { code: 'pi_turn_limit', message: 'The Pi node reached its turn limit before an accepted submission. Simplify the task or increase the host node turn limit.' };
73
+ break;
74
+ case 'submission_limit':
75
+ diagnostic = { code: 'pi_submission_limit', message: 'The Pi node exhausted its submission attempts. Correct schema or review rejections before retrying.' };
76
+ break;
77
+ case 'no_submission':
78
+ diagnostic = { code: 'pi_no_submission', message: 'The Pi node ended without an accepted submission. The model must call submit with a result that passes the schema and review.' };
79
+ break;
80
+ case 'aborted':
81
+ diagnostic = { code: 'pi_aborted', message: 'The Pi node was cancelled before an accepted submission. Check any effects already started before retrying.' };
82
+ break;
83
+ }
84
+ }
85
+ if (diagnostic)
86
+ throw new AdapterDiagnosticError({ ...diagnostic, stage: stage.slice(0, 200) });
87
+ // Adapter-created runtime errors are not interpreter-owned diagnostics.
88
+ if (error instanceof WorkflowInputInvalidError || error instanceof WorkflowOutputInvalidError
89
+ || error instanceof WorkflowInvalidError || error instanceof WorkflowStateError || error instanceof WorkflowCodeError) {
90
+ throw new Error('Host adapter failed.', { cause: error });
91
+ }
92
+ throw error;
93
+ }
94
+ }
95
+ function localDiagnostic(error) {
96
+ if (error instanceof AggregateError)
97
+ return error.errors.slice(0, 100).map(localDiagnostic).find(Boolean);
98
+ if (error instanceof AdapterDiagnosticError)
99
+ return error.diagnostic;
100
+ if (error instanceof ToolInputValidationError)
101
+ return {
102
+ code: error.code, stage: error.stage,
103
+ message: 'Tool arguments do not satisfy the registered input schema. Correct the listed schema problems before retrying; the tool implementation was not called.',
104
+ problems: error.problems,
105
+ };
106
+ if (error instanceof WorkflowStateError) {
107
+ const messages = {
108
+ required_nonempty: 'A required state path is missing or has no concrete value. requires is a nonempty-evidence guard, not just a field-existence check. Handle empty evidence explicitly.',
109
+ missing_interpolation: 'An interpolation path is absent. Check the upstream output schema and as/resultPath wrapping; no adapter was called for this failed binding.',
110
+ expected_list: 'The selected state path must contain an array. Check the upstream output and map resultPath.',
111
+ empty_selection: 'The selection list is empty and none is not allowed. Handle missing evidence explicitly.',
112
+ missing_map_result: 'A map body did not produce its resultPath. Select a path in the completed body state; a body with as is collected automatically when resultPath is omitted.',
113
+ parallel_write_conflict: 'Parallel branches changed the same top-level state key. Give independent branches distinct output keys and combine them after the parallel node.',
114
+ };
115
+ return { code: error.code, stage: error.stage.slice(0, 200),
116
+ // Parallel patch keys may be computed from source values, unlike authored paths.
117
+ ...(error.reason === 'parallel_write_conflict' ? {} : { path: error.path.slice(0, 500) }),
118
+ reason: error.reason, message: messages[error.reason] };
119
+ }
120
+ if (error instanceof JevError && error.code === 'invalid_request' && error.attempts === 0 && error.requestDiagnostic) {
121
+ const { reason, label, stateBytes, maxStateBytes } = error.requestDiagnostic;
122
+ const stage = typeof label === 'string' ? label.slice(0, 200) : 'judge';
123
+ if (reason === 'state_too_large' && Number.isSafeInteger(stateBytes) && Number.isSafeInteger(maxStateBytes)
124
+ && stateBytes > maxStateBytes && maxStateBytes > 0)
125
+ return {
126
+ code: 'jev_state_too_large', stage, stateBytes, maxStateBytes,
127
+ message: `Jev state at ${stage} is ${stateBytes} UTF-8 bytes; configured host limit is ${maxStateBytes}. No request was sent. A sift batches every item in one state; select original context explicitly or use per-item judgments without losing headers or qualifiers.`
128
+ };
129
+ if (reason === 'invalid_state' || reason === 'invalid_questions')
130
+ return { code: `jev_${reason}`, stage,
131
+ message: reason === 'invalid_state' ? 'Jev state must be JSON-serializable text, an object, an array or null. No request was sent.'
132
+ : 'Jev needs a nonempty JSON question map. No request was sent.' };
133
+ }
134
+ // Trusted validation can invoke authored factories; its free-form problems may contain thrown text.
135
+ if (error instanceof WorkflowInvalidError)
136
+ return { code: error.code, stage: error.stage,
137
+ message: 'Workflow validation failed. Inspect the graph for contract errors; trusted factory error details are omitted.' };
138
+ if (error instanceof WorkflowOutputInvalidError) {
139
+ return { code: error.code, stage: error.stage.slice(0, 200),
140
+ message: `Workflow contract failed at ${error.stage.slice(0, 200)}. Correct the listed contract problems before retrying.`,
141
+ problems: error.problems.slice(0, 8).map(problem => problem.slice(0, 500)) };
142
+ }
143
+ if (error instanceof WorkflowCodeError)
144
+ return { code: error.code, stage: error.stage.slice(0, 200),
145
+ message: `Code transform failed at ${error.stage.slice(0, 200)}. Check state paths, synchronous function syntax and supported globals; thrown details are omitted.` };
146
+ if (error instanceof EffectDeadlineExceededError || error instanceof EffectOutcomeUnknownError)
147
+ return {
148
+ code: error.code, stage: error.stage.slice(0, 200), message: 'A tool effect exceeded its deadline or did not settle. Reconcile its outcome before retrying.'
149
+ };
150
+ return undefined;
151
+ }
152
+ function snapshot(value, maxBytes = 512 * 1024) {
153
+ const ancestors = new Set();
154
+ let values = 0, bytes = 0;
155
+ const visit = (item, depth) => {
156
+ if (++values > 100_000 || depth > 128)
157
+ throw new ExtensionServiceError('bounds', 'JSON data exceeds the depth or value limit.');
158
+ if (typeof item === 'string')
159
+ bytes += Buffer.byteLength(item, 'utf8');
160
+ else
161
+ bytes += 8;
162
+ if (bytes > maxBytes)
163
+ throw new ExtensionServiceError('bounds', 'JSON data exceeds the byte limit.');
164
+ if (item === null || typeof item === 'string' || typeof item === 'boolean' || typeof item === 'number' && Number.isFinite(item))
165
+ return item;
166
+ if (!item || typeof item !== 'object' || utilTypes.isProxy(item))
167
+ throw new ExtensionServiceError('data', 'Only plain JSON data is accepted.');
168
+ const prototype = Object.getPrototypeOf(item);
169
+ if (prototype !== Object.prototype && prototype !== Array.prototype && prototype !== null)
170
+ throw new ExtensionServiceError('data', 'Only plain JSON objects and arrays are accepted.');
171
+ if (ancestors.has(item))
172
+ throw new ExtensionServiceError('data', 'JSON data cannot contain cycles.');
173
+ if (Object.getOwnPropertySymbols(item).length)
174
+ throw new ExtensionServiceError('data', 'JSON data cannot contain symbols.');
175
+ const descriptors = Object.getOwnPropertyDescriptors(item);
176
+ if (Object.values(descriptors).some(entry => entry.get || entry.set))
177
+ throw new ExtensionServiceError('data', 'JSON data cannot contain accessors.');
178
+ ancestors.add(item);
179
+ try {
180
+ if (Array.isArray(item)) {
181
+ if (item.length > 100_000)
182
+ throw new ExtensionServiceError('bounds', 'Array exceeds the value limit.');
183
+ return Array.from({ length: item.length }, (_, index) => visit(descriptors[index]?.value, depth + 1));
184
+ }
185
+ return Object.fromEntries(Object.entries(descriptors).filter(([, entry]) => entry.enumerable).map(([key, entry]) => {
186
+ bytes += Buffer.byteLength(key, 'utf8');
187
+ return [key, visit(entry.value, depth + 1)];
188
+ }));
189
+ }
190
+ finally {
191
+ ancestors.delete(item);
192
+ }
193
+ };
194
+ return visit(value, 0);
195
+ }
196
+ function freeze(value) {
197
+ if (value && typeof value === 'object') {
198
+ Object.values(value).forEach(freeze);
199
+ Object.freeze(value);
200
+ }
201
+ return value;
202
+ }
203
+ // Events are independently bounded before retention. Reapplying the input's
204
+ // depth/value-count limits to their aggregate would reintroduce a cumulative
205
+ // execution limit even when retained bytes fit.
206
+ function snapshotReport(report, maxEventBytes) {
207
+ const { events, ...rest } = report;
208
+ return { ...snapshot(rest, 4 * 1024 * 1024), events: events.map(event => snapshot(event, maxEventBytes)) };
209
+ }
210
+ const secretKey = /^(?:error|message|cause|stack|headers|authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret|credentials?|model)$/i;
211
+ function sanitize(value) {
212
+ if (Array.isArray(value))
213
+ return value.map(sanitize);
214
+ if (!value || typeof value !== 'object')
215
+ return value;
216
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !secretKey.test(key)).map(([key, entry]) => [key, sanitize(entry)]));
217
+ }
218
+ function abortable(promise, signal) {
219
+ return new Promise((resolve, reject) => {
220
+ const abort = () => { signal.removeEventListener('abort', abort); reject(signal.reason); };
221
+ signal.addEventListener('abort', abort, { once: true });
222
+ Promise.resolve(promise).then(value => { signal.removeEventListener('abort', abort); signal.aborted ? reject(signal.reason) : resolve(value); }, error => { signal.removeEventListener('abort', abort); reject(error); });
223
+ if (signal.aborted)
224
+ abort();
225
+ });
226
+ }
227
+ export class WorkflowExtensionService {
228
+ allowedTools;
229
+ rubricSections;
230
+ limits;
231
+ current;
232
+ lastReport;
233
+ disposed = false;
234
+ active;
235
+ constructor(options = {}) {
236
+ const tools = options.allowedTools ?? [];
237
+ if (!Array.isArray(tools) || tools.length > 100 || tools.some(tool => typeof tool !== 'string' || !tool.trim() || tool.length > 200))
238
+ throw new ExtensionServiceError('configuration', 'Provide at most 100 registered tool names.');
239
+ this.allowedTools = new Set(tools);
240
+ this.rubricSections = freeze(snapshot(options.rubricSections ?? {}));
241
+ if (Object.entries(this.rubricSections).some(([name, text]) => !name.trim() || typeof text !== 'string' || !text.trim()))
242
+ throw new ExtensionServiceError('configuration', 'Rubric sections require nonempty names and text.');
243
+ this.limits = { ...defaults, ...options.limits };
244
+ for (const key of Object.keys(ceilings)) {
245
+ const value = this.limits[key];
246
+ if (value === null && ['deadlineMs', 'maxAgentCalls', 'maxJudgeCalls', 'maxToolCalls'].includes(key))
247
+ continue;
248
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1 || value > ceilings[key])
249
+ throw new ExtensionServiceError('configuration', `${key} must be an integer from 1 to ${ceilings[key]}, or null for a deadline/call limit.`);
250
+ }
251
+ }
252
+ available() { if (this.disposed)
253
+ throw new ExtensionServiceError('disposed', 'This workflow session is disposed.'); }
254
+ idle() { this.available(); if (this.active)
255
+ throw new ExtensionServiceError('busy', 'Stop the current workflow before starting or preparing another.'); }
256
+ check(value, options) {
257
+ const workflow = snapshot(value);
258
+ const inspection = inspectWorkflow(workflow);
259
+ if (inspection.nodes.length > extensionStructuralLimits.maxNodes)
260
+ throw new ExtensionServiceError('bounds', 'A workflow may contain at most 200 nodes.');
261
+ const errors = workflowPolicyErrors(workflow, { allowExecutableCandidates: options.allowExecutableCandidates === true, rubricSections: this.rubricSections, allowedEffectTools: [...this.allowedTools] });
262
+ for (const entry of inspection.nodes) {
263
+ const node = entry.path.split('/').slice(1).reduce((item, key) => item[key.replace(/~1/g, '/').replace(/~0/g, '~')], workflow);
264
+ if (node.node === 'artifact')
265
+ errors.push('Artifact delivery is not available in this extension.');
266
+ if (node.node === 'call' && (node.via !== 'tool' || !this.allowedTools.has(node.tool)))
267
+ errors.push('Effects must use a registered host tool; shell and executor calls are unavailable.');
268
+ if (node.node === 'map' && (node.maxConcurrency ?? 4) > extensionStructuralLimits.maxMapConcurrency)
269
+ errors.push('Map concurrency must not exceed 8.');
270
+ if (node.node === 'parallel' && node.branches.length > extensionStructuralLimits.maxParallelBranches)
271
+ errors.push('Parallel nodes must not exceed 8 branches.');
272
+ if (Array.isArray(node.tools) && node.tools.some((name) => !this.allowedTools.has(name)))
273
+ errors.push('Agent tools must be explicitly registered by the host.');
274
+ }
275
+ if (errors.length)
276
+ throw new ExtensionServiceError('admission', errors.slice(0, 8).join(' '));
277
+ const valid = validateWorkflow(workflow, { executeCode: options.executeCode, ...(options.input === undefined ? {} : { input: options.input }) });
278
+ if (!valid.ok)
279
+ throw new ExtensionServiceError('invalid_workflow', valid.errors.slice(0, 8).join(' '));
280
+ return { workflow, inspection };
281
+ }
282
+ /** Admission and syntax feedback only: never executes code or authorizes a run. */
283
+ preflight(value, input) {
284
+ this.available();
285
+ return this.check(value, { allowExecutableCandidates: true, executeCode: false, input }).inspection;
286
+ }
287
+ prepare(value, options = {}) {
288
+ this.idle();
289
+ const { workflow, inspection } = this.check(value, options);
290
+ this.current = freeze({ digest: inspection.sha256, inspection, workflow, executableAuthorized: options.allowExecutableCandidates === true });
291
+ this.lastReport = undefined;
292
+ return snapshot(this.current);
293
+ }
294
+ inspect() {
295
+ this.available();
296
+ return { ...(this.current ? { current: snapshot(this.current, 4 * 1024 * 1024) } : {}),
297
+ ...(this.lastReport ? { lastReport: snapshotReport(this.lastReport, this.limits.maxEventBytes) } : {}) };
298
+ }
299
+ stop() { this.active?.controller.abort(new ExtensionServiceError('cancelled', 'Workflow stopped.')); }
300
+ async dispose() { this.disposed = true; this.stop(); await this.active?.done; this.current = undefined; }
301
+ async run(input, options) {
302
+ this.idle();
303
+ if (!this.current)
304
+ throw new ExtensionServiceError('not_prepared', 'Prepare a workflow first.');
305
+ if (!options?.deps || typeof options.deps !== 'object')
306
+ throw new ExtensionServiceError('configuration', 'Host execution adapters are required.');
307
+ const prepared = this.current;
308
+ const initial = freeze(snapshot(input));
309
+ if (!initial || typeof initial !== 'object' || Array.isArray(initial))
310
+ throw new ExtensionServiceError('input', 'Workflow input must be a JSON object.');
311
+ const controller = new AbortController();
312
+ let finish;
313
+ const done = new Promise(resolve => { finish = resolve; });
314
+ this.active = { controller, done };
315
+ const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : []), ...(options.deps.signal ? [options.deps.signal] : [])]);
316
+ const timer = this.limits.deadlineMs === null ? undefined : setTimeout(() => controller.abort(new ExtensionServiceError('deadline', 'Workflow exceeded its deadline.')), this.limits.deadlineMs);
317
+ const calls = { agent: 0, judge: 0, tool: 0 }, admitted = { agent: 0, judge: 0, tool: 0 };
318
+ const events = [];
319
+ let bytes = 0, closed = false, traceTruncated = false, occupied = 0;
320
+ const eventSizes = [];
321
+ let receivedEvents = 0, receivedBytes = 0, droppedEvents = 0, droppedBytes = 0, rejectedEvents = 0;
322
+ const queue = [];
323
+ const limit = (message) => { const error = new ExtensionServiceError('limit', message); controller.abort(error); throw error; };
324
+ const acquire = async (localSignal) => {
325
+ localSignal.throwIfAborted();
326
+ if (occupied < this.limits.maxConcurrency) {
327
+ occupied++;
328
+ return;
329
+ }
330
+ await new Promise((resolve, reject) => {
331
+ const enter = () => { localSignal.removeEventListener('abort', cancel); occupied++; resolve(); };
332
+ const cancel = () => {
333
+ const index = queue.indexOf(enter);
334
+ if (index >= 0)
335
+ queue.splice(index, 1);
336
+ localSignal.removeEventListener('abort', cancel);
337
+ reject(localSignal.reason);
338
+ };
339
+ queue.push(enter);
340
+ localSignal.addEventListener('abort', cancel, { once: true });
341
+ if (localSignal.aborted)
342
+ cancel();
343
+ });
344
+ };
345
+ const dispatch = async (kind, localSignal, run, race) => {
346
+ const activeSignal = localSignal ? AbortSignal.any([signal, localSignal]) : signal;
347
+ activeSignal.throwIfAborted();
348
+ const maximum = kind === 'agent' ? this.limits.maxAgentCalls : kind === 'judge' ? this.limits.maxJudgeCalls : this.limits.maxToolCalls;
349
+ admitted[kind]++;
350
+ if (maximum !== null && admitted[kind] > maximum)
351
+ limit(`Workflow exceeded its ${kind} admission limit.`);
352
+ await acquire(activeSignal);
353
+ try {
354
+ activeSignal.throwIfAborted();
355
+ calls[kind]++;
356
+ const promise = run();
357
+ return await (race ? abortable(promise, activeSignal) : promise);
358
+ }
359
+ finally {
360
+ occupied--;
361
+ queue.shift()?.();
362
+ }
363
+ };
364
+ const onEvent = (event) => {
365
+ if (closed)
366
+ return;
367
+ try {
368
+ const clean = sanitize(snapshot(event, this.limits.maxEventBytes));
369
+ const size = Buffer.byteLength(JSON.stringify(clean));
370
+ if (size > this.limits.maxEventBytes)
371
+ throw new ExtensionServiceError('bounds', 'Workflow event exceeds its serialized byte limit.');
372
+ receivedEvents++;
373
+ receivedBytes += size;
374
+ // Retain recent fitting events; a valid event larger than the retention
375
+ // budget is omitted but still observed. Retention never cancels execution.
376
+ if (size > this.limits.maxTraceBytes) {
377
+ droppedEvents++;
378
+ droppedBytes += size;
379
+ traceTruncated = true;
380
+ }
381
+ else {
382
+ while (bytes + size > this.limits.maxTraceBytes) {
383
+ const removed = eventSizes.shift();
384
+ events.shift();
385
+ bytes -= removed;
386
+ droppedEvents++;
387
+ droppedBytes += removed;
388
+ traceTruncated = true;
389
+ }
390
+ bytes += size;
391
+ events.push(clean);
392
+ eventSizes.push(size);
393
+ }
394
+ if (options.onTraceEvent) {
395
+ try {
396
+ void Promise.resolve(options.onTraceEvent(snapshot(clean, this.limits.maxEventBytes))).catch(() => { });
397
+ }
398
+ catch { /* Host observation is not execution. */ }
399
+ }
400
+ if (!this.disposed && !signal.aborted && options.onEvent) {
401
+ try {
402
+ void Promise.resolve(options.onEvent(snapshot(clean, this.limits.maxEventBytes))).catch(() => { });
403
+ }
404
+ catch { /* UI observation is not execution. */ }
405
+ }
406
+ }
407
+ catch (error) {
408
+ rejectedEvents++;
409
+ traceTruncated = true;
410
+ if (!signal.aborted) {
411
+ // Snapshot rejects unsafe/non-JSON values separately from resource bounds.
412
+ // Keep fail-closed cancellation; never echo values or arbitrary error text.
413
+ const diagnostic = error instanceof ExtensionServiceError && error.code === 'data'
414
+ ? new ExtensionServiceError('trace_invalid_data', 'Workflow trace contains non-JSON data. Numbers must be finite; objects and arrays must be plain, acyclic and free of accessors or symbols.')
415
+ : error instanceof ExtensionServiceError && error.code === 'bounds'
416
+ ? new ExtensionServiceError('limit', 'Workflow trace exceeded its JSON byte, depth or value-count limit.')
417
+ : new ExtensionServiceError('trace_capture_failed', 'Workflow trace capture failed. Unsafe event details are omitted.');
418
+ controller.abort(diagnostic);
419
+ }
420
+ }
421
+ };
422
+ const sections = Object.entries(this.rubricSections);
423
+ const deps = {
424
+ signal, onEvent,
425
+ ...(options.deps.skill ? { skill: options.deps.skill } : {}),
426
+ ...(sections.length ? { sop: sections.map(([name, text]) => `## ${name}\n${text}`).join('\n\n') } : options.deps.sop ? { sop: options.deps.sop } : {}),
427
+ ...(options.deps.maxQuestionsPerRequest ? { maxQuestionsPerRequest: options.deps.maxQuestionsPerRequest } : {}),
428
+ ...(options.deps.runNode ? { runNode: params => dispatch('agent', params.signal, () => withAdapterDiagnostic(params.label, () => options.deps.runNode({ ...params, tools: params.tools ?? [...this.allowedTools] })), true) } : {}),
429
+ ...(options.deps.runJudge ? { runJudge: params => dispatch('judge', params.signal, () => withAdapterDiagnostic(params.label, () => options.deps.runJudge(params)), true) } : {}),
430
+ ...(options.deps.runEffect ? { runEffect: params => dispatch('tool', params.signal, () => {
431
+ if (params.node.via !== 'tool' || !this.allowedTools.has(params.node.tool))
432
+ throw new ExtensionServiceError('admission', 'Effect is not a registered host tool.');
433
+ return withAdapterDiagnostic(params.node.label, () => options.deps.runEffect(params));
434
+ }, false) } : {}),
435
+ };
436
+ let report;
437
+ try {
438
+ signal.throwIfAborted();
439
+ const result = await runWorkflow(prepared.workflow, initial, deps);
440
+ signal.throwIfAborted();
441
+ report = { digest: prepared.digest, status: result.status, calls, events,
442
+ ...(result.status === 'complete' ? { output: snapshot(result.output) } : { escalation: Object.fromEntries(Object.entries(result.escalation).filter(([key]) => key !== 'state' && result.escalation[key] !== undefined)) }) };
443
+ }
444
+ catch (error) {
445
+ const reason = controller.signal.aborted && controller.signal.reason instanceof ExtensionServiceError ? controller.signal.reason : error;
446
+ const owned = controller.signal.aborted && reason === controller.signal.reason && reason instanceof ExtensionServiceError;
447
+ const invalidInput = error instanceof WorkflowInputInvalidError && events.length === 0
448
+ && calls.agent + calls.judge + calls.tool === 0;
449
+ const code = owned ? reason.code : signal.aborted ? 'cancelled' : invalidInput ? 'input_invalid' : 'execution_failed';
450
+ const unknown = [];
451
+ const collect = (failure) => {
452
+ if (failure instanceof EffectOutcomeUnknownError)
453
+ unknown.push({ idempotencyKey: failure.idempotencyKey, ...(failure.executionPath ? { executionPath: failure.executionPath } : {}), outcome: 'unknown' });
454
+ else if (failure instanceof AggregateError)
455
+ failure.errors.slice(0, 100).forEach(collect);
456
+ };
457
+ collect(error);
458
+ report = { digest: prepared.digest, status: code === 'cancelled' || code === 'deadline' ? 'interrupted' : 'failed', calls, events,
459
+ error: !owned && !signal.aborted && !invalidInput && localDiagnostic(error) || { code,
460
+ ...(invalidInput ? { stage: 'input', problems: error.problems.slice(0, 8).map(problem => problem.slice(0, 500)) } : {}), message: owned ? reason.message : signal.aborted ? 'Workflow interrupted.' : invalidInput ? 'Input does not match this workflow. Pass input with action run, or inspect the workflow with its input before running.' : 'Workflow failed. Check the prepared graph, input contracts and host adapter access; provider error bodies are omitted.' },
461
+ ...(unknown.length ? { uncertainEffects: unknown } : {}), ...(traceTruncated ? { traceTruncated: true } : {}) };
462
+ }
463
+ finally {
464
+ closed = true;
465
+ clearTimeout(timer);
466
+ this.active = undefined;
467
+ finish();
468
+ }
469
+ report.trace = { policy: 'tail', receivedEvents, receivedBytes, rejectedEvents,
470
+ retainedEvents: events.length, retainedBytes: bytes, droppedEvents, droppedBytes };
471
+ if (traceTruncated)
472
+ report.traceTruncated = true;
473
+ const retained = snapshotReport(report, this.limits.maxEventBytes);
474
+ this.lastReport = freeze(retained);
475
+ return snapshotReport(retained, this.limits.maxEventBytes);
476
+ }
477
+ }
@@ -0,0 +1,41 @@
1
+ import { type ExtensionAPI, type ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+ import { type WorkflowDeps } from '@parcha/agentrun-dsl';
3
+ import type { PiToolDefinition } from './types.js';
4
+ declare const nativeLimits: Readonly<{
5
+ deadlineMs: 600000;
6
+ modelRequests: 50;
7
+ judgeCalls: 100;
8
+ toolAttempts: 100;
9
+ nodeMaxTurns: 8;
10
+ nodeMaxSubmissions: 3;
11
+ nodeTimeoutMs: 120000;
12
+ }>;
13
+ export type AgentRunRuntimeLimits = {
14
+ [Key in keyof typeof nativeLimits]: number | null;
15
+ };
16
+ export interface AgentRunWorkflowEvent {
17
+ workflowDigest: string;
18
+ /** Monotonic within this extension registration, including across session changes. */
19
+ runOrdinal: number;
20
+ sequence: number;
21
+ event: Parameters<NonNullable<WorkflowDeps['onEvent']>>[0];
22
+ }
23
+ export interface AgentRunExtensionOptions {
24
+ /** Complete host-owned tool allowlist, replacing demo and built-in tools. */
25
+ hostTools?: (ctx: ExtensionContext) => readonly PiToolDefinition[];
26
+ /** Host-owned Jev transport; called only when the inspected graph needs it. */
27
+ createJudge?: (options: {
28
+ signal: AbortSignal;
29
+ }) => NonNullable<WorkflowDeps['runJudge']>;
30
+ /** Synchronous host admission before each direct or native child tool attempt. */
31
+ onToolAttempt?: () => void;
32
+ /** Sanitized, isolated execution events, independent of report retention and UI.
33
+ * Observation cannot change execution. The host owns persistence failures and async draining. */
34
+ onWorkflowEvent?: (frame: AgentRunWorkflowEvent) => unknown;
35
+ /** Explicit null disables an execution limit; omitted fields retain finite defaults. */
36
+ runtimeLimits?: Partial<AgentRunRuntimeLimits>;
37
+ }
38
+ /** Configure the native extension without granting configuration to workflow authors. */
39
+ export declare function createAgentRunExtension(options?: AgentRunExtensionOptions): (pi: ExtensionAPI) => void;
40
+ export default function agentRunExtension(pi: ExtensionAPI): void;
41
+ export {};