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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +104 -27
  2. package/dist/cli.js +15 -4
  3. package/dist/demo-cli.d.ts +2 -0
  4. package/dist/demo-cli.js +25 -0
  5. package/dist/demo.js +1 -0
  6. package/dist/durable/decision-recovery.d.ts +23 -0
  7. package/dist/durable/decision-recovery.js +57 -0
  8. package/dist/durable/decisions.d.ts +113 -0
  9. package/dist/durable/decisions.js +71 -0
  10. package/dist/durable/index.d.ts +20 -0
  11. package/dist/durable/index.js +14 -0
  12. package/dist/durable/node-runner.d.ts +106 -0
  13. package/dist/durable/node-runner.js +253 -0
  14. package/dist/durable/node.d.ts +88 -0
  15. package/dist/durable/node.js +98 -0
  16. package/dist/durable/pi-versions.d.ts +34 -0
  17. package/dist/durable/pi-versions.js +66 -0
  18. package/dist/durable/readers.d.ts +28 -0
  19. package/dist/durable/readers.js +103 -0
  20. package/dist/durable/receipts.d.ts +54 -0
  21. package/dist/durable/receipts.js +92 -0
  22. package/dist/durable/record-file.d.ts +10 -0
  23. package/dist/durable/record-file.js +56 -0
  24. package/dist/durable/record-tool.d.ts +76 -0
  25. package/dist/durable/record-tool.js +123 -0
  26. package/dist/durable/record.d.ts +157 -0
  27. package/dist/durable/record.js +402 -0
  28. package/dist/durable/recording.d.ts +90 -0
  29. package/dist/durable/recording.js +292 -0
  30. package/dist/durable/store.d.ts +23 -0
  31. package/dist/durable/store.js +240 -0
  32. package/dist/durable/task.d.ts +133 -0
  33. package/dist/durable/task.js +411 -0
  34. package/dist/durable/workflow-tool.d.ts +32 -0
  35. package/dist/durable/workflow-tool.js +188 -0
  36. package/dist/extension-service.js +63 -22
  37. package/dist/extension.d.ts +2 -0
  38. package/dist/extension.js +469 -80
  39. package/dist/host-addendum.d.ts +4 -0
  40. package/dist/host-addendum.js +20 -0
  41. package/dist/host-session.js +5 -2
  42. package/dist/index.d.ts +1 -2
  43. package/dist/index.js +1 -2
  44. package/dist/pi-host.d.ts +7 -0
  45. package/dist/pi-host.js +22 -0
  46. package/dist/presentation.js +2 -2
  47. package/dist/run-error.d.ts +6 -0
  48. package/dist/run-error.js +13 -0
  49. package/dist/runner.d.ts +1 -6
  50. package/dist/runner.js +7 -13
  51. package/dist/signin/index.d.ts +9 -0
  52. package/dist/signin/index.js +7 -0
  53. package/dist/signin/limit.d.ts +10 -0
  54. package/dist/signin/limit.js +23 -0
  55. package/dist/signin/resolve.d.ts +47 -0
  56. package/dist/signin/resolve.js +163 -0
  57. package/dist/signin/signin.d.ts +44 -0
  58. package/dist/signin/signin.js +126 -0
  59. package/dist/signin/spec.d.ts +116 -0
  60. package/dist/signin/spec.js +181 -0
  61. package/dist/signin/store.d.ts +40 -0
  62. package/dist/signin/store.js +206 -0
  63. package/dist/skills/author/SKILL.md +138 -0
  64. package/{skills → dist/skills}/author/references/jev-decisions.md +27 -14
  65. package/dist/skills/author/references/language.md +134 -0
  66. package/dist/skills/author/references/sdk-authoring.md +11 -0
  67. package/{skills → dist/skills}/author/references/workflow-format.md +7 -15
  68. package/dist/sop.d.ts +12 -0
  69. package/dist/sop.js +29 -0
  70. package/dist/thinking.d.ts +3 -0
  71. package/dist/thinking.js +7 -0
  72. package/dist/tool-context.d.ts +21 -0
  73. package/dist/tool-context.js +13 -0
  74. package/dist/triage-demo.d.ts +62 -0
  75. package/dist/triage-demo.js +153 -0
  76. package/dist/types.d.ts +7 -0
  77. package/dist/workflow-inspector.d.ts +47 -0
  78. package/dist/workflow-inspector.js +235 -0
  79. package/dist/workflow-session.d.ts +23 -0
  80. package/dist/workflow-session.js +168 -0
  81. package/dist/workflow-store.d.ts +36 -0
  82. package/dist/workflow-store.js +255 -0
  83. package/dist/workflow-tool.d.ts +81 -0
  84. package/dist/workflow-tool.js +94 -0
  85. package/dist/workflow-view.d.ts +64 -0
  86. package/dist/workflow-view.js +273 -0
  87. package/dist/workflows.d.ts +1 -0
  88. package/dist/workflows.js +3 -0
  89. package/package.json +57 -11
  90. package/dist/admission.d.ts +0 -6
  91. package/dist/admission.js +0 -51
  92. package/dist/author.d.ts +0 -21
  93. package/dist/author.js +0 -72
  94. package/dist/benchmark-contract.d.ts +0 -88
  95. package/dist/benchmark-contract.js +0 -50
  96. package/dist/benchmark-freeze.d.ts +0 -9
  97. package/dist/benchmark-freeze.js +0 -9
  98. package/dist/benchmark.d.ts +0 -127
  99. package/dist/benchmark.js +0 -260
  100. package/dist/comparison.d.ts +0 -69
  101. package/dist/comparison.js +0 -229
  102. package/dist/skill-bundle.d.ts +0 -5
  103. package/dist/skill-bundle.js +0 -15
  104. package/skills/author/SKILL.md +0 -102
  105. package/skills/author/references/sdk-authoring.md +0 -16
  106. /package/{skills → dist/skills}/author/examples/evidence-gate.input.json +0 -0
  107. /package/{skills → dist/skills}/author/examples/evidence-gate.json +0 -0
  108. /package/{skills → dist/skills}/author/examples/read-file.input.json +0 -0
  109. /package/{skills → dist/skills}/author/examples/read-file.json +0 -0
  110. /package/{skills → dist/skills}/author/examples/read-source-decision.input.json +0 -0
  111. /package/{skills → dist/skills}/author/examples/read-source-decision.json +0 -0
  112. /package/{skills → dist/skills}/author/examples/release-note.txt +0 -0
  113. /package/{skills → dist/skills}/author/examples/review-release-notes.input.json +0 -0
  114. /package/{skills → dist/skills}/author/examples/review-release-notes.json +0 -0
@@ -0,0 +1,188 @@
1
+ // The agentrun tool for a pi-durable session. describe, inspect, dry-run, save, load and list do what the coding-agent
2
+ // extension's tool does, through the same module (../workflow-tool.ts); run starts the workflow as one background task
3
+ // of the calling conversation through createWorkflowExtension, from the tool's own invocation, and answers with its
4
+ // outcome, writing the run's progress to the tool's running output as it goes. The draft a conversation inspected is a
5
+ // document of the session, so a reopened session keeps it. Code nodes run only through the host's code executor.
6
+ import { defineDocFamily, defineExtension, defineTool } from "@earendil-works/pi-durable";
7
+ import { Type } from "typebox";
8
+ import { inspectWorkflow, WORKFLOW_NODE_KINDS } from "@parcha/agentrun-dsl";
9
+ import { canonicalHash } from "@parcha/agentrun-dsl/recovery";
10
+ import { extensionStructuralLimits, WorkflowExtensionService } from "../extension-service.js";
11
+ import { modelJson } from "../presentation.js";
12
+ import { WorkflowStore } from "../workflow-store.js";
13
+ import { progressLines, WorkflowObservation } from "../workflow-view.js";
14
+ import { authoringReferences, describeTools, dryRunDraft, inspectCandidate, workflowToolProperties } from "../workflow-tool.js";
15
+ import { toolResult } from "./pi-versions.js";
16
+ import { createWorkflowExtension } from "./task.js";
17
+ /** What a pi-durable session adds to the workflow language, rendered for describe's `authoring.host`. */
18
+ export const DURABLE_HOST_ADDENDUM = Object.freeze({
19
+ name: "pi-durable session",
20
+ nodeKinds: WORKFLOW_NODE_KINDS.filter((kind) => kind !== "artifact"),
21
+ rules: [
22
+ "Call agentrun with action describe first, then inspect the complete workflow with its input, then dry-run it (action dry-run, without a workflow argument), then run without a workflow argument.",
23
+ "A dry run runs the inspected workflow on synthetic values, or the input you give it, with no model, Jev or tool call. Its status is passed, not_run (a check could not run; skipped says why, and it proves nothing) or failed (problems say why). Report not_run as not run, never as passed.",
24
+ "Use exactly the tools describe returns, as a call's tool and as an agent node's tools: the host registers them, and inspect refuses any other, naming it. A call uses via tool with one of them. Shell and executor calls and artifact delivery are unavailable.",
25
+ "Code nodes run only through the host's code executor. Without one, a dry run of a workflow with code is not_run and a run of it is refused.",
26
+ "A run is a pi-durable task of this conversation: it continues after the session reopens, and the run action answers with its outcome once it ends. Running the same draft on the same input again reaches that run: a live one is waited for, a finished one answers with its outcome, and its effects are not made again. To run the same draft on the same input anew, pass again: true; a later run without it reaches the newest one.",
27
+ "Save keeps a named revision of the inspected definition, not its input. Load replaces the draft with a saved revision and checks it against this host's tools. List shows the saved revisions.",
28
+ "The structural limits describe returns bound node count, map concurrency and parallel branches.",
29
+ ],
30
+ });
31
+ const DraftDoc = defineDocFamily({ kind: "agentrun.draft", version: 1, scope: "session", family: true, initial: () => ({ workflow: null, input: {} }) });
32
+ /** How many times `again` asked for a fresh run of one draft and input in a conversation: the newest run's number. */
33
+ const RunsDoc = defineDocFamily({ kind: "agentrun.runs", version: 1, scope: "session", family: true, initial: () => ({ generation: 0 }) });
34
+ const json = (value) => (value === undefined ? null : JSON.parse(JSON.stringify(value)));
35
+ const answer = (value) => toolResult([{ type: "text", text: modelJson(value) }], { details: json(value) });
36
+ /** The tool, and the workflow task its runs are: install `extension` (it carries both) and list the tool's name in the
37
+ * agent definition. `workflow` reads and stops runs from the host's side. */
38
+ export function createWorkflowTool(host) {
39
+ const names = (host.tools ?? []).map((tool) => tool.name);
40
+ const checker = () => new WorkflowExtensionService({ allowedTools: names });
41
+ /** Inspect's checks, after naming any tool the workflow uses (a call's, or an agent node's) that the host did not
42
+ * register: the host decides what a drafted workflow may use. */
43
+ const admit = (candidate, input) => {
44
+ let used = [];
45
+ try {
46
+ used = inspectWorkflow(candidate).requires.tools;
47
+ }
48
+ catch { /* inspect names what is wrong */ }
49
+ const missing = used.filter((name) => !names.includes(name));
50
+ if (missing.length)
51
+ throw new Error(`Cannot inspect workflow: it uses ${missing.length === 1 ? "a tool" : "tools"} this session's host has not registered: ${missing.map((name) => JSON.stringify(name)).join(", ")}. A workflow may call, or give an agent node, only the tools describe lists; a host registers them in createWorkflowTool's tools.`);
52
+ return inspectCandidate(checker(), candidate, input);
53
+ };
54
+ const store = () => new WorkflowStore(host.projectDir);
55
+ // A run's events reach the tool call that started it, while both live in this process.
56
+ const listeners = new Map();
57
+ const listen = (key, listener) => {
58
+ const set = listeners.get(key) ?? new Set();
59
+ listeners.set(key, set.add(listener));
60
+ return () => { set.delete(listener); if (!set.size)
61
+ listeners.delete(key); };
62
+ };
63
+ const given = { ...(host.runCode ? { runCode: host.runCode } : {}), ...(host.sop === undefined ? {} : { sop: host.sop }) };
64
+ const workflow = createWorkflowExtension({
65
+ ...host.task,
66
+ run: async (invocation) => {
67
+ const parts = await host.task.run(invocation);
68
+ // The host's own observer's result goes back to the interpreter, which consumes a rejected one; a progress listener
69
+ // that fails (its call ended, say) changes nothing in the run.
70
+ const tee = (own) => (event) => {
71
+ for (const listener of listeners.get(invocation.key) ?? []) {
72
+ try {
73
+ listener(event);
74
+ }
75
+ catch { /* progress only */ }
76
+ }
77
+ return own?.(event);
78
+ };
79
+ // The executor and SOP text the host gave the tool are the run's, as they are its dry run's.
80
+ return "deps" in parts
81
+ ? { ...parts, deps: { ...parts.deps, ...given, onEvent: tee(parts.deps.onEvent) } }
82
+ : { ...parts, adapters: { ...parts.adapters, ...given, onEvent: tee(parts.adapters.onEvent) } };
83
+ },
84
+ });
85
+ const tool = defineTool({
86
+ name: host.name ?? "agentrun",
87
+ description: "Compose and run AgentRun workflows in this session. Describe lists the tools a workflow may use, as a call's tool or an agent node's tools, and the language; a workflow uses only the tools describe lists, which the host registers. Inspect validates a complete workflow and keeps it as this conversation's draft. Dry-run checks the draft on synthetic values (or the given input) with no model, Jev or tool call. Run starts the draft as a durable task of this conversation and answers with its outcome. Running the same draft on the same input again, after a restart too, reaches the same run and makes no effect twice; again: true starts a fresh run of them, which a later run then reaches. Save, load and list keep named definition revisions.",
88
+ parameters: Type.Object({ ...workflowToolProperties(), again: Type.Optional(Type.Boolean({ description: "Run only: start a fresh run of the same draft and input instead of reaching the one already run." })) }),
89
+ executionMode: "sequential",
90
+ async execute(given, api, context) {
91
+ const args = given;
92
+ const key = String(api.conversationId);
93
+ const held = async () => {
94
+ const draft = await api.snapshot(DraftDoc, key, context);
95
+ return draft && draft.workflow !== null ? draft : undefined;
96
+ };
97
+ const keep = (candidate, input) => api.commit(async (tx) => {
98
+ const draft = await tx.doc(DraftDoc, key, null);
99
+ draft.workflow = json(candidate);
100
+ draft.input = json(input);
101
+ return undefined;
102
+ }, context);
103
+ const needDraft = async (action) => {
104
+ if (args.workflow !== undefined)
105
+ throw new Error(`Inspect the workflow first with action inspect, then ${action} without a workflow argument.`);
106
+ const draft = await held();
107
+ if (!draft)
108
+ throw new Error(`Inspect a workflow first, then ${action} it.`);
109
+ return draft;
110
+ };
111
+ switch (args.action) {
112
+ case "describe":
113
+ return answer({ tools: describeTools(host.tools ?? []), structuralLimits: { ...extensionStructuralLimits }, authoring: authoringReferences(DURABLE_HOST_ADDENDUM) });
114
+ case "inspect": {
115
+ const draft = await held();
116
+ const candidate = args.workflow ?? draft?.workflow;
117
+ if (candidate === undefined)
118
+ throw new Error("No workflow yet. Pass the complete workflow to inspect.");
119
+ // Without input, the input is not checked, as the coding-agent tool's inspect does; the draft keeps none.
120
+ const input = args.input ?? (args.workflow === undefined ? draft?.input : undefined);
121
+ const { tree } = admit(candidate, input);
122
+ await keep(candidate, input ?? {});
123
+ return answer({ preview: tree, workflow: candidate, input: input ?? {} });
124
+ }
125
+ case "dry-run": {
126
+ const draft = await needDraft("dry-run");
127
+ const dryRun = await dryRunDraft(draft.workflow, {
128
+ ...(args.input === undefined ? {} : { input: args.input }), ...(host.sop === undefined ? {} : { sop: host.sop }),
129
+ ...(host.runCode ? { runCode: host.runCode } : {}),
130
+ });
131
+ return answer({ dryRun });
132
+ }
133
+ case "save": {
134
+ const draft = await needDraft("save");
135
+ return answer(await store().save(args.name ?? "", draft.workflow));
136
+ }
137
+ case "list":
138
+ return answer({ workflows: await store().list() });
139
+ case "load": {
140
+ const saved = await store().load(args.name ?? "", args.digest);
141
+ const { tree } = admit(saved.workflow, args.input);
142
+ await keep(saved.workflow, args.input ?? {});
143
+ return answer({ preview: tree, workflow: saved.workflow, input: args.input ?? {}, saved: { name: saved.name, digest: saved.digest } });
144
+ }
145
+ case "run": {
146
+ const draft = await needDraft("run");
147
+ const input = args.input ?? draft.input;
148
+ const { inspection } = admit(draft.workflow, input);
149
+ const code = inspection.nodes.filter((node) => node.kind === "code").map((node) => JSON.stringify(node.label));
150
+ if (code.length && !host.runCode)
151
+ throw new Error(`Code node${code.length === 1 ? "" : "s"} ${code.join(", ")}: this host runs code only through its own executor and gave none, so the workflow does not run here. Nothing ran.`);
152
+ if (inspection.requires.sopSections.length && host.sop === undefined)
153
+ throw new Error(`The workflow needs SOP text for ${inspection.requires.sopSections.map((name) => JSON.stringify(name)).join(", ")}, and this host supplies none. Nothing ran.`);
154
+ // One run per draft and input in a conversation: a run asked for again (after a restart, say) reaches the same
155
+ // task, so its effects happen once. A live run is attached to, and a finished one answers with its outcome.
156
+ // `again` starts the next one; the count is a session document, so a later run, after a restart too, reaches it.
157
+ const base = `agentrun:${api.conversationId}:${inspection.sha256}:${canonicalHash(input)}`;
158
+ const generation = args.again
159
+ ? await api.commit(async (tx) => { const runs = await tx.doc(RunsDoc, base, null); runs.generation += 1; return runs.generation; }, context)
160
+ : (await api.snapshot(RunsDoc, base, context))?.generation ?? 0;
161
+ const runKey = generation ? `${base}:${generation}` : base;
162
+ const observation = new WorkflowObservation();
163
+ let shown = "";
164
+ const stop = listen(runKey, (event) => {
165
+ observation.record(event);
166
+ const text = progressLines(observation.data).join(" · ");
167
+ if (text !== shown) {
168
+ shown = text;
169
+ api.output(`${text}\n`);
170
+ }
171
+ });
172
+ try {
173
+ const run = await workflow.start(api, { key: runKey, workflow: draft.workflow, input }, context, { conversationId: Number(api.conversationId), background: true });
174
+ const outcome = await run.wait(context);
175
+ // Only this call's events are counted: a run that had ended sends none, and one joined after a restart sent some before.
176
+ const progress = run.started === "settled" ? ["The run had ended; its progress is not kept, and its outcome is above."]
177
+ : run.started === "attached" ? ["Steps before this call are not counted.", ...progressLines(observation.data)] : progressLines(observation.data);
178
+ return answer({ run: { key: runKey, taskId: run.taskId, started: run.started }, outcome, progress });
179
+ }
180
+ finally {
181
+ stop();
182
+ }
183
+ }
184
+ }
185
+ },
186
+ });
187
+ return { extension: defineExtension({ name: "agentrun.tool", tools: [tool], tasks: workflow.extension.tasks }), workflow };
188
+ }
@@ -1,9 +1,9 @@
1
1
  import { types as utilTypes } from 'node:util';
2
2
  import { JevError, isJevResponseReason } from '@parcha/agentrun-jev';
3
- import { PiRunError } from './runner.js';
3
+ import { RecoveryError } from '@parcha/agentrun-dsl/recovery';
4
+ import { PiRunError } from './run-error.js';
4
5
  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';
6
+ import { inspectWorkflow, runWorkflow, validateWorkflow, candidatePolicyErrors, EffectOutcomeUnknownError, WorkflowInvalidError, WorkflowInputInvalidError, WorkflowOutputInvalidError, WorkflowCodeError, WorkflowStateError, EffectDeadlineExceededError, } from '@parcha/agentrun-dsl';
7
7
  export class ExtensionServiceError extends Error {
8
8
  code;
9
9
  constructor(code, message) {
@@ -55,8 +55,10 @@ async function withAdapterDiagnostic(stage, run) {
55
55
  if (error.code === 'http') {
56
56
  if (Number.isInteger(error.status) && error.status >= 100 && error.status <= 599)
57
57
  diagnostic.status = error.status;
58
- if (reason === 'max_tokens_exceeded')
58
+ if (reason === 'max_tokens_exceeded') {
59
59
  diagnostic.reason = reason;
60
+ diagnostic.message = 'Jev refused the request: it exceeds the model\'s token budget. Send fewer or shorter items per request. No judgment was accepted; provider details are omitted.';
61
+ }
60
62
  }
61
63
  }
62
64
  }
@@ -92,7 +94,18 @@ async function withAdapterDiagnostic(stage, run) {
92
94
  throw error;
93
95
  }
94
96
  }
97
+ /** The code of the first RecoveryError in an error's cause chain. */
98
+ function recoveryCode(error) {
99
+ for (let current = error, depth = 0; current && depth < 16; current = current.cause, depth += 1) {
100
+ if (current instanceof RecoveryError)
101
+ return current.code;
102
+ }
103
+ return undefined;
104
+ }
95
105
  function localDiagnostic(error) {
106
+ if (recoveryCode(error) === 'FROZEN_EFFECT_UNKNOWN')
107
+ return { code: 'FROZEN_EFFECT_UNKNOWN',
108
+ message: 'A tool call of this run has no recorded outcome. It is not repeated. Find out whether it happened, then start a new run (change the input) or reconcile it in the run journal.' };
96
109
  if (error instanceof AggregateError)
97
110
  return error.errors.slice(0, 100).map(localDiagnostic).find(Boolean);
98
111
  if (error instanceof AdapterDiagnosticError)
@@ -112,6 +125,9 @@ function localDiagnostic(error) {
112
125
  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
126
  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
127
  reserved_state_key: 'A node or host hook wrote an engine-owned "$"-prefixed state key such as $host. Choose another state key; host state is written only through decodeSubmission.',
128
+ route_missing: 'A route by valuePath found no value at that path and declares no otherwise. Make the earlier step always write a branch name, or add otherwise for the missing case.',
129
+ route_unknown: 'A route by valuePath found a value that names none of its branches and declares no otherwise. Write only declared branch names upstream, or add otherwise for the rest.',
130
+ route_type: 'A route by valuePath found a value that is not a string. It reads a branch name and never converts numbers, booleans or objects; map them to a branch name in a code step first.',
115
131
  };
116
132
  return { code: error.code, stage: error.stage.slice(0, 200),
117
133
  // Parallel patch keys may be computed from source values, unlike authored paths.
@@ -125,7 +141,7 @@ function localDiagnostic(error) {
125
141
  && stateBytes > maxStateBytes && maxStateBytes > 0)
126
142
  return {
127
143
  code: 'jev_state_too_large', stage, stateBytes, maxStateBytes,
128
- 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.`
144
+ message: `Jev state at ${stage} is ${stateBytes} UTF-8 bytes; configured host limit is ${maxStateBytes}. No request was sent. A sift splits its items across requests only when the host sets maxStateBytesPerRequest; otherwise send less of each item or use per-item judgments without losing headers or qualifiers.`
129
145
  };
130
146
  if (reason === 'invalid_state' || reason === 'invalid_questions')
131
147
  return { code: `jev_${reason}`, stage,
@@ -259,7 +275,7 @@ export class WorkflowExtensionService {
259
275
  const inspection = inspectWorkflow(workflow);
260
276
  if (inspection.nodes.length > extensionStructuralLimits.maxNodes)
261
277
  throw new ExtensionServiceError('bounds', 'A workflow may contain at most 200 nodes.');
262
- const errors = workflowPolicyErrors(workflow, { allowExecutableCandidates: options.allowExecutableCandidates === true, rubricSections: this.rubricSections, allowedEffectTools: [...this.allowedTools] });
278
+ const errors = candidatePolicyErrors(workflow, { allowExecutableCandidates: options.allowExecutableCandidates === true, rubricSections: this.rubricSections, allowedEffectTools: [...this.allowedTools] });
263
279
  for (const entry of inspection.nodes) {
264
280
  const node = entry.path.split('/').slice(1).reduce((item, key) => item[key.replace(/~1/g, '/').replace(/~0/g, '~')], workflow);
265
281
  if (node.node === 'artifact')
@@ -362,11 +378,36 @@ export class WorkflowExtensionService {
362
378
  queue.shift()?.();
363
379
  }
364
380
  };
381
+ const rejectTrace = (error) => {
382
+ rejectedEvents++;
383
+ traceTruncated = true;
384
+ if (!signal.aborted) {
385
+ // Snapshot rejects unsafe/non-JSON values separately from resource bounds.
386
+ // Keep fail-closed cancellation; never echo values or arbitrary error text.
387
+ const diagnostic = error instanceof ExtensionServiceError && error.code === 'data'
388
+ ? 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.')
389
+ : error instanceof ExtensionServiceError && error.code === 'bounds'
390
+ ? new ExtensionServiceError('limit', 'Workflow trace exceeded its JSON byte, depth or value-count limit.')
391
+ : new ExtensionServiceError('trace_capture_failed', 'Workflow trace capture failed. Unsafe event details are omitted.');
392
+ controller.abort(diagnostic);
393
+ }
394
+ };
395
+ const prepareEvent = (event) => {
396
+ if (closed)
397
+ return { type: event.type, label: event.label };
398
+ try {
399
+ return snapshot(event, this.limits.maxEventBytes);
400
+ }
401
+ catch (error) {
402
+ rejectTrace(error);
403
+ return undefined;
404
+ }
405
+ };
365
406
  const onEvent = (event) => {
366
407
  if (closed)
367
408
  return;
368
409
  try {
369
- const clean = sanitize(snapshot(event, this.limits.maxEventBytes));
410
+ const clean = sanitize(event);
370
411
  const size = Buffer.byteLength(JSON.stringify(clean));
371
412
  if (size > this.limits.maxEventBytes)
372
413
  throw new ExtensionServiceError('bounds', 'Workflow event exceeds its serialized byte limit.');
@@ -406,26 +447,18 @@ export class WorkflowExtensionService {
406
447
  }
407
448
  }
408
449
  catch (error) {
409
- rejectedEvents++;
410
- traceTruncated = true;
411
- if (!signal.aborted) {
412
- // Snapshot rejects unsafe/non-JSON values separately from resource bounds.
413
- // Keep fail-closed cancellation; never echo values or arbitrary error text.
414
- const diagnostic = error instanceof ExtensionServiceError && error.code === 'data'
415
- ? 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.')
416
- : error instanceof ExtensionServiceError && error.code === 'bounds'
417
- ? new ExtensionServiceError('limit', 'Workflow trace exceeded its JSON byte, depth or value-count limit.')
418
- : new ExtensionServiceError('trace_capture_failed', 'Workflow trace capture failed. Unsafe event details are omitted.');
419
- controller.abort(diagnostic);
420
- }
450
+ rejectTrace(error);
421
451
  }
422
452
  };
423
453
  const sections = Object.entries(this.rubricSections);
424
454
  const deps = {
425
- signal, onEvent,
455
+ signal, onEvent, prepareEvent,
426
456
  ...(options.deps.skill ? { skill: options.deps.skill } : {}),
427
457
  ...(sections.length ? { sop: sections.map(([name, text]) => `## ${name}\n${text}`).join('\n\n') } : options.deps.sop ? { sop: options.deps.sop } : {}),
428
- ...(options.deps.maxQuestionsPerRequest ? { maxQuestionsPerRequest: options.deps.maxQuestionsPerRequest } : {}),
458
+ // Forward any set limit, so the interpreter rejects an invalid one rather than running without it.
459
+ ...(options.deps.recovery ? { recovery: options.deps.recovery } : {}),
460
+ ...(options.deps.maxQuestionsPerRequest !== undefined ? { maxQuestionsPerRequest: options.deps.maxQuestionsPerRequest } : {}),
461
+ ...(options.deps.maxStateBytesPerRequest !== undefined ? { maxStateBytesPerRequest: options.deps.maxStateBytesPerRequest } : {}),
429
462
  ...(options.deps.runNode ? { runNode: params => dispatch('agent', params.signal, () => withAdapterDiagnostic(params.label, () => options.deps.runNode({ ...params, tools: params.tools ?? [...this.allowedTools] })), true) } : {}),
430
463
  ...(options.deps.runJudge ? { runJudge: params => dispatch('judge', params.signal, () => withAdapterDiagnostic(params.label, () => options.deps.runJudge(params)), true) } : {}),
431
464
  ...(options.deps.runEffect ? { runEffect: params => dispatch('tool', params.signal, () => {
@@ -456,9 +489,17 @@ export class WorkflowExtensionService {
456
489
  failure.errors.slice(0, 100).forEach(collect);
457
490
  };
458
491
  collect(error);
492
+ const failedStep = events.findLast(event => event.type === 'node.end' && event.detail?.status === 'failed');
493
+ const failedPath = failedStep?.executionPath;
494
+ const toolFailed = failedStep && failedStep.detail?.kind === 'call'
495
+ && events.some(event => event.type === 'effect.failed' && event.executionPath === failedPath);
496
+ const observedStage = failedStep?.label.slice(0, 200);
459
497
  report = { digest: prepared.digest, status: code === 'cancelled' || code === 'deadline' ? 'interrupted' : 'failed', calls, events,
460
498
  error: !owned && !signal.aborted && !invalidInput && localDiagnostic(error) || { code,
461
- ...(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.' },
499
+ ...(invalidInput ? { stage: 'input', problems: error.problems.slice(0, 8).map(problem => problem.slice(0, 500)) } : observedStage ? { stage: observedStage } : {}),
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.'
501
+ : toolFailed ? 'A registered tool call failed. Inspect the tool step and reconcile any effect before a new run. Underlying error text is omitted.'
502
+ : 'Workflow failed. Inspect the affected step, input contracts and host adapters. Underlying error text is omitted.' },
462
503
  ...(unknown.length ? { uncertainEffects: unknown } : {}), ...(traceTruncated ? { traceTruncated: true } : {}) };
463
504
  }
464
505
  finally {
@@ -34,6 +34,8 @@ export interface AgentRunExtensionOptions {
34
34
  onWorkflowEvent?: (frame: AgentRunWorkflowEvent) => unknown;
35
35
  /** Explicit null disables an execution limit; omitted fields retain finite defaults. */
36
36
  runtimeLimits?: Partial<AgentRunRuntimeLimits>;
37
+ /** The host's SOP text for workflows that name `sopSection`s. It outranks a file the operator supplies with /agentrun sop. */
38
+ sop?: string;
37
39
  }
38
40
  /** Configure the native extension without granting configuration to workflow authors. */
39
41
  export declare function createAgentRunExtension(options?: AgentRunExtensionOptions): (pi: ExtensionAPI) => void;