wip: Phase 1 protocol + CAS types for Merkle call stack

小橘 <xiaoju@shazhou.work>
This commit is contained in:
2026-05-12 01:35:45 +00:00
parent 82d9abf260
commit e37dbc3f35
3 changed files with 46 additions and 3 deletions
@@ -9,5 +9,8 @@ export function collectRefs(payload: StateNode["payload"]): string[] {
if (payload.compact !== null) { if (payload.compact !== null) {
out.push(payload.compact); out.push(payload.compact);
} }
if (payload.childThread !== null) {
out.push(payload.childThread);
}
return out; return out;
} }
+39 -3
View File
@@ -18,6 +18,10 @@ function isStartPayload(value: unknown): value is StartNodePayload {
if (!isRecord(value)) { if (!isRecord(value)) {
return false; return false;
} }
const parentState = value.parentState;
if (parentState !== undefined && parentState !== null && typeof parentState !== "string") {
return false;
}
return ( return (
typeof value.name === "string" && typeof value.name === "string" &&
typeof value.hash === "string" && typeof value.hash === "string" &&
@@ -25,6 +29,16 @@ function isStartPayload(value: unknown): value is StartNodePayload {
); );
} }
/** Normalizes a raw start payload, defaulting `parentState` to `null` for legacy nodes. */
function normalizeStartPayload(raw: StartNodePayload): StartNodePayload {
return {
name: raw.name,
hash: raw.hash,
depth: raw.depth,
parentState: raw.parentState ?? null,
};
}
function isStatePayload(value: unknown): value is StateNodePayload { function isStatePayload(value: unknown): value is StateNodePayload {
if (!isRecord(value)) { if (!isRecord(value)) {
return false; return false;
@@ -41,6 +55,10 @@ function isStatePayload(value: unknown): value is StateNodePayload {
if (!isRecord(meta)) { if (!isRecord(meta)) {
return false; return false;
} }
const childThread = value.childThread;
if (childThread !== undefined && childThread !== null && typeof childThread !== "string") {
return false;
}
return ( return (
typeof value.role === "string" && typeof value.role === "string" &&
typeof value.start === "string" && typeof value.start === "string" &&
@@ -49,6 +67,20 @@ function isStatePayload(value: unknown): value is StateNodePayload {
); );
} }
/** Normalizes a raw state payload, defaulting `childThread` to `null` for legacy nodes. */
function normalizeStatePayload(raw: StateNodePayload): StateNodePayload {
return {
role: raw.role,
meta: raw.meta,
start: raw.start,
content: raw.content,
ancestors: raw.ancestors,
compact: raw.compact,
timestamp: raw.timestamp,
childThread: raw.childThread ?? null,
};
}
/** Parses a YAML CAS blob into a typed RFC v3 thread node (or legacy content layout with `children`). */ /** Parses a YAML CAS blob into a typed RFC v3 thread node (or legacy content layout with `children`). */
export function parseCasThreadNode(yamlText: string): ParsedCasThreadNode | null { export function parseCasThreadNode(yamlText: string): ParsedCasThreadNode | null {
let raw: unknown; let raw: unknown;
@@ -86,14 +118,14 @@ export function parseCasThreadNode(yamlText: string): ParsedCasThreadNode | null
if (!isStartPayload(raw.payload)) { if (!isStartPayload(raw.payload)) {
return null; return null;
} }
const node: StartNode = { type: "start", payload: raw.payload, refs: [...refs] }; const node: StartNode = { type: "start", payload: normalizeStartPayload(raw.payload), refs: [...refs] };
return { kind: "start", node }; return { kind: "start", node };
} }
if (!isStatePayload(raw.payload)) { if (!isStatePayload(raw.payload)) {
return null; return null;
} }
const node: StateNode = { type: "state", payload: raw.payload, refs: [...refs] }; const node: StateNode = { type: "state", payload: normalizeStatePayload(raw.payload), refs: [...refs] };
return { kind: "state", node }; return { kind: "state", node };
} }
@@ -143,10 +175,14 @@ export async function putStartNode(
payload: StartNode["payload"], payload: StartNode["payload"],
promptHash: string, promptHash: string,
): Promise<string> { ): Promise<string> {
const refs = [promptHash];
if (payload.parentState !== null) {
refs.push(payload.parentState);
}
const node: StartNode = { const node: StartNode = {
type: "start", type: "start",
payload, payload,
refs: [promptHash], refs,
}; };
return store.put(serializeCasNode(node)); return store.put(serializeCasNode(node));
} }
@@ -4,6 +4,8 @@ export type StartNodePayload = {
name: string; name: string;
hash: string; hash: string;
depth: number; depth: number;
/** Parent thread's head state hash at spawn time. `null` for top-level workflows. */
parentState: string | null;
}; };
export type StartNode = { export type StartNode = {
@@ -20,6 +22,8 @@ export type StateNodePayload = {
ancestors: string[]; ancestors: string[];
compact: string | null; compact: string | null;
timestamp: number; timestamp: number;
/** Child thread's final state hash (workflow-as-agent). `null` when no child spawned. */
childThread: string | null;
}; };
export type StateNode = { export type StateNode = {