@parcha/agentrun-pi 0.1.0-beta.1
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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +209 -0
- package/dist/admission.d.ts +6 -0
- package/dist/admission.js +51 -0
- package/dist/author.d.ts +21 -0
- package/dist/author.js +72 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +68 -0
- package/dist/demo-data.d.ts +361 -0
- package/dist/demo-data.js +386 -0
- package/dist/demo.d.ts +13 -0
- package/dist/demo.js +44 -0
- package/dist/extension-service.d.ts +110 -0
- package/dist/extension-service.js +477 -0
- package/dist/extension.d.ts +41 -0
- package/dist/extension.js +419 -0
- package/dist/host-session.d.ts +5 -0
- package/dist/host-session.js +80 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/presentation.d.ts +4 -0
- package/dist/presentation.js +34 -0
- package/dist/runner.d.ts +12 -0
- package/dist/runner.js +256 -0
- package/dist/skill-bundle.d.ts +5 -0
- package/dist/skill-bundle.js +15 -0
- package/dist/tool-input-error.d.ts +10 -0
- package/dist/tool-input-error.js +40 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.js +1 -0
- package/package.json +73 -0
- package/skills/author/SKILL.md +102 -0
- package/skills/author/examples/evidence-gate.input.json +6 -0
- package/skills/author/examples/evidence-gate.json +125 -0
- package/skills/author/examples/read-file.input.json +1 -0
- package/skills/author/examples/read-file.json +44 -0
- package/skills/author/examples/read-source-decision.input.json +4 -0
- package/skills/author/examples/read-source-decision.json +37 -0
- package/skills/author/examples/release-note.txt +10 -0
- package/skills/author/examples/review-release-notes.input.json +6 -0
- package/skills/author/examples/review-release-notes.json +123 -0
- package/skills/author/references/jev-decisions.md +112 -0
- package/skills/author/references/sdk-authoring.md +16 -0
- package/skills/author/references/workflow-format.md +145 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
|
+
import { Compile } from 'typebox/compile';
|
|
3
|
+
import { Text } from '@earendil-works/pi-tui';
|
|
4
|
+
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 { createJevRunner } from '@parcha/agentrun-jev';
|
|
7
|
+
import { createPiHostRunner, PI_MODEL_SETUP_MESSAGE } from './host-session.js';
|
|
8
|
+
import { WorkflowExtensionService, extensionStructuralLimits } from './extension-service.js';
|
|
9
|
+
import { demoInput, demoSearchTool, demoWorkflow, scriptedDemoDeps } from './demo.js';
|
|
10
|
+
import { cleanText as safe, formatRunReport, modelJson } from './presentation.js';
|
|
11
|
+
import { loadPiJevGuide, loadPiWorkflowGuide } from './skill-bundle.js';
|
|
12
|
+
import { ToolInputValidationError, toolInputProblems } from './tool-input-error.js';
|
|
13
|
+
const constructors = {
|
|
14
|
+
read: createReadToolDefinition, bash: createBashToolDefinition, edit: createEditToolDefinition,
|
|
15
|
+
write: createWriteToolDefinition, grep: createGrepToolDefinition, find: createFindToolDefinition, ls: createLsToolDefinition,
|
|
16
|
+
};
|
|
17
|
+
const mutatingTools = new Set(['bash', 'edit', 'write']);
|
|
18
|
+
const help = 'Describe a task: /agentrun <what you want done>\nTry it without keys: /agentrun demo | demo empty\nUse real Pi and Jev calls: /agentrun demo live\nInspect: /agentrun · Rerun: /agentrun run · Setup: /agentrun status\nStop: /agentrun stop (Escape does not cancel slash-started runs)';
|
|
19
|
+
const textResult = (text, details = {}) => ({ content: [{ type: 'text', text: safe(text) }], details });
|
|
20
|
+
const nativeLimits = Object.freeze({ deadlineMs: 600_000, modelRequests: 50, judgeCalls: 100, toolAttempts: 100,
|
|
21
|
+
nodeMaxTurns: 8, nodeMaxSubmissions: 3, nodeTimeoutMs: 120_000 });
|
|
22
|
+
/** Configure the native extension without granting configuration to workflow authors. */
|
|
23
|
+
export function createAgentRunExtension(options = {}) {
|
|
24
|
+
if (options.hostTools !== undefined && typeof options.hostTools !== 'function')
|
|
25
|
+
throw new Error('hostTools must be a host callback');
|
|
26
|
+
if (options.createJudge !== undefined && typeof options.createJudge !== 'function')
|
|
27
|
+
throw new Error('createJudge must be a host callback');
|
|
28
|
+
if (options.onToolAttempt !== undefined && typeof options.onToolAttempt !== 'function')
|
|
29
|
+
throw new Error('onToolAttempt must be a host callback');
|
|
30
|
+
if (options.onWorkflowEvent !== undefined && typeof options.onWorkflowEvent !== 'function')
|
|
31
|
+
throw new Error('onWorkflowEvent must be a host callback');
|
|
32
|
+
const limits = { ...nativeLimits, ...options.runtimeLimits };
|
|
33
|
+
for (const [key, value] of Object.entries(limits)) {
|
|
34
|
+
if (!(key in nativeLimits) || value !== null && (!Number.isSafeInteger(value) || value < 1 || value > nativeLimits[key])) {
|
|
35
|
+
throw new Error('runtimeLimits must use known fields with positive bounded integers or explicit null');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const configured = { hostTools: options.hostTools, createJudge: options.createJudge, onToolAttempt: options.onToolAttempt, onWorkflowEvent: options.onWorkflowEvent,
|
|
39
|
+
runtimeLimits: Object.freeze(limits) };
|
|
40
|
+
return pi => registerExtension(pi, configured);
|
|
41
|
+
}
|
|
42
|
+
export default function agentRunExtension(pi) {
|
|
43
|
+
registerExtension(pi, {});
|
|
44
|
+
}
|
|
45
|
+
function registerExtension(pi, configuration) {
|
|
46
|
+
const limits = { ...nativeLimits, ...configuration.runtimeLimits };
|
|
47
|
+
const serviceLimits = { deadlineMs: limits.deadlineMs, maxAgentCalls: limits.modelRequests,
|
|
48
|
+
maxJudgeCalls: limits.judgeCalls, maxToolCalls: limits.toolAttempts };
|
|
49
|
+
let current;
|
|
50
|
+
let runOrdinal = 0;
|
|
51
|
+
const toolSet = (ctx, trusted = false) => {
|
|
52
|
+
if (configuration.hostTools) {
|
|
53
|
+
const supplied = configuration.hostTools(ctx);
|
|
54
|
+
if (!Array.isArray(supplied) || supplied.length > 100)
|
|
55
|
+
throw new Error('Host must supply at most 100 explicit tool definitions');
|
|
56
|
+
const names = new Set();
|
|
57
|
+
return supplied.map(tool => {
|
|
58
|
+
if (!tool || typeof tool.name !== 'string' || !tool.name.trim() || tool.name.length > 200
|
|
59
|
+
|| ['agentrun', 'submit'].includes(tool.name) || names.has(tool.name))
|
|
60
|
+
throw new Error('Host tools contain an invalid, reserved or duplicate name');
|
|
61
|
+
if (typeof tool.execute !== 'function' || !tool.parameters || typeof tool.parameters !== 'object')
|
|
62
|
+
throw new Error('Host tools require an executable definition and parameter schema');
|
|
63
|
+
Compile(tool.parameters);
|
|
64
|
+
names.add(tool.name);
|
|
65
|
+
return { ...tool, parameters: structuredClone(tool.parameters) };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const active = new Set(pi.getActiveTools());
|
|
69
|
+
const builtins = new Set(pi.getAllTools().filter(t => t.sourceInfo.source === 'builtin').map(t => t.name));
|
|
70
|
+
const definitions = Object.entries(constructors).filter(([name]) => active.has(name) && builtins.has(name) && (trusted || !mutatingTools.has(name)))
|
|
71
|
+
.map(([, factory]) => factory(ctx.cwd));
|
|
72
|
+
return [demoSearchTool(), ...definitions];
|
|
73
|
+
};
|
|
74
|
+
const state = async (ctx) => {
|
|
75
|
+
const key = `${ctx.sessionManager.getSessionId()}:${ctx.cwd}`;
|
|
76
|
+
if (current && current.key === key && !current.closed)
|
|
77
|
+
return current;
|
|
78
|
+
if (current) {
|
|
79
|
+
const old = current;
|
|
80
|
+
old.clearStatus();
|
|
81
|
+
old.closed = true;
|
|
82
|
+
old.controller?.abort();
|
|
83
|
+
await old.service.dispose();
|
|
84
|
+
await old.inFlight;
|
|
85
|
+
}
|
|
86
|
+
const tools = toolSet(ctx);
|
|
87
|
+
current = { key, clearStatus: () => { if (ctx.hasUI) {
|
|
88
|
+
ctx.ui.setStatus('agentrun', undefined);
|
|
89
|
+
ctx.ui.setWidget('agentrun', undefined);
|
|
90
|
+
} }, tools, toolNames: tools.map(t => t.name), input: {}, busy: false, closed: false,
|
|
91
|
+
service: new WorkflowExtensionService({ allowedTools: tools.map(t => t.name), limits: serviceLimits }) };
|
|
92
|
+
return current;
|
|
93
|
+
};
|
|
94
|
+
const readiness = (ctx, s) => ({
|
|
95
|
+
skill: pi.getCommands().some(item => item.name === 'skill:agentrun-author' && item.source === 'skill'),
|
|
96
|
+
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: false, running: s.busy, workflow: !!s.draft,
|
|
98
|
+
mode: s.draft ? s.demo ? 'scripted' : 'live' : undefined,
|
|
99
|
+
});
|
|
100
|
+
const show = (s, content, details = {}) => {
|
|
101
|
+
if (current !== s || s.closed)
|
|
102
|
+
return;
|
|
103
|
+
pi.sendMessage({ customType: 'agentrun', content: safe(content), display: true, details });
|
|
104
|
+
};
|
|
105
|
+
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))
|
|
107
|
+
? 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
|
+
show(s, message);
|
|
109
|
+
};
|
|
110
|
+
const stage = (ctx, s, line) => {
|
|
111
|
+
if (current !== s || s.closed || !ctx.hasUI)
|
|
112
|
+
return;
|
|
113
|
+
ctx.ui.setStatus('agentrun', line ? safe(line).slice(0, 140) : undefined);
|
|
114
|
+
};
|
|
115
|
+
const inspect = (ctx, s, workflow, input) => {
|
|
116
|
+
if (s.busy && (workflow !== undefined || input !== undefined))
|
|
117
|
+
throw new Error('A workflow is running. Stop it before changing its definition or input.');
|
|
118
|
+
const candidate = workflow === undefined ? s.draft : workflow;
|
|
119
|
+
if (candidate === undefined)
|
|
120
|
+
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
|
+
// Include active tools available only with operator trust, without granting that trust.
|
|
125
|
+
// Execution rechecks the actual per-run permissions and captures implementations.
|
|
126
|
+
const checker = new WorkflowExtensionService({ allowedTools: toolSet(ctx, true).map(tool => tool.name), limits: serviceLimits });
|
|
127
|
+
let inspection;
|
|
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
|
+
}
|
|
136
|
+
// Stage the definition and input together only after nonexecuting checks succeed.
|
|
137
|
+
const nextDraft = workflow === undefined ? s.draft : structuredClone(workflow);
|
|
138
|
+
const changed = !s.draft || inspectWorkflow(s.draft).sha256 !== inspection.sha256;
|
|
139
|
+
const nextInput = input === undefined ? changed ? {} : s.input : structuredClone(input);
|
|
140
|
+
s.draft = nextDraft;
|
|
141
|
+
if (changed)
|
|
142
|
+
s.demo = undefined;
|
|
143
|
+
s.input = nextInput;
|
|
144
|
+
return { inspection, tree: formatWorkflowTree(inspection) };
|
|
145
|
+
};
|
|
146
|
+
const depsFor = (ctx, s, signal) => {
|
|
147
|
+
let modelCalls = 0, toolCalls = 0;
|
|
148
|
+
const countTool = () => {
|
|
149
|
+
signal.throwIfAborted();
|
|
150
|
+
toolCalls++;
|
|
151
|
+
if (limits.toolAttempts !== null && toolCalls > limits.toolAttempts)
|
|
152
|
+
throw new Error('Workflow tool-call limit reached');
|
|
153
|
+
configuration.onToolAttempt?.();
|
|
154
|
+
signal.throwIfAborted();
|
|
155
|
+
};
|
|
156
|
+
const tools = s.tools.map(tool => ({ ...tool, execute: async (...args) => {
|
|
157
|
+
const validator = Compile(tool.parameters);
|
|
158
|
+
if (!validator.Check(args[1]))
|
|
159
|
+
throw new ToolInputValidationError(tool.name, tool.name, toolInputProblems(validator.Errors(args[1])));
|
|
160
|
+
return tool.execute(...args);
|
|
161
|
+
} }));
|
|
162
|
+
const host = {
|
|
163
|
+
...ctx, thinkingLevel: ctx.thinkingLevel ?? pi.getThinkingLevel(),
|
|
164
|
+
modelRegistry: {
|
|
165
|
+
getAll: () => ctx.modelRegistry.getAll(),
|
|
166
|
+
streamSimple: (model, transcript, options) => {
|
|
167
|
+
signal.throwIfAborted();
|
|
168
|
+
modelCalls++;
|
|
169
|
+
if (limits.modelRequests !== null && modelCalls > limits.modelRequests)
|
|
170
|
+
throw new Error('Workflow model-request limit reached');
|
|
171
|
+
return ctx.modelRegistry.streamSimple(model, transcript, { ...options, reasoning: options?.reasoning === 'off' ? undefined : options?.reasoning, signal: options?.signal ? AbortSignal.any([signal, options.signal]) : signal });
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
const adapters = inspectWorkflow(s.draft).requires.adapters;
|
|
176
|
+
const runner = adapters.includes('runNode') ? createPiHostRunner(host, { tools, signal, maxTurns: limits.nodeMaxTurns,
|
|
177
|
+
maxSubmissions: limits.nodeMaxSubmissions, timeoutMs: limits.nodeTimeoutMs,
|
|
178
|
+
onEvent: event => { if (event.type === 'tool_execution_start')
|
|
179
|
+
countTool(); },
|
|
180
|
+
}) : undefined;
|
|
181
|
+
let judge;
|
|
182
|
+
if (adapters.includes('runJudge')) {
|
|
183
|
+
if (configuration.createJudge) {
|
|
184
|
+
judge = configuration.createJudge({ signal });
|
|
185
|
+
if (typeof judge !== 'function')
|
|
186
|
+
throw new Error('Host judge factory must return a runner');
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
try {
|
|
190
|
+
judge = createJevRunner({ signal, maxAttempts: 1 });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new Error('AgentRun requires TypeSafe configuration for this workflow. Set TYPESAFE_API_KEY and, for a gateway, TYPESAFE_BASE_URL before running.');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
runNode: request => {
|
|
199
|
+
if (!runner)
|
|
200
|
+
throw new Error('Workflow agent adapter was not admitted');
|
|
201
|
+
return runner(request);
|
|
202
|
+
},
|
|
203
|
+
runJudge: request => {
|
|
204
|
+
if (!judge)
|
|
205
|
+
throw new Error('Workflow Jev adapter was not admitted');
|
|
206
|
+
return judge(request);
|
|
207
|
+
},
|
|
208
|
+
runEffect: async ({ node, input, signal: effectSignal }) => {
|
|
209
|
+
countTool();
|
|
210
|
+
const tool = tools.find(tool => tool.name === node.tool);
|
|
211
|
+
if (!tool || node.via !== 'tool')
|
|
212
|
+
throw new Error('Workflow requested an unavailable tool');
|
|
213
|
+
let result;
|
|
214
|
+
try {
|
|
215
|
+
result = await tool.execute('agentrun-effect', input, effectSignal, undefined, ctx);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error instanceof ToolInputValidationError)
|
|
219
|
+
throw new ToolInputValidationError(node.label, tool.name, error.problems);
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
return !configuration.hostTools && node.tool === 'search' ? result.details : { content: result.content, ...(result.details === undefined ? {} : { details: result.details }) };
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
const execute = async (ctx, s, input, options = {}) => {
|
|
227
|
+
if (s.busy)
|
|
228
|
+
throw new Error('A workflow is already running. Use /agentrun stop first.');
|
|
229
|
+
if (!s.draft)
|
|
230
|
+
throw new Error('Inspect a workflow first. Try /agentrun demo, or describe a task with /agentrun <task>.');
|
|
231
|
+
const scripted = s.demo !== undefined;
|
|
232
|
+
s.busy = true;
|
|
233
|
+
s.controller = new AbortController();
|
|
234
|
+
const signals = [s.controller.signal, options.signal, ctx.signal].filter((v) => !!v);
|
|
235
|
+
const signal = AbortSignal.any(signals);
|
|
236
|
+
try {
|
|
237
|
+
if (inspectWorkflow(s.draft).requires.sopSections.length)
|
|
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.');
|
|
239
|
+
signal.throwIfAborted();
|
|
240
|
+
const available = toolSet(ctx, options.trusted);
|
|
241
|
+
if (available.map(t => t.name).join(',') !== s.toolNames.join(',')) {
|
|
242
|
+
await s.service.dispose();
|
|
243
|
+
signal.throwIfAborted();
|
|
244
|
+
s.tools = available;
|
|
245
|
+
s.toolNames = available.map(t => t.name);
|
|
246
|
+
s.service = new WorkflowExtensionService({ allowedTools: s.toolNames, limits: serviceLimits });
|
|
247
|
+
}
|
|
248
|
+
// Resolve implementations from the execution context even if names are unchanged.
|
|
249
|
+
s.tools = available;
|
|
250
|
+
const prepared = s.service.prepare(s.draft, { allowExecutableCandidates: options.trusted ?? false });
|
|
251
|
+
s.input = structuredClone(input);
|
|
252
|
+
const tree = formatWorkflowTree(inspectWorkflow(s.draft));
|
|
253
|
+
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
|
+
options.update?.(textResult(`${label}\n\n${tree}`));
|
|
255
|
+
stage(ctx, s, 'agent.run() · starting');
|
|
256
|
+
let completed = 0;
|
|
257
|
+
const active = new Map();
|
|
258
|
+
const ordinal = ++runOrdinal;
|
|
259
|
+
let sequence = 0;
|
|
260
|
+
const report = await s.service.run(s.input, {
|
|
261
|
+
deps: scripted ? scriptedDemoDeps(s.demo === 'empty') : depsFor(ctx, s, signal), signal,
|
|
262
|
+
...(configuration.onWorkflowEvent ? { onTraceEvent: (event) => configuration.onWorkflowEvent({ workflowDigest: prepared.digest, runOrdinal: ordinal, sequence: ++sequence, event }) } : {}),
|
|
263
|
+
onEvent: event => {
|
|
264
|
+
if (current !== s || s.closed || !event.label)
|
|
265
|
+
return;
|
|
266
|
+
if (event.type === 'node.start')
|
|
267
|
+
active.set(event.label, (active.get(event.label) ?? 0) + 1);
|
|
268
|
+
else if (event.type === 'node.end') {
|
|
269
|
+
const count = (active.get(event.label) ?? 1) - 1;
|
|
270
|
+
if (count)
|
|
271
|
+
active.set(event.label, count);
|
|
272
|
+
else
|
|
273
|
+
active.delete(event.label);
|
|
274
|
+
completed++;
|
|
275
|
+
}
|
|
276
|
+
else
|
|
277
|
+
return;
|
|
278
|
+
const running = [...active].map(([name, count]) => count > 1 ? `${name} ×${count}` : name).join(', ');
|
|
279
|
+
const progress = `agent.run() · ${completed} finished${running ? ` · ${running}` : ''}`;
|
|
280
|
+
stage(ctx, s, progress);
|
|
281
|
+
if (ctx.hasUI)
|
|
282
|
+
ctx.ui.setWidget('agentrun', [safe(progress), '/agentrun stop to cancel']);
|
|
283
|
+
options.update?.(textResult(`${label}\n\n${tree}\n\n${progress}`));
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
const summary = formatRunReport(report, scripted);
|
|
287
|
+
const { events: _events, ...modelReport } = report;
|
|
288
|
+
const mode = scripted ? 'scripted' : 'live';
|
|
289
|
+
return textResult(options.modelResult ? modelJson({ mode, ...modelReport }) : summary, { mode, ...report });
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
s.busy = false;
|
|
293
|
+
s.controller = undefined;
|
|
294
|
+
stage(ctx, s);
|
|
295
|
+
if (current === s && !s.closed && ctx.hasUI)
|
|
296
|
+
ctx.ui.setWidget('agentrun', undefined);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const launch = async (ctx, s, input, trusted = false) => {
|
|
300
|
+
if (s.busy)
|
|
301
|
+
throw new Error('A workflow is already running. Use /agentrun stop first.');
|
|
302
|
+
// Capture host selection and permission before yielding the interactive command loop.
|
|
303
|
+
const runContext = { ...ctx, model: ctx.model, thinkingLevel: ctx.thinkingLevel ?? pi.getThinkingLevel() };
|
|
304
|
+
const pending = execute(runContext, s, structuredClone(input), { trusted })
|
|
305
|
+
.then(result => show(s, result.content[0].text, result.details), error => commandError(s, error));
|
|
306
|
+
s.inFlight = pending;
|
|
307
|
+
const clear = () => { if (s.inFlight === pending)
|
|
308
|
+
s.inFlight = undefined; };
|
|
309
|
+
void pending.then(clear, clear);
|
|
310
|
+
// Headless callers await the report. Interactive Pi must regain its input
|
|
311
|
+
// loop so status/stop can be dispatched while a child Agent is running.
|
|
312
|
+
if (!ctx.hasUI)
|
|
313
|
+
await pending;
|
|
314
|
+
};
|
|
315
|
+
pi.registerTool({
|
|
316
|
+
name: 'agentrun', label: 'AgentRun workflow',
|
|
317
|
+
description: 'Use the agentrun-author skill to compose workflows. Describe lists available tools and their schemas. Inspect or run an AgentRun DSL workflow in this Pi session. First inspect to show its graph. Agent nodes capture the active Pi model when execution starts. Code needs the user command /agentrun run --trusted for each run. ' + (configuration.hostTools
|
|
318
|
+
? 'Only the explicit host-configured tools are available, including during trusted runs. Outer permission hooks are not inherited. '
|
|
319
|
+
: 'Enabled read-only Pi built-ins are available to declared steps. Shell/write/edit need the trusted command for each run; custom extension tools and permission hooks are not inherited. The search tool reads fictional demo sources only. ') + 'Scripted demos stay scripted on rerun; use /agentrun demo live to switch. Saving workflows is not part of v1.',
|
|
320
|
+
parameters: Type.Object({ action: Type.Union([Type.Literal('describe'), Type.Literal('inspect'), Type.Literal('run')]),
|
|
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())) }),
|
|
324
|
+
executionMode: 'sequential',
|
|
325
|
+
renderResult(result, options) {
|
|
326
|
+
const details = result.details;
|
|
327
|
+
const text = details?.status && details.calls ? options?.expanded
|
|
328
|
+
? JSON.stringify(details, null, 2) : formatRunReport(details, details.mode === 'scripted')
|
|
329
|
+
: result.content.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
330
|
+
return new Text(safe(text), 0, 0);
|
|
331
|
+
},
|
|
332
|
+
async execute(_id, args, signal, onUpdate, ctx) {
|
|
333
|
+
const s = await state(ctx);
|
|
334
|
+
if (args.action === 'describe') {
|
|
335
|
+
const details = { tools: toolSet(ctx).map(t => ({ name: t.name, description: t.description, parameters: t.parameters })),
|
|
336
|
+
limits: { ...limits }, structuralLimits: { ...extensionStructuralLimits }, ...readiness(ctx, s), authoring: { workflow: loadPiWorkflowGuide(), jev: loadPiJevGuide() } };
|
|
337
|
+
return textResult(JSON.stringify(details, null, 2), details);
|
|
338
|
+
}
|
|
339
|
+
if (args.action === 'run' && args.workflow !== undefined)
|
|
340
|
+
throw new Error('Inspect the workflow first with action inspect, then run without a workflow argument.');
|
|
341
|
+
if (args.action === 'inspect') {
|
|
342
|
+
const result = inspect(ctx, s, args.workflow, args.input);
|
|
343
|
+
return textResult(result.tree, { ...result, tools: toolSet(ctx).map(t => t.name) });
|
|
344
|
+
}
|
|
345
|
+
return execute(ctx, s, args.input ?? s.input, { signal, update: onUpdate, modelResult: true });
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
pi.registerCommand('agentrun', {
|
|
349
|
+
description: 'Describe a task, inspect its workflow, or try /agentrun demo',
|
|
350
|
+
async handler(args, ctx) {
|
|
351
|
+
const s = await state(ctx);
|
|
352
|
+
const raw = args.trim();
|
|
353
|
+
const command = /^(help|status|stop|demo|run)(?:\s|$)/i.test(raw) ? raw.toLowerCase().replace(/\s+/g, ' ') : raw;
|
|
354
|
+
const words = command.split(/\s+/);
|
|
355
|
+
const commandLike = /^(help|status|stop|demo|run)(?:-|$)/i.test(words[0])
|
|
356
|
+
&& words.slice(1).every(word => word.startsWith('-') || ['live', 'empty'].includes(word));
|
|
357
|
+
try {
|
|
358
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
359
|
+
show(s, help);
|
|
360
|
+
}
|
|
361
|
+
else if (!command) {
|
|
362
|
+
show(s, s.draft ? `${inspect(ctx, s).tree}\n\n${s.busy ? 'Running. /agentrun stop to cancel.' : s.demo ? 'Replay: /agentrun run (scripted, no model calls). Switch to real models: /agentrun demo live.' : 'Run: /agentrun run. Ask Pi to change this workflow.'}` : help);
|
|
363
|
+
}
|
|
364
|
+
else if (command === 'status') {
|
|
365
|
+
const ready = readiness(ctx, s);
|
|
366
|
+
show(s, `agent.run() · ${s.busy ? 'running' : 'idle'}\nDemo: ready — /agentrun demo (no keys or model calls)\nWorkflow: ${s.draft ? `${modelJson(inspectWorkflow(s.draft).name)} · ${s.demo ? 'scripted' : 'live adapters'}` : 'none — run the demo or describe a task; /reload clears the previous workflow'}\n\nFor your own workflows:\nPi host: ${PI_VERSION}${PI_VERSION === '0.87.0' ? '' : ' (tested on 0.87.0; use the bundled ./node_modules/.bin/pi)'}\nSkill: ${ready.skill ? 'loaded — /agentrun <task>' : 'missing — enable package skills, then /reload'}\nPi: ${ready.pi ? 'selected model available (connection not tested)' : 'no usable active model — /login to connect a provider, then /model to select it'}\nJev: ${ready.jev ? 'configuration present (connection not tested)' : 'not configured — only needed for system one decisions; set TYPESAFE_API_KEY before starting Pi'}`, ready);
|
|
367
|
+
}
|
|
368
|
+
else if (command === 'stop') {
|
|
369
|
+
s.controller?.abort();
|
|
370
|
+
s.service.stop();
|
|
371
|
+
show(s, s.busy ? 'Stop requested. Already-started tool effects may still finish.' : 'Nothing is running.');
|
|
372
|
+
}
|
|
373
|
+
else if (command === 'demo' || command === 'demo live' || command === 'demo empty') {
|
|
374
|
+
inspect(ctx, s, demoWorkflow);
|
|
375
|
+
s.demo = command === 'demo live' ? undefined : command === 'demo empty' ? 'empty' : 'scripted';
|
|
376
|
+
show(s, `${inspect(ctx, s).tree}\n\n${s.demo ? 'Running scripted demo. No model calls.' : 'Running with your Pi model and Jev.'}`);
|
|
377
|
+
await launch(ctx, s, demoInput);
|
|
378
|
+
}
|
|
379
|
+
else if (command === 'run' || command === 'run --trusted') {
|
|
380
|
+
await launch(ctx, s, s.input, command === 'run --trusted');
|
|
381
|
+
}
|
|
382
|
+
else if (commandLike || command.startsWith('-')) {
|
|
383
|
+
show(s, `Unknown AgentRun command: ${raw}\n\n${help}`);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
if (s.busy)
|
|
387
|
+
throw new Error('A workflow is running. Stop it before starting another.');
|
|
388
|
+
if (!pi.getCommands().some(item => item.name === 'skill:agentrun-author' && item.source === 'skill')) {
|
|
389
|
+
throw new Error('AgentRun authoring skill is unavailable. Enable package skills, then /reload. Run /agentrun status to check discovery.');
|
|
390
|
+
}
|
|
391
|
+
if (!readiness(ctx, s).pi)
|
|
392
|
+
throw new Error(PI_MODEL_SETUP_MESSAGE);
|
|
393
|
+
pi.sendUserMessage(`/skill:agentrun-author ${raw}`, { expandPromptTemplates: true });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
commandError(s, error);
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
const closeSession = async () => {
|
|
402
|
+
if (!current)
|
|
403
|
+
return;
|
|
404
|
+
const old = current;
|
|
405
|
+
old.clearStatus();
|
|
406
|
+
current = undefined;
|
|
407
|
+
old.closed = true;
|
|
408
|
+
old.controller?.abort();
|
|
409
|
+
await old.service.dispose();
|
|
410
|
+
await old.inFlight;
|
|
411
|
+
};
|
|
412
|
+
pi.on('session_shutdown', closeSession);
|
|
413
|
+
pi.on('session_before_switch', closeSession);
|
|
414
|
+
pi.on('session_before_fork', closeSession);
|
|
415
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
416
|
+
if (current && current.key !== `${ctx.sessionManager.getSessionId()}:${ctx.cwd}`)
|
|
417
|
+
await closeSession();
|
|
418
|
+
});
|
|
419
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type PiRunner } from './runner.js';
|
|
2
|
+
import type { PiHostContext, PiHostRunnerOptions, PiSessionFactory } from './types.js';
|
|
3
|
+
export declare const PI_MODEL_SETUP_MESSAGE = "AgentRun requires an active Pi model. Use /login to connect a provider, then /model to select it. Try /agentrun demo without model access.";
|
|
4
|
+
export declare function createPiHostSessionFactory(context: PiHostContext): PiSessionFactory;
|
|
5
|
+
export declare function createPiHostRunner(context: PiHostContext, options?: PiHostRunnerOptions): PiRunner;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Agent } from '@earendil-works/pi-agent-core';
|
|
2
|
+
import { createPiRunner } from './runner.js';
|
|
3
|
+
export const PI_MODEL_SETUP_MESSAGE = 'AgentRun requires an active Pi model. Use /login to connect a provider, then /model to select it. Try /agentrun demo without model access.';
|
|
4
|
+
function selectedModel(context) {
|
|
5
|
+
const model = context.model;
|
|
6
|
+
if (!model)
|
|
7
|
+
throw new Error(PI_MODEL_SETUP_MESSAGE);
|
|
8
|
+
if (!context.modelRegistry || typeof context.modelRegistry.streamSimple !== 'function' || typeof context.modelRegistry.getAll !== 'function') {
|
|
9
|
+
throw new Error('AgentRun requires the host public model registry');
|
|
10
|
+
}
|
|
11
|
+
if (!context.modelRegistry.getAll().some(known => known.id === model.id && known.provider === model.provider && known.api === model.api)) {
|
|
12
|
+
throw new Error(PI_MODEL_SETUP_MESSAGE);
|
|
13
|
+
}
|
|
14
|
+
return model;
|
|
15
|
+
}
|
|
16
|
+
export function createPiHostSessionFactory(context) {
|
|
17
|
+
const model = selectedModel(context);
|
|
18
|
+
const stream = context.modelRegistry.streamSimple.bind(context.modelRegistry);
|
|
19
|
+
return async (options) => {
|
|
20
|
+
if (options.model !== model)
|
|
21
|
+
throw new Error('The child session must use the captured active Pi model');
|
|
22
|
+
let delivered = false, turns = 0;
|
|
23
|
+
const allowed = new Set(options.tools);
|
|
24
|
+
const tools = options.customTools.filter(tool => allowed.has(tool.name)).map(tool => ({
|
|
25
|
+
name: tool.name, label: tool.label, description: tool.description,
|
|
26
|
+
parameters: tool.parameters,
|
|
27
|
+
async execute(id, args, signal, onUpdate) {
|
|
28
|
+
const result = await tool.execute(id, args, signal, onUpdate, context);
|
|
29
|
+
if (tool.name === 'submit' && result.details?.accepted === true)
|
|
30
|
+
delivered = true;
|
|
31
|
+
return result;
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
const agent = new Agent({
|
|
35
|
+
initialState: { model, thinkingLevel: options.thinkingLevel, systemPrompt: options.resourceLoader.getSystemPrompt() ?? '', tools },
|
|
36
|
+
streamFn: (_model, transcript, streamOptions) => stream(model, transcript, streamOptions),
|
|
37
|
+
toolExecution: 'sequential',
|
|
38
|
+
finishTurn: () => {
|
|
39
|
+
turns++;
|
|
40
|
+
if (delivered || (options.maxTurns !== undefined && turns >= options.maxTurns))
|
|
41
|
+
return { action: 'end' };
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
let disposed = false;
|
|
45
|
+
const unsubscribers = new Set();
|
|
46
|
+
return { session: {
|
|
47
|
+
async prompt(text) {
|
|
48
|
+
if (disposed)
|
|
49
|
+
throw new Error('Pi host child session is disposed');
|
|
50
|
+
await agent.prompt(text);
|
|
51
|
+
},
|
|
52
|
+
subscribe(listener) {
|
|
53
|
+
if (disposed)
|
|
54
|
+
throw new Error('Pi host child session is disposed');
|
|
55
|
+
const unsubscribe = agent.subscribe(event => { listener(event); });
|
|
56
|
+
unsubscribers.add(unsubscribe);
|
|
57
|
+
return () => { unsubscribe(); unsubscribers.delete(unsubscribe); };
|
|
58
|
+
},
|
|
59
|
+
async abort() { agent.abort(); },
|
|
60
|
+
dispose() {
|
|
61
|
+
if (disposed)
|
|
62
|
+
return;
|
|
63
|
+
disposed = true;
|
|
64
|
+
agent.abort();
|
|
65
|
+
agent.clearAllQueues();
|
|
66
|
+
for (const unsubscribe of unsubscribers)
|
|
67
|
+
unsubscribe();
|
|
68
|
+
unsubscribers.clear();
|
|
69
|
+
},
|
|
70
|
+
} };
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function createPiHostRunner(context, options = {}) {
|
|
74
|
+
const model = selectedModel(context);
|
|
75
|
+
return createPiRunner({
|
|
76
|
+
...options, model, cwd: options.cwd ?? context.cwd,
|
|
77
|
+
thinkingLevel: options.thinkingLevel ?? context.thinkingLevel,
|
|
78
|
+
sessionFactory: createPiHostSessionFactory(context),
|
|
79
|
+
});
|
|
80
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./runner.js";
|
|
2
|
+
export * from "./author.js";
|
|
3
|
+
export * from "./types.js";
|
|
4
|
+
export { createPiHostRunner, createPiHostSessionFactory } from './host-session.js';
|
|
5
|
+
export { WorkflowExtensionService } from './extension-service.js';
|
|
6
|
+
export { loadPiAuthorSkillBundle } from './skill-bundle.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./runner.js";
|
|
2
|
+
export * from "./author.js";
|
|
3
|
+
export * from "./types.js";
|
|
4
|
+
export { createPiHostRunner, createPiHostSessionFactory } from './host-session.js';
|
|
5
|
+
export { WorkflowExtensionService } from './extension-service.js';
|
|
6
|
+
export { loadPiAuthorSkillBundle } from './skill-bundle.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ExtensionRunReport } from './extension-service.js';
|
|
2
|
+
export declare const cleanText: (text: string) => string;
|
|
3
|
+
export declare const modelJson: (value: unknown) => string;
|
|
4
|
+
export declare function formatRunReport(report: ExtensionRunReport, scripted?: boolean): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const cleanText = (text) => text.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, '');
|
|
2
|
+
export const modelJson = (value) => JSON.stringify(value).replace(/[\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, character => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
3
|
+
function lines(value, depth = 0) {
|
|
4
|
+
if (value === null || typeof value !== 'object')
|
|
5
|
+
return [String(value)];
|
|
6
|
+
if (depth > 5)
|
|
7
|
+
return ['…'];
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return value.slice(0, 30).flatMap(item => {
|
|
10
|
+
const [first = '', ...rest] = lines(item, depth + 1);
|
|
11
|
+
return [`• ${first}`, ...rest.map(line => ` ${line}`)];
|
|
12
|
+
}).concat(value.length > 30 ? [`… ${value.length - 30} more items in the structured result`] : []);
|
|
13
|
+
const entries = Object.entries(value);
|
|
14
|
+
return entries.slice(0, 30).flatMap(([key, item]) => {
|
|
15
|
+
const label = key.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/_/g, ' ');
|
|
16
|
+
const children = lines(item, depth + 1);
|
|
17
|
+
return children.length === 1 ? [`${label}: ${children[0]}`] : [`${label}:`, ...children.map(line => ` ${line}`)];
|
|
18
|
+
}).concat(entries.length > 30 ? [`… ${entries.length - 30} more fields in the structured result`] : []);
|
|
19
|
+
}
|
|
20
|
+
export function formatRunReport(report, scripted = false) {
|
|
21
|
+
const body = report.output === undefined ? report.escalation?.summary ?? [
|
|
22
|
+
report.error?.stage ? `Step: ${report.error.stage}` : undefined,
|
|
23
|
+
report.error?.path ? `State path: ${report.error.path}` : undefined,
|
|
24
|
+
report.error?.reason ? `Reason: ${report.error.reason}` : undefined,
|
|
25
|
+
Number.isInteger(report.error?.status) && report.error.status >= 100 && report.error.status <= 599 ? `HTTP status: ${report.error.status}` : undefined,
|
|
26
|
+
report.error?.message, ...(report.error?.problems ?? []),
|
|
27
|
+
].filter(Boolean).join('\n') : lines(report.output).join('\n');
|
|
28
|
+
const recovery = report.error ? '\nInspect the failed step with /agentrun. Use /agentrun status to check configuration before retrying.' : '';
|
|
29
|
+
const uncertain = report.uncertainEffects?.length ? '\nSome tool effects have an unknown outcome. Check their actual state before retrying.' : '';
|
|
30
|
+
const trace = report.traceTruncated ? `\nTrace retention is partial${report.trace ? ` (${report.trace.droppedEvents} valid events omitted, ${report.trace.rejectedEvents} rejected)` : ''}; this does not mean the output is truncated.` : '';
|
|
31
|
+
const clipped = body.length > 12_000 ? `${body.slice(0, 12_000)}\n… Full output is in the structured result.` : body;
|
|
32
|
+
const status = report.status === 'escalated' ? 'stopped — needs attention (escalated)' : report.status;
|
|
33
|
+
return cleanText(`${scripted ? 'Scripted demo' : 'Workflow'}: ${status}${report.error ? ` (${report.error.code})` : ''}\n\n${clipped}${recovery}${uncertain}${trace}\n\nWorkflow calls: ${report.calls.tool} direct tool call${report.calls.tool === 1 ? '' : 's'}, ${report.calls.judge} system one decision${report.calls.judge === 1 ? '' : 's'}, ${report.calls.agent} model step${report.calls.agent === 1 ? '' : 's'}.${scripted ? '\nScripted responses; no model calls. Replay: /agentrun run. Real models: /agentrun demo live.' : ''}`);
|
|
34
|
+
}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WorkflowDeps } from "@parcha/agentrun-dsl";
|
|
2
|
+
import type { PiRunnerOptions } from "./types.js";
|
|
3
|
+
export type { PiRunnerOptions, PiSession, PiSessionFactory } from "./types.js";
|
|
4
|
+
export type PiRunner = NonNullable<WorkflowDeps["runNode"]>;
|
|
5
|
+
export type PiNodeRequest = Parameters<PiRunner>[0];
|
|
6
|
+
export declare class PiRunError extends Error {
|
|
7
|
+
readonly reason: "aborted" | "timeout" | "turn_limit" | "submission_limit" | "no_submission" | "model_error";
|
|
8
|
+
readonly turns: number;
|
|
9
|
+
readonly submissions: number;
|
|
10
|
+
constructor(reason: "aborted" | "timeout" | "turn_limit" | "submission_limit" | "no_submission" | "model_error", turns: number, submissions: number);
|
|
11
|
+
}
|
|
12
|
+
export declare function createPiRunner(options: PiRunnerOptions): PiRunner;
|