stageflow 0.9.0 → 0.13.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 +6 -1
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/agentBackend.d.ts +32 -0
- package/dist/agent/agentBackend.js +38 -0
- package/dist/agent/claudeActivity.d.ts +15 -0
- package/dist/agent/claudeActivity.js +58 -0
- package/dist/agent/claudeAdapter.d.ts +5 -0
- package/dist/agent/claudeAdapter.js +331 -0
- package/dist/agent/claudeSession.d.ts +18 -0
- package/dist/agent/claudeSession.js +54 -0
- package/dist/agent/claudeTools.d.ts +89 -0
- package/dist/agent/claudeTools.js +135 -0
- package/dist/agent/fakeAgent.js +24 -4
- package/dist/agent/piAdapter.d.ts +12 -7
- package/dist/agent/piAdapter.js +194 -44
- package/dist/agent/piIsolatedMcp.d.ts +23 -0
- package/dist/agent/piIsolatedMcp.js +223 -0
- package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
- package/dist/agent/piIsolatedMcpProbe.js +292 -0
- package/dist/agent/port.d.ts +52 -1
- package/dist/agent/port.js +1 -1
- package/dist/agent/resolveAgentPort.d.ts +26 -0
- package/dist/agent/resolveAgentPort.js +39 -0
- package/dist/cli/runCommand.js +2 -2
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +184 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +11 -5
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +2 -1
- package/dist/config/createPipeline.js +34 -9
- package/dist/config/createStage.js +14 -9
- package/dist/config/loadPipeline.js +11 -1
- package/dist/config/loadStage.d.ts +1 -0
- package/dist/config/loadStage.js +207 -2
- package/dist/config/loadStageflowManifest.js +14 -1
- package/dist/config/mergePipelineIncludes.d.ts +1 -0
- package/dist/config/mergePipelineIncludes.js +14 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
- package/dist/config/normalizePipelineStageEntry.js +56 -7
- package/dist/config/parseCompletionContract.d.ts +8 -0
- package/dist/config/parseCompletionContract.js +218 -0
- package/dist/config/parsePreEmitChecks.d.ts +3 -0
- package/dist/config/parsePreEmitChecks.js +65 -0
- package/dist/config/pipelineNeeds.d.ts +19 -0
- package/dist/config/pipelineNeeds.js +151 -0
- package/dist/config/pipelineStageKeys.js +17 -1
- package/dist/config/resolveForkEmitContext.d.ts +6 -2
- package/dist/config/resolveForkEmitContext.js +9 -2
- package/dist/config/resolvePipelineDag.d.ts +2 -1
- package/dist/config/resolvePipelineDag.js +182 -39
- package/dist/config/resolveStageMcpServers.d.ts +35 -0
- package/dist/config/resolveStageMcpServers.js +207 -0
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +51 -0
- package/dist/config/validateCompletionContract.d.ts +4 -0
- package/dist/config/validateCompletionContract.js +52 -0
- package/dist/envelope/check.d.ts +1 -1
- package/dist/envelope/check.js +86 -13
- package/dist/envelope/cloneForks.d.ts +1 -1
- package/dist/envelope/cloneForks.js +24 -9
- package/dist/envelope/feedbackLoop.d.ts +4 -0
- package/dist/envelope/feedbackLoop.js +31 -0
- package/dist/envelope/payloadSchema.d.ts +7 -1
- package/dist/envelope/payloadSchema.js +170 -13
- package/dist/envelope/preEmitChecks.d.ts +29 -0
- package/dist/envelope/preEmitChecks.js +57 -0
- package/dist/hitl/qaTrail.d.ts +25 -0
- package/dist/hitl/qaTrail.js +51 -0
- package/dist/mcp/catalogTools.js +4 -2
- package/dist/mcp/controlTools.js +72 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +25 -1
- package/dist/mcp/waitingGates.d.ts +2 -1
- package/dist/mcp/waitingGates.js +17 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +4 -1
- package/dist/projection/projectRun.js +7 -0
- package/dist/prompt/feedbackLoopContext.d.ts +2 -0
- package/dist/prompt/feedbackLoopContext.js +17 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -2
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
- package/dist/runstore/pipelineDagSnapshot.js +51 -11
- package/dist/runstore/port.d.ts +185 -3
- package/dist/runstore/port.js +42 -3
- package/dist/runstore/runProjection.d.ts +7 -3
- package/dist/runstore/runProjection.js +30 -15
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +99 -0
- package/dist/runstore/stageInstanceId.d.ts +1 -1
- package/dist/runstore/stageInstanceId.js +3 -2
- package/dist/runstore/trackProjection.js +59 -27
- package/dist/runstore/verificationHistory.d.ts +19 -0
- package/dist/runstore/verificationHistory.js +30 -0
- package/dist/runtime/cloneSchedule.d.ts +40 -4
- package/dist/runtime/cloneSchedule.js +212 -28
- package/dist/runtime/completionCheckRunner.d.ts +137 -0
- package/dist/runtime/completionCheckRunner.js +365 -0
- package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
- package/dist/runtime/completionCheckoutBaseline.js +44 -0
- package/dist/runtime/envelopeRouting.d.ts +10 -3
- package/dist/runtime/envelopeRouting.js +146 -5
- package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
- package/dist/runtime/feedbackLoopCoordinator.js +446 -0
- package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
- package/dist/runtime/feedbackLoopDecision.js +207 -0
- package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
- package/dist/runtime/feedbackLoopRoute.js +13 -0
- package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
- package/dist/runtime/feedbackLoopSchedule.js +72 -0
- package/dist/runtime/forkGeneration.d.ts +72 -0
- package/dist/runtime/forkGeneration.js +295 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
- package/dist/runtime/gitCheckoutCapability.js +97 -0
- package/dist/runtime/manualRecoveryState.d.ts +40 -0
- package/dist/runtime/manualRecoveryState.js +73 -0
- package/dist/runtime/pipelineScheduler.d.ts +13 -1
- package/dist/runtime/pipelineScheduler.js +328 -45
- package/dist/runtime/replayLifecycle.d.ts +107 -0
- package/dist/runtime/replayLifecycle.js +168 -0
- package/dist/runtime/runManager.d.ts +54 -1
- package/dist/runtime/runManager.js +471 -6
- package/dist/runtime/runRetryCoordinator.d.ts +6 -1
- package/dist/runtime/runRetryCoordinator.js +43 -9
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
- package/dist/runtime/stageAttemptBootstrap.js +117 -3
- package/dist/runtime/stageProcessLauncher.d.ts +1 -1
- package/dist/runtime/stageRecovery.js +1 -1
- package/dist/runtime/stageRunner.d.ts +4 -1
- package/dist/runtime/stageRunner.js +29 -3
- package/dist/runtime/stageWorker.js +69 -2
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
- package/dist/runtime/verifiedStageExecution.d.ts +31 -0
- package/dist/runtime/verifiedStageExecution.js +127 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +137 -4
- package/dist/server/operatorResults.js +2 -0
- package/dist/server/projectMcpRoutes.d.ts +6 -0
- package/dist/server/projectMcpRoutes.js +38 -0
- package/dist/tools/askOperator.d.ts +161 -5
- package/dist/tools/askOperator.js +58 -25
- package/dist/tools/emitStageEnvelope.d.ts +12 -12
- package/dist/tools/emitStageEnvelope.js +112 -24
- package/dist/types/completion.d.ts +43 -0
- package/dist/types/completion.js +1 -0
- package/dist/types/envelope.d.ts +18 -0
- package/dist/types/forkChoice.d.ts +9 -4
- package/dist/types/forkChoice.js +1 -1
- package/dist/types/pipeline.d.ts +41 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +14 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-Yzh42vhf.js +118 -0
- package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +5 -1
- package/skills/stageflow-author/SKILL.md +20 -2
- package/skills/stageflow-author/references/catalog-mapping.md +88 -3
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
- package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -1
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
package/dist/agent/piAdapter.js
CHANGED
|
@@ -26,14 +26,17 @@
|
|
|
26
26
|
* fails closed with `StageSessionReconstructError` (KTD7). Epic proving
|
|
27
27
|
* vehicle: pipeline `plan-review-proving` (Pi live wait + console answers).
|
|
28
28
|
*
|
|
29
|
-
* `ask_operator` is
|
|
30
|
-
*
|
|
29
|
+
* `ask_operator` is registered on sealed stage sessions unless the stage
|
|
30
|
+
* declares `gate_kinds: []`.
|
|
31
31
|
*/
|
|
32
32
|
import { existsSync } from "node:fs";
|
|
33
33
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
34
34
|
import path from "node:path";
|
|
35
35
|
import { createAgentSession, DefaultResourceLoader, defineTool, ModelRuntime, resolveCliModel, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
36
|
+
import { StageMcpError } from "../config/resolveStageMcpServers.js";
|
|
37
|
+
import { attachIsolatedMcp, STAGEFLOW_PI_MCP_EXTENSION_NAME, } from "./piIsolatedMcp.js";
|
|
36
38
|
import { isAdvancingEnvelope } from "../envelope/check.js";
|
|
39
|
+
import { formatFeedbackLoopContext } from "../prompt/feedbackLoopContext.js";
|
|
37
40
|
import { formatPriorEnvelope } from "../prompt/priorEnvelope.js";
|
|
38
41
|
import { attemptContext, noAttemptContext, } from "../runtime/stageAttemptContext.js";
|
|
39
42
|
import { createEmitStageEnvelopeTool, } from "../tools/emitStageEnvelope.js";
|
|
@@ -46,18 +49,21 @@ import { createStageActivityObserver, } from "./activityObserver.js";
|
|
|
46
49
|
import { DEFAULT_STAGE_TIMEOUT_MS, runtimeStageId, runStageViaOpen } from "./port.js";
|
|
47
50
|
/**
|
|
48
51
|
* Stage tool allowlist for sealed Pi sessions.
|
|
49
|
-
*
|
|
50
|
-
* (always, for bound and unbound —
|
|
52
|
+
* Includes `ask_operator` unless `gateKinds` is an empty list (R6).
|
|
53
|
+
* `write_stage_artifact` when registered (always, for bound and unbound —
|
|
54
|
+
* bound/unbound only gates cwd / env bind).
|
|
51
55
|
*/
|
|
52
|
-
export function resolveStageToolNames(emitToolName, artifactToolName, askOperatorToolName = "ask_operator") {
|
|
56
|
+
export function resolveStageToolNames(emitToolName, artifactToolName, askOperatorToolName = "ask_operator", gateKinds) {
|
|
53
57
|
const tools = [
|
|
54
58
|
"read",
|
|
55
59
|
"bash",
|
|
56
60
|
"write",
|
|
57
61
|
"edit",
|
|
58
62
|
emitToolName,
|
|
59
|
-
askOperatorToolName,
|
|
60
63
|
];
|
|
64
|
+
if (gateKinds === undefined || gateKinds.length > 0) {
|
|
65
|
+
tools.push(askOperatorToolName);
|
|
66
|
+
}
|
|
61
67
|
if (artifactToolName) {
|
|
62
68
|
tools.push(artifactToolName);
|
|
63
69
|
}
|
|
@@ -364,6 +370,37 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
|
|
|
364
370
|
if (input.stage.payload_schema !== undefined) {
|
|
365
371
|
emitHint += `\nOn status=success, payload is required and must match this JSON Schema:\n${JSON.stringify(input.stage.payload_schema, null, 2)}`;
|
|
366
372
|
}
|
|
373
|
+
const checklistChecks = input.completionContract?.checks.filter((check) => check.type === "checklist") ?? [];
|
|
374
|
+
if (checklistChecks.length > 0) {
|
|
375
|
+
emitHint += `\nBefore emitting success, self-review every required checklist item. Include checklist_attestations in the envelope with each check_id and its complete item list exactly as declared:\n${JSON.stringify(checklistChecks, null, 2)}`;
|
|
376
|
+
}
|
|
377
|
+
if (input.repairContext !== undefined) {
|
|
378
|
+
emitHint += `\nThis is recovery attempt ${input.roots.attempt ?? input.repairContext.prior_attempt + 1}. The previous candidate did not pass Stageflow verification. Address the failed checks before emitting success.`;
|
|
379
|
+
if (input.repairContext.operator_guidance) {
|
|
380
|
+
emitHint += `\nThe operator approved this retry with these instructions:\n${input.repairContext.operator_guidance}`;
|
|
381
|
+
}
|
|
382
|
+
if (input.repairContext.failed_checks?.length) {
|
|
383
|
+
emitHint += `\nFailed-check evidence from attempt ${input.repairContext.prior_attempt}:\n${JSON.stringify(input.repairContext.failed_checks, null, 2)}`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (input.cloneEmitContext !== undefined) {
|
|
387
|
+
const allowedActions = input.cloneEmitContext.allowedActions ?? ["skip", "once", "fanout"];
|
|
388
|
+
const successorLines = input.cloneEmitContext.clonableSuccessors.map((successor) => {
|
|
389
|
+
const header = `- ${successor.successorId} (clone_cap: ${successor.cloneCap})`;
|
|
390
|
+
if (successor.cloneInputSchema === undefined) {
|
|
391
|
+
return header;
|
|
392
|
+
}
|
|
393
|
+
return `${header}\n Assignment schema (clone_input_schema):\n${JSON.stringify(successor.cloneInputSchema, null, 2)}`;
|
|
394
|
+
});
|
|
395
|
+
emitHint += `\nClonable successors — emit clone_forks covering each of these ids exactly once:\n${successorLines.join("\n")}`;
|
|
396
|
+
emitHint += `\nEach once or fanout envelope is a full StageEnvelope — it requires status ("success" or "failure"), summary (non-empty string), and artifacts (array of strings). clone_input_schema fields belong in envelope.payload, not at the top level of the clone_forks item.`;
|
|
397
|
+
emitHint += `\nAllowed clone actions: ${allowedActions.join(", ")}`;
|
|
398
|
+
}
|
|
399
|
+
if (input.feedbackLoopEmitContext !== undefined) {
|
|
400
|
+
emitHint += `\nFeedback-loop decision: this stage is a feedback-loop source. On status=success, feedback_loop is required. Emit either {"action":"continue"} to proceed downstream or {"action":"send_back","target":"…"} to request another pass.`;
|
|
401
|
+
emitHint += `\nAllowed send_back target: ${input.feedbackLoopEmitContext.target}.`;
|
|
402
|
+
emitHint += `\nA send_back decision cannot be combined with fork_choice or clone_forks.`;
|
|
403
|
+
}
|
|
367
404
|
const attempt = input.roots.attempt ?? 1;
|
|
368
405
|
const attemptArtifactsPath = `stages/${runtimeStageId(input)}/attempts/${attempt}/artifacts/`;
|
|
369
406
|
const skillBaseDir = input.stage.skill !== undefined && input.skillFilePath !== undefined
|
|
@@ -394,7 +431,10 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
|
|
|
394
431
|
`Goal: ${input.task.goal}`,
|
|
395
432
|
input.task.context ? `Context: ${input.task.context}` : "",
|
|
396
433
|
input.task.constraints ? `Constraints: ${input.task.constraints}` : "",
|
|
397
|
-
formatPriorEnvelope(input.priorEnvelope, input.priorEnvelopes),
|
|
434
|
+
formatPriorEnvelope(input.priorEnvelope, input.priorEnvelopes, input.priorEnvelopesByStage),
|
|
435
|
+
input.feedbackLoopContext !== undefined
|
|
436
|
+
? formatFeedbackLoopContext(input.feedbackLoopContext, input.sessionMode)
|
|
437
|
+
: "",
|
|
398
438
|
"",
|
|
399
439
|
artifactGuidance,
|
|
400
440
|
emitHint,
|
|
@@ -409,6 +449,16 @@ export function composeStageUserPrompt(input, emitToolName, emitHintOverride, ar
|
|
|
409
449
|
}
|
|
410
450
|
return body;
|
|
411
451
|
}
|
|
452
|
+
export function composeFeedbackResumePrompt(input) {
|
|
453
|
+
if (input.feedbackLoopContext === undefined) {
|
|
454
|
+
throw new Error("feedback_resume requires feedbackLoopContext");
|
|
455
|
+
}
|
|
456
|
+
return [
|
|
457
|
+
"Continue this stage after feedback-loop send-back.",
|
|
458
|
+
"Incorporate the operator/source feedback below, then complete the stage (emit an envelope when done).",
|
|
459
|
+
formatFeedbackLoopContext(input.feedbackLoopContext, input.sessionMode ?? "feedback_resume"),
|
|
460
|
+
].join("\n\n");
|
|
461
|
+
}
|
|
412
462
|
/**
|
|
413
463
|
* DefaultResourceLoader with host/global discovery turned off.
|
|
414
464
|
*
|
|
@@ -416,9 +466,9 @@ export function composeStageUserPrompt(input, emitToolName, emitHintOverride, ar
|
|
|
416
466
|
* up the consumer project's AGENTS.md, `.agents/skills/`, `.pi/extensions`,
|
|
417
467
|
* and APPEND_SYSTEM.md. Stages must not inherit that context.
|
|
418
468
|
*
|
|
419
|
-
* `additionalExtensionPaths` is the
|
|
420
|
-
*
|
|
421
|
-
*
|
|
469
|
+
* `additionalExtensionPaths` is the Cursor/provider seam. `extensionFactories`
|
|
470
|
+
* is the isolated MCP seam. With `noExtensions: true`, discovered
|
|
471
|
+
* global/project packages stay out; only those allowlists load.
|
|
422
472
|
* `additionalSkillPaths` is the matching allowlist for one named skill.
|
|
423
473
|
*/
|
|
424
474
|
export function createSealedResourceLoader(options) {
|
|
@@ -430,6 +480,10 @@ export function createSealedResourceLoader(options) {
|
|
|
430
480
|
appendSystemPromptOverride: () => [],
|
|
431
481
|
additionalExtensionPaths: options.additionalExtensionPaths,
|
|
432
482
|
additionalSkillPaths: options.additionalSkillPaths,
|
|
483
|
+
...(options.extensionFactories !== undefined
|
|
484
|
+
? { extensionFactories: options.extensionFactories }
|
|
485
|
+
: {}),
|
|
486
|
+
...(options.eventBus !== undefined ? { eventBus: options.eventBus } : {}),
|
|
433
487
|
noContextFiles: true,
|
|
434
488
|
noExtensions: true,
|
|
435
489
|
noSkills: true,
|
|
@@ -437,6 +491,16 @@ export function createSealedResourceLoader(options) {
|
|
|
437
491
|
noThemes: true,
|
|
438
492
|
});
|
|
439
493
|
}
|
|
494
|
+
function collectMcpExtensionToolNames(loader) {
|
|
495
|
+
const inlinePath = `<inline:${STAGEFLOW_PI_MCP_EXTENSION_NAME}>`;
|
|
496
|
+
const names = [];
|
|
497
|
+
for (const ext of loader.getExtensions().extensions) {
|
|
498
|
+
if (ext.path !== inlinePath)
|
|
499
|
+
continue;
|
|
500
|
+
names.push(...ext.tools.keys());
|
|
501
|
+
}
|
|
502
|
+
return names;
|
|
503
|
+
}
|
|
440
504
|
async function shutdownSession(session) {
|
|
441
505
|
if (!session) {
|
|
442
506
|
return;
|
|
@@ -686,12 +750,20 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
686
750
|
const provider = findProviderSupport(input.stage.model);
|
|
687
751
|
const capture = {};
|
|
688
752
|
const askWaitChannel = existingAskWaitChannel ?? new AskOperatorWaitChannel();
|
|
689
|
-
const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema, input.forkEmitContext, input.cloneEmitContext
|
|
753
|
+
const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema, input.forkEmitContext, input.cloneEmitContext, {
|
|
754
|
+
checks: input.stage.pre_emit_checks,
|
|
755
|
+
readQaTrail: input.readQaTrail,
|
|
756
|
+
}, input.feedbackLoopEmitContext);
|
|
690
757
|
const emitTool = defineTool(emitDef);
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
758
|
+
const gateKinds = input.stage.gate_kinds;
|
|
759
|
+
const includeAsk = gateKinds === undefined || gateKinds.length > 0;
|
|
760
|
+
const askDef = includeAsk
|
|
761
|
+
? createAskOperatorTool({
|
|
762
|
+
requestWait: (prompt) => askWaitChannel.requestWait(prompt),
|
|
763
|
+
...(gateKinds !== undefined ? { allowedKinds: gateKinds } : {}),
|
|
764
|
+
})
|
|
765
|
+
: undefined;
|
|
766
|
+
const askTool = askDef ? defineTool(askDef) : undefined;
|
|
695
767
|
const artifactDef = createWriteStageArtifactTool({
|
|
696
768
|
runWorkspaceDir: roots.runWorkspaceDir,
|
|
697
769
|
stageId: runtimeStageId(input),
|
|
@@ -714,11 +786,28 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
714
786
|
additionalExtensionPaths.push(...prepared.extensionPaths);
|
|
715
787
|
restoreProvider = prepared.restore;
|
|
716
788
|
}
|
|
789
|
+
let attached;
|
|
717
790
|
try {
|
|
718
|
-
const modelRuntime = await ModelRuntime.create(roots.authPath
|
|
791
|
+
const modelRuntime = await ModelRuntime.create(roots.authPath
|
|
792
|
+
? {
|
|
793
|
+
authPath: roots.authPath,
|
|
794
|
+
// ModelRuntime otherwise resolves models.json from getAgentDir(),
|
|
795
|
+
// which bindPiAgentDirEnv seals to an empty per-attempt sandbox
|
|
796
|
+
// dir. Point it at the same durable agent dir as authPath so
|
|
797
|
+
// custom providers (e.g. a self-hosted OpenAI-compatible
|
|
798
|
+
// gateway) defined there stay usable in sealed stage sessions.
|
|
799
|
+
modelsPath: path.join(path.dirname(roots.authPath), "models.json"),
|
|
800
|
+
}
|
|
801
|
+
: undefined);
|
|
719
802
|
const settingsManager = SettingsManager.inMemory({
|
|
720
803
|
compaction: { enabled: false },
|
|
721
804
|
});
|
|
805
|
+
attached = await attachIsolatedMcp(input.resolvedMcpServers);
|
|
806
|
+
const failAfterAttach = (reason) => {
|
|
807
|
+
attached?.cancel?.();
|
|
808
|
+
restoreProvider?.();
|
|
809
|
+
return { ok: false, reason };
|
|
810
|
+
};
|
|
722
811
|
const loader = createSealedResourceLoader({
|
|
723
812
|
cwd: roots.cwd,
|
|
724
813
|
agentDir: roots.agentDir,
|
|
@@ -728,57 +817,61 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
728
817
|
...(input.skillFilePath !== undefined
|
|
729
818
|
? { additionalSkillPaths: [input.skillFilePath] }
|
|
730
819
|
: {}),
|
|
820
|
+
...(attached.extensionFactories !== undefined
|
|
821
|
+
? { extensionFactories: attached.extensionFactories }
|
|
822
|
+
: {}),
|
|
823
|
+
...(attached.eventBus !== undefined ? { eventBus: attached.eventBus } : {}),
|
|
731
824
|
});
|
|
732
825
|
await loader.reload();
|
|
733
826
|
const extensionErrors = loader.getExtensions().errors;
|
|
734
827
|
if (extensionErrors.length > 0) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
reason: `Failed to load Pi extension(s): ${extensionErrors
|
|
739
|
-
.map((e) => `${e.path}: ${e.error}`)
|
|
740
|
-
.join("; ")}`,
|
|
741
|
-
};
|
|
828
|
+
return failAfterAttach(`Failed to load Pi extension(s): ${extensionErrors
|
|
829
|
+
.map((e) => `${e.path}: ${e.error}`)
|
|
830
|
+
.join("; ")}`);
|
|
742
831
|
}
|
|
743
832
|
const skillDiagnostics = loader
|
|
744
833
|
.getSkills()
|
|
745
834
|
.diagnostics.filter((d) => d.type === "error" || d.type === "collision");
|
|
746
835
|
if (skillDiagnostics.length > 0) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
reason: `Failed to load skill(s): ${skillDiagnostics
|
|
751
|
-
.map((d) => (d.path !== undefined ? `${d.path}: ${d.message}` : d.message))
|
|
752
|
-
.join("; ")}`,
|
|
753
|
-
};
|
|
836
|
+
return failAfterAttach(`Failed to load skill(s): ${skillDiagnostics
|
|
837
|
+
.map((d) => (d.path !== undefined ? `${d.path}: ${d.message}` : d.message))
|
|
838
|
+
.join("; ")}`);
|
|
754
839
|
}
|
|
755
840
|
if (input.stage.skill !== undefined &&
|
|
756
841
|
!loader.getSkills().skills.some((skill) => skill.name === input.stage.skill)) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
842
|
+
return failAfterAttach(`Skill "${input.stage.skill}" is not installed`);
|
|
843
|
+
}
|
|
844
|
+
const customTools = askTool
|
|
845
|
+
? [emitTool, askTool, artifactTool]
|
|
846
|
+
: [emitTool, artifactTool];
|
|
847
|
+
const tools = resolveStageToolNames(emitDef.name, artifactDef.name, askDef?.name ?? "ask_operator", gateKinds);
|
|
848
|
+
if (attached.extensionFactories !== undefined) {
|
|
849
|
+
tools.push(...collectMcpExtensionToolNames(loader));
|
|
762
850
|
}
|
|
763
|
-
const customTools = [emitTool, askTool, artifactTool];
|
|
764
851
|
return {
|
|
765
852
|
sessionManager,
|
|
766
853
|
modelRuntime,
|
|
767
854
|
settingsManager,
|
|
768
855
|
loader,
|
|
769
|
-
tools
|
|
856
|
+
tools,
|
|
770
857
|
customTools,
|
|
771
858
|
emitDefName: emitDef.name,
|
|
772
|
-
askOperatorDefName: askDef.name,
|
|
859
|
+
...(askDef ? { askOperatorDefName: askDef.name } : {}),
|
|
773
860
|
artifactDefName: artifactDef.name,
|
|
774
861
|
providerEmitHint: provider?.emitToolHint?.(emitDef.name),
|
|
775
862
|
restoreProvider,
|
|
776
863
|
capture,
|
|
777
864
|
askWaitChannel,
|
|
865
|
+
connecting: attached.connecting,
|
|
866
|
+
cancelConnecting: attached.cancel,
|
|
778
867
|
};
|
|
779
868
|
}
|
|
780
869
|
catch (err) {
|
|
870
|
+
attached?.cancel?.();
|
|
781
871
|
restoreProvider?.();
|
|
872
|
+
if (err instanceof StageMcpError) {
|
|
873
|
+
return { ok: false, reason: err.message };
|
|
874
|
+
}
|
|
782
875
|
throw err;
|
|
783
876
|
}
|
|
784
877
|
}
|
|
@@ -818,6 +911,9 @@ export async function reconstructStageSessionForAnswer(input, answer) {
|
|
|
818
911
|
});
|
|
819
912
|
session = created.session;
|
|
820
913
|
await session.bindExtensions({});
|
|
914
|
+
if (wiring.connecting !== undefined) {
|
|
915
|
+
await wiring.connecting;
|
|
916
|
+
}
|
|
821
917
|
const resolved = resolveCliModel({
|
|
822
918
|
cliModel: input.stage.model,
|
|
823
919
|
modelRuntime: wiring.modelRuntime,
|
|
@@ -845,6 +941,7 @@ export async function reconstructStageSessionForAnswer(input, answer) {
|
|
|
845
941
|
};
|
|
846
942
|
}
|
|
847
943
|
catch (err) {
|
|
944
|
+
wiring.cancelConnecting?.();
|
|
848
945
|
await shutdownSession(session);
|
|
849
946
|
wiring.restoreProvider?.();
|
|
850
947
|
if (err instanceof StageSessionReconstructError)
|
|
@@ -866,7 +963,20 @@ async function bindStageSession(input, sessionManager, wiring) {
|
|
|
866
963
|
});
|
|
867
964
|
const session = created.session;
|
|
868
965
|
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
869
|
-
|
|
966
|
+
try {
|
|
967
|
+
await session.bindExtensions({});
|
|
968
|
+
if (wiring.connecting !== undefined) {
|
|
969
|
+
await wiring.connecting;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
catch (err) {
|
|
973
|
+
wiring.cancelConnecting?.();
|
|
974
|
+
await shutdownSession(session);
|
|
975
|
+
if (err instanceof StageMcpError) {
|
|
976
|
+
return { ok: false, reason: err.message };
|
|
977
|
+
}
|
|
978
|
+
throw err;
|
|
979
|
+
}
|
|
870
980
|
const resolved = resolveCliModel({
|
|
871
981
|
cliModel: input.stage.model,
|
|
872
982
|
modelRuntime: wiring.modelRuntime,
|
|
@@ -901,30 +1011,65 @@ export class PiAgentAdapter {
|
|
|
901
1011
|
const timeoutMs = input.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
|
|
902
1012
|
const askWaitChannel = new AskOperatorWaitChannel();
|
|
903
1013
|
const sessionFile = resolveStageSessionFile(input);
|
|
1014
|
+
const sessionMode = input.sessionMode ?? "fresh";
|
|
904
1015
|
let sessionManager;
|
|
905
1016
|
let resumeWaiting = false;
|
|
906
|
-
|
|
1017
|
+
let feedbackResume = false;
|
|
1018
|
+
let forceNewSession = sessionMode === "new_session";
|
|
1019
|
+
if (sessionMode === "feedback_resume") {
|
|
1020
|
+
if (input.feedbackLoopContext === undefined) {
|
|
1021
|
+
throw new StageSessionReconstructError(`feedback_resume requires feedbackLoopContext: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
1022
|
+
}
|
|
1023
|
+
if (!existsSync(sessionFile)) {
|
|
1024
|
+
throw new StageSessionReconstructError(`stage session file missing for feedback_resume: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
1025
|
+
}
|
|
907
1026
|
try {
|
|
908
1027
|
sessionManager = SessionManager.open(sessionFile, path.dirname(sessionFile), input.roots.cwd);
|
|
909
1028
|
}
|
|
910
1029
|
catch (err) {
|
|
911
|
-
throw new StageSessionReconstructError(`stage session file corrupt or unreadable for
|
|
1030
|
+
throw new StageSessionReconstructError(`stage session file corrupt or unreadable for feedback_resume: ${sessionFile}`, {
|
|
912
1031
|
stageId: runtimeStageId(input),
|
|
913
1032
|
sessionFile,
|
|
914
1033
|
cause: err,
|
|
915
1034
|
});
|
|
916
1035
|
}
|
|
917
1036
|
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
918
|
-
|
|
919
|
-
|
|
1037
|
+
feedbackResume = true;
|
|
1038
|
+
}
|
|
1039
|
+
else if (sessionMode !== "new_session") {
|
|
1040
|
+
if (sessionMode === "waiting_resume" || existsSync(sessionFile)) {
|
|
1041
|
+
if (existsSync(sessionFile)) {
|
|
1042
|
+
try {
|
|
1043
|
+
sessionManager = SessionManager.open(sessionFile, path.dirname(sessionFile), input.roots.cwd);
|
|
1044
|
+
}
|
|
1045
|
+
catch (err) {
|
|
1046
|
+
throw new StageSessionReconstructError(`stage session file corrupt or unreadable for resume: ${sessionFile}`, {
|
|
1047
|
+
stageId: runtimeStageId(input),
|
|
1048
|
+
sessionFile,
|
|
1049
|
+
cause: err,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1053
|
+
if (!findOpenToolCall(sessionManager)) {
|
|
1054
|
+
throw new StageSessionReconstructError(`stage session has no open tool call for resume: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
1055
|
+
}
|
|
1056
|
+
resumeWaiting = true;
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
throw new StageSessionReconstructError(`stage session file missing for resume: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
1060
|
+
}
|
|
920
1061
|
}
|
|
921
|
-
resumeWaiting = true;
|
|
922
1062
|
}
|
|
923
1063
|
let session;
|
|
924
1064
|
let wiring;
|
|
925
1065
|
let unsubscribeProgress;
|
|
926
1066
|
let prepareError;
|
|
927
1067
|
const preparePromise = (async () => {
|
|
1068
|
+
if (forceNewSession) {
|
|
1069
|
+
await mkdir(path.dirname(sessionFile), { recursive: true });
|
|
1070
|
+
await writeFile(sessionFile, "");
|
|
1071
|
+
sessionManager = undefined;
|
|
1072
|
+
}
|
|
928
1073
|
if (!sessionManager) {
|
|
929
1074
|
sessionManager = await createStageSessionManager(input.roots, runtimeStageId(input), sessionFile);
|
|
930
1075
|
}
|
|
@@ -987,6 +1132,11 @@ export class PiAgentAdapter {
|
|
|
987
1132
|
},
|
|
988
1133
|
run: async () => {
|
|
989
1134
|
return runWithTimeout(async () => {
|
|
1135
|
+
if (feedbackResume) {
|
|
1136
|
+
syncAgentMessagesFromSession(session, sessionManager);
|
|
1137
|
+
await session.prompt(composeFeedbackResumePrompt(input));
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
990
1140
|
const userPrompt = composeStageUserPrompt(input, wiring.emitDefName, wiring.providerEmitHint, wiring.artifactDefName);
|
|
991
1141
|
await session.prompt(userPrompt);
|
|
992
1142
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type EventBus, type InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type ResolvedMcpServers } from "../config/resolveStageMcpServers.js";
|
|
3
|
+
export declare const STAGEFLOW_PI_MCP_EXTENSION_NAME = "stageflow-mcp";
|
|
4
|
+
type IsolatedMcpServerEntry = Record<string, unknown> & {
|
|
5
|
+
lifecycle: "lazy" | "eager";
|
|
6
|
+
directTools: true;
|
|
7
|
+
};
|
|
8
|
+
export type IsolatedMcpAttachOptions = {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
lifecycle?: IsolatedMcpServerEntry["lifecycle"];
|
|
11
|
+
};
|
|
12
|
+
export type IsolatedMcpAttach = {
|
|
13
|
+
extensionFactories: InlineExtension[] | undefined;
|
|
14
|
+
eventBus: EventBus | undefined;
|
|
15
|
+
connecting: Promise<void> | undefined;
|
|
16
|
+
cancel?: () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function emitIsolatedMcpStatus(eventBus: EventBus, servers: ReadonlyArray<{
|
|
19
|
+
name: string;
|
|
20
|
+
status: string;
|
|
21
|
+
}>): void;
|
|
22
|
+
export declare function attachIsolatedMcp(snapshot?: ResolvedMcpServers, options?: IsolatedMcpAttachOptions): Promise<IsolatedMcpAttach>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createEventBus, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { StageMcpError, } from "../config/resolveStageMcpServers.js";
|
|
3
|
+
export const STAGEFLOW_PI_MCP_EXTENSION_NAME = "stageflow-mcp";
|
|
4
|
+
const MCP_STATUS_EVENT = "pi-mcp-adapter/status/v1";
|
|
5
|
+
const DEFAULT_ISOLATED_MCP_CONNECT_TIMEOUT_MS = 5_000;
|
|
6
|
+
const STATUS_POLL_MS = 50;
|
|
7
|
+
const PI_MCP_ADAPTER_SPEC = "pi-mcp-adapter";
|
|
8
|
+
const ISOLATED_MCP_SETTINGS = {
|
|
9
|
+
directTools: true,
|
|
10
|
+
elicitation: false,
|
|
11
|
+
hostConfigDiscovery: "off",
|
|
12
|
+
};
|
|
13
|
+
const SUCCESS_STATUSES = new Set([
|
|
14
|
+
"connected",
|
|
15
|
+
"cached",
|
|
16
|
+
]);
|
|
17
|
+
const FAILURE_STATUSES = new Set([
|
|
18
|
+
"failed",
|
|
19
|
+
"needs-auth",
|
|
20
|
+
"disabled",
|
|
21
|
+
]);
|
|
22
|
+
const EMPTY_ATTACH = {
|
|
23
|
+
extensionFactories: undefined,
|
|
24
|
+
eventBus: undefined,
|
|
25
|
+
connecting: undefined,
|
|
26
|
+
};
|
|
27
|
+
let cachedCreateMcpAdapter;
|
|
28
|
+
function toIsolatedMcpConfig(snapshot, lifecycle = "lazy") {
|
|
29
|
+
const mcpServers = {};
|
|
30
|
+
for (const [name, entry] of Object.entries(snapshot)) {
|
|
31
|
+
const isolated = {
|
|
32
|
+
...entry,
|
|
33
|
+
lifecycle,
|
|
34
|
+
directTools: true,
|
|
35
|
+
};
|
|
36
|
+
if (typeof entry.cwd === "string") {
|
|
37
|
+
isolated.cwd = entry.cwd;
|
|
38
|
+
}
|
|
39
|
+
mcpServers[name] = isolated;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
mcpServers,
|
|
43
|
+
settings: { ...ISOLATED_MCP_SETTINGS },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function loadCreateMcpAdapter() {
|
|
47
|
+
if (cachedCreateMcpAdapter !== undefined) {
|
|
48
|
+
return cachedCreateMcpAdapter;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const mod = (await import(PI_MCP_ADAPTER_SPEC));
|
|
52
|
+
if (typeof mod.createMcpAdapter === "function") {
|
|
53
|
+
cachedCreateMcpAdapter = mod.createMcpAdapter;
|
|
54
|
+
return cachedCreateMcpAdapter;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Node does not type-strip .ts under node_modules.
|
|
59
|
+
}
|
|
60
|
+
const { createJiti } = await import("jiti/static");
|
|
61
|
+
const jiti = createJiti(import.meta.url);
|
|
62
|
+
const mod = (await jiti.import(PI_MCP_ADAPTER_SPEC));
|
|
63
|
+
cachedCreateMcpAdapter = mod.createMcpAdapter;
|
|
64
|
+
return cachedCreateMcpAdapter;
|
|
65
|
+
}
|
|
66
|
+
function isRuntimeStatus(value) {
|
|
67
|
+
return (value === "connected" ||
|
|
68
|
+
value === "cached" ||
|
|
69
|
+
value === "failed" ||
|
|
70
|
+
value === "needs-auth" ||
|
|
71
|
+
value === "not-connected" ||
|
|
72
|
+
value === "disabled");
|
|
73
|
+
}
|
|
74
|
+
function asIsolatedMcpStatusSnapshot(data) {
|
|
75
|
+
if (data === null || typeof data !== "object")
|
|
76
|
+
return undefined;
|
|
77
|
+
const servers = data.servers;
|
|
78
|
+
if (!Array.isArray(servers))
|
|
79
|
+
return undefined;
|
|
80
|
+
const parsed = [];
|
|
81
|
+
for (const entry of servers) {
|
|
82
|
+
if (entry === null || typeof entry !== "object")
|
|
83
|
+
continue;
|
|
84
|
+
const name = entry.name;
|
|
85
|
+
const status = entry.status;
|
|
86
|
+
if (typeof name !== "string" || typeof status !== "string")
|
|
87
|
+
continue;
|
|
88
|
+
if (!isRuntimeStatus(status))
|
|
89
|
+
continue;
|
|
90
|
+
parsed.push({ name, status });
|
|
91
|
+
}
|
|
92
|
+
return { servers: parsed };
|
|
93
|
+
}
|
|
94
|
+
function mcpStatusSourceFromEvents(events) {
|
|
95
|
+
return {
|
|
96
|
+
subscribe(listener) {
|
|
97
|
+
const handler = (data) => {
|
|
98
|
+
const snapshot = asIsolatedMcpStatusSnapshot(data);
|
|
99
|
+
if (snapshot)
|
|
100
|
+
listener(snapshot);
|
|
101
|
+
};
|
|
102
|
+
const unsubscribe = events.on(MCP_STATUS_EVENT, handler);
|
|
103
|
+
if (typeof unsubscribe === "function")
|
|
104
|
+
return unsubscribe;
|
|
105
|
+
return () => {
|
|
106
|
+
events.off?.(MCP_STATUS_EVENT, handler);
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function connectFailed(name, status) {
|
|
112
|
+
return new StageMcpError(`MCP server "${name}" failed to connect (status: ${status})`, "connect_failed");
|
|
113
|
+
}
|
|
114
|
+
function timeoutError(pendingNames) {
|
|
115
|
+
if (pendingNames.length === 1) {
|
|
116
|
+
return connectFailed(pendingNames[0], "timed out");
|
|
117
|
+
}
|
|
118
|
+
const listed = pendingNames.map((name) => `"${name}"`).join(", ");
|
|
119
|
+
return new StageMcpError(`MCP servers ${listed} failed to connect (status: timed out)`, "connect_failed");
|
|
120
|
+
}
|
|
121
|
+
function evaluateConnectStatus(serverNames, snapshot) {
|
|
122
|
+
if (snapshot === undefined) {
|
|
123
|
+
return { kind: "wait", pending: [...serverNames] };
|
|
124
|
+
}
|
|
125
|
+
const byName = new Map(snapshot.servers.map((entry) => [entry.name, entry]));
|
|
126
|
+
const pending = [];
|
|
127
|
+
for (const name of serverNames) {
|
|
128
|
+
const entry = byName.get(name);
|
|
129
|
+
if (entry === undefined || entry.status === "not-connected") {
|
|
130
|
+
pending.push(name);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (FAILURE_STATUSES.has(entry.status)) {
|
|
134
|
+
return { kind: "fail", name, status: entry.status };
|
|
135
|
+
}
|
|
136
|
+
if (!SUCCESS_STATUSES.has(entry.status)) {
|
|
137
|
+
pending.push(name);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (pending.length > 0) {
|
|
141
|
+
return { kind: "wait", pending };
|
|
142
|
+
}
|
|
143
|
+
return { kind: "ok" };
|
|
144
|
+
}
|
|
145
|
+
function waitForIsolatedMcpConnect(snapshot, source, options) {
|
|
146
|
+
const serverNames = Object.keys(snapshot);
|
|
147
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_ISOLATED_MCP_CONNECT_TIMEOUT_MS;
|
|
148
|
+
let lastSnapshot = source.read?.();
|
|
149
|
+
let finish = () => { };
|
|
150
|
+
const connecting = new Promise((resolve, reject) => {
|
|
151
|
+
let settled = false;
|
|
152
|
+
let unsubscribe;
|
|
153
|
+
let poll;
|
|
154
|
+
let timer;
|
|
155
|
+
finish = (error) => {
|
|
156
|
+
if (settled)
|
|
157
|
+
return;
|
|
158
|
+
settled = true;
|
|
159
|
+
unsubscribe?.();
|
|
160
|
+
if (poll !== undefined)
|
|
161
|
+
clearInterval(poll);
|
|
162
|
+
if (timer !== undefined)
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
if (error)
|
|
165
|
+
reject(error);
|
|
166
|
+
else
|
|
167
|
+
resolve();
|
|
168
|
+
};
|
|
169
|
+
const consider = (next) => {
|
|
170
|
+
if (next !== undefined)
|
|
171
|
+
lastSnapshot = next;
|
|
172
|
+
const result = evaluateConnectStatus(serverNames, lastSnapshot);
|
|
173
|
+
if (result.kind === "ok") {
|
|
174
|
+
finish();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (result.kind === "fail") {
|
|
178
|
+
finish(connectFailed(result.name, result.status));
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
consider(lastSnapshot);
|
|
182
|
+
if (settled)
|
|
183
|
+
return;
|
|
184
|
+
unsubscribe = source.subscribe?.((next) => consider(next));
|
|
185
|
+
consider(source.read?.());
|
|
186
|
+
if (settled)
|
|
187
|
+
return;
|
|
188
|
+
if (source.read) {
|
|
189
|
+
poll = setInterval(() => consider(source.read?.()), STATUS_POLL_MS);
|
|
190
|
+
}
|
|
191
|
+
timer = setTimeout(() => {
|
|
192
|
+
const pending = evaluateConnectStatus(serverNames, lastSnapshot);
|
|
193
|
+
finish(timeoutError(pending.kind === "wait" ? pending.pending : serverNames));
|
|
194
|
+
}, timeoutMs);
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
connecting,
|
|
198
|
+
cancel() {
|
|
199
|
+
void connecting.then(() => undefined, () => undefined);
|
|
200
|
+
finish(new StageMcpError("MCP connect wait cancelled", "connect_failed"));
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function emitIsolatedMcpStatus(eventBus, servers) {
|
|
205
|
+
eventBus.emit(MCP_STATUS_EVENT, { servers });
|
|
206
|
+
}
|
|
207
|
+
export async function attachIsolatedMcp(snapshot, options) {
|
|
208
|
+
if (snapshot === undefined || Object.keys(snapshot).length === 0) {
|
|
209
|
+
return EMPTY_ATTACH;
|
|
210
|
+
}
|
|
211
|
+
const createMcpAdapter = await loadCreateMcpAdapter();
|
|
212
|
+
const extensionFactories = [
|
|
213
|
+
{
|
|
214
|
+
name: STAGEFLOW_PI_MCP_EXTENSION_NAME,
|
|
215
|
+
factory: createMcpAdapter({
|
|
216
|
+
config: toIsolatedMcpConfig(snapshot, options?.lifecycle ?? "lazy"),
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
const eventBus = createEventBus();
|
|
221
|
+
const { connecting, cancel } = waitForIsolatedMcpConnect(snapshot, mcpStatusSourceFromEvents(eventBus), options);
|
|
222
|
+
return { extensionFactories, eventBus, connecting, cancel };
|
|
223
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const PROJECT_MCP_PROBE_TIMEOUT_MS = 30000;
|
|
2
|
+
export type ProjectMcpProbeStatus = "connected" | "needs_auth" | "connect_failed" | "unresolved_var" | "invalid_config" | "missing_catalog" | "cancelled";
|
|
3
|
+
export type ProjectMcpProbeResult = {
|
|
4
|
+
name: string;
|
|
5
|
+
status: ProjectMcpProbeStatus;
|
|
6
|
+
error?: string;
|
|
7
|
+
};
|
|
8
|
+
export type ProbeProjectMcpServerOptions = {
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
name: string;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function probeProjectMcpServer(options: ProbeProjectMcpServerOptions): Promise<ProjectMcpProbeResult>;
|