From ca0403c8ab5a118548daa1c9771b4359106f5d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Sat, 9 May 2026 08:53:02 +0000 Subject: [PATCH] fix: content node refs field + thread head update Fixes #161 Fixes #162 Co-authored-by: Cursor --- packages/workflow-cas/src/merkle.ts | 56 ++++++++++++++----- packages/workflow-cas/src/reachable.ts | 5 +- .../src/extract/extract-fn.ts | 4 +- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/packages/workflow-cas/src/merkle.ts b/packages/workflow-cas/src/merkle.ts index 6dba6fd..2a78528 100644 --- a/packages/workflow-cas/src/merkle.ts +++ b/packages/workflow-cas/src/merkle.ts @@ -1,8 +1,41 @@ import { parse, stringify } from "yaml"; -import type { CasStore, MerkleNode, StepMerklePayload, ThreadMerklePayload } from "./types.js"; +import type { + CasStore, + MerkleNode, + MerkleNodeType, + StepMerklePayload, + ThreadMerklePayload, +} from "./types.js"; + +function requireStringHashArray(value: unknown, notArrayMessage: string): string[] { + if (!Array.isArray(value)) { + throw new Error(notArrayMessage); + } + const out: string[] = []; + for (const c of value) { + if (typeof c !== "string") { + throw new Error("merkle: hash entry must be a string"); + } + out.push(c); + } + return out; +} + +function edgeListRaw(rec: Record, type: MerkleNodeType): unknown { + if (type === "content") { + return rec.refs !== undefined ? rec.refs : rec.children; + } + return rec.children; +} export function serializeMerkleNode(node: MerkleNode): string { + if (node.type === "content") { + return stringify( + { type: node.type, payload: node.payload, refs: node.children }, + { indent: 2 }, + ); + } return stringify( { type: node.type, payload: node.payload, children: node.children }, { indent: 2 }, @@ -17,23 +50,18 @@ export function parseMerkleNode(yamlText: string): MerkleNode { const rec = raw as Record; const type = rec.type; const payload = rec.payload; - const children = rec.children; if (type !== "content" && type !== "step" && type !== "thread") { throw new Error("merkle: invalid or missing type"); } if (typeof payload !== "string" && (payload === null || typeof payload !== "object")) { throw new Error("merkle: payload must be a string or object"); } - if (!Array.isArray(children)) { - throw new Error("merkle: children must be an array"); - } - const childHashes: string[] = []; - for (const c of children) { - if (typeof c !== "string") { - throw new Error("merkle: child hash must be a string"); - } - childHashes.push(c); - } + + const notArrayMsg = + type === "content" + ? "merkle: content node requires refs or children array" + : "merkle: children must be an array"; + const childHashes = requireStringHashArray(edgeListRaw(rec, type), notArrayMsg); return { type, payload: typeof payload === "string" ? payload : (payload as Record), @@ -85,8 +113,8 @@ export async function putContentMerkleNode(store: CasStore, content: string): Pr /** * Loads a CAS blob and returns the payload string for a `content` node. * - * Accepts both the legacy `{type:content, payload, children}` Merkle layout - * and the RFC v3 `{type:content, payload, refs}` content node layout. + * Accepts both the legacy `{ type:content, payload, children }` Merkle layout + * and the RFC-aligned `{ type:content, payload, refs }` content node layout. */ export async function getContentMerklePayload( store: CasStore, diff --git a/packages/workflow-cas/src/reachable.ts b/packages/workflow-cas/src/reachable.ts index 00d2403..4c18ebb 100644 --- a/packages/workflow-cas/src/reachable.ts +++ b/packages/workflow-cas/src/reachable.ts @@ -9,7 +9,10 @@ function refsFromBlob(content: string): string[] { return []; } const rec = raw as Record; - const refs = rec.refs; + let refs = rec.refs; + if (!Array.isArray(refs) && Array.isArray(rec.children)) { + refs = rec.children; + } if (!Array.isArray(refs)) { return []; } diff --git a/packages/workflow-execute/src/extract/extract-fn.ts b/packages/workflow-execute/src/extract/extract-fn.ts index c121ed8..607b71d 100644 --- a/packages/workflow-execute/src/extract/extract-fn.ts +++ b/packages/workflow-execute/src/extract/extract-fn.ts @@ -20,7 +20,7 @@ const CAS_GET_TOOL_DEFINITION = { function: { name: "cas_get", description: - "Read a Merkle DAG node from content-addressed storage by its hash. Returns YAML-formatted node with type, payload, and children fields.", + "Read a Merkle DAG node from content-addressed storage by its hash. Returns YAML-formatted node with type, payload, and refs or children fields (content nodes use refs).", parameters: { type: "object", properties: { @@ -102,7 +102,7 @@ export function createExtract(provider: LlmProvider, deps: ExtractDeps): Extract }; }, systemPromptForStructuredTool: (structuredToolName) => - `You extract structured metadata from the agent output below. Use cas_get to read Merkle DAG nodes from CAS (YAML: type, payload, children) when the agent output references hashes you must traverse. When you have the complete structured object, call the ${structuredToolName} tool with JSON arguments matching the schema. You may instead reply with only a JSON object (no prose) when no tools are needed.`, + `You extract structured metadata from the agent output below. Use cas_get to read Merkle DAG nodes from CAS (YAML: type, payload, refs for content nodes or children for step/thread legacy nodes) when the agent output references hashes you must traverse. When you have the complete structured object, call the ${structuredToolName} tool with JSON arguments matching the schema. You may instead reply with only a JSON object (no prose) when no tools are needed.`, toolHandler: async (call, thread) => { if (call.function.name !== "cas_get") { return `Unexpected tool routed to handler: ${call.function.name}`; -- 2.43.0