@parcha/agentrun-pi 0.1.0-beta.14 → 0.1.0-beta.16
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 +40 -2
- package/dist/durable/index.d.ts +2 -1
- package/dist/durable/index.js +1 -0
- package/dist/durable/node.js +27 -10
- package/dist/durable/readers.d.ts +4 -1
- package/dist/durable/readers.js +44 -5
- package/dist/durable/receipts.d.ts +4 -2
- package/dist/durable/receipts.js +3 -2
- package/dist/durable/store.js +57 -9
- package/dist/durable/task.d.ts +6 -2
- package/dist/durable/task.js +18 -2
- package/dist/durable/workflow-tool.d.ts +32 -0
- package/dist/durable/workflow-tool.js +188 -0
- package/dist/extension.js +64 -32
- package/dist/host-addendum.js +3 -2
- package/dist/pi-host.d.ts +7 -0
- package/dist/pi-host.js +22 -0
- 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 +3 -2
- package/dist/skills/author/references/language.md +4 -3
- package/dist/skills/author/references/sdk-authoring.md +2 -0
- package/dist/workflow-inspector.d.ts +1 -1
- package/dist/workflow-inspector.js +2 -2
- package/dist/workflow-store.d.ts +6 -0
- package/dist/workflow-store.js +6 -0
- package/dist/workflow-tool.d.ts +81 -0
- package/dist/workflow-tool.js +94 -0
- package/dist/workflow-view.js +5 -1
- package/dist/workflows.d.ts +1 -0
- package/dist/workflows.js +3 -0
- package/package.json +16 -4
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// The agentrun tool for a pi-durable session. describe, inspect, dry-run, save, load and list do what the coding-agent
|
|
2
|
+
// extension's tool does, through the same module (../workflow-tool.ts); run starts the workflow as one background task
|
|
3
|
+
// of the calling conversation through createWorkflowExtension, from the tool's own invocation, and answers with its
|
|
4
|
+
// outcome, writing the run's progress to the tool's running output as it goes. The draft a conversation inspected is a
|
|
5
|
+
// document of the session, so a reopened session keeps it. Code nodes run only through the host's code executor.
|
|
6
|
+
import { defineDocFamily, defineExtension, defineTool } from "@earendil-works/pi-durable";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
import { inspectWorkflow, WORKFLOW_NODE_KINDS } from "@parcha/agentrun-dsl";
|
|
9
|
+
import { canonicalHash } from "@parcha/agentrun-dsl/recovery";
|
|
10
|
+
import { extensionStructuralLimits, WorkflowExtensionService } from "../extension-service.js";
|
|
11
|
+
import { modelJson } from "../presentation.js";
|
|
12
|
+
import { WorkflowStore } from "../workflow-store.js";
|
|
13
|
+
import { progressLines, WorkflowObservation } from "../workflow-view.js";
|
|
14
|
+
import { authoringReferences, describeTools, dryRunDraft, inspectCandidate, workflowToolProperties } from "../workflow-tool.js";
|
|
15
|
+
import { toolResult } from "./pi-versions.js";
|
|
16
|
+
import { createWorkflowExtension } from "./task.js";
|
|
17
|
+
/** What a pi-durable session adds to the workflow language, rendered for describe's `authoring.host`. */
|
|
18
|
+
export const DURABLE_HOST_ADDENDUM = Object.freeze({
|
|
19
|
+
name: "pi-durable session",
|
|
20
|
+
nodeKinds: WORKFLOW_NODE_KINDS.filter((kind) => kind !== "artifact"),
|
|
21
|
+
rules: [
|
|
22
|
+
"Call agentrun with action describe first, then inspect the complete workflow with its input, then dry-run it (action dry-run, without a workflow argument), then run without a workflow argument.",
|
|
23
|
+
"A dry run runs the inspected workflow on synthetic values, or the input you give it, with no model, Jev or tool call. Its status is passed, not_run (a check could not run; skipped says why, and it proves nothing) or failed (problems say why). Report not_run as not run, never as passed.",
|
|
24
|
+
"Use exactly the tools describe returns, as a call's tool and as an agent node's tools: the host registers them, and inspect refuses any other, naming it. A call uses via tool with one of them. Shell and executor calls and artifact delivery are unavailable.",
|
|
25
|
+
"Code nodes run only through the host's code executor. Without one, a dry run of a workflow with code is not_run and a run of it is refused.",
|
|
26
|
+
"A run is a pi-durable task of this conversation: it continues after the session reopens, and the run action answers with its outcome once it ends. Running the same draft on the same input again reaches that run: a live one is waited for, a finished one answers with its outcome, and its effects are not made again. To run the same draft on the same input anew, pass again: true; a later run without it reaches the newest one.",
|
|
27
|
+
"Save keeps a named revision of the inspected definition, not its input. Load replaces the draft with a saved revision and checks it against this host's tools. List shows the saved revisions.",
|
|
28
|
+
"The structural limits describe returns bound node count, map concurrency and parallel branches.",
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
const DraftDoc = defineDocFamily({ kind: "agentrun.draft", version: 1, scope: "session", family: true, initial: () => ({ workflow: null, input: {} }) });
|
|
32
|
+
/** How many times `again` asked for a fresh run of one draft and input in a conversation: the newest run's number. */
|
|
33
|
+
const RunsDoc = defineDocFamily({ kind: "agentrun.runs", version: 1, scope: "session", family: true, initial: () => ({ generation: 0 }) });
|
|
34
|
+
const json = (value) => (value === undefined ? null : JSON.parse(JSON.stringify(value)));
|
|
35
|
+
const answer = (value) => toolResult([{ type: "text", text: modelJson(value) }], { details: json(value) });
|
|
36
|
+
/** The tool, and the workflow task its runs are: install `extension` (it carries both) and list the tool's name in the
|
|
37
|
+
* agent definition. `workflow` reads and stops runs from the host's side. */
|
|
38
|
+
export function createWorkflowTool(host) {
|
|
39
|
+
const names = (host.tools ?? []).map((tool) => tool.name);
|
|
40
|
+
const checker = () => new WorkflowExtensionService({ allowedTools: names });
|
|
41
|
+
/** Inspect's checks, after naming any tool the workflow uses (a call's, or an agent node's) that the host did not
|
|
42
|
+
* register: the host decides what a drafted workflow may use. */
|
|
43
|
+
const admit = (candidate, input) => {
|
|
44
|
+
let used = [];
|
|
45
|
+
try {
|
|
46
|
+
used = inspectWorkflow(candidate).requires.tools;
|
|
47
|
+
}
|
|
48
|
+
catch { /* inspect names what is wrong */ }
|
|
49
|
+
const missing = used.filter((name) => !names.includes(name));
|
|
50
|
+
if (missing.length)
|
|
51
|
+
throw new Error(`Cannot inspect workflow: it uses ${missing.length === 1 ? "a tool" : "tools"} this session's host has not registered: ${missing.map((name) => JSON.stringify(name)).join(", ")}. A workflow may call, or give an agent node, only the tools describe lists; a host registers them in createWorkflowTool's tools.`);
|
|
52
|
+
return inspectCandidate(checker(), candidate, input);
|
|
53
|
+
};
|
|
54
|
+
const store = () => new WorkflowStore(host.projectDir);
|
|
55
|
+
// A run's events reach the tool call that started it, while both live in this process.
|
|
56
|
+
const listeners = new Map();
|
|
57
|
+
const listen = (key, listener) => {
|
|
58
|
+
const set = listeners.get(key) ?? new Set();
|
|
59
|
+
listeners.set(key, set.add(listener));
|
|
60
|
+
return () => { set.delete(listener); if (!set.size)
|
|
61
|
+
listeners.delete(key); };
|
|
62
|
+
};
|
|
63
|
+
const given = { ...(host.runCode ? { runCode: host.runCode } : {}), ...(host.sop === undefined ? {} : { sop: host.sop }) };
|
|
64
|
+
const workflow = createWorkflowExtension({
|
|
65
|
+
...host.task,
|
|
66
|
+
run: async (invocation) => {
|
|
67
|
+
const parts = await host.task.run(invocation);
|
|
68
|
+
// The host's own observer's result goes back to the interpreter, which consumes a rejected one; a progress listener
|
|
69
|
+
// that fails (its call ended, say) changes nothing in the run.
|
|
70
|
+
const tee = (own) => (event) => {
|
|
71
|
+
for (const listener of listeners.get(invocation.key) ?? []) {
|
|
72
|
+
try {
|
|
73
|
+
listener(event);
|
|
74
|
+
}
|
|
75
|
+
catch { /* progress only */ }
|
|
76
|
+
}
|
|
77
|
+
return own?.(event);
|
|
78
|
+
};
|
|
79
|
+
// The executor and SOP text the host gave the tool are the run's, as they are its dry run's.
|
|
80
|
+
return "deps" in parts
|
|
81
|
+
? { ...parts, deps: { ...parts.deps, ...given, onEvent: tee(parts.deps.onEvent) } }
|
|
82
|
+
: { ...parts, adapters: { ...parts.adapters, ...given, onEvent: tee(parts.adapters.onEvent) } };
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const tool = defineTool({
|
|
86
|
+
name: host.name ?? "agentrun",
|
|
87
|
+
description: "Compose and run AgentRun workflows in this session. Describe lists the tools a workflow may use, as a call's tool or an agent node's tools, and the language; a workflow uses only the tools describe lists, which the host registers. Inspect validates a complete workflow and keeps it as this conversation's draft. Dry-run checks the draft on synthetic values (or the given input) with no model, Jev or tool call. Run starts the draft as a durable task of this conversation and answers with its outcome. Running the same draft on the same input again, after a restart too, reaches the same run and makes no effect twice; again: true starts a fresh run of them, which a later run then reaches. Save, load and list keep named definition revisions.",
|
|
88
|
+
parameters: Type.Object({ ...workflowToolProperties(), again: Type.Optional(Type.Boolean({ description: "Run only: start a fresh run of the same draft and input instead of reaching the one already run." })) }),
|
|
89
|
+
executionMode: "sequential",
|
|
90
|
+
async execute(given, api, context) {
|
|
91
|
+
const args = given;
|
|
92
|
+
const key = String(api.conversationId);
|
|
93
|
+
const held = async () => {
|
|
94
|
+
const draft = await api.snapshot(DraftDoc, key, context);
|
|
95
|
+
return draft && draft.workflow !== null ? draft : undefined;
|
|
96
|
+
};
|
|
97
|
+
const keep = (candidate, input) => api.commit(async (tx) => {
|
|
98
|
+
const draft = await tx.doc(DraftDoc, key, null);
|
|
99
|
+
draft.workflow = json(candidate);
|
|
100
|
+
draft.input = json(input);
|
|
101
|
+
return undefined;
|
|
102
|
+
}, context);
|
|
103
|
+
const needDraft = async (action) => {
|
|
104
|
+
if (args.workflow !== undefined)
|
|
105
|
+
throw new Error(`Inspect the workflow first with action inspect, then ${action} without a workflow argument.`);
|
|
106
|
+
const draft = await held();
|
|
107
|
+
if (!draft)
|
|
108
|
+
throw new Error(`Inspect a workflow first, then ${action} it.`);
|
|
109
|
+
return draft;
|
|
110
|
+
};
|
|
111
|
+
switch (args.action) {
|
|
112
|
+
case "describe":
|
|
113
|
+
return answer({ tools: describeTools(host.tools ?? []), structuralLimits: { ...extensionStructuralLimits }, authoring: authoringReferences(DURABLE_HOST_ADDENDUM) });
|
|
114
|
+
case "inspect": {
|
|
115
|
+
const draft = await held();
|
|
116
|
+
const candidate = args.workflow ?? draft?.workflow;
|
|
117
|
+
if (candidate === undefined)
|
|
118
|
+
throw new Error("No workflow yet. Pass the complete workflow to inspect.");
|
|
119
|
+
// Without input, the input is not checked, as the coding-agent tool's inspect does; the draft keeps none.
|
|
120
|
+
const input = args.input ?? (args.workflow === undefined ? draft?.input : undefined);
|
|
121
|
+
const { tree } = admit(candidate, input);
|
|
122
|
+
await keep(candidate, input ?? {});
|
|
123
|
+
return answer({ preview: tree, workflow: candidate, input: input ?? {} });
|
|
124
|
+
}
|
|
125
|
+
case "dry-run": {
|
|
126
|
+
const draft = await needDraft("dry-run");
|
|
127
|
+
const dryRun = await dryRunDraft(draft.workflow, {
|
|
128
|
+
...(args.input === undefined ? {} : { input: args.input }), ...(host.sop === undefined ? {} : { sop: host.sop }),
|
|
129
|
+
...(host.runCode ? { runCode: host.runCode } : {}),
|
|
130
|
+
});
|
|
131
|
+
return answer({ dryRun });
|
|
132
|
+
}
|
|
133
|
+
case "save": {
|
|
134
|
+
const draft = await needDraft("save");
|
|
135
|
+
return answer(await store().save(args.name ?? "", draft.workflow));
|
|
136
|
+
}
|
|
137
|
+
case "list":
|
|
138
|
+
return answer({ workflows: await store().list() });
|
|
139
|
+
case "load": {
|
|
140
|
+
const saved = await store().load(args.name ?? "", args.digest);
|
|
141
|
+
const { tree } = admit(saved.workflow, args.input);
|
|
142
|
+
await keep(saved.workflow, args.input ?? {});
|
|
143
|
+
return answer({ preview: tree, workflow: saved.workflow, input: args.input ?? {}, saved: { name: saved.name, digest: saved.digest } });
|
|
144
|
+
}
|
|
145
|
+
case "run": {
|
|
146
|
+
const draft = await needDraft("run");
|
|
147
|
+
const input = args.input ?? draft.input;
|
|
148
|
+
const { inspection } = admit(draft.workflow, input);
|
|
149
|
+
const code = inspection.nodes.filter((node) => node.kind === "code").map((node) => JSON.stringify(node.label));
|
|
150
|
+
if (code.length && !host.runCode)
|
|
151
|
+
throw new Error(`Code node${code.length === 1 ? "" : "s"} ${code.join(", ")}: this host runs code only through its own executor and gave none, so the workflow does not run here. Nothing ran.`);
|
|
152
|
+
if (inspection.requires.sopSections.length && host.sop === undefined)
|
|
153
|
+
throw new Error(`The workflow needs SOP text for ${inspection.requires.sopSections.map((name) => JSON.stringify(name)).join(", ")}, and this host supplies none. Nothing ran.`);
|
|
154
|
+
// One run per draft and input in a conversation: a run asked for again (after a restart, say) reaches the same
|
|
155
|
+
// task, so its effects happen once. A live run is attached to, and a finished one answers with its outcome.
|
|
156
|
+
// `again` starts the next one; the count is a session document, so a later run, after a restart too, reaches it.
|
|
157
|
+
const base = `agentrun:${api.conversationId}:${inspection.sha256}:${canonicalHash(input)}`;
|
|
158
|
+
const generation = args.again
|
|
159
|
+
? await api.commit(async (tx) => { const runs = await tx.doc(RunsDoc, base, null); runs.generation += 1; return runs.generation; }, context)
|
|
160
|
+
: (await api.snapshot(RunsDoc, base, context))?.generation ?? 0;
|
|
161
|
+
const runKey = generation ? `${base}:${generation}` : base;
|
|
162
|
+
const observation = new WorkflowObservation();
|
|
163
|
+
let shown = "";
|
|
164
|
+
const stop = listen(runKey, (event) => {
|
|
165
|
+
observation.record(event);
|
|
166
|
+
const text = progressLines(observation.data).join(" · ");
|
|
167
|
+
if (text !== shown) {
|
|
168
|
+
shown = text;
|
|
169
|
+
api.output(`${text}\n`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
const run = await workflow.start(api, { key: runKey, workflow: draft.workflow, input }, context, { conversationId: Number(api.conversationId), background: true });
|
|
174
|
+
const outcome = await run.wait(context);
|
|
175
|
+
// Only this call's events are counted: a run that had ended sends none, and one joined after a restart sent some before.
|
|
176
|
+
const progress = run.started === "settled" ? ["The run had ended; its progress is not kept, and its outcome is above."]
|
|
177
|
+
: run.started === "attached" ? ["Steps before this call are not counted.", ...progressLines(observation.data)] : progressLines(observation.data);
|
|
178
|
+
return answer({ run: { key: runKey, taskId: run.taskId, started: run.started }, outcome, progress });
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
stop();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
return { extension: defineExtension({ name: "agentrun.tool", tools: [tool], tasks: workflow.extension.tasks }), workflow };
|
|
188
|
+
}
|
package/dist/extension.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { Type } from 'typebox';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
3
2
|
import { existsSync } from 'node:fs';
|
|
4
3
|
import { Compile } from 'typebox/compile';
|
|
5
4
|
import { Text } from '@earendil-works/pi-tui';
|
|
6
5
|
import { createReadToolDefinition, createBashToolDefinition, createEditToolDefinition, createWriteToolDefinition, createGrepToolDefinition, createFindToolDefinition, createLsToolDefinition, VERSION as PI_VERSION, } from '@earendil-works/pi-coding-agent';
|
|
7
|
-
import { formatWorkflowTree, inspectWorkflow, AUTHOR_SKILL_NAME
|
|
6
|
+
import { formatWorkflowTree, inspectWorkflow, AUTHOR_SKILL_NAME } from '@parcha/agentrun-dsl';
|
|
8
7
|
import { createJevRunner } from '@parcha/agentrun-jev';
|
|
9
8
|
import { createPiHostRunner, PI_MODEL_SETUP_MESSAGE } from './host-session.js';
|
|
10
|
-
import {
|
|
9
|
+
import { piHostLine } from './pi-host.js';
|
|
11
10
|
import { workflowToolContext } from './tool-context.js';
|
|
12
11
|
import { WorkflowExtensionService, ExtensionServiceError, extensionStructuralLimits } from './extension-service.js';
|
|
13
12
|
import { demoInput, demoSearchTool, demoWorkflow, scriptedDemoDeps } from './demo.js';
|
|
@@ -20,6 +19,7 @@ import { readSopFile, missingSopSections, sopDigest } from './sop.js';
|
|
|
20
19
|
import { WorkflowStore, WorkflowStoreError } from './workflow-store.js';
|
|
21
20
|
import { WorkflowObservation, workflowView, formatWorkflowView, progressLines, readable } from './workflow-view.js';
|
|
22
21
|
import { showWorkflowInspector } from './workflow-inspector.js';
|
|
22
|
+
import { authoringReferences, describeTools, dryRunDraft, formatDryRun, inspectCandidate, workflowToolParameters } from './workflow-tool.js';
|
|
23
23
|
import { supportTriageWorkflow, supportTriageInputs, supportTriageTools, scriptedSupportTriageDeps } from './triage-demo.js';
|
|
24
24
|
import { restoreWorkflowSession, workflowRunHistory } from './workflow-session.js';
|
|
25
25
|
const constructors = {
|
|
@@ -27,8 +27,7 @@ const constructors = {
|
|
|
27
27
|
write: createWriteToolDefinition, grep: createGrepToolDefinition, find: createFindToolDefinition, ls: createLsToolDefinition,
|
|
28
28
|
};
|
|
29
29
|
const mutatingTools = new Set(['bash', 'edit', 'write']);
|
|
30
|
-
const
|
|
31
|
-
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 · 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';
|
|
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';
|
|
32
31
|
const textResult = (text, details = {}) => ({ content: [{ type: 'text', text: safe(text) }], details });
|
|
33
32
|
function singleTextInput(workflow) {
|
|
34
33
|
const source = workflow;
|
|
@@ -177,19 +176,10 @@ function registerExtension(pi, configuration) {
|
|
|
177
176
|
const candidate = workflow === undefined ? s.draft : workflow;
|
|
178
177
|
if (candidate === undefined)
|
|
179
178
|
throw new Error('No workflow yet. Describe a task with /agentrun <task>.');
|
|
180
|
-
if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
181
|
-
throw new Error('Cannot inspect workflow: workflow must be a JSON object, not a JSON-encoded string or array. Pass the complete object directly.');
|
|
182
|
-
}
|
|
183
179
|
// Include active tools available only with operator trust, without granting that trust.
|
|
184
180
|
// Execution rechecks the actual per-run permissions and captures implementations.
|
|
185
181
|
const checker = new WorkflowExtensionService({ allowedTools: toolSet(ctx, true).map(tool => tool.name), limits: serviceLimits });
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
inspection = checker.preflight(candidate, input);
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
throw new Error(`Cannot inspect workflow: ${error instanceof Error ? error.message : 'Invalid workflow'}`);
|
|
192
|
-
}
|
|
182
|
+
const { inspection, tree } = inspectCandidate(checker, candidate, input);
|
|
193
183
|
// Stage the definition and input together only after nonexecuting checks succeed.
|
|
194
184
|
const nextDraft = workflow === undefined ? s.draft : structuredClone(workflow);
|
|
195
185
|
const changed = !s.draft || inspectWorkflow(s.draft).sha256 !== inspection.sha256;
|
|
@@ -210,7 +200,7 @@ function registerExtension(pi, configuration) {
|
|
|
210
200
|
s.input = nextInput;
|
|
211
201
|
if (workflow !== undefined || input !== undefined)
|
|
212
202
|
persist(s);
|
|
213
|
-
return { inspection, tree
|
|
203
|
+
return { inspection, tree };
|
|
214
204
|
};
|
|
215
205
|
const view = (ctx, s) => {
|
|
216
206
|
const result = workflowView(s.draft, { input: s.input,
|
|
@@ -441,6 +431,35 @@ function registerExtension(pi, configuration) {
|
|
|
441
431
|
ctx.ui.setWidget('agentrun', undefined);
|
|
442
432
|
}
|
|
443
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
|
+
};
|
|
444
463
|
const launch = async (ctx, s, input, trusted = false) => {
|
|
445
464
|
if (s.busy)
|
|
446
465
|
throw new Error('A workflow is already running. Use /agentrun stop first.');
|
|
@@ -459,18 +478,15 @@ function registerExtension(pi, configuration) {
|
|
|
459
478
|
};
|
|
460
479
|
pi.registerTool({
|
|
461
480
|
name: 'agentrun', label: 'AgentRun workflow',
|
|
462
|
-
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. Agent nodes capture the active Pi model when execution starts. Code needs the user command /agentrun run --trusted for each run. ' + (configuration.hostTools
|
|
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
|
|
463
482
|
? 'Only the explicit host-configured tools are available, including during trusted runs. Outer permission hooks are not inherited. '
|
|
464
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.',
|
|
465
|
-
parameters:
|
|
466
|
-
name: Type.Optional(Type.String({ description: 'Saved procedure name: lowercase letters, digits, hyphens or underscores.' })),
|
|
467
|
-
digest: Type.Optional(Type.String({ description: 'Optional exact saved SHA-256 revision to load.' })),
|
|
468
|
-
workflow: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
469
|
-
description: 'The complete AgentRun workflow JSON object. Pass the object directly, not a JSON-encoded string. Inspection validates its DSL structure.',
|
|
470
|
-
})), input: Type.Optional(Type.Record(Type.String(), Type.Unknown())) }),
|
|
484
|
+
parameters: workflowToolParameters(),
|
|
471
485
|
executionMode: 'sequential',
|
|
472
486
|
renderResult(result, options) {
|
|
473
487
|
const details = result.details;
|
|
488
|
+
if (details?.dryRun)
|
|
489
|
+
return new Text(safe(formatDryRun(details.dryRun)), 0, 0);
|
|
474
490
|
const text = details?.status && details.calls ? options?.expanded
|
|
475
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')
|
|
476
492
|
: details?.view ? formatWorkflowView(details.view) : result.content.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
@@ -507,13 +523,16 @@ function registerExtension(pi, configuration) {
|
|
|
507
523
|
return textResult(modelJson({ preview: formatWorkflowView(preview), workflow: s.draft, input: s.input }), { ...result, saved, view: preview });
|
|
508
524
|
}
|
|
509
525
|
if (args.action === 'describe') {
|
|
510
|
-
const details = { tools: toolSet(ctx)
|
|
511
|
-
|
|
512
|
-
resultContract: t.resultSchema ? 'Host-declared direct workflow call result.' : 'Pi result envelope; tool-specific details are not declared. Do not invent fields inside details.',
|
|
513
|
-
})),
|
|
514
|
-
limits: { ...limits }, structuralLimits: { ...extensionStructuralLimits }, ...readiness(ctx, s), authoring: { language: loadAuthorReference('language'), workflow: loadAuthorReference('workflow-format'), jev: loadAuthorReference('jev-decisions'), host: renderAuthorHostAddendum(PI_HOST_ADDENDUM) } };
|
|
526
|
+
const details = { tools: describeTools(toolSet(ctx)),
|
|
527
|
+
limits: { ...limits }, structuralLimits: { ...extensionStructuralLimits }, ...readiness(ctx, s), authoring: authoringReferences() };
|
|
515
528
|
return textResult(JSON.stringify(details, null, 2), details);
|
|
516
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
|
+
}
|
|
517
536
|
if (args.action === 'run' && args.workflow !== undefined)
|
|
518
537
|
throw new Error('Inspect the workflow first with action inspect, then run without a workflow argument.');
|
|
519
538
|
if (args.action === 'inspect') {
|
|
@@ -564,9 +583,9 @@ function registerExtension(pi, configuration) {
|
|
|
564
583
|
else
|
|
565
584
|
raw = action;
|
|
566
585
|
}
|
|
567
|
-
const command = /^(help|status|stop|demo|triage|run|inspect)(?:\s|$)/i.test(raw) ? raw.toLowerCase().replace(/\s+/g, ' ') : raw;
|
|
586
|
+
const command = /^(help|status|stop|demo|triage|run|dry-run|inspect)(?:\s|$)/i.test(raw) ? raw.toLowerCase().replace(/\s+/g, ' ') : raw;
|
|
568
587
|
const words = command.split(/\s+/);
|
|
569
|
-
const commandLike = /^(help|status|stop|demo|run)(?:-|$)/i.test(words[0])
|
|
588
|
+
const commandLike = /^(help|status|stop|demo|run|dry-run)(?:-|$)/i.test(words[0])
|
|
570
589
|
&& words.slice(1).every(word => word.startsWith('-') || ['live', 'empty'].includes(word));
|
|
571
590
|
if (command === 'help' || command === '--help' || command === '-h') {
|
|
572
591
|
show(s, help);
|
|
@@ -699,13 +718,15 @@ function registerExtension(pi, configuration) {
|
|
|
699
718
|
}
|
|
700
719
|
else if (command === 'status') {
|
|
701
720
|
const ready = readiness(ctx, s);
|
|
702
|
-
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:\
|
|
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);
|
|
703
722
|
}
|
|
704
723
|
else if (command === 'stop') {
|
|
724
|
+
const dry = s.dryRun;
|
|
725
|
+
dry?.abort();
|
|
705
726
|
s.pause?.();
|
|
706
727
|
s.controller?.abort();
|
|
707
728
|
s.service.stop();
|
|
708
|
-
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.');
|
|
709
730
|
}
|
|
710
731
|
else if (command === 'demo' || command === 'demo live' || command === 'demo empty') {
|
|
711
732
|
inspect(ctx, s, demoWorkflow);
|
|
@@ -716,6 +737,16 @@ function registerExtension(pi, configuration) {
|
|
|
716
737
|
else if (command === 'run' || command === 'run --trusted') {
|
|
717
738
|
await launch(ctx, s, s.input, command === 'run --trusted');
|
|
718
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
|
+
}
|
|
719
750
|
else if (commandLike || command.startsWith('-')) {
|
|
720
751
|
show(s, `Unknown AgentRun command: ${raw}\n\n${help}`);
|
|
721
752
|
}
|
|
@@ -741,6 +772,7 @@ function registerExtension(pi, configuration) {
|
|
|
741
772
|
const old = current, wasRunning = old.busy;
|
|
742
773
|
old.clearStatus();
|
|
743
774
|
old.closed = true;
|
|
775
|
+
old.dryRun?.abort();
|
|
744
776
|
old.pause?.();
|
|
745
777
|
old.controller?.abort();
|
|
746
778
|
await old.service.dispose();
|
package/dist/host-addendum.js
CHANGED
|
@@ -5,9 +5,10 @@ export const PI_HOST_ADDENDUM = Object.freeze({
|
|
|
5
5
|
name: 'Pi extension',
|
|
6
6
|
nodeKinds: WORKFLOW_NODE_KINDS.filter(kind => kind !== 'artifact'),
|
|
7
7
|
rules: [
|
|
8
|
-
'Call agentrun with action describe first, then inspect the complete workflow with its input, then run without a workflow argument.',
|
|
8
|
+
'Call agentrun with action describe first, then inspect the complete workflow with its input, then dry-run it (action dry-run, without a workflow argument), then run without a workflow argument.',
|
|
9
|
+
'A dry run runs the inspected workflow on synthetic values, or the input you give it, with no model, Jev or tool call. Its status is passed, not_run (a check could not run; skipped says why, and it proves nothing) or failed (problems say why). Report not_run as not run, never as passed.',
|
|
9
10
|
'Use exactly the tools describe returns. Without host-configured tools these are the active read-only built-ins (read, grep, find, ls), and bash, edit and write exist only in a trusted run; a host that configures its tools offers only those, including in trusted runs. The search tool reads fictional demo sources only.',
|
|
10
|
-
'Code nodes and mutating tools run only after the user issues /agentrun run --trusted for that run. Trusted execution is local and unsandboxed.',
|
|
11
|
+
'Code nodes and mutating tools run only after the user issues /agentrun run --trusted for that run. Trusted execution is local and unsandboxed. A dry run of a workflow with code nodes is not_run until the user issues /agentrun dry-run --trusted.',
|
|
11
12
|
'A call uses via tool with a tool describe returns. Shell and executor calls and artifact delivery are unavailable.',
|
|
12
13
|
'This extension supplies no SOP text: do not run a workflow with sopSection here; it needs an SDK host that supplies the authoritative SOP.',
|
|
13
14
|
'Custom extension tools and outer permission hooks are not inherited, including in trusted runs.',
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SUPPORTED_PI_RANGE: string;
|
|
2
|
+
export declare const NEWEST_TESTED_PI_VERSION = "1.3.0";
|
|
3
|
+
/** Whether Pi `version` is in the supported range. As npm reads the range, a prerelease is not; nor is a version that
|
|
4
|
+
* is not `x.y.z`. */
|
|
5
|
+
export declare function isSupportedPi(version: string): boolean;
|
|
6
|
+
/** The status line for Pi host `version`: the supported range, and for a host outside it the newest tested one. */
|
|
7
|
+
export declare function piHostLine(version: string): string;
|
package/dist/pi-host.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The Pi hosts this release supports: the range of its pi-durable and chord peers. `/agentrun status` flags a host
|
|
2
|
+
// outside it, without blocking it, and names the newest version CI runs (the `pi-1-2` job) as the one to install.
|
|
3
|
+
const FLOOR = [1, 0, 4];
|
|
4
|
+
const CEILING = [1, 4, 0];
|
|
5
|
+
export const SUPPORTED_PI_RANGE = `>=${FLOOR.join(".")} <${CEILING.join(".")}`;
|
|
6
|
+
export const NEWEST_TESTED_PI_VERSION = "1.3.0";
|
|
7
|
+
const compare = (a, b) => (a[0] - b[0]) || (a[1] - b[1]) || (a[2] - b[2]);
|
|
8
|
+
/** Whether Pi `version` is in the supported range. As npm reads the range, a prerelease is not; nor is a version that
|
|
9
|
+
* is not `x.y.z`. */
|
|
10
|
+
export function isSupportedPi(version) {
|
|
11
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
12
|
+
if (!match)
|
|
13
|
+
return false;
|
|
14
|
+
const parsed = match.slice(1).map(Number);
|
|
15
|
+
return compare(parsed, FLOOR) >= 0 && compare(parsed, CEILING) < 0;
|
|
16
|
+
}
|
|
17
|
+
/** The status line for Pi host `version`: the supported range, and for a host outside it the newest tested one. */
|
|
18
|
+
export function piHostLine(version) {
|
|
19
|
+
return isSupportedPi(version)
|
|
20
|
+
? `Pi host: ${version} (supported: ${SUPPORTED_PI_RANGE})`
|
|
21
|
+
: `Pi host: ${version} (outside the supported range ${SUPPORTED_PI_RANGE}; the newest tested host: npm install -g @earendil-works/pi-coding-agent@${NEWEST_TESTED_PI_VERSION})`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { checkKeys, checkSetupPrompt, JEV_KEYS } from "./spec.js";
|
|
2
|
+
export type { CheckOptions, Choice, DemoSpec, EyesNeed, Key, KeyReport, ModelNeed, Package, Platform, Program, ReportItem, RunMode, SetupPromptReport } from "./spec.js";
|
|
3
|
+
export { AGENT_NAME, CHATGPT_PROVIDER, consoleInteraction, signIn, SignInError } from "./signin.js";
|
|
4
|
+
export type { ConsoleIo, SignedIn, SignInOptions } from "./signin.js";
|
|
5
|
+
export { CHATGPT_UNSUPPORTED, resolveModel, ResolveError } from "./resolve.js";
|
|
6
|
+
export type { EyesModel, ResolvedModel, ResolveOptions } from "./resolve.js";
|
|
7
|
+
export { USAGE_LIMIT_CODE, UsageLimitError, usageLimitError } from "./limit.js";
|
|
8
|
+
export { agentrunHome, AuthStoreError, FileCredentialStore, fileCredentialStore } from "./store.js";
|
|
9
|
+
export type { FileCredentialStoreOptions } from "./store.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Sign in, pick a model, check keys: the shared piece every demo runs first. The only entry of this package that
|
|
2
|
+
// reaches pi-ai, an optional peer; every other entry stays free of it.
|
|
3
|
+
export { checkKeys, checkSetupPrompt, JEV_KEYS } from "./spec.js";
|
|
4
|
+
export { AGENT_NAME, CHATGPT_PROVIDER, consoleInteraction, signIn, SignInError } from "./signin.js";
|
|
5
|
+
export { CHATGPT_UNSUPPORTED, resolveModel, ResolveError } from "./resolve.js";
|
|
6
|
+
export { USAGE_LIMIT_CODE, UsageLimitError, usageLimitError } from "./limit.js";
|
|
7
|
+
export { agentrunHome, AuthStoreError, FileCredentialStore, fileCredentialStore } from "./store.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const USAGE_LIMIT_CODE = "subscription_sharing_usage_limit_exceeded";
|
|
2
|
+
export declare class UsageLimitError extends Error {
|
|
3
|
+
readonly code = "USAGE_LIMIT";
|
|
4
|
+
constructor(options?: {
|
|
5
|
+
cause?: unknown;
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
/** The typed limit error when `value` (an error, an assistant message from a failed request, or its text) is the plan's
|
|
9
|
+
* usage limit; undefined otherwise. */
|
|
10
|
+
export declare function usageLimitError(value: unknown): UsageLimitError | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The ChatGPT plan's usage limit (five hours on Plus, shared with every other app the user signed in to). OpenAI answers
|
|
2
|
+
// a request past it with 429 `subscription_sharing_usage_limit_exceeded`, at the start or mid-stream. It resets after
|
|
3
|
+
// hours, not seconds, so it is never retried: pi-ai sends a request once by default and classes the code as not
|
|
4
|
+
// retryable, and this maps it to one typed error.
|
|
5
|
+
export const USAGE_LIMIT_CODE = "subscription_sharing_usage_limit_exceeded";
|
|
6
|
+
export class UsageLimitError extends Error {
|
|
7
|
+
code = "USAGE_LIMIT";
|
|
8
|
+
constructor(options) {
|
|
9
|
+
super("Your ChatGPT plan's usage limit is reached; wait, or set a provider key.", options);
|
|
10
|
+
this.name = "UsageLimitError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** The typed limit error when `value` (an error, an assistant message from a failed request, or its text) is the plan's
|
|
14
|
+
* usage limit; undefined otherwise. */
|
|
15
|
+
export function usageLimitError(value) {
|
|
16
|
+
if (value instanceof UsageLimitError)
|
|
17
|
+
return value;
|
|
18
|
+
const text = typeof value === "string" ? value
|
|
19
|
+
: value instanceof Error ? value.message
|
|
20
|
+
: value && typeof value === "object" && "errorMessage" in value ? String(value.errorMessage ?? "")
|
|
21
|
+
: "";
|
|
22
|
+
return text.includes(USAGE_LIMIT_CODE) ? new UsageLimitError({ cause: value }) : undefined;
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type SignInOptions } from "./signin.js";
|
|
3
|
+
import type { DemoSpec } from "./spec.js";
|
|
4
|
+
/** What Sign in with ChatGPT covers: streaming Responses calls with `store: false`, without these. */
|
|
5
|
+
export declare const CHATGPT_UNSUPPORTED: readonly string[];
|
|
6
|
+
export type ResolveOptions = Omit<SignInOptions, "home"> & {
|
|
7
|
+
provider?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
home?: string;
|
|
11
|
+
/** An explicit eyes model (`--eyes-model`); `model` defaults to the spec's eyes default. */
|
|
12
|
+
eyes?: {
|
|
13
|
+
provider: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
};
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
};
|
|
18
|
+
type Resolved = {
|
|
19
|
+
model: Model<Api>;
|
|
20
|
+
/** The key valid now. Not enumerable, and left out of `toJSON`. */
|
|
21
|
+
readonly apiKey: string;
|
|
22
|
+
/** A key valid at the time of the call: a sign-in's token is refreshed under the store's lock when it is near expiry. */
|
|
23
|
+
getApiKey(): Promise<string>;
|
|
24
|
+
/** Only streaming Responses calls with `store: false`, without `unsupported`. */
|
|
25
|
+
responsesOnly: boolean;
|
|
26
|
+
unsupported: readonly string[];
|
|
27
|
+
toJSON(): Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
export type ResolvedModel = Resolved & {
|
|
30
|
+
source: "flag" | "chatgpt";
|
|
31
|
+
limit?: string;
|
|
32
|
+
eyes?: EyesModel;
|
|
33
|
+
};
|
|
34
|
+
export type EyesModel = Resolved & {
|
|
35
|
+
source: "flag" | "lead";
|
|
36
|
+
};
|
|
37
|
+
export declare class ResolveError extends Error {
|
|
38
|
+
readonly code: "FLAGS" | "UNKNOWN_PROVIDER" | "UNKNOWN_MODEL" | "MISSING_KEY" | "NEEDS" | "EYES" | "NO_MODEL" | "MODEL_LIST" | "SIGNED_OUT";
|
|
39
|
+
constructor(code: "FLAGS" | "UNKNOWN_PROVIDER" | "UNKNOWN_MODEL" | "MISSING_KEY" | "NEEDS" | "EYES" | "NO_MODEL" | "MODEL_LIST" | "SIGNED_OUT", message: string);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The model this run uses and the key to send, in the order above. The eyes model, when the spec has one, resolves the
|
|
43
|
+
* same way: an explicit eyes model with its provider's key, else the lead's model when it meets the eyes' needs, else
|
|
44
|
+
* a `ResolveError` ("EYES") naming the gap. The ChatGPT plan takes no video input.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveModel(spec: DemoSpec, options?: ResolveOptions): Promise<ResolvedModel>;
|
|
47
|
+
export {};
|