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,46 @@
|
|
|
1
|
+
import { derivePendingPrompt } from "../hitl/qaTrail.js";
|
|
2
|
+
import { stageStatusFromEvents } from "./port.js";
|
|
3
|
+
import { listArtifactNames } from "./workspaceLayout.js";
|
|
4
|
+
async function tryReadEnvelope(store, runId, stageId, latest) {
|
|
5
|
+
if (latest?.envelope != null) {
|
|
6
|
+
return latest.envelope;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
return await store.readEnvelope(runId, stageId);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function buildStageSnapshotFromStore(store, runId, stageId, workspaceDir) {
|
|
16
|
+
const latest = await store.getLatestStageExecution(runId, stageId);
|
|
17
|
+
let events;
|
|
18
|
+
let status;
|
|
19
|
+
let envelope;
|
|
20
|
+
let attempt_count;
|
|
21
|
+
if (latest !== null) {
|
|
22
|
+
events = await store.listStageEvents(runId, stageId, latest.attempt);
|
|
23
|
+
status = stageStatusFromEvents(events);
|
|
24
|
+
envelope = await tryReadEnvelope(store, runId, stageId, latest);
|
|
25
|
+
attempt_count = await store.countStageAttempts(runId, stageId);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
events = await store.listStageEvents(runId, stageId);
|
|
29
|
+
status = stageStatusFromEvents(events);
|
|
30
|
+
envelope = await tryReadEnvelope(store, runId, stageId, null);
|
|
31
|
+
attempt_count = 1;
|
|
32
|
+
}
|
|
33
|
+
const artifacts = await listArtifactNames(workspaceDir, stageId, latest?.attempt ?? 1);
|
|
34
|
+
const last_at = events.length > 0 ? events[events.length - 1]?.at : undefined;
|
|
35
|
+
const pending = derivePendingPrompt(events);
|
|
36
|
+
return {
|
|
37
|
+
stage_id: stageId,
|
|
38
|
+
status,
|
|
39
|
+
events,
|
|
40
|
+
envelope,
|
|
41
|
+
artifacts,
|
|
42
|
+
last_at,
|
|
43
|
+
attempt_count,
|
|
44
|
+
...(pending ? { pending_prompt: pending } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PipelineTrackProjection, RunPipelineDagSnapshot, RunStatus, StageSnapshot } from "./port.js";
|
|
2
|
+
export type BuildPipelineTrackInput = {
|
|
3
|
+
dagSnapshot: RunPipelineDagSnapshot | null | undefined;
|
|
4
|
+
stages: StageSnapshot[];
|
|
5
|
+
runStatus: RunStatus;
|
|
6
|
+
};
|
|
7
|
+
export declare function buildPipelineTrack(input: BuildPipelineTrackInput): PipelineTrackProjection;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { linearCompatDagSnapshot } from "./pipelineDagSnapshot.js";
|
|
2
|
+
function stageStarted(stage) {
|
|
3
|
+
return stage.events.some((e) => e.event === "started" || e.event === "resumed");
|
|
4
|
+
}
|
|
5
|
+
function runHalted(stages) {
|
|
6
|
+
return stages.some((s) => s.status === "failed");
|
|
7
|
+
}
|
|
8
|
+
function computeLayers(dag) {
|
|
9
|
+
const layers = new Map();
|
|
10
|
+
const nodeById = new Map(dag.nodes.map((n) => [n.id, n]));
|
|
11
|
+
function layerFor(id) {
|
|
12
|
+
const cached = layers.get(id);
|
|
13
|
+
if (cached !== undefined)
|
|
14
|
+
return cached;
|
|
15
|
+
const node = nodeById.get(id);
|
|
16
|
+
if (!node?.needs) {
|
|
17
|
+
layers.set(id, 0);
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const l = layerFor(node.needs) + 1;
|
|
21
|
+
layers.set(id, l);
|
|
22
|
+
return l;
|
|
23
|
+
}
|
|
24
|
+
for (const id of dag.stage_ids) {
|
|
25
|
+
layerFor(id);
|
|
26
|
+
}
|
|
27
|
+
return layers;
|
|
28
|
+
}
|
|
29
|
+
function deriveReadiness(stage, dag, snapshotsById, halted) {
|
|
30
|
+
const { status } = stage;
|
|
31
|
+
if (status === "succeeded")
|
|
32
|
+
return { readiness: "succeeded" };
|
|
33
|
+
if (status === "failed")
|
|
34
|
+
return { readiness: "failed" };
|
|
35
|
+
if (status === "running")
|
|
36
|
+
return { readiness: "running" };
|
|
37
|
+
if (status === "waiting_for_input")
|
|
38
|
+
return { readiness: "waiting" };
|
|
39
|
+
if (halted && !stageStarted(stage)) {
|
|
40
|
+
return { readiness: "skipped" };
|
|
41
|
+
}
|
|
42
|
+
const node = dag.nodes.find((n) => n.id === stage.stage_id);
|
|
43
|
+
const need = node?.needs;
|
|
44
|
+
if (need) {
|
|
45
|
+
const predecessor = snapshotsById.get(need);
|
|
46
|
+
if (!predecessor || predecessor.status !== "succeeded") {
|
|
47
|
+
return { readiness: "blocked", blocked_by: [need] };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { readiness: "ready" };
|
|
51
|
+
}
|
|
52
|
+
export function buildPipelineTrack(input) {
|
|
53
|
+
const snapshotsById = new Map(input.stages.map((s) => [s.stage_id, s]));
|
|
54
|
+
const stageIds = input.dagSnapshot?.stage_ids ??
|
|
55
|
+
input.stages.map((s) => s.stage_id);
|
|
56
|
+
const dag = input.dagSnapshot ?? linearCompatDagSnapshot(stageIds);
|
|
57
|
+
const layers = computeLayers(dag);
|
|
58
|
+
const halted = runHalted(input.stages);
|
|
59
|
+
const nodes = dag.stage_ids.map((stageId) => {
|
|
60
|
+
const stage = snapshotsById.get(stageId);
|
|
61
|
+
if (!stage) {
|
|
62
|
+
throw new Error(`buildPipelineTrack: missing overlay snapshot for "${stageId}" — call overlayPlannedStages first`);
|
|
63
|
+
}
|
|
64
|
+
const node = dag.nodes.find((n) => n.id === stageId);
|
|
65
|
+
const { readiness, blocked_by } = deriveReadiness(stage, dag, snapshotsById, halted);
|
|
66
|
+
const trackNode = {
|
|
67
|
+
stage_id: stageId,
|
|
68
|
+
status: stage.status,
|
|
69
|
+
readiness,
|
|
70
|
+
layer: layers.get(stageId) ?? 0,
|
|
71
|
+
layer_order: node?.stageIndex ?? dag.stage_ids.indexOf(stageId),
|
|
72
|
+
attempt_count: stage.attempt_count,
|
|
73
|
+
};
|
|
74
|
+
if (blocked_by?.length)
|
|
75
|
+
trackNode.blocked_by = blocked_by;
|
|
76
|
+
const gateKinds = dag.gate_kinds?.[stageId];
|
|
77
|
+
if (gateKinds?.length)
|
|
78
|
+
trackNode.gate_kinds = gateKinds;
|
|
79
|
+
return trackNode;
|
|
80
|
+
});
|
|
81
|
+
const edges = [];
|
|
82
|
+
for (const node of dag.nodes) {
|
|
83
|
+
if (!node.needs)
|
|
84
|
+
continue;
|
|
85
|
+
const fromStage = snapshotsById.get(node.needs);
|
|
86
|
+
const edge = { from: node.needs, to: node.id };
|
|
87
|
+
if (fromStage?.status === "succeeded" &&
|
|
88
|
+
fromStage.envelope?.summary) {
|
|
89
|
+
edge.envelope_summary = fromStage.envelope.summary;
|
|
90
|
+
}
|
|
91
|
+
edges.push(edge);
|
|
92
|
+
}
|
|
93
|
+
return { nodes, edges };
|
|
94
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { stageDir } from "./paths.js";
|
|
2
|
+
export declare function attemptWorkspaceDir(workspaceDir: string, stageId: string, attempt: number): string;
|
|
3
|
+
export declare function attemptLogPath(workspaceDir: string, stageId: string, attempt: number): string;
|
|
4
|
+
export declare function attemptEnvelopePath(workspaceDir: string, stageId: string, attempt: number): string;
|
|
5
|
+
export declare function attemptArtifactsDir(workspaceDir: string, stageId: string, attempt: number): string;
|
|
6
|
+
export declare function attemptAgentDir(workspaceDir: string, stageId: string, attempt: number): string;
|
|
7
|
+
export declare function attemptSessionPath(workspaceDir: string, stageId: string, attempt: number): string;
|
|
8
|
+
/** @deprecated Use attemptArtifactsDir with an explicit attempt number. */
|
|
9
|
+
export declare function artifactsDir(workspaceDir: string, stageId: string): string;
|
|
10
|
+
/** @deprecated Use attemptAgentDir with an explicit attempt number. */
|
|
11
|
+
export declare function agentDir(workspaceDir: string, stageId: string): string;
|
|
12
|
+
/** @deprecated Use attemptEnvelopePath with an explicit attempt number. */
|
|
13
|
+
export declare function envelopePath(workspaceDir: string, stageId: string): string;
|
|
14
|
+
/** @deprecated Use attemptLogPath with an explicit attempt number. */
|
|
15
|
+
export declare function stageLogPath(workspaceDir: string, stageId: string): string;
|
|
16
|
+
/** @deprecated Use attemptSessionPath with an explicit attempt number. */
|
|
17
|
+
export declare function stageSessionPath(workspaceDir: string, stageId: string): string;
|
|
18
|
+
export declare function isInsideDir(child: string, parent: string): boolean;
|
|
19
|
+
export declare function resolveArtifactTarget(runWorkspaceDir: string, stageId: string, attempt: number, relativePath: string): {
|
|
20
|
+
absolutePath: string;
|
|
21
|
+
runRelativePath: string;
|
|
22
|
+
artifactsDir: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function ensureRealArtifactsDir(runWorkspaceDir: string, artifactsDirectory: string): Promise<{
|
|
25
|
+
realRun: string;
|
|
26
|
+
realArtifacts: string;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function resolveContainedParent(realArtifacts: string, relativeToArtifacts: string): Promise<string>;
|
|
29
|
+
/** Legacy stage-root artifacts dir (pre-uniform unbound writes); kept for reads. */
|
|
30
|
+
export declare function stageArtifactsDir(workspaceDir: string, stageId: string): string;
|
|
31
|
+
export declare function listArtifactNames(workspaceDir: string, stageId: string, attempt: number): Promise<string[]>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { access, lstat, mkdir, readdir, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export { stageDir } from "./paths.js";
|
|
4
|
+
function guardStageId(stageId) {
|
|
5
|
+
if (stageId.includes("..") ||
|
|
6
|
+
stageId.includes("/") ||
|
|
7
|
+
stageId.includes("\\")) {
|
|
8
|
+
throw new Error("stageId must not contain path separators or ..");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function guardAttempt(attempt) {
|
|
12
|
+
if (!Number.isInteger(attempt) || attempt < 1) {
|
|
13
|
+
throw new Error("attempt must be a positive integer");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function attemptWorkspaceDir(workspaceDir, stageId, attempt) {
|
|
17
|
+
guardStageId(stageId);
|
|
18
|
+
guardAttempt(attempt);
|
|
19
|
+
return path.join(workspaceDir, "stages", stageId, "attempts", String(attempt));
|
|
20
|
+
}
|
|
21
|
+
export function attemptLogPath(workspaceDir, stageId, attempt) {
|
|
22
|
+
return path.join(attemptWorkspaceDir(workspaceDir, stageId, attempt), "log.jsonl");
|
|
23
|
+
}
|
|
24
|
+
export function attemptEnvelopePath(workspaceDir, stageId, attempt) {
|
|
25
|
+
return path.join(attemptWorkspaceDir(workspaceDir, stageId, attempt), "envelope.json");
|
|
26
|
+
}
|
|
27
|
+
export function attemptArtifactsDir(workspaceDir, stageId, attempt) {
|
|
28
|
+
return path.join(attemptWorkspaceDir(workspaceDir, stageId, attempt), "artifacts");
|
|
29
|
+
}
|
|
30
|
+
export function attemptAgentDir(workspaceDir, stageId, attempt) {
|
|
31
|
+
return path.join(attemptWorkspaceDir(workspaceDir, stageId, attempt), ".pi-agent");
|
|
32
|
+
}
|
|
33
|
+
export function attemptSessionPath(workspaceDir, stageId, attempt) {
|
|
34
|
+
return path.join(attemptWorkspaceDir(workspaceDir, stageId, attempt), "pi-session.jsonl");
|
|
35
|
+
}
|
|
36
|
+
/** @deprecated Use attemptArtifactsDir with an explicit attempt number. */
|
|
37
|
+
export function artifactsDir(workspaceDir, stageId) {
|
|
38
|
+
return attemptArtifactsDir(workspaceDir, stageId, 1);
|
|
39
|
+
}
|
|
40
|
+
/** @deprecated Use attemptAgentDir with an explicit attempt number. */
|
|
41
|
+
export function agentDir(workspaceDir, stageId) {
|
|
42
|
+
return attemptAgentDir(workspaceDir, stageId, 1);
|
|
43
|
+
}
|
|
44
|
+
/** @deprecated Use attemptEnvelopePath with an explicit attempt number. */
|
|
45
|
+
export function envelopePath(workspaceDir, stageId) {
|
|
46
|
+
return attemptEnvelopePath(workspaceDir, stageId, 1);
|
|
47
|
+
}
|
|
48
|
+
/** @deprecated Use attemptLogPath with an explicit attempt number. */
|
|
49
|
+
export function stageLogPath(workspaceDir, stageId) {
|
|
50
|
+
return attemptLogPath(workspaceDir, stageId, 1);
|
|
51
|
+
}
|
|
52
|
+
/** @deprecated Use attemptSessionPath with an explicit attempt number. */
|
|
53
|
+
export function stageSessionPath(workspaceDir, stageId) {
|
|
54
|
+
return attemptSessionPath(workspaceDir, stageId, 1);
|
|
55
|
+
}
|
|
56
|
+
export function isInsideDir(child, parent) {
|
|
57
|
+
const relative = path.relative(parent, child);
|
|
58
|
+
return (relative === "" ||
|
|
59
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
60
|
+
relative !== ".." &&
|
|
61
|
+
!path.isAbsolute(relative)));
|
|
62
|
+
}
|
|
63
|
+
export function resolveArtifactTarget(runWorkspaceDir, stageId, attempt, relativePath) {
|
|
64
|
+
guardStageId(stageId);
|
|
65
|
+
guardAttempt(attempt);
|
|
66
|
+
if (typeof relativePath !== "string" || relativePath.length === 0) {
|
|
67
|
+
throw new Error("path must be a non-empty string");
|
|
68
|
+
}
|
|
69
|
+
if (path.isAbsolute(relativePath)) {
|
|
70
|
+
throw new Error("path must be relative to the stage artifacts directory");
|
|
71
|
+
}
|
|
72
|
+
const artifacts = path.resolve(attemptArtifactsDir(runWorkspaceDir, stageId, attempt));
|
|
73
|
+
const resolved = path.resolve(artifacts, relativePath);
|
|
74
|
+
const relativeToArtifacts = path.relative(artifacts, resolved);
|
|
75
|
+
if (relativeToArtifacts === "") {
|
|
76
|
+
throw new Error("path must name a file under the stage artifacts directory");
|
|
77
|
+
}
|
|
78
|
+
if (relativeToArtifacts === ".." ||
|
|
79
|
+
relativeToArtifacts.startsWith(`..${path.sep}`) ||
|
|
80
|
+
path.isAbsolute(relativeToArtifacts)) {
|
|
81
|
+
throw new Error("path escapes the stage artifacts directory");
|
|
82
|
+
}
|
|
83
|
+
const runRelativePath = path.posix.join("stages", stageId, "attempts", String(attempt), "artifacts", ...relativeToArtifacts.split(path.sep));
|
|
84
|
+
return {
|
|
85
|
+
absolutePath: resolved,
|
|
86
|
+
runRelativePath,
|
|
87
|
+
artifactsDir: artifacts,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export async function ensureRealArtifactsDir(runWorkspaceDir, artifactsDirectory) {
|
|
91
|
+
await mkdir(artifactsDirectory, { recursive: true });
|
|
92
|
+
const realRun = await realpath(runWorkspaceDir);
|
|
93
|
+
const realArtifacts = await realpath(artifactsDirectory);
|
|
94
|
+
if (!isInsideDir(realArtifacts, realRun)) {
|
|
95
|
+
throw new Error("path escapes the stage artifacts directory");
|
|
96
|
+
}
|
|
97
|
+
return { realRun, realArtifacts };
|
|
98
|
+
}
|
|
99
|
+
export async function resolveContainedParent(realArtifacts, relativeToArtifacts) {
|
|
100
|
+
const segments = relativeToArtifacts.split(path.sep).filter((s) => s.length > 0);
|
|
101
|
+
if (segments.length === 0) {
|
|
102
|
+
throw new Error("path must name a file under the stage artifacts directory");
|
|
103
|
+
}
|
|
104
|
+
const dirSegments = segments.slice(0, -1);
|
|
105
|
+
let current = realArtifacts;
|
|
106
|
+
for (const segment of dirSegments) {
|
|
107
|
+
const next = path.join(current, segment);
|
|
108
|
+
try {
|
|
109
|
+
const st = await lstat(next);
|
|
110
|
+
if (st.isSymbolicLink()) {
|
|
111
|
+
throw new Error("path escapes the stage artifacts directory");
|
|
112
|
+
}
|
|
113
|
+
if (!st.isDirectory()) {
|
|
114
|
+
throw new Error("path escapes the stage artifacts directory");
|
|
115
|
+
}
|
|
116
|
+
current = await realpath(next);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (err.code !== "ENOENT") {
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
await mkdir(next);
|
|
123
|
+
current = next;
|
|
124
|
+
}
|
|
125
|
+
if (!isInsideDir(current, realArtifacts)) {
|
|
126
|
+
throw new Error("path escapes the stage artifacts directory");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return current;
|
|
130
|
+
}
|
|
131
|
+
async function fileExists(filePath) {
|
|
132
|
+
try {
|
|
133
|
+
await access(filePath);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function walkArtifactFiles(dir, runRelativePrefix, relativeParts, out) {
|
|
141
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
const nextParts = [...relativeParts, entry.name];
|
|
144
|
+
const full = path.join(dir, entry.name);
|
|
145
|
+
if (entry.isDirectory()) {
|
|
146
|
+
await walkArtifactFiles(full, runRelativePrefix, nextParts, out);
|
|
147
|
+
}
|
|
148
|
+
else if (entry.isFile()) {
|
|
149
|
+
out.push(path.posix.join(...runRelativePrefix, ...nextParts));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Legacy stage-root artifacts dir (pre-uniform unbound writes); kept for reads. */
|
|
154
|
+
export function stageArtifactsDir(workspaceDir, stageId) {
|
|
155
|
+
guardStageId(stageId);
|
|
156
|
+
return path.join(workspaceDir, "stages", stageId, "artifacts");
|
|
157
|
+
}
|
|
158
|
+
export async function listArtifactNames(workspaceDir, stageId, attempt) {
|
|
159
|
+
guardStageId(stageId);
|
|
160
|
+
guardAttempt(attempt);
|
|
161
|
+
const out = [];
|
|
162
|
+
const attemptDir = attemptArtifactsDir(workspaceDir, stageId, attempt);
|
|
163
|
+
if (await fileExists(attemptDir)) {
|
|
164
|
+
await walkArtifactFiles(attemptDir, ["stages", stageId, "attempts", String(attempt), "artifacts"], [], out);
|
|
165
|
+
}
|
|
166
|
+
const stageDirPath = stageArtifactsDir(workspaceDir, stageId);
|
|
167
|
+
if (await fileExists(stageDirPath)) {
|
|
168
|
+
await walkArtifactFiles(stageDirPath, ["stages", stageId, "artifacts"], [], out);
|
|
169
|
+
}
|
|
170
|
+
out.sort();
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StageExecutionMode } from "./stageConcurrency.js";
|
|
2
|
+
import type { DeliverAnswerResult } from "./stageHitl.js";
|
|
3
|
+
export declare const WAIT_WITHOUT_WORKER_DISPATCH = "stage entered wait without worker dispatch";
|
|
4
|
+
export type AnswerResumeAdapterResult = {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
reason?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function orchestrateAnswerResume(options: {
|
|
9
|
+
prefixMode: "live" | "restart";
|
|
10
|
+
executionMode: StageExecutionMode;
|
|
11
|
+
insertedForResume: boolean;
|
|
12
|
+
removeInsertedResume: () => void;
|
|
13
|
+
registerResumeUntrack: (done: Promise<AnswerResumeAdapterResult>) => void;
|
|
14
|
+
resumeViaSubprocess: () => Promise<AnswerResumeAdapterResult>;
|
|
15
|
+
reconstructAndContinue: () => Promise<AnswerResumeAdapterResult>;
|
|
16
|
+
}): Promise<DeliverAnswerResult>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const WAIT_WITHOUT_WORKER_DISPATCH = "stage entered wait without worker dispatch";
|
|
2
|
+
export async function orchestrateAnswerResume(options) {
|
|
3
|
+
if (options.prefixMode === "live" && options.executionMode !== "process") {
|
|
4
|
+
if (options.insertedForResume) {
|
|
5
|
+
options.removeInsertedResume();
|
|
6
|
+
}
|
|
7
|
+
return { ok: true };
|
|
8
|
+
}
|
|
9
|
+
const done = options.executionMode === "process"
|
|
10
|
+
? options.resumeViaSubprocess()
|
|
11
|
+
: options.reconstructAndContinue();
|
|
12
|
+
options.registerResumeUntrack(done);
|
|
13
|
+
const outcome = await done;
|
|
14
|
+
if (!outcome.ok) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
reason: outcome.reason ?? "resume failed",
|
|
18
|
+
status: 500,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return { ok: true };
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { parseCredentialSource, readCredentialSourceFromFile, writeCredentialSourceToFile, type CredentialSource } from "./settingsFile.js";
|
|
2
|
+
export { parseCredentialSource, readCredentialSourceFromFile, writeCredentialSourceToFile, };
|
|
3
|
+
export type CredentialBinding = {
|
|
4
|
+
source: CredentialSource;
|
|
5
|
+
authPath: string;
|
|
6
|
+
provisional: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type ResolveCredentialBindingOptions = {
|
|
9
|
+
piHomeAuthPath?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function sfOwnedAgentDir(cwd: string): string;
|
|
12
|
+
export declare function sfOwnedAuthPath(cwd: string): string;
|
|
13
|
+
export declare function piHomeAuthPath(override?: string): string;
|
|
14
|
+
export declare function isUsableAuthFile(authPath: string): boolean;
|
|
15
|
+
export declare function ensureSfOwnedAuthStore(cwd: string): string;
|
|
16
|
+
export declare function resolveCredentialBinding(cwd: string, options?: ResolveCredentialBindingOptions): CredentialBinding;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { storeRootFor } from "../runstore/paths.js";
|
|
5
|
+
import { parseCredentialSource, readCredentialSourceFromFile, writeCredentialSourceToFile, } from "./settingsFile.js";
|
|
6
|
+
export { parseCredentialSource, readCredentialSourceFromFile, writeCredentialSourceToFile, };
|
|
7
|
+
export function sfOwnedAgentDir(cwd) {
|
|
8
|
+
return path.join(storeRootFor(cwd), "agent");
|
|
9
|
+
}
|
|
10
|
+
export function sfOwnedAuthPath(cwd) {
|
|
11
|
+
return path.join(sfOwnedAgentDir(cwd), "auth.json");
|
|
12
|
+
}
|
|
13
|
+
export function piHomeAuthPath(override) {
|
|
14
|
+
return (override ??
|
|
15
|
+
path.join(os.homedir(), ".pi", "agent", "auth.json"));
|
|
16
|
+
}
|
|
17
|
+
export function isUsableAuthFile(authPath) {
|
|
18
|
+
if (!existsSync(authPath))
|
|
19
|
+
return false;
|
|
20
|
+
try {
|
|
21
|
+
const content = readFileSync(authPath, "utf8").trim();
|
|
22
|
+
return content !== "" && content !== "{}";
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function ensureSfOwnedAuthStore(cwd) {
|
|
29
|
+
const storeRoot = storeRootFor(cwd);
|
|
30
|
+
mkdirSync(storeRoot, { recursive: true });
|
|
31
|
+
const agentDir = sfOwnedAgentDir(cwd);
|
|
32
|
+
mkdirSync(agentDir, { recursive: true });
|
|
33
|
+
try {
|
|
34
|
+
chmodSync(agentDir, 0o700);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// best-effort on non-POSIX
|
|
38
|
+
}
|
|
39
|
+
const authPath = sfOwnedAuthPath(cwd);
|
|
40
|
+
if (!existsSync(authPath)) {
|
|
41
|
+
writeFileSync(authPath, "{}\n", { encoding: "utf8", mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
chmodSync(authPath, 0o600);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// best-effort on non-POSIX
|
|
48
|
+
}
|
|
49
|
+
return authPath;
|
|
50
|
+
}
|
|
51
|
+
export function resolveCredentialBinding(cwd, options = {}) {
|
|
52
|
+
const piHome = piHomeAuthPath(options.piHomeAuthPath);
|
|
53
|
+
const persisted = readCredentialSourceFromFile(cwd);
|
|
54
|
+
if (persisted !== undefined) {
|
|
55
|
+
if (persisted === "sf_owned") {
|
|
56
|
+
return {
|
|
57
|
+
source: "sf_owned",
|
|
58
|
+
authPath: ensureSfOwnedAuthStore(cwd),
|
|
59
|
+
provisional: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
source: "pi_home",
|
|
64
|
+
authPath: piHome,
|
|
65
|
+
provisional: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (isUsableAuthFile(piHome)) {
|
|
69
|
+
return {
|
|
70
|
+
source: "pi_home",
|
|
71
|
+
authPath: piHome,
|
|
72
|
+
provisional: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
source: "sf_owned",
|
|
77
|
+
authPath: ensureSfOwnedAuthStore(cwd),
|
|
78
|
+
provisional: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
2
|
+
export declare function collectDownstreamStageIds(dag: ResolvedPipelineDag, rootId: string): Set<string>;
|
|
3
|
+
export declare function collectDownstreamClosure(dag: ResolvedPipelineDag, rootIds: string[]): Set<string>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function collectDownstreamStageIds(dag, rootId) {
|
|
2
|
+
const downstream = new Set();
|
|
3
|
+
const queue = [...(dag.childrenOf[rootId] ?? [])];
|
|
4
|
+
while (queue.length > 0) {
|
|
5
|
+
const id = queue.shift();
|
|
6
|
+
if (downstream.has(id))
|
|
7
|
+
continue;
|
|
8
|
+
downstream.add(id);
|
|
9
|
+
const children = dag.childrenOf[id];
|
|
10
|
+
if (children) {
|
|
11
|
+
for (const child of children) {
|
|
12
|
+
queue.push(child);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return downstream;
|
|
17
|
+
}
|
|
18
|
+
export function collectDownstreamClosure(dag, rootIds) {
|
|
19
|
+
const downstream = new Set();
|
|
20
|
+
for (const rootId of rootIds) {
|
|
21
|
+
for (const id of collectDownstreamStageIds(dag, rootId)) {
|
|
22
|
+
downstream.add(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return downstream;
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RunStore, StageSnapshot } from "../runstore/port.js";
|
|
2
|
+
import type { StageEnvelope } from "../types/envelope.js";
|
|
3
|
+
import type { LoadedPipeline, ResolvedPipelineDag } from "../types/pipeline.js";
|
|
4
|
+
import type { StageConfig } from "../types/stage.js";
|
|
5
|
+
export declare function buildCompletedEnvelopesFromRun(store: RunStore, runId: string, stages?: StageSnapshot[]): Promise<Map<string, StageEnvelope>>;
|
|
6
|
+
export declare function buildStageConfigById(loaded: LoadedPipeline): Map<string, StageConfig>;
|
|
7
|
+
export type ResolvePriorEnvelopeResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
prior: StageEnvelope | null;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function resolvePriorEnvelope(options: {
|
|
15
|
+
dag: ResolvedPipelineDag;
|
|
16
|
+
stageId: string;
|
|
17
|
+
completedEnvelopes: Map<string, StageEnvelope>;
|
|
18
|
+
store?: RunStore;
|
|
19
|
+
runId?: string;
|
|
20
|
+
}): Promise<ResolvePriorEnvelopeResult>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export async function buildCompletedEnvelopesFromRun(store, runId, stages) {
|
|
2
|
+
const completedEnvelopes = new Map();
|
|
3
|
+
const snapshotStages = stages ?? (await store.readRun(runId)).stages;
|
|
4
|
+
for (const snap of snapshotStages) {
|
|
5
|
+
if (snap.status !== "succeeded")
|
|
6
|
+
continue;
|
|
7
|
+
try {
|
|
8
|
+
const envelope = snap.envelope ?? (await store.readEnvelope(runId, snap.stage_id));
|
|
9
|
+
completedEnvelopes.set(snap.stage_id, envelope);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// envelope may be missing for corrupt runs; leave map empty
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return completedEnvelopes;
|
|
16
|
+
}
|
|
17
|
+
export function buildStageConfigById(loaded) {
|
|
18
|
+
const map = new Map();
|
|
19
|
+
for (const stage of loaded.stages) {
|
|
20
|
+
map.set(stage.id, stage);
|
|
21
|
+
}
|
|
22
|
+
return map;
|
|
23
|
+
}
|
|
24
|
+
function dagNode(dag, stageId) {
|
|
25
|
+
return dag.nodes.find((node) => node.id === stageId);
|
|
26
|
+
}
|
|
27
|
+
export async function resolvePriorEnvelope(options) {
|
|
28
|
+
const node = dagNode(options.dag, options.stageId);
|
|
29
|
+
if (!node) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: `stage "${options.stageId}" not in pipeline DAG`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (node.needs === null) {
|
|
36
|
+
return { ok: true, prior: null };
|
|
37
|
+
}
|
|
38
|
+
const parentId = node.needs;
|
|
39
|
+
const fromMap = options.completedEnvelopes.get(parentId);
|
|
40
|
+
if (fromMap !== undefined) {
|
|
41
|
+
return { ok: true, prior: structuredClone(fromMap) };
|
|
42
|
+
}
|
|
43
|
+
if (options.store !== undefined && options.runId !== undefined) {
|
|
44
|
+
try {
|
|
45
|
+
const envelope = await options.store.readEnvelope(options.runId, parentId);
|
|
46
|
+
return { ok: true, prior: structuredClone(envelope) };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
reason: `missing envelope for upstream stage "${parentId}"`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
reason: `missing envelope for upstream stage "${parentId}"`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { OpaqueAnswer, OpaqueWaitRequest } from "../agent/port.js";
|
|
2
|
+
/** Context for T2 answer/prompt matching (T2 owns kind schema later). */
|
|
3
|
+
export type AnswerPromptAssertContext = {
|
|
4
|
+
runId: string;
|
|
5
|
+
stageId: string;
|
|
6
|
+
answer: OpaqueAnswer;
|
|
7
|
+
pendingRequest?: OpaqueWaitRequest;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* T2 pre-append gate. Stub accepts any opaque until ask-operator lands.
|
|
11
|
+
* Call at deliverAnswer entry before any append or inject (KTD6 / KTD9).
|
|
12
|
+
*/
|
|
13
|
+
export type AssertAnswerMatchesPrompt = (ctx: AnswerPromptAssertContext) => void | Promise<void>;
|
|
14
|
+
export type HitlWaitEnterContext = {
|
|
15
|
+
runId: string;
|
|
16
|
+
stageId: string;
|
|
17
|
+
request: OpaqueWaitRequest;
|
|
18
|
+
};
|
|
19
|
+
export type HitlAnswerDeliverContext = {
|
|
20
|
+
runId: string;
|
|
21
|
+
stageId: string;
|
|
22
|
+
answer: OpaqueAnswer;
|
|
23
|
+
pendingRequest?: OpaqueWaitRequest;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* T3 ordered hooks. No-op until durable Q&A owns operator_prompt / operator_answer.
|
|
27
|
+
* T1 must not define event bodies here (KTD9).
|
|
28
|
+
*/
|
|
29
|
+
export type HitlQaHooks = {
|
|
30
|
+
onWaitEnter?: (ctx: HitlWaitEnterContext) => void | Promise<void>;
|
|
31
|
+
onAnswerDeliver?: (ctx: HitlAnswerDeliverContext) => void | Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
export declare const noopAssertAnswerMatchesPrompt: AssertAnswerMatchesPrompt;
|
|
34
|
+
export declare const noopHitlQaHooks: HitlQaHooks;
|
|
35
|
+
export type HitlSeams = {
|
|
36
|
+
assertAnswerMatchesPrompt?: AssertAnswerMatchesPrompt;
|
|
37
|
+
qaHooks?: HitlQaHooks;
|
|
38
|
+
};
|