stageflow 0.19.0 → 0.23.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 +2 -1
- 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/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/runsCommand.d.ts +3 -7
- package/dist/cli/runsCommand.js +33 -94
- package/dist/cli.js +1 -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 +146 -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/port.d.ts +3 -1
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +15 -1
- 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 +3 -3
- package/dist/runtime/pipelineRunner.js +3 -1
- package/dist/runtime/resumeReconstruct.js +2 -2
- package/dist/runtime/runManager.d.ts +2 -1
- package/dist/runtime/runManager.js +36 -16
- 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.js +9 -2
- 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 +1 -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,30 @@
|
|
|
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";
|
|
4
6
|
import { validateCatalog } from "../config/validateCatalog.js";
|
|
7
|
+
import { PACKAGE_VERSION } from "../package-meta.js";
|
|
8
|
+
import { findProjectRoot } from "../project/findProjectRoot.js";
|
|
9
|
+
import { PipelineValidationError } from "../runtime/pipelineValidationError.js";
|
|
10
|
+
import { mapStoreLookupError } from "../server/operatorResults.js";
|
|
5
11
|
import { projectRunForMcp } from "./projectRun.js";
|
|
6
|
-
import {
|
|
7
|
-
import { textResult } from "./toolResults.js";
|
|
12
|
+
import { classifyArtifactContent, readRunArtifactBytes } from "./readArtifact.js";
|
|
13
|
+
import { imageResult, textResult } from "./toolResults.js";
|
|
14
|
+
/**
|
|
15
|
+
* Every project a host serving a global store has ever recorded a run for,
|
|
16
|
+
* plus this host's own launch directory (always included so a project with
|
|
17
|
+
* zero runs yet still sees its own catalog).
|
|
18
|
+
*/
|
|
19
|
+
async function catalogRootsFor(deps) {
|
|
20
|
+
const known = await deps.store.listProjectRoots();
|
|
21
|
+
return [...new Set([...known, deps.cwd])];
|
|
22
|
+
}
|
|
23
|
+
/** Resolve which project a given catalog path (pipeline/task) belongs to. */
|
|
24
|
+
function projectRootForPath(deps, catalogPath) {
|
|
25
|
+
const absDir = path.dirname(path.resolve(deps.cwd, catalogPath));
|
|
26
|
+
return findProjectRoot(absDir) ?? deps.cwd;
|
|
27
|
+
}
|
|
8
28
|
const taskFileSchema = z.object({
|
|
9
29
|
id: z.string(),
|
|
10
30
|
goal: z.string(),
|
|
@@ -13,9 +33,27 @@ const taskFileSchema = z.object({
|
|
|
13
33
|
checkout: z.string().optional(),
|
|
14
34
|
input: z.record(z.string(), z.unknown()).optional(),
|
|
15
35
|
});
|
|
36
|
+
/**
|
|
37
|
+
* Deliberately thin: only enough shape to route to the inline-pipeline
|
|
38
|
+
* loader. Real structural validation (required io schemas, DAG shape, stage
|
|
39
|
+
* id uniqueness) happens downstream through the same validator a file-based
|
|
40
|
+
* pipeline goes through, so a caller gets the same ValidationFinding-shaped
|
|
41
|
+
* error either way instead of a raw schema-validation error.
|
|
42
|
+
*/
|
|
43
|
+
const inlinePipelineSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
id: z.string().min(1),
|
|
46
|
+
stages: z.array(z.record(z.string(), z.unknown())).min(1),
|
|
47
|
+
agent: z.unknown().optional(),
|
|
48
|
+
model: z.unknown().optional(),
|
|
49
|
+
schemas: z.unknown().optional(),
|
|
50
|
+
})
|
|
51
|
+
.strict();
|
|
16
52
|
const startRunSchema = z
|
|
17
53
|
.object({
|
|
18
|
-
pipeline: z
|
|
54
|
+
pipeline: z
|
|
55
|
+
.union([z.string(), inlinePipelineSchema])
|
|
56
|
+
.describe("Filesystem path to a pipeline YAML file, or an inline pipeline definition object ({ id, stages: [...] }) authored directly in this call"),
|
|
19
57
|
task_path: z.string().optional(),
|
|
20
58
|
task: taskFileSchema.optional(),
|
|
21
59
|
})
|
|
@@ -26,18 +64,49 @@ const runStatusSchema = z.enum(["created", "running", "succeeded", "failed"]);
|
|
|
26
64
|
export function registerCatalogTools(server, deps) {
|
|
27
65
|
const { manager, store, cwd } = deps;
|
|
28
66
|
server.registerTool("list_pipelines", {
|
|
29
|
-
description: "List manifest-declared pipeline paths
|
|
67
|
+
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
68
|
inputSchema: z.object({}),
|
|
31
69
|
}, async () => {
|
|
32
|
-
const
|
|
33
|
-
|
|
70
|
+
const roots = await catalogRootsFor(deps);
|
|
71
|
+
const pipelines = [];
|
|
72
|
+
for (const root of roots) {
|
|
73
|
+
try {
|
|
74
|
+
const catalog = await browseCatalog(root);
|
|
75
|
+
for (const p of catalog.pipelines) {
|
|
76
|
+
pipelines.push({ ...p, project_root: root });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// stale/unreadable project root recorded on an old run; skip it
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return textResult({ pipelines });
|
|
34
84
|
});
|
|
35
85
|
server.registerTool("list_tasks", {
|
|
36
|
-
description: "List manifest-declared task paths
|
|
86
|
+
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.",
|
|
87
|
+
inputSchema: z.object({}),
|
|
88
|
+
}, async () => {
|
|
89
|
+
const roots = await catalogRootsFor(deps);
|
|
90
|
+
const tasks = [];
|
|
91
|
+
for (const root of roots) {
|
|
92
|
+
try {
|
|
93
|
+
const catalog = await browseCatalog(root);
|
|
94
|
+
for (const t of catalog.tasks) {
|
|
95
|
+
tasks.push({ ...t, project_root: root });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// stale/unreadable project root recorded on an old run; skip it
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return textResult({ tasks });
|
|
103
|
+
});
|
|
104
|
+
server.registerTool("list_models", {
|
|
105
|
+
description: "List catalog model ids from the project catalog (same source as GET /api/models)",
|
|
37
106
|
inputSchema: z.object({}),
|
|
38
107
|
}, async () => {
|
|
39
108
|
const catalog = await browseCatalog(cwd);
|
|
40
|
-
return textResult({
|
|
109
|
+
return textResult({ models: catalog.models });
|
|
41
110
|
});
|
|
42
111
|
server.registerTool("list_runs", {
|
|
43
112
|
description: "List known pipeline runs. Optional filters: status, since (ISO created_at lower bound), pipeline (id or path).",
|
|
@@ -62,21 +131,30 @@ export function registerCatalogTools(server, deps) {
|
|
|
62
131
|
return textResult({ runs });
|
|
63
132
|
});
|
|
64
133
|
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.",
|
|
134
|
+
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
135
|
inputSchema: z.object({}),
|
|
67
|
-
}, async () => textResult(manager.getHealth()));
|
|
136
|
+
}, async () => textResult({ ...manager.getHealth(), version: PACKAGE_VERSION }));
|
|
68
137
|
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.",
|
|
138
|
+
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
139
|
inputSchema: startRunSchema,
|
|
71
140
|
}, async ({ pipeline, task_path, task }) => {
|
|
72
|
-
if (!pipeline.trim()) {
|
|
141
|
+
if (typeof pipeline === "string" && !pipeline.trim()) {
|
|
73
142
|
return textResult({ error: "pipeline is required" }, true);
|
|
74
143
|
}
|
|
75
144
|
const taskInput = task_path ?? task;
|
|
76
145
|
if (taskInput === undefined) {
|
|
77
146
|
return textResult({ error: "Exactly one of task_path or task is required" }, true);
|
|
78
147
|
}
|
|
79
|
-
|
|
148
|
+
let result;
|
|
149
|
+
try {
|
|
150
|
+
result = await manager.startRun({ pipeline, task: taskInput });
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (err instanceof PipelineValidationError) {
|
|
154
|
+
return textResult({ error: "Pipeline validation failed", validation: err.result }, true);
|
|
155
|
+
}
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
80
158
|
if (!result.ok) {
|
|
81
159
|
const { ok: _ok, reason, ...rest } = result;
|
|
82
160
|
return textResult({ error: reason, ...rest }, true);
|
|
@@ -94,34 +172,39 @@ export function registerCatalogTools(server, deps) {
|
|
|
94
172
|
return textResult(projectRunForMcp(detail));
|
|
95
173
|
}
|
|
96
174
|
catch (err) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
return textResult({
|
|
100
|
-
error: message,
|
|
101
|
-
status: notFound ? 404 : 500,
|
|
102
|
-
}, true);
|
|
175
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
176
|
+
return textResult({ error: mapped.error, status: mapped.status }, true);
|
|
103
177
|
}
|
|
104
178
|
});
|
|
105
179
|
server.registerTool("read_artifact", {
|
|
106
|
-
description: "Read a
|
|
180
|
+
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
181
|
inputSchema: z.object({
|
|
108
182
|
runId: z.string(),
|
|
109
183
|
path: z.string(),
|
|
110
184
|
}),
|
|
111
185
|
}, async ({ runId, path: artifactPath }) => {
|
|
112
186
|
try {
|
|
113
|
-
const
|
|
114
|
-
|
|
187
|
+
const bytes = await readRunArtifactBytes(store, runId, artifactPath);
|
|
188
|
+
const classified = classifyArtifactContent(artifactPath, bytes);
|
|
189
|
+
if (classified.kind === "image") {
|
|
190
|
+
return imageResult(classified.mimeType, bytes, {
|
|
191
|
+
runId,
|
|
192
|
+
path: artifactPath,
|
|
193
|
+
mimeType: classified.mimeType,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (classified.kind === "utf8") {
|
|
197
|
+
return textResult({
|
|
198
|
+
runId,
|
|
199
|
+
path: artifactPath,
|
|
200
|
+
content: bytes.toString("utf8"),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return textResult({ error: "Artifact is not valid UTF-8 text", status: 400 }, true);
|
|
115
204
|
}
|
|
116
205
|
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);
|
|
206
|
+
const mapped = mapStoreLookupError(err, { policy: "artifact" });
|
|
207
|
+
return textResult({ error: mapped.error, status: mapped.status }, true);
|
|
125
208
|
}
|
|
126
209
|
});
|
|
127
210
|
server.registerTool("validate", {
|
|
@@ -138,11 +221,36 @@ export function registerCatalogTools(server, deps) {
|
|
|
138
221
|
scope = "pipeline";
|
|
139
222
|
else if (task !== undefined && task.trim())
|
|
140
223
|
scope = "task";
|
|
224
|
+
if (scope === "full") {
|
|
225
|
+
const roots = await catalogRootsFor(deps);
|
|
226
|
+
const results = [];
|
|
227
|
+
for (const root of roots) {
|
|
228
|
+
try {
|
|
229
|
+
results.push(await validateCatalog({ cwd: root, scope: "full", strict: strict ?? false }));
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// stale/unreadable project root recorded on an old run; skip it
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const merged = {
|
|
236
|
+
scope: "full",
|
|
237
|
+
ok: results.every((r) => r.ok),
|
|
238
|
+
summary: {
|
|
239
|
+
errors: results.reduce((n, r) => n + r.summary.errors, 0),
|
|
240
|
+
warnings: results.reduce((n, r) => n + r.summary.warnings, 0),
|
|
241
|
+
},
|
|
242
|
+
findings: results.flatMap((r) => r.findings),
|
|
243
|
+
};
|
|
244
|
+
return textResult(merged);
|
|
245
|
+
}
|
|
246
|
+
const target = scope === "pipeline" ? pipeline.trim() : task.trim();
|
|
247
|
+
const projectRoot = projectRootForPath(deps, target);
|
|
141
248
|
const result = await validateCatalog({
|
|
142
|
-
cwd,
|
|
249
|
+
cwd: projectRoot,
|
|
143
250
|
scope,
|
|
144
|
-
|
|
145
|
-
...(scope === "
|
|
251
|
+
projectRoot,
|
|
252
|
+
...(scope === "pipeline" ? { pipeline: target } : {}),
|
|
253
|
+
...(scope === "task" ? { task: target } : {}),
|
|
146
254
|
strict: strict ?? false,
|
|
147
255
|
});
|
|
148
256
|
return textResult(result);
|
|
@@ -153,7 +261,7 @@ export function registerCatalogTools(server, deps) {
|
|
|
153
261
|
}
|
|
154
262
|
});
|
|
155
263
|
server.registerTool("describe_pipeline", {
|
|
156
|
-
description: "Describe a pipeline DAG: stages with needs, fork,
|
|
264
|
+
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
265
|
inputSchema: z.object({
|
|
158
266
|
pipeline: z.string(),
|
|
159
267
|
}),
|
|
@@ -162,24 +270,10 @@ export function registerCatalogTools(server, deps) {
|
|
|
162
270
|
return textResult({ error: "pipeline is required", status: 400 }, true);
|
|
163
271
|
}
|
|
164
272
|
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,
|
|
273
|
+
const loaded = await loadPipeline(pipeline.trim(), {
|
|
274
|
+
cwd: projectRootForPath(deps, pipeline.trim()),
|
|
182
275
|
});
|
|
276
|
+
return textResult(describePipeline(loaded));
|
|
183
277
|
}
|
|
184
278
|
catch (err) {
|
|
185
279
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { inspectProviderReadiness, mapProviderAuthError, } from "../agent/providerInspect.js";
|
|
3
|
+
import { textResult } from "./toolResults.js";
|
|
4
|
+
export function registerProviderTools(server, deps) {
|
|
5
|
+
server.registerTool("list_providers", {
|
|
6
|
+
description: "List login-capable model providers with auth readiness and a Pi-home detect summary. Read-only; does not log in or return secrets.",
|
|
7
|
+
inputSchema: z.object({}),
|
|
8
|
+
}, async () => {
|
|
9
|
+
try {
|
|
10
|
+
return textResult(await inspectProviderReadiness(deps.cwd, deps.providerAuthContext));
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const mapped = mapProviderAuthError(err);
|
|
14
|
+
return textResult({ error: mapped.body.error, status: mapped.status }, true);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { RunStore } from "../runstore/port.js";
|
|
2
2
|
export declare function artifactMediaType(relativePath: string): string | undefined;
|
|
3
|
+
export type ClassifiedArtifactContent = {
|
|
4
|
+
kind: "image";
|
|
5
|
+
mimeType: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "utf8";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "binary";
|
|
10
|
+
};
|
|
11
|
+
export declare function classifyArtifactContent(relativePath: string, bytes: Buffer): ClassifiedArtifactContent;
|
|
3
12
|
export declare function readRunArtifact(store: RunStore, runId: string, relativePath: string): Promise<string>;
|
|
4
13
|
export declare function readRunArtifactBytes(store: RunStore, runId: string, relativePath: string): Promise<Buffer>;
|
package/dist/mcp/readArtifact.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isUtf8 } from "node:buffer";
|
|
1
2
|
import { readFile, realpath } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { isInsideDir } from "../runstore/workspaceLayout.js";
|
|
@@ -28,6 +29,16 @@ export function artifactMediaType(relativePath) {
|
|
|
28
29
|
return undefined;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
export function classifyArtifactContent(relativePath, bytes) {
|
|
33
|
+
const mimeType = artifactMediaType(relativePath);
|
|
34
|
+
if (mimeType !== undefined) {
|
|
35
|
+
return { kind: "image", mimeType };
|
|
36
|
+
}
|
|
37
|
+
if (isUtf8(bytes)) {
|
|
38
|
+
return { kind: "utf8" };
|
|
39
|
+
}
|
|
40
|
+
return { kind: "binary" };
|
|
41
|
+
}
|
|
31
42
|
async function resolveRunArtifactFile(store, runId, relativePath) {
|
|
32
43
|
assertSafeRunId(runId);
|
|
33
44
|
if (typeof relativePath !== "string" || relativePath.trim().length === 0) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -34,7 +34,15 @@ function sessionIdFromRequest(req) {
|
|
|
34
34
|
return undefined;
|
|
35
35
|
}
|
|
36
36
|
function toolDepsFrom(deps) {
|
|
37
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
manager: deps.manager,
|
|
39
|
+
store: deps.store,
|
|
40
|
+
cwd: deps.cwd,
|
|
41
|
+
...(deps.providerAuthContext !== undefined
|
|
42
|
+
? { providerAuthContext: deps.providerAuthContext }
|
|
43
|
+
: {}),
|
|
44
|
+
...(deps.projectRoot !== undefined ? { projectRoot: deps.projectRoot } : {}),
|
|
45
|
+
};
|
|
38
46
|
}
|
|
39
47
|
function attachBusNotifications(server, bus) {
|
|
40
48
|
return bus.on((event) => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type StreamLogTail = {
|
|
2
|
+
text: string;
|
|
3
|
+
nextOffset: number;
|
|
4
|
+
truncated?: boolean;
|
|
5
|
+
earliestOffset?: number;
|
|
6
|
+
};
|
|
7
|
+
/** Read new content from a stage's stream.log since a caller's last cursor. */
|
|
8
|
+
export declare function readStreamLogTail(streamLogPath: string, sinceOffset: number | undefined): Promise<StreamLogTail>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { parseStreamLogHeader } from "../runtime/stageStreamLog.js";
|
|
3
|
+
/** Read new content from a stage's stream.log since a caller's last cursor. */
|
|
4
|
+
export async function readStreamLogTail(streamLogPath, sinceOffset) {
|
|
5
|
+
let raw;
|
|
6
|
+
try {
|
|
7
|
+
raw = await readFile(streamLogPath);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return { text: "", nextOffset: 0 };
|
|
11
|
+
}
|
|
12
|
+
const { base, headerLength } = parseStreamLogHeader(raw);
|
|
13
|
+
const contentBytes = raw.subarray(headerLength);
|
|
14
|
+
const eof = base + contentBytes.length;
|
|
15
|
+
if (sinceOffset === undefined || sinceOffset < base) {
|
|
16
|
+
const truncated = sinceOffset !== undefined && sinceOffset < base;
|
|
17
|
+
return {
|
|
18
|
+
text: contentBytes.toString("utf8"),
|
|
19
|
+
nextOffset: eof,
|
|
20
|
+
...(truncated ? { truncated: true, earliestOffset: base } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (sinceOffset >= eof) {
|
|
24
|
+
return { text: "", nextOffset: eof };
|
|
25
|
+
}
|
|
26
|
+
const slice = contentBytes.subarray(sinceOffset - base);
|
|
27
|
+
return { text: slice.toString("utf8"), nextOffset: eof };
|
|
28
|
+
}
|
|
@@ -5,3 +5,20 @@ export declare function textResult(data: unknown, isError?: boolean): {
|
|
|
5
5
|
}[];
|
|
6
6
|
isError?: boolean | undefined;
|
|
7
7
|
};
|
|
8
|
+
export declare function imageResult(mimeType: string, bytes: Buffer, identity: {
|
|
9
|
+
runId: string;
|
|
10
|
+
path: string;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
}): {
|
|
13
|
+
content: ({
|
|
14
|
+
type: "image";
|
|
15
|
+
mimeType: string;
|
|
16
|
+
data: string;
|
|
17
|
+
text?: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
mimeType?: undefined;
|
|
20
|
+
data?: undefined;
|
|
21
|
+
type: "text";
|
|
22
|
+
text: string;
|
|
23
|
+
})[];
|
|
24
|
+
};
|
package/dist/mcp/toolResults.js
CHANGED
|
@@ -4,3 +4,18 @@ export function textResult(data, isError = false) {
|
|
|
4
4
|
...(isError ? { isError: true } : {}),
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
|
+
export function imageResult(mimeType, bytes, identity) {
|
|
8
|
+
return {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: "image",
|
|
12
|
+
mimeType,
|
|
13
|
+
data: bytes.toString("base64"),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
type: "text",
|
|
17
|
+
text: JSON.stringify(identity, null, 2),
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
}
|