feat: agent session protocol — sessionId in result, continue support, frontmatter retry

Breaking changes:
- AgentRunResult now requires sessionId field
- AgentOptions now requires continue function
- Agent CLI outputs JSON {stepHash, sessionId} instead of plain CAS hash
- Engine parses JSON output (with legacy CAS hash fallback)

New features:
- Frontmatter validation retry: if agent output lacks valid frontmatter,
  engine calls agent.continue() up to 2 times with correction message
- Session tracking: sessionId flows from agent → engine → StepOutput
- Hermes agent: session parse failure is now a hard error (no raw text fallback)
- Hermes agent: supports --resume for continue sessions

Closes #384
This commit is contained in:
2026-05-22 09:13:05 +00:00
parent e62d51d845
commit 7ff90cef4f
6 changed files with 149 additions and 48 deletions
+25 -5
View File
@@ -624,7 +624,12 @@ function resolveAgentConfig(
return agentConfig;
}
function spawnAgent(agent: AgentConfig, threadId: ThreadId, role: string): CasRef {
type SpawnAgentResult = {
stepHash: CasRef;
sessionId: string;
};
function spawnAgent(agent: AgentConfig, threadId: ThreadId, role: string): SpawnAgentResult {
const argv = [...agent.args, threadId, role];
let stdout: string;
try {
@@ -646,10 +651,24 @@ function spawnAgent(agent: AgentConfig, threadId: ThreadId, role: string): CasRe
}
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
if (!isCasRef(line)) {
fail(`agent stdout is not a valid CAS hash: ${line || "(empty)"}`);
// Try JSON output first (new protocol)
try {
const parsed = JSON.parse(line) as Record<string, unknown>;
const stepHash = parsed.stepHash;
const sessionId = parsed.sessionId;
if (typeof stepHash === "string" && isCasRef(stepHash) && typeof sessionId === "string") {
return { stepHash, sessionId };
}
} catch {
// Not JSON — fall through to legacy CAS hash parsing
}
return line;
// Legacy: plain CAS hash on stdout
if (!isCasRef(line)) {
fail(`agent stdout is not a valid CAS hash or JSON: ${line || "(empty)"}`);
}
return { stepHash: line, sessionId: "" };
}
async function archiveThread(
@@ -706,7 +725,7 @@ export async function cmdThreadStep(
const agent = resolveAgentConfig(config, workflow, role, agentOverride);
loadDotenv({ path: getEnvPath(storageRoot) });
const newHead = spawnAgent(agent, threadId, role);
const { stepHash: newHead, sessionId } = spawnAgent(agent, threadId, role);
// Re-create store to pick up nodes written by the agent subprocess
const uwfAfter = await createUwfStore(storageRoot);
@@ -737,6 +756,7 @@ export async function cmdThreadStep(
thread: threadId,
head: newHead,
done,
sessionId,
};
}