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