stageflow 0.9.0 → 0.12.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 +1 -0
- 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 +301 -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 +6 -3
- package/dist/agent/piAdapter.js +129 -21
- package/dist/agent/port.d.ts +50 -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 +10 -1
- package/dist/config/loadStage.js +138 -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 +49 -6
- 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 +16 -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/validateCatalog.d.ts +1 -1
- 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/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 +79 -1
- 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 +29 -0
- package/dist/runtime/verifiedStageExecution.js +119 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +114 -0
- package/dist/server/operatorResults.js +2 -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 +39 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +13 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-CXGz4nL7.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 +2 -1
- package/skills/stageflow-author/references/catalog-mapping.md +26 -2
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Agent SDK tool wrappers for the stage tool set.
|
|
3
|
+
*
|
|
4
|
+
* The actual envelope/artifact business logic lives in
|
|
5
|
+
* `tools/emitStageEnvelope.ts` / `tools/writeStageArtifact.ts` and is fully
|
|
6
|
+
* SDK-agnostic (typed `params: unknown`, no Pi imports) — this file reuses
|
|
7
|
+
* those `execute` functions as-is and only translates the *declaration*
|
|
8
|
+
* layer (typebox → Zod) and the *result* layer (drops Pi-only `terminate`/
|
|
9
|
+
* `details` fields, keeping just the MCP `CallToolResult` shape).
|
|
10
|
+
*
|
|
11
|
+
* `ask_operator` is different: `tools/askOperator.ts`'s own `execute` blocks
|
|
12
|
+
* on an injected wait bridge (Pi's in-process mechanism), which is exactly
|
|
13
|
+
* what Phase 3's "never let it go dangling" design avoids. This file reuses
|
|
14
|
+
* only the pure parsing/validation helpers from that module
|
|
15
|
+
* (`parseAskOperatorParams`, `normalizePromptIds`) and writes its own
|
|
16
|
+
* non-blocking handler: record the prompt on `capture`, return an immediate
|
|
17
|
+
* placeholder result, and rely on the adapter's `interrupt()` backstop
|
|
18
|
+
* (called right after this result lands) to end the turn — see
|
|
19
|
+
* claudeAdapter.ts.
|
|
20
|
+
*
|
|
21
|
+
* Registered under one in-process MCP server named "stageflow", so the model
|
|
22
|
+
* sees these as `mcp__stageflow__emit_stage_envelope`,
|
|
23
|
+
* `mcp__stageflow__write_stage_artifact`, and `mcp__stageflow__ask_operator`
|
|
24
|
+
* — those exact strings are what `composeStageUserPrompt`'s hints and the
|
|
25
|
+
* adapter's resume prompts must reference.
|
|
26
|
+
*/
|
|
27
|
+
import { createSdkMcpServer, tool, } from "@anthropic-ai/claude-agent-sdk";
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { createEmitStageEnvelopeTool, } from "../tools/emitStageEnvelope.js";
|
|
30
|
+
import { createWriteStageArtifactTool, } from "../tools/writeStageArtifact.js";
|
|
31
|
+
import { ASK_OPERATOR_KINDS, AskOperatorError, normalizePromptIds, parseAskOperatorParams, } from "../tools/askOperator.js";
|
|
32
|
+
export const STAGEFLOW_MCP_SERVER_NAME = "stageflow";
|
|
33
|
+
export const EMIT_STAGE_ENVELOPE_TOOL_NAME = `mcp__${STAGEFLOW_MCP_SERVER_NAME}__emit_stage_envelope`;
|
|
34
|
+
export const WRITE_STAGE_ARTIFACT_TOOL_NAME = `mcp__${STAGEFLOW_MCP_SERVER_NAME}__write_stage_artifact`;
|
|
35
|
+
export const ASK_OPERATOR_TOOL_NAME = `mcp__${STAGEFLOW_MCP_SERVER_NAME}__ask_operator`;
|
|
36
|
+
/**
|
|
37
|
+
* Exported so a parity test can assert this stays field-for-field aligned
|
|
38
|
+
* with `tools/emitStageEnvelope.ts`'s typebox schema — the two are hand
|
|
39
|
+
* written against the same envelope shape with no shared source, so
|
|
40
|
+
* nothing else catches one drifting from the other.
|
|
41
|
+
*
|
|
42
|
+
* Field presence mirrors the typebox schema's own conditionality:
|
|
43
|
+
* `clone_forks` only exists when a stage can clone-fork at all, and
|
|
44
|
+
* `fork_choice` is required (not just present) when the stage has a fork
|
|
45
|
+
* to route. Building this statically would silently offer the model a
|
|
46
|
+
* `clone_forks` field on stages that have nowhere to route one.
|
|
47
|
+
*/
|
|
48
|
+
export function buildEmitStageEnvelopeShape(options) {
|
|
49
|
+
return {
|
|
50
|
+
status: z.enum(["success", "failure"]),
|
|
51
|
+
summary: z.string().min(1),
|
|
52
|
+
artifacts: z.array(z.string()),
|
|
53
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
54
|
+
fork_choice: options.forkEmitContext !== undefined
|
|
55
|
+
? z.array(z.string())
|
|
56
|
+
: z.array(z.string()).optional(),
|
|
57
|
+
...(options.cloneEmitContext !== undefined
|
|
58
|
+
? { clone_forks: z.array(z.record(z.string(), z.unknown())) }
|
|
59
|
+
: {}),
|
|
60
|
+
checklist_attestations: z
|
|
61
|
+
.array(z.object({
|
|
62
|
+
check_id: z.string().min(1),
|
|
63
|
+
items: z.array(z.string()),
|
|
64
|
+
}))
|
|
65
|
+
.optional(),
|
|
66
|
+
stage_id: z.string().optional(),
|
|
67
|
+
notes: z.string().optional(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Exported for the same parity-test reason as `emitStageEnvelopeShape` above. */
|
|
71
|
+
export const writeStageArtifactShape = {
|
|
72
|
+
path: z.string().min(1),
|
|
73
|
+
content: z.string(),
|
|
74
|
+
};
|
|
75
|
+
const askOperatorShape = {
|
|
76
|
+
kind: z.string(),
|
|
77
|
+
message: z.string().optional(),
|
|
78
|
+
id: z.string().optional(),
|
|
79
|
+
questions: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
80
|
+
artifacts: z.array(z.string()).optional(),
|
|
81
|
+
};
|
|
82
|
+
/** Adapts a `{content, details?, isError?, terminate?}` tool result to plain MCP CallToolResult. */
|
|
83
|
+
function toCallToolResult(result) {
|
|
84
|
+
return {
|
|
85
|
+
content: result.content,
|
|
86
|
+
...(result.isError ? { isError: true } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function buildAskOperatorTool(capture, allowedKinds) {
|
|
90
|
+
const allowed = allowedKinds && allowedKinds.length > 0
|
|
91
|
+
? new Set(allowedKinds)
|
|
92
|
+
: null;
|
|
93
|
+
const kindList = allowed ? [...allowed].join(", ") : ASK_OPERATOR_KINDS.join(", ");
|
|
94
|
+
return tool("ask_operator", `Ask the operator a question and stop. Supports ${kindList} prompts. Calling this tool immediately ends your turn — after it returns, do not call any more tools and do not keep talking; you will be resumed in a new turn once the operator has answered. Does not complete the stage — call emit_stage_envelope when finished.`, askOperatorShape, async (args) => {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = parseAskOperatorParams(args);
|
|
97
|
+
if (allowed && !allowed.has(parsed.kind)) {
|
|
98
|
+
throw new AskOperatorError(`unsupported prompt kind "${parsed.kind}" (allowed: ${kindList})`);
|
|
99
|
+
}
|
|
100
|
+
capture.prompt = normalizePromptIds(parsed);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: "text", text: `Invalid ask_operator params: ${message}` }],
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: "Question recorded for the operator. Stop here — do not call any more tools or continue this turn; you will be resumed with the operator's answer in a new turn.",
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export function buildStageflowMcpServer(options) {
|
|
120
|
+
const emitDef = createEmitStageEnvelopeTool(options.capture, options.payloadSchema, options.forkEmitContext, options.cloneEmitContext, options.preEmitCheckOptions);
|
|
121
|
+
const artifactDef = createWriteStageArtifactTool(options.writeStageArtifact);
|
|
122
|
+
const emitTool = tool(emitDef.name, emitDef.description, buildEmitStageEnvelopeShape({
|
|
123
|
+
forkEmitContext: options.forkEmitContext,
|
|
124
|
+
cloneEmitContext: options.cloneEmitContext,
|
|
125
|
+
}), async (args) => toCallToolResult(await emitDef.execute("claude-tool-call", args)));
|
|
126
|
+
const artifactTool = tool(artifactDef.name, artifactDef.description, writeStageArtifactShape, async (args) => toCallToolResult(await artifactDef.execute("claude-tool-call", args)));
|
|
127
|
+
const tools = [emitTool, artifactTool];
|
|
128
|
+
if (options.askOperator) {
|
|
129
|
+
tools.push(buildAskOperatorTool(options.askOperator.capture, options.askOperator.allowedKinds));
|
|
130
|
+
}
|
|
131
|
+
return createSdkMcpServer({
|
|
132
|
+
name: STAGEFLOW_MCP_SERVER_NAME,
|
|
133
|
+
tools,
|
|
134
|
+
});
|
|
135
|
+
}
|
package/dist/agent/fakeAgent.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertRequiredEnvelope, isAdvancingEnvelope, } from "../envelope/check.js";
|
|
4
|
+
import { assertFeedbackLoopAction } from "../envelope/feedbackLoop.js";
|
|
4
5
|
import { assertEnvelopePayload } from "../envelope/payloadSchema.js";
|
|
5
6
|
import { assertCloneForks } from "../envelope/cloneForks.js";
|
|
6
7
|
import { assertForkEnvelope } from "../envelope/forkChoice.js";
|
|
7
8
|
import { assertAnswerMatchesPrompt, parseAskOperatorAnswer, } from "../tools/askOperator.js";
|
|
8
9
|
import { tryParsePendingPrompt } from "../runtime/stageHitl.js";
|
|
9
|
-
import { runtimeStageId, runStageViaOpen } from "./port.js";
|
|
10
|
+
import { runtimeStageId, runStageViaOpen, createCompletedOnlyStageHandle, } from "./port.js";
|
|
10
11
|
function opaqueEqual(a, b) {
|
|
11
12
|
if (Object.is(a, b))
|
|
12
13
|
return true;
|
|
@@ -68,6 +69,17 @@ export class FakeAgent {
|
|
|
68
69
|
let answerPromise;
|
|
69
70
|
let started = false;
|
|
70
71
|
let resumeCorrupt = false;
|
|
72
|
+
const sessionMode = input.sessionMode ?? "fresh";
|
|
73
|
+
const skipHitlPark = sessionMode === "feedback_resume" || sessionMode === "new_session";
|
|
74
|
+
if (sessionMode === "feedback_resume" && input.feedbackLoopContext === undefined) {
|
|
75
|
+
return createCompletedOnlyStageHandle({
|
|
76
|
+
stageId: runtimeStageId(input),
|
|
77
|
+
run: async () => ({
|
|
78
|
+
ok: false,
|
|
79
|
+
reason: "feedback_resume requires feedbackLoopContext",
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
71
83
|
const armWait = () => {
|
|
72
84
|
waiting = true;
|
|
73
85
|
answerPromise = new Promise((resolve) => {
|
|
@@ -83,7 +95,7 @@ export class FakeAgent {
|
|
|
83
95
|
answerPromise = undefined;
|
|
84
96
|
return answer;
|
|
85
97
|
};
|
|
86
|
-
if (behavior.type === "wait_then_emit") {
|
|
98
|
+
if (behavior.type === "wait_then_emit" && !skipHitlPark) {
|
|
87
99
|
const loaded = loadFakeHitlResume(input.roots, runtimeStageId(input));
|
|
88
100
|
if (loaded === "corrupt") {
|
|
89
101
|
resumeCorrupt = true;
|
|
@@ -99,6 +111,12 @@ export class FakeAgent {
|
|
|
99
111
|
armWait();
|
|
100
112
|
}
|
|
101
113
|
}
|
|
114
|
+
if (sessionMode === "feedback_resume" && behavior.type === "wait_then_emit") {
|
|
115
|
+
behavior = {
|
|
116
|
+
type: "emit",
|
|
117
|
+
envelope: behavior.envelope,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
102
120
|
const emitStartActivity = () => {
|
|
103
121
|
if (started)
|
|
104
122
|
return;
|
|
@@ -137,10 +155,12 @@ export class FakeAgent {
|
|
|
137
155
|
: undefined;
|
|
138
156
|
try {
|
|
139
157
|
const envelope = assertRequiredEnvelope(envelopeValue);
|
|
140
|
-
|
|
158
|
+
assertFeedbackLoopAction(envelope, input.feedbackLoopEmitContext);
|
|
159
|
+
const isFeedbackSendBack = envelope.feedback_loop?.action === "send_back";
|
|
160
|
+
if (!isFeedbackSendBack && input.forkEmitContext !== undefined) {
|
|
141
161
|
assertForkEnvelope(envelope, input.forkEmitContext);
|
|
142
162
|
}
|
|
143
|
-
if (input.cloneEmitContext !== undefined) {
|
|
163
|
+
if (!isFeedbackSendBack && input.cloneEmitContext !== undefined) {
|
|
144
164
|
assertCloneForks(envelope, input.cloneEmitContext);
|
|
145
165
|
}
|
|
146
166
|
assertEnvelopePayload(envelope, input.stage.payload_schema);
|
|
@@ -4,12 +4,14 @@ import { type AskOperatorPrompt, type AskOperatorWaitBridge } from "../tools/ask
|
|
|
4
4
|
import "./cursorProvider.js";
|
|
5
5
|
import { type StageActivityObserver } from "./activityObserver.js";
|
|
6
6
|
import type { AgentPort, OpaqueAnswer, StageHandle, StageHandleCloseOptions, StageRunInput, StageRunResult } from "./port.js";
|
|
7
|
+
import type { StageGateKind } from "../types/stage.js";
|
|
7
8
|
/**
|
|
8
9
|
* Stage tool allowlist for sealed Pi sessions.
|
|
9
|
-
*
|
|
10
|
-
* (always, for bound and unbound —
|
|
10
|
+
* Includes `ask_operator` unless `gateKinds` is an empty list (R6).
|
|
11
|
+
* `write_stage_artifact` when registered (always, for bound and unbound —
|
|
12
|
+
* bound/unbound only gates cwd / env bind).
|
|
11
13
|
*/
|
|
12
|
-
export declare function resolveStageToolNames(emitToolName: string, artifactToolName?: string, askOperatorToolName?: string): string[];
|
|
14
|
+
export declare function resolveStageToolNames(emitToolName: string, artifactToolName?: string, askOperatorToolName?: string, gateKinds?: StageGateKind[]): string[];
|
|
13
15
|
/**
|
|
14
16
|
* In-process wait channel for `ask_operator` → StageHandle yield/deliver.
|
|
15
17
|
* HITL `openStage` connects this via `setWaitHandler` + handle `deliverAnswer`.
|
|
@@ -62,6 +64,7 @@ export type RouteSessionEventToProgressOptions = {
|
|
|
62
64
|
*/
|
|
63
65
|
export declare function routeSessionEventToProgress(event: Record<string, unknown>, options: RouteSessionEventToProgressOptions): void;
|
|
64
66
|
export declare function composeStageUserPrompt(input: StageRunInput, emitToolName: string, emitHintOverride?: string, artifactToolName?: string): string;
|
|
67
|
+
export declare function composeFeedbackResumePrompt(input: StageRunInput): string;
|
|
65
68
|
/**
|
|
66
69
|
* DefaultResourceLoader with host/global discovery turned off.
|
|
67
70
|
*
|
package/dist/agent/piAdapter.js
CHANGED
|
@@ -26,14 +26,15 @@
|
|
|
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
36
|
import { isAdvancingEnvelope } from "../envelope/check.js";
|
|
37
|
+
import { formatFeedbackLoopContext } from "../prompt/feedbackLoopContext.js";
|
|
37
38
|
import { formatPriorEnvelope } from "../prompt/priorEnvelope.js";
|
|
38
39
|
import { attemptContext, noAttemptContext, } from "../runtime/stageAttemptContext.js";
|
|
39
40
|
import { createEmitStageEnvelopeTool, } from "../tools/emitStageEnvelope.js";
|
|
@@ -46,18 +47,21 @@ import { createStageActivityObserver, } from "./activityObserver.js";
|
|
|
46
47
|
import { DEFAULT_STAGE_TIMEOUT_MS, runtimeStageId, runStageViaOpen } from "./port.js";
|
|
47
48
|
/**
|
|
48
49
|
* Stage tool allowlist for sealed Pi sessions.
|
|
49
|
-
*
|
|
50
|
-
* (always, for bound and unbound —
|
|
50
|
+
* Includes `ask_operator` unless `gateKinds` is an empty list (R6).
|
|
51
|
+
* `write_stage_artifact` when registered (always, for bound and unbound —
|
|
52
|
+
* bound/unbound only gates cwd / env bind).
|
|
51
53
|
*/
|
|
52
|
-
export function resolveStageToolNames(emitToolName, artifactToolName, askOperatorToolName = "ask_operator") {
|
|
54
|
+
export function resolveStageToolNames(emitToolName, artifactToolName, askOperatorToolName = "ask_operator", gateKinds) {
|
|
53
55
|
const tools = [
|
|
54
56
|
"read",
|
|
55
57
|
"bash",
|
|
56
58
|
"write",
|
|
57
59
|
"edit",
|
|
58
60
|
emitToolName,
|
|
59
|
-
askOperatorToolName,
|
|
60
61
|
];
|
|
62
|
+
if (gateKinds === undefined || gateKinds.length > 0) {
|
|
63
|
+
tools.push(askOperatorToolName);
|
|
64
|
+
}
|
|
61
65
|
if (artifactToolName) {
|
|
62
66
|
tools.push(artifactToolName);
|
|
63
67
|
}
|
|
@@ -364,6 +368,37 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
|
|
|
364
368
|
if (input.stage.payload_schema !== undefined) {
|
|
365
369
|
emitHint += `\nOn status=success, payload is required and must match this JSON Schema:\n${JSON.stringify(input.stage.payload_schema, null, 2)}`;
|
|
366
370
|
}
|
|
371
|
+
const checklistChecks = input.completionContract?.checks.filter((check) => check.type === "checklist") ?? [];
|
|
372
|
+
if (checklistChecks.length > 0) {
|
|
373
|
+
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)}`;
|
|
374
|
+
}
|
|
375
|
+
if (input.repairContext !== undefined) {
|
|
376
|
+
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.`;
|
|
377
|
+
if (input.repairContext.operator_guidance) {
|
|
378
|
+
emitHint += `\nThe operator approved this retry with these instructions:\n${input.repairContext.operator_guidance}`;
|
|
379
|
+
}
|
|
380
|
+
if (input.repairContext.failed_checks?.length) {
|
|
381
|
+
emitHint += `\nFailed-check evidence from attempt ${input.repairContext.prior_attempt}:\n${JSON.stringify(input.repairContext.failed_checks, null, 2)}`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (input.cloneEmitContext !== undefined) {
|
|
385
|
+
const allowedActions = input.cloneEmitContext.allowedActions ?? ["skip", "once", "fanout"];
|
|
386
|
+
const successorLines = input.cloneEmitContext.clonableSuccessors.map((successor) => {
|
|
387
|
+
const header = `- ${successor.successorId} (clone_cap: ${successor.cloneCap})`;
|
|
388
|
+
if (successor.cloneInputSchema === undefined) {
|
|
389
|
+
return header;
|
|
390
|
+
}
|
|
391
|
+
return `${header}\n Assignment schema (clone_input_schema):\n${JSON.stringify(successor.cloneInputSchema, null, 2)}`;
|
|
392
|
+
});
|
|
393
|
+
emitHint += `\nClonable successors — emit clone_forks covering each of these ids exactly once:\n${successorLines.join("\n")}`;
|
|
394
|
+
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.`;
|
|
395
|
+
emitHint += `\nAllowed clone actions: ${allowedActions.join(", ")}`;
|
|
396
|
+
}
|
|
397
|
+
if (input.feedbackLoopEmitContext !== undefined) {
|
|
398
|
+
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.`;
|
|
399
|
+
emitHint += `\nAllowed send_back target: ${input.feedbackLoopEmitContext.target}.`;
|
|
400
|
+
emitHint += `\nA send_back decision cannot be combined with fork_choice or clone_forks.`;
|
|
401
|
+
}
|
|
367
402
|
const attempt = input.roots.attempt ?? 1;
|
|
368
403
|
const attemptArtifactsPath = `stages/${runtimeStageId(input)}/attempts/${attempt}/artifacts/`;
|
|
369
404
|
const skillBaseDir = input.stage.skill !== undefined && input.skillFilePath !== undefined
|
|
@@ -394,7 +429,10 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
|
|
|
394
429
|
`Goal: ${input.task.goal}`,
|
|
395
430
|
input.task.context ? `Context: ${input.task.context}` : "",
|
|
396
431
|
input.task.constraints ? `Constraints: ${input.task.constraints}` : "",
|
|
397
|
-
formatPriorEnvelope(input.priorEnvelope, input.priorEnvelopes),
|
|
432
|
+
formatPriorEnvelope(input.priorEnvelope, input.priorEnvelopes, input.priorEnvelopesByStage),
|
|
433
|
+
input.feedbackLoopContext !== undefined
|
|
434
|
+
? formatFeedbackLoopContext(input.feedbackLoopContext, input.sessionMode)
|
|
435
|
+
: "",
|
|
398
436
|
"",
|
|
399
437
|
artifactGuidance,
|
|
400
438
|
emitHint,
|
|
@@ -409,6 +447,16 @@ export function composeStageUserPrompt(input, emitToolName, emitHintOverride, ar
|
|
|
409
447
|
}
|
|
410
448
|
return body;
|
|
411
449
|
}
|
|
450
|
+
export function composeFeedbackResumePrompt(input) {
|
|
451
|
+
if (input.feedbackLoopContext === undefined) {
|
|
452
|
+
throw new Error("feedback_resume requires feedbackLoopContext");
|
|
453
|
+
}
|
|
454
|
+
return [
|
|
455
|
+
"Continue this stage after feedback-loop send-back.",
|
|
456
|
+
"Incorporate the operator/source feedback below, then complete the stage (emit an envelope when done).",
|
|
457
|
+
formatFeedbackLoopContext(input.feedbackLoopContext, input.sessionMode ?? "feedback_resume"),
|
|
458
|
+
].join("\n\n");
|
|
459
|
+
}
|
|
412
460
|
/**
|
|
413
461
|
* DefaultResourceLoader with host/global discovery turned off.
|
|
414
462
|
*
|
|
@@ -686,12 +734,20 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
686
734
|
const provider = findProviderSupport(input.stage.model);
|
|
687
735
|
const capture = {};
|
|
688
736
|
const askWaitChannel = existingAskWaitChannel ?? new AskOperatorWaitChannel();
|
|
689
|
-
const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema, input.forkEmitContext, input.cloneEmitContext
|
|
737
|
+
const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema, input.forkEmitContext, input.cloneEmitContext, {
|
|
738
|
+
checks: input.stage.pre_emit_checks,
|
|
739
|
+
readQaTrail: input.readQaTrail,
|
|
740
|
+
}, input.feedbackLoopEmitContext);
|
|
690
741
|
const emitTool = defineTool(emitDef);
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
742
|
+
const gateKinds = input.stage.gate_kinds;
|
|
743
|
+
const includeAsk = gateKinds === undefined || gateKinds.length > 0;
|
|
744
|
+
const askDef = includeAsk
|
|
745
|
+
? createAskOperatorTool({
|
|
746
|
+
requestWait: (prompt) => askWaitChannel.requestWait(prompt),
|
|
747
|
+
...(gateKinds !== undefined ? { allowedKinds: gateKinds } : {}),
|
|
748
|
+
})
|
|
749
|
+
: undefined;
|
|
750
|
+
const askTool = askDef ? defineTool(askDef) : undefined;
|
|
695
751
|
const artifactDef = createWriteStageArtifactTool({
|
|
696
752
|
runWorkspaceDir: roots.runWorkspaceDir,
|
|
697
753
|
stageId: runtimeStageId(input),
|
|
@@ -715,7 +771,17 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
715
771
|
restoreProvider = prepared.restore;
|
|
716
772
|
}
|
|
717
773
|
try {
|
|
718
|
-
const modelRuntime = await ModelRuntime.create(roots.authPath
|
|
774
|
+
const modelRuntime = await ModelRuntime.create(roots.authPath
|
|
775
|
+
? {
|
|
776
|
+
authPath: roots.authPath,
|
|
777
|
+
// ModelRuntime otherwise resolves models.json from getAgentDir(),
|
|
778
|
+
// which bindPiAgentDirEnv seals to an empty per-attempt sandbox
|
|
779
|
+
// dir. Point it at the same durable agent dir as authPath so
|
|
780
|
+
// custom providers (e.g. a self-hosted OpenAI-compatible
|
|
781
|
+
// gateway) defined there stay usable in sealed stage sessions.
|
|
782
|
+
modelsPath: path.join(path.dirname(roots.authPath), "models.json"),
|
|
783
|
+
}
|
|
784
|
+
: undefined);
|
|
719
785
|
const settingsManager = SettingsManager.inMemory({
|
|
720
786
|
compaction: { enabled: false },
|
|
721
787
|
});
|
|
@@ -760,16 +826,18 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
|
|
|
760
826
|
reason: `Skill "${input.stage.skill}" is not installed`,
|
|
761
827
|
};
|
|
762
828
|
}
|
|
763
|
-
const customTools =
|
|
829
|
+
const customTools = askTool
|
|
830
|
+
? [emitTool, askTool, artifactTool]
|
|
831
|
+
: [emitTool, artifactTool];
|
|
764
832
|
return {
|
|
765
833
|
sessionManager,
|
|
766
834
|
modelRuntime,
|
|
767
835
|
settingsManager,
|
|
768
836
|
loader,
|
|
769
|
-
tools: resolveStageToolNames(emitDef.name, artifactDef.name, askDef
|
|
837
|
+
tools: resolveStageToolNames(emitDef.name, artifactDef.name, askDef?.name ?? "ask_operator", gateKinds),
|
|
770
838
|
customTools,
|
|
771
839
|
emitDefName: emitDef.name,
|
|
772
|
-
askOperatorDefName: askDef.name,
|
|
840
|
+
...(askDef ? { askOperatorDefName: askDef.name } : {}),
|
|
773
841
|
artifactDefName: artifactDef.name,
|
|
774
842
|
providerEmitHint: provider?.emitToolHint?.(emitDef.name),
|
|
775
843
|
restoreProvider,
|
|
@@ -901,30 +969,65 @@ export class PiAgentAdapter {
|
|
|
901
969
|
const timeoutMs = input.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
|
|
902
970
|
const askWaitChannel = new AskOperatorWaitChannel();
|
|
903
971
|
const sessionFile = resolveStageSessionFile(input);
|
|
972
|
+
const sessionMode = input.sessionMode ?? "fresh";
|
|
904
973
|
let sessionManager;
|
|
905
974
|
let resumeWaiting = false;
|
|
906
|
-
|
|
975
|
+
let feedbackResume = false;
|
|
976
|
+
let forceNewSession = sessionMode === "new_session";
|
|
977
|
+
if (sessionMode === "feedback_resume") {
|
|
978
|
+
if (input.feedbackLoopContext === undefined) {
|
|
979
|
+
throw new StageSessionReconstructError(`feedback_resume requires feedbackLoopContext: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
980
|
+
}
|
|
981
|
+
if (!existsSync(sessionFile)) {
|
|
982
|
+
throw new StageSessionReconstructError(`stage session file missing for feedback_resume: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
983
|
+
}
|
|
907
984
|
try {
|
|
908
985
|
sessionManager = SessionManager.open(sessionFile, path.dirname(sessionFile), input.roots.cwd);
|
|
909
986
|
}
|
|
910
987
|
catch (err) {
|
|
911
|
-
throw new StageSessionReconstructError(`stage session file corrupt or unreadable for
|
|
988
|
+
throw new StageSessionReconstructError(`stage session file corrupt or unreadable for feedback_resume: ${sessionFile}`, {
|
|
912
989
|
stageId: runtimeStageId(input),
|
|
913
990
|
sessionFile,
|
|
914
991
|
cause: err,
|
|
915
992
|
});
|
|
916
993
|
}
|
|
917
994
|
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
918
|
-
|
|
919
|
-
|
|
995
|
+
feedbackResume = true;
|
|
996
|
+
}
|
|
997
|
+
else if (sessionMode !== "new_session") {
|
|
998
|
+
if (sessionMode === "waiting_resume" || existsSync(sessionFile)) {
|
|
999
|
+
if (existsSync(sessionFile)) {
|
|
1000
|
+
try {
|
|
1001
|
+
sessionManager = SessionManager.open(sessionFile, path.dirname(sessionFile), input.roots.cwd);
|
|
1002
|
+
}
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
throw new StageSessionReconstructError(`stage session file corrupt or unreadable for resume: ${sessionFile}`, {
|
|
1005
|
+
stageId: runtimeStageId(input),
|
|
1006
|
+
sessionFile,
|
|
1007
|
+
cause: err,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
ensureStageSessionFlushed(sessionManager, runtimeStageId(input));
|
|
1011
|
+
if (!findOpenToolCall(sessionManager)) {
|
|
1012
|
+
throw new StageSessionReconstructError(`stage session has no open tool call for resume: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
1013
|
+
}
|
|
1014
|
+
resumeWaiting = true;
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
throw new StageSessionReconstructError(`stage session file missing for resume: ${sessionFile}`, { stageId: runtimeStageId(input), sessionFile });
|
|
1018
|
+
}
|
|
920
1019
|
}
|
|
921
|
-
resumeWaiting = true;
|
|
922
1020
|
}
|
|
923
1021
|
let session;
|
|
924
1022
|
let wiring;
|
|
925
1023
|
let unsubscribeProgress;
|
|
926
1024
|
let prepareError;
|
|
927
1025
|
const preparePromise = (async () => {
|
|
1026
|
+
if (forceNewSession) {
|
|
1027
|
+
await mkdir(path.dirname(sessionFile), { recursive: true });
|
|
1028
|
+
await writeFile(sessionFile, "");
|
|
1029
|
+
sessionManager = undefined;
|
|
1030
|
+
}
|
|
928
1031
|
if (!sessionManager) {
|
|
929
1032
|
sessionManager = await createStageSessionManager(input.roots, runtimeStageId(input), sessionFile);
|
|
930
1033
|
}
|
|
@@ -987,6 +1090,11 @@ export class PiAgentAdapter {
|
|
|
987
1090
|
},
|
|
988
1091
|
run: async () => {
|
|
989
1092
|
return runWithTimeout(async () => {
|
|
1093
|
+
if (feedbackResume) {
|
|
1094
|
+
syncAgentMessagesFromSession(session, sessionManager);
|
|
1095
|
+
await session.prompt(composeFeedbackResumePrompt(input));
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
990
1098
|
const userPrompt = composeStageUserPrompt(input, wiring.emitDefName, wiring.providerEmitHint, wiring.artifactDefName);
|
|
991
1099
|
await session.prompt(userPrompt);
|
|
992
1100
|
});
|
package/dist/agent/port.d.ts
CHANGED
|
@@ -1,11 +1,47 @@
|
|
|
1
|
-
import type { StageEnvelope } from "../types/envelope.js";
|
|
1
|
+
import type { StageEnvelope, TerminalEnvelope } from "../types/envelope.js";
|
|
2
|
+
import type { CompletionContract } from "../types/completion.js";
|
|
2
3
|
import type { CloneEmitContext, ForkEmitContext } from "../types/forkChoice.js";
|
|
3
4
|
import type { StageConfig } from "../types/stage.js";
|
|
5
|
+
import type { FeedbackLoopConfig } from "../types/pipeline.js";
|
|
4
6
|
import type { TaskFile } from "../types/task.js";
|
|
5
7
|
import type { StageActivityEvent } from "./activity.js";
|
|
6
8
|
import type { StageRoots } from "../runtime/stageRoots.js";
|
|
9
|
+
import type { QaExchange } from "../hitl/qaTrail.js";
|
|
7
10
|
/** Opaque to runtime; adapters interpret. */
|
|
8
11
|
export type StageResumeToken = string;
|
|
12
|
+
export type StageSessionMode = "fresh" | "waiting_resume" | "feedback_resume" | "new_session";
|
|
13
|
+
export type StageRepairContext = {
|
|
14
|
+
prior_attempt: number;
|
|
15
|
+
/** Operator-authored instructions for an explicitly approved manual retry. */
|
|
16
|
+
operator_guidance?: string;
|
|
17
|
+
failed_checks?: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
type: string;
|
|
20
|
+
evidence_preview?: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
export type FeedbackLoopContext = {
|
|
24
|
+
loop_id: string;
|
|
25
|
+
replay_id: string;
|
|
26
|
+
source_stage_id: string;
|
|
27
|
+
target_stage_id: string;
|
|
28
|
+
/** Original send-back envelope from the source (feedback + artifacts). */
|
|
29
|
+
feedback_envelope: StageEnvelope;
|
|
30
|
+
/** One-based replay number for this send-back. */
|
|
31
|
+
replay_number: number;
|
|
32
|
+
max_replays: number;
|
|
33
|
+
remaining_replays: number;
|
|
34
|
+
is_final_replay: boolean;
|
|
35
|
+
replay_session: "resume" | "new_session";
|
|
36
|
+
/** Persistent stages on the active target→source route, forward order. */
|
|
37
|
+
route_stage_ids: string[];
|
|
38
|
+
/** Prior pass/session identity for THIS stage when resuming, if any. */
|
|
39
|
+
prior_stage_attempt?: number;
|
|
40
|
+
prior_pass_status?: string;
|
|
41
|
+
/** Active fork generation for this stage's parent fan-out, if any. */
|
|
42
|
+
active_fork_generation_id?: string;
|
|
43
|
+
active_fork_clone_stage_ids?: string[];
|
|
44
|
+
};
|
|
9
45
|
export type StageRunInput = {
|
|
10
46
|
roots: StageRoots;
|
|
11
47
|
stage: StageConfig;
|
|
@@ -14,13 +50,26 @@ export type StageRunInput = {
|
|
|
14
50
|
task: TaskFile;
|
|
15
51
|
priorEnvelope: StageEnvelope | null;
|
|
16
52
|
priorEnvelopes?: StageEnvelope[];
|
|
53
|
+
priorEnvelopesByStage?: Record<string, TerminalEnvelope | TerminalEnvelope[]>;
|
|
17
54
|
timeoutMs?: number;
|
|
18
55
|
resumeToken?: StageResumeToken;
|
|
56
|
+
/** Launch/session mode; omitted is treated as `"fresh"`. */
|
|
57
|
+
sessionMode?: StageSessionMode;
|
|
19
58
|
/** Optional observe hook; HITL wait/answer uses openStage beside this. */
|
|
20
59
|
onActivity?: (event: StageActivityEvent) => void;
|
|
21
60
|
skillFilePath?: string;
|
|
22
61
|
forkEmitContext?: ForkEmitContext;
|
|
23
62
|
cloneEmitContext?: CloneEmitContext;
|
|
63
|
+
/** Declared source policy; successful emits must continue or send work back. */
|
|
64
|
+
feedbackLoopEmitContext?: FeedbackLoopConfig;
|
|
65
|
+
/** Replay context for stages being replayed (and typically the source on send-back). */
|
|
66
|
+
feedbackLoopContext?: FeedbackLoopContext;
|
|
67
|
+
/** Frozen pipeline-owned checks that must pass before Stageflow advances. */
|
|
68
|
+
completionContract?: CompletionContract;
|
|
69
|
+
/** Compact evidence from the failed verification that triggered this repair. */
|
|
70
|
+
repairContext?: StageRepairContext;
|
|
71
|
+
/** Live attempt-scoped QA trail; read at emit execute time, not bootstrap time. */
|
|
72
|
+
readQaTrail?: () => QaExchange[] | Promise<QaExchange[]>;
|
|
24
73
|
};
|
|
25
74
|
export declare function runtimeStageId(input: Pick<StageRunInput, "stage" | "stageId">): string;
|
|
26
75
|
export type StageRunResult = {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type AgentBackendId } from "./agentBackend.js";
|
|
2
|
+
import type { AgentPort } from "./port.js";
|
|
3
|
+
import type { LoadedManifest } from "../types/stageflowManifest.js";
|
|
4
|
+
export type AgentSelection = {
|
|
5
|
+
/** stageflow.yaml top-level `agent`. */
|
|
6
|
+
global?: AgentBackendId;
|
|
7
|
+
/** Pipeline yaml top-level `agent`; overrides global. */
|
|
8
|
+
pipeline?: AgentBackendId;
|
|
9
|
+
/** Stage yaml `agent`; would override pipeline/global once enabled. */
|
|
10
|
+
stage?: AgentBackendId;
|
|
11
|
+
};
|
|
12
|
+
/** stage > pipeline > global > "pi", with stage gated off for now. */
|
|
13
|
+
export declare function resolveAgentBackend(selection?: AgentSelection): AgentBackendId;
|
|
14
|
+
export declare function resolveAgentPort(selection?: AgentSelection): AgentPort;
|
|
15
|
+
/** The global tier's backend id from an already-loaded manifest, or undefined for missing/invalid/unset. */
|
|
16
|
+
export declare function globalAgentBackendFromManifest(manifest: LoadedManifest | null): AgentBackendId | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Resolves an AgentPort using only the global (stageflow.yaml) tier — for
|
|
19
|
+
* the three call sites that construct an agent before any pipeline is
|
|
20
|
+
* known (`cli.ts`'s ui/mcp servers, `runCommand.ts`, `runsCommand.ts`).
|
|
21
|
+
* Named explicitly so it's clear these sites are scoped to the global tier
|
|
22
|
+
* on purpose, not by omission: production stage execution always goes
|
|
23
|
+
* through `stageWorker.ts`, which resolves the full stage > pipeline >
|
|
24
|
+
* global chain once it has loaded the specific pipeline and stage.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveGlobalOnlyAgentPort(projectRoot: string): Promise<AgentPort>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { asAgentBackendId, STAGE_LEVEL_AGENT_OVERRIDE_ENABLED, } from "./agentBackend.js";
|
|
2
|
+
import { PiAgentAdapter } from "./piAdapter.js";
|
|
3
|
+
import { ClaudeAgentAdapter } from "./claudeAdapter.js";
|
|
4
|
+
import { loadStageflowManifestOutcome } from "../config/loadStageflowManifest.js";
|
|
5
|
+
/** stage > pipeline > global > "pi", with stage gated off for now. */
|
|
6
|
+
export function resolveAgentBackend(selection = {}) {
|
|
7
|
+
if (STAGE_LEVEL_AGENT_OVERRIDE_ENABLED && selection.stage !== undefined) {
|
|
8
|
+
return selection.stage;
|
|
9
|
+
}
|
|
10
|
+
return selection.pipeline ?? selection.global ?? "pi";
|
|
11
|
+
}
|
|
12
|
+
export function resolveAgentPort(selection = {}) {
|
|
13
|
+
const backend = resolveAgentBackend(selection);
|
|
14
|
+
switch (backend) {
|
|
15
|
+
case "pi":
|
|
16
|
+
return new PiAgentAdapter();
|
|
17
|
+
case "claude":
|
|
18
|
+
return new ClaudeAgentAdapter();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** The global tier's backend id from an already-loaded manifest, or undefined for missing/invalid/unset. */
|
|
22
|
+
export function globalAgentBackendFromManifest(manifest) {
|
|
23
|
+
return manifest ? asAgentBackendId(manifest.manifest.agent) : undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolves an AgentPort using only the global (stageflow.yaml) tier — for
|
|
27
|
+
* the three call sites that construct an agent before any pipeline is
|
|
28
|
+
* known (`cli.ts`'s ui/mcp servers, `runCommand.ts`, `runsCommand.ts`).
|
|
29
|
+
* Named explicitly so it's clear these sites are scoped to the global tier
|
|
30
|
+
* on purpose, not by omission: production stage execution always goes
|
|
31
|
+
* through `stageWorker.ts`, which resolves the full stage > pipeline >
|
|
32
|
+
* global chain once it has loaded the specific pipeline and stage.
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveGlobalOnlyAgentPort(projectRoot) {
|
|
35
|
+
const outcome = await loadStageflowManifestOutcome(projectRoot);
|
|
36
|
+
return resolveAgentPort({
|
|
37
|
+
global: outcome.ok ? globalAgentBackendFromManifest(outcome.value) : undefined,
|
|
38
|
+
});
|
|
39
|
+
}
|
package/dist/cli/runCommand.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveGlobalOnlyAgentPort } from "../agent/resolveAgentPort.js";
|
|
2
2
|
import { createRunStore } from "../runstore/createStore.js";
|
|
3
3
|
import { PipelineValidationError } from "../runtime/pipelineRunner.js";
|
|
4
4
|
import { RunManager } from "../runtime/runManager.js";
|
|
@@ -137,7 +137,7 @@ function defaultStartRun(cwd, projectRoot, isGitProject, operatorCatalog) {
|
|
|
137
137
|
return async (input) => {
|
|
138
138
|
const store = createRunStore({ rootDir: projectRoot });
|
|
139
139
|
const manager = new RunManager({
|
|
140
|
-
agent:
|
|
140
|
+
agent: await resolveGlobalOnlyAgentPort(projectRoot),
|
|
141
141
|
store,
|
|
142
142
|
cwd,
|
|
143
143
|
projectRoot,
|