stageflow 0.23.0 → 0.25.0

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.
Files changed (44) hide show
  1. package/README.md +15 -3
  2. package/dist/a2a/configDiscovery.d.ts +9 -0
  3. package/dist/a2a/configDiscovery.js +18 -0
  4. package/dist/a2a/contracts.d.ts +93 -0
  5. package/dist/a2a/contracts.js +163 -0
  6. package/dist/a2a/limits.d.ts +14 -0
  7. package/dist/a2a/limits.js +29 -0
  8. package/dist/a2a/registry.d.ts +44 -0
  9. package/dist/a2a/registry.js +159 -0
  10. package/dist/a2a/server.d.ts +29 -0
  11. package/dist/a2a/server.js +341 -0
  12. package/dist/a2a/service.d.ts +97 -0
  13. package/dist/a2a/service.js +496 -0
  14. package/dist/a2a/store.d.ts +106 -0
  15. package/dist/a2a/store.js +245 -0
  16. package/dist/cli/a2aCommand.d.ts +8 -0
  17. package/dist/cli/a2aCommand.js +131 -0
  18. package/dist/cli/runStageCommand.d.ts +33 -0
  19. package/dist/cli/runStageCommand.js +407 -0
  20. package/dist/cli.js +19 -0
  21. package/dist/mcp/catalogTools.js +139 -0
  22. package/dist/package-meta.d.ts +1 -1
  23. package/dist/package-meta.js +1 -1
  24. package/dist/runstore/createStore.d.ts +10 -0
  25. package/dist/runstore/createStore.js +13 -4
  26. package/dist/runstore/port.d.ts +14 -1
  27. package/dist/runstore/sqlite/SqliteRunStore.d.ts +9 -0
  28. package/dist/runstore/sqlite/SqliteRunStore.js +66 -25
  29. package/dist/runstore/sqlite/schema.d.ts +1 -1
  30. package/dist/runstore/sqlite/schema.js +6 -0
  31. package/dist/runstore/submission.d.ts +11 -0
  32. package/dist/runstore/submission.js +7 -0
  33. package/dist/runtime/pipelineRunner.d.ts +2 -0
  34. package/dist/runtime/pipelineRunner.js +3 -0
  35. package/dist/runtime/pipelineScheduler.js +32 -15
  36. package/dist/runtime/reloadRunCatalog.js +7 -1
  37. package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
  38. package/dist/runtime/resolveEnvelopeRef.js +79 -0
  39. package/dist/runtime/runManager.d.ts +22 -1
  40. package/dist/runtime/runManager.js +48 -4
  41. package/dist/server/bootstrap.d.ts +2 -0
  42. package/dist/server/bootstrap.js +14 -3
  43. package/dist/server/createHttpHost.js +6 -0
  44. package/package.json +2 -1
@@ -0,0 +1,245 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, unlink, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import Database from "better-sqlite3";
5
+ import { storeRootFor } from "../runstore/paths.js";
6
+ import { MESSAGE_TOMBSTONE_RETENTION_MS, TERMINAL_RETENTION_MS } from "./limits.js";
7
+ const SCHEMA_SQL = `
8
+ CREATE TABLE IF NOT EXISTS a2a_contexts (
9
+ context_id TEXT PRIMARY KEY,
10
+ caller_id TEXT NOT NULL,
11
+ created_at TEXT NOT NULL
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS a2a_tasks (
15
+ task_id TEXT PRIMARY KEY,
16
+ context_id TEXT NOT NULL,
17
+ caller_id TEXT NOT NULL,
18
+ publication_id TEXT NOT NULL,
19
+ publication_revision TEXT NOT NULL,
20
+ submission_key TEXT NOT NULL UNIQUE,
21
+ run_id TEXT,
22
+ state TEXT NOT NULL,
23
+ result_json TEXT,
24
+ kind TEXT NOT NULL DEFAULT 'invoke',
25
+ result_stage_id TEXT,
26
+ created_at TEXT NOT NULL,
27
+ updated_at TEXT NOT NULL
28
+ );
29
+ CREATE INDEX IF NOT EXISTS a2a_tasks_context ON a2a_tasks(context_id);
30
+ CREATE INDEX IF NOT EXISTS a2a_tasks_caller ON a2a_tasks(caller_id);
31
+
32
+ CREATE TABLE IF NOT EXISTS a2a_messages (
33
+ caller_id TEXT NOT NULL,
34
+ message_id TEXT NOT NULL,
35
+ task_id TEXT NOT NULL,
36
+ operation TEXT NOT NULL,
37
+ request_hash TEXT NOT NULL,
38
+ outcome_json TEXT,
39
+ created_at TEXT NOT NULL,
40
+ PRIMARY KEY (caller_id, message_id)
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS a2a_artifacts (
44
+ artifact_id TEXT PRIMARY KEY,
45
+ task_id TEXT NOT NULL,
46
+ name TEXT NOT NULL,
47
+ media_type TEXT,
48
+ size INTEGER NOT NULL,
49
+ content_path TEXT NOT NULL,
50
+ hash TEXT NOT NULL,
51
+ created_at TEXT NOT NULL
52
+ );
53
+ CREATE INDEX IF NOT EXISTS a2a_artifacts_task ON a2a_artifacts(task_id);
54
+ `;
55
+ function nowIso() {
56
+ return new Date().toISOString();
57
+ }
58
+ export class A2aStore {
59
+ db;
60
+ artifactsRoot;
61
+ ownsConnection;
62
+ /**
63
+ * `connection`, when passed, is the SqliteRunStore's own connection to the same `state.db` file
64
+ * (see `createRunStoreWithConnection`) so the two stores share one transaction domain. Falls
65
+ * back to opening its own connection when no shared one is available (e.g. a caller-supplied
66
+ * non-SQLite `RunStore` in bootstrap).
67
+ */
68
+ constructor(rootDir, connection) {
69
+ const storeRoot = storeRootFor(rootDir);
70
+ if (connection) {
71
+ this.db = connection;
72
+ this.ownsConnection = false;
73
+ }
74
+ else {
75
+ this.db = new Database(path.join(storeRoot, "state.db"));
76
+ this.db.pragma("journal_mode = WAL");
77
+ this.db.pragma("busy_timeout = 5000");
78
+ this.ownsConnection = true;
79
+ }
80
+ this.db.exec(SCHEMA_SQL);
81
+ this.migrateAddStandaloneColumns();
82
+ this.artifactsRoot = path.join(storeRoot, "a2a-artifacts");
83
+ }
84
+ /**
85
+ * Additive migration for a `state.db` created before the run_stage A2A
86
+ * operation existed: `CREATE TABLE IF NOT EXISTS` above never alters an
87
+ * already-existing a2a_tasks table, so a pre-existing one is missing these
88
+ * two columns. Same PRAGMA table_info-guarded ADD COLUMN pattern as
89
+ * src/runstore/sqlite/SqliteRunStore.ts.
90
+ */
91
+ migrateAddStandaloneColumns() {
92
+ const cols = this.db.prepare(`PRAGMA table_info(a2a_tasks)`).all();
93
+ if (!cols.some((c) => c.name === "kind")) {
94
+ this.db.exec(`ALTER TABLE a2a_tasks ADD COLUMN kind TEXT NOT NULL DEFAULT 'invoke'`);
95
+ }
96
+ if (!cols.some((c) => c.name === "result_stage_id")) {
97
+ this.db.exec(`ALTER TABLE a2a_tasks ADD COLUMN result_stage_id TEXT`);
98
+ }
99
+ }
100
+ close() {
101
+ if (this.ownsConnection)
102
+ this.db.close();
103
+ }
104
+ ensureContext(callerId, contextId) {
105
+ if (contextId) {
106
+ const row = this.db
107
+ .prepare("SELECT caller_id FROM a2a_contexts WHERE context_id = ?")
108
+ .get(contextId);
109
+ if (row && row.caller_id !== callerId) {
110
+ throw new Error("Context belongs to a different caller");
111
+ }
112
+ if (row)
113
+ return contextId;
114
+ this.db
115
+ .prepare("INSERT INTO a2a_contexts (context_id, caller_id, created_at) VALUES (?, ?, ?)")
116
+ .run(contextId, callerId, nowIso());
117
+ return contextId;
118
+ }
119
+ const generated = randomUUID();
120
+ this.db
121
+ .prepare("INSERT INTO a2a_contexts (context_id, caller_id, created_at) VALUES (?, ?, ?)")
122
+ .run(generated, callerId, nowIso());
123
+ return generated;
124
+ }
125
+ getMessage(callerId, messageId) {
126
+ const row = this.db
127
+ .prepare("SELECT task_id AS taskId, request_hash AS requestHash, outcome_json AS outcomeJson FROM a2a_messages WHERE caller_id = ? AND message_id = ?")
128
+ .get(callerId, messageId);
129
+ if (!row)
130
+ return undefined;
131
+ return { taskId: row.taskId, requestHash: row.requestHash, outcome: row.outcomeJson ? JSON.parse(row.outcomeJson) : undefined };
132
+ }
133
+ recordMessage(params) {
134
+ this.db
135
+ .prepare("INSERT INTO a2a_messages (caller_id, message_id, task_id, operation, request_hash, outcome_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
136
+ .run(params.callerId, params.messageId, params.taskId, params.operation, params.requestHash, JSON.stringify(params.outcome), nowIso());
137
+ }
138
+ getTaskBySubmissionKey(key) {
139
+ return this.db.prepare("SELECT * FROM a2a_tasks WHERE submission_key = ?").get(key);
140
+ }
141
+ createTask(params) {
142
+ const at = nowIso();
143
+ this.db
144
+ .prepare(`INSERT INTO a2a_tasks
145
+ (task_id, context_id, caller_id, publication_id, publication_revision, submission_key, run_id, state, result_json, kind, result_stage_id, created_at, updated_at)
146
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'submitted', NULL, ?, ?, ?, ?)`)
147
+ .run(params.taskId, params.contextId, params.callerId, params.publicationId, params.publicationRevision, params.submissionKey, params.runId, params.kind ?? "invoke", params.resultStageId ?? null, at, at);
148
+ return this.getTask(params.taskId);
149
+ }
150
+ getTask(taskId) {
151
+ return this.db.prepare("SELECT * FROM a2a_tasks WHERE task_id = ?").get(taskId);
152
+ }
153
+ listTasksForCaller(callerId, contextId) {
154
+ if (contextId) {
155
+ return this.db
156
+ .prepare("SELECT * FROM a2a_tasks WHERE caller_id = ? AND context_id = ? ORDER BY created_at ASC")
157
+ .all(callerId, contextId);
158
+ }
159
+ return this.db
160
+ .prepare("SELECT * FROM a2a_tasks WHERE caller_id = ? ORDER BY created_at ASC")
161
+ .all(callerId);
162
+ }
163
+ getArtifact(taskId, artifactId) {
164
+ return this.db
165
+ .prepare("SELECT * FROM a2a_artifacts WHERE task_id = ? AND artifact_id = ?")
166
+ .get(taskId, artifactId);
167
+ }
168
+ listArtifacts(taskId) {
169
+ return this.db.prepare("SELECT * FROM a2a_artifacts WHERE task_id = ?").all(taskId);
170
+ }
171
+ /** Freezes a successful outcome exactly once; a second call for an already-terminal task is a no-op. */
172
+ async freezeCompleted(taskId, result) {
173
+ return this.freeze(taskId, "completed", result);
174
+ }
175
+ /** Freezes a failure exactly once; a second call for an already-terminal task is a no-op. */
176
+ async freezeFailed(taskId) {
177
+ return this.freeze(taskId, "failed", undefined);
178
+ }
179
+ async freeze(taskId, state, result) {
180
+ const current = this.getTask(taskId);
181
+ if (!current || current.state === "completed" || current.state === "failed")
182
+ return;
183
+ const artifactRows = [];
184
+ if (result) {
185
+ for (const artifact of result.artifacts) {
186
+ const artifactId = randomUUID();
187
+ const hash = createHash("sha256").update(artifact.bytes).digest("hex");
188
+ const dir = path.join(this.artifactsRoot, taskId);
189
+ await mkdir(dir, { recursive: true });
190
+ const contentPath = path.join(dir, artifactId);
191
+ await writeFile(contentPath, artifact.bytes);
192
+ artifactRows.push({
193
+ artifact_id: artifactId,
194
+ task_id: taskId,
195
+ name: artifact.name,
196
+ media_type: artifact.mediaType ?? null,
197
+ size: artifact.bytes.length,
198
+ content_path: contentPath,
199
+ hash,
200
+ created_at: nowIso(),
201
+ });
202
+ }
203
+ }
204
+ const resultJson = result ? JSON.stringify({ summary: result.summary, payload: result.payload }) : null;
205
+ const transact = this.db.transaction(() => {
206
+ const outcome = this.db
207
+ .prepare("UPDATE a2a_tasks SET state = ?, result_json = ?, updated_at = ? WHERE task_id = ? AND state NOT IN ('completed', 'failed')")
208
+ .run(state, resultJson, nowIso(), taskId);
209
+ if (outcome.changes === 0)
210
+ return;
211
+ for (const row of artifactRows) {
212
+ this.db
213
+ .prepare("INSERT INTO a2a_artifacts (artifact_id, task_id, name, media_type, size, content_path, hash, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
214
+ .run(row.artifact_id, row.task_id, row.name, row.media_type, row.size, row.content_path, row.hash, row.created_at);
215
+ }
216
+ });
217
+ transact();
218
+ }
219
+ countNonterminal(callerId) {
220
+ const row = this.db
221
+ .prepare("SELECT COUNT(*) AS n FROM a2a_tasks WHERE caller_id = ? AND state NOT IN ('completed', 'failed')")
222
+ .get(callerId);
223
+ return row.n;
224
+ }
225
+ /** Deletes terminal tasks (and their frozen artifact files) and message tombstones older than the retention window. */
226
+ async pruneExpired(now, terminalRetentionMs = TERMINAL_RETENTION_MS, messageRetentionMs = MESSAGE_TOMBSTONE_RETENTION_MS) {
227
+ const terminalCutoff = new Date(now.getTime() - terminalRetentionMs).toISOString();
228
+ const messageCutoff = new Date(now.getTime() - messageRetentionMs).toISOString();
229
+ const expiredTasks = this.db
230
+ .prepare("SELECT task_id FROM a2a_tasks WHERE state IN ('completed', 'failed') AND updated_at < ?")
231
+ .all(terminalCutoff);
232
+ for (const { task_id: taskId } of expiredTasks) {
233
+ for (const artifact of this.listArtifacts(taskId)) {
234
+ await unlink(artifact.content_path).catch(() => undefined);
235
+ }
236
+ this.db.transaction(() => {
237
+ this.db.prepare("DELETE FROM a2a_artifacts WHERE task_id = ?").run(taskId);
238
+ this.db.prepare("DELETE FROM a2a_messages WHERE task_id = ?").run(taskId);
239
+ this.db.prepare("DELETE FROM a2a_tasks WHERE task_id = ?").run(taskId);
240
+ })();
241
+ }
242
+ const removedMessages = this.db.prepare("DELETE FROM a2a_messages WHERE created_at < ?").run(messageCutoff).changes;
243
+ return { removedTasks: expiredTasks.length, removedMessages };
244
+ }
245
+ }
@@ -0,0 +1,8 @@
1
+ export declare const A2A_USAGE = "sf a2a validate [--config <path>]\nsf a2a list [--config <path>]\nsf a2a add-caller <id> [--config <path>] [--token-env <NAME>]\n\nValidate or inspect an A2A publication configuration, or register a new caller.\n--config defaults to <project-root>/a2a.yaml when omitted -- the same file\nSTAGEFLOW_A2A_CONFIG would point at if set. Caller credentials are read from\nthe environment variables named in the configuration; add-caller only writes\nthe caller's id and its token_env name to the file, never a secret value.";
2
+ export declare function runA2aCommand(args: string[], options: {
3
+ cwd: string;
4
+ projectRoot?: string;
5
+ env?: NodeJS.ProcessEnv;
6
+ log?: (line: string) => void;
7
+ error?: (line: string) => void;
8
+ }): Promise<number>;
@@ -0,0 +1,131 @@
1
+ import path from "node:path";
2
+ import { randomBytes } from "node:crypto";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { parse, stringify } from "yaml";
5
+ import { loadPublicationRegistry } from "../a2a/registry.js";
6
+ import { a2aConfigPathFor, resolveA2aConfigPath } from "../a2a/configDiscovery.js";
7
+ export const A2A_USAGE = `sf a2a validate [--config <path>]
8
+ sf a2a list [--config <path>]
9
+ sf a2a add-caller <id> [--config <path>] [--token-env <NAME>]
10
+
11
+ Validate or inspect an A2A publication configuration, or register a new caller.
12
+ --config defaults to <project-root>/a2a.yaml when omitted -- the same file
13
+ STAGEFLOW_A2A_CONFIG would point at if set. Caller credentials are read from
14
+ the environment variables named in the configuration; add-caller only writes
15
+ the caller's id and its token_env name to the file, never a secret value.`;
16
+ const SCAFFOLD = { version: 1, callers: [], publications: [] };
17
+ function deriveTokenEnvName(callerId) {
18
+ const normalized = callerId
19
+ .trim()
20
+ .toUpperCase()
21
+ .replace(/[^A-Z0-9]+/g, "_")
22
+ .replace(/^_+|_+$/g, "");
23
+ return (normalized || "CALLER") + "_TOKEN";
24
+ }
25
+ function parseFlags(args) {
26
+ let config;
27
+ let tokenEnv;
28
+ const positionals = [];
29
+ for (let i = 0; i < args.length; i += 1) {
30
+ if (args[i] === "--config") {
31
+ config = args[(i += 1)];
32
+ }
33
+ else if (args[i] === "--token-env") {
34
+ tokenEnv = args[(i += 1)];
35
+ }
36
+ else {
37
+ positionals.push(args[i]);
38
+ }
39
+ }
40
+ return { config, tokenEnv, positionals };
41
+ }
42
+ async function addCaller(configPath, callerId, tokenEnvOverride) {
43
+ let raw;
44
+ try {
45
+ raw = await readFile(configPath, "utf8");
46
+ }
47
+ catch {
48
+ raw = undefined;
49
+ }
50
+ const config = raw ? (parse(raw) ?? {}) : { ...SCAFFOLD };
51
+ if (typeof config !== "object" || Array.isArray(config)) {
52
+ throw new Error(configPath + " does not contain a YAML mapping");
53
+ }
54
+ if (config.version === undefined)
55
+ config.version = 1;
56
+ if (config.callers === undefined)
57
+ config.callers = [];
58
+ if (config.publications === undefined)
59
+ config.publications = [];
60
+ const callers = config.callers;
61
+ if (!Array.isArray(callers)) {
62
+ throw new Error('"callers" in ' + configPath + " is not a list");
63
+ }
64
+ if (callers.some((entry) => entry && typeof entry === "object" && entry.id === callerId)) {
65
+ throw new Error('Caller "' + callerId + '" already exists in ' + configPath);
66
+ }
67
+ const tokenEnv = tokenEnvOverride ?? deriveTokenEnvName(callerId);
68
+ callers.push({ id: callerId, token_env: tokenEnv });
69
+ await mkdir(path.dirname(configPath), { recursive: true });
70
+ await writeFile(configPath, stringify(config));
71
+ const token = randomBytes(24).toString("hex");
72
+ return { tokenEnv, token, created: raw === undefined };
73
+ }
74
+ export async function runA2aCommand(args, options) {
75
+ const log = options.log ?? console.log;
76
+ const error = options.error ?? console.error;
77
+ const env = options.env ?? process.env;
78
+ const projectRoot = options.projectRoot ?? options.cwd;
79
+ if (args[0] === "--help" || args[0] === "-h") {
80
+ log(A2A_USAGE);
81
+ return 0;
82
+ }
83
+ const sub = args[0];
84
+ if (sub !== "validate" && sub !== "list" && sub !== "add-caller") {
85
+ error(A2A_USAGE);
86
+ return 1;
87
+ }
88
+ const { config, tokenEnv, positionals } = parseFlags(args.slice(1));
89
+ if (sub === "add-caller") {
90
+ if (positionals.length !== 1 || !positionals[0]) {
91
+ error(A2A_USAGE);
92
+ return 1;
93
+ }
94
+ const configPath = config ? path.resolve(options.cwd, config) : a2aConfigPathFor(projectRoot);
95
+ try {
96
+ const result = await addCaller(configPath, positionals[0], tokenEnv);
97
+ log(JSON.stringify({ ok: true, configPath, callerId: positionals[0], tokenEnv: result.tokenEnv, created: result.created }));
98
+ log('Generated a token for caller "' + positionals[0] + '". Export it before starting the host (or store it in your secrets manager):');
99
+ log(" export " + result.tokenEnv + "=" + result.token);
100
+ if (result.created) {
101
+ log("Created " + configPath + " with no publications yet -- add at least one under `publications:` before this config can be loaded.");
102
+ }
103
+ return 0;
104
+ }
105
+ catch (err) {
106
+ error(err instanceof Error ? err.message : String(err));
107
+ return 1;
108
+ }
109
+ }
110
+ if (positionals.length !== 0) {
111
+ error(A2A_USAGE);
112
+ return 1;
113
+ }
114
+ const configPath = config ? path.resolve(options.cwd, config) : resolveA2aConfigPath(projectRoot, env);
115
+ if (!configPath) {
116
+ error("No A2A configuration found. Pass --config <path>, or create " + a2aConfigPathFor(projectRoot) + ".");
117
+ return 1;
118
+ }
119
+ try {
120
+ const registry = await loadPublicationRegistry(configPath, env);
121
+ log(JSON.stringify({ ok: true, configPath: registry.configPath, publicUrl: registry.publicUrl, mode: "discovery", publications: registry.summaries() }));
122
+ if (sub === "list") {
123
+ log("Configuration validated locally. An already-running host must be restarted explicitly to pick up changes.");
124
+ }
125
+ return 0;
126
+ }
127
+ catch (err) {
128
+ error(err instanceof Error ? err.message : String(err));
129
+ return 1;
130
+ }
131
+ }
@@ -0,0 +1,33 @@
1
+ import { type EnsureGlobalServiceResult } from "../server/ensureGlobalService.js";
2
+ export declare const RUN_STAGE_USAGE = "Usage:\n sf run-stage (--stage <path> | --stage-inline '<json>') (--task <path> | --task-inline '<json>' | --envelope-ref <runId>:<stageId>[:<attempt>] [--envelope-ref ...]) [--checkout <path>] [--model <id>] [--blocking] [--timeout-ms <n>] [--json]\n\nRun a single stage directly against the shared Stageflow service, without authoring a pipeline file. Distinct from the internal-only, worker-process-only `sf internal run-stage`.\n --stage <path> Filesystem path to a catalog stage YAML file\n --stage-inline <json> Inline stage body object ({ id, system_prompt, io, ... } \u2014 no uses:/route/pipeline wrapper)\n --task <path> Filesystem path to a catalog task YAML file\n --task-inline <json> Inline task object ({ id, goal, ... })\n --envelope-ref <ref> Resolve a previously stored StageEnvelope as this stage's input instead of a task: <runId>:<stageId>[:<attempt>]. Repeat to pass more than one \u2014 each resolved payload is namespaced under its stageId in input (disambiguated by runId on a stageId collision), and summaries are combined into goal.\n --checkout <path> Optional checkout to use with --envelope-ref (--task/--task-inline carry their own checkout)\n --model <id> Override the model/backend for this call only, ahead of the stage's own declared model\n --blocking Wait for the run to reach a terminal or waiting state and print the result in this same call, instead of returning immediately with just the run id\n --timeout-ms <n> Wait budget in ms when --blocking is set\n --json Machine-readable JSON output";
3
+ export type RunStageEnvelopeRef = {
4
+ runId: string;
5
+ stageId: string;
6
+ attempt?: number;
7
+ };
8
+ export type RunStageToolArgs = {
9
+ stage: string | Record<string, unknown>;
10
+ task_path?: string;
11
+ task?: Record<string, unknown>;
12
+ envelope_ref?: RunStageEnvelopeRef | RunStageEnvelopeRef[];
13
+ checkout?: string;
14
+ model?: string;
15
+ blocking?: boolean;
16
+ timeout_ms?: number;
17
+ };
18
+ export type RunStageToolResult = {
19
+ isError: boolean;
20
+ payload: Record<string, unknown> | null;
21
+ };
22
+ export type RunStageCallFn = (args: RunStageToolArgs) => Promise<RunStageToolResult>;
23
+ export type RunStageCommandIo = {
24
+ log: (line: string) => void;
25
+ error: (line: string) => void;
26
+ };
27
+ export declare function runRunStageCommand(args: string[], options?: {
28
+ cwd?: string;
29
+ io?: Partial<RunStageCommandIo>;
30
+ callTool?: RunStageCallFn;
31
+ hostBaseUrl?: string;
32
+ ensureService?: () => Promise<EnsureGlobalServiceResult>;
33
+ }): Promise<number>;