feat(builtin-agent): persist ReAct loop turns as session JSONL #434

Merged
xiaomo merged 3 commits from feat/turn-jsonl-session into main 2026-05-23 10:48:49 +00:00
Owner

What

ReAct loop 每轮实时写 JSONL,完成后转存 CAS。

Why

  • 运行时不可观测(#433):loop 跑了多少轮、每轮做了什么,只能靠 stderr log
  • 崩溃后无法恢复

Changes

  • 新增 session.ts:JSONL 文件管理(init/append/read/remove),路径 ~/.uncaged/workflow/sessions/<sessionId>.jsonl
  • loop.ts:每轮 append JSONL(assistant turn + tool results),不再内存攒 turns[]
  • detail.ts:读 session JSONL → 逐个 store.put turn CAS node → 存 detail(turns: string[] 正序数组)
  • agent.ts:传 storageRoot/sessionId 给 loop,完成后 removeSession 清理
  • types.ts:TurnPayload 去掉 index(顺序由 JSONL/数组隐含)
  • schemas.ts:同步 type 变更
  • chore:修掉全仓库 biome lint errors(0 errors, 12 warnings)
  • test:skip 2 个 flaky hermes ACP tests(依赖真 LLM)

设计

  • 运行时:每轮 appendFile 一行 JSON 到 sessions/<sessionId>.jsonl,天然正序,零竞争
  • 完成时:JSONL → CAS turns[] → detail node → 删 JSONL
  • 观测tail -f sessions/<id>.jsonl 实时看进度
  • 崩溃恢复:JSONL 残留可用于后续恢复

Ref: #433

## What ReAct loop 每轮实时写 JSONL,完成后转存 CAS。 ## Why - 运行时不可观测(#433):loop 跑了多少轮、每轮做了什么,只能靠 stderr log - 崩溃后无法恢复 ## Changes - **新增 `session.ts`**:JSONL 文件管理(init/append/read/remove),路径 `~/.uncaged/workflow/sessions/<sessionId>.jsonl` - **`loop.ts`**:每轮 append JSONL(assistant turn + tool results),不再内存攒 turns[] - **`detail.ts`**:读 session JSONL → 逐个 `store.put` turn CAS node → 存 detail(`turns: string[]` 正序数组) - **`agent.ts`**:传 storageRoot/sessionId 给 loop,完成后 removeSession 清理 - **`types.ts`**:TurnPayload 去掉 `index`(顺序由 JSONL/数组隐含) - **`schemas.ts`**:同步 type 变更 - **chore**:修掉全仓库 biome lint errors(0 errors, 12 warnings) - **test**:skip 2 个 flaky hermes ACP tests(依赖真 LLM) ## 设计 - **运行时**:每轮 `appendFile` 一行 JSON 到 `sessions/<sessionId>.jsonl`,天然正序,零竞争 - **完成时**:JSONL → CAS turns[] → detail node → 删 JSONL - **观测**:`tail -f sessions/<id>.jsonl` 实时看进度 - **崩溃恢复**:JSONL 残留可用于后续恢复 Ref: #433
xingyue added 3 commits 2026-05-23 10:44:12 +00:00
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
- Fix import ordering (organizeImports) across multiple packages
- Replace forEach with for...of loops (noForEach)
- Replace non-null assertions with fallback values (noNonNullAssertion)
- Add biome-ignore comments for justified noExplicitAny usages
- Remove parameter properties, use explicit class properties (noParameterProperties)
- Fix string concatenation to template literals (useTemplate)
- Fix format issues (CSS, TypeScript)
- Add tailwindDirectives CSS parser config in biome.json
- Replace var with const (noVar)

Result: 0 errors, 12 warnings (all cognitive complexity, acceptable)
Skip acp-client 'prompt() collects structured messages' and
resume-e2e 'resume() after close' — both require live LLM calls
and fail intermittently in CI.
xiaomo requested changes 2026-05-23 10:46:31 +00:00
Dismissed
xiaomo left a comment
Owner

JSONL session 设计干净,loop 重构合理,detail 简化到位。Lint 修复纯格式变更没问题。

🔴 Blocking

Skipped tests 缺少 issue 编号 — 团队规范要求 skip 注释必须引用 issue number:

  • acp-client.test.ts: it.skip("prompt() collects structured messages...")
  • resume-e2e.test.ts: it.skip("resume() after close...")

请开 issue 跟踪这两个 flaky test,然后在 skip 注释里加上 // Flaky: #<issue-number>

🟡 Non-blocking

  • readSessionTurnsJSON.parse(l) as BuiltinTurnPayload 无运行时校验,corrupted line 会静默产生畸形对象
  • BuiltinSessionState export 移除是 breaking change,确认无外部消费者
JSONL session 设计干净,loop 重构合理,detail 简化到位。Lint 修复纯格式变更没问题。 ### 🔴 Blocking **Skipped tests 缺少 issue 编号** — 团队规范要求 skip 注释必须引用 issue number: - `acp-client.test.ts`: `it.skip("prompt() collects structured messages...")` - `resume-e2e.test.ts`: `it.skip("resume() after close...")` 请开 issue 跟踪这两个 flaky test,然后在 skip 注释里加上 `// Flaky: #<issue-number>`。 ### 🟡 Non-blocking - `readSessionTurns` 的 `JSON.parse(l) as BuiltinTurnPayload` 无运行时校验,corrupted line 会静默产生畸形对象 - `BuiltinSessionState` export 移除是 breaking change,确认无外部消费者
xingyue force-pushed feat/turn-jsonl-session from 6474de3419 to 50cd93aa05 2026-05-23 10:48:15 +00:00 Compare
xiaomo approved these changes 2026-05-23 10:48:45 +00:00
xiaomo left a comment
Owner

LGTM Skip 注释已引用 #435,规范合规。

LGTM ✅ Skip 注释已引用 #435,规范合规。
xiaomo merged commit c1f04929f4 into main 2026-05-23 10:48:49 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/workflow#434