@parcha/agentrun-pi 0.1.0-beta.2 → 0.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -32
- package/dist/cli.js +15 -4
- package/dist/demo-cli.d.ts +2 -0
- package/dist/demo-cli.js +25 -0
- package/dist/demo.js +1 -0
- package/dist/durable/decision-recovery.d.ts +23 -0
- package/dist/durable/decision-recovery.js +57 -0
- package/dist/durable/decisions.d.ts +113 -0
- package/dist/durable/decisions.js +71 -0
- package/dist/durable/index.d.ts +20 -0
- package/dist/durable/index.js +14 -0
- package/dist/durable/node-runner.d.ts +106 -0
- package/dist/durable/node-runner.js +253 -0
- package/dist/durable/node.d.ts +88 -0
- package/dist/durable/node.js +98 -0
- package/dist/durable/pi-versions.d.ts +34 -0
- package/dist/durable/pi-versions.js +66 -0
- package/dist/durable/readers.d.ts +28 -0
- package/dist/durable/readers.js +103 -0
- package/dist/durable/receipts.d.ts +54 -0
- package/dist/durable/receipts.js +92 -0
- package/dist/durable/record-file.d.ts +10 -0
- package/dist/durable/record-file.js +56 -0
- package/dist/durable/record-tool.d.ts +76 -0
- package/dist/durable/record-tool.js +123 -0
- package/dist/durable/record.d.ts +157 -0
- package/dist/durable/record.js +402 -0
- package/dist/durable/recording.d.ts +90 -0
- package/dist/durable/recording.js +292 -0
- package/dist/durable/store.d.ts +23 -0
- package/dist/durable/store.js +242 -0
- package/dist/durable/task.d.ts +133 -0
- package/dist/durable/task.js +411 -0
- package/dist/durable/workflow-tool.d.ts +32 -0
- package/dist/durable/workflow-tool.js +188 -0
- package/dist/extension-service.js +63 -22
- package/dist/extension.d.ts +2 -0
- package/dist/extension.js +463 -80
- package/dist/host-addendum.d.ts +4 -0
- package/dist/host-addendum.js +20 -0
- package/dist/host-session.js +5 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/pi-host.d.ts +7 -0
- package/dist/pi-host.js +22 -0
- package/dist/presentation.js +2 -2
- package/dist/run-error.d.ts +6 -0
- package/dist/run-error.js +13 -0
- package/dist/runner.d.ts +1 -6
- package/dist/runner.js +7 -13
- package/dist/signin/index.d.ts +9 -0
- package/dist/signin/index.js +7 -0
- package/dist/signin/limit.d.ts +10 -0
- package/dist/signin/limit.js +23 -0
- package/dist/signin/resolve.d.ts +47 -0
- package/dist/signin/resolve.js +163 -0
- package/dist/signin/signin.d.ts +44 -0
- package/dist/signin/signin.js +126 -0
- package/dist/signin/spec.d.ts +116 -0
- package/dist/signin/spec.js +181 -0
- package/dist/signin/store.d.ts +40 -0
- package/dist/signin/store.js +206 -0
- package/dist/skills/author/SKILL.md +138 -0
- package/{skills → dist/skills}/author/references/jev-decisions.md +27 -14
- package/dist/skills/author/references/language.md +134 -0
- package/dist/skills/author/references/sdk-authoring.md +11 -0
- package/{skills → dist/skills}/author/references/workflow-format.md +7 -15
- package/dist/sop.d.ts +12 -0
- package/dist/sop.js +29 -0
- package/dist/thinking.d.ts +3 -0
- package/dist/thinking.js +7 -0
- package/dist/tool-context.d.ts +21 -0
- package/dist/tool-context.js +13 -0
- package/dist/triage-demo.d.ts +62 -0
- package/dist/triage-demo.js +153 -0
- package/dist/types.d.ts +7 -0
- package/dist/workflow-inspector.d.ts +47 -0
- package/dist/workflow-inspector.js +235 -0
- package/dist/workflow-session.d.ts +23 -0
- package/dist/workflow-session.js +168 -0
- package/dist/workflow-store.d.ts +36 -0
- package/dist/workflow-store.js +255 -0
- package/dist/workflow-tool.d.ts +81 -0
- package/dist/workflow-tool.js +94 -0
- package/dist/workflow-view.d.ts +64 -0
- package/dist/workflow-view.js +273 -0
- package/dist/workflows.d.ts +1 -0
- package/dist/workflows.js +3 -0
- package/package.json +57 -11
- package/dist/admission.d.ts +0 -6
- package/dist/admission.js +0 -51
- package/dist/author.d.ts +0 -21
- package/dist/author.js +0 -72
- package/dist/benchmark-contract.d.ts +0 -88
- package/dist/benchmark-contract.js +0 -50
- package/dist/benchmark-freeze.d.ts +0 -9
- package/dist/benchmark-freeze.js +0 -9
- package/dist/benchmark.d.ts +0 -127
- package/dist/benchmark.js +0 -260
- package/dist/comparison.d.ts +0 -69
- package/dist/comparison.js +0 -229
- package/dist/skill-bundle.d.ts +0 -5
- package/dist/skill-bundle.js +0 -15
- package/skills/author/SKILL.md +0 -102
- package/skills/author/references/sdk-authoring.md +0 -16
- /package/{skills → dist/skills}/author/examples/evidence-gate.input.json +0 -0
- /package/{skills → dist/skills}/author/examples/evidence-gate.json +0 -0
- /package/{skills → dist/skills}/author/examples/read-file.input.json +0 -0
- /package/{skills → dist/skills}/author/examples/read-file.json +0 -0
- /package/{skills → dist/skills}/author/examples/read-source-decision.input.json +0 -0
- /package/{skills → dist/skills}/author/examples/read-source-decision.json +0 -0
- /package/{skills → dist/skills}/author/examples/release-note.txt +0 -0
- /package/{skills → dist/skills}/author/examples/review-release-notes.input.json +0 -0
- /package/{skills → dist/skills}/author/examples/review-release-notes.json +0 -0
package/dist/extension.js
CHANGED
|
@@ -1,24 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { Compile } from 'typebox/compile';
|
|
3
4
|
import { Text } from '@earendil-works/pi-tui';
|
|
4
5
|
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';
|
|
6
|
+
import { formatWorkflowTree, inspectWorkflow, AUTHOR_SKILL_NAME } from '@parcha/agentrun-dsl';
|
|
6
7
|
import { createJevRunner } from '@parcha/agentrun-jev';
|
|
7
8
|
import { createPiHostRunner, PI_MODEL_SETUP_MESSAGE } from './host-session.js';
|
|
8
|
-
import {
|
|
9
|
+
import { piHostLine } from './pi-host.js';
|
|
10
|
+
import { workflowToolContext } from './tool-context.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 { mkdir, readFile } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { RecoveryError, fileStore, openRecovery, withRecovery } from '@parcha/agentrun-dsl/recovery';
|
|
18
|
+
import { readSopFile, missingSopSections, sopDigest } from './sop.js';
|
|
19
|
+
import { WorkflowStore, WorkflowStoreError } from './workflow-store.js';
|
|
20
|
+
import { WorkflowObservation, workflowView, formatWorkflowView, progressLines, readable } from './workflow-view.js';
|
|
21
|
+
import { showWorkflowInspector } from './workflow-inspector.js';
|
|
22
|
+
import { authoringReferences, describeTools, dryRunDraft, formatDryRun, inspectCandidate, workflowToolParameters } from './workflow-tool.js';
|
|
23
|
+
import { supportTriageWorkflow, supportTriageInputs, supportTriageTools, scriptedSupportTriageDeps } from './triage-demo.js';
|
|
24
|
+
import { restoreWorkflowSession, workflowRunHistory } from './workflow-session.js';
|
|
13
25
|
const constructors = {
|
|
14
26
|
read: createReadToolDefinition, bash: createBashToolDefinition, edit: createEditToolDefinition,
|
|
15
27
|
write: createWriteToolDefinition, grep: createGrepToolDefinition, find: createFindToolDefinition, ls: createLsToolDefinition,
|
|
16
28
|
};
|
|
17
29
|
const mutatingTools = new Set(['bash', 'edit', 'write']);
|
|
18
|
-
const help = 'Describe a task: /agentrun <what you want done>\nTry
|
|
30
|
+
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 · Dry run: /agentrun dry-run · SOP text: /agentrun sop <file> · Setup: /agentrun status\nStop: /agentrun stop (closing the inspector does not stop a run)\nOther examples: /agentrun demo | demo empty | demo live';
|
|
19
31
|
const textResult = (text, details = {}) => ({ content: [{ type: 'text', text: safe(text) }], details });
|
|
32
|
+
function singleTextInput(workflow) {
|
|
33
|
+
const source = workflow;
|
|
34
|
+
const schema = source?.input?.schemaId && source.schemas?.[source.input.schemaId];
|
|
35
|
+
if (schema?.type !== 'object' || !schema.properties || typeof schema.properties !== 'object')
|
|
36
|
+
return;
|
|
37
|
+
const keys = Object.keys(schema.properties);
|
|
38
|
+
return keys.length === 1 && schema.required?.length === 1 && schema.required[0] === keys[0]
|
|
39
|
+
&& schema.properties[keys[0]]?.type === 'string' ? keys[0] : undefined;
|
|
40
|
+
}
|
|
20
41
|
const nativeLimits = Object.freeze({ deadlineMs: 600_000, modelRequests: 50, judgeCalls: 100, toolAttempts: 100,
|
|
21
42
|
nodeMaxTurns: 8, nodeMaxSubmissions: 3, nodeTimeoutMs: 120_000 });
|
|
43
|
+
/** Forgets a session's last run (its observation, report and run id), so the next run starts fresh. */
|
|
44
|
+
const clearRun = (s) => { s.observation = undefined; s.report = undefined; s.runId = undefined; };
|
|
22
45
|
/** Configure the native extension without granting configuration to workflow authors. */
|
|
23
46
|
export function createAgentRunExtension(options = {}) {
|
|
24
47
|
if (options.hostTools !== undefined && typeof options.hostTools !== 'function')
|
|
@@ -29,13 +52,15 @@ export function createAgentRunExtension(options = {}) {
|
|
|
29
52
|
throw new Error('onToolAttempt must be a host callback');
|
|
30
53
|
if (options.onWorkflowEvent !== undefined && typeof options.onWorkflowEvent !== 'function')
|
|
31
54
|
throw new Error('onWorkflowEvent must be a host callback');
|
|
55
|
+
if (options.sop !== undefined && (typeof options.sop !== 'string' || !options.sop.trim()))
|
|
56
|
+
throw new Error('sop must be nonempty text');
|
|
32
57
|
const limits = { ...nativeLimits, ...options.runtimeLimits };
|
|
33
58
|
for (const [key, value] of Object.entries(limits)) {
|
|
34
59
|
if (!(key in nativeLimits) || value !== null && (!Number.isSafeInteger(value) || value < 1 || value > nativeLimits[key])) {
|
|
35
60
|
throw new Error('runtimeLimits must use known fields with positive bounded integers or explicit null');
|
|
36
61
|
}
|
|
37
62
|
}
|
|
38
|
-
const configured = { hostTools: options.hostTools, createJudge: options.createJudge, onToolAttempt: options.onToolAttempt, onWorkflowEvent: options.onWorkflowEvent,
|
|
63
|
+
const configured = { hostTools: options.hostTools, createJudge: options.createJudge, onToolAttempt: options.onToolAttempt, onWorkflowEvent: options.onWorkflowEvent, sop: options.sop,
|
|
39
64
|
runtimeLimits: Object.freeze(limits) };
|
|
40
65
|
return pi => registerExtension(pi, configured);
|
|
41
66
|
}
|
|
@@ -61,15 +86,17 @@ function registerExtension(pi, configuration) {
|
|
|
61
86
|
if (typeof tool.execute !== 'function' || !tool.parameters || typeof tool.parameters !== 'object')
|
|
62
87
|
throw new Error('Host tools require an executable definition and parameter schema');
|
|
63
88
|
Compile(tool.parameters);
|
|
89
|
+
if (tool.resultSchema)
|
|
90
|
+
Compile(tool.resultSchema);
|
|
64
91
|
names.add(tool.name);
|
|
65
|
-
return { ...tool, parameters: structuredClone(tool.parameters) };
|
|
92
|
+
return { ...tool, parameters: structuredClone(tool.parameters), ...(tool.resultSchema ? { resultSchema: structuredClone(tool.resultSchema) } : {}) };
|
|
66
93
|
});
|
|
67
94
|
}
|
|
68
95
|
const active = new Set(pi.getActiveTools());
|
|
69
96
|
const builtins = new Set(pi.getAllTools().filter(t => t.sourceInfo.source === 'builtin').map(t => t.name));
|
|
70
97
|
const definitions = Object.entries(constructors).filter(([name]) => active.has(name) && builtins.has(name) && (trusted || !mutatingTools.has(name)))
|
|
71
98
|
.map(([, factory]) => factory(ctx.cwd));
|
|
72
|
-
return [demoSearchTool(), ...definitions];
|
|
99
|
+
return [demoSearchTool(), ...supportTriageTools(), ...definitions];
|
|
73
100
|
};
|
|
74
101
|
const state = async (ctx) => {
|
|
75
102
|
const key = `${ctx.sessionManager.getSessionId()}:${ctx.cwd}`;
|
|
@@ -84,17 +111,50 @@ function registerExtension(pi, configuration) {
|
|
|
84
111
|
await old.inFlight;
|
|
85
112
|
}
|
|
86
113
|
const tools = toolSet(ctx);
|
|
87
|
-
current = { key, clearStatus: () => { if (ctx.hasUI) {
|
|
114
|
+
current = { key, sessionFile: () => ctx.sessionManager.getSessionFile(), clearStatus: () => { if (ctx.hasUI) {
|
|
88
115
|
ctx.ui.setStatus('agentrun', undefined);
|
|
89
116
|
ctx.ui.setWidget('agentrun', undefined);
|
|
90
117
|
} }, tools, toolNames: tools.map(t => t.name), input: {}, busy: false, closed: false,
|
|
91
118
|
service: new WorkflowExtensionService({ allowedTools: tools.map(t => t.name), limits: serviceLimits }) };
|
|
119
|
+
const restored = restoreWorkflowSession(ctx.sessionManager.getBranch());
|
|
120
|
+
if (restored) {
|
|
121
|
+
current.draft = restored.workflow;
|
|
122
|
+
current.input = restored.input;
|
|
123
|
+
current.demo = restored.demo;
|
|
124
|
+
current.savedName = restored.savedName;
|
|
125
|
+
current.report = restored.report;
|
|
126
|
+
current.observation = restored.observation;
|
|
127
|
+
current.runId = restored.runId;
|
|
128
|
+
current.sop = restored.sop;
|
|
129
|
+
}
|
|
92
130
|
return current;
|
|
93
131
|
};
|
|
132
|
+
const persist = (s, running = false, closing = false) => {
|
|
133
|
+
if (current !== s || s.closed && !closing || !s.draft)
|
|
134
|
+
return;
|
|
135
|
+
const record = { version: 1, workflow: s.draft,
|
|
136
|
+
input: s.input, running, createdAt: new Date().toISOString(),
|
|
137
|
+
...(s.savedName ? { savedName: s.savedName } : {}), ...(s.demo ? { demo: s.demo } : {}),
|
|
138
|
+
...(s.runId ? { runId: s.runId } : {}), ...(s.report ? { report: s.report } : {}),
|
|
139
|
+
...(s.observation ? { observation: s.observation } : {}), ...(s.sop ? { sop: s.sop } : {}), };
|
|
140
|
+
try {
|
|
141
|
+
const data = structuredClone(record);
|
|
142
|
+
// Never advertise history that this release cannot restore (including
|
|
143
|
+
// non-JSON or oversized receipts). Display/retention limits are not run limits.
|
|
144
|
+
if (!restoreWorkflowSession([{ type: 'custom', customType: 'agentrun:snapshot', data }]))
|
|
145
|
+
throw new Error('Unrestorable workflow receipt');
|
|
146
|
+
pi.appendEntry('agentrun:snapshot', data);
|
|
147
|
+
const file = s.sessionFile();
|
|
148
|
+
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.';
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
s.persistenceWarning = 'Run state could not be retained in the Pi session. Keep this session open; named workflow saving is separate.';
|
|
152
|
+
}
|
|
153
|
+
};
|
|
94
154
|
const readiness = (ctx, s) => ({
|
|
95
|
-
skill: pi.getCommands().some(item => item.name ===
|
|
155
|
+
skill: pi.getCommands().some(item => item.name === `skill:${AUTHOR_SKILL_NAME}` && item.source === 'skill'),
|
|
96
156
|
pi: !!ctx.model && ctx.modelRegistry.getAll().some(model => model.id === ctx.model?.id && model.provider === ctx.model?.provider && model.api === ctx.model?.api),
|
|
97
|
-
jev: !!configuration.createJudge || !!process.env.TYPESAFE_API_KEY?.trim(), sop:
|
|
157
|
+
jev: !!configuration.createJudge || !!process.env.TYPESAFE_API_KEY?.trim(), sop: !!configuration.sop || !!s.sop, running: s.busy, workflow: !!s.draft,
|
|
98
158
|
mode: s.draft ? s.demo ? 'scripted' : 'live' : undefined,
|
|
99
159
|
});
|
|
100
160
|
const show = (s, content, details = {}) => {
|
|
@@ -103,7 +163,7 @@ function registerExtension(pi, configuration) {
|
|
|
103
163
|
pi.sendMessage({ customType: 'agentrun', content: safe(content), display: true, details });
|
|
104
164
|
};
|
|
105
165
|
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))
|
|
166
|
+
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))
|
|
107
167
|
? 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
168
|
show(s, message);
|
|
109
169
|
};
|
|
@@ -118,30 +178,37 @@ function registerExtension(pi, configuration) {
|
|
|
118
178
|
const candidate = workflow === undefined ? s.draft : workflow;
|
|
119
179
|
if (candidate === undefined)
|
|
120
180
|
throw new Error('No workflow yet. Describe a task with /agentrun <task>.');
|
|
121
|
-
if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
122
|
-
throw new Error('Cannot inspect workflow: workflow must be a JSON object, not a JSON-encoded string or array. Pass the complete object directly.');
|
|
123
|
-
}
|
|
124
181
|
// Include active tools available only with operator trust, without granting that trust.
|
|
125
182
|
// Execution rechecks the actual per-run permissions and captures implementations.
|
|
126
183
|
const checker = new WorkflowExtensionService({ allowedTools: toolSet(ctx, true).map(tool => tool.name), limits: serviceLimits });
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
inspection = checker.preflight(candidate, input);
|
|
130
|
-
if (inspection.requires.sopSections.length)
|
|
131
|
-
throw new Error('Native Pi does not supply SOP text in V1; use a configured SDK host. Keep the required sections intact.');
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
throw new Error(`Cannot inspect workflow: ${error instanceof Error ? error.message : 'Invalid workflow'}`);
|
|
135
|
-
}
|
|
184
|
+
const { inspection, tree } = inspectCandidate(checker, candidate, input);
|
|
136
185
|
// Stage the definition and input together only after nonexecuting checks succeed.
|
|
137
186
|
const nextDraft = workflow === undefined ? s.draft : structuredClone(workflow);
|
|
138
187
|
const changed = !s.draft || inspectWorkflow(s.draft).sha256 !== inspection.sha256;
|
|
139
188
|
const nextInput = input === undefined ? changed ? {} : s.input : structuredClone(input);
|
|
140
189
|
s.draft = nextDraft;
|
|
141
|
-
if (changed)
|
|
190
|
+
if (changed) {
|
|
142
191
|
s.demo = undefined;
|
|
192
|
+
s.savedName = undefined;
|
|
193
|
+
clearRun(s);
|
|
194
|
+
}
|
|
195
|
+
if (input !== undefined)
|
|
196
|
+
clearRun(s);
|
|
143
197
|
s.input = nextInput;
|
|
144
|
-
|
|
198
|
+
if (workflow !== undefined || input !== undefined)
|
|
199
|
+
persist(s);
|
|
200
|
+
return { inspection, tree };
|
|
201
|
+
};
|
|
202
|
+
const view = (ctx, s) => {
|
|
203
|
+
const result = workflowView(s.draft, { input: s.input,
|
|
204
|
+
observation: s.observation, report: s.report, running: s.busy, savedName: s.savedName,
|
|
205
|
+
mode: s.demo ? 'scripted · fictional · no model calls' : 'live adapters',
|
|
206
|
+
model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : 'not selected', limits,
|
|
207
|
+
availableTools: toolSet(ctx).map(tool => tool.name),
|
|
208
|
+
});
|
|
209
|
+
if (s.persistenceWarning)
|
|
210
|
+
result.summary.push(s.persistenceWarning);
|
|
211
|
+
return result;
|
|
145
212
|
};
|
|
146
213
|
const depsFor = (ctx, s, signal) => {
|
|
147
214
|
let modelCalls = 0, toolCalls = 0;
|
|
@@ -212,17 +279,37 @@ function registerExtension(pi, configuration) {
|
|
|
212
279
|
throw new Error('Workflow requested an unavailable tool');
|
|
213
280
|
let result;
|
|
214
281
|
try {
|
|
215
|
-
result = await tool.execute('agentrun-effect', input, effectSignal, undefined, ctx);
|
|
282
|
+
result = await tool.execute('agentrun-effect', input, effectSignal, undefined, workflowToolContext(ctx));
|
|
216
283
|
}
|
|
217
284
|
catch (error) {
|
|
218
285
|
if (error instanceof ToolInputValidationError)
|
|
219
286
|
throw new ToolInputValidationError(node.label, tool.name, error.problems);
|
|
220
287
|
throw error;
|
|
221
288
|
}
|
|
222
|
-
return !configuration.hostTools && node.tool
|
|
289
|
+
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
290
|
},
|
|
224
291
|
};
|
|
225
292
|
};
|
|
293
|
+
/** The SOP text a run of a workflow naming `needed` sections uses: the host's, else the file the operator named. */
|
|
294
|
+
const sopFor = async (ctx, s, needed) => {
|
|
295
|
+
if (!needed.length)
|
|
296
|
+
return undefined;
|
|
297
|
+
let sop;
|
|
298
|
+
if (configuration.sop)
|
|
299
|
+
sop = { text: configuration.sop, sha256: sopDigest(configuration.sop) };
|
|
300
|
+
else if (s.sop) {
|
|
301
|
+
const read = await readSopFile(ctx.cwd, s.sop.file);
|
|
302
|
+
if (read.sha256 !== s.sop.sha256)
|
|
303
|
+
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.`);
|
|
304
|
+
sop = read;
|
|
305
|
+
}
|
|
306
|
+
else
|
|
307
|
+
throw new Error(`Workflow requires SOP text for ${needed.map(name => `"${name}"`).join(', ')}. Supply it with /agentrun sop <file>.`);
|
|
308
|
+
const missing = missingSopSections(sop.text, needed);
|
|
309
|
+
if (missing.length)
|
|
310
|
+
throw new Error(`SOP text has no "## <section>" heading for ${missing.map(name => `"${name}"`).join(', ')}.`);
|
|
311
|
+
return sop;
|
|
312
|
+
};
|
|
226
313
|
const execute = async (ctx, s, input, options = {}) => {
|
|
227
314
|
if (s.busy)
|
|
228
315
|
throw new Error('A workflow is already running. Use /agentrun stop first.');
|
|
@@ -233,9 +320,9 @@ function registerExtension(pi, configuration) {
|
|
|
233
320
|
s.controller = new AbortController();
|
|
234
321
|
const signals = [s.controller.signal, options.signal, ctx.signal].filter((v) => !!v);
|
|
235
322
|
const signal = AbortSignal.any(signals);
|
|
323
|
+
let driver;
|
|
236
324
|
try {
|
|
237
|
-
|
|
238
|
-
throw new Error('Workflow requires SOP text. Native Pi does not supply SOP text in V1; use a configured SDK host. Keep the required sections intact.');
|
|
325
|
+
const sop = await sopFor(ctx, s, inspectWorkflow(s.draft).requires.sopSections);
|
|
239
326
|
signal.throwIfAborted();
|
|
240
327
|
const available = toolSet(ctx, options.trusted);
|
|
241
328
|
if (available.map(t => t.name).join(',') !== s.toolNames.join(',')) {
|
|
@@ -247,55 +334,130 @@ function registerExtension(pi, configuration) {
|
|
|
247
334
|
}
|
|
248
335
|
// Resolve implementations from the execution context even if names are unchanged.
|
|
249
336
|
s.tools = available;
|
|
337
|
+
// Validate the actual input before asking for model credentials or executing code probes.
|
|
338
|
+
s.service.preflight(s.draft, input);
|
|
250
339
|
const prepared = s.service.prepare(s.draft, { allowExecutableCandidates: options.trusted ?? false });
|
|
340
|
+
// Adapter admission is not a run. Failed setup must not leave a running receipt.
|
|
341
|
+
const deps = scripted
|
|
342
|
+
? s.demo === 'triage' || s.demo === 'triage-failure' ? scriptedSupportTriageDeps({ failLookup: s.demo === 'triage-failure' }) : scriptedDemoDeps(s.demo === 'empty')
|
|
343
|
+
: depsFor(ctx, s, signal);
|
|
251
344
|
s.input = structuredClone(input);
|
|
252
345
|
const tree = formatWorkflowTree(inspectWorkflow(s.draft));
|
|
253
346
|
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
347
|
options.update?.(textResult(`${label}\n\n${tree}`));
|
|
255
348
|
stage(ctx, s, 'agent.run() · starting');
|
|
256
|
-
|
|
257
|
-
|
|
349
|
+
const observation = new WorkflowObservation();
|
|
350
|
+
// A run this session left interrupted or failed, over the same draft and input, continues from its journal;
|
|
351
|
+
// anything else starts a new run.
|
|
352
|
+
const runRoot = join(ctx.cwd, '.pi', 'agentrun', 'runs');
|
|
353
|
+
const resumable = !scripted && s.runId !== undefined && ['interrupted', 'failed'].includes(s.report?.status ?? '') && existsSync(join(runRoot, s.runId));
|
|
354
|
+
let runId = resumable ? s.runId : randomUUID();
|
|
355
|
+
if (!scripted) {
|
|
356
|
+
await mkdir(runRoot, { recursive: true });
|
|
357
|
+
const bind = { input: s.input, tools: [...s.toolNames].sort(), ...(sop ? { sop: sop.sha256 } : {}) };
|
|
358
|
+
const open = (id) => openRecovery(fileStore(join(runRoot, id)), s.draft, { key: id, view: prepared.workflow, bind });
|
|
359
|
+
try {
|
|
360
|
+
try {
|
|
361
|
+
driver = await open(runId);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
// A run bound to other input, SOP text or tools is a different run: it starts anew, and the old journal stays.
|
|
365
|
+
if (!resumable || !(error instanceof RecoveryError) || error.code !== 'RUN_STORE_BINDING_MISMATCH')
|
|
366
|
+
throw error;
|
|
367
|
+
runId = randomUUID();
|
|
368
|
+
driver = await open(runId);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
throw new Error(`Cannot ${resumable ? 'resume' : 'start'} run ${runId}: ${error instanceof Error ? error.message : 'recovery store refused'}`);
|
|
373
|
+
}
|
|
374
|
+
s.pause = () => driver.stop({ action: 'pause', source: 'operator' });
|
|
375
|
+
}
|
|
376
|
+
s.observation = observation.data;
|
|
377
|
+
s.report = undefined;
|
|
378
|
+
s.runId = runId;
|
|
379
|
+
persist(s, true);
|
|
258
380
|
const ordinal = ++runOrdinal;
|
|
259
381
|
let sequence = 0;
|
|
382
|
+
const runDeps = sop ? { ...deps, sop: sop.text } : deps;
|
|
260
383
|
const report = await s.service.run(s.input, {
|
|
261
|
-
deps:
|
|
384
|
+
deps: observation.observe(driver ? withRecovery(driver, runDeps) : runDeps),
|
|
385
|
+
signal: driver ? AbortSignal.any([signal, driver.signal]) : signal,
|
|
262
386
|
...(configuration.onWorkflowEvent ? { onTraceEvent: (event) => configuration.onWorkflowEvent({ workflowDigest: prepared.digest, runOrdinal: ordinal, sequence: ++sequence, event }) } : {}),
|
|
263
387
|
onEvent: event => {
|
|
264
|
-
|
|
388
|
+
observation.record(event);
|
|
389
|
+
if (current !== s || s.closed)
|
|
265
390
|
return;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
}
|
|
391
|
+
const progress = progressLines(observation.data);
|
|
392
|
+
if (ctx.mode === 'tui')
|
|
393
|
+
ctx.ui.setWidget('agentrun', [safe(prepared.inspection.name), ...progress.map(safe), '/agentrun inspect · /agentrun stop']);
|
|
276
394
|
else
|
|
277
|
-
|
|
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}`));
|
|
395
|
+
options.update?.(textResult(`${label}\n\n${progress.join('\n')}`));
|
|
284
396
|
},
|
|
285
397
|
});
|
|
398
|
+
if (driver && report.error?.code === 'FROZEN_EFFECT_UNKNOWN') {
|
|
399
|
+
// The journal names what the driver refused to repeat. Read it once the driver has let go of it.
|
|
400
|
+
await driver.close().catch(() => { });
|
|
401
|
+
const journal = await readFile(join(runRoot, runId, 'journal.json'), 'utf8').then(text => JSON.parse(text), () => undefined);
|
|
402
|
+
const unknown = (journal?.effects ?? []).filter(effect => effect.status === 'unknown').slice(0, 100);
|
|
403
|
+
if (unknown.length)
|
|
404
|
+
report.uncertainEffects = unknown.map(effect => ({ idempotencyKey: effect.id, outcome: 'unknown' }));
|
|
405
|
+
}
|
|
406
|
+
s.report = report;
|
|
407
|
+
persist(s);
|
|
286
408
|
const summary = formatRunReport(report, scripted);
|
|
287
409
|
const { events: _events, ...modelReport } = report;
|
|
288
410
|
const mode = scripted ? 'scripted' : 'live';
|
|
289
|
-
return textResult(options.modelResult ? modelJson({ mode, ...modelReport }) : summary, { mode, ...report
|
|
411
|
+
return textResult(options.modelResult ? modelJson({ mode, ...modelReport }) : summary + (s.persistenceWarning ? `\n${s.persistenceWarning}` : ''), { mode, ...report,
|
|
412
|
+
// Failure diagnostics must not echo rejected raw arguments. The operator
|
|
413
|
+
// can inspect the retained draft explicitly; the error receipt stays sanitized.
|
|
414
|
+
...(report.error ? {} : { view: workflowView(s.draft, { input: s.input, observation: s.observation, report, mode }) }) });
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
if (error instanceof ExtensionServiceError && error.code === 'admission' && error.message.includes('code requires allowExecutableCandidates')) {
|
|
418
|
+
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.'));
|
|
419
|
+
}
|
|
420
|
+
throw error;
|
|
290
421
|
}
|
|
291
422
|
finally {
|
|
292
423
|
s.busy = false;
|
|
293
424
|
s.controller = undefined;
|
|
425
|
+
s.pause = undefined;
|
|
426
|
+
await driver?.close().catch(() => { });
|
|
294
427
|
stage(ctx, s);
|
|
295
428
|
if (current === s && !s.closed && ctx.hasUI)
|
|
296
429
|
ctx.ui.setWidget('agentrun', undefined);
|
|
297
430
|
}
|
|
298
431
|
};
|
|
432
|
+
/** The inspected draft on synthetic values, or `input`: nothing is paid, and the session's run is untouched. Code runs
|
|
433
|
+
* only when the user's command trusts this dry run. */
|
|
434
|
+
const dryRun = async (ctx, s, input, trusted, signal) => {
|
|
435
|
+
if (!s.draft)
|
|
436
|
+
throw new Error('Inspect a workflow first. Try /agentrun demo, or describe a task with /agentrun <task>.');
|
|
437
|
+
const draft = s.draft;
|
|
438
|
+
// /agentrun stop, the tool call's own abort and the session's end each stop it, from before its first await on.
|
|
439
|
+
const controller = new AbortController();
|
|
440
|
+
s.dryRun?.abort();
|
|
441
|
+
s.dryRun = controller;
|
|
442
|
+
const stopped = AbortSignal.any([controller.signal, ...[signal, ctx.signal].filter((v) => !!v)]);
|
|
443
|
+
try {
|
|
444
|
+
let sop, sopUnavailable;
|
|
445
|
+
try {
|
|
446
|
+
sop = await sopFor(ctx, s, inspectWorkflow(draft).requires.sopSections);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
sopUnavailable = error instanceof Error ? error.message : String(error);
|
|
450
|
+
}
|
|
451
|
+
if (stopped.aborted || s.closed)
|
|
452
|
+
return { status: 'not_run', ok: true, skipped: ['The dry run was stopped before it started.'] };
|
|
453
|
+
return await dryRunDraft(draft, { ...(input === undefined ? {} : { input }), ...(sop ? { sop: sop.text } : {}), ...(sopUnavailable ? { sopUnavailable } : {}),
|
|
454
|
+
trusted, trustedCommand: '/agentrun dry-run --trusted', signal: stopped });
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
if (s.dryRun === controller)
|
|
458
|
+
s.dryRun = undefined;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
299
461
|
const launch = async (ctx, s, input, trusted = false) => {
|
|
300
462
|
if (s.busy)
|
|
301
463
|
throw new Error('A workflow is already running. Use /agentrun stop first.');
|
|
@@ -314,33 +476,67 @@ function registerExtension(pi, configuration) {
|
|
|
314
476
|
};
|
|
315
477
|
pi.registerTool({
|
|
316
478
|
name: 'agentrun', label: 'AgentRun workflow',
|
|
317
|
-
description:
|
|
479
|
+
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. Dry-run checks the inspected workflow on synthetic values (or the given input) with no model, Jev or tool call; code runs only with the user command /agentrun dry-run --trusted. Agent nodes capture the active Pi model when execution starts. Code needs the user command /agentrun run --trusted for each run. ' + (configuration.hostTools
|
|
318
480
|
? '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
|
|
320
|
-
parameters:
|
|
321
|
-
workflow: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
322
|
-
description: 'The complete AgentRun workflow JSON object. Pass the object directly, not a JSON-encoded string. Inspection validates its DSL structure.',
|
|
323
|
-
})), input: Type.Optional(Type.Record(Type.String(), Type.Unknown())) }),
|
|
481
|
+
: '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.',
|
|
482
|
+
parameters: workflowToolParameters(),
|
|
324
483
|
executionMode: 'sequential',
|
|
325
484
|
renderResult(result, options) {
|
|
326
485
|
const details = result.details;
|
|
486
|
+
if (details?.dryRun)
|
|
487
|
+
return new Text(safe(formatDryRun(details.dryRun)), 0, 0);
|
|
327
488
|
const text = details?.status && details.calls ? options?.expanded
|
|
328
|
-
?
|
|
329
|
-
: result.content.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
489
|
+
? 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')
|
|
490
|
+
: details?.view ? formatWorkflowView(details.view) : result.content.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
330
491
|
return new Text(safe(text), 0, 0);
|
|
331
492
|
},
|
|
332
493
|
async execute(_id, args, signal, onUpdate, ctx) {
|
|
333
494
|
const s = await state(ctx);
|
|
495
|
+
const store = new WorkflowStore(ctx.cwd);
|
|
496
|
+
if (args.action === 'save') {
|
|
497
|
+
if (s.busy)
|
|
498
|
+
throw new Error('A workflow is running. Save after it stops.');
|
|
499
|
+
if (!s.draft)
|
|
500
|
+
throw new Error('No workflow to save. Inspect a workflow first.');
|
|
501
|
+
const saved = await store.save(args.name ?? '', s.draft);
|
|
502
|
+
s.savedName = saved.name;
|
|
503
|
+
persist(s);
|
|
504
|
+
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);
|
|
505
|
+
}
|
|
506
|
+
if (args.action === 'list') {
|
|
507
|
+
const workflows = await store.list();
|
|
508
|
+
return textResult(modelJson(workflows), { workflows });
|
|
509
|
+
}
|
|
510
|
+
if (args.action === 'load') {
|
|
511
|
+
const saved = await store.load(args.name ?? '', args.digest);
|
|
512
|
+
const result = inspect(ctx, s, saved.workflow, args.input);
|
|
513
|
+
s.input = structuredClone(args.input ?? {});
|
|
514
|
+
s.savedName = saved.name;
|
|
515
|
+
s.demo = undefined;
|
|
516
|
+
clearRun(s);
|
|
517
|
+
persist(s);
|
|
518
|
+
const preview = view(ctx, s);
|
|
519
|
+
return textResult(modelJson({ preview: formatWorkflowView(preview), workflow: s.draft, input: s.input }), { ...result, saved, view: preview });
|
|
520
|
+
}
|
|
334
521
|
if (args.action === 'describe') {
|
|
335
|
-
const details = { tools: toolSet(ctx)
|
|
336
|
-
limits: { ...limits }, structuralLimits: { ...extensionStructuralLimits }, ...readiness(ctx, s), authoring:
|
|
522
|
+
const details = { tools: describeTools(toolSet(ctx)),
|
|
523
|
+
limits: { ...limits }, structuralLimits: { ...extensionStructuralLimits }, ...readiness(ctx, s), authoring: authoringReferences() };
|
|
337
524
|
return textResult(JSON.stringify(details, null, 2), details);
|
|
338
525
|
}
|
|
526
|
+
if (args.action === 'dry-run') {
|
|
527
|
+
if (args.workflow !== undefined)
|
|
528
|
+
throw new Error('Inspect the workflow first with action inspect, then dry-run without a workflow argument.');
|
|
529
|
+
const report = await dryRun(ctx, s, args.input, false, signal);
|
|
530
|
+
return textResult(modelJson({ dryRun: report }), { dryRun: report });
|
|
531
|
+
}
|
|
339
532
|
if (args.action === 'run' && args.workflow !== undefined)
|
|
340
533
|
throw new Error('Inspect the workflow first with action inspect, then run without a workflow argument.');
|
|
341
534
|
if (args.action === 'inspect') {
|
|
342
535
|
const result = inspect(ctx, s, args.workflow, args.input);
|
|
343
|
-
|
|
536
|
+
const preview = view(ctx, s);
|
|
537
|
+
// The author needs the exact definition when editing a restored procedure.
|
|
538
|
+
// Pi's renderer shows the readable view, not this model-facing JSON.
|
|
539
|
+
return textResult(modelJson({ preview: formatWorkflowView(preview), workflow: s.draft, input: s.input }), { ...result, view: preview, tools: toolSet(ctx).map(t => t.name) });
|
|
344
540
|
}
|
|
345
541
|
return execute(ctx, s, args.input ?? s.input, { signal, update: onUpdate, modelResult: true });
|
|
346
542
|
},
|
|
@@ -349,26 +545,182 @@ function registerExtension(pi, configuration) {
|
|
|
349
545
|
description: 'Describe a task, inspect its workflow, or try /agentrun demo',
|
|
350
546
|
async handler(args, ctx) {
|
|
351
547
|
const s = await state(ctx);
|
|
352
|
-
|
|
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));
|
|
548
|
+
let raw = args.trim();
|
|
357
549
|
try {
|
|
550
|
+
if ((!raw || raw === 'inspect') && s.draft && ctx.mode === 'tui') {
|
|
551
|
+
const action = await showWorkflowInspector(ctx, () => view(ctx, s));
|
|
552
|
+
if (!action)
|
|
553
|
+
return;
|
|
554
|
+
if (action === 'save') {
|
|
555
|
+
const name = await ctx.ui.input('Save procedure', s.savedName ?? 'support-triage');
|
|
556
|
+
if (!name)
|
|
557
|
+
return;
|
|
558
|
+
raw = `save ${name}`;
|
|
559
|
+
}
|
|
560
|
+
else if (action === 'load') {
|
|
561
|
+
const stored = await new WorkflowStore(ctx.cwd).list();
|
|
562
|
+
const names = [...new Set(stored.map(item => item.name))];
|
|
563
|
+
if (!names.length) {
|
|
564
|
+
show(s, 'No saved procedures yet. Use /agentrun save <name>.');
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const name = await ctx.ui.select('Load procedure', names);
|
|
568
|
+
if (!name)
|
|
569
|
+
return;
|
|
570
|
+
raw = `load ${name}`;
|
|
571
|
+
}
|
|
572
|
+
else if (action === 'edit') {
|
|
573
|
+
const request = await ctx.ui.input('Edit procedure', 'Describe the change; Pi will preview a candidate before running it.');
|
|
574
|
+
if (!request)
|
|
575
|
+
return;
|
|
576
|
+
pi.sendUserMessage(`/skill:agentrun-author Edit the current procedure: ${request}. Inspect the candidate and explain the change. Do not run it yet.`, { expandPromptTemplates: true });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
else
|
|
580
|
+
raw = action;
|
|
581
|
+
}
|
|
582
|
+
const command = /^(help|status|stop|demo|triage|run|dry-run|inspect)(?:\s|$)/i.test(raw) ? raw.toLowerCase().replace(/\s+/g, ' ') : raw;
|
|
583
|
+
const words = command.split(/\s+/);
|
|
584
|
+
const commandLike = /^(help|status|stop|demo|run|dry-run)(?:-|$)/i.test(words[0])
|
|
585
|
+
&& words.slice(1).every(word => word.startsWith('-') || ['live', 'empty'].includes(word));
|
|
358
586
|
if (command === 'help' || command === '--help' || command === '-h') {
|
|
359
587
|
show(s, help);
|
|
360
588
|
}
|
|
361
|
-
else if (!command) {
|
|
362
|
-
show(s, s.draft ?
|
|
589
|
+
else if (!command || command === 'inspect') {
|
|
590
|
+
show(s, s.draft ? formatWorkflowView(view(ctx, s)) : help);
|
|
591
|
+
}
|
|
592
|
+
else if (/^save(?:\s|$)/.test(command)) {
|
|
593
|
+
if (s.busy)
|
|
594
|
+
throw new Error('A workflow is running. Save after it stops.');
|
|
595
|
+
if (!s.draft)
|
|
596
|
+
throw new Error('No workflow to save. Inspect a workflow first.');
|
|
597
|
+
const saved = await new WorkflowStore(ctx.cwd).save(command.slice(4).trim(), s.draft);
|
|
598
|
+
s.savedName = saved.name;
|
|
599
|
+
persist(s);
|
|
600
|
+
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);
|
|
601
|
+
}
|
|
602
|
+
else if (/^load(?:\s|$)/.test(command)) {
|
|
603
|
+
const [, name, digest] = command.split(/\s+/);
|
|
604
|
+
const saved = await new WorkflowStore(ctx.cwd).load(name ?? '', digest);
|
|
605
|
+
inspect(ctx, s, saved.workflow);
|
|
606
|
+
s.savedName = saved.name;
|
|
607
|
+
s.input = {};
|
|
608
|
+
s.demo = undefined;
|
|
609
|
+
clearRun(s);
|
|
610
|
+
persist(s);
|
|
611
|
+
show(s, `Loaded procedure. Supply new input with /agentrun input. Loading never executes it.\n\n${formatWorkflowView(view(ctx, s))}`);
|
|
612
|
+
}
|
|
613
|
+
else if (command === 'list') {
|
|
614
|
+
const stored = await new WorkflowStore(ctx.cwd).list();
|
|
615
|
+
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 });
|
|
616
|
+
}
|
|
617
|
+
else if (/^history(?:\s|$)/.test(command)) {
|
|
618
|
+
const history = workflowRunHistory(ctx.sessionManager.getBranch());
|
|
619
|
+
if (!history.length) {
|
|
620
|
+
show(s, 'No retained runs on this Pi session branch yet.');
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const labels = history.map((run, index) => `${index + 1}. ${inspectWorkflow(run.workflow).name} · ${run.report?.status ?? 'interrupted'} · ${run.createdAt}`);
|
|
624
|
+
let index = Number(command.slice(7).trim()) - 1;
|
|
625
|
+
if (command === 'history' && ctx.mode === 'tui') {
|
|
626
|
+
const selected = await ctx.ui.select('Retained runs on this session branch', labels);
|
|
627
|
+
if (!selected)
|
|
628
|
+
return;
|
|
629
|
+
index = labels.indexOf(selected);
|
|
630
|
+
}
|
|
631
|
+
if (!Number.isInteger(index) || index < 0 || index >= history.length) {
|
|
632
|
+
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 })) });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const run = history[index];
|
|
636
|
+
const receipt = workflowView(run.workflow, { input: run.input, observation: run.observation, report: run.report, mode: run.demo ? 'scripted' : 'live adapters', savedName: run.savedName });
|
|
637
|
+
// History is read-only. It never replaces the active draft or replays a run.
|
|
638
|
+
if (ctx.mode === 'tui')
|
|
639
|
+
await showWorkflowInspector(ctx, () => ({ ...receipt, readOnly: true }));
|
|
640
|
+
else
|
|
641
|
+
show(s, formatWorkflowView(receipt), { run });
|
|
642
|
+
}
|
|
643
|
+
else if (/^sop(?:\s|$)/.test(command)) {
|
|
644
|
+
const file = raw.trim().slice(3).trim();
|
|
645
|
+
if (!file) {
|
|
646
|
+
show(s, configuration.sop ? 'SOP text comes from the host.' : s.sop ? `SOP: ${s.sop.file} (${s.sop.sha256.slice(0, 12)}). Supply another with /agentrun sop <file>.` : 'No SOP supplied. Use /agentrun sop <file> for a workflow that names sopSection.');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (s.busy)
|
|
650
|
+
throw new Error('A workflow is running. Stop it before changing its SOP.');
|
|
651
|
+
if (configuration.sop)
|
|
652
|
+
throw new Error('SOP text comes from the host; /agentrun sop is not used.');
|
|
653
|
+
const read = await readSopFile(ctx.cwd, file);
|
|
654
|
+
const needed = s.draft ? inspectWorkflow(s.draft).requires.sopSections : [];
|
|
655
|
+
const missing = missingSopSections(read.text, needed);
|
|
656
|
+
if (missing.length)
|
|
657
|
+
throw new Error(`SOP text has no "## <section>" heading for ${missing.map(name => `"${name}"`).join(', ')}. Nothing was changed.`);
|
|
658
|
+
s.sop = { file: read.file, sha256: read.sha256 };
|
|
659
|
+
persist(s);
|
|
660
|
+
show(s, `SOP set: ${read.file} (${read.text.length} characters). ${needed.length ? `Covers ${needed.map(name => `"${name}"`).join(', ')}.` : 'The current workflow names no section.'}`);
|
|
661
|
+
}
|
|
662
|
+
else if (/^input(?:\s|$)/.test(command)) {
|
|
663
|
+
if (!s.draft)
|
|
664
|
+
throw new Error('No workflow yet. Load or describe a procedure first.');
|
|
665
|
+
if (s.busy)
|
|
666
|
+
throw new Error('A workflow is running. Stop it before changing its input.');
|
|
667
|
+
let supplied = raw.slice(5).trim();
|
|
668
|
+
if (!supplied && ctx.hasUI)
|
|
669
|
+
supplied = await ctx.ui.input('New workflow input', 'Describe the new case in plain language, or paste a JSON object.') ?? '';
|
|
670
|
+
if (!supplied) {
|
|
671
|
+
show(s, 'Use /agentrun input <describe the new case>, or ask Pi to use new input. Nothing was run.');
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (supplied.startsWith('{')) {
|
|
675
|
+
let input;
|
|
676
|
+
try {
|
|
677
|
+
input = JSON.parse(supplied);
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
throw new Error('Workflow input must be valid JSON, or describe it in plain language.');
|
|
681
|
+
}
|
|
682
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
683
|
+
throw new Error('Workflow input must be an object.');
|
|
684
|
+
inspect(ctx, s, undefined, input);
|
|
685
|
+
show(s, `Input updated. Nothing was run.\n\n${formatWorkflowView(view(ctx, s))}`);
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
const key = singleTextInput(s.draft);
|
|
689
|
+
if (key) {
|
|
690
|
+
inspect(ctx, s, undefined, { [key]: supplied });
|
|
691
|
+
show(s, `Input updated. Nothing was run.\n\n${formatWorkflowView(view(ctx, s))}`);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
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 });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
else if (/^triage(?:\s|$)/.test(command)) {
|
|
699
|
+
const parts = command.split(/\s+/).slice(1);
|
|
700
|
+
const live = parts[0] === 'live';
|
|
701
|
+
const name = parts[live ? 1 : 0] ?? 'billing';
|
|
702
|
+
if (!['billing', 'technical', 'ambiguous', 'failure'].includes(name) || parts.length > (live ? 2 : 1) || live && name === 'failure') {
|
|
703
|
+
show(s, 'Use /agentrun triage [billing|technical|ambiguous|failure], or /agentrun triage live [billing|technical|ambiguous].');
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const input = structuredClone(supportTriageInputs[name === 'failure' ? 'billing' : name]);
|
|
707
|
+
inspect(ctx, s, supportTriageWorkflow, input);
|
|
708
|
+
s.demo = live ? undefined : name === 'failure' ? 'triage-failure' : 'triage';
|
|
709
|
+
show(s, `Fictional support triage · ${live ? 'real model calls' : 'scripted, no model calls'}. No customer actions.\n${formatWorkflowView(view(ctx, s))}`);
|
|
710
|
+
const result = await execute(ctx, s, input);
|
|
711
|
+
show(s, result.content[0].text, result.details);
|
|
363
712
|
}
|
|
364
713
|
else if (command === 'status') {
|
|
365
714
|
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
|
|
715
|
+
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:\n${piHostLine(PI_VERSION)}\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
716
|
}
|
|
368
717
|
else if (command === 'stop') {
|
|
718
|
+
const dry = s.dryRun;
|
|
719
|
+
dry?.abort();
|
|
720
|
+
s.pause?.();
|
|
369
721
|
s.controller?.abort();
|
|
370
722
|
s.service.stop();
|
|
371
|
-
show(s, s.busy ? 'Stop requested. Already-started tool effects may still finish.' : 'Nothing is running.');
|
|
723
|
+
show(s, s.busy ? 'Stop requested. Already-started tool effects may still finish.' : dry ? 'Dry run stopped.' : 'Nothing is running.');
|
|
372
724
|
}
|
|
373
725
|
else if (command === 'demo' || command === 'demo live' || command === 'demo empty') {
|
|
374
726
|
inspect(ctx, s, demoWorkflow);
|
|
@@ -379,18 +731,28 @@ function registerExtension(pi, configuration) {
|
|
|
379
731
|
else if (command === 'run' || command === 'run --trusted') {
|
|
380
732
|
await launch(ctx, s, s.input, command === 'run --trusted');
|
|
381
733
|
}
|
|
734
|
+
else if (command === 'dry-run' || command === 'dry-run --trusted') {
|
|
735
|
+
// As with /agentrun run: interactive Pi reads the next command only once this handler returns, so the dry run
|
|
736
|
+
// goes on in the background, where /agentrun stop reaches it. Headless callers await its answer.
|
|
737
|
+
stage(ctx, s, 'agent.run() · dry run · /agentrun stop ends it');
|
|
738
|
+
const pending = dryRun(ctx, s, undefined, command === 'dry-run --trusted')
|
|
739
|
+
.then(report => show(s, formatDryRun(report), { dryRun: report }), error => commandError(s, error))
|
|
740
|
+
.finally(() => stage(ctx, s));
|
|
741
|
+
if (!ctx.hasUI)
|
|
742
|
+
await pending;
|
|
743
|
+
}
|
|
382
744
|
else if (commandLike || command.startsWith('-')) {
|
|
383
745
|
show(s, `Unknown AgentRun command: ${raw}\n\n${help}`);
|
|
384
746
|
}
|
|
385
747
|
else {
|
|
386
748
|
if (s.busy)
|
|
387
749
|
throw new Error('A workflow is running. Stop it before starting another.');
|
|
388
|
-
if (!pi.getCommands().some(item => item.name ===
|
|
750
|
+
if (!pi.getCommands().some(item => item.name === `skill:${AUTHOR_SKILL_NAME}` && item.source === 'skill')) {
|
|
389
751
|
throw new Error('AgentRun authoring skill is unavailable. Enable package skills, then /reload. Run /agentrun status to check discovery.');
|
|
390
752
|
}
|
|
391
753
|
if (!readiness(ctx, s).pi)
|
|
392
754
|
throw new Error(PI_MODEL_SETUP_MESSAGE);
|
|
393
|
-
pi.sendUserMessage(`/skill
|
|
755
|
+
pi.sendUserMessage(`/skill:${AUTHOR_SKILL_NAME} ${raw}`, { expandPromptTemplates: true });
|
|
394
756
|
}
|
|
395
757
|
}
|
|
396
758
|
catch (error) {
|
|
@@ -398,22 +760,43 @@ function registerExtension(pi, configuration) {
|
|
|
398
760
|
}
|
|
399
761
|
},
|
|
400
762
|
});
|
|
401
|
-
const closeSession = async () => {
|
|
763
|
+
const closeSession = async (retainFinal = false) => {
|
|
402
764
|
if (!current)
|
|
403
765
|
return;
|
|
404
|
-
const old = current;
|
|
766
|
+
const old = current, wasRunning = old.busy;
|
|
405
767
|
old.clearStatus();
|
|
406
|
-
current = undefined;
|
|
407
768
|
old.closed = true;
|
|
769
|
+
old.dryRun?.abort();
|
|
770
|
+
old.pause?.();
|
|
408
771
|
old.controller?.abort();
|
|
409
772
|
await old.service.dispose();
|
|
410
773
|
await old.inFlight;
|
|
774
|
+
// Only before-switch/shutdown hooks still own the originating branch. A
|
|
775
|
+
// session_tree event is already on another branch and must never receive this receipt.
|
|
776
|
+
if (retainFinal && wasRunning && old.report)
|
|
777
|
+
persist(old, false, true);
|
|
778
|
+
if (current === old)
|
|
779
|
+
current = undefined;
|
|
411
780
|
};
|
|
412
|
-
pi.on('session_shutdown', closeSession);
|
|
413
|
-
pi.on('session_before_switch', closeSession);
|
|
414
|
-
pi.on('session_before_fork', closeSession);
|
|
781
|
+
pi.on('session_shutdown', () => closeSession(true));
|
|
782
|
+
pi.on('session_before_switch', () => closeSession(true));
|
|
783
|
+
pi.on('session_before_fork', () => closeSession(true));
|
|
415
784
|
pi.on('session_start', async (_event, ctx) => {
|
|
416
785
|
if (current && current.key !== `${ctx.sessionManager.getSessionId()}:${ctx.cwd}`)
|
|
417
786
|
await closeSession();
|
|
787
|
+
await state(ctx);
|
|
788
|
+
});
|
|
789
|
+
pi.on('session_tree', async (_event, ctx) => { await closeSession(); await state(ctx); });
|
|
790
|
+
pi.registerEntryRenderer('agentrun:snapshot', (entry, options) => {
|
|
791
|
+
if (entry.data?.running)
|
|
792
|
+
return undefined;
|
|
793
|
+
const snapshot = restoreWorkflowSession([entry]);
|
|
794
|
+
if (!snapshot)
|
|
795
|
+
return undefined;
|
|
796
|
+
if (!snapshot.report && !snapshot.running)
|
|
797
|
+
return undefined;
|
|
798
|
+
return new Text(safe(options.expanded ? formatWorkflowView(workflowView(snapshot.workflow, {
|
|
799
|
+
input: snapshot.input, report: snapshot.report, observation: snapshot.observation, mode: snapshot.demo ? 'scripted' : 'live adapters',
|
|
800
|
+
})) : `AgentRun receipt · ${snapshot.report?.status ?? 'interrupted'} · ${inspectWorkflow(snapshot.workflow).name}`), 0, 0);
|
|
418
801
|
});
|
|
419
802
|
}
|