feat: record suspend event as StepNode in CAS chain

- ThreadIndexEntry supports suspendedRole + suspendMessage metadata
- threads.yaml: suspended threads serialize as objects (backward compat)
- cmdThreadStepOnce writes step before marking thread suspended
- StepOutput extended with suspendedRole/suspendMessage fields
- thread show displays suspend message

Closes #589
This commit is contained in:
2026-06-02 04:44:05 +00:00
parent b0ef9c55a9
commit 10b478640d
17 changed files with 560 additions and 81 deletions
+2 -2
View File
@@ -163,7 +163,7 @@ export async function buildContext(
const { store, schemas } = agentStore;
const index = await loadThreadsIndex(storageRoot);
const headHash = index[threadId];
const headHash = index[threadId]?.head;
if (headHash === undefined) {
fail(`thread not found in threads.yaml: ${threadId}`);
}
@@ -212,7 +212,7 @@ export async function buildContextWithMeta(
const { store, schemas } = agentStore;
const index = await loadThreadsIndex(storageRoot);
const headHash = index[threadId];
const headHash = index[threadId]?.head;
if (headHash === undefined) {
fail(`thread not found in threads.yaml: ${threadId}`);
}
+2 -11
View File
@@ -12,11 +12,11 @@ import type {
ProviderAlias,
ProviderConfig,
Scenario,
ThreadId,
ThreadsIndex,
WorkflowConfig,
WorkflowName,
} from "@uncaged/workflow-protocol";
import { parseThreadsIndex } from "@uncaged/workflow-protocol";
import { parse } from "yaml";
import { registerAgentSchemas } from "./schemas.js";
@@ -207,16 +207,7 @@ export async function loadThreadsIndex(storageRoot: string): Promise<ThreadsInde
try {
const text = await readFile(path, "utf8");
const raw = parse(text) as unknown;
if (!isRecord(raw)) {
return {};
}
const index: ThreadsIndex = {};
for (const [threadId, head] of Object.entries(raw)) {
if (typeof head === "string") {
index[threadId as ThreadId] = head;
}
}
return index;
return parseThreadsIndex(raw);
} catch (e) {
const err = e as NodeJS.ErrnoException;
if (err.code === "ENOENT") {