From c42125946d9e17213304026721d2cb8d628e4eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 19 May 2026 03:19:40 +0000 Subject: [PATCH] feat(thread-read): expand detail recursively via cas_ref --detail now uses expandDeep to recursively resolve all cas_ref fields in the detail merkle tree, showing full turn content instead of raw hashes. Refs #349 --- packages/cli-uwf/src/commands/thread.ts | 64 ++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/cli-uwf/src/commands/thread.ts b/packages/cli-uwf/src/commands/thread.ts index 69b7ff6..f15ead4 100644 --- a/packages/cli-uwf/src/commands/thread.ts +++ b/packages/cli-uwf/src/commands/thread.ts @@ -1,6 +1,7 @@ import { execFileSync } from "node:child_process"; -import { validate } from "@uncaged/json-cas"; +import { getSchema, validate } from "@uncaged/json-cas"; +import type { JSONSchema, Store as CasStore } from "@uncaged/json-cas"; import { stringify } from "yaml"; import { getEnvPath, loadWorkflowConfig } from "@uncaged/uwf-agent-kit"; import { evaluate } from "@uncaged/uwf-moderator"; @@ -274,6 +275,64 @@ function expandOutput(uwf: UwfStore, outputRef: CasRef): unknown { return node.payload; } +/** + * Recursively expand all cas_ref fields in a CAS node's payload, + * replacing hash strings with the referenced node's expanded payload. + */ +function expandDeep(store: CasStore, hash: CasRef, visited?: Set): unknown { + const seen = visited ?? new Set(); + if (seen.has(hash)) return hash; // cycle guard + seen.add(hash); + + const node = store.get(hash); + if (node === null) return hash; + + const schema = getSchema(store, node.type); + if (schema === null) return node.payload; + + return expandValue(store, schema, node.payload, seen); +} + +function expandValue(store: CasStore, schema: JSONSchema, value: unknown, visited: Set): unknown { + // If this field is a cas_ref, expand it + if (schema.format === "cas_ref") { + if (typeof value === "string") { + return expandDeep(store, value as CasRef, visited); + } + return value; + } + + // anyOf (nullable refs) + if (Array.isArray(schema.anyOf)) { + for (const sub of schema.anyOf as JSONSchema[]) { + if (sub.format === "cas_ref" && typeof value === "string") { + return expandDeep(store, value as CasRef, visited); + } + } + return value; + } + + // Array of cas_ref items + if (schema.type === "array" && schema.items && Array.isArray(value)) { + const itemSchema = schema.items as JSONSchema; + return (value as unknown[]).map((item) => expandValue(store, itemSchema, item, visited)); + } + + // Object with properties + if (value !== null && typeof value === "object" && !Array.isArray(value) && schema.properties) { + const props = schema.properties as Record; + const obj = value as Record; + const result: Record = {}; + for (const [key, val] of Object.entries(obj)) { + const propSchema = props[key]; + result[key] = propSchema ? expandValue(store, propSchema, val, visited) : val; + } + return result; + } + + return value; +} + function collectOrderedSteps( uwf: UwfStore, headHash: CasRef, @@ -406,7 +465,8 @@ function formatThreadReadMarkdown(options: { "```", ]; if (showDetail && item.payload.detail) { - const detailYaml = formatYaml(expandOutput(uwf, item.payload.detail)); + const detailExpanded = expandDeep(uwf.store, item.payload.detail); + const detailYaml = formatYaml(detailExpanded); stepLines.push("", "### Detail", "", "```yaml", detailYaml, "```"); } parts.push(stepLines.join("\n"));