feat: add channel event log
This commit is contained in:
parent
c7cd28c404
commit
ff5b3dc937
|
|
@ -9,3 +9,4 @@ TEMP/
|
|||
.DS_Store
|
||||
*.tsbuildinfo
|
||||
.fable/
|
||||
.runs/
|
||||
|
|
|
|||
|
|
@ -204,6 +204,8 @@ fable-agent plinius godmode "improve explanation quality"
|
|||
- `-d, --domain <domain>`
|
||||
- `fable5 models`
|
||||
- `fable5 flue`
|
||||
- `fable5 channels`
|
||||
- `--run <id>`
|
||||
- `fable5 verify <task>`
|
||||
- `--repo <path>`
|
||||
- `fable5 goal <text>`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { channelPath, emitChannelEvent, readChannelEvents } from "./channel.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function tmpRoot(): string {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "fable-channel-"));
|
||||
roots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
describe("channel event log", () => {
|
||||
it("creates a JSONL channel file", () => {
|
||||
const root = tmpRoot();
|
||||
const event = emitChannelEvent("demo/run", { source: "workflow", type: "started", data: { step: "x" } }, root, new Date("2026-06-16T00:00:00.000Z"));
|
||||
|
||||
expect(event.runId).toBe("demo/run");
|
||||
expect(event.ts).toBe("2026-06-16T00:00:00.000Z");
|
||||
expect(fs.existsSync(channelPath("demo/run", root))).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves multiple events in order", () => {
|
||||
const root = tmpRoot();
|
||||
emitChannelEvent("run-1", { source: "intake", type: "started", data: { n: 1 } }, root);
|
||||
emitChannelEvent("run-1", { source: "gate", type: "done", data: { n: 2 } }, root);
|
||||
|
||||
const events = readChannelEvents("run-1", root);
|
||||
expect(events.map((event) => event.source)).toEqual(["intake", "gate"]);
|
||||
expect(events.map((event) => event.type)).toEqual(["started", "done"]);
|
||||
});
|
||||
|
||||
it("returns an empty list for missing channels", () => {
|
||||
expect(readChannelEvents("missing", tmpRoot())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
export type ChannelEventSource = "agent" | "tool" | "workflow" | "gate" | "intake";
|
||||
export type ChannelEventType = "started" | "output" | "receipt" | "error" | "done";
|
||||
|
||||
export interface ChannelEvent {
|
||||
runId: string;
|
||||
ts: string;
|
||||
source: ChannelEventSource;
|
||||
type: ChannelEventType;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export type NewChannelEvent = Omit<ChannelEvent, "runId" | "ts">;
|
||||
|
||||
export function channelPath(runId: string, root = ".runs"): string {
|
||||
return path.join(root, safeRunId(runId), "channel.jsonl");
|
||||
}
|
||||
|
||||
export function emitChannelEvent(runId: string, event: NewChannelEvent, root = ".runs", now = new Date()): ChannelEvent {
|
||||
const full: ChannelEvent = { runId, ts: now.toISOString(), ...event };
|
||||
const file = channelPath(runId, root);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.appendFileSync(file, `${JSON.stringify(full)}\n`);
|
||||
return full;
|
||||
}
|
||||
|
||||
export function readChannelEvents(runId: string, root = ".runs"): ChannelEvent[] {
|
||||
const file = channelPath(runId, root);
|
||||
if (!fs.existsSync(file)) return [];
|
||||
return fs.readFileSync(file, "utf-8").trim().split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line) as ChannelEvent);
|
||||
}
|
||||
|
||||
function safeRunId(runId: string): string {
|
||||
return runId.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-|-$/g, "") || "run";
|
||||
}
|
||||
|
|
@ -50,5 +50,8 @@ export type { CapabilityProbeResult, CapabilityReport, FactoryCapabilityService
|
|||
export { FLUE_INTEROP_ROWS, formatFlueInteropReport, summarizeFlueInterop } from "./flue-interop.js";
|
||||
export type { FlueInteropRow, FlueInteropStatus } from "./flue-interop.js";
|
||||
|
||||
export { channelPath, emitChannelEvent, readChannelEvents } from "./channel.js";
|
||||
export type { ChannelEvent, ChannelEventSource, ChannelEventType, NewChannelEvent } from "./channel.js";
|
||||
|
||||
export { createForgejoIntakeReceipt, intakeForgejoItem, parseForgejoRef, routeForgejoLabels } from "./forgejo-intake.js";
|
||||
export type { ForgejoIntakeKind, ForgejoIntakeOptions, ForgejoIntakeReceipt, ForgejoIntakeSource, ForgejoItem, ForgejoRoute } from "./forgejo-intake.js";
|
||||
|
|
|
|||
14
src/index.ts
14
src/index.ts
|
|
@ -1587,6 +1587,20 @@ fable
|
|||
console.log(formatFlueInteropReport());
|
||||
});
|
||||
|
||||
fable
|
||||
.command("channels")
|
||||
.description("Show Flue-style channel event log location")
|
||||
.option("--run <id>", "Run id", "demo")
|
||||
.action(async (opts: { run?: string }) => {
|
||||
const { channelPath, readChannelEvents } = await import("./fable5/channel.js");
|
||||
const runId = opts.run ?? "demo";
|
||||
const events = readChannelEvents(runId);
|
||||
console.log(`
|
||||
Channel: ${channelPath(runId)}`);
|
||||
console.log(` Events: ${events.length}`);
|
||||
console.log(``);
|
||||
});
|
||||
|
||||
fable
|
||||
.command("verify <task>")
|
||||
.description("Run independent verifier against a task")
|
||||
|
|
|
|||
Loading…
Reference in New Issue