feat(builtin-agent): persist ReAct loop turns as session JSONL
Each turn (assistant response / tool result) is appended to a JSONL file at ~/.uncaged/workflow/sessions/<sessionId>.jsonl during the loop. On completion, the JSONL is read back, each turn is stored as a CAS node, and the detail payload references them as a flat turns[] array in chronological order. The session file is then deleted. Benefits: - Real-time observability: tail -f the JSONL to watch loop progress - Crash recovery: partial JSONL survives process death - Zero write contention: one file per session - Detail stays a flat array for easy consumption by CLI/dashboard Changes: - New session.ts: initSessionDir, appendSessionTurn, readSessionTurns, removeSession - loop.ts: append JSONL each turn instead of accumulating in-memory - detail.ts: reads session JSONL → persists turns to CAS → stores detail - agent.ts: passes storageRoot/sessionId to loop, cleans up session on completion - types.ts: remove index from TurnPayload (order is implicit in JSONL/array) - schemas.ts: sync with type changes Ref: #433
This commit is contained in:
@@ -7,17 +7,26 @@ import {
|
||||
resolveModel,
|
||||
resolveStorageRoot,
|
||||
} from "@uncaged/workflow-agent-kit";
|
||||
import { generateUlid } from "@uncaged/workflow-util";
|
||||
import { createLogger, generateUlid } from "@uncaged/workflow-util";
|
||||
|
||||
import { storeBuiltinDetail } from "./detail.js";
|
||||
import type { ChatMessage } from "./llm/index.js";
|
||||
import { BUILTIN_CONTINUE_MAX_TURNS, BUILTIN_MAX_TURNS, runBuiltinLoop } from "./loop.js";
|
||||
import { buildBuiltinMessages } from "./prompt.js";
|
||||
import type { BuiltinSessionState } from "./types.js";
|
||||
import { initSessionDir, removeSession } from "./session.js";
|
||||
|
||||
const sessions = new Map<string, BuiltinSessionState>();
|
||||
const log = createLogger({ sink: { kind: "stderr" } });
|
||||
|
||||
function getSession(sessionId: string): BuiltinSessionState {
|
||||
type SessionRecord = {
|
||||
sessionId: string;
|
||||
model: string;
|
||||
startedAtMs: number;
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
|
||||
const sessions = new Map<string, SessionRecord>();
|
||||
|
||||
function getSession(sessionId: string): SessionRecord {
|
||||
const session = sessions.get(sessionId);
|
||||
if (session === undefined) {
|
||||
throw new Error(`builtin session not found: ${sessionId}`);
|
||||
@@ -36,7 +45,7 @@ async function runBuiltinWithMessages(
|
||||
storageRoot: string,
|
||||
provider: ReturnType<typeof resolveModel>,
|
||||
messages: ChatMessage[],
|
||||
session: BuiltinSessionState,
|
||||
session: SessionRecord,
|
||||
store: Store,
|
||||
maxTurns: number,
|
||||
): Promise<AgentRunResult> {
|
||||
@@ -45,22 +54,31 @@ async function runBuiltinWithMessages(
|
||||
messages,
|
||||
toolCtx: buildToolContext(storageRoot),
|
||||
maxTurns,
|
||||
existingTurns: session.turns,
|
||||
storageRoot,
|
||||
sessionId: session.sessionId,
|
||||
});
|
||||
|
||||
session.messages = loopResult.messages;
|
||||
session.turns = loopResult.turns;
|
||||
|
||||
const { detailHash, output } = await storeBuiltinDetail(
|
||||
if (loopResult.turnCount === 0) {
|
||||
log("5RWTK9NB", "no turns produced, returning empty output");
|
||||
await removeSession(storageRoot, session.sessionId);
|
||||
return { output: "", detailHash: "", sessionId: session.sessionId };
|
||||
}
|
||||
|
||||
// Read jsonl → persist turns to CAS → store detail
|
||||
const { detailHash } = await storeBuiltinDetail(
|
||||
store,
|
||||
storageRoot,
|
||||
session.sessionId,
|
||||
session.model,
|
||||
session.startedAtMs,
|
||||
session.turns,
|
||||
);
|
||||
|
||||
const finalOutput = output !== "" ? output : loopResult.finalText;
|
||||
return { output: finalOutput, detailHash, sessionId: session.sessionId };
|
||||
// Clean up session jsonl
|
||||
await removeSession(storageRoot, session.sessionId);
|
||||
|
||||
return { output: loopResult.finalText, detailHash, sessionId: session.sessionId };
|
||||
}
|
||||
|
||||
async function runBuiltin(ctx: AgentContext): Promise<AgentRunResult> {
|
||||
@@ -69,14 +87,14 @@ async function runBuiltin(ctx: AgentContext): Promise<AgentRunResult> {
|
||||
const provider = resolveModel(config, config.defaultModel);
|
||||
|
||||
const sessionId = generateUlid(Date.now());
|
||||
await initSessionDir(storageRoot);
|
||||
const messages = buildBuiltinMessages(ctx);
|
||||
|
||||
const session: BuiltinSessionState = {
|
||||
const session: SessionRecord = {
|
||||
sessionId,
|
||||
model: provider.model,
|
||||
startedAtMs: Date.now(),
|
||||
messages,
|
||||
turns: [],
|
||||
};
|
||||
sessions.set(sessionId, session);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user