348 lines
9.3 KiB
TypeScript
348 lines
9.3 KiB
TypeScript
/* eslint-disable max-classes-per-file -- the smoke contract keeps its schemas, errors, and transitions together. */
|
|
import { Effect, Schema } from "effect";
|
|
|
|
const MeaningfulString = Schema.String.check(
|
|
Schema.makeFilter((value) => value.trim().length > 0, {
|
|
expected: "a non-empty string",
|
|
})
|
|
);
|
|
|
|
const SmokePhase = Schema.Literals([
|
|
"preflight",
|
|
"planning",
|
|
"worker-running",
|
|
"reviewing",
|
|
"completed",
|
|
"failed",
|
|
]);
|
|
export type SmokePhase = typeof SmokePhase.Type;
|
|
|
|
const SmokeTransition = Schema.Literals([
|
|
"preflight-passed",
|
|
"plan-validated",
|
|
"worker-succeeded",
|
|
"review-approved",
|
|
"contract-failed",
|
|
"runtime-failed",
|
|
]);
|
|
type SmokeTransition = typeof SmokeTransition.Type;
|
|
|
|
export const SmokeRole = Schema.Literals(["planner-reviewer", "worker"]);
|
|
export type SmokeRole = typeof SmokeRole.Type;
|
|
|
|
const SmokeToolCallMode = Schema.Literal("serial");
|
|
|
|
export const SmokeRoleSelectionInput = Schema.Struct({
|
|
plannerReviewerModel: MeaningfulString,
|
|
plannerReviewerProvider: MeaningfulString,
|
|
workerModel: MeaningfulString,
|
|
workerProvider: MeaningfulString,
|
|
workerToolCallMode: SmokeToolCallMode,
|
|
});
|
|
export type SmokeRoleSelectionInput = typeof SmokeRoleSelectionInput.Type;
|
|
|
|
export const SmokeModelRole = Schema.Struct({
|
|
model: MeaningfulString,
|
|
provider: MeaningfulString,
|
|
role: SmokeRole,
|
|
toolCallMode: Schema.Literal("serial"),
|
|
});
|
|
export type SmokeModelRole = typeof SmokeModelRole.Type;
|
|
|
|
export const SmokeModelRoles = Schema.Struct({
|
|
plannerReviewer: SmokeModelRole,
|
|
worker: SmokeModelRole,
|
|
});
|
|
export type SmokeModelRoles = typeof SmokeModelRoles.Type;
|
|
|
|
export const SmokeContractReason = Schema.Literals([
|
|
"InvalidInput",
|
|
"InvalidModelRole",
|
|
"InvalidTransition",
|
|
"InvalidModelResponse",
|
|
"InvalidReport",
|
|
]);
|
|
export type SmokeContractReason = typeof SmokeContractReason.Type;
|
|
|
|
export class SmokeContractError extends Schema.TaggedErrorClass<SmokeContractError>()(
|
|
"SmokeContractError",
|
|
{
|
|
message: Schema.String,
|
|
reason: SmokeContractReason,
|
|
}
|
|
) {}
|
|
|
|
export const selectSmokeModelRoles = Effect.fn("Smoke.selectModelRoles")(
|
|
function* selectSmokeModelRoles(input: unknown) {
|
|
const selection: SmokeRoleSelectionInput =
|
|
yield* Schema.decodeUnknownEffect(SmokeRoleSelectionInput)(input).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new SmokeContractError({
|
|
message: cause.message,
|
|
reason: "InvalidInput",
|
|
})
|
|
)
|
|
);
|
|
|
|
if (selection.workerModel !== "minimax-m3") {
|
|
return yield* Effect.fail(
|
|
new SmokeContractError({
|
|
message: "The smoke worker must use minimax-m3",
|
|
reason: "InvalidModelRole",
|
|
})
|
|
);
|
|
}
|
|
|
|
if (selection.plannerReviewerModel !== "glm-5.2") {
|
|
return yield* Effect.fail(
|
|
new SmokeContractError({
|
|
message: "The smoke planner/reviewer must use glm-5.2",
|
|
reason: "InvalidModelRole",
|
|
})
|
|
);
|
|
}
|
|
|
|
return {
|
|
plannerReviewer: {
|
|
model: selection.plannerReviewerModel,
|
|
provider: selection.plannerReviewerProvider,
|
|
role: "planner-reviewer" as const,
|
|
toolCallMode: "serial" as const,
|
|
},
|
|
worker: {
|
|
model: selection.workerModel,
|
|
provider: selection.workerProvider,
|
|
role: "worker" as const,
|
|
toolCallMode: "serial" as const,
|
|
},
|
|
} satisfies SmokeModelRoles;
|
|
}
|
|
);
|
|
|
|
export const SmokePlan = Schema.Struct({
|
|
acceptanceCriteria: Schema.NonEmptyArray(MeaningfulString),
|
|
nonGoals: Schema.Array(MeaningfulString),
|
|
steps: Schema.NonEmptyArray(MeaningfulString),
|
|
summary: MeaningfulString,
|
|
});
|
|
export type SmokePlan = typeof SmokePlan.Type;
|
|
|
|
export const SmokeWorkerReport = Schema.Struct({
|
|
status: Schema.Literals(["completed", "blocked", "failed"]),
|
|
summary: MeaningfulString,
|
|
});
|
|
export type SmokeWorkerReport = typeof SmokeWorkerReport.Type;
|
|
|
|
export const SmokeReview = Schema.Struct({
|
|
approved: Schema.Boolean,
|
|
findings: Schema.Array(MeaningfulString),
|
|
summary: MeaningfulString,
|
|
});
|
|
export type SmokeReview = typeof SmokeReview.Type;
|
|
|
|
export const SmokeChatResponse = Schema.Struct({
|
|
choices: Schema.NonEmptyArray(
|
|
Schema.Struct({
|
|
message: Schema.Struct({ content: Schema.String }),
|
|
})
|
|
),
|
|
});
|
|
|
|
export const SmokeModelCatalog = Schema.Struct({
|
|
data: Schema.Array(Schema.Struct({ id: MeaningfulString })),
|
|
});
|
|
export type SmokeModelCatalog = typeof SmokeModelCatalog.Type;
|
|
|
|
export const decodeSmokeModelCatalog = Effect.fn("Smoke.decodeModelCatalog")(
|
|
function* decodeSmokeModelCatalog(input: unknown) {
|
|
return yield* Schema.decodeUnknownEffect(SmokeModelCatalog)(input).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new SmokeContractError({
|
|
message: cause.message,
|
|
reason: "InvalidModelResponse",
|
|
})
|
|
)
|
|
);
|
|
}
|
|
);
|
|
|
|
export const decodeSmokeChatText = Effect.fn("Smoke.decodeChatText")(
|
|
function* decodeSmokeChatText(input: unknown) {
|
|
const response = yield* Schema.decodeUnknownEffect(SmokeChatResponse)(
|
|
input
|
|
).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new SmokeContractError({
|
|
message: cause.message,
|
|
reason: "InvalidModelResponse",
|
|
})
|
|
)
|
|
);
|
|
const text = response.choices[0].message.content;
|
|
if (text.trim().length === 0) {
|
|
return yield* Effect.fail(
|
|
new SmokeContractError({
|
|
message: "The model returned an empty response",
|
|
reason: "InvalidModelResponse",
|
|
})
|
|
);
|
|
}
|
|
return text;
|
|
}
|
|
);
|
|
|
|
export const decodeSmokePlan = Effect.fn("Smoke.decodePlan")(
|
|
function* decodeSmokePlan(input: unknown) {
|
|
return yield* Schema.decodeUnknownEffect(SmokePlan)(input).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new SmokeContractError({
|
|
message: cause.message,
|
|
reason: "InvalidReport",
|
|
})
|
|
)
|
|
);
|
|
}
|
|
);
|
|
|
|
export const decodeSmokeWorkerReport = Effect.fn("Smoke.decodeWorkerReport")(
|
|
function* decodeSmokeWorkerReport(input: unknown) {
|
|
return yield* Schema.decodeUnknownEffect(SmokeWorkerReport)(input).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new SmokeContractError({
|
|
message: cause.message,
|
|
reason: "InvalidReport",
|
|
})
|
|
)
|
|
);
|
|
}
|
|
);
|
|
|
|
export const decodeSmokeReview = Effect.fn("Smoke.decodeReview")(
|
|
function* decodeSmokeReview(input: unknown) {
|
|
return yield* Schema.decodeUnknownEffect(SmokeReview)(input).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new SmokeContractError({
|
|
message: cause.message,
|
|
reason: "InvalidReport",
|
|
})
|
|
)
|
|
);
|
|
}
|
|
);
|
|
|
|
export const SmokeCapabilityCheck = Schema.Struct({
|
|
id: MeaningfulString,
|
|
message: MeaningfulString,
|
|
passed: Schema.Boolean,
|
|
});
|
|
export type SmokeCapabilityCheck = typeof SmokeCapabilityCheck.Type;
|
|
|
|
export const validateSmokeCapabilities = Effect.fn(
|
|
"Smoke.validateCapabilities"
|
|
)(function* validateSmokeCapabilities(input: unknown) {
|
|
const checks = yield* Schema.decodeUnknownEffect(
|
|
Schema.NonEmptyArray(SmokeCapabilityCheck)
|
|
)(input).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new SmokeContractError({
|
|
message: cause.message,
|
|
reason: "InvalidInput",
|
|
})
|
|
)
|
|
);
|
|
const failed = checks.filter((check) => !check.passed);
|
|
if (failed.length > 0) {
|
|
return yield* Effect.fail(
|
|
new SmokeContractError({
|
|
message: failed
|
|
.map((check) => `${check.id}: ${check.message}`)
|
|
.join("; "),
|
|
reason: "InvalidReport",
|
|
})
|
|
);
|
|
}
|
|
return checks;
|
|
});
|
|
|
|
export const SmokeLoopState = Schema.Struct({
|
|
history: Schema.Array(SmokePhase),
|
|
lastError: Schema.NullOr(Schema.String),
|
|
phase: SmokePhase,
|
|
});
|
|
export type SmokeLoopState = typeof SmokeLoopState.Type;
|
|
|
|
export const initialSmokeLoopState = (): SmokeLoopState => ({
|
|
history: ["preflight"],
|
|
lastError: null,
|
|
phase: "preflight",
|
|
});
|
|
|
|
const nextPhase: Readonly<
|
|
Record<SmokePhase, Partial<Record<SmokeTransition, SmokePhase>>>
|
|
> = {
|
|
completed: {},
|
|
failed: {},
|
|
planning: {
|
|
"plan-validated": "worker-running",
|
|
"runtime-failed": "failed",
|
|
},
|
|
preflight: {
|
|
"contract-failed": "failed",
|
|
"preflight-passed": "planning",
|
|
"runtime-failed": "failed",
|
|
},
|
|
reviewing: {
|
|
"contract-failed": "failed",
|
|
"review-approved": "completed",
|
|
"runtime-failed": "failed",
|
|
},
|
|
"worker-running": {
|
|
"contract-failed": "failed",
|
|
"runtime-failed": "failed",
|
|
"worker-succeeded": "reviewing",
|
|
},
|
|
};
|
|
|
|
export const advanceSmokeLoop = Effect.fn("Smoke.advanceLoop")(
|
|
function* advanceSmokeLoop(
|
|
state: SmokeLoopState,
|
|
transition: SmokeTransition,
|
|
error?: string
|
|
) {
|
|
const phase = nextPhase[state.phase][transition];
|
|
if (phase === undefined) {
|
|
return yield* Effect.fail(
|
|
new SmokeContractError({
|
|
message: `Cannot apply ${transition} while smoke loop is ${state.phase}`,
|
|
reason: "InvalidTransition",
|
|
})
|
|
);
|
|
}
|
|
return {
|
|
history: [...state.history, phase],
|
|
lastError: phase === "failed" ? (error ?? "Smoke loop failed") : null,
|
|
phase,
|
|
} satisfies SmokeLoopState;
|
|
}
|
|
);
|
|
|
|
export const redactSmokeText = (
|
|
text: string,
|
|
secrets: readonly string[]
|
|
): string => {
|
|
let redacted = text;
|
|
for (const secret of secrets) {
|
|
if (secret.length > 0) {
|
|
redacted = redacted.replaceAll(secret, "[REDACTED]");
|
|
}
|
|
}
|
|
return redacted;
|
|
};
|