Files
zopu-code/packages/primitives/src/smoke.ts
T

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;
};