stageflow 0.19.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.
- package/README.md +16 -3
- package/dist/a2a/configDiscovery.d.ts +9 -0
- package/dist/a2a/configDiscovery.js +18 -0
- package/dist/a2a/contracts.d.ts +93 -0
- package/dist/a2a/contracts.js +163 -0
- package/dist/a2a/limits.d.ts +14 -0
- package/dist/a2a/limits.js +29 -0
- package/dist/a2a/registry.d.ts +44 -0
- package/dist/a2a/registry.js +159 -0
- package/dist/a2a/server.d.ts +29 -0
- package/dist/a2a/server.js +341 -0
- package/dist/a2a/service.d.ts +97 -0
- package/dist/a2a/service.js +496 -0
- package/dist/a2a/store.d.ts +106 -0
- package/dist/a2a/store.js +245 -0
- package/dist/agent/activity.d.ts +5 -0
- package/dist/agent/activityObserver.d.ts +3 -0
- package/dist/agent/activityObserver.js +6 -1
- package/dist/agent/piAdapter.js +7 -3
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/providerInspect.d.ts +19 -0
- package/dist/agent/providerInspect.js +35 -0
- package/dist/agent/streamLogRedact.d.ts +2 -0
- package/dist/agent/streamLogRedact.js +13 -0
- package/dist/cli/a2aCommand.d.ts +8 -0
- package/dist/cli/a2aCommand.js +131 -0
- package/dist/cli/artifactCommand.js +2 -2
- package/dist/cli/envelopeCommand.js +2 -2
- package/dist/cli/exportRunCommand.js +2 -2
- package/dist/cli/hostClient.d.ts +43 -0
- package/dist/cli/hostClient.js +182 -0
- package/dist/cli/providersCommand.js +5 -3
- package/dist/cli/runCommand.d.ts +6 -5
- package/dist/cli/runCommand.js +23 -31
- package/dist/cli/runOutput.d.ts +1 -1
- package/dist/cli/runStageCommand.d.ts +33 -0
- package/dist/cli/runStageCommand.js +407 -0
- package/dist/cli/runsCommand.d.ts +3 -7
- package/dist/cli/runsCommand.js +33 -94
- package/dist/cli.js +20 -5
- package/dist/config/describePipeline.d.ts +26 -0
- package/dist/config/describePipeline.js +37 -0
- package/dist/config/loadPipeline.d.ts +13 -1
- package/dist/config/loadPipeline.js +34 -4
- package/dist/config/mergePipelineIncludes.d.ts +11 -0
- package/dist/config/mergePipelineIncludes.js +75 -0
- package/dist/config/validateCatalog.d.ts +2 -2
- package/dist/config/validateCatalog.js +24 -17
- package/dist/mcp/catalogTools.js +285 -52
- package/dist/mcp/controlTools.js +77 -19
- package/dist/mcp/deps.d.ts +3 -0
- package/dist/mcp/projectMcpTools.d.ts +3 -0
- package/dist/mcp/projectMcpTools.js +25 -0
- package/dist/mcp/providerTools.d.ts +3 -0
- package/dist/mcp/providerTools.js +17 -0
- package/dist/mcp/readArtifact.d.ts +9 -0
- package/dist/mcp/readArtifact.js +11 -0
- package/dist/mcp/server.js +9 -1
- package/dist/mcp/tailStreamLog.d.ts +8 -0
- package/dist/mcp/tailStreamLog.js +28 -0
- package/dist/mcp/toolResults.d.ts +17 -0
- package/dist/mcp/toolResults.js +15 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/mcp/waitRun.js +4 -4
- package/dist/mcp/waitingGates.d.ts +1 -0
- package/dist/mcp/waitingGates.js +3 -2
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +3 -0
- package/dist/projection/projectRun.js +7 -0
- package/dist/runstore/createStore.d.ts +10 -0
- package/dist/runstore/createStore.js +13 -4
- package/dist/runstore/port.d.ts +17 -2
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +81 -26
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +6 -0
- package/dist/runstore/submission.d.ts +11 -0
- package/dist/runstore/submission.js +7 -0
- package/dist/runstore/workspaceLayout.d.ts +1 -0
- package/dist/runstore/workspaceLayout.js +3 -0
- package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
- package/dist/runtime/feedbackLoopCoordinator.js +50 -20
- package/dist/runtime/feedbackLoopDecision.js +33 -1
- package/dist/runtime/pipelineRunner.d.ts +5 -3
- package/dist/runtime/pipelineRunner.js +6 -1
- package/dist/runtime/pipelineScheduler.js +32 -15
- package/dist/runtime/reloadRunCatalog.js +7 -1
- package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
- package/dist/runtime/resolveEnvelopeRef.js +79 -0
- package/dist/runtime/resumeReconstruct.js +2 -2
- package/dist/runtime/runManager.d.ts +24 -2
- package/dist/runtime/runManager.js +83 -19
- package/dist/runtime/settingsFile.d.ts +2 -0
- package/dist/runtime/settingsFile.js +19 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
- package/dist/runtime/stageAttemptBootstrap.js +8 -2
- package/dist/runtime/stageStreamLog.d.ts +17 -0
- package/dist/runtime/stageStreamLog.js +89 -0
- package/dist/runtime/stageWorker.js +2 -1
- package/dist/server/bootstrap.d.ts +2 -0
- package/dist/server/bootstrap.js +22 -4
- package/dist/server/createHttpHost.js +6 -0
- package/dist/server/ensureGlobalService.d.ts +35 -0
- package/dist/server/ensureGlobalService.js +138 -0
- package/dist/server/http.d.ts +20 -1
- package/dist/server/http.js +450 -402
- package/dist/server/mcpHost.js +7 -8
- package/dist/server/operatorResults.d.ts +10 -0
- package/dist/server/operatorResults.js +24 -0
- package/dist/server/providerRoutes.d.ts +0 -6
- package/dist/server/providerRoutes.js +10 -18
- package/dist/types/pipeline.d.ts +8 -0
- package/dist/ui/assets/{index-DMVdl1qB.js → index-Da5EHU6M.js} +5 -5
- package/dist/ui/index.html +1 -1
- package/package.json +2 -1
- package/skills/stageflow/references/control-surface.md +2 -2
- package/skills/stageflow-run/SKILL.md +27 -9
- package/skills/stageflow-run/references/mcp-call.md +1 -1
- package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
package/dist/mcp/catalogTools.js
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import { browseCatalog } from "../config/browseCatalog.js";
|
|
3
|
+
import { browseCatalog, } from "../config/browseCatalog.js";
|
|
4
|
+
import { describePipeline } from "../config/describePipeline.js";
|
|
3
5
|
import { loadPipeline } from "../config/loadPipeline.js";
|
|
6
|
+
import { readYamlObject } from "../config/readYamlObject.js";
|
|
4
7
|
import { validateCatalog } from "../config/validateCatalog.js";
|
|
8
|
+
import { PACKAGE_VERSION } from "../package-meta.js";
|
|
9
|
+
import { findProjectRoot } from "../project/findProjectRoot.js";
|
|
10
|
+
import { PipelineValidationError } from "../runtime/pipelineValidationError.js";
|
|
11
|
+
import { EnvelopeRefError, resolveEnvelopeRefsToTask, } from "../runtime/resolveEnvelopeRef.js";
|
|
12
|
+
import { isTerminalProjection, isWaitingProjection, waitRun } from "./waitRun.js";
|
|
13
|
+
import { mapStoreLookupError } from "../server/operatorResults.js";
|
|
5
14
|
import { projectRunForMcp } from "./projectRun.js";
|
|
6
|
-
import {
|
|
7
|
-
import { textResult } from "./toolResults.js";
|
|
15
|
+
import { classifyArtifactContent, readRunArtifactBytes } from "./readArtifact.js";
|
|
16
|
+
import { imageResult, textResult } from "./toolResults.js";
|
|
17
|
+
/**
|
|
18
|
+
* Every project a host serving a global store has ever recorded a run for,
|
|
19
|
+
* plus this host's own launch directory (always included so a project with
|
|
20
|
+
* zero runs yet still sees its own catalog).
|
|
21
|
+
*/
|
|
22
|
+
async function catalogRootsFor(deps) {
|
|
23
|
+
const known = await deps.store.listProjectRoots();
|
|
24
|
+
return [...new Set([...known, deps.cwd])];
|
|
25
|
+
}
|
|
26
|
+
/** Resolve which project a given catalog path (pipeline/task) belongs to. */
|
|
27
|
+
function projectRootForPath(deps, catalogPath) {
|
|
28
|
+
const absDir = path.dirname(path.resolve(deps.cwd, catalogPath));
|
|
29
|
+
return findProjectRoot(absDir) ?? deps.cwd;
|
|
30
|
+
}
|
|
8
31
|
const taskFileSchema = z.object({
|
|
9
32
|
id: z.string(),
|
|
10
33
|
goal: z.string(),
|
|
@@ -13,31 +36,119 @@ const taskFileSchema = z.object({
|
|
|
13
36
|
checkout: z.string().optional(),
|
|
14
37
|
input: z.record(z.string(), z.unknown()).optional(),
|
|
15
38
|
});
|
|
39
|
+
/**
|
|
40
|
+
* Deliberately thin: only enough shape to route to the inline-pipeline
|
|
41
|
+
* loader. Real structural validation (required io schemas, DAG shape, stage
|
|
42
|
+
* id uniqueness) happens downstream through the same validator a file-based
|
|
43
|
+
* pipeline goes through, so a caller gets the same ValidationFinding-shaped
|
|
44
|
+
* error either way instead of a raw schema-validation error.
|
|
45
|
+
*/
|
|
46
|
+
const inlinePipelineSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
id: z.string().min(1),
|
|
49
|
+
stages: z.array(z.record(z.string(), z.unknown())).min(1),
|
|
50
|
+
agent: z.unknown().optional(),
|
|
51
|
+
model: z.unknown().optional(),
|
|
52
|
+
schemas: z.unknown().optional(),
|
|
53
|
+
})
|
|
54
|
+
.strict();
|
|
16
55
|
const startRunSchema = z
|
|
17
56
|
.object({
|
|
18
|
-
pipeline: z
|
|
57
|
+
pipeline: z
|
|
58
|
+
.union([z.string(), inlinePipelineSchema])
|
|
59
|
+
.describe("Filesystem path to a pipeline YAML file, or an inline pipeline definition object ({ id, stages: [...] }) authored directly in this call"),
|
|
19
60
|
task_path: z.string().optional(),
|
|
20
61
|
task: taskFileSchema.optional(),
|
|
21
62
|
})
|
|
22
63
|
.refine((data) => Boolean(data.task_path) !== Boolean(data.task), {
|
|
23
64
|
message: "Exactly one of task_path or task is required",
|
|
24
65
|
});
|
|
66
|
+
/**
|
|
67
|
+
* Deliberately thin, same reasoning as inlinePipelineSchema above: real
|
|
68
|
+
* structural validation of the stage body happens downstream through the
|
|
69
|
+
* same stage loader a pipeline's inline stage goes through.
|
|
70
|
+
*/
|
|
71
|
+
const envelopeRefSchema = z.object({
|
|
72
|
+
runId: z.string(),
|
|
73
|
+
stageId: z.string(),
|
|
74
|
+
attempt: z.number().int().positive().optional(),
|
|
75
|
+
});
|
|
76
|
+
const runStageSchema = z
|
|
77
|
+
.object({
|
|
78
|
+
stage: z
|
|
79
|
+
.union([z.string(), z.record(z.string(), z.unknown())])
|
|
80
|
+
.describe("Filesystem path to a stage YAML file, or a bare inline stage body object ({ id, system_prompt, io, model?, gate_kinds?, mcp?, verify?, timeout_ms? } — no uses:/route/pipeline wrapper) authored directly in this call"),
|
|
81
|
+
task_path: z.string().optional(),
|
|
82
|
+
task: taskFileSchema.optional(),
|
|
83
|
+
envelope_ref: z
|
|
84
|
+
.union([envelopeRefSchema, z.array(envelopeRefSchema).min(1)])
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Resolve one or more previously stored StageEnvelopes (from another run_stage call or from any stage inside a full pipeline run) and use them as this stage's input, instead of an inline task/task_path. A single ref: its payload becomes input, its summary becomes goal. Multiple refs (an array): each resolved payload is namespaced under its stageId in input (disambiguated by runId on a stageId collision), and summaries are combined into goal."),
|
|
87
|
+
checkout: z
|
|
88
|
+
.string()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Optional checkout to use with envelope_ref (ignored with task/task_path, which carry their own checkout). Resolving envelope_ref never implies a checkout on its own."),
|
|
91
|
+
model: z
|
|
92
|
+
.string()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("Override the model/backend for this call only, taking precedence over the stage's own declared model and the global default. Omit to use the stage's own model (or the global default)."),
|
|
95
|
+
blocking: z
|
|
96
|
+
.boolean()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("When true, wait for the run to reach a terminal or waiting state and return the result in this same call, instead of returning immediately with just { runId }"),
|
|
99
|
+
timeout_ms: z
|
|
100
|
+
.number()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("Wait budget in ms when blocking is true (same bounds as wait_run)"),
|
|
103
|
+
})
|
|
104
|
+
.refine((data) => [data.task_path, data.task, data.envelope_ref].filter(Boolean).length === 1, { message: "Exactly one of task_path, task, or envelope_ref is required" });
|
|
25
105
|
const runStatusSchema = z.enum(["created", "running", "succeeded", "failed"]);
|
|
26
106
|
export function registerCatalogTools(server, deps) {
|
|
27
107
|
const { manager, store, cwd } = deps;
|
|
28
108
|
server.registerTool("list_pipelines", {
|
|
29
|
-
description: "List manifest-declared pipeline paths
|
|
109
|
+
description: "List manifest-declared pipeline paths across every project this host knows about (this host's own project plus any project a run has ever been recorded for). Each entry is tagged with project_root. A project with no runs yet won't appear until its first run exists.",
|
|
30
110
|
inputSchema: z.object({}),
|
|
31
111
|
}, async () => {
|
|
32
|
-
const
|
|
33
|
-
|
|
112
|
+
const roots = await catalogRootsFor(deps);
|
|
113
|
+
const pipelines = [];
|
|
114
|
+
for (const root of roots) {
|
|
115
|
+
try {
|
|
116
|
+
const catalog = await browseCatalog(root);
|
|
117
|
+
for (const p of catalog.pipelines) {
|
|
118
|
+
pipelines.push({ ...p, project_root: root });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// stale/unreadable project root recorded on an old run; skip it
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return textResult({ pipelines });
|
|
34
126
|
});
|
|
35
127
|
server.registerTool("list_tasks", {
|
|
36
|
-
description: "List manifest-declared task paths
|
|
128
|
+
description: "List manifest-declared task paths across every project this host knows about (this host's own project plus any project a run has ever been recorded for). Each entry is tagged with project_root. A project with no runs yet won't appear until its first run exists.",
|
|
129
|
+
inputSchema: z.object({}),
|
|
130
|
+
}, async () => {
|
|
131
|
+
const roots = await catalogRootsFor(deps);
|
|
132
|
+
const tasks = [];
|
|
133
|
+
for (const root of roots) {
|
|
134
|
+
try {
|
|
135
|
+
const catalog = await browseCatalog(root);
|
|
136
|
+
for (const t of catalog.tasks) {
|
|
137
|
+
tasks.push({ ...t, project_root: root });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// stale/unreadable project root recorded on an old run; skip it
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return textResult({ tasks });
|
|
145
|
+
});
|
|
146
|
+
server.registerTool("list_models", {
|
|
147
|
+
description: "List catalog model ids from the project catalog (same source as GET /api/models)",
|
|
37
148
|
inputSchema: z.object({}),
|
|
38
149
|
}, async () => {
|
|
39
150
|
const catalog = await browseCatalog(cwd);
|
|
40
|
-
return textResult({
|
|
151
|
+
return textResult({ models: catalog.models });
|
|
41
152
|
});
|
|
42
153
|
server.registerTool("list_runs", {
|
|
43
154
|
description: "List known pipeline runs. Optional filters: status, since (ISO created_at lower bound), pipeline (id or path).",
|
|
@@ -62,27 +173,133 @@ export function registerCatalogTools(server, deps) {
|
|
|
62
173
|
return textResult({ runs });
|
|
63
174
|
});
|
|
64
175
|
server.registerTool("get_health", {
|
|
65
|
-
description: "Server health and soft-max run capacity: activeRunIds, activeCount, maxConcurrent, slotsAvailable. Start until slotsAvailable is 0; then wait for a run to finish or raise STAGEFLOW_MAX_CONCURRENT_RUNS.",
|
|
176
|
+
description: "Server health and soft-max run capacity: activeRunIds, activeCount, maxConcurrent, slotsAvailable, version. Start until slotsAvailable is 0; then wait for a run to finish or raise STAGEFLOW_MAX_CONCURRENT_RUNS.",
|
|
66
177
|
inputSchema: z.object({}),
|
|
67
|
-
}, async () => textResult(manager.getHealth()));
|
|
178
|
+
}, async () => textResult({ ...manager.getHealth(), version: PACKAGE_VERSION }));
|
|
68
179
|
server.registerTool("start_run", {
|
|
69
|
-
description: "Start a pipeline run using a filesystem pipeline path and either task_path (catalog task file) or an inline task object. Returns { runId }. On conflict returns isError with code busy_capacity (soft max full) or busy_checkout (same checkout leased), plus activeCount/maxConcurrent/activeRunIds and optional conflictingRunId/conflictingCheckout.",
|
|
180
|
+
description: "Start a pipeline run using a filesystem pipeline path or an inline pipeline definition ({ id, stages: [...] }, each stage the same shape as a YAML stage body — no uses: refs), and either task_path (catalog task file) or an inline task object. Returns { runId }. On conflict returns isError with code busy_capacity (soft max full) or busy_checkout (same checkout leased), plus activeCount/maxConcurrent/activeRunIds and optional conflictingRunId/conflictingCheckout.",
|
|
70
181
|
inputSchema: startRunSchema,
|
|
71
182
|
}, async ({ pipeline, task_path, task }) => {
|
|
72
|
-
if (!pipeline.trim()) {
|
|
183
|
+
if (typeof pipeline === "string" && !pipeline.trim()) {
|
|
73
184
|
return textResult({ error: "pipeline is required" }, true);
|
|
74
185
|
}
|
|
75
186
|
const taskInput = task_path ?? task;
|
|
76
187
|
if (taskInput === undefined) {
|
|
77
188
|
return textResult({ error: "Exactly one of task_path or task is required" }, true);
|
|
78
189
|
}
|
|
79
|
-
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
result = await manager.startRun({ pipeline, task: taskInput });
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (err instanceof PipelineValidationError) {
|
|
196
|
+
return textResult({ error: "Pipeline validation failed", validation: err.result }, true);
|
|
197
|
+
}
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
80
200
|
if (!result.ok) {
|
|
81
201
|
const { ok: _ok, reason, ...rest } = result;
|
|
82
202
|
return textResult({ error: reason, ...rest }, true);
|
|
83
203
|
}
|
|
84
204
|
return textResult({ runId: result.runId });
|
|
85
205
|
});
|
|
206
|
+
server.registerTool("run_stage", {
|
|
207
|
+
description: "Run a single stage directly, without authoring a pipeline. `stage` is a filesystem path to a stage YAML file, or a bare inline stage body ({ id, system_prompt, io, model?, gate_kinds?, mcp?, verify?, timeout_ms? } — no uses:/route/pipeline wrapper), and exactly one of task_path, an inline task, or envelope_ref ({ runId, stageId, attempt? }) to resolve a previously stored envelope — from another run_stage call or any stage in a full pipeline run — as this stage's input. Internally this synthesizes a one-stage pipeline and executes it through the normal run path, so it shows up in list_runs/get_run and is polled with wait_run / get_envelope exactly like any other run. By default returns { runId, stageId } immediately (async); pass blocking:true to wait in this same call and get back { runId, stageId, status: \"completed\"|\"needs_input\"|\"timeout\", envelope? , pending_prompt? }.",
|
|
208
|
+
inputSchema: runStageSchema,
|
|
209
|
+
}, async ({ stage, task_path, task, envelope_ref, checkout, model, blocking, timeout_ms }) => {
|
|
210
|
+
let taskInput = task_path ?? task;
|
|
211
|
+
if (envelope_ref) {
|
|
212
|
+
try {
|
|
213
|
+
taskInput = await resolveEnvelopeRefsToTask(store, envelope_ref, checkout);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
if (err instanceof EnvelopeRefError) {
|
|
217
|
+
return textResult({ error: err.message, status: err.status }, true);
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (taskInput === undefined) {
|
|
223
|
+
return textResult({ error: "Exactly one of task_path, task, or envelope_ref is required" }, true);
|
|
224
|
+
}
|
|
225
|
+
let stageBody;
|
|
226
|
+
if (typeof stage === "string") {
|
|
227
|
+
if (!stage.trim()) {
|
|
228
|
+
return textResult({ error: "stage is required" }, true);
|
|
229
|
+
}
|
|
230
|
+
const absPath = path.resolve(cwd, stage);
|
|
231
|
+
try {
|
|
232
|
+
stageBody = await readYamlObject(absPath);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
236
|
+
return textResult({ error: `Failed to read stage file: ${message}` }, true);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
stageBody = stage;
|
|
241
|
+
}
|
|
242
|
+
if (typeof stageBody.id !== "string" || !stageBody.id.trim()) {
|
|
243
|
+
return textResult({ error: "stage.id is required" }, true);
|
|
244
|
+
}
|
|
245
|
+
const stageId = stageBody.id;
|
|
246
|
+
// A call-level model override wins over both the stage's own declared
|
|
247
|
+
// model and the global default: it replaces the field the stage body
|
|
248
|
+
// itself would otherwise resolve through, rather than adding a new
|
|
249
|
+
// tier to resolveModel's stage > pipeline > global chain.
|
|
250
|
+
const effectiveStageBody = model !== undefined ? { ...stageBody, model } : stageBody;
|
|
251
|
+
const pipeline = {
|
|
252
|
+
id: `standalone-${stageId}`,
|
|
253
|
+
stages: [effectiveStageBody],
|
|
254
|
+
};
|
|
255
|
+
let result;
|
|
256
|
+
try {
|
|
257
|
+
result = await manager.startRun({ pipeline, task: taskInput });
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
if (err instanceof PipelineValidationError) {
|
|
261
|
+
return textResult({ error: "Stage validation failed", validation: err.result }, true);
|
|
262
|
+
}
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
if (!result.ok) {
|
|
266
|
+
const { ok: _ok, reason, ...rest } = result;
|
|
267
|
+
return textResult({ error: reason, ...rest }, true);
|
|
268
|
+
}
|
|
269
|
+
const runId = result.runId;
|
|
270
|
+
if (!blocking) {
|
|
271
|
+
return textResult({ runId, stageId });
|
|
272
|
+
}
|
|
273
|
+
const waited = await waitRun({ store, runId, timeoutMs: timeout_ms, until: "any" });
|
|
274
|
+
if (!waited.ok) {
|
|
275
|
+
return textResult({
|
|
276
|
+
error: waited.error,
|
|
277
|
+
...(waited.status !== undefined ? { status: waited.status } : {}),
|
|
278
|
+
...(waited.code !== undefined ? { code: waited.code } : {}),
|
|
279
|
+
}, true);
|
|
280
|
+
}
|
|
281
|
+
const stageProjection = waited.run.stages.find((s) => s.stage_id === stageId);
|
|
282
|
+
if (isTerminalProjection(waited.run)) {
|
|
283
|
+
return textResult({
|
|
284
|
+
runId,
|
|
285
|
+
stageId,
|
|
286
|
+
status: "completed",
|
|
287
|
+
envelope: stageProjection?.envelope ?? null,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (isWaitingProjection(waited.run)) {
|
|
291
|
+
return textResult({
|
|
292
|
+
runId,
|
|
293
|
+
stageId,
|
|
294
|
+
status: "needs_input",
|
|
295
|
+
pending_prompt: stageProjection?.pending_prompt,
|
|
296
|
+
waiting_kind: waited.run.waiting_kind,
|
|
297
|
+
waiting_prompt_id: waited.run.waiting_prompt_id,
|
|
298
|
+
waiting_summary: waited.run.waiting_summary,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return textResult({ runId, stageId, status: "timeout" });
|
|
302
|
+
});
|
|
86
303
|
server.registerTool("get_run", {
|
|
87
304
|
description: "Poll a run: status, stage statuses, waiting_* / pending_prompt HITL fields, and envelope summary/payload/artifact paths (no events). Use list_stage_events, get_envelope, or get_stage_verification for stage detail.",
|
|
88
305
|
inputSchema: z.object({
|
|
@@ -94,34 +311,39 @@ export function registerCatalogTools(server, deps) {
|
|
|
94
311
|
return textResult(projectRunForMcp(detail));
|
|
95
312
|
}
|
|
96
313
|
catch (err) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
return textResult({
|
|
100
|
-
error: message,
|
|
101
|
-
status: notFound ? 404 : 500,
|
|
102
|
-
}, true);
|
|
314
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
315
|
+
return textResult({ error: mapped.error, status: mapped.status }, true);
|
|
103
316
|
}
|
|
104
317
|
});
|
|
105
318
|
server.registerTool("read_artifact", {
|
|
106
|
-
description: "Read a
|
|
319
|
+
description: "Read a run-workspace artifact by relative path (contained under the run workspace). Known image extensions (png, jpeg, gif, webp) return an MCP image content block. UTF-8 text returns JSON { runId, path, content }. Non-UTF-8 non-image files return isError 400.",
|
|
107
320
|
inputSchema: z.object({
|
|
108
321
|
runId: z.string(),
|
|
109
322
|
path: z.string(),
|
|
110
323
|
}),
|
|
111
324
|
}, async ({ runId, path: artifactPath }) => {
|
|
112
325
|
try {
|
|
113
|
-
const
|
|
114
|
-
|
|
326
|
+
const bytes = await readRunArtifactBytes(store, runId, artifactPath);
|
|
327
|
+
const classified = classifyArtifactContent(artifactPath, bytes);
|
|
328
|
+
if (classified.kind === "image") {
|
|
329
|
+
return imageResult(classified.mimeType, bytes, {
|
|
330
|
+
runId,
|
|
331
|
+
path: artifactPath,
|
|
332
|
+
mimeType: classified.mimeType,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (classified.kind === "utf8") {
|
|
336
|
+
return textResult({
|
|
337
|
+
runId,
|
|
338
|
+
path: artifactPath,
|
|
339
|
+
content: bytes.toString("utf8"),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return textResult({ error: "Artifact is not valid UTF-8 text", status: 400 }, true);
|
|
115
343
|
}
|
|
116
344
|
catch (err) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
message.startsWith("Artifact not found") ||
|
|
120
|
-
/no such|not found/i.test(message);
|
|
121
|
-
return textResult({
|
|
122
|
-
error: message,
|
|
123
|
-
status: notFound ? 404 : 400,
|
|
124
|
-
}, true);
|
|
345
|
+
const mapped = mapStoreLookupError(err, { policy: "artifact" });
|
|
346
|
+
return textResult({ error: mapped.error, status: mapped.status }, true);
|
|
125
347
|
}
|
|
126
348
|
});
|
|
127
349
|
server.registerTool("validate", {
|
|
@@ -138,11 +360,36 @@ export function registerCatalogTools(server, deps) {
|
|
|
138
360
|
scope = "pipeline";
|
|
139
361
|
else if (task !== undefined && task.trim())
|
|
140
362
|
scope = "task";
|
|
363
|
+
if (scope === "full") {
|
|
364
|
+
const roots = await catalogRootsFor(deps);
|
|
365
|
+
const results = [];
|
|
366
|
+
for (const root of roots) {
|
|
367
|
+
try {
|
|
368
|
+
results.push(await validateCatalog({ cwd: root, scope: "full", strict: strict ?? false }));
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// stale/unreadable project root recorded on an old run; skip it
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const merged = {
|
|
375
|
+
scope: "full",
|
|
376
|
+
ok: results.every((r) => r.ok),
|
|
377
|
+
summary: {
|
|
378
|
+
errors: results.reduce((n, r) => n + r.summary.errors, 0),
|
|
379
|
+
warnings: results.reduce((n, r) => n + r.summary.warnings, 0),
|
|
380
|
+
},
|
|
381
|
+
findings: results.flatMap((r) => r.findings),
|
|
382
|
+
};
|
|
383
|
+
return textResult(merged);
|
|
384
|
+
}
|
|
385
|
+
const target = scope === "pipeline" ? pipeline.trim() : task.trim();
|
|
386
|
+
const projectRoot = projectRootForPath(deps, target);
|
|
141
387
|
const result = await validateCatalog({
|
|
142
|
-
cwd,
|
|
388
|
+
cwd: projectRoot,
|
|
143
389
|
scope,
|
|
144
|
-
|
|
145
|
-
...(scope === "
|
|
390
|
+
projectRoot,
|
|
391
|
+
...(scope === "pipeline" ? { pipeline: target } : {}),
|
|
392
|
+
...(scope === "task" ? { task: target } : {}),
|
|
146
393
|
strict: strict ?? false,
|
|
147
394
|
});
|
|
148
395
|
return textResult(result);
|
|
@@ -153,7 +400,7 @@ export function registerCatalogTools(server, deps) {
|
|
|
153
400
|
}
|
|
154
401
|
});
|
|
155
402
|
server.registerTool("describe_pipeline", {
|
|
156
|
-
description: "Describe a pipeline DAG: stages with needs, fork,
|
|
403
|
+
description: "Describe a pipeline DAG from a filesystem path (same as start_run): stages with inbound needs (id, on, optional if), fork, gate_kinds, Clone Chain clone_cap and clone_mode, and feedback_loop, entry, and replay_safe when set.",
|
|
157
404
|
inputSchema: z.object({
|
|
158
405
|
pipeline: z.string(),
|
|
159
406
|
}),
|
|
@@ -162,24 +409,10 @@ export function registerCatalogTools(server, deps) {
|
|
|
162
409
|
return textResult({ error: "pipeline is required", status: 400 }, true);
|
|
163
410
|
}
|
|
164
411
|
try {
|
|
165
|
-
const loaded = await loadPipeline(pipeline.trim(), {
|
|
166
|
-
|
|
167
|
-
const stages = loaded.dag.nodes.map((node) => ({
|
|
168
|
-
id: node.id,
|
|
169
|
-
needs: node.needsEdges.length > 1
|
|
170
|
-
? node.needsEdges.map((edge) => ({ id: edge.id, on: [...edge.on] }))
|
|
171
|
-
: node.needs,
|
|
172
|
-
...(node.fork !== undefined ? { fork: node.fork } : {}),
|
|
173
|
-
...(node.clone_cap !== undefined ? { clone_cap: node.clone_cap } : {}),
|
|
174
|
-
...(gateById.get(node.id) !== undefined
|
|
175
|
-
? { gate_kinds: gateById.get(node.id) }
|
|
176
|
-
: {}),
|
|
177
|
-
}));
|
|
178
|
-
return textResult({
|
|
179
|
-
id: loaded.pipeline.id,
|
|
180
|
-
path: loaded.pipelinePath,
|
|
181
|
-
stages,
|
|
412
|
+
const loaded = await loadPipeline(pipeline.trim(), {
|
|
413
|
+
cwd: projectRootForPath(deps, pipeline.trim()),
|
|
182
414
|
});
|
|
415
|
+
return textResult(describePipeline(loaded));
|
|
183
416
|
}
|
|
184
417
|
catch (err) {
|
|
185
418
|
const message = err instanceof Error ? err.message : String(err);
|
package/dist/mcp/controlTools.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
+
import { findProjectRoot } from "../project/findProjectRoot.js";
|
|
4
|
+
import { mapRetryStageFailure, mapStartFailure, mapStoreLookupError, } from "../server/operatorResults.js";
|
|
5
|
+
import { attemptStreamLogPath } from "../runstore/workspaceLayout.js";
|
|
3
6
|
import { parseAskOperatorAnswer } from "../tools/askOperator.js";
|
|
7
|
+
import { readStreamLogTail } from "./tailStreamLog.js";
|
|
4
8
|
import { textResult } from "./toolResults.js";
|
|
5
9
|
import { projectWaitingGates } from "./waitingGates.js";
|
|
6
10
|
import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
|
|
7
11
|
export function registerControlTools(server, deps) {
|
|
8
|
-
const { manager, store } = deps;
|
|
12
|
+
const { manager, store, cwd } = deps;
|
|
9
13
|
server.registerTool("list_waiting", {
|
|
10
14
|
description: "List stages waiting for operator input across runs (optional runId filter). Includes pending_prompt and waiting_* fields per gate.",
|
|
11
15
|
inputSchema: z.object({
|
|
@@ -15,6 +19,24 @@ export function registerControlTools(server, deps) {
|
|
|
15
19
|
const waiting = await projectWaitingGates(store, { runId });
|
|
16
20
|
return textResult({ waiting });
|
|
17
21
|
});
|
|
22
|
+
server.registerTool("get_waiting_summary", {
|
|
23
|
+
description: "Lightweight count and identity of stages waiting for operator input — no pending_prompt, waiting_artifacts, or waiting_questions (use list_waiting for full detail). Optional runId scopes to one run; optional path scopes to one project (derived via findProjectRoot, same rule as elsewhere); omitting both spans every project the store knows about.",
|
|
24
|
+
inputSchema: z.object({
|
|
25
|
+
runId: z.string().optional(),
|
|
26
|
+
path: z.string().optional(),
|
|
27
|
+
}),
|
|
28
|
+
}, async ({ runId, path: scopePath }) => {
|
|
29
|
+
const projectRoot = runId === undefined && scopePath !== undefined
|
|
30
|
+
? (findProjectRoot(path.resolve(cwd, scopePath)) ?? path.resolve(cwd, scopePath))
|
|
31
|
+
: undefined;
|
|
32
|
+
const waiting = await projectWaitingGates(store, { runId, projectRoot });
|
|
33
|
+
const runs = waiting.map((item) => ({
|
|
34
|
+
runId: item.runId,
|
|
35
|
+
stageId: item.stageId,
|
|
36
|
+
...(item.waiting_kind !== undefined ? { kind: item.waiting_kind } : {}),
|
|
37
|
+
}));
|
|
38
|
+
return textResult({ count: runs.length, runs });
|
|
39
|
+
});
|
|
18
40
|
server.registerTool("answer_gate", {
|
|
19
41
|
description: "Deliver an AskOperatorAnswer for a stage in waiting_for_input (same semantics as POST /api/runs/:id/stages/:stageId/answer). Pass answer as { promptId, kind, ... } matching the pending prompt.",
|
|
20
42
|
inputSchema: z.object({
|
|
@@ -73,8 +95,8 @@ export function registerControlTools(server, deps) {
|
|
|
73
95
|
await store.readRunMeta(runId);
|
|
74
96
|
}
|
|
75
97
|
catch (err) {
|
|
76
|
-
const
|
|
77
|
-
return textResult({ error:
|
|
98
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
99
|
+
return textResult({ error: mapped.error, status: 404 }, true);
|
|
78
100
|
}
|
|
79
101
|
try {
|
|
80
102
|
const detail = await store.readRun(runId);
|
|
@@ -86,9 +108,46 @@ export function registerControlTools(server, deps) {
|
|
|
86
108
|
return textResult({ runId, stageId, attempt, events });
|
|
87
109
|
}
|
|
88
110
|
catch (err) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
112
|
+
return textResult({ error: mapped.error, status: mapped.status }, true);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
server.registerTool("tail_stage_log", {
|
|
116
|
+
description: "Poll for new live assistant text a running (or recently finished) stage attempt has produced, since a byte-offset cursor. Separate from list_stage_events — no prompt/artifact/question detail, just the raw redacted text as it streamed. Omit since_offset to catch up on everything currently retained.",
|
|
117
|
+
inputSchema: z.object({
|
|
118
|
+
runId: z.string(),
|
|
119
|
+
stageId: z.string(),
|
|
120
|
+
attempt: z.number().int().positive().optional(),
|
|
121
|
+
since_offset: z.number().int().nonnegative().optional(),
|
|
122
|
+
}),
|
|
123
|
+
}, async ({ runId, stageId, attempt, since_offset }) => {
|
|
124
|
+
try {
|
|
125
|
+
await store.readRunMeta(runId);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
129
|
+
return textResult({ error: mapped.error, status: 404 }, true);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const detail = await store.readRun(runId);
|
|
133
|
+
const stage = detail.stages.find((s) => s.stage_id === stageId);
|
|
134
|
+
if (!stage) {
|
|
135
|
+
return textResult({ error: `Stage not found: ${stageId}`, status: 404 }, true);
|
|
136
|
+
}
|
|
137
|
+
const effectiveAttempt = attempt ?? stage.attempt_count;
|
|
138
|
+
const attemptComplete = effectiveAttempt < stage.attempt_count || stage.status !== "running";
|
|
139
|
+
const streamLogPath = attemptStreamLogPath(store.getWorkspaceDir(runId), stageId, effectiveAttempt);
|
|
140
|
+
const tail = await readStreamLogTail(streamLogPath, since_offset);
|
|
141
|
+
return textResult({
|
|
142
|
+
text: tail.text,
|
|
143
|
+
next_offset: tail.nextOffset,
|
|
144
|
+
attempt_complete: attemptComplete,
|
|
145
|
+
...(tail.truncated ? { truncated: true, earliest_offset: tail.earliestOffset } : {}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
150
|
+
return textResult({ error: mapped.error, status: mapped.status }, true);
|
|
92
151
|
}
|
|
93
152
|
});
|
|
94
153
|
server.registerTool("get_stage_verification", {
|
|
@@ -102,9 +161,8 @@ export function registerControlTools(server, deps) {
|
|
|
102
161
|
return textResult(await readStageVerificationHistory(store, runId, stageId));
|
|
103
162
|
}
|
|
104
163
|
catch (err) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
return textResult({ error: message, status: notFound ? 404 : 500 }, true);
|
|
164
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
165
|
+
return textResult({ error: mapped.error, status: mapped.status }, true);
|
|
108
166
|
}
|
|
109
167
|
});
|
|
110
168
|
server.registerTool("recover_manual_stage", {
|
|
@@ -139,31 +197,31 @@ export function registerControlTools(server, deps) {
|
|
|
139
197
|
return textResult(result);
|
|
140
198
|
});
|
|
141
199
|
server.registerTool("get_envelope", {
|
|
142
|
-
description: "Read the full StageEnvelope for a run stage (latest
|
|
200
|
+
description: "Read the full StageEnvelope for a run stage. Optional attempt (omit = latest). Returns 404 when absent.",
|
|
143
201
|
inputSchema: z.object({
|
|
144
202
|
runId: z.string(),
|
|
145
203
|
stageId: z.string(),
|
|
204
|
+
attempt: z.number().int().positive().optional(),
|
|
146
205
|
}),
|
|
147
|
-
}, async ({ runId, stageId }) => {
|
|
206
|
+
}, async ({ runId, stageId, attempt }) => {
|
|
148
207
|
try {
|
|
149
208
|
await store.readRunMeta(runId);
|
|
150
209
|
}
|
|
151
210
|
catch (err) {
|
|
152
|
-
const
|
|
153
|
-
return textResult({ error:
|
|
211
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
212
|
+
return textResult({ error: mapped.error, status: 404 }, true);
|
|
154
213
|
}
|
|
155
214
|
try {
|
|
156
215
|
const detail = await store.readRun(runId);
|
|
157
216
|
if (!detail.stages.some((s) => s.stage_id === stageId)) {
|
|
158
217
|
return textResult({ error: `Stage not found: ${stageId}`, status: 404 }, true);
|
|
159
218
|
}
|
|
160
|
-
const envelope = await store.readEnvelope(runId, stageId);
|
|
161
|
-
return textResult({ runId, stageId, envelope });
|
|
219
|
+
const envelope = await store.readEnvelope(runId, stageId, attempt);
|
|
220
|
+
return textResult({ runId, stageId, attempt, envelope });
|
|
162
221
|
}
|
|
163
222
|
catch (err) {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
return textResult({ error: message, status: notFound ? 404 : 500 }, true);
|
|
223
|
+
const mapped = mapStoreLookupError(err, { policy: "envelope" });
|
|
224
|
+
return textResult({ error: mapped.error, status: mapped.status }, true);
|
|
167
225
|
}
|
|
168
226
|
});
|
|
169
227
|
server.registerTool("retry_stage", {
|
package/dist/mcp/deps.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProviderAuthContext } from "../agent/providerAuth.js";
|
|
1
2
|
import type { RunStore } from "../runstore/port.js";
|
|
2
3
|
import type { RunChangeBus } from "../runtime/runChangeBus.js";
|
|
3
4
|
import type { RunManager } from "../runtime/runManager.js";
|
|
@@ -5,6 +6,8 @@ export type McpToolDeps = {
|
|
|
5
6
|
manager: RunManager;
|
|
6
7
|
store: RunStore;
|
|
7
8
|
cwd: string;
|
|
9
|
+
providerAuthContext?: ProviderAuthContext;
|
|
10
|
+
projectRoot?: string;
|
|
8
11
|
};
|
|
9
12
|
export type McpHttpDeps = McpToolDeps & {
|
|
10
13
|
runChangeBus: RunChangeBus;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { probeProjectMcpServer } from "../agent/piIsolatedMcpProbe.js";
|
|
3
|
+
import { listProjectMcpCatalog } from "../config/resolveStageMcpServers.js";
|
|
4
|
+
import { textResult } from "./toolResults.js";
|
|
5
|
+
export function registerProjectMcpTools(server, deps) {
|
|
6
|
+
const { cwd, projectRoot } = deps;
|
|
7
|
+
const root = projectRoot ?? cwd;
|
|
8
|
+
server.registerTool("list_project_mcp", {
|
|
9
|
+
description: "List git-root .mcp.json servers as names and transport only (same as GET /api/project-mcp). Inspect is not attach; it does not interpolate, spawn, or change stage MCP.",
|
|
10
|
+
inputSchema: z.object({}),
|
|
11
|
+
}, async () => textResult(await listProjectMcpCatalog(root)));
|
|
12
|
+
server.registerTool("probe_project_mcp", {
|
|
13
|
+
description: "Probe one named git-root .mcp.json server with connect-and-exit (same as POST /api/project-mcp/:name/probe). Inspect is not attach; YAML mcp: still allowlists what a stage receives.",
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
name: z.string(),
|
|
16
|
+
}),
|
|
17
|
+
}, async ({ name }, ctx) => {
|
|
18
|
+
const result = await probeProjectMcpServer({
|
|
19
|
+
projectRoot: root,
|
|
20
|
+
name,
|
|
21
|
+
signal: ctx.mcpReq.signal,
|
|
22
|
+
});
|
|
23
|
+
return textResult(result);
|
|
24
|
+
});
|
|
25
|
+
}
|