stageflow 0.1.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/agent/activity.d.ts +76 -0
- package/dist/agent/activity.js +89 -0
- package/dist/agent/activityObserver.d.ts +17 -0
- package/dist/agent/activityObserver.js +66 -0
- package/dist/agent/cursorExtension.d.ts +12 -0
- package/dist/agent/cursorExtension.js +88 -0
- package/dist/agent/cursorProvider.d.ts +14 -0
- package/dist/agent/cursorProvider.js +139 -0
- package/dist/agent/fakeAgent.d.ts +29 -0
- package/dist/agent/fakeAgent.js +295 -0
- package/dist/agent/oauthSessionManager.d.ts +33 -0
- package/dist/agent/oauthSessionManager.js +214 -0
- package/dist/agent/piAdapter.d.ts +128 -0
- package/dist/agent/piAdapter.js +967 -0
- package/dist/agent/port.d.ts +63 -0
- package/dist/agent/port.js +46 -0
- package/dist/agent/providerAuth.d.ts +101 -0
- package/dist/agent/providerAuth.js +282 -0
- package/dist/agent/providerSupport.d.ts +28 -0
- package/dist/agent/providerSupport.js +19 -0
- package/dist/cli/providersCommand.d.ts +11 -0
- package/dist/cli/providersCommand.js +275 -0
- package/dist/cli/terminalAuthInteraction.d.ts +8 -0
- package/dist/cli/terminalAuthInteraction.js +65 -0
- package/dist/cli/terminalSecret.d.ts +2 -0
- package/dist/cli/terminalSecret.js +67 -0
- package/dist/cli/validateCommand.d.ts +11 -0
- package/dist/cli/validateCommand.js +81 -0
- package/dist/cli/validateOutput.d.ts +8 -0
- package/dist/cli/validateOutput.js +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +273 -0
- package/dist/config/createPipeline.d.ts +31 -0
- package/dist/config/createPipeline.js +255 -0
- package/dist/config/createStage.d.ts +25 -0
- package/dist/config/createStage.js +181 -0
- package/dist/config/listConfig.d.ts +33 -0
- package/dist/config/listConfig.js +195 -0
- package/dist/config/listExtensions.d.ts +26 -0
- package/dist/config/listExtensions.js +50 -0
- package/dist/config/listSkills.d.ts +29 -0
- package/dist/config/listSkills.js +55 -0
- package/dist/config/loadOutcome.d.ts +27 -0
- package/dist/config/loadOutcome.js +6 -0
- package/dist/config/loadPipeline.d.ts +25 -0
- package/dist/config/loadPipeline.js +196 -0
- package/dist/config/loadStage.d.ts +4 -0
- package/dist/config/loadStage.js +127 -0
- package/dist/config/loadTask.d.ts +7 -0
- package/dist/config/loadTask.js +69 -0
- package/dist/config/readYamlObject.d.ts +1 -0
- package/dist/config/readYamlObject.js +5 -0
- package/dist/config/resolvePipelineDag.d.ts +12 -0
- package/dist/config/resolvePipelineDag.js +282 -0
- package/dist/config/validateCatalog.d.ts +36 -0
- package/dist/config/validateCatalog.js +322 -0
- package/dist/envelope/check.d.ts +3 -0
- package/dist/envelope/check.js +52 -0
- package/dist/envelope/payloadSchema.d.ts +10 -0
- package/dist/envelope/payloadSchema.js +91 -0
- package/dist/hitl/qaTrail.d.ts +31 -0
- package/dist/hitl/qaTrail.js +65 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7 -0
- package/dist/mcp/projectRun.d.ts +37 -0
- package/dist/mcp/projectRun.js +57 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +46 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +26 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +96 -0
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -0
- package/dist/prompt/priorEnvelope.js +5 -0
- package/dist/runstore/catalog.d.ts +8 -0
- package/dist/runstore/catalog.js +16 -0
- package/dist/runstore/createStore.d.ts +15 -0
- package/dist/runstore/createStore.js +23 -0
- package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
- package/dist/runstore/disk/DiskRunStore.js +238 -0
- package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
- package/dist/runstore/disk/catalogHelpers.js +102 -0
- package/dist/runstore/layout.d.ts +22 -0
- package/dist/runstore/layout.js +58 -0
- package/dist/runstore/paths.d.ts +7 -0
- package/dist/runstore/paths.js +60 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
- package/dist/runstore/pipelineDagSnapshot.js +43 -0
- package/dist/runstore/port.d.ts +146 -0
- package/dist/runstore/port.js +28 -0
- package/dist/runstore/runProjection.d.ts +6 -0
- package/dist/runstore/runProjection.js +102 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
- package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
- package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
- package/dist/runstore/sqlite/schema.d.ts +1 -0
- package/dist/runstore/sqlite/schema.js +59 -0
- package/dist/runstore/stageExecution.d.ts +13 -0
- package/dist/runstore/stageExecution.js +22 -0
- package/dist/runstore/stageSnapshot.d.ts +2 -0
- package/dist/runstore/stageSnapshot.js +46 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
- package/dist/runstore/syntheticStageSnapshot.js +10 -0
- package/dist/runstore/trackProjection.d.ts +7 -0
- package/dist/runstore/trackProjection.js +94 -0
- package/dist/runstore/workspaceLayout.d.ts +31 -0
- package/dist/runstore/workspaceLayout.js +172 -0
- package/dist/runtime/answerResume.d.ts +16 -0
- package/dist/runtime/answerResume.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +16 -0
- package/dist/runtime/credentialBinding.js +80 -0
- package/dist/runtime/dagTraversal.d.ts +3 -0
- package/dist/runtime/dagTraversal.js +26 -0
- package/dist/runtime/envelopeRouting.d.ts +20 -0
- package/dist/runtime/envelopeRouting.js +59 -0
- package/dist/runtime/hitlSeams.d.ts +38 -0
- package/dist/runtime/hitlSeams.js +2 -0
- package/dist/runtime/pipelineRunner.d.ts +77 -0
- package/dist/runtime/pipelineRunner.js +141 -0
- package/dist/runtime/pipelineScheduler.d.ts +90 -0
- package/dist/runtime/pipelineScheduler.js +494 -0
- package/dist/runtime/pipelineValidationError.d.ts +6 -0
- package/dist/runtime/pipelineValidationError.js +8 -0
- package/dist/runtime/resumeReconstruct.d.ts +33 -0
- package/dist/runtime/resumeReconstruct.js +144 -0
- package/dist/runtime/runManager.d.ts +129 -0
- package/dist/runtime/runManager.js +818 -0
- package/dist/runtime/runRetryCoordinator.d.ts +85 -0
- package/dist/runtime/runRetryCoordinator.js +372 -0
- package/dist/runtime/settingsFile.d.ts +17 -0
- package/dist/runtime/settingsFile.js +88 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
- package/dist/runtime/stageAttemptBootstrap.js +82 -0
- package/dist/runtime/stageAttemptContext.d.ts +11 -0
- package/dist/runtime/stageAttemptContext.js +26 -0
- package/dist/runtime/stageConcurrency.d.ts +10 -0
- package/dist/runtime/stageConcurrency.js +51 -0
- package/dist/runtime/stageHitl.d.ts +95 -0
- package/dist/runtime/stageHitl.js +198 -0
- package/dist/runtime/stageProcessLauncher.d.ts +45 -0
- package/dist/runtime/stageProcessLauncher.js +199 -0
- package/dist/runtime/stageRecovery.d.ts +10 -0
- package/dist/runtime/stageRecovery.js +43 -0
- package/dist/runtime/stageRoots.d.ts +22 -0
- package/dist/runtime/stageRoots.js +93 -0
- package/dist/runtime/stageRunner.d.ts +59 -0
- package/dist/runtime/stageRunner.js +221 -0
- package/dist/runtime/stageWorker.d.ts +6 -0
- package/dist/runtime/stageWorker.js +109 -0
- package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
- package/dist/runtime/stageWorkerProtocol.js +16 -0
- package/dist/runtime/taskInput.d.ts +15 -0
- package/dist/runtime/taskInput.js +38 -0
- package/dist/server/http.d.ts +31 -0
- package/dist/server/http.js +561 -0
- package/dist/server/providerRoutes.d.ts +15 -0
- package/dist/server/providerRoutes.js +186 -0
- package/dist/tools/askOperator.d.ts +185 -0
- package/dist/tools/askOperator.js +409 -0
- package/dist/tools/emitStageEnvelope.d.ts +42 -0
- package/dist/tools/emitStageEnvelope.js +61 -0
- package/dist/tools/writeStageArtifact.d.ts +26 -0
- package/dist/tools/writeStageArtifact.js +103 -0
- package/dist/types/envelope.d.ts +12 -0
- package/dist/types/envelope.js +6 -0
- package/dist/types/pipeline.d.ts +25 -0
- package/dist/types/pipeline.js +1 -0
- package/dist/types/stage.d.ts +12 -0
- package/dist/types/stage.js +6 -0
- package/dist/types/task.d.ts +7 -0
- package/dist/types/task.js +1 -0
- package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
- package/dist/ui/assets/index-DCsDopak.css +1 -0
- package/dist/ui/index.html +21 -0
- package/package.json +72 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertRequiredEnvelope, isAdvancingEnvelope, } from "../envelope/check.js";
|
|
4
|
+
import { assertEnvelopePayload } from "../envelope/payloadSchema.js";
|
|
5
|
+
import { assertAnswerMatchesPrompt, parseAskOperatorAnswer, } from "../tools/askOperator.js";
|
|
6
|
+
import { tryParsePendingPrompt } from "../runtime/stageHitl.js";
|
|
7
|
+
import { runStageViaOpen } from "./port.js";
|
|
8
|
+
function opaqueEqual(a, b) {
|
|
9
|
+
if (Object.is(a, b))
|
|
10
|
+
return true;
|
|
11
|
+
try {
|
|
12
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function fakeHitlResumePath(roots, stageId) {
|
|
19
|
+
return path.join(roots.runWorkspaceDir, "stages", stageId, "fake-hitl-resume.json");
|
|
20
|
+
}
|
|
21
|
+
function writeFakeHitlResume(roots, stageId, state) {
|
|
22
|
+
const filePath = fakeHitlResumePath(roots, stageId);
|
|
23
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
24
|
+
writeFileSync(filePath, `${JSON.stringify(state)}\n`, "utf8");
|
|
25
|
+
}
|
|
26
|
+
function clearFakeHitlResume(roots, stageId) {
|
|
27
|
+
const filePath = fakeHitlResumePath(roots, stageId);
|
|
28
|
+
if (existsSync(filePath)) {
|
|
29
|
+
unlinkSync(filePath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function loadFakeHitlResume(roots, stageId) {
|
|
33
|
+
const filePath = fakeHitlResumePath(roots, stageId);
|
|
34
|
+
if (!existsSync(filePath))
|
|
35
|
+
return undefined;
|
|
36
|
+
try {
|
|
37
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
38
|
+
if (!raw ||
|
|
39
|
+
!Array.isArray(raw.waitRequests) ||
|
|
40
|
+
typeof raw.waitIndex !== "number" ||
|
|
41
|
+
raw.envelope === undefined) {
|
|
42
|
+
return "corrupt";
|
|
43
|
+
}
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return "corrupt";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class FakeAgent {
|
|
51
|
+
behavior;
|
|
52
|
+
lastRoots;
|
|
53
|
+
receivedAnswers = [];
|
|
54
|
+
constructor(behavior) {
|
|
55
|
+
this.behavior = behavior;
|
|
56
|
+
}
|
|
57
|
+
openStage(input) {
|
|
58
|
+
this.lastRoots = input.roots;
|
|
59
|
+
this.receivedAnswers = [];
|
|
60
|
+
let behavior = this.behavior;
|
|
61
|
+
const receivedAnswers = this.receivedAnswers;
|
|
62
|
+
let waitIndex = 0;
|
|
63
|
+
let waiting = false;
|
|
64
|
+
let closed = false;
|
|
65
|
+
let answerResolver;
|
|
66
|
+
let answerPromise;
|
|
67
|
+
let started = false;
|
|
68
|
+
let resumeCorrupt = false;
|
|
69
|
+
const armWait = () => {
|
|
70
|
+
waiting = true;
|
|
71
|
+
answerPromise = new Promise((resolve) => {
|
|
72
|
+
answerResolver = resolve;
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
const awaitAnswer = async () => {
|
|
76
|
+
if (!answerPromise)
|
|
77
|
+
return undefined;
|
|
78
|
+
const answer = await answerPromise;
|
|
79
|
+
waiting = false;
|
|
80
|
+
answerResolver = undefined;
|
|
81
|
+
answerPromise = undefined;
|
|
82
|
+
return answer;
|
|
83
|
+
};
|
|
84
|
+
if (behavior.type === "wait_then_emit") {
|
|
85
|
+
const loaded = loadFakeHitlResume(input.roots, input.stage.id);
|
|
86
|
+
if (loaded === "corrupt") {
|
|
87
|
+
resumeCorrupt = true;
|
|
88
|
+
}
|
|
89
|
+
else if (loaded) {
|
|
90
|
+
behavior = {
|
|
91
|
+
type: "wait_then_emit",
|
|
92
|
+
waitRequests: loaded.waitRequests,
|
|
93
|
+
expectedAnswers: loaded.expectedAnswers,
|
|
94
|
+
envelope: loaded.envelope,
|
|
95
|
+
};
|
|
96
|
+
waitIndex = loaded.waitIndex;
|
|
97
|
+
armWait();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const emitStartActivity = () => {
|
|
101
|
+
if (started)
|
|
102
|
+
return;
|
|
103
|
+
started = true;
|
|
104
|
+
input.onActivity?.({ event: "agent_start" });
|
|
105
|
+
input.onActivity?.({
|
|
106
|
+
event: "tool_start",
|
|
107
|
+
toolName: "fake_tool",
|
|
108
|
+
toolCallId: "fake-1",
|
|
109
|
+
});
|
|
110
|
+
input.onActivity?.({
|
|
111
|
+
event: "tool_end",
|
|
112
|
+
toolName: "fake_tool",
|
|
113
|
+
toolCallId: "fake-1",
|
|
114
|
+
isError: false,
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
const finishEmit = () => {
|
|
118
|
+
clearFakeHitlResume(input.roots, input.stage.id);
|
|
119
|
+
if (behavior.type === "throw") {
|
|
120
|
+
input.onActivity?.({ event: "agent_end" });
|
|
121
|
+
return {
|
|
122
|
+
status: "completed",
|
|
123
|
+
result: { ok: false, reason: behavior.message },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (behavior.type === "never_emit") {
|
|
127
|
+
input.onActivity?.({ event: "agent_end" });
|
|
128
|
+
return {
|
|
129
|
+
status: "completed",
|
|
130
|
+
result: { ok: false, reason: "missing emit_stage_envelope" },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const envelopeValue = behavior.type === "emit" || behavior.type === "wait_then_emit"
|
|
134
|
+
? behavior.envelope
|
|
135
|
+
: undefined;
|
|
136
|
+
try {
|
|
137
|
+
const envelope = assertRequiredEnvelope(envelopeValue);
|
|
138
|
+
assertEnvelopePayload(envelope, input.stage.payload_schema);
|
|
139
|
+
if (!isAdvancingEnvelope(envelope)) {
|
|
140
|
+
input.onActivity?.({ event: "agent_end" });
|
|
141
|
+
return {
|
|
142
|
+
status: "completed",
|
|
143
|
+
result: {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: "status: failure",
|
|
146
|
+
envelope,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
input.onActivity?.({
|
|
151
|
+
event: "message",
|
|
152
|
+
role: "assistant",
|
|
153
|
+
text: envelope.summary,
|
|
154
|
+
});
|
|
155
|
+
input.onActivity?.({ event: "agent_end" });
|
|
156
|
+
return { status: "completed", result: { ok: true, envelope } };
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
input.onActivity?.({ event: "agent_end" });
|
|
160
|
+
return {
|
|
161
|
+
status: "completed",
|
|
162
|
+
result: {
|
|
163
|
+
ok: false,
|
|
164
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
stageId: input.stage.id,
|
|
171
|
+
async next() {
|
|
172
|
+
if (closed) {
|
|
173
|
+
return {
|
|
174
|
+
status: "completed",
|
|
175
|
+
result: { ok: false, reason: "stage handle closed" },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (resumeCorrupt) {
|
|
179
|
+
input.onActivity?.({ event: "agent_end" });
|
|
180
|
+
return {
|
|
181
|
+
status: "completed",
|
|
182
|
+
result: {
|
|
183
|
+
ok: false,
|
|
184
|
+
reason: "corrupt resume context for waiting stage",
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
emitStartActivity();
|
|
189
|
+
if (behavior.type !== "wait_then_emit") {
|
|
190
|
+
return finishEmit();
|
|
191
|
+
}
|
|
192
|
+
if (waiting) {
|
|
193
|
+
const answer = await awaitAnswer();
|
|
194
|
+
if (answer === undefined && closed) {
|
|
195
|
+
return {
|
|
196
|
+
status: "completed",
|
|
197
|
+
result: { ok: false, reason: "stage handle closed" },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const priorRequest = behavior.waitRequests[waitIndex - 1];
|
|
201
|
+
const t2Prompt = tryParsePendingPrompt(priorRequest);
|
|
202
|
+
if (t2Prompt !== undefined && answer !== undefined) {
|
|
203
|
+
try {
|
|
204
|
+
const parsed = parseAskOperatorAnswer(answer);
|
|
205
|
+
assertAnswerMatchesPrompt(t2Prompt, parsed);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
input.onActivity?.({ event: "agent_end" });
|
|
209
|
+
clearFakeHitlResume(input.roots, input.stage.id);
|
|
210
|
+
return {
|
|
211
|
+
status: "completed",
|
|
212
|
+
result: {
|
|
213
|
+
ok: false,
|
|
214
|
+
reason: err instanceof Error
|
|
215
|
+
? err.message
|
|
216
|
+
: `invalid T2 answer at wait ${waitIndex - 1}`,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const expected = behavior.expectedAnswers?.[waitIndex - 1];
|
|
222
|
+
if (expected !== undefined &&
|
|
223
|
+
answer !== undefined &&
|
|
224
|
+
!opaqueEqual(answer, expected)) {
|
|
225
|
+
input.onActivity?.({ event: "agent_end" });
|
|
226
|
+
clearFakeHitlResume(input.roots, input.stage.id);
|
|
227
|
+
return {
|
|
228
|
+
status: "completed",
|
|
229
|
+
result: {
|
|
230
|
+
ok: false,
|
|
231
|
+
reason: `unexpected answer at wait ${waitIndex - 1}`,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (waitIndex < behavior.waitRequests.length) {
|
|
237
|
+
const request = behavior.waitRequests[waitIndex];
|
|
238
|
+
waitIndex += 1;
|
|
239
|
+
armWait();
|
|
240
|
+
writeFakeHitlResume(input.roots, input.stage.id, {
|
|
241
|
+
waitRequests: behavior.waitRequests,
|
|
242
|
+
expectedAnswers: behavior.expectedAnswers,
|
|
243
|
+
envelope: behavior.envelope,
|
|
244
|
+
waitIndex,
|
|
245
|
+
});
|
|
246
|
+
return { status: "waiting_for_input", request };
|
|
247
|
+
}
|
|
248
|
+
return finishEmit();
|
|
249
|
+
},
|
|
250
|
+
deliverAnswer(answer) {
|
|
251
|
+
if (!waiting || !answerResolver)
|
|
252
|
+
return;
|
|
253
|
+
const resolve = answerResolver;
|
|
254
|
+
answerResolver = undefined;
|
|
255
|
+
receivedAnswers.push(answer);
|
|
256
|
+
resolve(answer);
|
|
257
|
+
},
|
|
258
|
+
async close(options) {
|
|
259
|
+
closed = true;
|
|
260
|
+
if (options?.park) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (waiting && answerResolver) {
|
|
264
|
+
const resolve = answerResolver;
|
|
265
|
+
answerResolver = undefined;
|
|
266
|
+
waiting = false;
|
|
267
|
+
answerPromise = undefined;
|
|
268
|
+
resolve(undefined);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
async runStage(input) {
|
|
274
|
+
return runStageViaOpen(this, input);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export function scriptedFakeAgent(behaviors) {
|
|
278
|
+
const recorded = [];
|
|
279
|
+
let index = 0;
|
|
280
|
+
return {
|
|
281
|
+
recorded,
|
|
282
|
+
openStage(input) {
|
|
283
|
+
const behavior = behaviors[index] ?? { type: "never_emit" };
|
|
284
|
+
index += 1;
|
|
285
|
+
const agent = new FakeAgent(behavior);
|
|
286
|
+
const handle = agent.openStage(input);
|
|
287
|
+
if (agent.lastRoots)
|
|
288
|
+
recorded.push(agent.lastRoots);
|
|
289
|
+
return handle;
|
|
290
|
+
},
|
|
291
|
+
async runStage(input) {
|
|
292
|
+
return runStageViaOpen(this, input);
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AuthEvent, AuthPrompt } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type ProviderAuthContext, type ProviderAuthStatus } from "./providerAuth.js";
|
|
3
|
+
export type LoginSessionStatus = "running" | "completed" | "failed" | "cancelled";
|
|
4
|
+
export type LoginSessionPendingPrompt = {
|
|
5
|
+
type: AuthPrompt["type"];
|
|
6
|
+
message: string;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
options?: readonly {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
export type LoginSessionProjection = {
|
|
15
|
+
id: string;
|
|
16
|
+
providerId: string;
|
|
17
|
+
authType: "oauth";
|
|
18
|
+
status: LoginSessionStatus;
|
|
19
|
+
events: AuthEvent[];
|
|
20
|
+
pendingPrompt?: LoginSessionPendingPrompt;
|
|
21
|
+
error?: {
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
warning?: {
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
provider?: ProviderAuthStatus;
|
|
28
|
+
};
|
|
29
|
+
export declare function getLoginSession(sessionId: string): LoginSessionProjection | undefined;
|
|
30
|
+
export declare function answerLoginSession(sessionId: string, value: string): LoginSessionProjection;
|
|
31
|
+
export declare function cancelLoginSession(sessionId: string): LoginSessionProjection;
|
|
32
|
+
export declare function resetSessionsForTests(): void;
|
|
33
|
+
export declare function startOAuthLoginSession(cwd: string, providerId: string, ctx?: ProviderAuthContext): Promise<LoginSessionProjection>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { CredentialSynchronizationError } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { defaultContext, openRuntime, statusForProvider, ProviderAuthError, } from "./providerAuth.js";
|
|
4
|
+
const loginSessions = new Map();
|
|
5
|
+
let runningLoginSessionId;
|
|
6
|
+
function promptMeta(prompt) {
|
|
7
|
+
if (prompt.type === "select") {
|
|
8
|
+
return {
|
|
9
|
+
type: "select",
|
|
10
|
+
message: prompt.message,
|
|
11
|
+
options: prompt.options.map((o) => ({
|
|
12
|
+
id: o.id,
|
|
13
|
+
label: o.label,
|
|
14
|
+
...(o.description !== undefined ? { description: o.description } : {}),
|
|
15
|
+
})),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
type: prompt.type,
|
|
20
|
+
message: prompt.message,
|
|
21
|
+
...(prompt.placeholder !== undefined
|
|
22
|
+
? { placeholder: prompt.placeholder }
|
|
23
|
+
: {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function projectLoginSession(session) {
|
|
27
|
+
return {
|
|
28
|
+
id: session.id,
|
|
29
|
+
providerId: session.providerId,
|
|
30
|
+
authType: "oauth",
|
|
31
|
+
status: session.status,
|
|
32
|
+
events: [...session.events],
|
|
33
|
+
...(session.pendingPrompt
|
|
34
|
+
? { pendingPrompt: session.pendingPrompt.meta }
|
|
35
|
+
: {}),
|
|
36
|
+
...(session.error ? { error: session.error } : {}),
|
|
37
|
+
...(session.warning ? { warning: session.warning } : {}),
|
|
38
|
+
...(session.provider ? { provider: session.provider } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function rejectPendingPrompt(session, reason) {
|
|
42
|
+
const pending = session.pendingPrompt;
|
|
43
|
+
if (!pending)
|
|
44
|
+
return;
|
|
45
|
+
session.pendingPrompt = undefined;
|
|
46
|
+
pending.reject(reason);
|
|
47
|
+
}
|
|
48
|
+
function clearRunningIf(sessionId) {
|
|
49
|
+
if (runningLoginSessionId === sessionId) {
|
|
50
|
+
runningLoginSessionId = undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function getLoginSession(sessionId) {
|
|
54
|
+
const session = loginSessions.get(sessionId);
|
|
55
|
+
return session ? projectLoginSession(session) : undefined;
|
|
56
|
+
}
|
|
57
|
+
export function answerLoginSession(sessionId, value) {
|
|
58
|
+
const session = loginSessions.get(sessionId);
|
|
59
|
+
if (!session) {
|
|
60
|
+
throw new ProviderAuthError("Login session not found", 404);
|
|
61
|
+
}
|
|
62
|
+
if (session.status !== "running") {
|
|
63
|
+
throw new ProviderAuthError("Login session is not awaiting input", 409);
|
|
64
|
+
}
|
|
65
|
+
if (!session.pendingPrompt) {
|
|
66
|
+
throw new ProviderAuthError("No pending login prompt", 409);
|
|
67
|
+
}
|
|
68
|
+
if (typeof value !== "string") {
|
|
69
|
+
throw new ProviderAuthError("value is required", 400);
|
|
70
|
+
}
|
|
71
|
+
const pending = session.pendingPrompt;
|
|
72
|
+
session.pendingPrompt = undefined;
|
|
73
|
+
pending.resolve(value);
|
|
74
|
+
return projectLoginSession(session);
|
|
75
|
+
}
|
|
76
|
+
export function cancelLoginSession(sessionId) {
|
|
77
|
+
const session = loginSessions.get(sessionId);
|
|
78
|
+
if (!session) {
|
|
79
|
+
throw new ProviderAuthError("Login session not found", 404);
|
|
80
|
+
}
|
|
81
|
+
if (session.status !== "running") {
|
|
82
|
+
return projectLoginSession(session);
|
|
83
|
+
}
|
|
84
|
+
session.status = "cancelled";
|
|
85
|
+
session.error = { message: "Login cancelled" };
|
|
86
|
+
clearRunningIf(sessionId);
|
|
87
|
+
rejectPendingPrompt(session, new Error("Login cancelled"));
|
|
88
|
+
session.abortController.abort();
|
|
89
|
+
return projectLoginSession(session);
|
|
90
|
+
}
|
|
91
|
+
export function resetSessionsForTests() {
|
|
92
|
+
for (const session of loginSessions.values()) {
|
|
93
|
+
if (session.status === "running") {
|
|
94
|
+
session.status = "cancelled";
|
|
95
|
+
rejectPendingPrompt(session, new Error("Login cancelled"));
|
|
96
|
+
session.abortController.abort();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
loginSessions.clear();
|
|
100
|
+
runningLoginSessionId = undefined;
|
|
101
|
+
}
|
|
102
|
+
export async function startOAuthLoginSession(cwd, providerId, ctx = defaultContext) {
|
|
103
|
+
if (runningLoginSessionId !== undefined) {
|
|
104
|
+
throw new ProviderAuthError("An OAuth login is already in progress", 409);
|
|
105
|
+
}
|
|
106
|
+
const runtime = await openRuntime(cwd, ctx);
|
|
107
|
+
const provider = runtime.getProvider(providerId);
|
|
108
|
+
if (!provider) {
|
|
109
|
+
throw new ProviderAuthError("Provider not found", 404);
|
|
110
|
+
}
|
|
111
|
+
if (provider.auth.oauth === undefined) {
|
|
112
|
+
throw new ProviderAuthError("Provider does not support oauth login", 400);
|
|
113
|
+
}
|
|
114
|
+
const sessionId = randomUUID();
|
|
115
|
+
const abortController = new AbortController();
|
|
116
|
+
const session = {
|
|
117
|
+
id: sessionId,
|
|
118
|
+
providerId,
|
|
119
|
+
authType: "oauth",
|
|
120
|
+
status: "running",
|
|
121
|
+
events: [],
|
|
122
|
+
abortController,
|
|
123
|
+
};
|
|
124
|
+
loginSessions.set(sessionId, session);
|
|
125
|
+
runningLoginSessionId = sessionId;
|
|
126
|
+
const interaction = {
|
|
127
|
+
signal: abortController.signal,
|
|
128
|
+
notify(event) {
|
|
129
|
+
if (session.status !== "running")
|
|
130
|
+
return;
|
|
131
|
+
session.events.push(event);
|
|
132
|
+
},
|
|
133
|
+
prompt(prompt) {
|
|
134
|
+
if (session.status !== "running") {
|
|
135
|
+
return Promise.reject(new Error("Login session is not running"));
|
|
136
|
+
}
|
|
137
|
+
if (abortController.signal.aborted || prompt.signal?.aborted) {
|
|
138
|
+
return Promise.reject(new Error("Login cancelled"));
|
|
139
|
+
}
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const onAbort = () => {
|
|
142
|
+
if (session.pendingPrompt) {
|
|
143
|
+
session.pendingPrompt = undefined;
|
|
144
|
+
reject(new Error("Login cancelled"));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
abortController.signal.addEventListener("abort", onAbort, {
|
|
148
|
+
once: true,
|
|
149
|
+
});
|
|
150
|
+
if (prompt.signal) {
|
|
151
|
+
prompt.signal.addEventListener("abort", onAbort, { once: true });
|
|
152
|
+
}
|
|
153
|
+
session.pendingPrompt = {
|
|
154
|
+
meta: promptMeta(prompt),
|
|
155
|
+
resolve: (value) => {
|
|
156
|
+
abortController.signal.removeEventListener("abort", onAbort);
|
|
157
|
+
prompt.signal?.removeEventListener("abort", onAbort);
|
|
158
|
+
resolve(value);
|
|
159
|
+
},
|
|
160
|
+
reject: (err) => {
|
|
161
|
+
abortController.signal.removeEventListener("abort", onAbort);
|
|
162
|
+
prompt.signal?.removeEventListener("abort", onAbort);
|
|
163
|
+
reject(err);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
void (async () => {
|
|
170
|
+
try {
|
|
171
|
+
await runtime.login(providerId, "oauth", interaction);
|
|
172
|
+
if (session.status !== "running")
|
|
173
|
+
return;
|
|
174
|
+
session.status = "completed";
|
|
175
|
+
session.provider = await statusForProvider(runtime, providerId);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (session.status === "cancelled") {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (err instanceof CredentialSynchronizationError) {
|
|
182
|
+
session.status = "completed";
|
|
183
|
+
session.warning = {
|
|
184
|
+
message: "Connected, but model catalog sync failed. Credentials were saved — you can retry later without disconnecting.",
|
|
185
|
+
};
|
|
186
|
+
try {
|
|
187
|
+
session.provider = await statusForProvider(runtime, providerId);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
session.provider = {
|
|
191
|
+
providerId,
|
|
192
|
+
configured: true,
|
|
193
|
+
authKind: "oauth",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (abortController.signal.aborted) {
|
|
199
|
+
session.status = "cancelled";
|
|
200
|
+
session.error = { message: "Login cancelled" };
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
session.status = "failed";
|
|
204
|
+
session.error = { message: "Provider login failed" };
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
if (session.pendingPrompt) {
|
|
208
|
+
rejectPendingPrompt(session, new Error("Login finished"));
|
|
209
|
+
}
|
|
210
|
+
clearRunningIf(sessionId);
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
return projectLoginSession(session);
|
|
214
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { type AgentSession, DefaultResourceLoader, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { StageRoots } from "../runtime/stageRoots.js";
|
|
3
|
+
import { type AskOperatorPrompt, type AskOperatorWaitBridge } from "../tools/askOperator.js";
|
|
4
|
+
import "./cursorProvider.js";
|
|
5
|
+
import type { AgentPort, OpaqueAnswer, StageHandle, StageHandleCloseOptions, StageRunInput, StageRunResult } from "./port.js";
|
|
6
|
+
/**
|
|
7
|
+
* Stage tool allowlist for sealed Pi sessions.
|
|
8
|
+
* Always includes `ask_operator` (R7); `write_stage_artifact` when registered
|
|
9
|
+
* (always, for bound and unbound — bound/unbound only gates cwd / env bind).
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveStageToolNames(emitToolName: string, artifactToolName?: string, askOperatorToolName?: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* In-process wait channel for `ask_operator` → StageHandle yield/deliver.
|
|
14
|
+
* HITL `openStage` connects this via `setWaitHandler` + handle `deliverAnswer`.
|
|
15
|
+
*/
|
|
16
|
+
export declare class AskOperatorWaitChannel implements AskOperatorWaitBridge {
|
|
17
|
+
private pending;
|
|
18
|
+
private onWait;
|
|
19
|
+
setWaitHandler(handler: (prompt: AskOperatorPrompt) => void): void;
|
|
20
|
+
requestWait(prompt: AskOperatorPrompt): Promise<unknown>;
|
|
21
|
+
deliverAnswer(answer: unknown): boolean;
|
|
22
|
+
rejectPending(reason: string): void;
|
|
23
|
+
get hasPending(): boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Bridge used when no StageHandle wait loop is connected.
|
|
27
|
+
* Calling ask_operator fails closed instead of hanging the session.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createUnconnectedAskWaitBridge(): AskOperatorWaitBridge;
|
|
30
|
+
export type ConnectedAskWaitResume = {
|
|
31
|
+
onDeliver: (answer: OpaqueAnswer) => void;
|
|
32
|
+
continueRun: () => Promise<StageRunResult>;
|
|
33
|
+
};
|
|
34
|
+
export type ConnectedAskWaitStageHandleOptions = {
|
|
35
|
+
stageId: string;
|
|
36
|
+
askWaitChannel: AskOperatorWaitChannel;
|
|
37
|
+
run: () => Promise<StageRunResult>;
|
|
38
|
+
onBeforeWaitYield?: (prompt: AskOperatorPrompt) => void;
|
|
39
|
+
onClose?: (options?: StageHandleCloseOptions) => Promise<void>;
|
|
40
|
+
resume?: ConnectedAskWaitResume;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Connect `AskOperatorWaitChannel` (tool Promise) to a StageHandle wait/deliver
|
|
44
|
+
* loop. Pipeline park stays on StageHitlController via this handle only —
|
|
45
|
+
* runtime never imports the channel.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createConnectedAskWaitStageHandle(options: ConnectedAskWaitStageHandleOptions): StageHandle;
|
|
48
|
+
export declare function composeStageUserPrompt(input: StageRunInput, emitToolName: string, emitHintOverride?: string, artifactToolName?: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* DefaultResourceLoader with host/global discovery turned off.
|
|
51
|
+
*
|
|
52
|
+
* Without these flags the loader walks up from the run folder and would pick
|
|
53
|
+
* up the consumer project's AGENTS.md, `.agents/skills/`, `.pi/extensions`,
|
|
54
|
+
* and APPEND_SYSTEM.md. Stages must not inherit that context.
|
|
55
|
+
*
|
|
56
|
+
* `additionalExtensionPaths` is the only way extensions enter a sealed stage
|
|
57
|
+
* (used by StageProviderSupport implementations). With `noExtensions: true`,
|
|
58
|
+
* discovered global/project packages stay out; only allowlisted paths load.
|
|
59
|
+
* `additionalSkillPaths` is the matching allowlist for one named skill.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createSealedResourceLoader(options: {
|
|
62
|
+
cwd: string;
|
|
63
|
+
agentDir: string;
|
|
64
|
+
settingsManager: SettingsManager;
|
|
65
|
+
systemPrompt: string;
|
|
66
|
+
additionalExtensionPaths?: string[];
|
|
67
|
+
additionalSkillPaths?: string[];
|
|
68
|
+
}): DefaultResourceLoader;
|
|
69
|
+
/** Stable Pi session JSONL path under the run workspace stage dir. */
|
|
70
|
+
export declare function stageSessionFilePath(roots: Pick<StageRoots, "runWorkspaceDir" | "attempt">, stageId: string): string;
|
|
71
|
+
export declare class StageSessionReconstructError extends Error {
|
|
72
|
+
readonly stageId: string;
|
|
73
|
+
readonly sessionFile: string;
|
|
74
|
+
constructor(message: string, options: {
|
|
75
|
+
stageId: string;
|
|
76
|
+
sessionFile: string;
|
|
77
|
+
cause?: unknown;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Create or reopen the durable stage session file under the run workspace.
|
|
82
|
+
* Touches an empty file first so Pi flushes the session header immediately.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createStageSessionManager(roots: StageRoots, stageId: string): Promise<SessionManager>;
|
|
85
|
+
/**
|
|
86
|
+
* Open an existing stage session for post-restart reconstruct.
|
|
87
|
+
* Missing or corrupt files fail closed (KTD7) — never create a fresh session.
|
|
88
|
+
*/
|
|
89
|
+
export declare function openStageSessionManager(roots: StageRoots, stageId: string): Promise<SessionManager>;
|
|
90
|
+
/**
|
|
91
|
+
* Ensure the session is on disk before signaling waiting_for_input (KTD4).
|
|
92
|
+
* Dispose of the live AgentSession must not delete this file.
|
|
93
|
+
*/
|
|
94
|
+
export declare function ensureStageSessionFlushed(sessionManager: SessionManager, stageId: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* Remove an erroneous ask_operator toolResult written when a parked worker
|
|
97
|
+
* tears down before the operator answers. Restores an open tool call on disk.
|
|
98
|
+
*/
|
|
99
|
+
export declare function repairPrematureAskOperatorClosure(sessionFile: string): Promise<boolean>;
|
|
100
|
+
export type InjectOpaqueAnswerResult = {
|
|
101
|
+
injectedAs: "tool_result" | "custom_message";
|
|
102
|
+
toolCallId?: string;
|
|
103
|
+
toolName?: string;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Inject an opaque operator answer into the persisted conversation (R9).
|
|
107
|
+
* Prefers completing an open tool call as a tool result (T2 seam); otherwise
|
|
108
|
+
* appends a custom message that stays in LLM context.
|
|
109
|
+
*/
|
|
110
|
+
export declare function injectOpaqueAnswerIntoSession(sessionManager: SessionManager, answer: OpaqueAnswer): InjectOpaqueAnswerResult;
|
|
111
|
+
export type ReconstructedStageSession = {
|
|
112
|
+
session: AgentSession;
|
|
113
|
+
sessionManager: SessionManager;
|
|
114
|
+
sessionFile: string;
|
|
115
|
+
injection: InjectOpaqueAnswerResult;
|
|
116
|
+
restore: () => void;
|
|
117
|
+
shutdown: () => Promise<void>;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Reconstruct an equivalent Pi session after process loss and inject the
|
|
121
|
+
* operator answer into conversation context (R5, R9, R10 / KTD5).
|
|
122
|
+
* Does not auto-continue the model turn — U4 owns the yield loop.
|
|
123
|
+
*/
|
|
124
|
+
export declare function reconstructStageSessionForAnswer(input: StageRunInput, answer: OpaqueAnswer): Promise<ReconstructedStageSession>;
|
|
125
|
+
export declare class PiAgentAdapter implements AgentPort {
|
|
126
|
+
openStage(input: StageRunInput): StageHandle;
|
|
127
|
+
runStage(input: StageRunInput): Promise<StageRunResult>;
|
|
128
|
+
}
|