@parcha/agentrun-pi 0.1.0-beta.2 → 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 (52) hide show
  1. package/README.md +46 -19
  2. package/dist/cli.js +5 -3
  3. package/dist/demo.js +1 -0
  4. package/dist/extension-service.js +11 -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/benchmark-contract.d.ts +0 -88
  33. package/dist/benchmark-contract.js +0 -50
  34. package/dist/benchmark-freeze.d.ts +0 -9
  35. package/dist/benchmark-freeze.js +0 -9
  36. package/dist/benchmark.d.ts +0 -127
  37. package/dist/benchmark.js +0 -260
  38. package/dist/comparison.d.ts +0 -69
  39. package/dist/comparison.js +0 -229
  40. package/dist/skill-bundle.d.ts +0 -5
  41. package/dist/skill-bundle.js +0 -15
  42. package/skills/author/SKILL.md +0 -102
  43. package/skills/author/references/sdk-authoring.md +0 -16
  44. /package/{skills → dist/skills}/author/examples/evidence-gate.input.json +0 -0
  45. /package/{skills → dist/skills}/author/examples/evidence-gate.json +0 -0
  46. /package/{skills → dist/skills}/author/examples/read-file.input.json +0 -0
  47. /package/{skills → dist/skills}/author/examples/read-file.json +0 -0
  48. /package/{skills → dist/skills}/author/examples/read-source-decision.input.json +0 -0
  49. /package/{skills → dist/skills}/author/examples/read-source-decision.json +0 -0
  50. /package/{skills → dist/skills}/author/examples/release-note.txt +0 -0
  51. /package/{skills → dist/skills}/author/examples/review-release-notes.input.json +0 -0
  52. /package/{skills → dist/skills}/author/examples/review-release-notes.json +0 -0
package/dist/extension.js CHANGED
@@ -1,22 +1,38 @@
1
1
  import { Type } from 'typebox';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { existsSync } from 'node:fs';
2
4
  import { Compile } from 'typebox/compile';
3
5
  import { Text } from '@earendil-works/pi-tui';
4
6
  import { createReadToolDefinition, createBashToolDefinition, createEditToolDefinition, createWriteToolDefinition, createGrepToolDefinition, createFindToolDefinition, createLsToolDefinition, VERSION as PI_VERSION, } from '@earendil-works/pi-coding-agent';
5
- import { formatWorkflowTree, inspectWorkflow } from '@parcha/agentrun-dsl';
7
+ import { formatWorkflowTree, inspectWorkflow, AUTHOR_SKILL_NAME, loadAuthorReference, renderAuthorHostAddendum, } from '@parcha/agentrun-dsl';
6
8
  import { createJevRunner } from '@parcha/agentrun-jev';
7
9
  import { createPiHostRunner, PI_MODEL_SETUP_MESSAGE } from './host-session.js';
8
- import { WorkflowExtensionService, extensionStructuralLimits } from './extension-service.js';
10
+ import { PI_HOST_ADDENDUM } from './host-addendum.js';
11
+ import { WorkflowExtensionService, ExtensionServiceError, extensionStructuralLimits } from './extension-service.js';
9
12
  import { demoInput, demoSearchTool, demoWorkflow, scriptedDemoDeps } from './demo.js';
10
13
  import { cleanText as safe, formatRunReport, modelJson } from './presentation.js';
11
- import { loadPiJevGuide, loadPiWorkflowGuide } from './skill-bundle.js';
12
14
  import { ToolInputValidationError, toolInputProblems } from './tool-input-error.js';
15
+ import { WorkflowStore, WorkflowStoreError } from './workflow-store.js';
16
+ import { WorkflowObservation, workflowView, formatWorkflowView, progressLines, readable } from './workflow-view.js';
17
+ import { showWorkflowInspector } from './workflow-inspector.js';
18
+ import { supportTriageWorkflow, supportTriageInputs, supportTriageTools, scriptedSupportTriageDeps } from './triage-demo.js';
19
+ import { restoreWorkflowSession, workflowRunHistory } from './workflow-session.js';
13
20
  const constructors = {
14
21
  read: createReadToolDefinition, bash: createBashToolDefinition, edit: createEditToolDefinition,
15
22
  write: createWriteToolDefinition, grep: createGrepToolDefinition, find: createFindToolDefinition, ls: createLsToolDefinition,
16
23
  };
17
24
  const mutatingTools = new Set(['bash', 'edit', 'write']);
18
- const help = 'Describe a task: /agentrun <what you want done>\nTry it without keys: /agentrun demo | demo empty\nUse real Pi and Jev calls: /agentrun demo live\nInspect: /agentrun · Rerun: /agentrun run · Setup: /agentrun status\nStop: /agentrun stop (Escape does not cancel slash-started runs)';
25
+ const help = 'Describe a task: /agentrun <what you want done>\nTry support triage: /agentrun triage (fictional, no model calls)\nInspect: /agentrun · Change input: /agentrun input\nSave: /agentrun save <name> · Load: /agentrun load <name>\nLibrary: /agentrun list · History: /agentrun history\nRun: /agentrun run · Setup: /agentrun status\nStop: /agentrun stop (closing the inspector does not stop a run)\nOther examples: /agentrun demo | demo empty | demo live';
19
26
  const textResult = (text, details = {}) => ({ content: [{ type: 'text', text: safe(text) }], details });
27
+ function singleTextInput(workflow) {
28
+ const source = workflow;
29
+ const schema = source?.input?.schemaId && source.schemas?.[source.input.schemaId];
30
+ if (schema?.type !== 'object' || !schema.properties || typeof schema.properties !== 'object')
31
+ return;
32
+ const keys = Object.keys(schema.properties);
33
+ return keys.length === 1 && schema.required?.length === 1 && schema.required[0] === keys[0]
34
+ && schema.properties[keys[0]]?.type === 'string' ? keys[0] : undefined;
35
+ }
20
36
  const nativeLimits = Object.freeze({ deadlineMs: 600_000, modelRequests: 50, judgeCalls: 100, toolAttempts: 100,
21
37
  nodeMaxTurns: 8, nodeMaxSubmissions: 3, nodeTimeoutMs: 120_000 });
22
38
  /** Configure the native extension without granting configuration to workflow authors. */
@@ -61,15 +77,17 @@ function registerExtension(pi, configuration) {
61
77
  if (typeof tool.execute !== 'function' || !tool.parameters || typeof tool.parameters !== 'object')
62
78
  throw new Error('Host tools require an executable definition and parameter schema');
63
79
  Compile(tool.parameters);
80
+ if (tool.resultSchema)
81
+ Compile(tool.resultSchema);
64
82
  names.add(tool.name);
65
- return { ...tool, parameters: structuredClone(tool.parameters) };
83
+ return { ...tool, parameters: structuredClone(tool.parameters), ...(tool.resultSchema ? { resultSchema: structuredClone(tool.resultSchema) } : {}) };
66
84
  });
67
85
  }
68
86
  const active = new Set(pi.getActiveTools());
69
87
  const builtins = new Set(pi.getAllTools().filter(t => t.sourceInfo.source === 'builtin').map(t => t.name));
70
88
  const definitions = Object.entries(constructors).filter(([name]) => active.has(name) && builtins.has(name) && (trusted || !mutatingTools.has(name)))
71
89
  .map(([, factory]) => factory(ctx.cwd));
72
- return [demoSearchTool(), ...definitions];
90
+ return [demoSearchTool(), ...supportTriageTools(), ...definitions];
73
91
  };
74
92
  const state = async (ctx) => {
75
93
  const key = `${ctx.sessionManager.getSessionId()}:${ctx.cwd}`;
@@ -84,15 +102,47 @@ function registerExtension(pi, configuration) {
84
102
  await old.inFlight;
85
103
  }
86
104
  const tools = toolSet(ctx);
87
- current = { key, clearStatus: () => { if (ctx.hasUI) {
105
+ current = { key, sessionFile: () => ctx.sessionManager.getSessionFile(), clearStatus: () => { if (ctx.hasUI) {
88
106
  ctx.ui.setStatus('agentrun', undefined);
89
107
  ctx.ui.setWidget('agentrun', undefined);
90
108
  } }, tools, toolNames: tools.map(t => t.name), input: {}, busy: false, closed: false,
91
109
  service: new WorkflowExtensionService({ allowedTools: tools.map(t => t.name), limits: serviceLimits }) };
110
+ const restored = restoreWorkflowSession(ctx.sessionManager.getBranch());
111
+ if (restored) {
112
+ current.draft = restored.workflow;
113
+ current.input = restored.input;
114
+ current.demo = restored.demo;
115
+ current.savedName = restored.savedName;
116
+ current.report = restored.report;
117
+ current.observation = restored.observation;
118
+ current.runId = restored.runId;
119
+ }
92
120
  return current;
93
121
  };
122
+ const persist = (s, running = false, closing = false) => {
123
+ if (current !== s || s.closed && !closing || !s.draft)
124
+ return;
125
+ const record = { version: 1, workflow: s.draft,
126
+ input: s.input, running, createdAt: new Date().toISOString(),
127
+ ...(s.savedName ? { savedName: s.savedName } : {}), ...(s.demo ? { demo: s.demo } : {}),
128
+ ...(s.runId ? { runId: s.runId } : {}), ...(s.report ? { report: s.report } : {}),
129
+ ...(s.observation ? { observation: s.observation } : {}), };
130
+ try {
131
+ const data = structuredClone(record);
132
+ // Never advertise history that this release cannot restore (including
133
+ // non-JSON or oversized receipts). Display/retention limits are not run limits.
134
+ if (!restoreWorkflowSession([{ type: 'custom', customType: 'agentrun:snapshot', data }]))
135
+ throw new Error('Unrestorable workflow receipt');
136
+ pi.appendEntry('agentrun:snapshot', data);
137
+ const file = s.sessionFile();
138
+ s.persistenceWarning = file && existsSync(file) ? undefined : 'Run receipts are in memory only: Pi has not yet persisted this session. Named procedure saves are durable; begin a normal Pi conversation to persist native session history.';
139
+ }
140
+ catch {
141
+ s.persistenceWarning = 'Run state could not be retained in the Pi session. Keep this session open; named workflow saving is separate.';
142
+ }
143
+ };
94
144
  const readiness = (ctx, s) => ({
95
- skill: pi.getCommands().some(item => item.name === 'skill:agentrun-author' && item.source === 'skill'),
145
+ skill: pi.getCommands().some(item => item.name === `skill:${AUTHOR_SKILL_NAME}` && item.source === 'skill'),
96
146
  pi: !!ctx.model && ctx.modelRegistry.getAll().some(model => model.id === ctx.model?.id && model.provider === ctx.model?.provider && model.api === ctx.model?.api),
97
147
  jev: !!configuration.createJudge || !!process.env.TYPESAFE_API_KEY?.trim(), sop: false, running: s.busy, workflow: !!s.draft,
98
148
  mode: s.draft ? s.demo ? 'scripted' : 'live' : undefined,
@@ -103,7 +153,7 @@ function registerExtension(pi, configuration) {
103
153
  pi.sendMessage({ customType: 'agentrun', content: safe(content), display: true, details });
104
154
  };
105
155
  const commandError = (s, error) => {
106
- const message = error instanceof Error && (error.name === 'ExtensionServiceError' || /^(No workflow|Inspect a workflow|A workflow|Cannot inspect|Workflow|Code|Executable|Unsupported|Invalid workflow|AgentRun requires|AgentRun authoring|The active Pi)/.test(error.message))
156
+ const message = error instanceof Error && (error instanceof WorkflowStoreError || error.name === 'ExtensionServiceError' || /^(No workflow|Inspect a workflow|A workflow|Cannot inspect|Workflow|Code|Executable|Unsupported|Invalid workflow|AgentRun requires|AgentRun authoring|The active Pi|Saved workflow)/.test(error.message))
107
157
  ? error.message.slice(0, 2000) : 'AgentRun could not complete this operation. Use /agentrun status to check setup and /agentrun to inspect the workflow. No fallback was selected.';
108
158
  show(s, message);
109
159
  };
@@ -138,11 +188,34 @@ function registerExtension(pi, configuration) {
138
188
  const changed = !s.draft || inspectWorkflow(s.draft).sha256 !== inspection.sha256;
139
189
  const nextInput = input === undefined ? changed ? {} : s.input : structuredClone(input);
140
190
  s.draft = nextDraft;
141
- if (changed)
191
+ if (changed) {
142
192
  s.demo = undefined;
193
+ s.savedName = undefined;
194
+ s.observation = undefined;
195
+ s.report = undefined;
196
+ s.runId = undefined;
197
+ }
198
+ if (input !== undefined) {
199
+ s.observation = undefined;
200
+ s.report = undefined;
201
+ s.runId = undefined;
202
+ }
143
203
  s.input = nextInput;
204
+ if (workflow !== undefined || input !== undefined)
205
+ persist(s);
144
206
  return { inspection, tree: formatWorkflowTree(inspection) };
145
207
  };
208
+ const view = (ctx, s) => {
209
+ const result = workflowView(s.draft, { input: s.input,
210
+ observation: s.observation, report: s.report, running: s.busy, savedName: s.savedName,
211
+ mode: s.demo ? 'scripted · fictional · no model calls' : 'live adapters',
212
+ model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : 'not selected', limits,
213
+ availableTools: toolSet(ctx).map(tool => tool.name),
214
+ });
215
+ if (s.persistenceWarning)
216
+ result.summary.push(s.persistenceWarning);
217
+ return result;
218
+ };
146
219
  const depsFor = (ctx, s, signal) => {
147
220
  let modelCalls = 0, toolCalls = 0;
148
221
  const countTool = () => {
@@ -219,7 +292,7 @@ function registerExtension(pi, configuration) {
219
292
  throw new ToolInputValidationError(node.label, tool.name, error.problems);
220
293
  throw error;
221
294
  }
222
- return !configuration.hostTools && node.tool === 'search' ? result.details : { content: result.content, ...(result.details === undefined ? {} : { details: result.details }) };
295
+ return !configuration.hostTools && ['search', 'support_demo_lookup', 'support_demo_handoff'].includes(node.tool ?? '') ? result.details : { content: result.content, ...(result.details === undefined ? {} : { details: result.details }) };
223
296
  },
224
297
  };
225
298
  };
@@ -247,46 +320,54 @@ function registerExtension(pi, configuration) {
247
320
  }
248
321
  // Resolve implementations from the execution context even if names are unchanged.
249
322
  s.tools = available;
323
+ // Validate the actual input before asking for model credentials or executing code probes.
324
+ s.service.preflight(s.draft, input);
250
325
  const prepared = s.service.prepare(s.draft, { allowExecutableCandidates: options.trusted ?? false });
326
+ // Adapter admission is not a run. Failed setup must not leave a running receipt.
327
+ const deps = scripted
328
+ ? s.demo === 'triage' || s.demo === 'triage-failure' ? scriptedSupportTriageDeps({ failLookup: s.demo === 'triage-failure' }) : scriptedDemoDeps(s.demo === 'empty')
329
+ : depsFor(ctx, s, signal);
251
330
  s.input = structuredClone(input);
252
331
  const tree = formatWorkflowTree(inspectWorkflow(s.draft));
253
332
  const label = scripted ? 'Scripted demo — fictional sources; no model calls.' : `Running with Pi · ${limits.deadlineMs === null ? 'no workflow deadline; operator cancellation remains available' : `${limits.deadlineMs / 1000}-second workflow deadline`}. Jev is used only for system one nodes.`;
254
333
  options.update?.(textResult(`${label}\n\n${tree}`));
255
334
  stage(ctx, s, 'agent.run() · starting');
256
- let completed = 0;
257
- const active = new Map();
335
+ const observation = new WorkflowObservation();
336
+ s.observation = observation.data;
337
+ s.report = undefined;
338
+ s.runId = randomUUID();
339
+ persist(s, true);
258
340
  const ordinal = ++runOrdinal;
259
341
  let sequence = 0;
260
342
  const report = await s.service.run(s.input, {
261
- deps: scripted ? scriptedDemoDeps(s.demo === 'empty') : depsFor(ctx, s, signal), signal,
343
+ deps: observation.observe(deps), signal,
262
344
  ...(configuration.onWorkflowEvent ? { onTraceEvent: (event) => configuration.onWorkflowEvent({ workflowDigest: prepared.digest, runOrdinal: ordinal, sequence: ++sequence, event }) } : {}),
263
345
  onEvent: event => {
264
- if (current !== s || s.closed || !event.label)
346
+ observation.record(event);
347
+ if (current !== s || s.closed)
265
348
  return;
266
- if (event.type === 'node.start')
267
- active.set(event.label, (active.get(event.label) ?? 0) + 1);
268
- else if (event.type === 'node.end') {
269
- const count = (active.get(event.label) ?? 1) - 1;
270
- if (count)
271
- active.set(event.label, count);
272
- else
273
- active.delete(event.label);
274
- completed++;
275
- }
349
+ const progress = progressLines(observation.data);
350
+ if (ctx.mode === 'tui')
351
+ ctx.ui.setWidget('agentrun', [safe(prepared.inspection.name), ...progress.map(safe), '/agentrun inspect · /agentrun stop']);
276
352
  else
277
- return;
278
- const running = [...active].map(([name, count]) => count > 1 ? `${name} ×${count}` : name).join(', ');
279
- const progress = `agent.run() · ${completed} finished${running ? ` · ${running}` : ''}`;
280
- stage(ctx, s, progress);
281
- if (ctx.hasUI)
282
- ctx.ui.setWidget('agentrun', [safe(progress), '/agentrun stop to cancel']);
283
- options.update?.(textResult(`${label}\n\n${tree}\n\n${progress}`));
353
+ options.update?.(textResult(`${label}\n\n${progress.join('\n')}`));
284
354
  },
285
355
  });
356
+ s.report = report;
357
+ persist(s);
286
358
  const summary = formatRunReport(report, scripted);
287
359
  const { events: _events, ...modelReport } = report;
288
360
  const mode = scripted ? 'scripted' : 'live';
289
- return textResult(options.modelResult ? modelJson({ mode, ...modelReport }) : summary, { mode, ...report });
361
+ return textResult(options.modelResult ? modelJson({ mode, ...modelReport }) : summary + (s.persistenceWarning ? `\n${s.persistenceWarning}` : ''), { mode, ...report,
362
+ // Failure diagnostics must not echo rejected raw arguments. The operator
363
+ // can inspect the retained draft explicitly; the error receipt stays sanitized.
364
+ ...(report.error ? {} : { view: workflowView(s.draft, { input: s.input, observation: s.observation, report, mode }) }) });
365
+ }
366
+ catch (error) {
367
+ if (error instanceof ExtensionServiceError && error.code === 'admission' && error.message.includes('code requires allowExecutableCandidates')) {
368
+ throw new ExtensionServiceError(error.code, error.message.replaceAll('code requires allowExecutableCandidates', 'Code requires the user command /agentrun run --trusted for each run. This permits local, unsandboxed execution.'));
369
+ }
370
+ throw error;
290
371
  }
291
372
  finally {
292
373
  s.busy = false;
@@ -314,10 +395,12 @@ function registerExtension(pi, configuration) {
314
395
  };
315
396
  pi.registerTool({
316
397
  name: 'agentrun', label: 'AgentRun workflow',
317
- description: 'Use the agentrun-author skill to compose workflows. Describe lists available tools and their schemas. Inspect or run an AgentRun DSL workflow in this Pi session. First inspect to show its graph. Agent nodes capture the active Pi model when execution starts. Code needs the user command /agentrun run --trusted for each run. ' + (configuration.hostTools
398
+ description: `Use the ${AUTHOR_SKILL_NAME} skill to compose workflows.` + ' Describe lists available tools and their schemas. Inspect or run an AgentRun DSL workflow in this Pi session. First inspect to show its graph. Agent nodes capture the active Pi model when execution starts. Code needs the user command /agentrun run --trusted for each run. ' + (configuration.hostTools
318
399
  ? 'Only the explicit host-configured tools are available, including during trusted runs. Outer permission hooks are not inherited. '
319
- : 'Enabled read-only Pi built-ins are available to declared steps. Shell/write/edit need the trusted command for each run; custom extension tools and permission hooks are not inherited. The search tool reads fictional demo sources only. ') + 'Scripted demos stay scripted on rerun; use /agentrun demo live to switch. Saving workflows is not part of v1.',
320
- parameters: Type.Object({ action: Type.Union([Type.Literal('describe'), Type.Literal('inspect'), Type.Literal('run')]),
400
+ : 'Enabled read-only Pi built-ins are available to declared steps. Shell/write/edit need the trusted command for each run; custom extension tools and permission hooks are not inherited. The search and support_demo tools use fictional supplied sources only. ') + 'Save stores a named procedure revision, not its input or permission. Load preflights current host capabilities. Run always restarts; there is no checkpoint resume. Scripted demos stay scripted on rerun.',
401
+ parameters: Type.Object({ action: Type.Union(['describe', 'inspect', 'run', 'save', 'load', 'list'].map(value => Type.Literal(value))),
402
+ name: Type.Optional(Type.String({ description: 'Saved procedure name: lowercase letters, digits, hyphens or underscores.' })),
403
+ digest: Type.Optional(Type.String({ description: 'Optional exact saved SHA-256 revision to load.' })),
321
404
  workflow: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
322
405
  description: 'The complete AgentRun workflow JSON object. Pass the object directly, not a JSON-encoded string. Inspection validates its DSL structure.',
323
406
  })), input: Type.Optional(Type.Record(Type.String(), Type.Unknown())) }),
@@ -325,22 +408,56 @@ function registerExtension(pi, configuration) {
325
408
  renderResult(result, options) {
326
409
  const details = result.details;
327
410
  const text = details?.status && details.calls ? options?.expanded
328
- ? JSON.stringify(details, null, 2) : formatRunReport(details, details.mode === 'scripted')
329
- : result.content.filter(part => part.type === 'text').map(part => part.text).join('\n');
411
+ ? details.view ? formatWorkflowView({ ...details.view, output: undefined }) + (details.output === undefined ? '' : `\n\nOutput:\n${readable(details.output, Number.MAX_SAFE_INTEGER)}`) : formatRunReport(details, details.mode === 'scripted') : formatRunReport(details, details.mode === 'scripted')
412
+ : details?.view ? formatWorkflowView(details.view) : result.content.filter(part => part.type === 'text').map(part => part.text).join('\n');
330
413
  return new Text(safe(text), 0, 0);
331
414
  },
332
415
  async execute(_id, args, signal, onUpdate, ctx) {
333
416
  const s = await state(ctx);
417
+ const store = new WorkflowStore(ctx.cwd);
418
+ if (args.action === 'save') {
419
+ if (s.busy)
420
+ throw new Error('A workflow is running. Save after it stops.');
421
+ if (!s.draft)
422
+ throw new Error('No workflow to save. Inspect a workflow first.');
423
+ const saved = await store.save(args.name ?? '', s.draft);
424
+ s.savedName = saved.name;
425
+ persist(s);
426
+ return textResult(`Saved ${saved.name} · ${saved.digest.slice(0, 12)}. Input and execution permission are not saved.\nLoad this revision: /agentrun load ${saved.name} ${saved.digest}${s.demo ? '\nScripted demo adapters are not saved. Loading this definition uses live adapters.' : ''}`, saved);
427
+ }
428
+ if (args.action === 'list') {
429
+ const workflows = await store.list();
430
+ return textResult(modelJson(workflows), { workflows });
431
+ }
432
+ if (args.action === 'load') {
433
+ const saved = await store.load(args.name ?? '', args.digest);
434
+ const result = inspect(ctx, s, saved.workflow, args.input);
435
+ s.input = structuredClone(args.input ?? {});
436
+ s.savedName = saved.name;
437
+ s.demo = undefined;
438
+ s.report = undefined;
439
+ s.observation = undefined;
440
+ s.runId = undefined;
441
+ persist(s);
442
+ const preview = view(ctx, s);
443
+ return textResult(modelJson({ preview: formatWorkflowView(preview), workflow: s.draft, input: s.input }), { ...result, saved, view: preview });
444
+ }
334
445
  if (args.action === 'describe') {
335
- const details = { tools: toolSet(ctx).map(t => ({ name: t.name, description: t.description, parameters: t.parameters })),
336
- limits: { ...limits }, structuralLimits: { ...extensionStructuralLimits }, ...readiness(ctx, s), authoring: { workflow: loadPiWorkflowGuide(), jev: loadPiJevGuide() } };
446
+ const details = { tools: toolSet(ctx).map(t => ({ name: t.name, description: t.description, parameters: t.parameters,
447
+ resultSchema: t.resultSchema ?? { type: 'object', properties: { content: { type: 'array', items: { type: 'object' } }, details: {} }, required: ['content'] },
448
+ resultContract: t.resultSchema ? 'Host-declared direct workflow call result.' : 'Pi result envelope; tool-specific details are not declared. Do not invent fields inside details.',
449
+ })),
450
+ limits: { ...limits }, structuralLimits: { ...extensionStructuralLimits }, ...readiness(ctx, s), authoring: { language: loadAuthorReference('language'), workflow: loadAuthorReference('workflow-format'), jev: loadAuthorReference('jev-decisions'), host: renderAuthorHostAddendum(PI_HOST_ADDENDUM) } };
337
451
  return textResult(JSON.stringify(details, null, 2), details);
338
452
  }
339
453
  if (args.action === 'run' && args.workflow !== undefined)
340
454
  throw new Error('Inspect the workflow first with action inspect, then run without a workflow argument.');
341
455
  if (args.action === 'inspect') {
342
456
  const result = inspect(ctx, s, args.workflow, args.input);
343
- return textResult(result.tree, { ...result, tools: toolSet(ctx).map(t => t.name) });
457
+ const preview = view(ctx, s);
458
+ // The author needs the exact definition when editing a restored procedure.
459
+ // Pi's renderer shows the readable view, not this model-facing JSON.
460
+ return textResult(modelJson({ preview: formatWorkflowView(preview), workflow: s.draft, input: s.input }), { ...result, view: preview, tools: toolSet(ctx).map(t => t.name) });
344
461
  }
345
462
  return execute(ctx, s, args.input ?? s.input, { signal, update: onUpdate, modelResult: true });
346
463
  },
@@ -349,21 +466,157 @@ function registerExtension(pi, configuration) {
349
466
  description: 'Describe a task, inspect its workflow, or try /agentrun demo',
350
467
  async handler(args, ctx) {
351
468
  const s = await state(ctx);
352
- const raw = args.trim();
353
- const command = /^(help|status|stop|demo|run)(?:\s|$)/i.test(raw) ? raw.toLowerCase().replace(/\s+/g, ' ') : raw;
354
- const words = command.split(/\s+/);
355
- const commandLike = /^(help|status|stop|demo|run)(?:-|$)/i.test(words[0])
356
- && words.slice(1).every(word => word.startsWith('-') || ['live', 'empty'].includes(word));
469
+ let raw = args.trim();
357
470
  try {
471
+ if ((!raw || raw === 'inspect') && s.draft && ctx.mode === 'tui') {
472
+ const action = await showWorkflowInspector(ctx, () => view(ctx, s));
473
+ if (!action)
474
+ return;
475
+ if (action === 'save') {
476
+ const name = await ctx.ui.input('Save procedure', s.savedName ?? 'support-triage');
477
+ if (!name)
478
+ return;
479
+ raw = `save ${name}`;
480
+ }
481
+ else if (action === 'load') {
482
+ const stored = await new WorkflowStore(ctx.cwd).list();
483
+ const names = [...new Set(stored.map(item => item.name))];
484
+ if (!names.length) {
485
+ show(s, 'No saved procedures yet. Use /agentrun save <name>.');
486
+ return;
487
+ }
488
+ const name = await ctx.ui.select('Load procedure', names);
489
+ if (!name)
490
+ return;
491
+ raw = `load ${name}`;
492
+ }
493
+ else if (action === 'edit') {
494
+ const request = await ctx.ui.input('Edit procedure', 'Describe the change; Pi will preview a candidate before running it.');
495
+ if (!request)
496
+ return;
497
+ pi.sendUserMessage(`/skill:agentrun-author Edit the current procedure: ${request}. Inspect the candidate and explain the change. Do not run it yet.`, { expandPromptTemplates: true });
498
+ return;
499
+ }
500
+ else
501
+ raw = action;
502
+ }
503
+ const command = /^(help|status|stop|demo|triage|run|inspect)(?:\s|$)/i.test(raw) ? raw.toLowerCase().replace(/\s+/g, ' ') : raw;
504
+ const words = command.split(/\s+/);
505
+ const commandLike = /^(help|status|stop|demo|run)(?:-|$)/i.test(words[0])
506
+ && words.slice(1).every(word => word.startsWith('-') || ['live', 'empty'].includes(word));
358
507
  if (command === 'help' || command === '--help' || command === '-h') {
359
508
  show(s, help);
360
509
  }
361
- else if (!command) {
362
- show(s, s.draft ? `${inspect(ctx, s).tree}\n\n${s.busy ? 'Running. /agentrun stop to cancel.' : s.demo ? 'Replay: /agentrun run (scripted, no model calls). Switch to real models: /agentrun demo live.' : 'Run: /agentrun run. Ask Pi to change this workflow.'}` : help);
510
+ else if (!command || command === 'inspect') {
511
+ show(s, s.draft ? formatWorkflowView(view(ctx, s)) : help);
512
+ }
513
+ else if (/^save(?:\s|$)/.test(command)) {
514
+ if (s.busy)
515
+ throw new Error('A workflow is running. Save after it stops.');
516
+ if (!s.draft)
517
+ throw new Error('No workflow to save. Inspect a workflow first.');
518
+ const saved = await new WorkflowStore(ctx.cwd).save(command.slice(4).trim(), s.draft);
519
+ s.savedName = saved.name;
520
+ persist(s);
521
+ show(s, `Saved ${saved.name} · ${saved.digest.slice(0, 12)}. Input and execution permission are not saved.\nReuse: /agentrun load ${saved.name}\nLoad this revision: /agentrun load ${saved.name} ${saved.digest}${s.demo ? '\nScripted demo adapters are not saved. Loading this definition uses live adapters.' : ''}`, saved);
522
+ }
523
+ else if (/^load(?:\s|$)/.test(command)) {
524
+ const [, name, digest] = command.split(/\s+/);
525
+ const saved = await new WorkflowStore(ctx.cwd).load(name ?? '', digest);
526
+ inspect(ctx, s, saved.workflow);
527
+ s.savedName = saved.name;
528
+ s.input = {};
529
+ s.demo = undefined;
530
+ s.report = undefined;
531
+ s.observation = undefined;
532
+ s.runId = undefined;
533
+ persist(s);
534
+ show(s, `Loaded procedure. Supply new input with /agentrun input. Loading never executes it.\n\n${formatWorkflowView(view(ctx, s))}`);
535
+ }
536
+ else if (command === 'list') {
537
+ const stored = await new WorkflowStore(ctx.cwd).list();
538
+ show(s, stored.length ? stored.map(item => `${item.name} · ${item.workflowName}\n/agentrun load ${item.name} ${item.digest}`).join('\n') : 'No saved procedures yet. Use /agentrun save <name>.', { workflows: stored });
539
+ }
540
+ else if (/^history(?:\s|$)/.test(command)) {
541
+ const history = workflowRunHistory(ctx.sessionManager.getBranch());
542
+ if (!history.length) {
543
+ show(s, 'No retained runs on this Pi session branch yet.');
544
+ return;
545
+ }
546
+ const labels = history.map((run, index) => `${index + 1}. ${inspectWorkflow(run.workflow).name} · ${run.report?.status ?? 'interrupted'} · ${run.createdAt}`);
547
+ let index = Number(command.slice(7).trim()) - 1;
548
+ if (command === 'history' && ctx.mode === 'tui') {
549
+ const selected = await ctx.ui.select('Retained runs on this session branch', labels);
550
+ if (!selected)
551
+ return;
552
+ index = labels.indexOf(selected);
553
+ }
554
+ if (!Number.isInteger(index) || index < 0 || index >= history.length) {
555
+ show(s, `${labels.join('\n')}\n\nInspect: /agentrun history <number>`, { runs: history.map(run => ({ runId: run.runId, createdAt: run.createdAt, digest: run.report?.digest, status: run.report?.status })) });
556
+ return;
557
+ }
558
+ const run = history[index];
559
+ const receipt = workflowView(run.workflow, { input: run.input, observation: run.observation, report: run.report, mode: run.demo ? 'scripted' : 'live adapters', savedName: run.savedName });
560
+ // History is read-only. It never replaces the active draft or replays a run.
561
+ if (ctx.mode === 'tui')
562
+ await showWorkflowInspector(ctx, () => ({ ...receipt, readOnly: true }));
563
+ else
564
+ show(s, formatWorkflowView(receipt), { run });
565
+ }
566
+ else if (/^input(?:\s|$)/.test(command)) {
567
+ if (!s.draft)
568
+ throw new Error('No workflow yet. Load or describe a procedure first.');
569
+ if (s.busy)
570
+ throw new Error('A workflow is running. Stop it before changing its input.');
571
+ let supplied = raw.slice(5).trim();
572
+ if (!supplied && ctx.hasUI)
573
+ supplied = await ctx.ui.input('New workflow input', 'Describe the new case in plain language, or paste a JSON object.') ?? '';
574
+ if (!supplied) {
575
+ show(s, 'Use /agentrun input <describe the new case>, or ask Pi to use new input. Nothing was run.');
576
+ return;
577
+ }
578
+ if (supplied.startsWith('{')) {
579
+ let input;
580
+ try {
581
+ input = JSON.parse(supplied);
582
+ }
583
+ catch {
584
+ throw new Error('Workflow input must be valid JSON, or describe it in plain language.');
585
+ }
586
+ if (!input || typeof input !== 'object' || Array.isArray(input))
587
+ throw new Error('Workflow input must be an object.');
588
+ inspect(ctx, s, undefined, input);
589
+ show(s, `Input updated. Nothing was run.\n\n${formatWorkflowView(view(ctx, s))}`);
590
+ }
591
+ else {
592
+ const key = singleTextInput(s.draft);
593
+ if (key) {
594
+ inspect(ctx, s, undefined, { [key]: supplied });
595
+ show(s, `Input updated. Nothing was run.\n\n${formatWorkflowView(view(ctx, s))}`);
596
+ }
597
+ else {
598
+ pi.sendUserMessage(`/skill:agentrun-author Set new input for the current inspected workflow: ${supplied}. Keep the procedure unchanged; inspect with input only. Ask for missing facts instead of inventing them. Do not run yet.`, { expandPromptTemplates: true });
599
+ }
600
+ }
601
+ }
602
+ else if (/^triage(?:\s|$)/.test(command)) {
603
+ const parts = command.split(/\s+/).slice(1);
604
+ const live = parts[0] === 'live';
605
+ const name = parts[live ? 1 : 0] ?? 'billing';
606
+ if (!['billing', 'technical', 'ambiguous', 'failure'].includes(name) || parts.length > (live ? 2 : 1) || live && name === 'failure') {
607
+ show(s, 'Use /agentrun triage [billing|technical|ambiguous|failure], or /agentrun triage live [billing|technical|ambiguous].');
608
+ return;
609
+ }
610
+ const input = structuredClone(supportTriageInputs[name === 'failure' ? 'billing' : name]);
611
+ inspect(ctx, s, supportTriageWorkflow, input);
612
+ s.demo = live ? undefined : name === 'failure' ? 'triage-failure' : 'triage';
613
+ show(s, `Fictional support triage · ${live ? 'real model calls' : 'scripted, no model calls'}. No customer actions.\n${formatWorkflowView(view(ctx, s))}`);
614
+ const result = await execute(ctx, s, input);
615
+ show(s, result.content[0].text, result.details);
363
616
  }
364
617
  else if (command === 'status') {
365
618
  const ready = readiness(ctx, s);
366
- show(s, `agent.run() · ${s.busy ? 'running' : 'idle'}\nDemo: ready — /agentrun demo (no keys or model calls)\nWorkflow: ${s.draft ? `${modelJson(inspectWorkflow(s.draft).name)} · ${s.demo ? 'scripted' : 'live adapters'}` : 'none — run the demo or describe a task; /reload clears the previous workflow'}\n\nFor your own workflows:\nPi host: ${PI_VERSION}${PI_VERSION === '0.87.0' ? '' : ' (tested on 0.87.0; use the bundled ./node_modules/.bin/pi)'}\nSkill: ${ready.skill ? 'loaded — /agentrun <task>' : 'missing — enable package skills, then /reload'}\nPi: ${ready.pi ? 'selected model available (connection not tested)' : 'no usable active model — /login to connect a provider, then /model to select it'}\nJev: ${ready.jev ? 'configuration present (connection not tested)' : 'not configured — only needed for system one decisions; set TYPESAFE_API_KEY before starting Pi'}`, ready);
619
+ show(s, `agent.run() · ${s.busy ? 'running' : 'idle'}\nDemo: ready — /agentrun demo (no keys or model calls)\nWorkflow: ${s.draft ? `${modelJson(inspectWorkflow(s.draft).name)} · ${s.demo ? 'scripted' : 'live adapters'}` : 'none — load a saved procedure, run the demo, or describe a task'}\n\nFor your own workflows:\nPi host: ${PI_VERSION}${PI_VERSION === '0.87.0' ? '' : ' (tested on 0.87.0; use the bundled ./node_modules/.bin/pi)'}\nSkill: ${ready.skill ? 'loaded — /agentrun <task>' : 'missing — enable package skills, then /reload'}\nPi: ${ready.pi ? 'selected model available (connection not tested)' : 'no usable active model — /login to connect a provider, then /model to select it'}\nJev: ${ready.jev ? 'configuration present (connection not tested)' : 'not configured — only needed for system one decisions; set TYPESAFE_API_KEY before starting Pi'}`, ready);
367
620
  }
368
621
  else if (command === 'stop') {
369
622
  s.controller?.abort();
@@ -385,12 +638,12 @@ function registerExtension(pi, configuration) {
385
638
  else {
386
639
  if (s.busy)
387
640
  throw new Error('A workflow is running. Stop it before starting another.');
388
- if (!pi.getCommands().some(item => item.name === 'skill:agentrun-author' && item.source === 'skill')) {
641
+ if (!pi.getCommands().some(item => item.name === `skill:${AUTHOR_SKILL_NAME}` && item.source === 'skill')) {
389
642
  throw new Error('AgentRun authoring skill is unavailable. Enable package skills, then /reload. Run /agentrun status to check discovery.');
390
643
  }
391
644
  if (!readiness(ctx, s).pi)
392
645
  throw new Error(PI_MODEL_SETUP_MESSAGE);
393
- pi.sendUserMessage(`/skill:agentrun-author ${raw}`, { expandPromptTemplates: true });
646
+ pi.sendUserMessage(`/skill:${AUTHOR_SKILL_NAME} ${raw}`, { expandPromptTemplates: true });
394
647
  }
395
648
  }
396
649
  catch (error) {
@@ -398,22 +651,41 @@ function registerExtension(pi, configuration) {
398
651
  }
399
652
  },
400
653
  });
401
- const closeSession = async () => {
654
+ const closeSession = async (retainFinal = false) => {
402
655
  if (!current)
403
656
  return;
404
- const old = current;
657
+ const old = current, wasRunning = old.busy;
405
658
  old.clearStatus();
406
- current = undefined;
407
659
  old.closed = true;
408
660
  old.controller?.abort();
409
661
  await old.service.dispose();
410
662
  await old.inFlight;
663
+ // Only before-switch/shutdown hooks still own the originating branch. A
664
+ // session_tree event is already on another branch and must never receive this receipt.
665
+ if (retainFinal && wasRunning && old.report)
666
+ persist(old, false, true);
667
+ if (current === old)
668
+ current = undefined;
411
669
  };
412
- pi.on('session_shutdown', closeSession);
413
- pi.on('session_before_switch', closeSession);
414
- pi.on('session_before_fork', closeSession);
670
+ pi.on('session_shutdown', () => closeSession(true));
671
+ pi.on('session_before_switch', () => closeSession(true));
672
+ pi.on('session_before_fork', () => closeSession(true));
415
673
  pi.on('session_start', async (_event, ctx) => {
416
674
  if (current && current.key !== `${ctx.sessionManager.getSessionId()}:${ctx.cwd}`)
417
675
  await closeSession();
676
+ await state(ctx);
677
+ });
678
+ pi.on('session_tree', async (_event, ctx) => { await closeSession(); await state(ctx); });
679
+ pi.registerEntryRenderer('agentrun:snapshot', (entry, options) => {
680
+ if (entry.data?.running)
681
+ return undefined;
682
+ const snapshot = restoreWorkflowSession([entry]);
683
+ if (!snapshot)
684
+ return undefined;
685
+ if (!snapshot.report && !snapshot.running)
686
+ return undefined;
687
+ return new Text(safe(options.expanded ? formatWorkflowView(workflowView(snapshot.workflow, {
688
+ input: snapshot.input, report: snapshot.report, observation: snapshot.observation, mode: snapshot.demo ? 'scripted' : 'live adapters',
689
+ })) : `AgentRun receipt · ${snapshot.report?.status ?? 'interrupted'} · ${inspectWorkflow(snapshot.workflow).name}`), 0, 0);
418
690
  });
419
691
  }
@@ -0,0 +1,4 @@
1
+ import { type AuthorHostAddendum } from '@parcha/agentrun-dsl';
2
+ /** What this extension adds to the shared workflow language: its commands, tools and unavailable transports.
3
+ * The packaged skill is rendered with it, and `describe` returns it as `authoring.host`. */
4
+ export declare const PI_HOST_ADDENDUM: AuthorHostAddendum;
@@ -0,0 +1,19 @@
1
+ import { WORKFLOW_NODE_KINDS } from '@parcha/agentrun-dsl';
2
+ /** What this extension adds to the shared workflow language: its commands, tools and unavailable transports.
3
+ * The packaged skill is rendered with it, and `describe` returns it as `authoring.host`. */
4
+ export const PI_HOST_ADDENDUM = Object.freeze({
5
+ name: 'Pi extension',
6
+ nodeKinds: WORKFLOW_NODE_KINDS.filter(kind => kind !== 'artifact'),
7
+ rules: [
8
+ 'Call agentrun with action describe first, then inspect the complete workflow with its input, then run without a workflow argument.',
9
+ 'Use exactly the tools describe returns. Without host-configured tools these are the active read-only built-ins (read, grep, find, ls), and bash, edit and write exist only in a trusted run; a host that configures its tools offers only those, including in trusted runs. The search tool reads fictional demo sources only.',
10
+ 'Code nodes and mutating tools run only after the user issues /agentrun run --trusted for that run. Trusted execution is local and unsandboxed.',
11
+ 'A call uses via tool with a tool describe returns. Shell and executor calls and artifact delivery are unavailable.',
12
+ 'This extension supplies no SOP text: do not run a workflow with sopSection here; it needs an SDK host that supplies the authoritative SOP.',
13
+ 'Custom extension tools and outer permission hooks are not inherited, including in trusted runs.',
14
+ 'A preview, procedure edit or input change does not authorize execution: inspect it, report Inspected — not run, and execute only when requested.',
15
+ 'Use /agentrun to inspect the current graph, /agentrun input to change input, /agentrun save and /agentrun load for named definition revisions, /agentrun history for retained receipts, /agentrun run to restart, and /agentrun stop to request cancellation; closing the inspector does not stop a run.',
16
+ 'Saving retains the definition without input or execution permission; loading rechecks current host capabilities and never resumes an interrupted run.',
17
+ 'The structural limits describe returns bound node count, map concurrency and parallel branches.',
18
+ ],
19
+ });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export * from "./runner.js";
2
- export * from "./author.js";
3
2
  export * from "./types.js";
4
3
  export { createPiHostRunner, createPiHostSessionFactory } from './host-session.js';
5
4
  export { WorkflowExtensionService } from './extension-service.js';
6
- export { loadPiAuthorSkillBundle } from './skill-bundle.js';
5
+ export { PI_HOST_ADDENDUM } from './host-addendum.js';
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  export * from "./runner.js";
2
- export * from "./author.js";
3
2
  export * from "./types.js";
4
3
  export { createPiHostRunner, createPiHostSessionFactory } from './host-session.js';
5
4
  export { WorkflowExtensionService } from './extension-service.js';
6
- export { loadPiAuthorSkillBundle } from './skill-bundle.js';
5
+ export { PI_HOST_ADDENDUM } from './host-addendum.js';
@@ -12,7 +12,7 @@ function lines(value, depth = 0) {
12
12
  }).concat(value.length > 30 ? [`… ${value.length - 30} more items in the structured result`] : []);
13
13
  const entries = Object.entries(value);
14
14
  return entries.slice(0, 30).flatMap(([key, item]) => {
15
- const label = key.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/_/g, ' ');
15
+ const label = key.replace(/([a-z])([A-Z])/g, (_, first, upper) => `${first} ${upper.toLowerCase()}`).replace(/_/g, ' ');
16
16
  const children = lines(item, depth + 1);
17
17
  return children.length === 1 ? [`${label}: ${children[0]}`] : [`${label}:`, ...children.map(line => ` ${line}`)];
18
18
  }).concat(entries.length > 30 ? [`… ${entries.length - 30} more fields in the structured result`] : []);
@@ -30,5 +30,5 @@ export function formatRunReport(report, scripted = false) {
30
30
  const trace = report.traceTruncated ? `\nTrace retention is partial${report.trace ? ` (${report.trace.droppedEvents} valid events omitted, ${report.trace.rejectedEvents} rejected)` : ''}; this does not mean the output is truncated.` : '';
31
31
  const clipped = body.length > 12_000 ? `${body.slice(0, 12_000)}\n… Full output is in the structured result.` : body;
32
32
  const status = report.status === 'escalated' ? 'stopped — needs attention (escalated)' : report.status;
33
- return cleanText(`${scripted ? 'Scripted demo' : 'Workflow'}: ${status}${report.error ? ` (${report.error.code})` : ''}\n\n${clipped}${recovery}${uncertain}${trace}\n\nWorkflow calls: ${report.calls.tool} direct tool call${report.calls.tool === 1 ? '' : 's'}, ${report.calls.judge} system one decision${report.calls.judge === 1 ? '' : 's'}, ${report.calls.agent} model step${report.calls.agent === 1 ? '' : 's'}.${scripted ? '\nScripted responses; no model calls. Replay: /agentrun run. Real models: /agentrun demo live.' : ''}`);
33
+ return cleanText(`${scripted ? 'Scripted demo' : 'Workflow'}: ${status}${report.error ? ` (${report.error.code})` : ''}\n\n${clipped}${recovery}${uncertain}${trace}\n\nWorkflow calls: ${report.calls.tool} direct tool call${report.calls.tool === 1 ? '' : 's'}, ${report.calls.judge} system one decision${report.calls.judge === 1 ? '' : 's'}, ${report.calls.agent} model step${report.calls.agent === 1 ? '' : 's'}.${scripted ? '\nScripted responses; no model calls. Replay: /agentrun run. Example modes: /agentrun help.' : ''}`);
34
34
  }