@parcha/agentrun-pi 0.1.0-beta.23 → 0.1.0-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/durable/index.d.ts +1 -1
- package/dist/durable/node-runner.js +3 -2
- package/dist/durable/readers.d.ts +8 -3
- package/dist/durable/readers.js +7 -6
- package/dist/durable/record-tool.js +2 -1
- package/dist/durable/workflow-tool.js +4 -3
- package/dist/extension.js +6 -4
- package/dist/host-session.js +2 -1
- package/dist/host-tools.d.ts +4 -1
- package/dist/host-tools.js +4 -1
- package/dist/runner.js +8 -6
- package/dist/workflow-tool.d.ts +1 -1
- package/dist/workflow-tool.js +4 -1
- package/package.json +4 -4
package/dist/durable/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { RecoveryBinding, RecoveryEffect, RecoveryJournal, RecoveryNote, RecoveryStore } from "@parcha/agentrun-dsl/recovery";
|
|
2
2
|
export { documentStore, type DocumentStoreHost, type DocumentStoreOptions } from "./store.js";
|
|
3
|
-
export { committedRoutes, durableDecisions, durableDoc, readJournal, type JournalView } from "./readers.js";
|
|
3
|
+
export { committedRoutes, durableDecisions, durableDoc, readJournal, type DurableReadOptions, type JournalView } from "./readers.js";
|
|
4
4
|
export { recordCommits, recordingLines, snapshotLines, type RecordingLine, type RecordingSource } from "./recording.js";
|
|
5
5
|
export { reuseReceipts, seedReceipts, ReceiptIndex, type ReceiptReuse, type ReuseReceiptsOptions, type ReusableTool } from "./receipts.js";
|
|
6
6
|
export { effectResultOf } from "../effect-result.js";
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
13
|
import { configure, defineDoc, defineExtension, GenerationTask, hook, section, } from "@earendil-works/pi-durable";
|
|
14
14
|
import { canonicalSha256, runStopOf } from "@parcha/agentrun-dsl/recovery";
|
|
15
|
+
import { SUBMIT_TOOL } from "../host-tools.js";
|
|
15
16
|
import { nodeThinking } from "../thinking.js";
|
|
16
17
|
import { closeNodeAttempt, runNodeAttempt } from "./node.js";
|
|
17
18
|
import { DELIVERY_ATTEMPTS, lintRecordRoot, submitFooter } from "./record.js";
|
|
@@ -78,7 +79,7 @@ async function underStop(signal, work) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
const listed = (value) => Array.isArray(value);
|
|
81
|
-
const isSubmit = (tool) => tool.name ===
|
|
82
|
+
const isSubmit = (tool) => tool.name === SUBMIT_TOOL;
|
|
82
83
|
/** The node runner over a host. Install `extension` in the registry before the Harness opens; `on` binds the runner
|
|
83
84
|
* to where its conversations live (`taskScope`, `hostScope`). */
|
|
84
85
|
export function nodeRunner(host, options = {}) {
|
|
@@ -177,7 +178,7 @@ export function nodeRunner(host, options = {}) {
|
|
|
177
178
|
const { agent, record } = await underStop(signal, async () => ({ agent: leveled(await host.agent(node), node), record: await host.record?.(node) ?? {} }));
|
|
178
179
|
const label = record.label ?? `the ${node.label} record`;
|
|
179
180
|
const offered = (listed(agent.tools) ? agent.tools : listed(agent.extensions) ? agent.extensions.flatMap((selected) => selected.tools ?? []) : [])
|
|
180
|
-
.map((offer) => offer.name).filter((name) => name !==
|
|
181
|
+
.map((offer) => offer.name).filter((name) => name !== SUBMIT_TOOL);
|
|
181
182
|
const system = [...node.system, host.stance?.(node, { tools: offered })].filter(Boolean).join("\n\n");
|
|
182
183
|
const input = inputOf(node.user);
|
|
183
184
|
const reviewers = [
|
|
@@ -11,11 +11,16 @@ export type JournalView = {
|
|
|
11
11
|
notes: RecoveryNote[];
|
|
12
12
|
effects: RecoveryEffect[];
|
|
13
13
|
};
|
|
14
|
+
/** How a reader opens the store file. `immutable` opens it as SQLite's immutable file, for a store no process writes any
|
|
15
|
+
* more and with no write-ahead log beside it (an ended run's): nothing beside it is read or made. */
|
|
16
|
+
export type DurableReadOptions = {
|
|
17
|
+
immutable?: boolean;
|
|
18
|
+
};
|
|
14
19
|
/** The run's store `file`, opened read-only for `read`; null when there is no such file. Every query of one `read` runs
|
|
15
20
|
* in one read transaction, so what it returns is one commit's view, whatever a writer commits meanwhile. */
|
|
16
|
-
export declare function withDurableFile<T>(file: string, read: (db: Database) => T): T | null;
|
|
21
|
+
export declare function withDurableFile<T>(file: string, read: (db: Database) => T, options?: DurableReadOptions): T | null;
|
|
17
22
|
/** A Session document of the run's store `file`, read-only; undefined when the file or the document is absent. */
|
|
18
|
-
export declare function durableDoc<T = Record<string, unknown>>(file: string, kind: string, key?: string): T | undefined;
|
|
23
|
+
export declare function durableDoc<T = Record<string, unknown>>(file: string, kind: string, key?: string, options?: DurableReadOptions): T | undefined;
|
|
19
24
|
/** The decision rows (`agentrun.decisions`) of the run's store `file`, in commit order, in whichever form the store holds: the single
|
|
20
25
|
* document's `receipts` list, or the keyed documents (one row each, in the order of their keys). A reader that folds decisions from the
|
|
21
26
|
* store uses this and not `durableDoc(file, "agentrun.decisions").receipts`, which is nothing in the keyed form. */
|
|
@@ -25,7 +30,7 @@ export declare function durableDecisions(file: string): Array<Record<string, unk
|
|
|
25
30
|
* admitted, in either layout: in the keyed one (`agentrun.frozen_run.v4`) the effects a map item owns, nested maps
|
|
26
31
|
* included, are listed by that item's record, not the driver's, and are read from there. `state` is the driver's own
|
|
27
32
|
* record: in the keyed layout what lies in each map item stays in the item's record. */
|
|
28
|
-
export declare function readJournal(file: string, key: string): JournalView | undefined;
|
|
33
|
+
export declare function readJournal(file: string, key: string, options?: DurableReadOptions): JournalView | undefined;
|
|
29
34
|
/** Every committed route decision a frozen snapshot holds (each judge's answer, whole), by execution path. Empty for a
|
|
30
35
|
* state that is no snapshot. */
|
|
31
36
|
export declare function committedRoutes(state: unknown): Record<string, Json>;
|
package/dist/durable/readers.js
CHANGED
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
// readers take the store file itself (pi-durable's SQLite file, wherever the host keeps it), never a directory.
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
7
8
|
import { apply } from "@earendil-works/chord/delta";
|
|
8
9
|
import { decisionKey } from "./decisions.js";
|
|
9
10
|
import { effectKey } from "./store.js";
|
|
10
11
|
/** The run's store `file`, opened read-only for `read`; null when there is no such file. Every query of one `read` runs
|
|
11
12
|
* in one read transaction, so what it returns is one commit's view, whatever a writer commits meanwhile. */
|
|
12
|
-
export function withDurableFile(file, read) {
|
|
13
|
+
export function withDurableFile(file, read, options = {}) {
|
|
13
14
|
if (!fs.existsSync(file))
|
|
14
15
|
return null;
|
|
15
16
|
const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
16
|
-
const db = new DatabaseSync(file, { readOnly: true, timeout: 5000 });
|
|
17
|
+
const db = new DatabaseSync(options.immutable ? `${pathToFileURL(file).href}?immutable=1` : file, { readOnly: true, timeout: 5000 });
|
|
17
18
|
try {
|
|
18
19
|
db.exec("BEGIN");
|
|
19
20
|
return read(db);
|
|
@@ -67,8 +68,8 @@ function itemEffects(db, key, table, into) {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
/** A Session document of the run's store `file`, read-only; undefined when the file or the document is absent. */
|
|
70
|
-
export function durableDoc(file, kind, key) {
|
|
71
|
-
return (withDurableFile(file, (db) => sessionDoc(db, kind, key)) ?? undefined);
|
|
71
|
+
export function durableDoc(file, kind, key, options = {}) {
|
|
72
|
+
return (withDurableFile(file, (db) => sessionDoc(db, kind, key), options) ?? undefined);
|
|
72
73
|
}
|
|
73
74
|
/** The decision rows (`agentrun.decisions`) of the run's store `file`, in commit order, in whichever form the store holds: the single
|
|
74
75
|
* document's `receipts` list, or the keyed documents (one row each, in the order of their keys). A reader that folds decisions from the
|
|
@@ -94,7 +95,7 @@ export function durableDecisions(file) {
|
|
|
94
95
|
* admitted, in either layout: in the keyed one (`agentrun.frozen_run.v4`) the effects a map item owns, nested maps
|
|
95
96
|
* included, are listed by that item's record, not the driver's, and are read from there. `state` is the driver's own
|
|
96
97
|
* record: in the keyed layout what lies in each map item stays in the item's record. */
|
|
97
|
-
export function readJournal(file, key) {
|
|
98
|
+
export function readJournal(file, key, options = {}) {
|
|
98
99
|
return withDurableFile(file, (db) => {
|
|
99
100
|
const doc = sessionDoc(db, "agentrun.driver", key);
|
|
100
101
|
if (!doc || doc.binding === null)
|
|
@@ -113,7 +114,7 @@ export function readJournal(file, key) {
|
|
|
113
114
|
return { id, name: e?.name ?? "", argsHash: e?.argsHash ?? "", status: e?.status ?? "unknown", session: e?.session ?? null, result: e?.result ?? null, ...(e?.intent ? { intent: e.intent } : {}), ...(e?.args !== undefined ? { args: e.args } : {}), ...(Array.isArray(e?.calls) ? { calls: e.calls } : {}) };
|
|
114
115
|
});
|
|
115
116
|
return { binding: doc.binding, inputs: doc.inputs, generation: doc.generation, revision: doc.revision, state: doc.state, notes: doc.notes, effects };
|
|
116
|
-
}) ?? undefined;
|
|
117
|
+
}, options) ?? undefined;
|
|
117
118
|
}
|
|
118
119
|
/** Every committed route decision a frozen snapshot holds (each judge's answer, whole), by execution path. Empty for a
|
|
119
120
|
* state that is no snapshot. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineDoc, defineTool, GenerationTask, hook, } from "@earendil-works/pi-durable";
|
|
2
2
|
import { DELIVERY_ATTEMPTS, deliver, deliveryTerminates, deliveryText, fileSubmission, mixedFileSubmission, nudgeText, repairRecord, VIOLATIONS_SHOWN, } from "./record.js";
|
|
3
|
+
import { SUBMIT_TOOL } from "../host-tools.js";
|
|
3
4
|
import { toolResult } from "./pi-versions.js";
|
|
4
5
|
/** The record a conversation delivered, or null while it has not, and the delivery attempts it has spent. */
|
|
5
6
|
export const RecordDoc = defineDoc({
|
|
@@ -76,7 +77,7 @@ export function recordTool(options) {
|
|
|
76
77
|
...(deliveryTerminates(delivery) ? { control: { terminate: true } } : {}),
|
|
77
78
|
});
|
|
78
79
|
return defineTool({
|
|
79
|
-
name:
|
|
80
|
+
name: SUBMIT_TOOL,
|
|
80
81
|
description: shown.description,
|
|
81
82
|
parameters: shown.parameters,
|
|
82
83
|
// Every call's arguments are carried to `execute`; one record's are repaired here, before the Harness validates.
|
|
@@ -8,7 +8,7 @@ import { Type } from "typebox";
|
|
|
8
8
|
import { inspectWorkflow, WORKFLOW_NODE_KINDS } from "@parcha/agentrun-dsl";
|
|
9
9
|
import { canonicalHash } from "@parcha/agentrun-dsl/recovery";
|
|
10
10
|
import { extensionStructuralLimits, WorkflowExtensionService } from "../extension-service.js";
|
|
11
|
-
import {
|
|
11
|
+
import { assertHostTools, SUBMIT_TOOL } from "../host-tools.js";
|
|
12
12
|
import { modelJson } from "../presentation.js";
|
|
13
13
|
import { WorkflowStore } from "../workflow-store.js";
|
|
14
14
|
import { progressLines, WorkflowObservation } from "../workflow-view.js";
|
|
@@ -38,7 +38,8 @@ const answer = (value) => toolResult([{ type: "text", text: modelJson(value) }],
|
|
|
38
38
|
* agent definition. `workflow` reads and stops runs from the host's side. */
|
|
39
39
|
export function createWorkflowTool(host) {
|
|
40
40
|
// The host's tools pass the check the coding-agent extension's do; the tool's own name and the node runner's submit are reserved.
|
|
41
|
-
|
|
41
|
+
const name = host.name ?? "agentrun";
|
|
42
|
+
assertHostTools(host.tools ?? [], { reserved: [name, SUBMIT_TOOL], executable: false });
|
|
42
43
|
const names = (host.tools ?? []).map((tool) => tool.name);
|
|
43
44
|
const checker = () => new WorkflowExtensionService({ allowedTools: names });
|
|
44
45
|
/** Inspect's checks, after naming any tool the workflow uses (a call's, or an agent node's) that the host did not
|
|
@@ -86,7 +87,7 @@ export function createWorkflowTool(host) {
|
|
|
86
87
|
},
|
|
87
88
|
});
|
|
88
89
|
const tool = defineTool({
|
|
89
|
-
name
|
|
90
|
+
name,
|
|
90
91
|
description: "Compose and run AgentRun workflows in this session. Describe lists the tools a workflow may use, as a call's tool or an agent node's tools, and the language; a workflow uses only the tools describe lists, which the host registers. Inspect validates a complete workflow and keeps it as this conversation's draft. Dry-run checks the draft on synthetic values (or the given input) with no model, Jev or tool call. Run starts the draft as a durable task of this conversation and answers with its outcome. Running the same draft on the same input again, after a restart too, reaches the same run and makes no effect twice; again: true starts a fresh run of them, which a later run then reaches. Save, load and list keep named definition revisions.",
|
|
91
92
|
parameters: Type.Object({ ...workflowToolProperties(), again: Type.Optional(Type.Boolean({ description: "Run only: start a fresh run of the same draft and input instead of reaching the one already run." })) }),
|
|
92
93
|
executionMode: "sequential",
|
package/dist/extension.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { Compile } from 'typebox/compile';
|
|
4
|
-
import {
|
|
4
|
+
import { assertHostTools, SUBMIT_TOOL } from './host-tools.js';
|
|
5
5
|
import { Text } from '@earendil-works/pi-tui';
|
|
6
6
|
import { createReadToolDefinition, createBashToolDefinition, createEditToolDefinition, createWriteToolDefinition, createGrepToolDefinition, createFindToolDefinition, createLsToolDefinition, VERSION as PI_VERSION, } from '@earendil-works/pi-coding-agent';
|
|
7
7
|
import { formatWorkflowTree, inspectWorkflow, AUTHOR_SKILL_NAME } from '@parcha/agentrun-dsl';
|
|
@@ -29,6 +29,8 @@ const constructors = {
|
|
|
29
29
|
write: createWriteToolDefinition, grep: createGrepToolDefinition, find: createFindToolDefinition, ls: createLsToolDefinition,
|
|
30
30
|
};
|
|
31
31
|
const mutatingTools = new Set(['bash', 'edit', 'write']);
|
|
32
|
+
/** The extension's own tool's name, which a host tool may not take. */
|
|
33
|
+
const toolName = 'agentrun';
|
|
32
34
|
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';
|
|
33
35
|
const textResult = (text, details = {}) => ({ content: [{ type: 'text', text: safe(text) }], details });
|
|
34
36
|
function singleTextInput(workflow) {
|
|
@@ -79,7 +81,7 @@ function registerExtension(pi, configuration) {
|
|
|
79
81
|
const toolSet = (ctx, trusted = false) => {
|
|
80
82
|
if (configuration.hostTools) {
|
|
81
83
|
const supplied = configuration.hostTools(ctx);
|
|
82
|
-
|
|
84
|
+
assertHostTools(supplied, { reserved: [toolName, SUBMIT_TOOL], executable: true });
|
|
83
85
|
return supplied.map(tool => ({ ...tool, parameters: structuredClone(tool.parameters), ...(tool.resultSchema ? { resultSchema: structuredClone(tool.resultSchema) } : {}) }));
|
|
84
86
|
}
|
|
85
87
|
const active = new Set(pi.getActiveTools());
|
|
@@ -467,10 +469,10 @@ function registerExtension(pi, configuration) {
|
|
|
467
469
|
await pending;
|
|
468
470
|
};
|
|
469
471
|
pi.registerTool({
|
|
470
|
-
name:
|
|
472
|
+
name: toolName, label: 'AgentRun workflow',
|
|
471
473
|
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
|
|
472
474
|
? 'Only the explicit host-configured tools are available, including during trusted runs. Outer permission hooks are not inherited. '
|
|
473
|
-
: '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.
|
|
475
|
+
: '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. A run this session left interrupted or failed resumes from its journal when its draft, input, SOP and tools are unchanged; any other run starts new. Scripted demos stay scripted on rerun.',
|
|
474
476
|
parameters: workflowToolParameters(),
|
|
475
477
|
executionMode: 'sequential',
|
|
476
478
|
renderResult(result, options) {
|
package/dist/host-session.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Agent } from '@earendil-works/pi-agent-core';
|
|
2
|
+
import { SUBMIT_TOOL } from './host-tools.js';
|
|
2
3
|
import { createPiRunner } from './runner.js';
|
|
3
4
|
import { workflowToolContext } from './tool-context.js';
|
|
4
5
|
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.';
|
|
@@ -28,7 +29,7 @@ export function createPiHostSessionFactory(context) {
|
|
|
28
29
|
parameters: tool.parameters,
|
|
29
30
|
async execute(id, args, signal, onUpdate) {
|
|
30
31
|
const result = await tool.execute(id, args, signal, onUpdate, toolContext);
|
|
31
|
-
if (tool.name ===
|
|
32
|
+
if (tool.name === SUBMIT_TOOL && result.details?.accepted === true)
|
|
32
33
|
delivered = true;
|
|
33
34
|
return result;
|
|
34
35
|
},
|
package/dist/host-tools.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
/** The tool a workflow node delivers its result with (the runner's and the durable record tool's), so no host tool may
|
|
2
|
+
* take its name. */
|
|
3
|
+
export declare const SUBMIT_TOOL = "submit";
|
|
1
4
|
/**
|
|
2
5
|
* Refuses a host's tool list unless it is an array of at most 100 tools, each with a name of 1 to 200 characters that
|
|
3
6
|
* is not blank, not in `reserved` and not repeated, and an object parameter schema (and result schema, when given) the
|
|
4
7
|
* compiler takes. With `executable`, each tool also needs an `execute` function. Throws on the first problem.
|
|
5
8
|
*/
|
|
6
|
-
export declare function
|
|
9
|
+
export declare function assertHostTools(tools: unknown, options: {
|
|
7
10
|
reserved: readonly string[];
|
|
8
11
|
executable: boolean;
|
|
9
12
|
}): void;
|
package/dist/host-tools.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
import { Compile } from 'typebox/compile';
|
|
4
4
|
/** The most tools a host may register. */
|
|
5
5
|
const MAX_HOST_TOOLS = 100;
|
|
6
|
+
/** The tool a workflow node delivers its result with (the runner's and the durable record tool's), so no host tool may
|
|
7
|
+
* take its name. */
|
|
8
|
+
export const SUBMIT_TOOL = 'submit';
|
|
6
9
|
/**
|
|
7
10
|
* Refuses a host's tool list unless it is an array of at most 100 tools, each with a name of 1 to 200 characters that
|
|
8
11
|
* is not blank, not in `reserved` and not repeated, and an object parameter schema (and result schema, when given) the
|
|
9
12
|
* compiler takes. With `executable`, each tool also needs an `execute` function. Throws on the first problem.
|
|
10
13
|
*/
|
|
11
|
-
export function
|
|
14
|
+
export function assertHostTools(tools, options) {
|
|
12
15
|
if (!Array.isArray(tools) || tools.length > MAX_HOST_TOOLS)
|
|
13
16
|
throw new Error(`Host must supply at most ${MAX_HOST_TOOLS} explicit tool definitions`);
|
|
14
17
|
const names = new Set();
|
package/dist/runner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Compile } from "typebox/compile";
|
|
2
2
|
import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, ModelRuntime, } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { SUBMIT_TOOL } from "./host-tools.js";
|
|
3
4
|
import { PiRunError } from "./run-error.js";
|
|
4
5
|
import { nodeThinking } from "./thinking.js";
|
|
5
6
|
export { PiRunError } from "./run-error.js";
|
|
@@ -49,7 +50,7 @@ export function createPiRunner(options) {
|
|
|
49
50
|
const timeoutMs = options.timeoutMs === null ? null : positive(options.timeoutMs ?? 120_000, "timeoutMs");
|
|
50
51
|
const names = new Set();
|
|
51
52
|
for (const tool of options.tools ?? []) {
|
|
52
|
-
if (tool.name ===
|
|
53
|
+
if (tool.name === SUBMIT_TOOL || names.has(tool.name))
|
|
53
54
|
throw new Error(`Reserved or duplicate Pi tool: ${tool.name}`);
|
|
54
55
|
names.add(tool.name);
|
|
55
56
|
}
|
|
@@ -96,7 +97,7 @@ export function createPiRunner(options) {
|
|
|
96
97
|
let unsubscribe;
|
|
97
98
|
const result = (text, ok) => ({ content: [{ type: "text", text }], details: { accepted: ok } });
|
|
98
99
|
const submit = {
|
|
99
|
-
name:
|
|
100
|
+
name: SUBMIT_TOOL, label: "Submit result", description: "Deliver a result matching the required JSON Schema. Rejection feedback must be corrected in this session.",
|
|
100
101
|
parameters,
|
|
101
102
|
executionMode: "sequential",
|
|
102
103
|
async execute(id, args) {
|
|
@@ -162,7 +163,7 @@ export function createPiRunner(options) {
|
|
|
162
163
|
cwd, model: options.model, modelRuntime: options.modelRuntime,
|
|
163
164
|
thinkingLevel: nodeThinking(request.thinking, options.thinkingLevel), maxTurns: maxTurns ?? undefined, resourceLoader, settingsManager,
|
|
164
165
|
sessionManager,
|
|
165
|
-
tools: [...requested,
|
|
166
|
+
tools: [...requested, SUBMIT_TOOL],
|
|
166
167
|
customTools: [...(options.tools ?? []).filter(tool => requested.includes(tool.name)).map(tool => ({
|
|
167
168
|
...tool,
|
|
168
169
|
executionMode: "sequential",
|
|
@@ -192,7 +193,7 @@ export function createPiRunner(options) {
|
|
|
192
193
|
unsubscribe = session.subscribe(event => {
|
|
193
194
|
// Native events carry correlation IDs, including SDK-rejected calls. Custom
|
|
194
195
|
// sessions without IDs use execute's counter so one attempt is not counted twice.
|
|
195
|
-
if (event.type === "tool_execution_start" && event.toolName ===
|
|
196
|
+
if (event.type === "tool_execution_start" && event.toolName === SUBMIT_TOOL && event.toolCallId !== undefined && !accepted && !stopped && !fatal) {
|
|
196
197
|
submissions++;
|
|
197
198
|
pendingSubmissions.set(event.toolCallId, {
|
|
198
199
|
...(Object.hasOwn(event, "args") ? { args: structuredClone(event.args) } : {}), executed: false,
|
|
@@ -200,7 +201,7 @@ export function createPiRunner(options) {
|
|
|
200
201
|
if (maxSubmissions !== null && submissions > maxSubmissions)
|
|
201
202
|
stop("submission_limit");
|
|
202
203
|
}
|
|
203
|
-
if (event.type === "tool_execution_end" && event.toolName ===
|
|
204
|
+
if (event.type === "tool_execution_end" && event.toolName === SUBMIT_TOOL) {
|
|
204
205
|
if (event.toolCallId !== undefined)
|
|
205
206
|
pendingSubmissions.delete(event.toolCallId);
|
|
206
207
|
if (!accepted && !stopped && !fatal && maxSubmissions !== null && submissions >= maxSubmissions)
|
|
@@ -228,7 +229,8 @@ export function createPiRunner(options) {
|
|
|
228
229
|
throw new PiRunError(stopped, turns, submissions);
|
|
229
230
|
if (accepted)
|
|
230
231
|
return submission;
|
|
231
|
-
|
|
232
|
+
// The loop ends only on one of the three above or at the turn cap, so what is left is the cap.
|
|
233
|
+
throw new PiRunError("turn_limit", turns, submissions);
|
|
232
234
|
}
|
|
233
235
|
catch (error) {
|
|
234
236
|
if (fatal)
|
package/dist/workflow-tool.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from 'typebox';
|
|
2
2
|
import { type AuthorHostAddendum, type DryRunResult, type WorkflowDeps } from '@parcha/agentrun-dsl';
|
|
3
|
-
import type
|
|
3
|
+
import { type WorkflowExtensionService } from './extension-service.js';
|
|
4
4
|
export declare const WORKFLOW_TOOL_ACTIONS: readonly ["describe", "inspect", "dry-run", "run", "save", "load", "list"];
|
|
5
5
|
/** The tool's fields: one action, and the fields an action reads. A host's tool may add its own. */
|
|
6
6
|
export declare const workflowToolProperties: () => {
|
package/dist/workflow-tool.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// and the dry run. Nothing here imports pi's coding agent, its agent core or its TUI, so a pi-durable host can use it.
|
|
3
3
|
import { Type } from 'typebox';
|
|
4
4
|
import { compileTransform, dryRunWorkflow, formatWorkflowTree, inspectWorkflow, loadAuthorReference, renderAuthorHostAddendum, } from '@parcha/agentrun-dsl';
|
|
5
|
+
import { ExtensionServiceError } from './extension-service.js';
|
|
5
6
|
import { PI_HOST_ADDENDUM } from './host-addendum.js';
|
|
6
7
|
export const WORKFLOW_TOOL_ACTIONS = ['describe', 'inspect', 'dry-run', 'run', 'save', 'load', 'list'];
|
|
7
8
|
/** The tool's fields: one action, and the fields an action reads. A host's tool may add its own. */
|
|
@@ -35,7 +36,9 @@ export function inspectCandidate(checker, candidate, input) {
|
|
|
35
36
|
inspection = checker.preflight(candidate, input);
|
|
36
37
|
}
|
|
37
38
|
catch (error) {
|
|
38
|
-
|
|
39
|
+
const message = `Cannot inspect workflow: ${error instanceof Error ? error.message : 'Invalid workflow'}`;
|
|
40
|
+
// The service's refusal keeps its type and code, so a host branches on it as it does for a run; either keeps its cause.
|
|
41
|
+
throw error instanceof ExtensionServiceError ? Object.assign(new ExtensionServiceError(error.code, message), { cause: error }) : new Error(message, { cause: error });
|
|
39
42
|
}
|
|
40
43
|
return { inspection, tree: formatWorkflowTree(inspection) };
|
|
41
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parcha/agentrun-pi",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.26",
|
|
4
4
|
"description": "Pi extension for building, running and evaluating AgentRun workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
51
51
|
"@earendil-works/pi-durable": "1.1.0",
|
|
52
52
|
"@earendil-works/pi-tui": "1.1.0",
|
|
53
|
-
"@parcha/pi-durable-disk": "0.1.0-beta.
|
|
53
|
+
"@parcha/pi-durable-disk": "0.1.0-beta.26",
|
|
54
54
|
"typebox": "1.3.7"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@parcha/agentrun-dsl": "0.1.0-beta.
|
|
58
|
-
"@parcha/agentrun-jev": "0.1.0-beta.
|
|
57
|
+
"@parcha/agentrun-dsl": "0.1.0-beta.26",
|
|
58
|
+
"@parcha/agentrun-jev": "0.1.0-beta.26"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@earendil-works/pi-agent-core": "*",
|