feat: add refs tracking to RoleStep
- RoleOutput gains refs: string[] for CAS reference tracking - RoleDefinition gains extractRefs: ((meta) => string[]) | null - planner: phases.map(p => p.hash), coder: [completedPhase] - Engine persists refs, fork preserves refs - Backward compat: missing refs normalized to [] - 137 tests passing Fixes #31
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
END,
|
||||
type ExtractContext,
|
||||
type ModeratorContext,
|
||||
type RoleDefinition,
|
||||
type RoleMeta,
|
||||
type RoleOutput,
|
||||
type RoleStep,
|
||||
@@ -22,6 +23,17 @@ function isRoleNext<M extends RoleMeta>(
|
||||
return next !== END;
|
||||
}
|
||||
|
||||
function resolveExtractedRefs(
|
||||
roleDef: RoleDefinition<Record<string, unknown>>,
|
||||
meta: unknown,
|
||||
): string[] {
|
||||
const extractRefsFn = roleDef.extractRefs;
|
||||
if (extractRefsFn === null || typeof extractRefsFn !== "function") {
|
||||
return [];
|
||||
}
|
||||
return extractRefsFn(meta as Record<string, unknown>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds pure role definitions + moderator to runtime agents and structured extraction.
|
||||
* Assign with `export const run = createWorkflow(def, binding, extract)`.
|
||||
@@ -48,6 +60,7 @@ export function createWorkflow<M extends RoleMeta>(
|
||||
role: out.role,
|
||||
content: out.content,
|
||||
meta: out.meta,
|
||||
refs: out.refs,
|
||||
timestamp: baseTs + i,
|
||||
})) as RoleStep<M>[];
|
||||
|
||||
@@ -96,15 +109,21 @@ export function createWorkflow<M extends RoleMeta>(
|
||||
extractCtx as unknown as ExtractContext,
|
||||
);
|
||||
|
||||
const refs = resolveExtractedRefs(
|
||||
roleDef as unknown as RoleDefinition<Record<string, unknown>>,
|
||||
meta,
|
||||
);
|
||||
|
||||
const ts = Date.now();
|
||||
const step = {
|
||||
role: next,
|
||||
content: raw,
|
||||
meta,
|
||||
refs,
|
||||
timestamp: ts,
|
||||
} as RoleStep<M>;
|
||||
|
||||
yield { role: step.role, content: step.content, meta: step.meta };
|
||||
yield { role: step.role, content: step.content, meta: step.meta, refs: step.refs };
|
||||
|
||||
steps = [...steps, step];
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { appendFile, mkdir } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
import type { LogFn } from "./logger.js";
|
||||
import { normalizeRefsField } from "./refs-field.js";
|
||||
import type { ThreadInput, WorkflowFn, WorkflowFnOptions, WorkflowResult } from "./types.js";
|
||||
|
||||
export type ExecuteThreadIo = {
|
||||
@@ -16,6 +17,7 @@ export type PrefilledDiskStep = {
|
||||
role: string;
|
||||
content: string;
|
||||
meta: Record<string, unknown>;
|
||||
refs: string[];
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
@@ -79,6 +81,7 @@ async function driveWorkflowGenerator(params: {
|
||||
role: step.role,
|
||||
content: step.content,
|
||||
meta: step.meta,
|
||||
refs: normalizeRefsField(step.refs),
|
||||
timestamp: ts,
|
||||
});
|
||||
|
||||
@@ -151,6 +154,7 @@ export async function executeThread(
|
||||
role: row.role,
|
||||
content: row.content,
|
||||
meta: row.meta,
|
||||
refs: normalizeRefsField(row.refs),
|
||||
timestamp: row.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeRefsField } from "./refs-field.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import type { RoleOutput } from "./types.js";
|
||||
|
||||
@@ -36,6 +37,7 @@ function parseRoleLine(
|
||||
role,
|
||||
content,
|
||||
meta: meta as Record<string, unknown>,
|
||||
refs: normalizeRefsField(obj.refs),
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/** Normalize `refs` from persisted JSONL or IPC payloads (missing or invalid → []). */
|
||||
export function normalizeRefsField(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (const x of value) {
|
||||
if (typeof x === "string") {
|
||||
out.push(x);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -19,6 +19,8 @@ export type RoleOutput = {
|
||||
role: string;
|
||||
content: string;
|
||||
meta: Record<string, unknown>;
|
||||
/** CAS hashes produced or consumed by this step (for GC traceability). */
|
||||
refs: string[];
|
||||
};
|
||||
|
||||
/** What the workflow AsyncGenerator returns when done. */
|
||||
@@ -55,7 +57,13 @@ export type StartStep = {
|
||||
|
||||
/** A completed role step in the thread. */
|
||||
export type RoleStep<M extends RoleMeta> = {
|
||||
[K in keyof M & string]: { role: K; meta: M[K]; content: string; timestamp: number };
|
||||
[K in keyof M & string]: {
|
||||
role: K;
|
||||
meta: M[K];
|
||||
content: string;
|
||||
refs: string[];
|
||||
timestamp: number;
|
||||
};
|
||||
}[keyof M & string];
|
||||
|
||||
/** Phase 1: Moderator decides next role. */
|
||||
@@ -96,6 +104,8 @@ export type RoleDefinition<Meta extends Record<string, unknown>> = {
|
||||
systemPrompt: string;
|
||||
extractPrompt: string;
|
||||
schema: z.ZodType<Meta>;
|
||||
/** When non-null, produces CAS hashes to persist on this role's steps (see `RoleOutput.refs`). */
|
||||
extractRefs: ((meta: Meta) => string[]) | null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
||||
import type { PrefilledDiskStep } from "./engine.js";
|
||||
import { type ExecuteThreadIo, executeThread } from "./engine.js";
|
||||
import { createLogger } from "./logger.js";
|
||||
import { normalizeRefsField } from "./refs-field.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { createThreadPauseGate, type ThreadPauseGate } from "./thread-pause-gate.js";
|
||||
import type { RoleOutput, WorkflowFn } from "./types.js";
|
||||
@@ -55,7 +56,12 @@ function parseRoleOutputRecord(obj: Record<string, unknown>): RoleOutput | null
|
||||
if (meta === null || typeof meta !== "object") {
|
||||
return null;
|
||||
}
|
||||
return { role, content, meta: meta as Record<string, unknown> };
|
||||
return {
|
||||
role,
|
||||
content,
|
||||
meta: meta as Record<string, unknown>,
|
||||
refs: normalizeRefsField(obj.refs),
|
||||
};
|
||||
}
|
||||
|
||||
function parseRunStepsPayload(rec: Record<string, unknown>): {
|
||||
@@ -382,6 +388,7 @@ async function main(): Promise<void> {
|
||||
role: step.role,
|
||||
content: step.content,
|
||||
meta: step.meta,
|
||||
refs: normalizeRefsField(step.refs),
|
||||
timestamp: typeof ts === "number" && ts > 0 ? ts : baseTs + i,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user