@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
119 lines (108 loc) • 3.53 kB
text/typescript
import { nowIso } from "./protocol.ts";
export const WORKER_SCHEMA = "kxm.worker.v1" as const;
export const WORKER_RESULT_SCHEMA = "kxm.worker-result.v1" as const;
import { parseRoutingRecord } from "./routing.ts";
export type WorkerKind = "agent" | "gate";
export type WorkerDriver = "ai" | "code";
export type WorkerOutcome = "passed" | "warning" | "failed" | "running";
/** Shared worker base. Agents and gates are both workers; they differ only by driver. */
export interface WorkerBase {
schema: typeof WORKER_SCHEMA;
kind: WorkerKind;
driver: WorkerDriver;
name: string;
project?: string;
purpose?: string;
}
export interface AgentWorker extends WorkerBase {
kind: "agent";
driver: "ai";
model?: string;
thinking?: string;
}
export interface GateWorker extends WorkerBase {
kind: "gate";
driver: "code";
}
export type Worker = AgentWorker | GateWorker;
/** Standard result envelope emitted by every worker, AI or code. */
export interface WorkerResultEnvelope<T extends Worker = Worker> {
schema: typeof WORKER_RESULT_SCHEMA;
worker: T;
command: string;
ok: boolean;
outcome: WorkerOutcome;
createdAt: string;
summary: string;
}
export function agentWorker(input: {
name: string;
project?: string;
purpose?: string;
model?: string;
thinking?: string;
}): AgentWorker {
return {
schema: WORKER_SCHEMA,
kind: "agent",
driver: "ai",
name: input.name,
...(input.project ? { project: input.project } : {}),
...(input.purpose ? { purpose: input.purpose } : {}),
...(input.model ? { model: input.model } : {}),
...(input.thinking ? { thinking: input.thinking } : {}),
};
}
export function gateWorker(input: {
name: string;
project?: string;
purpose?: string;
}): GateWorker {
return {
schema: WORKER_SCHEMA,
kind: "gate",
driver: "code",
name: input.name,
...(input.project ? { project: input.project } : {}),
...(input.purpose ? { purpose: input.purpose } : {}),
};
}
/**
* Build the standard result envelope. Additive payload keys extend the
* envelope, but hub-owned identity keys (`schema` and `worker`) are written
* after the payload spread and cannot be clobbered. Caller-owned `summary`,
* optional `createdAt`, and optional `outcome` are canonical inputs. An explicit `outcome`
* must not contradict `ok` ("passed" requires ok: true; "failed" requires
* ok: false); contradictions throw instead of emitting a lying envelope.
*/
export function workerResult<T extends Worker, P extends Record<string, unknown>>(
worker: T,
payload: P & {
command: string;
ok: boolean;
summary: string;
outcome?: WorkerOutcome;
createdAt?: string;
},
): WorkerResultEnvelope<T> & P {
const { summary, outcome, createdAt, ...rest } = payload;
if (outcome === "passed" && payload.ok === false) {
throw new Error("workerResult outcome contradicts ok: \"passed\" requires ok: true");
}
if (outcome === "failed" && payload.ok === true) {
throw new Error("workerResult outcome contradicts ok: \"failed\" requires ok: false");
}
// Routing records are validated, not trusted: a malformed or body-bearing
// routing payload fails closed instead of polluting telemetry.
if (rest.routing !== undefined) {
parseRoutingRecord(rest.routing);
}
return {
...rest,
schema: WORKER_RESULT_SCHEMA,
worker,
createdAt: createdAt ?? nowIso(),
outcome: outcome ?? (payload.ok ? "passed" : "failed"),
summary,
} as WorkerResultEnvelope<T> & P;
}