@parcha/agentrun-pi 0.1.0-beta.26 → 0.1.0-beta.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/extension.js CHANGED
@@ -24,6 +24,7 @@ import { showWorkflowInspector } from './workflow-inspector.js';
24
24
  import { authoringReferences, describeTools, dryRunDraft, formatDryRun, inspectCandidate, workflowToolParameters } from './workflow-tool.js';
25
25
  import { supportTriageWorkflow, supportTriageInputs, supportTriageTools, scriptedSupportTriageDeps } from './triage-demo.js';
26
26
  import { restoreWorkflowSession, workflowRunHistory } from './workflow-session.js';
27
+ import { OperatorError } from './operator-error.js';
27
28
  const constructors = {
28
29
  read: createReadToolDefinition, bash: createBashToolDefinition, edit: createEditToolDefinition,
29
30
  write: createWriteToolDefinition, grep: createGrepToolDefinition, find: createFindToolDefinition, ls: createLsToolDefinition,
@@ -155,7 +156,8 @@ function registerExtension(pi, configuration) {
155
156
  pi.sendMessage({ customType: 'agentrun', content: safe(content), display: true, details });
156
157
  };
157
158
  const commandError = (s, error) => {
158
- 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|SOP|The SOP|Use \/agentrun sop)/.test(error.message))
159
+ // What may reach the operator is decided by the error's type, never by its text.
160
+ const message = error instanceof Error && (error instanceof WorkflowStoreError || error.name === 'ExtensionServiceError' || error instanceof OperatorError)
159
161
  ? 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.';
160
162
  show(s, message);
161
163
  };
@@ -166,10 +168,10 @@ function registerExtension(pi, configuration) {
166
168
  };
167
169
  const inspect = (ctx, s, workflow, input) => {
168
170
  if (s.busy && (workflow !== undefined || input !== undefined))
169
- throw new Error('A workflow is running. Stop it before changing its definition or input.');
171
+ throw new OperatorError('A workflow is running. Stop it before changing its definition or input.');
170
172
  const candidate = workflow === undefined ? s.draft : workflow;
171
173
  if (candidate === undefined)
172
- throw new Error('No workflow yet. Describe a task with /agentrun <task>.');
174
+ throw new OperatorError('No workflow yet. Describe a task with /agentrun <task>.');
173
175
  // Include active tools available only with operator trust, without granting that trust.
174
176
  // Execution rechecks the actual per-run permissions and captures implementations.
175
177
  const checker = new WorkflowExtensionService({ allowedTools: toolSet(ctx, true).map(tool => tool.name), limits: serviceLimits });
@@ -208,7 +210,7 @@ function registerExtension(pi, configuration) {
208
210
  signal.throwIfAborted();
209
211
  toolCalls++;
210
212
  if (limits.toolAttempts !== null && toolCalls > limits.toolAttempts)
211
- throw new Error('Workflow tool-call limit reached');
213
+ throw new OperatorError('Workflow tool-call limit reached');
212
214
  configuration.onToolAttempt?.();
213
215
  signal.throwIfAborted();
214
216
  };
@@ -226,7 +228,7 @@ function registerExtension(pi, configuration) {
226
228
  signal.throwIfAborted();
227
229
  modelCalls++;
228
230
  if (limits.modelRequests !== null && modelCalls > limits.modelRequests)
229
- throw new Error('Workflow model-request limit reached');
231
+ throw new OperatorError('Workflow model-request limit reached');
230
232
  return ctx.modelRegistry.streamSimple(model, transcript, { ...options, reasoning: options?.reasoning === 'off' ? undefined : options?.reasoning, signal: options?.signal ? AbortSignal.any([signal, options.signal]) : signal });
231
233
  },
232
234
  },
@@ -249,26 +251,26 @@ function registerExtension(pi, configuration) {
249
251
  judge = createJevRunner({ signal, maxAttempts: 1 });
250
252
  }
251
253
  catch {
252
- throw new Error('AgentRun requires TypeSafe configuration for this workflow. Set TYPESAFE_API_KEY and, for a gateway, TYPESAFE_BASE_URL before running.');
254
+ throw new OperatorError('AgentRun requires TypeSafe configuration for this workflow. Set TYPESAFE_API_KEY and, for a gateway, TYPESAFE_BASE_URL before running.');
253
255
  }
254
256
  }
255
257
  }
256
258
  return {
257
259
  runNode: request => {
258
260
  if (!runner)
259
- throw new Error('Workflow agent adapter was not admitted');
261
+ throw new OperatorError('Workflow agent adapter was not admitted');
260
262
  return runner(request);
261
263
  },
262
264
  runJudge: request => {
263
265
  if (!judge)
264
- throw new Error('Workflow Jev adapter was not admitted');
266
+ throw new OperatorError('Workflow Jev adapter was not admitted');
265
267
  return judge(request);
266
268
  },
267
269
  runEffect: async ({ node, input, signal: effectSignal }) => {
268
270
  countTool();
269
271
  const tool = tools.find(tool => tool.name === node.tool);
270
272
  if (!tool || node.via !== 'tool')
271
- throw new Error('Workflow requested an unavailable tool');
273
+ throw new OperatorError('Workflow requested an unavailable tool');
272
274
  let result;
273
275
  try {
274
276
  result = await tool.execute('agentrun-effect', input, effectSignal, undefined, workflowToolContext(ctx));
@@ -294,21 +296,21 @@ function registerExtension(pi, configuration) {
294
296
  else if (s.sop) {
295
297
  const read = await readSopFile(ctx.cwd, s.sop.file);
296
298
  if (read.sha256 !== s.sop.sha256)
297
- throw new Error(`SOP file ${s.sop.file} changed since it was supplied. Run /agentrun sop ${s.sop.file} again to use the new text.`);
299
+ throw new OperatorError(`SOP file ${s.sop.file} changed since it was supplied. Run /agentrun sop ${s.sop.file} again to use the new text.`);
298
300
  sop = read;
299
301
  }
300
302
  else
301
- throw new Error(`Workflow requires SOP text for ${needed.map(name => `"${name}"`).join(', ')}. Supply it with /agentrun sop <file>.`);
303
+ throw new OperatorError(`Workflow requires SOP text for ${needed.map(name => `"${name}"`).join(', ')}. Supply it with /agentrun sop <file>.`);
302
304
  const missing = missingSopSections(sop.text, needed);
303
305
  if (missing.length)
304
- throw new Error(`SOP text has no "## <section>" heading for ${missing.map(name => `"${name}"`).join(', ')}.`);
306
+ throw new OperatorError(`SOP text has no "## <section>" heading for ${missing.map(name => `"${name}"`).join(', ')}.`);
305
307
  return sop;
306
308
  };
307
309
  const execute = async (ctx, s, input, options = {}) => {
308
310
  if (s.busy)
309
- throw new Error('A workflow is already running. Use /agentrun stop first.');
311
+ throw new OperatorError('A workflow is already running. Use /agentrun stop first.');
310
312
  if (!s.draft)
311
- throw new Error('Inspect a workflow first. Try /agentrun demo, or describe a task with /agentrun <task>.');
313
+ throw new OperatorError('Inspect a workflow first. Try /agentrun demo, or describe a task with /agentrun <task>.');
312
314
  const scripted = s.demo !== undefined;
313
315
  s.busy = true;
314
316
  s.controller = new AbortController();
@@ -427,7 +429,7 @@ function registerExtension(pi, configuration) {
427
429
  * only when the user's command trusts this dry run. */
428
430
  const dryRun = async (ctx, s, input, trusted, signal) => {
429
431
  if (!s.draft)
430
- throw new Error('Inspect a workflow first. Try /agentrun demo, or describe a task with /agentrun <task>.');
432
+ throw new OperatorError('Inspect a workflow first. Try /agentrun demo, or describe a task with /agentrun <task>.');
431
433
  const draft = s.draft;
432
434
  // /agentrun stop, the tool call's own abort and the session's end each stop it, from before its first await on.
433
435
  const controller = new AbortController();
@@ -454,7 +456,7 @@ function registerExtension(pi, configuration) {
454
456
  };
455
457
  const launch = async (ctx, s, input, trusted = false) => {
456
458
  if (s.busy)
457
- throw new Error('A workflow is already running. Use /agentrun stop first.');
459
+ throw new OperatorError('A workflow is already running. Use /agentrun stop first.');
458
460
  // Capture host selection and permission before yielding the interactive command loop.
459
461
  const runContext = { ...ctx, model: ctx.model, thinkingLevel: ctx.thinkingLevel ?? pi.getThinkingLevel() };
460
462
  const pending = execute(runContext, s, structuredClone(input), { trusted })
@@ -489,9 +491,9 @@ function registerExtension(pi, configuration) {
489
491
  const store = new WorkflowStore(ctx.cwd);
490
492
  if (args.action === 'save') {
491
493
  if (s.busy)
492
- throw new Error('A workflow is running. Save after it stops.');
494
+ throw new OperatorError('A workflow is running. Save after it stops.');
493
495
  if (!s.draft)
494
- throw new Error('No workflow to save. Inspect a workflow first.');
496
+ throw new OperatorError('No workflow to save. Inspect a workflow first.');
495
497
  const saved = await store.save(args.name ?? '', s.draft);
496
498
  s.savedName = saved.name;
497
499
  persist(s);
@@ -585,9 +587,9 @@ function registerExtension(pi, configuration) {
585
587
  }
586
588
  else if (/^save(?:\s|$)/.test(command)) {
587
589
  if (s.busy)
588
- throw new Error('A workflow is running. Save after it stops.');
590
+ throw new OperatorError('A workflow is running. Save after it stops.');
589
591
  if (!s.draft)
590
- throw new Error('No workflow to save. Inspect a workflow first.');
592
+ throw new OperatorError('No workflow to save. Inspect a workflow first.');
591
593
  const saved = await new WorkflowStore(ctx.cwd).save(command.slice(4).trim(), s.draft);
592
594
  s.savedName = saved.name;
593
595
  persist(s);
@@ -641,23 +643,23 @@ function registerExtension(pi, configuration) {
641
643
  return;
642
644
  }
643
645
  if (s.busy)
644
- throw new Error('A workflow is running. Stop it before changing its SOP.');
646
+ throw new OperatorError('A workflow is running. Stop it before changing its SOP.');
645
647
  if (configuration.sop)
646
- throw new Error('SOP text comes from the host; /agentrun sop is not used.');
648
+ throw new OperatorError('SOP text comes from the host; /agentrun sop is not used.');
647
649
  const read = await readSopFile(ctx.cwd, file);
648
650
  const needed = s.draft ? inspectWorkflow(s.draft).requires.sopSections : [];
649
651
  const missing = missingSopSections(read.text, needed);
650
652
  if (missing.length)
651
- throw new Error(`SOP text has no "## <section>" heading for ${missing.map(name => `"${name}"`).join(', ')}. Nothing was changed.`);
653
+ throw new OperatorError(`SOP text has no "## <section>" heading for ${missing.map(name => `"${name}"`).join(', ')}. Nothing was changed.`);
652
654
  s.sop = { file: read.file, sha256: read.sha256 };
653
655
  persist(s);
654
656
  show(s, `SOP set: ${read.file} (${read.text.length} characters). ${needed.length ? `Covers ${needed.map(name => `"${name}"`).join(', ')}.` : 'The current workflow names no section.'}`);
655
657
  }
656
658
  else if (/^input(?:\s|$)/.test(command)) {
657
659
  if (!s.draft)
658
- throw new Error('No workflow yet. Load or describe a procedure first.');
660
+ throw new OperatorError('No workflow yet. Load or describe a procedure first.');
659
661
  if (s.busy)
660
- throw new Error('A workflow is running. Stop it before changing its input.');
662
+ throw new OperatorError('A workflow is running. Stop it before changing its input.');
661
663
  let supplied = raw.slice(5).trim();
662
664
  if (!supplied && ctx.hasUI)
663
665
  supplied = await ctx.ui.input('New workflow input', 'Describe the new case in plain language, or paste a JSON object.') ?? '';
@@ -671,10 +673,10 @@ function registerExtension(pi, configuration) {
671
673
  input = JSON.parse(supplied);
672
674
  }
673
675
  catch {
674
- throw new Error('Workflow input must be valid JSON, or describe it in plain language.');
676
+ throw new OperatorError('Workflow input must be valid JSON, or describe it in plain language.');
675
677
  }
676
678
  if (!input || typeof input !== 'object' || Array.isArray(input))
677
- throw new Error('Workflow input must be an object.');
679
+ throw new OperatorError('Workflow input must be an object.');
678
680
  inspect(ctx, s, undefined, input);
679
681
  show(s, `Input updated. Nothing was run.\n\n${formatWorkflowView(view(ctx, s))}`);
680
682
  }
@@ -740,12 +742,12 @@ function registerExtension(pi, configuration) {
740
742
  }
741
743
  else {
742
744
  if (s.busy)
743
- throw new Error('A workflow is running. Stop it before starting another.');
745
+ throw new OperatorError('A workflow is running. Stop it before starting another.');
744
746
  if (!pi.getCommands().some(item => item.name === `skill:${AUTHOR_SKILL_NAME}` && item.source === 'skill')) {
745
- throw new Error('AgentRun authoring skill is unavailable. Enable package skills, then /reload. Run /agentrun status to check discovery.');
747
+ throw new OperatorError('AgentRun authoring skill is unavailable. Enable package skills, then /reload. Run /agentrun status to check discovery.');
746
748
  }
747
749
  if (!readiness(ctx, s).pi)
748
- throw new Error(PI_MODEL_SETUP_MESSAGE);
750
+ throw new OperatorError(PI_MODEL_SETUP_MESSAGE);
749
751
  pi.sendUserMessage(`/skill:${AUTHOR_SKILL_NAME} ${raw}`, { expandPromptTemplates: true });
750
752
  }
751
753
  }
@@ -2,16 +2,17 @@ import { Agent } from '@earendil-works/pi-agent-core';
2
2
  import { SUBMIT_TOOL } from './host-tools.js';
3
3
  import { createPiRunner } from './runner.js';
4
4
  import { workflowToolContext } from './tool-context.js';
5
+ import { OperatorError } from './operator-error.js';
5
6
  export const PI_MODEL_SETUP_MESSAGE = 'AgentRun requires an active Pi model. Use /login to connect a provider, then /model to select it. Try /agentrun demo without model access.';
6
7
  function selectedModel(context) {
7
8
  const model = context.model;
8
9
  if (!model)
9
- throw new Error(PI_MODEL_SETUP_MESSAGE);
10
+ throw new OperatorError(PI_MODEL_SETUP_MESSAGE);
10
11
  if (!context.modelRegistry || typeof context.modelRegistry.streamSimple !== 'function' || typeof context.modelRegistry.getAll !== 'function') {
11
- throw new Error('AgentRun requires the host public model registry');
12
+ throw new OperatorError('AgentRun requires the host public model registry');
12
13
  }
13
14
  if (!context.modelRegistry.getAll().some(known => known.id === model.id && known.provider === model.provider && known.api === model.api)) {
14
- throw new Error(PI_MODEL_SETUP_MESSAGE);
15
+ throw new OperatorError(PI_MODEL_SETUP_MESSAGE);
15
16
  }
16
17
  return model;
17
18
  }
@@ -0,0 +1,4 @@
1
+ /** An error whose message is written for the operator, so the extension shows it as it is. Any other error reaches the
2
+ * operator as a generic message, since its text may carry what an operator should not see. Its name stays `Error`. */
3
+ export declare class OperatorError extends Error {
4
+ }
@@ -0,0 +1,4 @@
1
+ /** An error whose message is written for the operator, so the extension shows it as it is. Any other error reaches the
2
+ * operator as a generic message, since its text may carry what an operator should not see. Its name stays `Error`. */
3
+ export class OperatorError extends Error {
4
+ }
package/dist/sop.js CHANGED
@@ -1,25 +1,26 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { lstat, readFile, realpath } from 'node:fs/promises';
3
3
  import { isAbsolute, relative, resolve, sep } from 'node:path';
4
+ import { OperatorError } from './operator-error.js';
4
5
  export const MAX_SOP_BYTES = 512 * 1024;
5
6
  export const sopDigest = (text) => createHash('sha256').update(text).digest('hex');
6
7
  /** The SOP text of a regular file inside `cwd`; a path that resolves outside it, or a file over the bound, is refused. */
7
8
  export async function readSopFile(cwd, file) {
8
9
  if (!file.trim() || file.includes('\0'))
9
- throw new Error('Use /agentrun sop <file>, a path inside this project.');
10
+ throw new OperatorError('Use /agentrun sop <file>, a path inside this project.');
10
11
  const root = await realpath(cwd);
11
- const target = await realpath(resolve(cwd, file)).catch(() => { throw new Error(`SOP file not found: ${file}`); });
12
+ const target = await realpath(resolve(cwd, file)).catch(() => { throw new OperatorError(`SOP file not found: ${file}`); });
12
13
  const inside = relative(root, target);
13
14
  if (!inside || inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside))
14
- throw new Error('The SOP file must be inside this project.');
15
+ throw new OperatorError('The SOP file must be inside this project.');
15
16
  const info = await lstat(target);
16
17
  if (!info.isFile())
17
- throw new Error('The SOP must be a regular file.');
18
+ throw new OperatorError('The SOP must be a regular file.');
18
19
  if (info.size > MAX_SOP_BYTES)
19
- throw new Error(`The SOP file is larger than ${MAX_SOP_BYTES / 1024} KiB.`);
20
+ throw new OperatorError(`The SOP file is larger than ${MAX_SOP_BYTES / 1024} KiB.`);
20
21
  const text = (await readFile(target, 'utf8')).replace(/^/, '');
21
22
  if (!text.trim())
22
- throw new Error('The SOP file is empty.');
23
+ throw new OperatorError('The SOP file is empty.');
23
24
  return { file: inside.split(sep).join('/'), text, sha256: sopDigest(text) };
24
25
  }
25
26
  /** The sections a workflow names that the SOP has no `## <section>` heading line for. */
@@ -4,6 +4,7 @@ import { Type } from 'typebox';
4
4
  import { compileTransform, dryRunWorkflow, formatWorkflowTree, inspectWorkflow, loadAuthorReference, renderAuthorHostAddendum, } from '@parcha/agentrun-dsl';
5
5
  import { ExtensionServiceError } from './extension-service.js';
6
6
  import { PI_HOST_ADDENDUM } from './host-addendum.js';
7
+ import { OperatorError } from './operator-error.js';
7
8
  export const WORKFLOW_TOOL_ACTIONS = ['describe', 'inspect', 'dry-run', 'run', 'save', 'load', 'list'];
8
9
  /** The tool's fields: one action, and the fields an action reads. A host's tool may add its own. */
9
10
  export const workflowToolProperties = () => ({ action: Type.Union(WORKFLOW_TOOL_ACTIONS.map(value => Type.Literal(value))),
@@ -29,7 +30,7 @@ export const authoringReferences = (host = PI_HOST_ADDENDUM) => ({
29
30
  * refusal throws, naming why. */
30
31
  export function inspectCandidate(checker, candidate, input) {
31
32
  if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) {
32
- throw new Error('Cannot inspect workflow: workflow must be a JSON object, not a JSON-encoded string or array. Pass the complete object directly.');
33
+ throw new OperatorError('Cannot inspect workflow: workflow must be a JSON object, not a JSON-encoded string or array. Pass the complete object directly.');
33
34
  }
34
35
  let inspection;
35
36
  try {
@@ -38,7 +39,7 @@ export function inspectCandidate(checker, candidate, input) {
38
39
  catch (error) {
39
40
  const message = `Cannot inspect workflow: ${error instanceof Error ? error.message : 'Invalid workflow'}`;
40
41
  // The service's refusal keeps its type and code, so a host branches on it as it does for a run; either keeps its cause.
41
- throw error instanceof ExtensionServiceError ? Object.assign(new ExtensionServiceError(error.code, message), { cause: error }) : new Error(message, { cause: error });
42
+ throw error instanceof ExtensionServiceError ? Object.assign(new ExtensionServiceError(error.code, message), { cause: error }) : new OperatorError(message, { cause: error });
42
43
  }
43
44
  return { inspection, tree: formatWorkflowTree(inspection) };
44
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parcha/agentrun-pi",
3
- "version": "0.1.0-beta.26",
3
+ "version": "0.1.0-beta.27",
4
4
  "description": "Pi extension for building, running and evaluating AgentRun workflows",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -50,12 +50,12 @@
50
50
  "@earendil-works/pi-coding-agent": "1.1.0",
51
51
  "@earendil-works/pi-durable": "1.1.0",
52
52
  "@earendil-works/pi-tui": "1.1.0",
53
- "@parcha/pi-durable-disk": "0.1.0-beta.26",
53
+ "@parcha/pi-durable-disk": "0.1.0-beta.27",
54
54
  "typebox": "1.3.7"
55
55
  },
56
56
  "dependencies": {
57
- "@parcha/agentrun-dsl": "0.1.0-beta.26",
58
- "@parcha/agentrun-jev": "0.1.0-beta.26"
57
+ "@parcha/agentrun-dsl": "0.1.0-beta.27",
58
+ "@parcha/agentrun-jev": "0.1.0-beta.27"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@earendil-works/pi-agent-core": "*",