瀏覽代碼

Merge branch 'codex/agent-lab-mvp' of huzhenyu/agent-lab into develop

huzhenyu 1 周之前
父節點
當前提交
9658431704
共有 100 個文件被更改,包括 6104 次插入1 次删除
  1. 5 0
      .env.example
  2. 13 0
      .gitignore
  3. 20 0
      .vscode/settings.json
  4. 37 0
      AGENTS.md
  5. 326 1
      README.md
  6. 18 0
      benchmarks/single-target.example.json
  7. 370 0
      docs/plans/2026-07-12-chat-event-agent-production-events-design.md
  8. 129 0
      docs/plans/2026-07-13-agent-benchmark-design.md
  9. 188 0
      docs/plans/2026-07-14-professional-debug-console-ui-design.md
  10. 69 0
      docs/plans/todo-1-project-foundation.md
  11. 110 0
      docs/plans/todo-10-tool-registry-management.md
  12. 107 0
      docs/plans/todo-11-prompt-workspace.md
  13. 104 0
      docs/plans/todo-12-round-stats.md
  14. 105 0
      docs/plans/todo-13-real-queue-websocket-loop.md
  15. 63 0
      docs/plans/todo-14-message-role-validation.md
  16. 67 0
      docs/plans/todo-15-static-package-data.md
  17. 65 0
      docs/plans/todo-16-tool-error-isolation.md
  18. 41 0
      docs/plans/todo-17-agent-config-audit-tools.md
  19. 36 0
      docs/plans/todo-18-prompt-modal-tool-ui.md
  20. 41 0
      docs/plans/todo-19-latency-oriented-polish.md
  21. 85 0
      docs/plans/todo-2-domain-runtime.md
  22. 25 0
      docs/plans/todo-20-agent-config-modals.md
  23. 30 0
      docs/plans/todo-21-event-agent-context-summary.md
  24. 34 0
      docs/plans/todo-22-text-event-protocol.md
  25. 40 0
      docs/plans/todo-23-agent-prompt-and-event-llm.md
  26. 25 0
      docs/plans/todo-24-chat-agent-prompt-list.md
  27. 23 0
      docs/plans/todo-25-prompt-list-message-types.md
  28. 27 0
      docs/plans/todo-26-workspace-snapshot-save.md
  29. 33 0
      docs/plans/todo-27-event-agent-tool-arguments.md
  30. 30 0
      docs/plans/todo-28-session-turns.md
  31. 24 0
      docs/plans/todo-29-rich-audit-log.md
  32. 121 0
      docs/plans/todo-3-websocket-api.md
  33. 32 0
      docs/plans/todo-30-sqlite-session-store.md
  34. 26 0
      docs/plans/todo-31-runtime-persistence.md
  35. 26 0
      docs/plans/todo-32-session-ui.md
  36. 24 0
      docs/plans/todo-33-audit-replay-stream-ui.md
  37. 22 0
      docs/plans/todo-34-event-agent-history-boundary.md
  38. 23 0
      docs/plans/todo-35-audit-modal-raw-llm-events.md
  39. 24 0
      docs/plans/todo-36-audit-detail-compact-ui.md
  40. 25 0
      docs/plans/todo-37-audit-detail-flow-layout.md
  41. 22 0
      docs/plans/todo-38-audit-round-event-semantics.md
  42. 19 0
      docs/plans/todo-39-audit-turn-relative-time.md
  43. 91 0
      docs/plans/todo-4-real-llm-smoke.md
  44. 21 0
      docs/plans/todo-40-chat-message-stream-audit.md
  45. 18 0
      docs/plans/todo-41-session-button-feedback.md
  46. 19 0
      docs/plans/todo-42-session-switch-idle-socket.md
  47. 79 0
      docs/plans/todo-43-typed-provider-tool-calls.md
  48. 67 0
      docs/plans/todo-43.1-typed-event-consumer-regression.md
  49. 65 0
      docs/plans/todo-43.2-remove-legacy-event-consumption.md
  50. 97 0
      docs/plans/todo-44-provider-tool-transcript.md
  51. 69 0
      docs/plans/todo-44.1-tool-transcript-hardening.md
  52. 101 0
      docs/plans/todo-45-generic-event-kernel.md
  53. 70 0
      docs/plans/todo-45.1-event-kernel-boundary-hardening.md
  54. 63 0
      docs/plans/todo-45.2-event-fallback-semantics.md
  55. 52 0
      docs/plans/todo-45.3-sync-resolution-completeness.md
  56. 67 0
      docs/plans/todo-45.4-event-kernel-quality-boundaries.md
  57. 65 0
      docs/plans/todo-45.5-composed-schema-and-copy-failures.md
  58. 61 0
      docs/plans/todo-45.6-mixed-validation-and-handler-snapshot.md
  59. 41 0
      docs/plans/todo-45.7-structured-incomplete-mixed-validation.md
  60. 91 0
      docs/plans/todo-46-configurable-tool-invocation-mode.md
  61. 49 0
      docs/plans/todo-46.1-mode-parity-and-duplicate-call-safety.md
  62. 111 0
      docs/plans/todo-47-built-in-event-plugins.md
  63. 57 0
      docs/plans/todo-47.1-plugin-normalization-and-validation.md
  64. 50 0
      docs/plans/todo-47.2-plugin-token-and-compatibility-boundaries.md
  65. 28 0
      docs/plans/todo-47.3-bare-volume-numeric-guard.md
  66. 114 0
      docs/plans/todo-48-parallel-event-batches.md
  67. 75 0
      docs/plans/todo-48.1-batch-canonical-replay-and-terminal-safety.md
  68. 58 0
      docs/plans/todo-48.2-outcome-cache-and-sync-resource-lifecycle.md
  69. 33 0
      docs/plans/todo-48.3-strict-canonical-types-and-scope-token-cleanup.md
  70. 75 0
      docs/plans/todo-49-tool-mode-ui-session-restore.md
  71. 53 0
      docs/plans/todo-49.1-session-mode-and-atomic-transcript-safety.md
  72. 42 0
      docs/plans/todo-49.2-session-load-race-and-visible-repair.md
  73. 31 0
      docs/plans/todo-49.3-stale-replay-token-order.md
  74. 102 0
      docs/plans/todo-5-tool-call-history.md
  75. 73 0
      docs/plans/todo-50-comparable-model-tool-metrics.md
  76. 32 0
      docs/plans/todo-50.1-metric-key-migration-dedup.md
  77. 110 0
      docs/plans/todo-51-comparison-matrix-closeout.md
  78. 34 0
      docs/plans/todo-51.1-first-reply-and-protocol-matrix.md
  79. 128 0
      docs/plans/todo-52-agent-benchmark-runner.md
  80. 29 0
      docs/plans/todo-52.1-benchmark-url-control-characters.md
  81. 29 0
      docs/plans/todo-52.10-benchmark-bundle-cli-contract.md
  82. 32 0
      docs/plans/todo-52.11-bundle-interrupt-and-path-safety.md
  83. 38 0
      docs/plans/todo-52.12-benchmark-metric-integrity-and-parallel-proof.md
  84. 30 0
      docs/plans/todo-52.13-provider-cleanup-exception-priority.md
  85. 29 0
      docs/plans/todo-52.2-benchmark-url-transport-compatibility.md
  86. 31 0
      docs/plans/todo-52.3-benchmark-case-mode-single-source.md
  87. 32 0
      docs/plans/todo-52.4-benchmark-visible-ttft-and-call-counts.md
  88. 32 0
      docs/plans/todo-52.5-benchmark-ledger-event-identity.md
  89. 29 0
      docs/plans/todo-52.6-benchmark-report-auditability.md
  90. 28 0
      docs/plans/todo-52.7-benchmark-output-reservation-and-cardinality.md
  91. 27 0
      docs/plans/todo-52.8-benchmark-no-overwrite-publication.md
  92. 36 0
      docs/plans/todo-52.9-atomic-report-bundles-and-audit-parity.md
  93. 402 0
      docs/plans/todo-53-professional-debug-console-ui.md
  94. 38 0
      docs/plans/todo-53.1-event-prompt-order-preservation.md
  95. 33 0
      docs/plans/todo-53.2-console-interaction-state.md
  96. 28 0
      docs/plans/todo-53.3-event-prompt-payload-singleton.md
  97. 20 0
      docs/plans/todo-53.4-managed-event-prompt-test-identity.md
  98. 19 0
      docs/plans/todo-53.5-event-prompt-order-independent-identity.md
  99. 41 0
      docs/plans/todo-53.6-browser-visual-qa.md
  100. 75 0
      docs/plans/todo-6-cleanup-closeout.md

+ 5 - 0
.env.example

@@ -0,0 +1,5 @@
+AGENT_LAB_OPENAI_API_KEY=replace-with-your-api-key
+AGENT_LAB_OPENAI_BASE_URL=https://api.openai.com/v1
+AGENT_LAB_OPENAI_DEFAULT_MODEL=gpt-4.1-mini
+AGENT_LAB_OPENAI_INCLUDE_USAGE=true
+AGENT_LAB_REQUEST_TIMEOUT_SECONDS=60

+ 13 - 0
.gitignore

@@ -38,6 +38,10 @@ pip-delete-this-directory.txt
 # Unit test / coverage reports
 htmlcov/
 .tox/
+.pytest_cache/
+.ruff_cache/
+.mypy_cache/
+.pytype/
 .coverage
 .coverage.*
 .cache
@@ -58,3 +62,12 @@ docs/_build/
 # PyBuilder
 target/
 
+# Local environment and agent workspace files
+.env
+.venv/
+.superpowers/
+.worktrees/
+*.db
+*.sqlite
+*.sqlite3
+outputs/benchmarks/

+ 20 - 0
.vscode/settings.json

@@ -0,0 +1,20 @@
+{
+  "files.exclude": {
+    "**/__pycache__": true,
+    "**/.pytest_cache": true,
+    "**/.ruff_cache": true,
+    "**/.mypy_cache": true,
+    "**/.cache": true,
+    "**/.pytype": true,
+    "**/.venv": true
+  },
+  "search.exclude": {
+    "**/__pycache__": true,
+    "**/.pytest_cache": true,
+    "**/.ruff_cache": true,
+    "**/.mypy_cache": true,
+    "**/.cache": true,
+    "**/.pytype": true,
+    "**/.venv": true
+  }
+}

+ 37 - 0
AGENTS.md

@@ -0,0 +1,37 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+
+This repository is a Python Agent Lab for debugging chat and event agents. The source package lives under `src/agent_lab/`:
+
+- `domain/`: message and event models shared across the app.
+- `application/`: runtime orchestration, request/response contracts, and EventAgent tool handling.
+- `infrastructure/`: OpenAI Chat Completions compatible streaming client and parser integration.
+- `presentation/`: FastAPI routes, WebSocket endpoint, and static debug UI under `presentation/static/`.
+- `settings.py`: `pydantic-settings` configuration.
+
+Tests live in `tests/`, planning docs in `docs/plans/`, and the manual WebSocket smoke helper in `scripts/ws_smoke.py`.
+
+## Build, Test, and Development Commands
+
+- `uv sync`: install runtime and dev dependencies from `pyproject.toml` and `uv.lock`.
+- `uv run pytest`: run the full test suite.
+- `uv run uvicorn agent_lab.main:app --reload`: start the FastAPI debug console locally.
+- `uv run python scripts/ws_smoke.py --help`: inspect the WebSocket smoke-test CLI.
+- `uv run python -c "from agent_lab.main import app; print(app.title)"`: verify app import wiring.
+
+## Coding Style & Naming Conventions
+
+Use Python 3.11+ with type hints on public boundaries. Keep DDD layers separate: domain types should not depend on FastAPI or HTTP clients, application code should coordinate use cases, and infrastructure should own provider-specific I/O. Prefer small dataclasses or Pydantic models for exchanged data. Use snake_case for modules, functions, fields, and environment variables.
+
+## Testing Guidelines
+
+Use `pytest` and `pytest-asyncio`. Add focused tests beside the behavior being changed, using names like `test_openai_stream_parser.py` or `test_websocket_api.py`. Runtime and WebSocket tests should use mocked clients unless a task explicitly requires a live LLM smoke.
+
+## Configuration & Runtime Conventions
+
+Configuration comes from `Settings` in `src/agent_lab/settings.py` with the `AGENT_LAB_` prefix and optional local `.env`. Do not commit secrets. The app exposes `GET /health`, `GET /`, and `WebSocket /ws/debug`; keep WebSocket payloads aligned with `application/contracts.py`.
+
+## Commit & Pull Request Guidelines
+
+Keep commits scoped to one todo or behavior change. PRs should summarize user-visible behavior, list verification commands, mention config changes, and include screenshots only when UI layout changes.

+ 326 - 1
README.md

@@ -1,2 +1,327 @@
-# agent-lab
+# Agent Lab
 
+Agent Lab is a FastAPI debug console for comparing ChatAgent capability-invocation
+protocols on one shared event runtime. The implementation uses a generic event
+kernel plus a flat registry of built-in plugins; invocation mode changes how the
+model selects capabilities, not how accepted events are normalized, validated,
+scheduled, executed, persisted, or reported.
+
+The architecture source of truth is the
+[production events design](docs/plans/2026-07-12-chat-event-agent-production-events-design.md).
+The repeatable experiment and closeout scope is tracked by
+[Todo 51](docs/plans/todo-51-comparison-matrix-closeout.md).
+
+## Architecture and Built-ins
+
+The generic kernel owns event lookup, deterministic resolution, validation,
+bounded fallback, deduplication, conflict-aware concurrency, timeouts, terminal
+barriers, normalized results, result-policy aggregation, audit events, and
+metrics. Runtime and the kernel do not branch on a built-in event name.
+
+The four production-shaped plugins are flat, namespaced definitions:
+
+| Event | Result policy | Current default behavior |
+| --- | --- | --- |
+| `session.terminate` | `terminate` | Records an in-memory terminated result and ends the debug session on success. |
+| `device.volume.adjust` | `silent_success` | Validates and records an in-memory volume action; success adds no second answer. |
+| `calendar.schedule.create` | `template_follow_up` | Records an in-memory schedule and emits a deterministic confirmation. |
+| `knowledge.web.search` | `llm_follow_up` | Returns deterministic mock sources, then asks ChatAgent for one grounded follow-up. |
+
+The default registry also retains the lab-only `handoff_note`, `mock_search`, and
+`mock_ticket` tools. `GET /api/tools` is the authoritative runtime catalog.
+
+## Invocation Modes
+
+Set `tool_invocation_mode` per request:
+
+| Mode | Selection protocol | Argument source | Mode-specific cost or dependency |
+| --- | --- | --- | --- |
+| `dual_agent` | ChatAgent receives a compact catalog and emits hidden text events after visible content. | Shared deterministic resolvers run first; EventAgent may make a bounded fallback model call for incomplete supported events. | Depends on marker adherence and adds fallback calls when deterministic resolution is incomplete. |
+| `chat_agent_tools` | ChatAgent receives enabled provider tool schemas and emits native tool calls. | ChatAgent proposes arguments; the shared kernel still normalizes, validates, authorizes, deduplicates, schedules, and executes them. | Full schemas and provider tool transcript remain in ChatAgent context and usage; EventAgent model/prompt settings are inactive for provider-resolved calls. |
+
+Both modes share the same enabled definitions, JSON schemas, handlers/adapters,
+event budget, batch concurrency, timeout, conflict and terminal policies,
+normalized results, and user-visible success/failure contract. Provider-native
+assistant/tool transcript details are intentionally allowed to differ.
+
+### Web search always has two answers
+
+For `knowledge.web.search`, both modes implement the same visible sequence:
+
+1. ChatAgent emits a useful or qualified first answer.
+2. The shared pipeline executes the search and emits its tool result.
+3. ChatAgent makes exactly one follow-up model call and emits a distinct second answer.
+
+The first answer must not wait for the search adapter, and the second answer must
+use the compact returned evidence. This is a shared behavior contract, not a
+provider transcript normalization trick.
+
+## Current Adapter Limits
+
+The default application wiring is safe for local experiments, but it is not
+production integration:
+
+- session termination is an in-memory result, not a real dialogue-service command;
+- volume adjustment does not control a device or publish MQTT/device actions;
+- schedule creation does not write to a real calendar or reminder service;
+- web search returns deterministic `example.invalid` mock sources and does not access the internet.
+
+Inject implementations of `SessionTerminationPort`, `DeviceVolumePort`,
+`CalendarSchedulePort`, and `WebSearchPort` when testing service-owned adapters.
+Real device, calendar, session, and internet integration belongs outside the
+default Agent Lab wiring.
+
+## Configure and Run
+
+Install dependencies and create a local environment file:
+
+```bash
+uv sync
+cp .env.example .env
+```
+
+Supported settings use the `AGENT_LAB_` prefix:
+
+- `AGENT_LAB_OPENAI_API_KEY`: API key for the compatible provider;
+- `AGENT_LAB_OPENAI_BASE_URL`: provider `/v1` root;
+- `AGENT_LAB_OPENAI_DEFAULT_MODEL`: model used when a request leaves `model` blank;
+- `AGENT_LAB_OPENAI_INCLUDE_USAGE`: disable if the provider rejects streamed usage options;
+- `AGENT_LAB_REQUEST_TIMEOUT_SECONDS`: provider HTTP timeout;
+- `AGENT_LAB_DATABASE_PATH`: SQLite session/audit/usage database path (defaults to `.agent_lab.sqlite3`).
+
+Do not commit secrets. Start the app with:
+
+```bash
+uv run uvicorn agent_lab.main:app --reload
+```
+
+Open `http://127.0.0.1:8000`. The professional console keeps sessions and tools
+in the left control rail, gives the conversation the primary workspace, anchors
+round metrics and the composer at the bottom, and places Audit Replay plus usage
+in a collapsible right debug drawer. Control-section and drawer preferences are
+browser-only presentation state and are not included in requests or snapshots.
+
+In `dual_agent`, the read-only EventPrompt is generated from the selected tools
+and appears exactly once in the Chat Agent Prompt List. In `chat_agent_tools`,
+EventPrompt is hidden and excluded from `system_prompts`; switching modes or
+restoring a session/snapshot regenerates the correct state. EventAgent model
+settings are unavailable in direct mode, while shared loop, concurrency, and
+timeout controls remain active.
+
+## Benchmark CLI
+
+`agent-lab-benchmark` runs the same built-in scenarios through `dual_agent` and
+`chat_agent_tools`, then writes matching machine-readable and review-friendly
+reports. It runs in process against the production runtime and event kernel; it
+does not start FastAPI or add WebSocket/network timing to the measured turn.
+
+The API key is read only from `.env` or the environment through
+`AGENT_LAB_OPENAI_API_KEY`. Do not put a key in the benchmark JSON or commit it.
+The JSON config supplies the OpenAI-compatible `base_url`, `model`, run count,
+modes, and cases:
+
+```bash
+cp benchmarks/single-target.example.json benchmarks/single-target.local.json
+```
+
+Edit the copied file before a live run. The checked-in example deliberately uses
+`https://provider.example/v1` and `replace-with-model-id` placeholders and
+contains no secret.
+
+Start with the deterministic mock benchmark. This command does not require an
+API key and makes no network request:
+
+```bash
+uv run agent-lab-benchmark \
+  --config benchmarks/single-target.example.json \
+  --mock
+```
+
+For a live OpenAI-compatible provider, set the key locally, configure a real
+`base_url` and `model` in the copied JSON, and omit `--mock`:
+
+```bash
+export AGENT_LAB_OPENAI_API_KEY='replace-locally'
+uv run agent-lab-benchmark \
+  --config benchmarks/single-target.local.json
+```
+
+CLI values override JSON values. `--base-url`, `--model`, and `--runs` replace
+their scalar settings; repeat `--case` or `--mode` to replace the corresponding
+JSON list. For example, this still performs a zero-network mock run:
+
+```bash
+uv run agent-lab-benchmark \
+  --config benchmarks/single-target.example.json \
+  --mock \
+  --runs 3 \
+  --case web_search_two_answers \
+  --mode dual_agent \
+  --mode chat_agent_tools
+```
+
+The six built-in cases are:
+
+| Case | Expected behavior |
+| --- | --- |
+| `ordinary_chat` | One visible answer and no event. |
+| `device_volume_silent` | One volume event with silent success and no model follow-up. |
+| `calendar_schedule_template` | One schedule event followed by the deterministic confirmation template. |
+| `web_search_two_answers` | A fast first answer, one search event, and exactly one grounded second answer. |
+| `session_terminate` | One farewell, one terminate event, and a terminal session. |
+| `parallel_volume_schedule` | Volume and schedule events both succeed and their async handlers are proven to overlap. |
+
+Each run records provider TTFT (first provider stream item), visible TTFT (first
+non-whitespace user-visible delta), turn wall time, per-call model latency, tool
+handler latency, prompt/completion/total/cached tokens, model/fallback/tool call
+counts, and event names, sources, statuses, and semantic correctness. Provider
+TTFT and visible TTFT are intentionally separate; model and tool spans may
+overlap and are not added to manufacture a total latency.
+
+Per-call model elapsed time accumulates only time awaiting the provider stream
+iterator. It excludes downstream runtime work and consumer backpressure after a
+yield, while provider TTFT and visible TTFT remain wall-clock arrival metrics.
+Token totals are reported only when every model and argument-fallback call
+returns usage; otherwise all token metrics are `null` and the run fails with an
+explicit missing-usage semantic error. The parallel volume/schedule case uses a
+benchmark-owned barrier probe and passes only when both event handlers are
+active at the same time, not merely when two successful event results exist.
+
+By default, reports are published as one complete bundle:
+
+```text
+outputs/benchmarks/<timestamp>/report.json
+outputs/benchmarks/<timestamp>/report.md
+```
+
+The timestamp path is a single relative symlink published only after both files
+are fully written and synced; collisions receive a numeric suffix. JSON is the
+machine-readable source of truth and Markdown is rendered from the same report
+model.
+
+Exit codes are stable:
+
+- `0`: all runs passed and reports were written;
+- `1`: at least one request or semantic check failed, but reports were written;
+- `2`: CLI, config, environment, runner-cardinality, or report/output failure;
+- `130`: interruption.
+
+For a fair mode comparison, keep the provider/model, generation parameters,
+input cases, run count, enabled events, limits, timeouts, adapters, and run
+conditions unchanged. Use fresh paired runs and inspect failed samples instead
+of comparing only averages. p95 is reported with a low-confidence warning when
+fewer than 20 successful samples exist in a case/mode group.
+
+Interpret results within these limits:
+
+- mock mode and the repository's scripted tests validate deterministic flow,
+  event calls, metrics, and report generation, not live provider behavior;
+- the runner is in process, so it does not measure FastAPI, WebSocket, service
+  transport, reconnect, or deployment overhead;
+- default event adapters are in-memory implementations and do not control a
+  device, create a real calendar entry, terminate a service conversation, or
+  search the public web;
+- the CLI does not calculate pricing or automatically declare/rank a winner;
+  evaluate latency, tokens, correctness, and operational tradeoffs together;
+- live runs send prompts to the configured provider and may incur API charges.
+
+## WebSocket
+
+Connect to `ws://127.0.0.1:8000/ws/debug`. The smallest valid initial payload is:
+
+```json
+{
+  "user_message": "Hello",
+  "chat_agent": {}
+}
+```
+
+This uses the default `dual_agent` mode and default EventAgent configuration. A
+minimal built-in event request can make the comparison controls explicit:
+
+```json
+{
+  "user_message": "Set the device volume to 30",
+  "chat_agent": {},
+  "event_agent": {
+    "enabled_tools": ["device.volume.adjust"]
+  },
+  "tool_invocation_mode": "dual_agent"
+}
+```
+
+Change only `tool_invocation_mode` to `chat_agent_tools` for the other protocol
+while keeping model parameters, enabled events, event limits, input, and adapter
+wiring equal. After `session_started`, the same socket accepts later turns as
+`{"type":"user_message","content":"..."}` until a terminal `done` or `error`.
+
+### `ws_smoke.py` is not a dual-mode benchmark
+
+With the server running, `scripts/ws_smoke.py` sends one convenience request and
+prints streamed JSON:
+
+```bash
+uv run python scripts/ws_smoke.py --message "Debug this agent handoff path."
+```
+
+It defaults to `dual_agent`, exercises the `handoff_note` path, has no paired
+mode runner or fairness controls, and does not collect the Todo 51 scenario
+matrix. Use it only for WebSocket connectivity and payload smoke testing.
+
+## HTTP API
+
+| Method and path | Purpose |
+| --- | --- |
+| `GET /health` | Process health. |
+| `GET /` | Static debug UI. |
+| `GET /api/tools` | Current flat registry catalog and schemas. |
+| `POST /api/sessions` | Create a persisted session with optional `title` and `config`. |
+| `GET /api/sessions` | List persisted sessions. |
+| `GET /api/sessions/{session_id}` | Read session metadata and immutable comparison config. |
+| `GET /api/sessions/{session_id}/messages` | Replay persisted visible/provider messages. |
+| `GET /api/sessions/{session_id}/audit` | Read ordered runtime audit events. |
+| `GET /api/sessions/{session_id}/usage` | Read comparable call, turn, and session metrics. |
+
+## `/usage` Fair-Comparison Contract
+
+`GET /api/sessions/{session_id}/usage` returns `calls`, `turns`, and `session`.
+Interpret it with these controls:
+
+- compare runs with the same model parameters, enabled event set, user input,
+  output budget, registry/adapters, event budget, concurrency, and timeouts;
+- token totals include real ChatAgent and EventAgent model calls only; tool
+  execution rows have zero tokens;
+- direct-mode provider tool schema and assistant/tool protocol tokens remain
+  counted rather than being normalized away;
+- EventAgent argument-fallback calls are separate `argument_fallback` rows and
+  contribute their model tokens;
+- `turn_wall_time_ms` is the turn/session wall-clock measure. Do not add model
+  `elapsed_ms` or overlapping `tool_latency_ms` spans to estimate total latency;
+- only logical tool executions count: deduplicated/replayed clones do not inflate
+  `tool_count` or fallback count;
+- call-level `ttft_ms` measures the first provider stream item. Visible-text TTFT
+  is a separate runtime/audit observation and must not be conflated with it.
+
+## Verification
+
+Run the closeout matrix and focused runtime checks:
+
+```bash
+uv run pytest tests/test_tool_invocation_comparison.py -q
+uv run pytest tests/test_debug_runtime.py tests/test_builtin_event_plugins.py tests/test_sqlite_store.py tests/test_websocket_api.py -q
+```
+
+Run the repository gates:
+
+```bash
+uv run pytest
+node --check src/agent_lab/presentation/static/app.js
+uv run python -c "from agent_lab.main import app; print(app.title)"
+git diff --check
+```
+
+These tests establish repeatable application semantics with scripted model
+clients and injected/default mock ports. They do not prove live-provider content
+plus-tool streaming behavior, real network search quality, real device/calendar
+side effects, production credentials, or production latency. Run provider and
+service integration smokes separately before drawing deployment conclusions.

+ 18 - 0
benchmarks/single-target.example.json

@@ -0,0 +1,18 @@
+{
+  "schema_version": 1,
+  "base_url": "https://provider.example/v1",
+  "model": "replace-with-model-id",
+  "runs_per_case": 1,
+  "modes": [
+    "dual_agent",
+    "chat_agent_tools"
+  ],
+  "cases": [
+    "ordinary_chat",
+    "device_volume_silent",
+    "calendar_schedule_template",
+    "web_search_two_answers",
+    "session_terminate",
+    "parallel_volume_schedule"
+  ]
+}

+ 370 - 0
docs/plans/2026-07-12-chat-event-agent-production-events-design.md

@@ -0,0 +1,370 @@
+# ChatAgent / EventAgent Production Events Design
+
+**Status:** implemented and closed in Agent Lab by Todos 45-51
+
+**Date:** 2026-07-12
+
+**Implementation links:** [Todo 45 generic event kernel](todo-45-generic-event-kernel.md),
+[Todo 46 invocation modes](todo-46-configurable-tool-invocation-mode.md),
+[Todo 47 built-in plugins](todo-47-built-in-event-plugins.md),
+[Todo 48 parallel batches](todo-48-parallel-event-batches.md),
+[Todo 49 UI/session restore](todo-49-tool-mode-ui-session-restore.md),
+[Todo 50 comparable metrics](todo-50-comparable-model-tool-metrics.md), and
+[Todo 51 comparison closeout](todo-51-comparison-matrix-closeout.md).
+
+## 1. Goal
+
+Evaluate two configurable capability-invocation architectures for the service voice-reply path, primarily targeting a low-latency conversational toy:
+
+- `ChatAgent` owns low-latency, natural, user-facing speech.
+- the shared event pipeline owns extensible argument resolution, validation, concurrency, execution, and result policy;
+- in dual-agent mode ChatAgent selects event names and EventAgent supplies missing-argument fallback, while direct mode lets ChatAgent select events and propose arguments through native tool calls.
+
+The first production events are:
+
+- terminate the current voice session;
+- adjust device volume;
+- add a calendar/reminder schedule;
+- search the web and provide a second, search-grounded answer.
+
+The primary success criterion is to measure whether keeping provider tool schemas off ChatAgent's first-token critical path actually improves reply latency and cost without reducing event accuracy. The lab must support both direct ChatAgent tool calling and the dual-agent text-event design under one consistent capability contract.
+
+## 2. Architecture Decision
+
+Keep one shared flat event registry and make the event-selection path configurable. The two experimental modes differ only in how events are selected and how initial arguments are proposed; they share argument normalization, execution, concurrency, result policies, and user-visible behavior.
+
+Configuration uses one explicit `tool_invocation_mode` value:
+
+- `dual_agent`: ChatAgent receives a compact event catalog but no provider `tools`. It streams its visible reply and appends internal event names in the text-event block. EventAgent resolves missing arguments.
+- `chat_agent_tools`: ChatAgent receives the enabled provider tool schemas and emits native tool calls, including initial arguments. The shared event pipeline validates and normalizes those arguments before execution.
+
+In `dual_agent` mode, the parser hides the event block from the user and dispatches the named events to EventAgent. In `chat_agent_tools` mode, the streaming client collects native tool-call deltas while continuing to expose any visible assistant content allowed by the provider.
+
+Both modes convert model output into the same internal event batch. The shared pipeline performs deterministic parsing and normalization first, then a small LLM fallback only when required fields remain ambiguous or missing. It validates the resolved arguments and invokes the same handlers through the registry.
+
+ChatAgent may emit multiple event names in one response. Independent events are resolved and executed concurrently. The runtime applies an explicit conflict policy before execution so terminal or mutually exclusive side effects do not race unpredictably.
+
+Each event definition declares a result policy so the runtime does not hardcode behavior by event name:
+
+| Result policy | Runtime behavior |
+| --- | --- |
+| `terminate` | Execute after ChatAgent's farewell, end the session on success, and do not run ChatAgent again. |
+| `silent_success` | Keep ChatAgent's first acknowledgement as the normal response; run a corrective follow-up only if execution fails. |
+| `template_follow_up` | Produce a deterministic confirmation from the normalized execution result without another LLM call. |
+| `llm_follow_up` | Feed a compact structured result back to ChatAgent and produce a second user-facing answer. |
+
+Initial mapping:
+
+| Event | Result policy |
+| --- | --- |
+| `session.terminate` | `terminate` |
+| `device.volume.adjust` | `silent_success` |
+| `calendar.schedule.create` | `template_follow_up` |
+| `knowledge.web.search` | `llm_follow_up` |
+
+### 2.1 Experimental consistency contract
+
+The comparison must not become two separate implementations. Both modes use:
+
+- the same enabled event set and registry-owned JSON schemas;
+- the same ChatAgent model, persona prompt, user-visible conversational history, sampling parameters, and output budget, except for the minimum mode-specific event/tool instruction;
+- the same normalized internal `EventBatch` and event IDs;
+- the same deterministic resolvers, validators, optional low-cost LLM fallback, handlers, timeouts, retries, idempotency keys, concurrency rules, and terminal barriers;
+- the same result policies and user-visible behavior, including the two-answer `knowledge.web.search` flow;
+- the same audit event names and metric definitions.
+
+Only these variables change:
+
+| Concern | `dual_agent` | `chat_agent_tools` |
+| --- | --- | --- |
+| ChatAgent capability context | Compact event names/descriptions | Full enabled tool schemas |
+| Event selection | Hidden text event names | Native provider tool calls |
+| Initial arguments | Empty or minimal; resolved by shared pipeline | Proposed by ChatAgent tool-call arguments |
+| EventAgent LLM fallback | Used only when deterministic resolution is incomplete | Used only when ChatAgent arguments plus deterministic normalization remain incomplete |
+| Provider dependency | Requires reliable content streaming | Requires reliable streaming native tool calls and multiple tool calls |
+
+Direct ChatAgent tools do not bypass the shared execution pipeline. ChatAgent proposes calls; the application still validates, authorizes, deduplicates, schedules, executes, and applies result policies.
+
+Provider-required assistant tool-call and tool-result messages are allowed to differ between modes. They are internal protocol overhead, not user-visible conversation, and their prompt/completion tokens must be counted as part of the direct-mode result rather than normalized away.
+
+### 2.2 Expected comparison
+
+Neither mode is assumed to win before measurement:
+
+| Dimension | `dual_agent` | `chat_agent_tools` |
+| --- | --- | --- |
+| Ordinary-turn LLM calls | One ChatAgent call | One ChatAgent call |
+| ChatAgent prompt growth | Concise descriptions per event | Full JSON schema per tool |
+| Clear side-effect event | Usually one ChatAgent call plus deterministic execution | Usually one ChatAgent call plus deterministic execution |
+| Ambiguous arguments | May add one low-cost EventAgent fallback | ChatAgent arguments may avoid fallback; invalid/incomplete arguments may still add fallback |
+| Web search | First ChatAgent answer + search + second ChatAgent answer | Same user-visible flow and minimum calls |
+| First-answer behavior | Text protocol explicitly places visible reply before event names | Depends on provider support for visible content alongside streamed tool calls |
+| Multiple events | Flat text event list | Multiple native tool calls |
+| Capability growth cost | ChatAgent context grows slowly | ChatAgent receives every enabled schema on every turn |
+| Provider dependence | Requires reliable content streaming and marker adherence | Requires reliable native tool-call streaming, argument JSON, and multiple calls |
+
+The likely trade-off is that direct tools may reduce argument-fallback calls on event-heavy conversations, while dual-agent mode should reduce prompt tokens and preserve first-answer behavior as the enabled capability set grows.
+
+### 2.3 Generic event kernel and flat built-in plugins
+
+The architecture separates a generic event kernel from concrete built-in event plugins. The plugin registry stays flat: the first version does not introduce event category inheritance, nested capability trees, dynamic installation, or a general workflow engine.
+
+The generic kernel owns only reusable orchestration concepts:
+
+- `EventRequest` and ordered `EventBatch` creation from either model protocol;
+- deterministic resolver chains and bounded LLM fallback;
+- schema validation and confirmation decisions;
+- execution-port invocation;
+- deduplication, concurrency, conflicts, cancellation, timeout, and terminal barriers;
+- normalized `EventResult` values and result-policy aggregation;
+- audit and performance spans.
+
+The kernel must not import, compare, or branch on built-in event names.
+
+A built-in plugin owns one concrete capability:
+
+- its namespaced event name and schema version;
+- the concise ChatAgent catalog description and provider tool schema;
+- deterministic resolver and validator;
+- optional LLM fallback policy;
+- execution handler/port adapter;
+- confirmation, risk, idempotency, concurrency, and result policies;
+- focused contract tests.
+
+Initial plugin names are:
+
+- `session.terminate`;
+- `device.volume.adjust`;
+- `calendar.schedule.create`;
+- `knowledge.web.search`.
+
+Adding a capability should normally require only:
+
+1. one new built-in plugin definition containing its namespaced name, schema version, concise ChatAgent description, argument schema, resolution strategy, result policy, timeout, concurrency metadata, and terminal flag;
+2. one deterministic resolver/validator, with optional LLM fallback configuration;
+3. one handler or service-owned port adapter;
+4. focused contract, execution, failure, and concurrency tests.
+
+The parsers, ChatAgent protocols, Runtime, and generic kernel must not require an event-name-specific branch. They operate on the metadata declared by each plugin definition. The event-specific flow sections below describe built-in plugin behavior and acceptance criteria, not hardcoded Runtime architecture.
+
+Multiple events remain a flat ordered list in one batch. Generic batch orchestration handles deduplication, conflict metadata, bounded concurrency, result aggregation, and terminal barriers.
+
+The flat plugin registry should evolve only when real additions reveal repeated structures that cannot be expressed cleanly with definition metadata. Examples include several events sharing one authorization flow, a multi-step transaction/rollback requirement, or recurring dependencies between event outputs. Until then, those abstractions are deliberately out of scope.
+
+## 3. Event Flows
+
+### 3.1 Terminate session
+
+1. The user expresses a clear intention to leave or end the voice conversation.
+2. ChatAgent immediately streams a short, natural farewell.
+3. ChatAgent requests `session.terminate` through the configured text-event or native tool-call protocol.
+4. EventAgent executes a dialogue-session termination command.
+5. On success, the runtime marks the session terminal and suppresses another ChatAgent round.
+6. On failure, the runtime records the failure and may emit one concise corrective message instead of pretending that the session ended.
+
+This event should terminate the dialogue through the dialogue/session boundary. It should not be implemented as a direct MQTT action.
+
+### 3.2 Adjust device volume
+
+1. The user asks for an absolute volume, relative change, mute, or unmute.
+2. ChatAgent immediately acknowledges the requested action naturally.
+3. ChatAgent requests `device.volume.adjust` through the configured protocol.
+4. EventAgent resolves normalized arguments such as:
+   - `mode`: `absolute`, `relative`, `mute`, or `unmute`;
+   - `value`: an absolute target when present;
+   - `delta`: a signed relative change when present.
+5. The handler sends a device-action command through the device-action/dispatch boundary.
+6. A successful action does not produce a second spoken answer. A failed or rejected action produces a short corrective follow-up.
+
+Validation must clamp or reject values outside the supported range and must distinguish "set to 30" from "turn it up by 30".
+
+### 3.3 Add schedule
+
+1. The user asks the toy to remember a future activity, reminder, or calendar item.
+2. ChatAgent immediately acknowledges the request naturally and requests `calendar.schedule.create` through the configured protocol.
+3. EventAgent first attempts deterministic extraction and normalization of:
+   - title or activity;
+   - local date and time;
+   - timezone;
+   - optional recurrence;
+   - optional reminder offset.
+4. If required fields cannot be resolved deterministically from the utterance and recent context, EventAgent uses its LLM fallback with only the schedule schema and minimal relevant history.
+5. If the time remains ambiguous after fallback, no schedule is created and ChatAgent asks one focused clarification question.
+6. On success, the runtime returns a deterministic confirmation containing the normalized date, time, and title. This confirmation does not require a second ChatAgent LLM call.
+
+Schedule execution must use an idempotency key so retransmission or duplicate event detection does not create duplicate entries. Relative phrases such as "tomorrow morning" are resolved against the session timezone and a captured request timestamp.
+
+### 3.4 Web search with two answers
+
+1. ChatAgent immediately gives a useful preliminary response in a natural tone.
+2. When freshness or external facts matter, ChatAgent mentions that it will check and requests `knowledge.web.search` through the configured protocol after the visible reply where the provider supports content plus tool calls.
+3. EventAgent derives a focused search query from the current user request and relevant conversation history.
+4. The search adapter returns structured results, including query, answer evidence, source title, source location, and retrieval time where available.
+5. The runtime sends a compact search-result summary back to ChatAgent.
+6. ChatAgent produces a second answer that:
+   - clearly sounds like an update after checking;
+   - answers with the retrieved evidence;
+   - corrects the preliminary answer if necessary;
+   - avoids repeating the entire first response;
+   - preserves source information for clients that can display it.
+
+The first answer must not fabricate current facts. When the answer depends heavily on search, it should provide a natural bridge or a clearly qualified preliminary view rather than guessing.
+
+### 3.5 Multiple events in one response
+
+ChatAgent may output multiple event names in the hidden event block or multiple native tool calls in one response. Both protocols produce the same flat ordered event batch. The shared pipeline resolves and executes independent events concurrently with a bounded concurrency limit.
+
+Examples that may run concurrently:
+
+- adjust volume and add a schedule;
+- add a schedule and search related information;
+- adjust volume and search the web.
+
+Conflict policy:
+
+- duplicate event names are deduplicated only when their normalized arguments are equivalent;
+- read-only events such as `knowledge.web.search` may run alongside side-effecting events;
+- `session.terminate` is dispatched after other accepted events have reached a terminal result, with a short overall deadline, so ending the dialogue does not cancel an already acknowledged schedule or device action;
+- an event failure does not cancel unrelated events;
+- follow-up outputs are aggregated by policy: deterministic confirmations first, then at most one ChatAgent LLM follow-up for all results that require natural synthesis.
+
+## 4. Generic Kernel, Runtime, and Plugin Registry
+
+The design extracts a generic kernel from Runtime and extends registry-owned plugin definitions with execution metadata rather than adding event-name conditionals throughout orchestration.
+
+Conceptually, a built-in plugin definition contains:
+
+- name, description, and JSON argument schema;
+- argument-generation behavior;
+- deterministic parser/normalizer and required-field rules;
+- whether LLM fallback is allowed;
+- execution handler;
+- result policy;
+- timeout and retry policy;
+- concurrency class and conflict metadata;
+- whether failure is safe to expose to ChatAgent;
+- whether the event makes the session terminal.
+
+Runtime remains responsible for session/turn and model-stream lifecycle. It delegates event work to the generic kernel:
+
+1. stream ChatAgent output;
+2. parse hidden text events or collect native tool calls into the same internal event batch;
+3. submit the flat batch to the event kernel;
+4. let the kernel resolve, validate, confirm, deduplicate, schedule, execute, and normalize results using plugin metadata;
+5. publish normalized audit/tool results;
+6. apply the kernel's aggregated continuation decision;
+7. optionally run one aggregated follow-up ChatAgent round.
+
+EventAgent supplies optional LLM fallback to the kernel; it does not own built-in event behavior. Production side effects remain behind service-owned ports/adapters implemented by plugins.
+
+The local fast Gate for obvious high-confidence intents remains a later optimization. It should be evaluated only after the two configured model-driven modes establish a baseline and production-like measurements show that event-catalog/tool-schema cost or event-selection errors justify the added routing complexity.
+
+## 5. Context and Token Boundaries
+
+ChatAgent should receive:
+
+- the user-visible conversation;
+- its reply/personality instructions;
+- in `dual_agent` mode, a compact event catalog containing only enabled event names and concise descriptions;
+- in `chat_agent_tools` mode, the enabled schemas produced by the same event registry.
+
+In `dual_agent` mode, ChatAgent should not receive:
+
+- provider tool schemas;
+- tool execution details;
+- raw search documents;
+- internal service configuration;
+- prior tool-role protocol messages.
+
+EventAgent should receive:
+
+- the selected event schema only;
+- the smallest relevant slice of user/assistant conversation history;
+- EventAgent-specific instructions and runtime metadata needed for execution.
+
+For `knowledge.web.search`, only the compact result summary enters the second ChatAgent call. Raw provider payloads stay in audit/debug data.
+
+For deterministic resolution, no EventAgent LLM request is made. When fallback is needed, EventAgent should use a lower-cost model and a small token limit because its output is structured arguments rather than conversational text.
+
+## 6. Error Handling and Safety
+
+- Event execution is bounded by per-event timeouts.
+- Termination, volume, and schedule events are not automatically retried when duplicate side effects are possible; handlers should support an idempotency key based on session, turn, event ID, and normalized arguments.
+- Web search may retry transient read-only failures once within its latency budget.
+- Invalid or incomplete volume requests are returned as structured failures so ChatAgent can ask one focused clarification question.
+- Ambiguous schedule time is never guessed into a side effect; it produces a focused clarification request.
+- Parallel event batches use per-event timeouts plus one batch deadline; timed-out events do not discard completed sibling results.
+- EventAgent failure must not corrupt conversation history or block future user turns.
+- Audit data records event detection, deterministic-resolution outcome, whether LLM fallback was used, execution result, policy decision, timings, and redacted raw provider data.
+
+## 7. Service Integration Boundaries
+
+`agent-lab` is the behavior and protocol proving ground. Production integration belongs to `mituan-service` and must use its existing boundaries:
+
+- session termination through the dialogue/session service boundary;
+- volume adjustment through the device-action dispatcher and published device signal, not direct intent-to-MQTT coupling;
+- schedule creation through a calendar/reminder application port with an explicit user/timezone contract;
+- web search through an external-capability adapter that returns a stable internal search-result contract.
+
+The current service voice reply path does not yet wire web search into its model request, so real search integration is a separate service-repository change.
+
+Because implementation spans repositories, implementation planning must be split:
+
+- one `agent-lab` plan for runtime policies, mock/contract tools, UI/audit visibility, and tests;
+- one `mituan-service` plan for production ports, adapters, device/session commands, configuration, and integration tests.
+
+Each repository plan must load that repository's `AGENTS.md` and maintain its own `todos.md` execution order and status.
+
+## 8. Verification
+
+### Agent Lab behavior
+
+- `dual_agent` sends `tools=[]`, injects the compact event catalog, and extracts hidden text events.
+- `chat_agent_tools` sends exactly the enabled registry schemas and converts native tool calls into the same internal event batch.
+- Switching mode does not change enabled events, handlers, normalized arguments, result policies, concurrency, or user-visible success/failure semantics.
+- Both modes support multiple events in one model response and execute the resulting batch through the same bounded parallel orchestrator.
+- The event catalog and full tool-schema prompt costs are measured independently.
+- Both modes are prompted to emit the natural first response before event execution; if a native tool provider suppresses visible content until after tool execution, that is recorded as a direct-mode behavior/consistency failure rather than hidden by the harness.
+- Hidden event blocks never appear in user-visible deltas.
+- Deterministic argument resolution bypasses the EventAgent LLM, while incomplete or ambiguous cases use the configured fallback.
+- `session.terminate` yields farewell -> accepted sibling events finish or time out -> successful termination -> no second ChatAgent call.
+- `device.volume.adjust` yields acknowledgement -> action result, with no second answer on success and a corrective answer on failure.
+- `calendar.schedule.create` yields acknowledgement -> normalized execution -> deterministic confirmation, without a second LLM call.
+- `knowledge.web.search` yields first answer -> search execution -> distinct second answer grounded in the returned result.
+- Multiple independent events execute concurrently, preserve input order in reported results, and isolate failures.
+- A mixed batch produces at most one extra ChatAgent LLM call even when several events require natural follow-up.
+- Search failure still yields a natural, non-blocking second message.
+- Event IDs make duplicate execution idempotent.
+
+### Service integration
+
+- Termination cleans up active dialogue state across normal, stop, disconnect, and transport-error paths.
+- Volume commands validate absolute/relative semantics and reach the device through the established dispatcher/protocol boundary.
+- Schedule commands resolve relative time against the correct timezone and do not create duplicates.
+- Search configuration is environment-specific and contains no committed credentials.
+- End-to-end tracing can correlate one user turn, one or two ChatAgent responses, parallel EventAgent work, and downstream side effects.
+
+### Performance acceptance
+
+- Replay the same conversation/event dataset in `dual_agent` and `chat_agent_tools` modes with the same model parameters and enabled events.
+- Compare each mode with all events disabled and with all four enabled.
+- The first answer must not wait for search completion.
+- Report first-answer TTFT, first-answer completion time, ChatAgent prompt/completion tokens, LLM calls per turn, deterministic-resolution hit rate, EventAgent fallback rate/tokens, event-dispatch delay, per-event latency, second-answer TTFT, total turn latency, and estimated cost separately.
+- Report event-selection precision/recall, multiple-event recall, valid-argument rate before and after normalization, execution success rate, and first-answer quality for each mode.
+- Track average LLM calls per turn and cost per successful event; the target is one ChatAgent call for ordinary turns, zero EventAgent LLM calls for deterministic events, and a second ChatAgent call only when result policy requires natural synthesis.
+
+Expected hypotheses, not predetermined conclusions:
+
+- `dual_agent` should use fewer ChatAgent prompt tokens and scale better as tool schemas grow;
+- `chat_agent_tools` may avoid EventAgent fallback calls when ChatAgent produces complete arguments;
+- provider-native tool calls may delay or suppress visible content before execution, so actual TTFT must be measured rather than assumed;
+- the better production default depends on event frequency, schema size, provider behavior, argument accuracy, and total cost per successful turn.
+
+## 9. Non-Goals
+
+- Replacing the current compatible Chat Completions client with a different API solely for web search.
+- Moving device protocol or MQTT details into EventAgent.
+- Making every event produce a second answer.
+- Implementing the local fast Gate in the first iteration.
+- Introducing event inheritance, a nested capability taxonomy, a plugin lifecycle, or a general DAG/workflow engine before repeated requirements justify them.
+- Broad refactoring of the service voice pipeline outside the boundaries required by these four events.

+ 129 - 0
docs/plans/2026-07-13-agent-benchmark-design.md

@@ -0,0 +1,129 @@
+# Agent Invocation Benchmark Design
+
+**Status:** implemented and verified on 2026-07-14 (Asia/Shanghai)
+
+**Date:** 2026-07-13
+
+## Goal
+
+Provide a repeatable benchmark command where the operator configures an OpenAI-compatible `base_url`, `model`, and API key, then automatically runs the same business scenarios in `dual_agent` and `chat_agent_tools` modes and produces reviewable evaluation reports.
+
+## Chosen Architecture
+
+The first version is an in-process packaged CLI. It constructs the existing `OpenAICompatibleChatClient`, `DebugRuntime`, generic event kernel, built-in mock adapters, and SQLite session store directly. It does not start FastAPI or connect through WebSocket.
+
+This preserves the complete ChatAgent/EventAgent/event-kernel behavior while reducing benchmark orchestration noise. A later black-box benchmark may exercise WebSocket/network overhead after the in-process baseline is stable.
+
+Alternatives considered:
+
+- WebSocket black-box benchmark: validates the complete service boundary but adds server lifecycle, port, reconnect, and transport timing noise.
+- Provider-only benchmark: easiest latency measurement but cannot validate event detection, fallback, execution, result policies, or two-answer behavior.
+
+## Configuration
+
+The API key is read only from `.env`/environment through `AGENT_LAB_OPENAI_API_KEY`. Benchmark JSON must not contain secrets.
+
+```json
+{
+  "schema_version": 1,
+  "base_url": "https://provider.example/v1",
+  "model": "model-name",
+  "runs_per_case": 1,
+  "modes": ["dual_agent", "chat_agent_tools"],
+  "cases": [
+    "ordinary_chat",
+    "device_volume_silent",
+    "calendar_schedule_template",
+    "web_search_two_answers",
+    "session_terminate",
+    "parallel_volume_schedule"
+  ]
+}
+```
+
+CLI overrides JSON using `CLI > JSON`. Repeatable `--case` and `--mode` options replace the corresponding JSON lists. Live mode requires explicit base URL, model, and API key. `--mock` requires no API key and makes no network request.
+
+The first version intentionally supports one target per command. Multi-provider orchestration, parallel load, custom case DSL, pricing, CSV, and automatic winner ranking are out of scope.
+
+## Execution and Isolation
+
+Each case/mode/iteration run receives:
+
+- a unique session ID;
+- a dedicated temporary SQLite database;
+- the production `DebugRuntime` and default built-in in-memory adapters;
+- a timing wrapper around the configured chat client;
+- identical model parameters, enabled event definitions, event limits, and inputs across modes.
+
+The runner uses `start_session()` and consumes output through the first `turn_completed` or `error`, then reads persisted audit and usage data. It always closes the runtime/client and removes the temporary database.
+
+One failed run does not stop later combinations. Results preserve deterministic case → mode → iteration ordering.
+
+## Built-in Cases and Semantic Checks
+
+The benchmark catalog owns the same six production-shaped cases as the closeout matrix:
+
+- ordinary chat: one visible answer, no event;
+- device volume: fast first acknowledgement, one successful `device.volume.adjust`, no model follow-up;
+- schedule: fast acknowledgement, one successful `calendar.schedule.create`, one deterministic template;
+- web search: first answer, successful `knowledge.web.search`, exactly one second answer;
+- terminate: one farewell, successful `session.terminate`, terminal session;
+- parallel volume plus schedule: both events accepted, stable result order, one schedule template.
+
+Mock mode emits deterministic streams for these cases. Live mode relies on the configured model and marks semantic mismatches as failed runs rather than silently accepting them.
+
+Checks include answer count, first-answer ordering before tool completion, exact expected event names/source, normalized event status, model/tool/fallback counts, and terminal behavior.
+
+## Metrics
+
+Each run records:
+
+- initial provider TTFT from the first yielded provider stream item;
+- first visible delta TTFT measured from benchmark turn start;
+- pure model call elapsed values from the timing wrapper;
+- `turn_wall_time_ms` from the persisted turn ledger;
+- prompt, completion, total, and cached tokens;
+- ChatAgent model call count, EventAgent fallback count, logical tool count;
+- detected event names/sources, tool statuses, and handler latency;
+- semantic failures and a redacted/truncated error.
+
+Provider TTFT, visible TTFT, pure model elapsed, tool latency, and turn wall time remain separate. Overlapping spans are never summed to manufacture a total.
+
+For each case/mode group, successful non-null samples produce p50 and p95 using linear interpolation with `index = (n - 1) * q`. Markdown flags p95 as low confidence when fewer than 20 successful samples exist.
+
+## Reports and Exit Codes
+
+One command writes a matching timestamped report bundle:
+
+- `outputs/benchmarks/<timestamp>/report.json` as the machine-readable source of truth;
+- `outputs/benchmarks/<timestamp>/report.md` rendered from the same report model.
+
+Both files are fully written and synced in one private bundle directory before a
+single relative symlink publishes the timestamp path. Publication therefore has
+one atomic, no-overwrite boundary; a timestamp collision selects a numeric suffix
+instead of replacing an existing bundle.
+
+Exit codes:
+
+- `0`: every run passed and both reports were written;
+- `1`: at least one request or semantic check failed, but reports were written;
+- `2`: CLI/config/environment/report-write failure;
+- `130`: interruption.
+
+## Security
+
+- API keys cannot appear in JSON configuration or report models.
+- Base URLs containing userinfo, query, or fragment are rejected.
+- Errors are redacted and truncated before stdout, JSON, or Markdown rendering.
+- Redaction covers configured key values, Bearer/API-Key forms, and common key/token parameters.
+- Reports do not include headers, raw SSE chunks, full provider bodies, or secrets.
+- The default benchmark adapters do not reach real device, calendar, session, or web-search services.
+
+## Testing
+
+- Config/CLI override and validation tests.
+- Deterministic mock end-to-end run with zero network construction.
+- Metric source separation and semantic validation tests.
+- Partial-failure continuation and exit-code tests.
+- Percentile, aggregation, JSON/Markdown consistency, atomic write, and secret-redaction tests.
+- Full repository regression and one generated mock report smoke.

+ 188 - 0
docs/plans/2026-07-14-professional-debug-console-ui-design.md

@@ -0,0 +1,188 @@
+# Professional Debug Console UI Design
+
+**Status:** approved for implementation
+
+**Date:** 2026-07-14
+
+## Goal
+
+Refactor the existing static Agent Lab page into a clearer professional debugging console while preserving its current FastAPI, WebSocket, session, tool, prompt, audit, and usage contracts.
+
+The same change fixes EventPrompt state drift:
+
+- in `dual_agent` mode, EventPrompt is visible, read-only, unique, and derived from the currently selected tools;
+- in `chat_agent_tools` mode, EventPrompt is hidden and excluded from outbound `system_prompts`;
+- mode changes, tool changes, session restores, and workspace snapshot restores all produce the same deterministic UI state.
+
+The work is limited to the existing static frontend and focused regression tests. It does not introduce a frontend framework, bundler, backend protocol revision, or new runtime capability.
+
+## Chosen Approach
+
+Keep the current server-rendered static entrypoint and refactor `index.html`, `styles.css`, and `app.js` in place.
+
+This is preferred over introducing React/Vue because the page is an experimental service console rather than a product frontend. A framework migration would add build tooling, dependency cost, and regression surface without improving the core experiment. A CSS-only reskin is also insufficient because the requested behavior needs layout state, drawer state, collapsible controls, and one authoritative EventPrompt synchronization path.
+
+## Information Architecture
+
+### Top status bar
+
+The top bar contains:
+
+- Agent Lab identity;
+- current session identity;
+- connection state;
+- current invocation-mode badge;
+- the control that opens or closes the Audit/Usage drawer.
+
+Connection and mode information remain visible without consuming the chat workspace.
+
+### Left control rail
+
+The left rail groups controls into collapsible sections:
+
+- **Session** — expanded by default;
+- **Tools** — expanded by default;
+- **Agent Config** — collapsed by default;
+- **Snapshot** — collapsed by default.
+
+Collapse state is persisted in browser storage. Existing control IDs and request behavior are preserved where practical so the change remains presentation-focused.
+
+Tools are rendered as compact selectable rows/cards with a selected count and clear All/None actions. Tool selection remains available in both invocation modes because the selected set controls either the EventAgent catalog or ChatAgent provider tools.
+
+### Main chat workspace
+
+The center column is the primary workspace:
+
+- conversation history receives most available width and height;
+- user, assistant, tool/status, warning, and error messages have distinct but restrained visual treatments;
+- streaming and turn state remain visible without adding decorative animation;
+- the composer stays anchored at the bottom of the workspace.
+
+Round metrics are placed directly above the composer as a compact strip. They remain secondary to the conversation and do not move into the debug drawer.
+
+### Audit and usage drawer
+
+Audit Replay and usage summaries move from an always-visible fixed column into a collapsible right drawer.
+
+- Opening the drawer shows audit events and usage information together.
+- Closing it returns the width to the chat workspace.
+- Existing audit detail interactions are preserved.
+- Drawer state is persisted in browser storage.
+
+On narrow screens, the page becomes a stacked layout. Controls, chat, metrics/composer, and the opened debug drawer must remain usable without horizontal page scrolling.
+
+## Visual System
+
+Use a light professional console palette with neutral surfaces, subtle borders, restrained shadows, and a small accent color for active states. The design should feel operational rather than decorative.
+
+Define shared CSS variables for color, spacing, radii, control height, and typography. Buttons have explicit primary, secondary, and danger treatments. Status badges communicate connection, invocation mode, and active selections. Keyboard focus remains visible and color is not the only indicator of state.
+
+The UI keeps the existing browser-native implementation and does not add icon libraries or external assets.
+
+## EventPrompt State Model
+
+### Single source of truth
+
+EventPrompt is generated state, not a normal user-authored prompt item. One frontend refresh function owns all derived tool/mode presentation and performs these steps in a fixed order:
+
+1. read the current invocation mode and selected tools;
+2. update the selected-tool count and mode help/status;
+3. remove every existing generated or legacy EventPrompt entry from the prompt list;
+4. if the mode is `dual_agent`, generate and insert exactly one read-only EventPrompt from the selected tools;
+5. if the mode is `chat_agent_tools`, leave EventPrompt absent and mark EventAgent-only model controls unavailable;
+6. refresh any dependent visibility and disabled states.
+
+Every relevant mutation calls this same function:
+
+- individual tool checkbox change;
+- All/None actions;
+- invocation-mode change;
+- initial application bootstrap;
+- session restoration;
+- workspace snapshot restoration;
+- prompt-workspace restoration.
+
+This replaces the current scattered update calls that can update mode help without updating EventPrompt.
+
+### Restore compatibility and deduplication
+
+Persisted `system_prompts` are currently flat text values and do not preserve whether a value was generated EventPrompt content. Legacy restores can therefore treat an old EventPrompt as a custom prompt and append a newly generated EventPrompt, producing duplicates.
+
+During restore, prompt entries matching the generated EventPrompt marker/shape are discarded before normal custom prompts are rebuilt. The derived-state refresh then inserts at most one current EventPrompt when required. User-authored prompts keep their order and content.
+
+No storage schema migration is required for this UI repair. New saves continue to serialize only prompts that are valid for the current invocation mode.
+
+### Outbound request rules
+
+`collectSystemPrompts()` must obey mode semantics rather than blindly serializing every visible prompt-list item:
+
+- `dual_agent`: include user-authored prompts and the one current generated EventPrompt;
+- `chat_agent_tools`: include user-authored prompts only, even if stale DOM or legacy persisted data contains EventPrompt-like content.
+
+The request builder remains compatible with existing backend contracts. This filtering is a defensive boundary in addition to hiding/removing the generated item in the UI.
+
+### Configuration availability
+
+In `chat_agent_tools` mode, EventAgent model configuration is visually unavailable because EventAgent does not perform the invocation decision. Shared runtime controls such as concurrency and timeout remain editable because direct tool execution still uses the shared event kernel and policies.
+
+Switching back to `dual_agent` restores EventAgent model configuration availability and regenerates EventPrompt from the current tool selection.
+
+## Error and State Handling
+
+- A failed session or snapshot restore retains the existing visible workspace and uses the existing feedback/error surface.
+- Empty tool selection produces a deterministic EventPrompt representing no enabled events rather than retaining stale tool descriptions.
+- UI state changes are synchronous and do not wait for a server round trip.
+- Active-turn guards remain unchanged; the refactor must not weaken protection around session switching or configuration changes during a turn.
+- The drawer and collapsible-section preferences are presentation state only and must not enter session runtime payloads.
+
+## File Boundaries
+
+- `src/agent_lab/presentation/static/index.html`: semantic layout, collapsible groups, top status, drawer toggle, and accessible labels.
+- `src/agent_lab/presentation/static/styles.css`: design tokens, responsive shell, control groups, tool rows, message roles, metrics/composer, and drawer styling.
+- `src/agent_lab/presentation/static/app.js`: drawer/collapse persistence, unified derived-state refresh, restore deduplication, mode-aware prompt collection, and configuration availability.
+- `tests/`: focused static/WebSocket contract regressions using the repository's existing test style.
+- project docs: update UI/runtime guidance after behavior is verified.
+
+Backend runtime, event-kernel, persistence, and WebSocket contracts are not changed unless a failing regression proves that a minimal compatibility adjustment is required. Any such adjustment becomes a separate follow-up todo rather than silently expanding Todo 53.
+
+## Verification
+
+Focused regressions must prove:
+
+- changing one tool immediately rebuilds EventPrompt with only selected tools;
+- All/None actions synchronize the selected count and EventPrompt;
+- loading a session or snapshot cannot create duplicate EventPrompt entries;
+- `chat_agent_tools` hides EventPrompt and excludes it from `system_prompts`;
+- switching back to `dual_agent` restores exactly one current EventPrompt;
+- EventAgent-only model controls and shared execution controls have the correct mode-dependent availability;
+- existing request/session/audit contracts remain intact.
+
+Implementation verification will include:
+
+- focused static and WebSocket tests;
+- `node --check src/agent_lab/presentation/static/app.js`;
+- `uv run pytest`;
+- `git diff --check`;
+- local browser QA at desktop and narrow widths, covering chat, tool selection, mode switching, prompt editing, session restore, drawer behavior, and composer usability.
+
+## Delivery Workflow
+
+After this design is approved:
+
+1. create the detailed Todo 53 implementation plan;
+2. execute in an isolated git worktree;
+3. use tests first for each behavior change;
+4. delegate bounded implementation/review tasks to subagents while the main session owns planning and evaluation;
+5. commit each completed todo with the solved problem and any relevant risk in the commit message;
+6. record review-driven corrections as `53.x` rows beneath Todo 53 instead of rewriting completed todo history;
+7. update project documentation after final verification;
+8. merge the verified result locally into the base branch.
+
+## Out of Scope
+
+- React, Vue, another frontend framework, or a build pipeline;
+- backend API/schema redesign;
+- changing the two invocation-mode semantics;
+- adding new event plugins or benchmark cases;
+- a dark theme, theme switcher, or visual-companion assets;
+- a general prompt persistence schema migration.

+ 69 - 0
docs/plans/todo-1-project-foundation.md

@@ -0,0 +1,69 @@
+# Todo 1 Project Foundation Plan
+
+**Status:** done
+
+## Result
+
+`uv run pytest` starts from `pyproject.toml`, loads tests from `tests/`, and fails on missing implementation modules:
+
+- `agent_lab.domain.events`
+- `agent_lab.application.contracts`
+- `agent_lab.application.event_agent`
+
+This is the accepted red-test baseline for the next todo.
+
+## Goal
+
+Establish the Python project skeleton and red-test baseline for the Agent Lab MVP.
+
+## Scope
+
+This todo covers only:
+
+- uv-managed Python package metadata.
+- Initial DDD-oriented package directories.
+- Tests that describe parser, EventAgent, and runtime-routing behavior.
+- A red test run that proves implementation is still missing.
+
+This todo does not implement the parser, agents, FastAPI app, WebSocket endpoint, or UI.
+
+## Files
+
+- Create or keep: `pyproject.toml`
+- Create or keep: `uv.lock`
+- Create: `src/agent_lab/__init__.py`
+- Create: `src/agent_lab/domain/__init__.py`
+- Create: `src/agent_lab/application/__init__.py`
+- Create: `src/agent_lab/infrastructure/__init__.py`
+- Create: `src/agent_lab/presentation/__init__.py`
+- Create or keep: `tests/test_openai_stream_parser.py`
+- Create or keep: `tests/test_event_agent.py`
+- Create or keep: `tests/test_debug_runtime.py`
+- Update: `docs/plans/todos.md`
+
+## Steps
+
+1. Confirm current uncommitted files with `git status --porcelain=v2 --branch`.
+2. Ensure `pyproject.toml` declares the package, Python version, FastAPI/httpx/pydantic-settings/uvicorn dependencies, and pytest config.
+3. Ensure the `src/agent_lab/` package directories exist with `__init__.py` files.
+4. Ensure tests cover these behaviors:
+   - OpenAI Chat Completions stream chunks produce message deltas, usage, and tool-call events.
+   - EventAgent converts an enabled event/tool into a `tool` reply message.
+   - Runtime routes ChatAgent events through EventAgent and resumes ChatAgent with the tool reply.
+5. Run `uv run pytest`.
+6. Accept Todo 1 only if the tests fail because implementation modules or methods are missing. If failures are syntax errors, bad test imports unrelated to missing implementation, or malformed pyproject config, fix Todo 1 before moving on.
+7. Update `docs/plans/todos.md`: set Todo 1 to `done` if accepted, otherwise keep it `in_progress` with a note.
+
+## Verification
+
+Run:
+
+```bash
+uv run pytest
+```
+
+Expected Todo 1 result:
+
+- pytest starts successfully from `pyproject.toml`.
+- tests are discovered.
+- failures point to missing implementation for `agent_lab` modules or classes.

+ 110 - 0
docs/plans/todo-10-tool-registry-management.md

@@ -0,0 +1,110 @@
+# Todo 10 Tool Registry Management Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and committed as `43b0cc8 feat: add tool registry management`.
+
+Main-session verification:
+
+```bash
+uv run pytest tests/test_event_agent.py tests/test_debug_runtime.py tests/test_websocket_api.py
+uv run pytest
+```
+
+Results:
+
+- focused EventAgent/runtime/WebSocket tests: `20 passed, 1 warning in 0.26s`
+- full suite: `23 passed, 1 warning in 0.27s`
+
+## Goal
+
+Replace the single hardcoded EventAgent tool checkbox with a backend tool registry and UI-managed loaded tools.
+
+## Why
+
+The original requirement says the debug page must manage which tools are loaded in EventAgent. The MVP has only one hardcoded `handoff_note` checkbox and duplicated tool schema logic in runtime. This todo makes tools a backend-owned registry so UI and runtime share the same source of truth.
+
+## Scope
+
+This todo covers:
+
+- A backend registry for available EventAgent tools.
+- An API endpoint that lists available tools and their schema/description.
+- Runtime tool schema lookup through the registry.
+- EventAgent execution through registered handlers.
+- Static UI rendering tool checkboxes from the API.
+- Tests for listing tools, selected-tool behavior, and UI request payload.
+
+This todo does not cover:
+
+- User-authored Python tool plugins.
+- Persistent tool configuration.
+- Hot-reloading tools from files.
+- Adding tools beyond the existing `handoff_note`.
+
+## Files
+
+- Create: `src/agent_lab/application/tools.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/presentation/web.py`
+- Modify: `src/agent_lab/presentation/static/index.html`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify/add tests in `tests/test_event_agent.py`, `tests/test_debug_runtime.py`, and `tests/test_websocket_api.py`.
+- Do not modify: `docs/plans/todos.md`.
+
+## Design
+
+Create a small registry in application layer:
+
+- `ToolDefinition`: name, description, parameters, handler.
+- `ToolRegistry.available_tools()`: returns serializable public tool metadata.
+- `ToolRegistry.chat_tools(enabled_names)`: returns OpenAI-compatible tool definitions for selected names.
+- `ToolRegistry.handle(event)`: executes a registered tool and returns payload dict.
+
+`handoff_note` remains the only registered MVP tool. Unknown or disabled tools should still produce tool replies with explicit JSON errors, preserving current runtime behavior.
+
+`DebugRuntime` should accept an optional registry, defaulting to the built-in registry. `EventAgent` should also receive that registry. The runtime should stop building tool schemas manually.
+
+`GET /api/tools` returns:
+
+```json
+[
+  {
+    "name": "handoff_note",
+    "description": "Send a note to the event agent.",
+    "parameters": { "type": "object", "properties": { "message": { "type": "string" } }, "required": ["message"] }
+  }
+]
+```
+
+The static UI should fetch `/api/tools` on load, render a checkbox per tool, and send selected names in `event_agent.enabled_tools`.
+
+## Steps
+
+1. Add failing tests for:
+   - `GET /api/tools` returns `handoff_note` metadata.
+   - Runtime passes selected tool schemas from registry into ChatAgent.
+   - EventAgent returns disabled/unknown errors through the registry path.
+   - Static HTML/JS no longer hardcodes a `tool-handoff-note` checkbox dependency.
+2. Implement `application/tools.py`.
+3. Refactor `EventAgent` and `DebugRuntime` to use `ToolRegistry`.
+4. Add `GET /api/tools`.
+5. Update static UI to render tool checkboxes dynamically and preserve default checked state.
+6. Run:
+
+```bash
+uv run pytest tests/test_event_agent.py tests/test_debug_runtime.py tests/test_websocket_api.py
+uv run pytest
+```
+
+## Verification
+
+Expected result:
+
+- Tool metadata comes from backend API.
+- Runtime and EventAgent use the same registry.
+- UI selected tool list still drives `event_agent.enabled_tools`.
+- Full suite passes.

+ 107 - 0
docs/plans/todo-11-prompt-workspace.md

@@ -0,0 +1,107 @@
+# Todo 11 Prompt Workspace Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and committed as `ee2334f feat: add prompt workspace persistence`.
+
+Main-session verification:
+
+```bash
+uv run pytest tests/test_websocket_api.py
+uv run pytest
+```
+
+Results:
+
+- focused static/WebSocket tests: `14 passed, 1 warning in 0.23s`
+- full suite: `25 passed, 1 warning in 0.18s`
+
+## Goal
+
+Improve prompt and pre-message editing with browser-side persistence and reusable prompt sets.
+
+## Why
+
+The original requirement expects editing multiple system prompts and pre-messages. The MVP supports editing them, but every refresh loses the setup. A local prompt workspace lets a debugger quickly switch between prompt configurations without introducing a backend database.
+
+## Scope
+
+This todo covers:
+
+- Browser-side prompt set save/load/delete using `localStorage`.
+- Saving system prompts, pre-messages, and current ChatAgent/EventAgent parameter values.
+- A small UI section for prompt set name, saved-set selector, save, load, and delete.
+- Tests that verify the static page exposes prompt workspace controls and JS uses stable storage keys.
+
+This todo does not cover:
+
+- Server-side persistence.
+- Sharing prompt sets between users.
+- Import/export files.
+- Rich prompt templating.
+
+## Files
+
+- Modify: `src/agent_lab/presentation/static/index.html`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify: `src/agent_lab/presentation/static/styles.css` only if needed for compact layout.
+- Modify/add tests, preferably `tests/test_websocket_api.py` or a new focused static UI test.
+- Do not modify: `docs/plans/todos.md`.
+
+## Design
+
+Use `localStorage` key `agent-lab.prompt-sets.v1`. Store JSON:
+
+```json
+{
+  "sets": {
+    "default debugger": {
+      "system_prompts": ["..."],
+      "pre_messages": [{"role": "user", "content": "..."}],
+      "chat_agent": {"model": "", "temperature": 0.2, "max_tokens": 800},
+      "event_agent": {"enabled_tools": ["handoff_note"], "max_event_loops": 3}
+    }
+  }
+}
+```
+
+The UI should include:
+
+- prompt set name input;
+- saved set selector;
+- save button;
+- load button;
+- delete button.
+
+Saving reads the current form. Loading restores fields and tool checkboxes after tools are loaded. Deleting removes the selected set.
+
+## Steps
+
+1. Add failing static tests for:
+   - `index.html` contains prompt workspace controls.
+   - `app.js` contains `agent-lab.prompt-sets.v1` and functions for save/load/delete.
+2. Implement prompt workspace controls in HTML.
+3. Implement localStorage helpers in JS.
+4. Ensure loading a prompt set can restore dynamic tool checkboxes after `/api/tools` finishes.
+5. Run:
+
+```bash
+uv run pytest tests/test_websocket_api.py
+uv run pytest
+```
+
+6. If successful, commit implementation files only with message:
+
+```bash
+git commit -m "feat: add prompt workspace persistence"
+```
+
+## Verification
+
+Expected result:
+
+- Static tests prove controls and storage key exist.
+- Existing WebSocket/API tests still pass.
+- Full suite passes.

+ 104 - 0
docs/plans/todo-12-round-stats.md

@@ -0,0 +1,104 @@
+# Todo 12 Structured Round Stats Plan
+
+**Status:** done
+
+## Goal
+
+Move round statistics to structured backend events for token counts, cached tokens, TTFT, elapsed time, and per-turn summaries.
+
+## Why
+
+The original requirement calls for chat-window round statistics: token count, cache hits, TTFT, and total elapsed time. The MVP UI calculates TTFT/elapsed in the browser and only displays usage chunks. That is useful visually, but not enough for reproducible Agent debugging. Backend-emitted stats should become part of the WebSocket event stream.
+
+## Scope
+
+This todo covers:
+
+- Backend timing collection inside `DebugRuntime.run()`.
+- Structured `round_stats` WebSocket messages.
+- Usage aggregation from streamed `usage` items.
+- UI rendering from backend `round_stats` events.
+- Tests for deterministic stats using an injectable clock.
+
+This todo does not cover:
+
+- Persistent run history.
+- Multi-run comparison dashboards.
+- Provider-side detailed cache diagnostics beyond available `cached_tokens`.
+- Cost estimation.
+
+## Files
+
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify/add application contracts or domain models only if needed.
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify/add tests, preferably `tests/test_debug_runtime.py` and `tests/test_websocket_api.py`.
+- Do not modify: `docs/plans/todos.md`.
+
+## Design
+
+Add an injectable monotonic clock to `DebugRuntime`:
+
+```python
+Clock = Callable[[], float]
+DebugRuntime(chat_client, queues=None, registry=None, clock=time.perf_counter)
+```
+
+Track one `round_index` per ChatAgent call. For each round:
+
+- `started_at = clock()`;
+- first message delta sets `ttft_ms` if unset;
+- usage item updates `prompt_tokens`, `completion_tokens`, `total_tokens`, and `cached_tokens`;
+- after the ChatAgent stream completes, emit:
+
+```json
+{
+  "type": "round_stats",
+  "round_index": 1,
+  "ttft_ms": 123,
+  "elapsed_ms": 456,
+  "prompt_tokens": 10,
+  "completion_tokens": 20,
+  "total_tokens": 30,
+  "cached_tokens": 5,
+  "had_event": true
+}
+```
+
+Keep existing `usage` messages for compatibility, but the UI should prefer `round_stats` for the stats panel. If no token arrives, `ttft_ms` can be `null`.
+
+## Steps
+
+1. Add failing runtime tests for:
+   - `round_stats` is emitted after a model turn;
+   - TTFT and elapsed are derived from the injected clock;
+   - usage totals are included;
+   - multi-turn event handoff emits one stats object per ChatAgent call.
+2. Refactor `DebugRuntime` to collect and emit stats through the existing output queue.
+3. Update `app.js` to update stats from `round_stats` messages and keep current browser timing only as a fallback before stats arrives.
+4. Add or update static JS tests/assertions for `round_stats` handling.
+5. Run:
+
+```bash
+uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py
+uv run pytest
+```
+
+6. If successful, commit implementation files only with:
+
+```bash
+git commit -m "feat: emit structured round stats"
+```
+
+## Verification
+
+Result:
+
+- `uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py` passed: 23 tests, 1 existing Starlette deprecation warning.
+- `uv run pytest` passed: 28 tests, 1 existing Starlette deprecation warning.
+
+## Evaluation
+
+- Backend emits deterministic `round_stats` messages with round index, TTFT, elapsed time, token totals, cached tokens, and event presence.
+- UI handles backend `round_stats` and keeps browser-side timing as a fallback.
+- The original MVP requirement for chat-window round statistics is covered.

+ 105 - 0
docs/plans/todo-13-real-queue-websocket-loop.md

@@ -0,0 +1,105 @@
+# Todo 13 Real Queue WebSocket Loop Plan
+
+**Status:** done
+
+## Goal
+
+Make runtime queues real producer/consumer boundaries and let WebSocket run separate upstream and downstream tasks.
+
+## Why
+
+The original queue requirement names producers and consumers explicitly:
+
+- input queue: WebSocket upstream and EventAgent replies produce; ChatAgent consumes.
+- output queue: ChatAgent produces; WebSocket downstream consumes.
+- event queue: ChatAgent produces; EventAgent consumes.
+
+The current runtime logs queue put/get calls, but `DebugRuntime.run()` still produces and consumes output in one method, and WebSocket reads only one initial request.
+
+## Scope
+
+This todo covers:
+
+- Add a runtime session entrypoint that starts ChatAgent/EventAgent work and returns `RuntimeQueues`.
+- Keep `DebugRuntime.run()` as a compatibility wrapper for tests and scripts.
+- Add an EventAgent queue consumer task.
+- Preserve provider-valid history ordering when a user message arrives while a tool call is being handled.
+- Split the WebSocket handler into downstream output sending and upstream user-message receiving tasks.
+
+This todo does not cover:
+
+- Multi-session persistence.
+- Multiple simultaneous ChatAgent workers inside one debug session.
+- UI controls for sending a second user message during an active run.
+- Role validation and static asset packaging; those are separate todos.
+
+## Files
+
+- Modify: `src/agent_lab/application/queues.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/presentation/web.py`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `tests/test_websocket_api.py`
+- Do not modify unrelated UI styling.
+
+## Design
+
+Add `DebugRuntime.start(request) -> RuntimeQueues`. It should create or reuse `RuntimeQueues`, seed the initial user message into `queues.input`, start background runtime work, and return queues immediately.
+
+`DebugRuntime.run(request)` remains an async iterator by calling `start()` and then consuming `queues.output` until `done` or `error`.
+
+Runtime work should:
+
+- put output payloads onto `queues.output` only;
+- put ChatAgent events onto `queues.events`;
+- run an EventAgent worker that consumes `queues.events` and puts tool replies onto `queues.input`;
+- when waiting for a tool reply, buffer unrelated input messages so the next provider history is `assistant(tool_calls)` -> `tool` -> later `user`.
+
+WebSocket handling should:
+
+- read the first JSON payload as `DebugRunRequest`;
+- start the runtime session;
+- run a downstream task that consumes `queues.output` and sends JSON;
+- run an upstream task that accepts `{"type": "user_message", "content": "..."}` and puts `ChatMessage(role="user", content=...)` into `queues.input`.
+
+## Steps
+
+1. Add failing runtime tests:
+   - `DebugRuntime.start()` returns queues and output can be consumed directly.
+   - User input enqueued during an event handoff is preserved after the matching tool reply.
+2. Add a failing WebSocket test:
+   - send initial request;
+   - receive an event;
+   - send `{"type": "user_message", "content": "follow-up"}`;
+   - assert the second ChatAgent call receives provider-valid ordering.
+3. Refactor `DebugRuntime` to add `start()`, output queue consumption, EventAgent worker, and tool-reply wait logic.
+4. Refactor WebSocket presentation code into separate upstream/downstream tasks for runtimes that expose queue sessions.
+5. Run focused tests:
+
+```bash
+uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py
+```
+
+6. Run the full suite:
+
+```bash
+uv run pytest
+```
+
+7. Update `docs/plans/todos.md` and this plan with the result, then commit.
+
+## Verification
+
+Result:
+
+- Red run passed as expected: the new tests failed because `DebugRuntime.start()` did not exist, event-time user input was ordered before the tool reply, and WebSocket still called only `run()`.
+- `uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py` passed: 26 tests, 1 existing Starlette deprecation warning.
+- `uv run pytest` passed: 31 tests, 1 existing Starlette deprecation warning.
+
+## Evaluation
+
+- `DebugRuntime.start()` now starts a queue-backed session and returns `RuntimeQueues` for downstream consumers.
+- `DebugRuntime.run()` remains as a compatibility wrapper over the output queue.
+- EventAgent now consumes the event queue in a worker task and writes tool replies to the input queue.
+- WebSocket sessions now split downstream output sending from upstream `user_message` receiving.
+- User messages that arrive during a tool handoff are buffered until after the matching tool reply, preserving provider-valid message order.

+ 63 - 0
docs/plans/todo-14-message-role-validation.md

@@ -0,0 +1,63 @@
+# Todo 14 Message Role Validation Plan
+
+**Status:** done
+
+## Goal
+
+Reject provider-invalid API message roles and malformed tool replies before runtime work or network calls.
+
+## Why
+
+The UI no longer exposes `tool` pre-messages, and the OpenAI client rejects tool messages without `tool_call_id`. API callers can still send arbitrary `role` values in `pre_messages`, which can fail later at the provider boundary. The request contract should fail fast.
+
+## Scope
+
+This todo covers:
+
+- Constrain `ChatMessage.role` to Chat Completions-compatible roles.
+- Reject `pre_messages` with `role="tool"` in `DebugRunRequest`, because request pre-messages cannot provide a matching assistant `tool_calls` context safely.
+- Preserve internally generated EventAgent tool replies.
+- Keep OpenAI client payload serialization tests passing.
+
+This todo does not cover:
+
+- Full arbitrary conversation transcript validation.
+- UI changes.
+- Tool handler exception isolation.
+
+## Files
+
+- Modify: `src/agent_lab/domain/messages.py`
+- Modify: `src/agent_lab/application/contracts.py`
+- Modify: `tests/test_websocket_api.py` or add focused contract tests.
+
+## Steps
+
+1. Add failing tests:
+   - invalid pre-message role is rejected by `DebugRunRequest.model_validate()`;
+   - `pre_messages` with `role="tool"` is rejected before runtime;
+   - internally created `ChatMessage(role="tool", tool_call_id="...")` remains valid.
+2. Implement a `Literal` role type in `ChatMessage`.
+3. Add a `DebugRunRequest` validator that rejects tool pre-messages.
+4. Run:
+
+```bash
+uv run pytest tests/test_websocket_api.py tests/test_event_agent.py
+uv run pytest
+```
+
+5. Update this plan and `docs/plans/todos.md`, then commit.
+
+## Verification
+
+Result:
+
+- Red run passed as expected: invalid role and tool pre-message tests failed before implementation.
+- `uv run pytest tests/test_websocket_api.py tests/test_event_agent.py` passed: 21 tests, 1 existing Starlette deprecation warning.
+- `uv run pytest` passed: 34 tests, 1 existing Starlette deprecation warning.
+
+## Evaluation
+
+- `ChatMessage.role` now accepts only `system`, `user`, `assistant`, or `tool`.
+- `DebugRunRequest` rejects `tool` entries in `pre_messages`, so API callers cannot send orphan tool replies as front-loaded context.
+- Internally generated EventAgent tool replies still validate and serialize correctly.

+ 67 - 0
docs/plans/todo-15-static-package-data.md

@@ -0,0 +1,67 @@
+# Todo 15 Static Package Data Plan
+
+**Status:** done
+
+## Goal
+
+Include the static debug UI assets in installed package builds.
+
+## Why
+
+The FastAPI app serves `index.html`, `app.js`, and `styles.css` from `agent_lab.presentation.static`. Local source runs work because those files are present in the checkout. A built wheel needs explicit setuptools package-data configuration so installed console usage can still serve the UI.
+
+## Scope
+
+This todo covers:
+
+- Add package-data configuration for static HTML, JS, and CSS files.
+- Add a focused test that prevents removing this packaging rule.
+- Build the package and inspect the wheel contents.
+
+This todo does not cover:
+
+- Changing runtime static path lookup.
+- Adding new frontend assets.
+- Production server entrypoint behavior.
+
+## Files
+
+- Modify: `pyproject.toml`
+- Add/modify: `tests/test_packaging.py`
+- Modify: `docs/plans/todos.md`
+- Modify: this plan
+
+## Steps
+
+1. Add a failing test that reads `pyproject.toml` and asserts `agent_lab.presentation` includes `static/*.html`, `static/*.js`, and `static/*.css` package data.
+2. Add `[tool.setuptools.package-data]` to `pyproject.toml`.
+3. Run:
+
+```bash
+uv run pytest tests/test_packaging.py
+uv run pytest
+```
+
+4. Build and inspect package contents:
+
+```bash
+uv build
+unzip -l dist/agent_lab-0.1.0-py3-none-any.whl | rg "agent_lab/presentation/static/(index.html|app.js|styles.css)"
+```
+
+5. Update this plan and `docs/plans/todos.md`, then commit.
+
+## Verification
+
+Result:
+
+- Red run passed as expected: `tests/test_packaging.py` failed because `tool.setuptools.package-data` was missing.
+- `uv run pytest tests/test_packaging.py` passed.
+- `uv run pytest` passed: 35 tests, 1 existing Starlette deprecation warning.
+- `uv build` passed after rerunning with permission to access the user uv cache.
+- `unzip -l dist/agent_lab-0.1.0-py3-none-any.whl | rg "agent_lab/presentation/static/(index.html|app.js|styles.css)"` found all three static UI assets.
+
+## Evaluation
+
+- `pyproject.toml` now declares package data for static HTML, JS, and CSS.
+- The built wheel contains `agent_lab/presentation/static/index.html`, `app.js`, and `styles.css`.

+ 65 - 0
docs/plans/todo-16-tool-error-isolation.md

@@ -0,0 +1,65 @@
+# Todo 16 Tool Error Isolation Plan
+
+**Status:** done
+
+## Goal
+
+Convert EventAgent tool handler exceptions into structured tool-result errors instead of aborting or hanging the debug session.
+
+## Why
+
+Dynamic EventAgent tools will eventually call real code. A single tool exception should be visible to the ChatAgent as a tool reply, not break the WebSocket session or leave the runtime waiting forever for a tool response.
+
+## Scope
+
+This todo covers:
+
+- Catch `Exception` from registry tool handlers.
+- Return a `ChatMessage(role="tool")` containing a JSON error payload.
+- Prove runtime continues to the next ChatAgent call after a failing tool.
+
+This todo does not cover:
+
+- Retry policies.
+- Tool timeout controls.
+- Full traceback exposure.
+
+## Files
+
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `tests/test_event_agent.py`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `docs/plans/todos.md`
+- Modify: this plan
+
+## Steps
+
+1. Add failing EventAgent test for a registry handler that raises `RuntimeError("boom")`.
+2. Add failing runtime test proving a failing handler emits `tool_result` and reaches `done`.
+3. Catch handler exceptions in `EventAgent.handle()` and return payload:
+
+```json
+{"tool": "handoff_note", "error": "tool handler failed: boom"}
+```
+
+4. Run:
+
+```bash
+uv run pytest tests/test_event_agent.py tests/test_debug_runtime.py
+uv run pytest
+```
+
+5. Update this plan and `docs/plans/todos.md`, then commit.
+
+## Verification
+
+Result:
+
+- Red run passed as expected: direct EventAgent raised `RuntimeError("boom")`, and runtime output collection timed out waiting for a missing tool reply.
+- `uv run pytest tests/test_event_agent.py tests/test_debug_runtime.py` passed: 14 tests.
+- `uv run pytest` passed: 37 tests, 1 existing Starlette deprecation warning.
+
+## Evaluation
+
+- `EventAgent.handle()` now catches tool handler `Exception` and returns a structured tool reply with `{"tool": name, "error": "tool handler failed: ..."}`.
+- Runtime receives that reply through the input queue, emits `tool_result`, and continues to the next ChatAgent call.

+ 41 - 0
docs/plans/todo-17-agent-config-audit-tools.md

@@ -0,0 +1,41 @@
+# Todo 17 Agent Config, Audit, and Mock Tools Plan
+
+**Status:** done
+
+## Goal
+
+Align ChatAgent/EventAgent config, add extra provider body defaults, default EventAgent to one round, add mock tools, audit events, and backend logger calls.
+
+## Design
+
+- `AgentParams` becomes the shared model for model, temperature, max tokens, system prompt, and `extra_body`.
+- `EventAgentParams` extends shared agent params with enabled tools and `max_event_loops`, defaulting to one round.
+- `OpenAICompatibleChatClient` merges `extra_body` into the request payload.
+- `ToolRegistry` exposes several mock tools for the UI while still giving ChatAgent name/description-only event tools.
+- `DebugRuntime` emits `audit` output events for key pipeline transitions and logs them through `logging`.
+
+## Files
+
+- Modify: `src/agent_lab/application/contracts.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/application/tools.py`
+- Modify: `src/agent_lab/infrastructure/chat_client.py`
+- Modify tests in `tests/test_debug_runtime.py`, `tests/test_websocket_api.py`, and `tests/test_ws_smoke.py`.
+
+## Verification
+
+- Focused backend tests prove:
+  - ChatAgent and EventAgent share agent config fields.
+  - `extra_body` is merged into provider payloads.
+  - EventAgent defaults to one round.
+  - `/api/tools` exposes multiple mock tools.
+  - Runtime emits audit events and logger records key events.
+- Full suite passes.
+
+## Result
+
+- Added shared ChatAgent/EventAgent config fields: `system_prompt` and default `extra_body`.
+- Set EventAgent to one event round by default.
+- Added `mock_search` and `mock_ticket` to the default tool registry.
+- Added runtime audit output events and logger entries for session, round, event, and EventAgent transitions.
+- Verified with `uv run pytest` (`42 passed`, one existing Starlette deprecation warning).

+ 36 - 0
docs/plans/todo-18-prompt-modal-tool-ui.md

@@ -0,0 +1,36 @@
+# Todo 18 Prompt Modal and Tool UI Plan
+
+**Status:** done
+
+## Goal
+
+Move prompt editing into a modal, expose matching ChatAgent/EventAgent controls, and make tools/audit activity visible in the frontend.
+
+## Design
+
+- Keep existing prompt-set, system prompt, and pre-message DOM ids so saved prompt behavior remains stable.
+- Add a top-level Prompt Config button that opens a native `<dialog>`.
+- Add matching ChatAgent/EventAgent controls for model, temperature, max tokens, system prompt, and `extra_body`.
+- Render tool metadata from `/api/tools` as selectable rows with description and required parameter names.
+- Render backend `audit` events in the transcript as lightweight chain logs.
+
+## Files
+
+- Modify: `src/agent_lab/presentation/static/index.html`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify: `src/agent_lab/presentation/static/styles.css`
+- Modify: `tests/test_websocket_api.py`
+- Modify: `docs/plans/todos.md`
+
+## Verification
+
+- Static tests prove modal hooks, matching agent controls, tool metadata UI, and audit handling are wired.
+- Full suite passes with `uv run pytest`.
+
+## Result
+
+- Moved prompt workspace controls into a native modal opened by `Prompt Config`.
+- Added matching ChatAgent/EventAgent fields for model, temperature, max tokens, system prompt, and extra body.
+- Rendered tool cards with descriptions, required parameter metadata, selected count, and select-all/clear actions.
+- Added frontend handling for backend `audit` events.
+- Verified with focused static tests, `uv run pytest` (`46 passed`), and a local browser check at `http://127.0.0.1:8001`.

+ 41 - 0
docs/plans/todo-19-latency-oriented-polish.md

@@ -0,0 +1,41 @@
+# Todo 19 Latency-Oriented Polish Plan
+
+**Status:** done
+
+## Goal
+
+Reduce perceived reply wait time with earlier event visibility and clearer frontend waiting feedback.
+
+## Design
+
+- Emit `event` output as soon as ChatAgent streaming produces an event, instead of waiting for the full model stream to finish.
+- Keep EventAgent execution after the assistant turn is finalized so history remains provider-compatible.
+- Show the user's submitted message immediately, disable the run button while streaming, and update elapsed time while waiting for the first token.
+- Add lightweight WebSocket logger entries for backend session boundaries.
+
+## Files
+
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/presentation/web.py`
+- Modify: `src/agent_lab/presentation/static/index.html`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify: `src/agent_lab/presentation/static/styles.css`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `tests/test_websocket_api.py`
+- Modify: `docs/plans/todos.md`
+
+## Verification
+
+- Runtime test proves event messages reach the output queue before the ChatAgent stream is released.
+- WebSocket test proves backend logger records session lifecycle events.
+- Static UI tests prove immediate user echo, waiting state, disabled run button, and live elapsed timer hooks.
+- Full suite passes with `uv run pytest`.
+
+## Result
+
+- Runtime now emits `event` output immediately when the ChatAgent stream detects an event.
+- WebSocket service logs accepted, validated, terminal, rejected, failed, disconnected, and closed lifecycle states.
+- Frontend now echoes the submitted user message immediately, disables Run while active, shows "Waiting for first token", and keeps elapsed time moving before the first token.
+- Review follow-up: elapsed no longer freezes when an event-only first round emits `round_stats` before the first assistant token.
+- Tools management is moved to the top of the sidebar so it is visible in the first viewport.
+- Verified with focused tests, `uv run pytest` (`49 passed`), and local browser checks at `http://127.0.0.1:8001`.

+ 85 - 0
docs/plans/todo-2-domain-runtime.md

@@ -0,0 +1,85 @@
+# Todo 2 Domain Runtime Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and locally verified with:
+
+```bash
+uv run pytest tests/test_openai_stream_parser.py tests/test_event_agent.py tests/test_debug_runtime.py
+```
+
+Result: `3 passed in 0.07s`.
+
+Evaluation note: the runtime currently streams message deltas to the caller but does not yet persist the aggregated assistant message into conversation history. Todo 3 should cover transcript persistence when wiring the WebSocket/API surface.
+
+## Goal
+
+Implement the core domain/runtime loop that passes the existing Todo 1 tests without adding WebSocket, FastAPI routes, or UI.
+
+## Scope
+
+This todo covers:
+
+- Domain objects for chat messages, tool-call events, stream items, and usage.
+- Application contracts for debug run requests and agent parameters.
+- EventAgent execution for enabled tools.
+- OpenAI Chat Completions compatible stream parsing.
+- DebugRuntime loop that routes ChatAgent tool events through EventAgent and resumes ChatAgent with tool replies.
+
+This todo does not cover:
+
+- Real HTTP calls to an LLM endpoint.
+- pydantic-settings config.
+- WebSocket protocol.
+- Static frontend.
+
+## Files
+
+- Create: `src/agent_lab/domain/events.py`
+- Create: `src/agent_lab/domain/messages.py`
+- Create: `src/agent_lab/application/contracts.py`
+- Create: `src/agent_lab/application/event_agent.py`
+- Create: `src/agent_lab/application/runtime.py`
+- Create: `src/agent_lab/infrastructure/openai_compatible.py`
+- Do not modify: `docs/plans/todos.md`
+- Avoid modifying tests unless a test is internally inconsistent.
+
+## Design
+
+Use small Pydantic models for contracts that cross application boundaries and dataclasses for internal stream items. Keep tool handling intentionally simple:
+
+- `ToolCallEvent.name` is the ChatAgent tool name.
+- EventAgent treats `ToolCallEvent.name` as the tool identifier.
+- The MVP ships one built-in event tool, `handoff_note`, that returns a JSON tool reply.
+- Disabled tools return a JSON error tool reply instead of raising.
+
+`DebugRuntime.run()` is an async generator. It builds initial messages from system prompts, pre-messages, and the user message. It calls `chat_client.stream_chat()`, yields message deltas and events, sends events to EventAgent, appends the returned tool message, and calls ChatAgent again until no event is produced or `max_event_loops` is reached.
+
+## Steps
+
+1. Implement domain models in `events.py` and `messages.py`.
+2. Implement request/parameter contracts in `contracts.py`.
+3. Implement `EventAgent.handle()`.
+4. Implement `ChatCompletionStreamParser.feed()` for OpenAI-compatible streamed chunks:
+   - content deltas become `StreamItem.message_delta`.
+   - tool call chunks are accumulated by `index`.
+   - on `finish_reason == "tool_calls"`, emit `StreamItem.event` for each completed tool call.
+   - usage becomes `StreamItem.usage`.
+5. Implement `DebugRuntime.run()` around the chat client protocol.
+6. Run `uv run pytest tests/test_openai_stream_parser.py tests/test_event_agent.py tests/test_debug_runtime.py`.
+7. Fix only Todo 2 scope failures.
+
+## Verification
+
+Run:
+
+```bash
+uv run pytest tests/test_openai_stream_parser.py tests/test_event_agent.py tests/test_debug_runtime.py
+```
+
+Expected result:
+
+- 3 tests pass.
+- No FastAPI/WebSocket/UI code is required for this todo.

+ 25 - 0
docs/plans/todo-20-agent-config-modals.md

@@ -0,0 +1,25 @@
+# Todo 20 Agent Config Modals Plan
+
+**Status:** done
+
+## Goal
+
+Move ChatAgent and EventAgent settings out of the crowded sidebar into two separate modal dialogs.
+
+## Scope
+
+- Keep the existing request payload field ids and JavaScript data flow.
+- Add separate ChatAgent and EventAgent config entry buttons.
+- Move each agent's fields into its own `<dialog>`.
+- Keep Tools visible in the sidebar.
+
+## Verification
+
+- Static tests assert both modal dialogs, open buttons, close buttons, and JavaScript modal calls exist.
+- `uv run pytest` passes.
+
+## Evaluation
+
+- The sidebar now focuses on tools and agent-config entry points.
+- ChatAgent and EventAgent no longer compete for vertical space in the left panel.
+- Existing prompt-set save/load and WebSocket request building continue to use the same field ids.

+ 30 - 0
docs/plans/todo-21-event-agent-context-summary.md

@@ -0,0 +1,30 @@
+# Todo 21 EventAgent Context Summary Plan
+
+**Status:** done
+
+## Goal
+
+Make EventAgent configuration part of tool-parameter generation and send a combined EventAgent result summary into the ChatAgent input queue.
+
+## Why
+
+The UI and contracts already expose EventAgent `system_prompt` and `extra_body`, but parameter generation currently only receives event/history. Also, individual tool replies are provider-compatible, but ChatAgent should get a concise aggregate result from EventAgent after all tools in the current event round finish.
+
+## Scope
+
+- Add an EventAgent execution context containing history, system prompt, and extra body.
+- Pass EventAgent config from `DebugRuntime` into `EventAgentRequest`.
+- Keep individual `tool` replies for Chat Completions compatibility.
+- Add one aggregate `user` message after tool replies so the next ChatAgent round sees the combined EventAgent result.
+
+## Verification
+
+- EventAgent tests prove custom resolvers receive `system_prompt` and `extra_body`.
+- Runtime tests prove the second ChatAgent round contains tool replies followed by the aggregate summary.
+- `uv run pytest` passed: 50 tests, 1 existing Starlette deprecation warning.
+
+## Result
+
+- Added `ToolExecutionContext` for EventAgent parameter generation.
+- Passed EventAgent `system_prompt` and `extra_body` from `DebugRuntime` into EventAgent requests.
+- Kept individual provider-compatible `tool` replies and added one aggregate `EventAgent results` user message for the next ChatAgent round.

+ 34 - 0
docs/plans/todo-22-text-event-protocol.md

@@ -0,0 +1,34 @@
+# Todo 22 Text Event Protocol Plan
+
+**Status:** done
+
+## Goal
+
+Remove ChatAgent dependence on provider `tool_calls`, parse event names from streamed text, and fix Agent config buttons that could stop responding after stale script/HTML mismatches.
+
+## Why
+
+The target architecture keeps ChatAgent focused on fast user-facing replies. EventAgent extends external capability after ChatAgent names the needed events. Provider tool-call payloads force ChatAgent to know tool-call mechanics and can delay or complicate streaming.
+
+## Scope
+
+- Parse a small text protocol from `delta.content`: visible text streams to the UI, while names inside `<agent_events>...</agent_events>` become internal `ToolCallEvent` values.
+- Stop passing `tools` to ChatAgent and stop adding assistant `tool_calls` or `tool` role replies to ChatAgent history.
+- Insert a generated system message listing enabled event names and descriptions for each ChatAgent round that can still request events.
+- Keep EventAgent tool results visible to the UI and pass only one aggregate `EventAgent results` user message back to ChatAgent.
+- Version the static JS URL and guard click binding so missing/old buttons do not abort initialization.
+
+## Verification
+
+- Parser test proves text events are extracted from streamed content and hidden from displayed message deltas.
+- Runtime tests prove ChatAgent receives no tools, no `tool_calls`, no `tool` history, and does receive the event catalog system message plus aggregate EventAgent summary.
+- Static tests prove the app script is versioned and Agent config buttons use guarded click binding.
+- `uv run pytest` passed: 50 tests, 1 existing Starlette deprecation warning.
+- Local browser check confirmed one ChatAgent button, one EventAgent button, versioned `app.js`, and both dialogs open on click.
+
+## Result
+
+- Added streamed text event parsing around `<agent_events>`.
+- Replaced ChatAgent tool schemas with a generated event catalog system message.
+- Preserved EventAgent execution and UI tool-result visibility while simplifying ChatAgent history.
+- Fixed the practical button failure mode caused by stale or mismatched static JS.

+ 40 - 0
docs/plans/todo-23-agent-prompt-and-event-llm.md

@@ -0,0 +1,40 @@
+# Todo 23 Agent Prompt and EventAgent LLM Plan
+
+**Status:** done
+
+## Goal
+
+Move prompt configuration into Agent configuration and make EventAgent use a real LLM tool-call step to generate tool parameters.
+
+## Why
+
+Prompt Config is a separate modal while ChatAgent already owns the prompt that shapes visible replies. This splits one concept across two places. EventAgent also still resolves mock parameters locally; the target design requires EventAgent to use conversation context plus the selected event tool schema to ask the LLM for real arguments before executing tools.
+
+## Scope
+
+- Remove the standalone Prompt Config button and modal.
+- Move prompt set management, system prompts, and pre-messages into the ChatAgent config modal.
+- Show the generated available-event protocol prompt in ChatAgent prompt by default.
+- Keep ChatAgent free of provider tools.
+- Restore provider `tool_calls` parsing for EventAgent LLM parameter generation only.
+- Make EventAgent call the LLM with only the corresponding event tool schema, then execute the returned arguments through the registry.
+- Run multiple EventAgent events concurrently.
+
+## Implementation
+
+- Removed the standalone Prompt Config modal and moved prompt-set, system prompt, and pre-message controls into ChatAgent config.
+- Added a generated available-event protocol prompt from the selected tools and populated it into ChatAgent system prompt by default.
+- Kept ChatAgent provider calls tool-free while preserving text event extraction from `delta.content`.
+- Restored provider `tool_calls` parsing for EventAgent LLM requests.
+- EventAgent now calls the LLM with exactly the selected event tool schema, then executes the returned arguments.
+- EventAgent handles multiple events concurrently with `asyncio.gather`.
+
+## Verification
+
+- Parser test proves provider `tool_calls` still become internal events for EventAgent.
+- EventAgent tests prove it calls the LLM with the target tool schema, passes history/config, and executes generated arguments.
+- Static UI tests prove Prompt Config is gone and prompt controls live in ChatAgent config.
+- Runtime tests continue to prove ChatAgent receives no tools or `tool` role history.
+- `uv run pytest` passes (`51 passed`, one existing Starlette deprecation warning).
+- `UV_CACHE_DIR=.uv-cache uv run python -m py_compile ...` passes for the changed Python modules.
+- Local server started and returned the page, static assets, and `/api/tools`; browser automation timed out during click verification, so modal click behavior is covered by static binding tests rather than a completed browser click run.

+ 25 - 0
docs/plans/todo-24-chat-agent-prompt-list.md

@@ -0,0 +1,25 @@
+# Todo 24 ChatAgent Prompt List Plan
+
+**Status:** done
+
+## Goal
+
+Put all ChatAgent prompts into one ordered list so configuration is not split between a standalone ChatAgent system prompt, generated event instructions, and prompt-config system prompts.
+
+## Why
+
+ChatAgent depends on the event-instruction prompt to know when and how to emit event names. Keeping that prompt outside the editable prompt sequence makes ordering unclear and makes debugging prompt effects harder.
+
+## Scope
+
+- Replace the standalone ChatAgent System Prompt field and Available Events Prompt preview with one prompt list.
+- Include the generated EventAgent event-rule prompt as a fixed item in that list.
+- Allow custom prompt items to be added, deleted, and reordered by dragging.
+- Serialize `system_prompts` from the ordered list and stop sending a separate `chat_agent.system_prompt`.
+- Preserve existing prompt-set save/load behavior for user-created prompts.
+
+## Verification
+
+- Static tests failed before implementation and pass after implementation.
+- `uv run pytest` passed: 51 tests, 1 existing Starlette deprecation warning.
+- Browser check confirmed the ChatAgent config shows one prompt list, supports add/delete/reorder controls, includes the generated event-rule prompt in the list, and no longer shows separate ChatAgent prompt/event-preview fields.

+ 23 - 0
docs/plans/todo-25-prompt-list-message-types.md

@@ -0,0 +1,23 @@
+# Todo 25: Prompt List Message Types
+
+## Status
+
+done
+
+## Goal
+
+The ChatAgent prompt list should be easier to scan and each item should clearly show its message type. For this iteration, prompt list items remain ChatAgent `system` messages; the UI must make that explicit instead of only showing "Custom Prompt".
+
+## Scope
+
+- Add a visible `type: system` marker to each ChatAgent prompt item.
+- Keep the generated EventAgent event-rules prompt in the same ordered list.
+- Make the item header, action buttons, and text area more compact inside the modal.
+- Preserve existing prompt save/load, add/delete, and drag ordering behavior.
+
+## Verification
+
+- Static tests assert that prompt items expose message type markup and compact styling hooks.
+- `uv run pytest` passes: 51 tests.
+- Local HTTP check returned `/health` and `/static/app.js?v=20260706-prompt-types`.
+- Browser automation was attempted but the in-app browser connection timed out, and direct Playwright did not have a browser binary installed in this environment.

+ 27 - 0
docs/plans/todo-26-workspace-snapshot-save.md

@@ -0,0 +1,27 @@
+# Todo 26: Workspace Snapshot Save
+
+## Status
+
+done
+
+## Goal
+
+Move save/load/delete controls out of the ChatAgent config modal and make their meaning explicit: a saved item is a workspace snapshot, not only a prompt set.
+
+## Root Cause
+
+The existing save feature still works, but it is hidden inside the ChatAgent config dialog and named as a prompt set. The saved payload already includes prompt items, pre-messages, ChatAgent config, EventAgent config, selected tools, and event loop settings, so the UI label and placement no longer match the actual behavior.
+
+## Scope
+
+- Add a visible Workspace Snapshot section in the left panel.
+- Remove the save/load/delete controls from the ChatAgent config modal.
+- Rename browser-side storage helpers from prompt-set wording to workspace-snapshot wording.
+- Keep backward compatibility for existing `agent-lab.prompt-sets.v1` localStorage data.
+- Preserve the request payload behavior used by chat runs.
+
+## Verification
+
+- Static tests prove snapshot controls are outside the ChatAgent dialog and save the same workspace fields used by `buildRequest()`.
+- `uv run pytest tests/test_websocket_api.py -q` passes: 27 tests.
+- `uv run pytest`

+ 33 - 0
docs/plans/todo-27-event-agent-tool-arguments.md

@@ -0,0 +1,33 @@
+# Todo 27: EventAgent Tool Arguments
+
+## Status
+
+done
+
+## Goal
+
+Stop EventAgent turns from ending with `event agent did not return arguments for mock_search` when the compatible provider fails to return a tool call in the exact shape the parser expects.
+
+## Root Cause
+
+EventAgent currently sends one tool schema to the LLM and expects the stream parser to emit a provider tool-call event. There are two brittle points:
+
+- The OpenAI-compatible request includes `tools` but does not force the single tool through `tool_choice`.
+- The stream parser only drains accumulated provider tool calls when `finish_reason == "tool_calls"`, so providers that end with another terminal reason can drop the accumulated tool call.
+
+If neither path emits an event, EventAgent raises a ValueError and the tool result becomes a handler error.
+
+## Scope
+
+- Force the single supplied tool in OpenAI-compatible requests.
+- Drain accumulated provider tool calls on any terminal finish reason.
+- Add an EventAgent fallback to registry/context argument resolution when the LLM returns no tool call.
+- Keep ChatAgent tool-free.
+
+## Verification
+
+- Failing parser test for terminal `finish_reason="stop"` with accumulated tool call.
+- Failing OpenAI client payload test for `tool_choice`.
+- Failing EventAgent test for no-tool-call fallback.
+- `uv run pytest tests/test_openai_stream_parser.py tests/test_event_agent.py tests/test_websocket_api.py::test_openai_chat_client_streams_sse_chunks_through_parser -q` passes: 9 tests.
+- `uv run pytest` passes: 53 tests.

+ 30 - 0
docs/plans/todo-28-session-turns.md

@@ -0,0 +1,30 @@
+# Todo 28: Session Turns
+
+## Status
+
+done
+
+## Goal
+
+Separate session and turn behavior. A WebSocket session is the ongoing conversation. A turn starts with one user message and finishes after ChatAgent has produced the visible reply, including any EventAgent handoff and final ChatAgent response.
+
+## Root Cause
+
+The UI currently closes and recreates the WebSocket for every submit. The backend runtime also treats `done` as the end of the whole run after one user message. `round_index` is a ChatAgent call counter, so the post-event ChatAgent call appears as a second round with `events_enabled=[]`, which is expected for a one-event-loop turn but confusing without turn boundaries.
+
+## Scope
+
+- Add a persistent runtime session entrypoint for WebSocket use.
+- Emit `turn_started` and `turn_completed` events.
+- Reset EventAgent event-loop budget for each new user turn.
+- Keep post-event final ChatAgent calls inside the same turn.
+- Keep existing one-shot `runtime.run()` behavior for tests and smoke helpers.
+- Reuse an open WebSocket from the frontend instead of closing it on every submit.
+
+## Verification
+
+- Runtime test proves a second user turn gets enabled events again.
+- WebSocket test proves one connection can process two user messages.
+- Static UI test proves the browser sends `user_message` on an open socket and handles `turn_completed`.
+- `uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py -q` passes: 45 tests.
+- `uv run pytest` passes: 56 tests.

+ 24 - 0
docs/plans/todo-29-rich-audit-log.md

@@ -0,0 +1,24 @@
+# Todo 29: Rich Audit Log
+
+## Status
+
+done
+
+## Goal
+
+Make the debug console audit trail useful for real LLM debugging. A user should be able to see which model parameters and prompts/messages were sent, what text/events/results came back, and what usage was reported.
+
+## Scope
+
+- Add ChatAgent request audit details: agent, turn/round, model params, messages, and tools.
+- Add ChatAgent response audit details: text content, event names, and usage.
+- Add EventAgent request audit details: agent params, requested events, history, system prompt, extra body, and tool schemas.
+- Add EventAgent response audit details: tool reply messages.
+- Keep API keys and Authorization headers out of audit output.
+
+## Verification
+
+- Runtime tests assert audit payloads contain model params, prompt/messages, event/tool result data, and usage.
+- Existing audit/log tests still pass with the richer event stream.
+- `uv run pytest tests/test_debug_runtime.py -q` passes: 17 tests.
+- `uv run pytest` passes: 57 tests.

+ 121 - 0
docs/plans/todo-3-websocket-api.md

@@ -0,0 +1,121 @@
+# Todo 3 WebSocket API and Static UI Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and locally verified.
+
+Commands:
+
+```bash
+uv run pytest
+uv run python -c "from agent_lab.main import app; print(app.title)"
+```
+
+Results:
+
+- `uv run pytest`: `8 passed, 1 warning in 0.24s`
+- import smoke: `Agent Lab`
+
+Evaluation note: real LLM configuration already exists through `Settings`, so Todo 4 should focus on documentation and a minimal manual smoke helper rather than adding another runtime layer.
+
+## Goal
+
+Expose the domain runtime through FastAPI WebSocket and serve a usable static debug console.
+
+## Scope
+
+This todo covers:
+
+- FastAPI app factory and HTTP index route.
+- WebSocket endpoint for one debug run per request.
+- OpenAI Chat Completions compatible HTTP client for streaming completions.
+- Assistant message transcript persistence during runtime loops.
+- Static HTML/CSS/JS page without a frontend build chain.
+- Contract tests using FastAPI `TestClient` and a fake runtime/client where possible.
+
+This todo does not cover:
+
+- Production auth.
+- Multi-user session storage.
+- Redis/NATS/external queues.
+- Full visual polish.
+- Final README/env docs; those stay in Todo 4.
+
+## Files
+
+- Modify: `src/agent_lab/application/runtime.py`
+- Create: `src/agent_lab/infrastructure/chat_client.py`
+- Create: `src/agent_lab/settings.py`
+- Create: `src/agent_lab/main.py`
+- Create: `src/agent_lab/presentation/web.py`
+- Create: `src/agent_lab/presentation/static/index.html`
+- Create: `src/agent_lab/presentation/static/app.js`
+- Create: `src/agent_lab/presentation/static/styles.css`
+- Create: `tests/test_websocket_api.py`
+- Update: `docs/plans/todos.md` after evaluation only.
+
+## Protocol
+
+Frontend sends one JSON object to `/ws/debug`:
+
+```json
+{
+  "user_message": "debug this",
+  "system_prompts": ["You are a debugger."],
+  "pre_messages": [],
+  "chat_agent": {
+    "model": "gpt-4.1-mini",
+    "temperature": 0.2,
+    "max_tokens": 800
+  },
+  "event_agent": {
+    "enabled_tools": ["handoff_note"],
+    "max_event_loops": 3
+  }
+}
+```
+
+Server sends newline-independent JSON WebSocket messages already produced by `DebugRuntime.run()`, plus an `error` message on validation/runtime failures.
+
+## Steps
+
+1. Add tests for `/health` and `/ws/debug` with a fake runtime or fake chat client.
+2. Update `DebugRuntime` to aggregate streamed assistant content and append assistant messages to history when a model turn completes.
+3. Implement `Settings` with pydantic-settings fields:
+   - `openai_api_key`
+   - `openai_base_url`
+   - `openai_default_model`
+   - `request_timeout_seconds`
+4. Implement `OpenAICompatibleChatClient` using `httpx.AsyncClient.stream()` against `/chat/completions`, with `stream: true`, configured model params, tool definitions, and `ChatCompletionStreamParser`.
+5. Implement FastAPI app factory and routes:
+   - `GET /health`
+   - `GET /`
+   - `WebSocket /ws/debug`
+6. Implement static page controls:
+   - multiple system prompts.
+   - pre-message editor.
+   - chat input and streamed output.
+   - ChatAgent/EventAgent parameter inputs.
+   - EventAgent tool toggles.
+   - round stats area for tokens, cached tokens, TTFT, and elapsed time.
+7. Run focused tests:
+
+```bash
+uv run pytest tests/test_websocket_api.py tests/test_debug_runtime.py
+```
+
+8. Run full tests:
+
+```bash
+uv run pytest
+```
+
+## Verification
+
+Expected result:
+
+- WebSocket contract test passes without a real LLM key.
+- Existing Todo 2 tests still pass.
+- App imports with `uv run python -c "from agent_lab.main import app; print(app.title)"`.

+ 32 - 0
docs/plans/todo-30-sqlite-session-store.md

@@ -0,0 +1,32 @@
+# Todo 30: SQLite Session Store
+
+## Status
+
+done
+
+## Goal
+
+Persist debugging-console data in SQLite and expose read APIs for session list, chat messages, audit replay, and usage summaries.
+
+## Scope
+
+- Add a small synchronous SQLite store under `infrastructure/`.
+- Keep the store independent from runtime orchestration so it can be tested directly.
+- Add FastAPI endpoints for creating/listing sessions and reading messages, audit logs, and usage.
+- Add `AGENT_LAB_DATABASE_PATH` via `Settings.database_path`.
+- Ignore local SQLite database files in git.
+
+## Data Model
+
+- `sessions`: id, title, timestamps, full workspace/agent config JSON.
+- `turns`: session id, turn index, user message, timestamps.
+- `messages`: ordered chat transcript rows.
+- `audit_logs`: ordered audit rows with event name and JSON details.
+- `usage_stats`: one row per model call; REST response also returns turn and session aggregates.
+
+## Verification
+
+- Direct store tests cover create/list/detail/messages/audit/usage, including a regression for usage totals not being multiplied by turn joins.
+- API tests cover `/api/sessions` and session detail/replay endpoints with empty and non-empty data.
+- `uv run pytest tests/test_sqlite_store.py tests/test_websocket_api.py -q` passes: 36 tests.
+- `uv run pytest` passes: 66 tests.

+ 26 - 0
docs/plans/todo-31-runtime-persistence.md

@@ -0,0 +1,26 @@
+# Todo 31: Runtime Persistence
+
+## Status
+
+done
+
+## Goal
+
+Persist live WebSocket runtime data into the SQLite session store so completed turns can be replayed through the REST APIs.
+
+## Scope
+
+- Add an optional `session_id` to `DebugRunRequest`.
+- Let the WebSocket runtime create or reuse a persisted session.
+- Persist Agent config snapshots when a session is created or reused.
+- Persist user and assistant messages for each turn.
+- Persist audit entries with turn and round indexes.
+- Persist one usage row per ChatAgent model call.
+- Continue turn indexes for an existing persisted session.
+
+## Verification
+
+- Runtime tests prove session creation, turn/message persistence, audit rows, usage rows, and existing-session turn indexes.
+- Existing queue, audit, WebSocket, and store tests still pass.
+- `uv run pytest tests/test_sqlite_store.py tests/test_debug_runtime.py tests/test_websocket_api.py -q` passes: 55 tests.
+- `uv run pytest` passes: 66 tests.

+ 26 - 0
docs/plans/todo-32-session-ui.md

@@ -0,0 +1,26 @@
+# Todo 32: Session UI
+
+## Status
+
+done
+
+## Goal
+
+Expose persisted sessions in the debug console so users can create/load sessions, replay chat messages and audit logs, and inspect call/turn/session usage.
+
+## Scope
+
+- Add sidebar session controls for creating, refreshing, selecting, and loading sessions.
+- Send the selected `session_id` in the first WebSocket request while preserving lightweight follow-up turn messages.
+- Handle `session_started.session_id` from the backend.
+- Add a separate audit replay panel.
+- Add session usage displays for aggregate, turn, and model-call usage.
+- Keep live round stats separate from session aggregate usage.
+
+## Verification
+
+- Static UI tests cover session controls, replay panels, session API calls, `session_id` WebSocket payloads, and usage render hooks.
+- WebSocket tests still prove multi-turn reuse behavior.
+- `node --check src/agent_lab/presentation/static/app.js` passes.
+- `uv run pytest tests/test_websocket_api.py -q` passes: 34 tests.
+- `uv run pytest` passes: 66 tests.

+ 24 - 0
docs/plans/todo-33-audit-replay-stream-ui.md

@@ -0,0 +1,24 @@
+# Todo 33: Audit Replay Event Stream UI
+
+## Status
+
+done
+
+## Goal
+
+Redesign Audit Replay as a scrollable event stream with click-to-inspect details, keep audit events out of the chat transcript, and lock the sidebar controls plus chat composer while letting chat and replay panes scroll independently.
+
+## Plan
+
+1. Split live audit handling from chat message rendering.
+2. Add an Audit Detail panel that renders event-specific sections for agent requests, responses, detected events, round lifecycle, usage, and generic details.
+3. Restyle the replay pane as a compact timeline/event stream and make the workspace layout constrain scroll to chat/replay panes.
+4. Add static regression tests and run focused/full verification.
+
+## Verification
+
+- `node --check src/agent_lab/presentation/static/app.js` passes.
+- `uv run pytest tests/test_websocket_api.py -q` passes (`36 passed`, one existing Starlette deprecation warning).
+- `uv run pytest` passes (`68 passed`, one existing Starlette deprecation warning).
+- `git diff --check` passes.
+- Browser smoke was attempted with Playwright, but the installed package has no local Chromium binary in this environment.

+ 22 - 0
docs/plans/todo-34-event-agent-history-boundary.md

@@ -0,0 +1,22 @@
+# Todo 34: EventAgent History Boundary
+
+## Status
+
+done
+
+## Goal
+
+Ensure EventAgent receives only conversational `user` and `assistant` messages, not the full ChatAgent prompt/context list or EventAgent/tool internal replies.
+
+## Plan
+
+1. Add a runtime helper that derives EventAgent history from the current conversation.
+2. Use it in both direct runtime and WebSocket session turn paths before enqueueing `EventAgentRequest`.
+3. Add regression tests proving system prompts, pre-messages with system role, and internal EventAgent replies are excluded from EventAgent history and audit.
+
+## Verification
+
+- `uv run pytest tests/test_debug_runtime.py -q` passes (`21 passed`).
+- `node --check src/agent_lab/presentation/static/app.js` passes.
+- `uv run pytest` passes (`70 passed`, one existing Starlette deprecation warning).
+- `git diff --check` passes.

+ 23 - 0
docs/plans/todo-35-audit-modal-raw-llm-events.md

@@ -0,0 +1,23 @@
+# Todo 35: Audit Modal, Round Event Metadata, Raw LLM Chunks
+
+## Status
+
+done
+
+## Goal
+
+Make audit replay usable in a larger modal, clarify per-round event metadata after EventAgent results trigger ChatAgent again, and expose raw LLM streaming chunks in audit details.
+
+## Plan
+
+1. Split configured available events from per-round event generation state in `chat_round_started` audit entries.
+2. Add raw chunk stream items from the OpenAI-compatible client and collect them into ChatAgent/EventAgent response audit records.
+3. Move Audit Replay into a modal with full-height event stream and detail panes.
+4. Add backend/frontend regression tests and run verification.
+
+## Verification
+
+- `node --check src/agent_lab/presentation/static/app.js` passes.
+- `uv run pytest tests/test_debug_runtime.py tests/test_event_agent.py tests/test_websocket_api.py -q` passes (`63 passed`, one existing Starlette deprecation warning).
+- `uv run pytest` passes (`71 passed`, one existing Starlette deprecation warning).
+- `git diff --check` passes.

+ 24 - 0
docs/plans/todo-36-audit-detail-compact-ui.md

@@ -0,0 +1,24 @@
+# Todo 36: Compact Audit Detail UI
+
+## Status
+
+done
+
+## Goal
+
+Fix the Audit Detail modal layout so selected events render as compact, readable detail rows instead of oversized pill-like blocks, and prevent stale CSS from keeping the old layout.
+
+## Plan
+
+1. Version the stylesheet URL to bust browser cache.
+2. Replace Audit Detail metadata pills with compact key/value fields.
+3. Render event-name lists as small code tags instead of large rounded blocks.
+4. Override global `section + section` spacing inside the audit modal/detail area.
+5. Add static regression checks and run verification.
+
+## Verification
+
+- `node --check src/agent_lab/presentation/static/app.js` passes.
+- `uv run pytest tests/test_websocket_api.py -q` passes (`36 passed`, one existing Starlette deprecation warning).
+- `uv run pytest` passes (`71 passed`, one existing Starlette deprecation warning).
+- `git diff --check` passes.

+ 25 - 0
docs/plans/todo-37-audit-detail-flow-layout.md

@@ -0,0 +1,25 @@
+# Todo 37: Audit Detail Flow Layout
+
+## Status
+
+done
+
+## Goal
+
+Fully replace the stretched Audit Detail card layout with a content-sized flow layout. Metadata must render as compact inline fields, not grid rows or large cards.
+
+## Plan
+
+1. Remove internal CSS Grid layout from `.audit-detail` and `.audit-detail-header`.
+2. Render metadata as compact inline definition fields.
+3. Keep detail sections in normal block flow with small spacing.
+4. Version static assets again to prevent stale browser CSS.
+5. Add tests that prevent `.audit-detail` and `.audit-detail-header` from returning to grid.
+
+## Verification
+
+- `node --check src/agent_lab/presentation/static/app.js` passes.
+- `uv run pytest tests/test_websocket_api.py -q` passes (`36 passed`, one existing Starlette deprecation warning).
+- `uv run pytest` passes (`71 passed`, one existing Starlette deprecation warning).
+- `git diff --check` passes.
+- Static regression asserts `.audit-detail` and `.audit-detail-header` do not use `display: grid`.

+ 22 - 0
docs/plans/todo-38-audit-round-event-semantics.md

@@ -0,0 +1,22 @@
+# Todo 38: Audit Round Event Semantics
+
+## Status
+
+done
+
+## Goal
+
+Make `chat_round_started.events_enabled` represent the events actually enabled for the current ChatAgent round. Preserve the full configured tool/event list separately so round 2 after EventAgent results is not misreported.
+
+## Plan
+
+1. Update runtime audit payloads so `events_enabled` is the per-round prompt/event generation list and `configured_events` is the full EventAgent configuration list.
+2. Update Audit Detail labels and summaries to distinguish configured events from current-round enabled events.
+3. Adjust regression tests for round 2 and keep existing raw LLM/audit modal coverage intact.
+
+## Verification
+
+- `node --check src/agent_lab/presentation/static/app.js` passes.
+- `uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py -q` passes (`58 passed`, one existing Starlette deprecation warning).
+- `uv run pytest` passes (`71 passed`, one existing Starlette deprecation warning).
+- `git diff --check` passes.

+ 19 - 0
docs/plans/todo-39-audit-turn-relative-time.md

@@ -0,0 +1,19 @@
+# Todo 39: Audit Turn-Relative Time
+
+## Goal
+
+Show every in-turn audit event with elapsed time relative to the user message that started the current turn.
+
+## Plan
+
+1. Add `turn_elapsed_ms` to audit details when an audit event belongs to a turn.
+2. Carry the turn start timestamp into EventAgent audit work so request/response events use the same baseline.
+3. Render the relative time in the audit event stream and detail header.
+
+## Verification
+
+- `uv run pytest tests/test_debug_runtime.py::test_runtime_audit_events_include_turn_relative_elapsed_time -q`
+- `uv run pytest tests/test_websocket_api.py::test_static_audit_replay_uses_event_stream_and_detail_panel -q`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `uv run pytest`
+- `git diff --check`

+ 91 - 0
docs/plans/todo-4-real-llm-smoke.md

@@ -0,0 +1,91 @@
+# Todo 4 Real LLM Smoke Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and locally verified.
+
+Commands:
+
+```bash
+uv run pytest
+uv run python scripts/ws_smoke.py --help
+```
+
+Results:
+
+- `uv run pytest`: `9 passed, 1 warning in 0.22s`
+- `ws_smoke.py --help`: command help prints successfully.
+
+Evaluation note: this todo made the local real-LLM run path documented and smokeable, but raised a provider-compatibility risk around tool-call history. Todo 5 now covers that before final cleanup.
+
+## Goal
+
+Document and lightly verify the real OpenAI Chat Completions compatible run path without requiring a live API key in automated tests.
+
+## Scope
+
+This todo covers:
+
+- README run instructions for uv, env vars, and local server startup.
+- `.env.example` with non-secret defaults/placeholders.
+- A small manual smoke script that connects to the local WebSocket endpoint and prints streamed messages.
+- Tests for smoke-script request construction if practical.
+
+This todo does not cover:
+
+- Running a real paid/network LLM call in CI or automated tests.
+- Adding secrets to the repo.
+- Adding another frontend framework.
+- Changing runtime behavior unless a small bug blocks the documented smoke path.
+
+## Files
+
+- Modify: `README.md`
+- Create: `.env.example`
+- Create: `scripts/ws_smoke.py`
+- Create or modify: tests only if needed for the smoke helper.
+- Do not modify: `docs/plans/todos.md`.
+
+## Environment Contract
+
+Use these variables:
+
+- `AGENT_LAB_OPENAI_API_KEY`
+- `AGENT_LAB_OPENAI_BASE_URL`
+- `AGENT_LAB_OPENAI_DEFAULT_MODEL`
+- `AGENT_LAB_REQUEST_TIMEOUT_SECONDS`
+
+The default base URL is already `https://api.openai.com/v1`. For compatible gateways, users set `AGENT_LAB_OPENAI_BASE_URL` to the gateway `/v1` root.
+
+## Steps
+
+1. Update `README.md` with:
+   - `uv sync`
+   - `cp .env.example .env`
+   - required env vars
+   - `uv run uvicorn agent_lab.main:app --reload`
+   - browser URL `http://127.0.0.1:8000`
+   - WebSocket smoke command.
+2. Add `.env.example` with placeholder key and clear compatible endpoint defaults.
+3. Add `scripts/ws_smoke.py`:
+   - connects to `ws://127.0.0.1:8000/ws/debug` by default.
+   - sends one valid debug request.
+   - prints each server message as compact JSON.
+   - accepts optional CLI args for URL, message, model, and tool enablement.
+4. If adding smoke helper logic beyond straightforward I/O, add a focused test.
+5. Run:
+
+```bash
+uv run pytest
+uv run python scripts/ws_smoke.py --help
+```
+
+## Verification
+
+Expected result:
+
+- Tests still pass.
+- Smoke helper help output works without starting the server.
+- README contains exact env names and local run commands.

+ 21 - 0
docs/plans/todo-40-chat-message-stream-audit.md

@@ -0,0 +1,21 @@
+# Todo 40: Chat Message Stream Audit Boundaries
+
+## Goal
+
+Add audit events around ChatAgent visible message streaming so replay can show when user-visible text starts and when it ends.
+
+## Plan
+
+1. Emit `chat_message_stream_started` before the first `message_delta` reaches the output queue.
+2. Emit `chat_message_stream_finished` after the model stream ends and before `chat_agent_response`.
+3. Keep `chat_agent_response` as the complete aggregated visible ChatAgent text for that call.
+4. Add readable Audit Replay summaries for the new events.
+
+## Verification
+
+- `uv run pytest tests/test_debug_runtime.py::test_runtime_audits_chat_message_stream_boundaries_in_output_order -q`
+- `uv run pytest tests/test_debug_runtime.py::test_runtime_emits_audit_events_and_backend_logs -q`
+- `uv run pytest tests/test_websocket_api.py::test_static_app_handles_audit_messages -q`
+- `uv run pytest`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `git diff --check`

+ 18 - 0
docs/plans/todo-41-session-button-feedback.md

@@ -0,0 +1,18 @@
+# Todo 41: Session Button Feedback
+
+## Goal
+
+Make the Sessions panel respond visibly when users click New, Refresh, or Load.
+
+## Plan
+
+1. Add a local session status line inside the Sessions panel.
+2. Update session actions to set immediate in-panel feedback before requests.
+3. Keep the top-level connection status in sync for session actions.
+
+## Verification
+
+- `uv run pytest tests/test_websocket_api.py::test_static_session_ui_controls_and_replay_panels_are_available tests/test_websocket_api.py::test_static_app_loads_session_replay_and_usage -q`
+- `uv run pytest`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `git diff --check`

+ 19 - 0
docs/plans/todo-42-session-switch-idle-socket.md

@@ -0,0 +1,19 @@
+# Todo 42: Session Switch Idle Socket
+
+## Goal
+
+Allow New and Load session actions after the current turn has completed, even when the reusable WebSocket session is still open.
+
+## Plan
+
+1. Add a focused static test that distinguishes active turn state from open WebSocket state.
+2. Track active turn state explicitly in the browser app.
+3. Guard session switching only during an active turn, and close any idle socket before changing sessions.
+
+## Verification
+
+- `uv run pytest tests/test_websocket_api.py::test_static_session_switch_guard_uses_active_turn_not_open_socket -q`
+- `uv run pytest tests/test_websocket_api.py -q`
+- `uv run pytest`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `git diff --check`

+ 79 - 0
docs/plans/todo-43-typed-provider-tool-calls.md

@@ -0,0 +1,79 @@
+# Todo 43 Typed Provider Tool Calls Plan
+
+**Status:** done
+
+## Result
+
+- Added distinct `text_event` and `provider_tool_call` stream kinds.
+- Preserved provider IDs plus parsed/raw arguments and ordered multiple calls.
+- Added exactly-once flush behavior for providers that omit `finish_reason`.
+- Follow-up todos 43.1 and 43.2 repaired legacy consumers and removed production acceptance of the compatibility `event` kind.
+- Verified with focused regressions and the full suite (`82 passed`, one existing Starlette deprecation warning).
+
+## Goal
+
+Preserve native provider tool calls as a distinct stream item so direct ChatAgent tools can retain provider-generated IDs and arguments without changing the existing hidden text-event protocol.
+
+## Scope
+
+- Keep text protocol output as name-only events for dual-agent mode.
+- Represent provider-native tool calls with a separate stream kind.
+- Preserve `id`, `name`, parsed arguments, and raw arguments from the provider.
+- Flush buffered provider tool calls when a compatible provider ends with `[DONE]` but omits the expected tool-call finish reason.
+- Do not change Runtime tool routing or provider transcript serialization in this todo.
+
+## Files
+
+- Modify: `src/agent_lab/domain/messages.py`
+- Modify: `src/agent_lab/infrastructure/openai_compatible.py`
+- Modify: `tests/test_openai_stream_parser.py`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `docs/plans/todos.md`
+
+## Plan
+
+### Plan
+
+1. Add failing parser tests for distinct event sources.
+2. Verify the tests fail because provider calls and text events currently share `kind="event"`.
+3. Add explicit `StreamItem.text_event(...)` and `StreamItem.provider_tool_call(...)` constructors while keeping the event payload as `ToolCallEvent`.
+4. Update the text-event parser to emit `text_event` and the provider tool-call accumulator to emit `provider_tool_call`.
+5. Make parser flush drain buffered provider calls exactly once when no finish reason was received.
+6. Update existing Runtime regression fixtures to accept only `text_event` in the current dual-agent path; direct routing remains out of scope.
+
+### Act
+
+Follow TDD in this order:
+
+- Add `test_parser_distinguishes_text_events_from_provider_tool_calls` asserting:
+  - visible text remains `message_delta`;
+  - `<agent_events>mock_search</agent_events>` produces `kind="text_event"`;
+  - provider `delta.tool_calls` produces `kind="provider_tool_call"` with intact arguments.
+- Add `test_parser_flushes_provider_tool_calls_without_finish_reason` using fragmented name/argument chunks followed by `flush()` and asserting one call is emitted.
+- Add `test_parser_does_not_emit_provider_tool_call_twice_after_finish_and_flush` to protect normal providers.
+- Run `uv run pytest tests/test_openai_stream_parser.py -q` and confirm the new tests fail for the missing typed kinds/flush behavior.
+- Implement the minimum domain/parser changes.
+- Run the focused parser tests until green.
+- Run `uv run pytest tests/test_debug_runtime.py tests/test_event_agent.py tests/test_openai_stream_parser.py -q` to verify the existing dual-agent path still passes.
+
+### Observe
+
+Confirm from test output and inspection that:
+
+- text events remain name-only;
+- provider calls retain raw and parsed arguments;
+- multiple provider calls preserve provider order/index;
+- flush emits only buffered calls that were not already drained;
+- no Runtime direct-tools behavior was added early.
+
+### Update
+
+- Mark only todo 43 as `done` after focused and regression tests pass.
+- If review finds a defect after the commit, add a new `43.1` row directly below todo 43 instead of modifying any completed historical todo row.
+- Commit with a message explaining that the previous shared event kind discarded the distinction required for direct tools; mention compatibility risk if any provider-specific flush behavior remains.
+
+## Verification
+
+- `uv run pytest tests/test_openai_stream_parser.py -q`
+- `uv run pytest tests/test_debug_runtime.py tests/test_event_agent.py tests/test_openai_stream_parser.py -q`
+- `git diff --check`

+ 67 - 0
docs/plans/todo-43.1-typed-event-consumer-regression.md

@@ -0,0 +1,67 @@
+# Todo 43.1 Typed Event Consumer Regression Plan
+
+**Status:** done
+
+## Result
+
+- Runtime now consumes typed ChatAgent `text_event` output.
+- EventAgent now consumes typed provider `provider_tool_call` output.
+- Realistic fixtures exposed and fixed the legacy consumer mismatch.
+- Todo 43.2 completed strict source isolation and remaining fixture migration.
+
+## Goal
+
+Restore the existing dual-agent production paths after Todo 43 introduced typed stream kinds but legacy consumers and fixtures still listened for `kind="event"`.
+
+## Scope
+
+- Runtime consumes `text_event` from ChatAgent and preserves its name-only handoff behavior.
+- EventAgent argument generation consumes `provider_tool_call` and preserves provider-generated arguments.
+- Production-like test fixtures emit typed kinds instead of the legacy compatibility constructor.
+- Keep `StreamItem.event(...)` only as a temporary compatibility API for unrelated callers; new/updated production-path tests must not use it.
+- Do not add direct ChatAgent tool routing.
+
+## Files
+
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `tests/test_event_agent.py`
+
+## Plan
+
+### Plan
+
+1. Add or update tests so realistic ChatAgent fixtures emit `text_event` and EventAgent provider fixtures emit `provider_tool_call`.
+2. Verify tests fail because Runtime/EventAgent still consume only `event`.
+3. Change only the two legacy consumer checks to the correct typed kinds.
+4. Run focused and full regressions to prove no legacy path was silently lost.
+
+### Act
+
+- Update the minimum runtime fixture that drives text-event handoff to use `StreamItem.text_event(...)`.
+- Update EventAgent LLM argument fixtures to use `StreamItem.provider_tool_call(...)`.
+- Add explicit assertions that Runtime enqueues/executes the text event and EventAgent executes provider arguments without falling back.
+- Run the focused tests and confirm expected RED failures before production changes.
+- Update Runtime and EventAgent kind checks.
+- Re-run focused and full tests.
+
+### Observe
+
+Confirm that:
+
+- Runtime receives a real typed text event and still emits `chat_event_detected` plus tool execution.
+- EventAgent receives a real provider tool call and returns its arguments.
+- No direct ChatAgent tool transcript behavior appears in this fix.
+- The compatibility constructor is not used to hide production-path type mismatches in the updated tests.
+
+### Update
+
+- Mark 43 and 43.1 `done` only after spec and quality reviews pass.
+- Commit this repair separately with the regression and compatibility risk in the message.
+
+## Verification
+
+- `uv run pytest tests/test_debug_runtime.py tests/test_event_agent.py tests/test_openai_stream_parser.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 65 - 0
docs/plans/todo-43.2-remove-legacy-event-consumption.md

@@ -0,0 +1,65 @@
+# Todo 43.2 Remove Legacy Event Consumption Plan
+
+**Status:** done
+
+## Result
+
+- Removed legacy `event` acceptance from Runtime and EventAgent production consumers.
+- Added wrong-source negative coverage.
+- Added a real chat-client `[DONE]` integration test for compatible providers without `finish_reason`.
+- Focused tests pass (`77 passed`) and the full suite passes (`82 passed`, one existing warning).
+
+## Goal
+
+Enforce typed event-source isolation at production consumer boundaries so the legacy compatibility constructor cannot silently mask incorrect ChatAgent or EventAgent wiring.
+
+## Scope
+
+- Runtime accepts only `text_event` from ChatAgent streams.
+- EventAgent accepts only `provider_tool_call` from its provider-tools argument request.
+- Remaining runtime/WebSocket test fixtures use the correct typed constructor.
+- Add negative tests for wrong-source events.
+- Add one chat-client SSE integration test proving `[DONE]` flush emits one provider tool call when no finish reason is present.
+- Keep the legacy constructor temporarily for unrelated external callers, but production consumers must not accept it.
+
+## Files
+
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `tests/test_event_agent.py`
+- Modify: `tests/test_websocket_api.py`
+
+## Plan
+
+### Plan
+
+1. Replace remaining production-path legacy fixtures with typed constructors.
+2. Add negative tests that wrong-source and legacy events do not cross the consumer boundaries.
+3. Add a MockTransport/SSE `[DONE]` integration test for the real chat client.
+4. Verify RED failures expose the three legacy consumer branches.
+5. Remove legacy consumption from Runtime/EventAgent and run all regressions.
+
+### Act
+
+- Runtime tests: emit `provider_tool_call` and legacy `event` from the ChatAgent fake and assert no tool handoff occurs; keep the positive `text_event` test.
+- EventAgent tests: emit `text_event`/legacy `event` from its provider fake and assert it falls back rather than treating either as resolved provider arguments; keep the positive provider call test.
+- WebSocket/client tests: replace legacy fixtures with typed constructors and add SSE chunks with provider tool calls, no `finish_reason`, then `[DONE]`; assert exactly one `provider_tool_call`.
+- Run focused tests and confirm expected failure before production changes.
+- Remove `event` from consumer condition sets.
+- Re-run focused and full tests.
+
+### Observe
+
+Confirm typed-source isolation is enforced by negative tests and that no remaining production-path fixture relies on `StreamItem.event(...)`.
+
+### Update
+
+- Mark 43, 43.1, and 43.2 `done` only after renewed spec and quality review.
+- Commit separately with the masked-regression risk in the message.
+
+## Verification
+
+- `uv run pytest tests/test_debug_runtime.py tests/test_event_agent.py tests/test_websocket_api.py tests/test_openai_stream_parser.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 97 - 0
docs/plans/todo-44-provider-tool-transcript.md

@@ -0,0 +1,97 @@
+# Todo 44 Provider Tool Transcript Plan
+
+**Status:** done
+
+## Result
+
+- Added structured assistant tool calls and provider-compatible tool replies.
+- Added local transcript-state validation and explicit tool-choice behavior.
+- Removed implicit forcing for a single ChatAgent tool while EventAgent explicitly forces its selected function.
+- Todo 44.1 hardened identity, payload precedence, history projection, and public request boundaries.
+- Verified with focused tests (`97 passed`) and the full suite (`108 passed`, one existing warning).
+
+## Goal
+
+Represent and serialize a valid OpenAI-compatible assistant tool-call transcript so a later Runtime todo can continue ChatAgent after native tool execution without weakening the existing dual-agent path.
+
+## Scope
+
+- Add structured assistant `tool_calls` to the internal `ChatMessage` model.
+- Serialize assistant tool calls and matched `tool` replies into provider-compatible request messages.
+- Reject orphaned, mismatched, duplicate, or incomplete tool transcripts before network I/O.
+- Replace implicit “one tool means forced invocation” behavior with an explicit `tool_choice` argument.
+- EventAgent explicitly forces its one selected tool; ordinary ChatAgent requests keep automatic/no forced choice behavior.
+- Do not enable ChatAgent tools in Runtime in this todo.
+
+## Files
+
+- Modify: `src/agent_lab/domain/messages.py`
+- Modify: `src/agent_lab/infrastructure/chat_client.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `src/agent_lab/application/contracts.py`
+- Modify: `tests/test_websocket_api.py`
+- Modify: `tests/test_event_agent.py`
+- Modify: `tests/test_debug_runtime.py` only for protocol signature compatibility if required
+- Modify: `docs/plans/todos.md`
+
+## Plan
+
+### Plan
+
+1. Define the desired internal transcript and tool-choice API through failing tests.
+2. Verify failures occur because `ChatMessage` lacks assistant tool calls, the client rejects all tool messages, and a single tool is implicitly forced.
+3. Add the minimum typed message fields and transcript validation/serialization.
+4. Add explicit optional `tool_choice` to the chat-client protocols and implementation.
+5. Make EventAgent pass a forced function choice; keep Runtime ChatAgent calls unchanged with `tools=[]` and no tool choice.
+6. Run focused and full regressions.
+
+### Act
+
+Follow TDD in small RED/GREEN slices:
+
+1. **Message shape RED**
+   - Add a test constructing an assistant `ChatMessage` with two `ToolCallEvent` entries and asserting `model_dump()` preserves IDs, names, parsed arguments, and raw arguments.
+   - Add validation tests rejecting `tool_calls` on non-assistant roles and rejecting a `tool` role without `tool_call_id`.
+2. **Provider serialization RED**
+   - Add a MockTransport request test containing:
+     - assistant content plus two tool calls;
+     - two matching tool replies;
+     - a final user or assistant-visible message after replies.
+   - Assert the provider payload uses standard `assistant.tool_calls[].function.arguments` raw JSON and `tool.tool_call_id`.
+3. **Transcript guard RED**
+   - Add separate tests rejecting before network I/O:
+     - orphan tool reply;
+     - mismatched call ID;
+     - duplicate tool reply;
+     - a non-tool message while assistant calls remain unresolved.
+4. **Tool choice RED**
+   - Add a test showing one ChatAgent tool with no explicit choice does not add forced `tool_choice`.
+   - Add a test showing an explicit forced function choice is serialized exactly.
+   - Update EventAgent tests to assert its argument-generation call supplies the selected function choice.
+5. Run focused tests and confirm expected failures.
+6. Implement minimal domain validation, transcript serialization, explicit choice plumbing, and EventAgent forcing.
+7. Re-run focused tests and then the full suite.
+
+### Observe
+
+Confirm that:
+
+- the legal transcript matches OpenAI-compatible message ordering;
+- visible assistant content can coexist with tool calls;
+- invalid histories fail locally before HTTP calls;
+- single-tool ChatAgent requests are no longer accidentally forced;
+- EventAgent retains deterministic selected-tool invocation;
+- Runtime still sends `tools=[]` in the existing dual-agent mode.
+
+### Update
+
+- Mark only todo 44 `done` after spec and quality reviews pass.
+- If review finds post-commit defects, add `44.1` directly below todo 44 and do not edit the completed row.
+- Commit code with the problem and any compatibility risk in the body.
+
+## Verification
+
+- `uv run pytest tests/test_websocket_api.py tests/test_event_agent.py tests/test_debug_runtime.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 69 - 0
docs/plans/todo-44.1-tool-transcript-hardening.md

@@ -0,0 +1,69 @@
+# Todo 44.1 Tool Transcript Hardening Plan
+
+**Status:** done
+
+## Result
+
+- Rejected duplicate call IDs within and across assistant messages.
+- Protected provider request fields from `extra_body` overrides and validated forced choices locally.
+- Projected EventAgent history without unresolved provider protocol state.
+- Kept public pre-messages user-facing by rejecting internal tool protocol messages.
+- Completed role-field and test-double validation; full suite passes (`108 passed`).
+
+## Goal
+
+Close identity, payload-precedence, history-projection, and request-boundary gaps found by Todo 44 code review before direct Runtime tool mode builds on the transcript layer.
+
+## Scope
+
+- Reject duplicate assistant tool-call IDs before serialization.
+- Prevent `extra_body` from overriding reserved provider request fields, especially explicit `tool_choice`.
+- Validate forced tool choices locally against the supplied tools.
+- Project EventAgent conversation history without unresolved assistant tool-call protocol state.
+- Keep public `pre_messages` user-facing only by rejecting both tool replies and assistant tool calls.
+- Complete role-specific field validation and update all affected fakes.
+
+## Files
+
+- Modify: `src/agent_lab/domain/messages.py`
+- Modify: `src/agent_lab/application/contracts.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `src/agent_lab/infrastructure/chat_client.py`
+- Modify: `tests/test_websocket_api.py`
+- Modify: `tests/test_event_agent.py`
+- Modify: `tests/test_debug_runtime.py`
+
+## Plan
+
+### Plan
+
+1. Add failing negative tests for every review finding.
+2. Verify failures are caused by current acceptance/override behavior, not test setup.
+3. Implement the smallest domain/client/history/request-boundary hardening.
+4. Run focused and full regressions.
+
+### Act
+
+- Add tests rejecting duplicate tool-call IDs within one assistant message and across a transcript.
+- Add tests proving `extra_body` cannot override `messages`, `tools`, `tool_choice`, model, streaming, or token/sampling controls.
+- Add tests rejecting forced tool choice with no tools or a function name absent from the supplied list.
+- Add an EventAgent test with a prior complete assistant/tool round and assert its argument prompt contains visible assistant content without unresolved `tool_calls`.
+- Add request-contract tests rejecting `pre_messages` that contain either tool replies or assistant tool calls.
+- Update ChatMessage role-field tests so irrelevant `tool_call_id`/`tool_calls` fields fail instead of being ignored.
+- Update all fake `stream_chat` signatures and assertions for optional `tool_choice`.
+- Run RED, apply minimum fixes, then run focused/full verification.
+
+### Observe
+
+Confirm transcript identity is unique, explicit controlled fields win over provider extras, EventAgent receives a provider-valid projected history, and public pre-messages cannot inject partial internal protocol state.
+
+### Update
+
+- Mark 44 and 44.1 `done` only after spec and quality re-review.
+- Commit separately with the malformed transcript and provider override risks in the body.
+
+## Verification
+
+- `uv run pytest tests/test_websocket_api.py tests/test_event_agent.py tests/test_debug_runtime.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 101 - 0
docs/plans/todo-45-generic-event-kernel.md

@@ -0,0 +1,101 @@
+# Todo 45 Generic Event Kernel Plan
+
+**Status:** done
+
+## Result
+
+Implemented a name-agnostic event kernel with registry metadata, deterministic-first resolution, optional asynchronous fallback, policy-bearing results, sync/async handlers, and ToolRegistry/EventAgent compatibility. Final full verification: `203 passed`, one existing Starlette deprecation warning; bounded specification and quality gates passed.
+
+## Goal
+
+Extract a generic, event-name-agnostic execution kernel so text events and future native ChatAgent tool calls share deterministic resolution, bounded LLM fallback, validation, execution, and normalized results before built-in plugins are added.
+
+## Architecture
+
+Create a focused `agent_lab.application.events` package:
+
+- `models.py`: event source, result/continuation policies, definition metadata, execution request/context, normalized result.
+- `registry.py`: flat definition registration, catalog/schema lookup, enabled-name filtering, and compatibility validation.
+- `kernel.py`: deterministic-first resolution, optional injected argument fallback, schema-required-field validation, handler isolation, and normalized result creation.
+
+Keep `application/tools.py` as a compatibility facade during migration. Existing Runtime and EventAgent APIs continue working, but their resolution/execution delegates to the kernel. The kernel must not compare built-in event names.
+
+## Scope
+
+- Add generic definition metadata and namespaced-event support without adding the four production plugins yet.
+- Deterministic resolver runs first.
+- LLM fallback is called only when deterministic output is incomplete and the definition allows fallback.
+- Provider-resolved arguments are validated/executed without another fallback call.
+- Normalize unknown, disabled, invalid, handler-failure, and success outcomes.
+- Introduce generic continuation/result policies as metadata; Runtime application of those policies remains for Todo 48.
+- Preserve current mock tool names and public registry APIs through adapters.
+- Do not add parallel scheduling or direct ChatAgent tools in this todo.
+
+## Files
+
+- Create: `src/agent_lab/application/events/__init__.py`
+- Create: `src/agent_lab/application/events/models.py`
+- Create: `src/agent_lab/application/events/registry.py`
+- Create: `src/agent_lab/application/events/kernel.py`
+- Modify: `src/agent_lab/application/tools.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `src/agent_lab/application/runtime.py` only for dependency construction/types if required
+- Create: `tests/test_event_kernel.py`
+- Modify: `tests/test_event_agent.py`
+- Modify: `tests/test_debug_runtime.py` only for regression compatibility
+
+## Plan
+
+### Plan
+
+1. Define desired kernel contracts through focused failing tests.
+2. Verify RED failures for missing package/types and current EventAgent always-LLM behavior.
+3. Implement minimal models, flat registry, and sequential kernel.
+4. Adapt existing ToolRegistry/EventAgent through compatibility boundaries.
+5. Run focused and full regressions.
+
+### Act
+
+Follow TDD in these slices:
+
+1. **Registry contract RED**
+   - Register two synthetic definitions and assert catalog/schema lookup, enabled filtering, duplicate-name rejection, and no event-name-specific knowledge.
+2. **Deterministic resolution RED**
+   - Definition resolver returns complete required arguments; assert handler executes and fallback mock has zero calls.
+3. **Fallback RED**
+   - Resolver returns incomplete required arguments; assert one fallback call supplies arguments and handler executes.
+   - Definition with fallback disabled returns normalized invalid-arguments result without calling fallback.
+4. **Provider-resolved RED**
+   - Request marked provider-resolved with complete arguments executes directly without resolver/fallback rewriting.
+   - Missing/invalid provider arguments return normalized invalid result.
+5. **Failure isolation RED**
+   - Unknown, disabled, handler exception, duplicate definition, and malformed arguments return or raise the documented normalized contract.
+6. **Metadata RED**
+   - Assert result policy, confirmation policy, risk level, idempotency/concurrency metadata, timeout, and terminal flag survive registration and result creation.
+7. **Compatibility RED**
+   - Existing `ToolRegistry.available_tools()`, `chat_event_system_message()`, `tool_schema()`, `handle()`, and `execute()` retain their observable behavior through the new registry/kernel.
+   - EventAgent deterministic mock tools skip its LLM; ambiguous synthetic definition invokes exactly one EventAgent provider fallback.
+8. Run RED tests, implement minimum code, then run focused and full verification.
+
+### Observe
+
+Confirm that:
+
+- the kernel has no built-in event-name branches;
+- existing Runtime behavior remains green;
+- deterministic tools reduce EventAgent model calls;
+- provider-resolved arguments are not rewritten;
+- policies are data on definitions/results rather than Runtime conditionals;
+- no direct-tools mode or parallel engine is introduced early.
+
+### Update
+
+- Mark todo 45 `done` only after spec and quality reviews pass.
+- Add `45.1` directly below todo 45 for any post-commit repair.
+- Re-evaluate Todo 46 against the final kernel API before writing its detailed plan.
+
+## Verification
+
+- `uv run pytest tests/test_event_kernel.py tests/test_event_agent.py tests/test_debug_runtime.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 70 - 0
docs/plans/todo-45.1-event-kernel-boundary-hardening.md

@@ -0,0 +1,70 @@
+# Todo 45.1 Event Kernel Boundary Hardening Plan
+
+**Status:** done
+
+## Result
+
+Added Draft 2020-12 validation, schema isolation, normalized resolver/fallback/handler failures, strict argument consistency, async-safe execution, and regression coverage for EventAgent concurrency.
+
+## Goal
+
+Make the generic event kernel a trustworthy execution boundary by using standard JSON Schema validation, normalizing all resolver/fallback/handler failures, preserving provider raw arguments, supporting async handlers through the async path, and restoring existing EventAgent concurrency until Todo 48 replaces it atomically.
+
+## Scope
+
+- Add the `jsonschema` runtime dependency and validate definitions with Draft 2020-12 schema checks at registration.
+- Validate event arguments with the registered schema and normalize validation-library failures.
+- Normalize resolver, fallback, handler invocation, and handler payload-shape failures into stable `EventResult` statuses.
+- Replace bare fallback dictionaries with a structured arguments/raw-arguments result and verify provider tool name matches the requested event.
+- Support sync and async handlers in the async kernel and ToolRegistry type contract.
+- Add explicit async ToolRegistry entry points; sync facade rejects coroutine functions before creating an un-awaited coroutine.
+- Restore `EventAgent.handle_many()` concurrency with `asyncio.gather`; bounded scheduling remains Todo 48.
+- Strengthen generic behavior tests rather than searching for only known event-name strings.
+
+## Files
+
+- Modify: `pyproject.toml`
+- Modify: `uv.lock`
+- Modify: `src/agent_lab/application/events/models.py`
+- Modify: `src/agent_lab/application/events/registry.py`
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `src/agent_lab/application/tools.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `tests/test_event_kernel.py`
+- Modify: `tests/test_event_agent.py`
+- Modify: `tests/test_debug_runtime.py` only for concurrency regression if needed
+
+## Plan
+
+### Plan
+
+1. Add failing tests for every quality-review issue.
+2. Verify RED failures demonstrate leaked exceptions, incomplete schema checks, inconsistent raw arguments, async facade warnings, and lost concurrency.
+3. Add the standard validator dependency and implement minimum boundary hardening.
+4. Run focused and full regressions.
+
+### Act
+
+- Add schema tests for union types, enum, nested objects/arrays, additional properties, invalid schemas, and boolean-vs-number behavior.
+- Add resolver and fallback exception tests asserting normalized resolution failure rather than raised exceptions.
+- Add handler tests for exception, `None`, scalar, and other non-dict returns, all normalized as handler errors.
+- Add fallback tests asserting resolved arguments and raw JSON remain consistent and a provider response for the wrong tool is rejected.
+- Add sync and async handler tests through `EventKernel.execute`; add ToolRegistry async-entry tests and sync rejection without runtime warnings.
+- Add a timing/barrier test proving `EventAgent.handle_many()` starts independent calls concurrently.
+- Replace the weak known-name source search with synthetic-definition tests proving arbitrary names share identical kernel behavior.
+- Run RED, implement minimum changes, and re-run all verification.
+
+### Observe
+
+Confirm standard schema semantics, no exception leaks across the kernel contract, raw/parsed argument consistency, clean async handler behavior, and preserved existing concurrency.
+
+### Update
+
+- Mark 45 and 45.1 `done` only after renewed spec and quality review.
+- Commit separately with validation and compatibility risks in the body.
+
+## Verification
+
+- `uv run pytest tests/test_event_kernel.py tests/test_event_agent.py tests/test_debug_runtime.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 63 - 0
docs/plans/todo-45.2-event-fallback-semantics.md

@@ -0,0 +1,63 @@
+# Todo 45.2 Event Fallback Semantics Plan
+
+**Status:** done
+
+## Result
+
+Fallback now represents missing information rather than arbitrary invalid input. Complete-invalid and provider-resolved arguments skip fallback; strict JSON type/raw consistency and definition-error separation are covered.
+
+## Goal
+
+Make fallback and validation semantics suitable for latency/cost experiments: invoke LLM fallback only for incomplete deterministic resolution, compare raw and parsed JSON with type sensitivity, and distinguish plugin-definition failures from user argument errors.
+
+## Scope
+
+- Fall back only when required information is missing or a structured resolver explicitly marks its result incomplete.
+- Complete but invalid arguments return `INVALID_ARGUMENTS` without an LLM call.
+- Compare fallback raw JSON and parsed arguments canonically with JSON type sensitivity and reject non-standard JSON values.
+- Add `DEFINITION_ERROR` for validator/schema infrastructure failures.
+- Deep-copy/freeze registered schemas so post-registration mutation cannot bypass validation.
+- Add focused regression tests for each quality-review finding.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/models.py`
+- Modify: `src/agent_lab/application/events/registry.py`
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `src/agent_lab/application/tools.py` only for status compatibility mapping
+- Modify: `tests/test_event_kernel.py`
+- Modify: `tests/test_event_agent.py` only if fallback contracts change
+
+## Plan
+
+### Plan
+
+1. Add RED tests for complete-invalid no-fallback, JSON type-sensitive mismatch, broken `$ref`, and schema mutation.
+2. Verify each fails for the reviewed reason.
+3. Implement explicit resolution completeness, canonical strict JSON consistency, definition-error status, and registry schema copying.
+4. Run focused and full regressions.
+
+### Act
+
+- Test resolver output with all required fields present but wrong type/enum/additional property; fallback call count must remain zero.
+- Test missing required fields; fallback remains exactly one call.
+- Test raw `true` vs parsed `1`, raw `1` vs parsed `1.0`, nested mismatches, NaN, and Infinity; reject inconsistent/non-standard data.
+- Test an unresolved or failing `$ref`; return `DEFINITION_ERROR`, not `INVALID_ARGUMENTS`.
+- Register a definition, mutate the caller-owned schema afterward, and prove registry validation behavior is unchanged.
+- If structured deterministic resolution is introduced, preserve plain-dict resolver compatibility and require an explicit incomplete flag only for optional-schema cases.
+- Run RED, minimum implementation, focused/full verification.
+
+### Observe
+
+Confirm fallback counts now reflect missing information rather than arbitrary validation failures, raw/parsed values remain auditable, and plugin configuration failures are distinguishable from user mistakes.
+
+### Update
+
+- Mark 45, 45.1, and 45.2 `done` only after renewed reviews.
+- Commit separately with latency/cost and definition-error risks in the body.
+
+## Verification
+
+- `uv run pytest tests/test_event_kernel.py tests/test_event_agent.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 52 - 0
docs/plans/todo-45.3-sync-resolution-completeness.md

@@ -0,0 +1,52 @@
+# Todo 45.3 Sync Resolution Completeness Plan
+
+**Status:** done
+
+## Result
+
+Synchronous execution honors structured resolution completeness and rejects incomplete optional-only arguments without invoking the handler.
+
+## Goal
+
+Keep synchronous and asynchronous event execution semantically aligned by rejecting a structured deterministic resolution that explicitly reports `complete=False`, even when the schema has no required fields.
+
+## Scope
+
+- Add a regression test for `execute_sync()` with an optional-only schema and an explicitly incomplete structured resolver result.
+- Return `INVALID_ARGUMENTS` with a stable incomplete-arguments error before invoking the handler.
+- Preserve complete structured results and legacy plain-dict resolver compatibility.
+- Do not add synchronous LLM fallback support in this fix.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `tests/test_event_kernel.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Write a failing test proving the current sync path executes the handler for an explicitly incomplete result.
+2. Capture the completeness flag in `execute_sync()` and normalize it to the same invalid result used by the async path when no fallback can run.
+3. Run focused kernel tests, the full suite, and `git diff --check`.
+
+### Act
+
+- Use `EventArgumentResolution(arguments={}, complete=False)` with a schema whose fields are optional.
+- Track handler calls and assert the count remains zero.
+- Assert status is `INVALID_ARGUMENTS` and the error is `event arguments incomplete`.
+
+### Observe
+
+Confirm sync execution no longer treats schema-valid-but-incomplete resolution as executable, while complete and plain-dict resolvers remain unchanged.
+
+### Update
+
+- Re-run Todo 45 specification and quality reviews.
+- Mark Todos 45 through 45.3 done only after review and fresh verification.
+
+## Verification
+
+- `uv run pytest tests/test_event_kernel.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 67 - 0
docs/plans/todo-45.4-event-kernel-quality-boundaries.md

@@ -0,0 +1,67 @@
+# Todo 45.4 Event Kernel Quality Boundaries Plan
+
+**Status:** done
+
+## Result
+
+Preserved EventAgent default/explicit configuration semantics, isolated plugin mutations, encapsulated validator state, and normalized non-JSON handler payloads through kernel and EventAgent integrations.
+
+## Goal
+
+Close five reproducible quality-review gaps before treating the generic event kernel as the stable foundation for runtime modes and built-in plugins.
+
+## Scope
+
+- Preserve configured `EventAgent.params.extra_body` when a call does not provide an override.
+- Detect missing required arguments nested under composed JSON Schemas such as `anyOf` and `oneOf`, so eligible incomplete input can use fallback.
+- Isolate nested resolved arguments from handler mutation and preserve consistent result/audit values.
+- Stop exposing the registry's mutable internal validator object.
+- Reject handler dictionaries that are not strict JSON-serializable and normalize them to `HANDLER_ERROR`.
+
+## Boundaries
+
+- Do not add direct ChatAgent tool mode, built-in event plugins, batch scheduling, or new UI behavior.
+- Preserve `extra_body={}` as an explicit override; only `None` means use the configured default.
+- Do not broaden fallback to complete-invalid input; only recursively discovered missing-required errors qualify.
+- Keep event payloads as dictionaries and reject, rather than coerce, sets, arbitrary objects, NaN, or Infinity.
+
+## Files
+
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `src/agent_lab/application/events/registry.py`
+- Modify: `tests/test_event_agent.py`
+- Modify: `tests/test_event_kernel.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add one RED regression per reviewed issue and verify each fails for the reported reason.
+2. Fix configuration propagation and composed-schema error classification without changing valid call behavior.
+3. Deep-copy arguments at handler/result boundaries and encapsulate validator usage inside the registry.
+4. Validate handler payloads with the same strict JSON rules used for arguments.
+5. Run focused tests, full tests, diff checks, and renewed specification/quality reviews.
+
+### Act
+
+- Call `EventAgent.handle()` without `extra_body` while constructor params contain a marker; assert the provider receives the marker. Also assert explicit `{}` remains `{}`.
+- Use `anyOf`/`oneOf` branches containing `required`; assert missing data invokes fallback once, while nested complete-invalid data does not.
+- Mutate a nested handler argument; assert the original request, `EventResult.arguments`, and `raw_arguments` remain consistent and unchanged.
+- Replace public validator retrieval with a registry-owned validation operation; prove callers cannot mutate future validation behavior.
+- Return payloads containing `object()`, `set`, NaN, and Infinity; assert stable `HANDLER_ERROR` and no downstream `json.dumps()` failure.
+
+### Observe
+
+Confirm the kernel remains generic and deterministic-first while all externally observable results are JSON-safe, immutable from plugin side effects, and consistent for audit/comparison metrics.
+
+### Update
+
+- Re-run final Todo 45 specification and quality reviews.
+- Mark Todos 45 through 45.4 done only after fresh full verification.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_kernel.py tests/test_event_agent.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 65 - 0
docs/plans/todo-45.5-composed-schema-and-copy-failures.md

@@ -0,0 +1,65 @@
+# Todo 45.5 Composed Schema and Copy Failures Plan
+
+**Status:** done
+
+## Result
+
+Added immutable validation summaries and conservative branch-aware fallback classification. Uncopyable/non-JSON resolver and provider arguments produce stable results without exception leakage or definition-error misclassification.
+
+## Goal
+
+Restore the cost-sensitive fallback contract for composed schemas and ensure argument isolation cannot leak copy exceptions or misclassify input failures as plugin-definition failures.
+
+## Scope
+
+- Classify `anyOf`/`oneOf` branches so a missing field in an unrelated branch cannot make complete-invalid input eligible for fallback.
+- Preserve fallback for a viable branch that fails only because required data is missing.
+- Replace exposed/deep-copied `ValidationError` values with registry-owned immutable validation summaries needed by the kernel.
+- Normalize strict JSON snapshot failures for deterministic resolver and provider-resolved arguments in both async and sync paths.
+
+## Boundaries
+
+- Prefer a conservative fallback decision: no LLM call when branch viability is ambiguous or any candidate failure is substantive rather than missing-only.
+- Do not implement a general JSON Schema inference engine or discriminator extension.
+- Non-JSON and uncopyable values are invalid event arguments/resolution data; do not coerce them.
+- Preserve the four already verified Todo 45.4 behaviors.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `src/agent_lab/application/events/registry.py`
+- Modify: `tests/test_event_kernel.py`
+- Modify: `tests/test_event_agent.py` only if EventAgent integration needs a regression
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED tests for discriminated `anyOf` and `oneOf`: target branch complete-but-wrong-type, unrelated branch missing fields, fallback count zero.
+2. Add RED tests proving a matching/viable branch with only required fields missing can still call fallback once.
+3. Add RED tests for argument objects whose `__deepcopy__` raises in resolver and provider-resolved inputs; assert no exception escapes and no `DEFINITION_ERROR` misclassification.
+4. Introduce immutable validation issue summaries and branch-aware missing-only classification.
+5. Use strict JSON snapshots at argument isolation boundaries and normalize failures consistently across sync/async execution.
+
+### Act
+
+- Cover both `anyOf` and `oneOf` with discriminator-like `const` branches.
+- Verify complete-invalid input returns `INVALID_ARGUMENTS`, `used_fallback=False`, fallback calls `0`.
+- Verify a matching branch missing only its required value invokes fallback exactly once; fully ambiguous invalid input remains no-fallback.
+- Prove validation summaries cannot mutate registry behavior and do not retain caller-owned instances or schema objects.
+- Preserve handler/result mutation isolation for ordinary nested JSON values.
+
+### Observe
+
+Confirm the LLM fallback count now reflects genuinely missing information, and malformed argument objects are contained as stable user/resolution failures rather than exceptions or definition errors.
+
+### Update
+
+- Re-run Todo 45 specification and quality reviews over the full range.
+- Mark Todos 45 through 45.5 done only after fresh verification.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_kernel.py tests/test_event_agent.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 61 - 0
docs/plans/todo-45.6-mixed-validation-and-handler-snapshot.md

@@ -0,0 +1,61 @@
+# Todo 45.6 Mixed Validation and Handler Snapshot Plan
+
+**Status:** done
+
+## Result
+
+Fallback requires every reported validation issue to be missing-only. Required plus substantive validation errors skip LLM calls, and arbitrary payload snapshot exceptions become `HANDLER_ERROR` across sync, async, ToolRegistry, and EventAgent paths.
+
+## Goal
+
+Close the final reviewed cost and error-isolation gaps: mixed validation failures must never trigger LLM fallback, and arbitrary exceptions while snapshotting a handler payload must become normalized handler failures.
+
+## Scope
+
+- Treat validation as fallback-eligible only when every reported top-level failure classifies as missing-only.
+- Cover required fields combined with enum, type, additional-property, and composed-schema substantive errors.
+- Catch any normal exception raised while producing a strict JSON snapshot of a handler payload in async and sync execution.
+- Verify ToolRegistry and EventAgent receive normalized error payloads rather than exceptions.
+
+## Boundaries
+
+- Preserve fallback for pure missing-only input, including the unique viable composed branch from Todo 45.5.
+- Do not catch `BaseException` subclasses such as cancellation or process-exit signals.
+- Keep strict JSON rejection behavior; do not coerce unusual dictionary subclasses or values.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `tests/test_event_kernel.py`
+- Modify: `tests/test_event_agent.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED tests for required plus enum/type/additionalProperties and required plus composed substantive errors; fallback count must remain zero.
+2. Add RED tests whose handler returns a dictionary subclass raising `RuntimeError` during JSON serialization.
+3. Require all validation issues to be missing-only before setting `missing_required=True`.
+4. Normalize strict payload snapshot exceptions to `HANDLER_ERROR` in sync and async paths.
+5. Verify the behavior through EventKernel, ToolRegistry, and EventAgent, then run full reviews.
+
+### Act
+
+- Assert mixed invalid input reports `INVALID_ARGUMENTS`, `used_fallback=False`, and zero fallback calls.
+- Assert pure missing input still falls back exactly once.
+- Assert exploding payload snapshots return `event handler returned non-JSON payload` without escaping from kernel, ToolRegistry, or EventAgent.
+
+### Observe
+
+Confirm fallback counts represent only missing information and every ordinary plugin payload failure remains isolated inside the event result pipeline.
+
+### Update
+
+- Re-run final Todo 45 specification and quality reviews.
+- Mark Todos 45 through 45.6 done only after fresh verification.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_kernel.py tests/test_event_agent.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 41 - 0
docs/plans/todo-45.7-structured-incomplete-mixed-validation.md

@@ -0,0 +1,41 @@
+# Todo 45.7 Structured Incomplete Mixed Validation Plan
+
+**Status:** done
+
+## Result
+
+Separated fallback eligibility from incomplete-result normalization. Structured `complete=False` no longer bypasses enum, type, or additional-property errors; optional-only incomplete and pure missing-only paths remain eligible.
+
+## Goal
+
+Ensure a structured resolver's explicit `complete=False` signal cannot trigger LLM fallback when the resolved arguments also contain substantive validation errors.
+
+## Scope
+
+- Add structured-incomplete regressions for required plus enum, type, and additional-property failures.
+- Compute fallback eligibility before converting optional-schema incomplete results into `INVALID_ARGUMENTS`.
+- Allow fallback only for pure missing-only validation or explicit incomplete with no substantive validation error.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `tests/test_event_kernel.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED tests proving `EventArgumentResolution(..., complete=False)` plus mixed substantive errors currently calls fallback.
+2. Separate `fallback_eligible` from the normalized invalid result status.
+3. Preserve optional-only explicit incomplete fallback and pure required-missing fallback.
+4. Run focused/full tests and final bounded reviews.
+
+### Observe / Update
+
+Close Todo 45 only when the named reproductions pass and no Critical/Important findings remain.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_kernel.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 91 - 0
docs/plans/todo-46-configurable-tool-invocation-mode.md

@@ -0,0 +1,91 @@
+# Todo 46 Configurable Tool Invocation Mode Plan
+
+**Status:** done
+
+## Result
+
+Added request-level `dual_agent` and `chat_agent_tools` modes. Direct mode sends the same enabled registry schemas to ChatAgent, executes provider-resolved calls through the shared kernel, preserves visible content plus ordered assistant/tool transcripts, and records mode in audits/session config. Default dual behavior remains backward compatible. Final verification: `217 passed`, one existing warning.
+
+## Review Update
+
+The initial implementation passed the specification review but remains open pending Todo 46.1 fixes for duplicate provider IDs, direct/dual batch scheduling parity, and symmetric event-budget enforcement.
+
+## Goal
+
+Add a backend-selectable comparison between the existing fast-reply `dual_agent` path and a `chat_agent_tools` path where ChatAgent receives provider tools directly, while both modes use the same registry, schemas, handlers, event budget, and externally visible tool results.
+
+## Decisions
+
+- Add `tool_invocation_mode` to `DebugRunRequest`, defaulting to `dual_agent` for backward compatibility.
+- Keep mode at the orchestration/request level, not in model parameters or process settings.
+- `dual_agent` keeps the existing compact text-event prompt, EventAgent queue, deterministic-first resolution, optional LLM fallback, and summary follow-up.
+- `chat_agent_tools` sends the enabled registry schemas to ChatAgent, consumes only `provider_tool_call`, executes those calls through the shared `EventKernel` with `PROVIDER_RESOLVED`, and continues with a valid assistant/tool provider transcript.
+- Provider-resolved arguments are not rewritten by EventAgent fallback in this todo.
+- UI selection and reconnect/session transcript restoration remain Todo 49.
+
+## Scope
+
+- Validate the two supported mode values and reject unknown values.
+- Mode-select ChatAgent prompt injection, tools, and accepted stream event type.
+- Execute one or multiple direct provider calls through the shared registry/kernel with enabled-name enforcement.
+- Preserve provider call ID, name, arguments, raw arguments, assistant visible content, tool-call order, and tool-reply order.
+- Append direct assistant `tool_calls` followed immediately by matching tool messages before the next ChatAgent request.
+- Reuse `max_event_loops`; after budget exhaustion, send no tools and ignore further provider tool calls.
+- Include mode in audit/session config snapshots without changing the current UI payload.
+- Cover both one-shot `run()` and reusable session/turn runtime paths.
+
+## Boundaries
+
+- Do not modify parser, provider HTTP client, or message model unless a failing contract proves a gap; typed calls and transcript serialization already exist.
+- Do not route direct mode through the EventAgent request/summary queue.
+- Do not let direct transcripts contaminate `dual_agent`; dual mode must continue to ignore provider tool calls.
+- Do not implement built-in production event plugins, bounded batch scheduling, UI controls, restore migrations, or comparison metrics in this todo.
+- Unknown or disabled provider tool names must produce a structured tool result and never execute a handler.
+
+## Files
+
+- Modify: `src/agent_lab/application/contracts.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `tests/test_websocket_api.py`
+- Modify: `src/agent_lab/application/tools.py` only if an enabled-name provider execution facade is needed to avoid Runtime reaching into registry internals.
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED contract tests for default, valid modes, and invalid mode rejection.
+2. Add RED runtime tests proving mode-specific prompt/tool payloads and typed stream consumption.
+3. Add RED direct-mode transcript tests for mixed visible content plus one/multiple provider tool calls.
+4. Add RED execution tests for enabled, disabled, unknown, handler failure, and exhausted event budget.
+5. Implement the smallest mode helpers shared by one-shot and session turn loops; preserve existing audit/output contracts.
+6. Run focused/full verification and independent specification/quality reviews.
+
+### Act
+
+- In `dual_agent`, assert ChatAgent receives `tools=[]`, receives compact event instructions, ignores `provider_tool_call`, and continues through EventAgent summary exactly as before.
+- In `chat_agent_tools`, assert ChatAgent receives exactly the enabled schemas and no compact text-event system block.
+- Collect only provider calls in direct mode; emit existing `event` output/audit records with full provider arguments.
+- Create one assistant message containing both visible content and ordered `tool_calls`.
+- Execute calls using `enabled_names`; create ordered `tool` replies with matching IDs, emit `tool_result`, append them to active history, and continue ChatAgent.
+- Assert the second provider request transcript is `assistant(tool_calls)` followed by all matching tool replies.
+- Assert direct mode ignores text events and dual mode ignores provider calls.
+- Assert the next round sends `tools=[]` after `max_event_loops` is reached.
+- Include `tool_invocation_mode` in session config and relevant round/request audit details.
+
+### Observe
+
+Compare both paths against the same fake registry and scenarios. Confirm the architectural difference is limited to model/tool orchestration, not schemas, handlers, results, or event budget.
+
+### Update
+
+- Record verification evidence and review findings.
+- Add `46.x` rows for any post-commit correction.
+- Mark Todo 46 done only when dual-agent regressions and direct provider transcripts both pass.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `UV_CACHE_DIR=.uv-cache uv run python -c "from agent_lab.main import app; print(app.title)"`
+- `git diff --check`

+ 49 - 0
docs/plans/todo-46.1-mode-parity-and-duplicate-call-safety.md

@@ -0,0 +1,49 @@
+# Todo 46.1 Mode Parity and Duplicate Call Safety Plan
+
+**Status:** done
+
+## Result
+
+Added pre-side-effect duplicate ID rejection, ordered concurrent direct execution, and symmetric event-budget enforcement for direct and dual modes. Specification and quality gates passed with no Critical/Important findings.
+
+## Goal
+
+Remove three review-confirmed sources of unsafe or unfair comparison behavior between `dual_agent` and `chat_agent_tools`.
+
+## Scope
+
+- Detect provider tool-call IDs already declared in active direct-mode history before creating a new assistant transcript or executing handlers.
+- Prevent duplicate IDs within a batch and across rounds from causing repeated side effects.
+- Execute direct-mode batches concurrently while preserving provider call order in returned tool messages.
+- Apply `max_event_loops` acceptance to dual text events as well as direct provider calls.
+
+## Boundaries
+
+- A duplicate provider call ID should fail the current runtime before the duplicate handler runs; do not invent a second invalid assistant/tool transcript.
+- Use ordinary `asyncio.gather` parity for this todo; bounded concurrency, conflict classes, terminal barriers, and timeout policy remain Todo 48.
+- Preserve visible reply streaming that occurred before a duplicate is detected.
+
+## Files
+
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `tests/test_debug_runtime.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED one-shot and session tests for duplicate provider IDs across rounds; handler call count must remain one.
+2. Add a timing/overlap RED test proving two direct async handlers start before either finishes and replies retain input order.
+3. Add RED dual-mode tests proving events after budget exhaustion are ignored and the loop terminates.
+4. Implement pre-execution ID validation, concurrent direct execution, and symmetric budget acceptance.
+5. Run focused/full verification and renewed reviews.
+
+### Observe / Update
+
+Close Todo 46 only when both modes differ only in orchestration protocol, not duplicate safety, event budget, or handler scheduling semantics.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_debug_runtime.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 111 - 0
docs/plans/todo-47-built-in-event-plugins.md

@@ -0,0 +1,111 @@
+# Todo 47 Built-in Event Plugins Plan
+
+**Status:** done
+
+## Result
+
+Added four flat namespaced built-in plugins with injectable session, device, calendar, and search ports plus deterministic in-memory adapters. The default registry preserves the original lab tools and exposes the new definitions to both invocation modes. Final full verification: `313 passed`, one existing warning.
+
+## Review Update
+
+The initial plugin implementation remains open pending Todo 47.1 corrections for shared deterministic normalization, strict calendar/query validation, safe volume parsing, and adapter idempotency scope.
+
+## Goal
+
+Add four namespaced built-in plugins on the generic event kernel without introducing event-name branches in Runtime or the kernel:
+
+- `session.terminate`
+- `device.volume.adjust`
+- `calendar.schedule.create`
+- `knowledge.web.search`
+
+## Architecture
+
+- Keep the registry flat.
+- Each plugin definition owns its name, concise description, JSON Schema, deterministic resolver, fallback policy, handler adapter, result/confirmation/risk policy, idempotency metadata, concurrency/conflict metadata, timeout, and terminal flag.
+- Put production side effects behind injectable application ports. The lab default uses deterministic in-memory/mock adapters; `mituan-service` integration remains a separate repository task.
+- `build_default_tool_registry()` includes the four plugins while preserving existing lab mock tools.
+- Both invocation modes discover and execute the same definitions automatically through the existing registry/kernel.
+
+## Plugin Contracts
+
+### `session.terminate`
+
+- Arguments: optional `reason` string.
+- Deterministic, no LLM fallback required.
+- Port: dialogue/session termination boundary, keyed by event ID.
+- Policy: `TERMINATE`, high risk, terminal, serialized against session lifecycle work.
+- Default adapter returns a normalized terminated result and is idempotent by event ID.
+
+### `device.volume.adjust`
+
+- Arguments:
+  - `mode`: `absolute`, `relative`, `mute`, or `unmute`;
+  - `value`: integer `0..100` for absolute;
+  - `delta`: integer `-100..100`, excluding zero, for relative.
+- JSON Schema enforces mode-specific required/forbidden fields.
+- Deterministic resolver recognizes clear Chinese/English mute, unmute, absolute, and signed relative requests; incomplete requests remain eligible for EventAgent fallback.
+- Port: device-action dispatcher boundary, keyed by event ID.
+- Policy: `SILENT_SUCCESS`, medium risk, device-volume conflict class.
+- Invalid/out-of-range/ambiguous requests never call the port.
+
+### `calendar.schedule.create`
+
+- Arguments: `title`, RFC3339 `start_at`, IANA-style `timezone`, optional `recurrence`, optional non-negative `reminder_minutes`.
+- Deterministic resolver accepts already structured arguments and explicit ISO/RFC3339 details only; it must not guess relative or ambiguous natural-language times.
+- Missing/ambiguous fields can use the existing EventAgent LLM fallback in dual mode.
+- Port: calendar/reminder application boundary with event-ID idempotency.
+- Policy: `TEMPLATE_FOLLOW_UP`, medium risk, schedule-write concurrency class.
+- Default adapter deduplicates repeated event IDs and returns normalized confirmation data.
+
+### `knowledge.web.search`
+
+- Arguments: focused non-empty `query`, optional `max_results` from `1..5`.
+- Deterministic resolver derives the latest relevant user request when provider arguments are absent.
+- Port: external-search adapter returning a stable compact result contract.
+- Policy: `LLM_FOLLOW_UP`, low risk, read-only concurrency class.
+- Default adapter returns deterministic mock sources with retrieval metadata; no network or credentials.
+
+## Scope
+
+- Add plugin definitions and injectable ports/default adapters.
+- Register plugins in the default registry and expose them from the existing tools API/catalog.
+- Add focused schema, resolver, metadata, execution, failure, idempotency, and mode-parity tests.
+- Preserve existing `handoff_note`, `mock_search`, and `mock_ticket` lab tools.
+
+## Boundaries
+
+- Do not implement real MQTT/device commands, calendar storage, session shutdown, or internet search.
+- Do not add name-specific branches to Runtime, EventAgent, EventKernel, or provider parsers.
+- Do not implement bounded batch scheduling, terminal barriers, result-policy aggregation, or two-answer runtime behavior here; those are Todo 48.
+- Do not guess schedule relative times without an explicit clock/timezone context.
+
+## Files
+
+- Add: `src/agent_lab/application/events/builtin_plugins.py`
+- Modify: `src/agent_lab/application/events/__init__.py`
+- Modify: `src/agent_lab/application/tools.py`
+- Add: `tests/test_builtin_event_plugins.py`
+- Modify: `tests/test_websocket_api.py` only for default tools catalog coverage if needed.
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED catalog/metadata tests for all four namespaced plugins.
+2. Add RED valid/invalid resolver and schema tests per plugin.
+3. Add RED port execution, normalized failure, and idempotency tests.
+4. Implement ports, default adapters, and flat definitions without touching generic orchestration.
+5. Verify both `dual_agent` and `chat_agent_tools` receive the same schemas/results for the built-ins.
+6. Run focused/full verification and independent reviews.
+
+### Observe / Update
+
+Record any plugin contract ambiguity as a `47.x` todo. Mark complete only when adding these plugins requires no Runtime/kernel event-name conditionals and all external actions remain behind ports.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_builtin_event_plugins.py tests/test_event_agent.py tests/test_debug_runtime.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `UV_CACHE_DIR=.uv-cache uv run python -c "from agent_lab.application.tools import build_default_tool_registry; print([x['name'] for x in build_default_tool_registry().available_tools()])"`
+- `git diff --check`

+ 57 - 0
docs/plans/todo-47.1-plugin-normalization-and-validation.md

@@ -0,0 +1,57 @@
+# Todo 47.1 Plugin Normalization and Validation Plan
+
+**Status:** done
+
+## Result
+
+Added generic deterministic normalizers shared by text and provider sources, standard/custom format checking, strict blank/invalid input rejection, safe volume parsing, and canonical adapter cache keys while preserving provider raw arguments and no-fallback behavior.
+
+## Goal
+
+Close review-confirmed plugin safety gaps while preserving the generic kernel and identical built-in behavior across `dual_agent` and `chat_agent_tools`.
+
+## Scope
+
+- Add an optional generic event argument normalizer that runs after text-event resolution and on provider-resolved arguments, before validation and execution.
+- Keep provider-resolved arguments ineligible for EventAgent LLM fallback; normalization is deterministic only.
+- Move canonical volume/search structured normalization into this shared hook.
+- Treat negated volume commands as incomplete/invalid and capture full numeric tokens so values such as `1000` are rejected rather than truncated to `100`.
+- Enforce real RFC3339 date-time validity and real IANA timezone validity through generic JSON Schema format checking.
+- Reject blank/whitespace-only search queries and blank schedule titles.
+- Prevent in-memory adapters from returning stale results when short parser event IDs collide with different normalized arguments.
+
+## Idempotency Decision
+
+The lab adapters will cache by event ID plus canonical normalized arguments, not event ID alone. This preserves same-call replay behavior while preventing `event_1` with a different operation from returning an earlier result. Runtime-scoped deduplication and production session/turn idempotency remain Todo 48/service integration responsibilities.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/models.py`
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `src/agent_lab/application/events/registry.py`
+- Modify: `src/agent_lab/application/events/__init__.py`
+- Modify: `src/agent_lab/application/events/builtin_plugins.py`
+- Modify: `src/agent_lab/application/tools.py` only to preserve ToolDefinition compatibility with the new optional normalizer
+- Modify: `tests/test_event_kernel.py`
+- Modify: `tests/test_builtin_event_plugins.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED generic-kernel tests proving normalizers run for both sources, preserve provider raw arguments, and never enable provider LLM fallback.
+2. Add RED plugin parity tests for canonicalization through text and provider sources.
+3. Add RED safety tests for English/Chinese negation, `1000`, invalid dates, invalid IANA zones, blank query/title, and no port calls.
+4. Add RED adapter tests for same short ID with different arguments and replay with identical arguments.
+5. Implement generic format checking and the smallest plugin normalizers/validators.
+6. Run focused/full verification and renewed reviews.
+
+### Observe / Update
+
+Close Todo 47 only when all four plugins reject unsafe inputs before ports, both invocation modes share deterministic normalization, and no event-name logic enters generic orchestration.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_kernel.py tests/test_builtin_event_plugins.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 50 - 0
docs/plans/todo-47.2-plugin-token-and-compatibility-boundaries.md

@@ -0,0 +1,50 @@
+# Todo 47.2 Plugin Token and Compatibility Boundaries Plan
+
+**Status:** done
+
+## Result
+
+Closed negation and malformed numeric side-effect gaps, canonicalized integral floats, preserved fallback provenance, restored standard RFC3339 behavior, and kept positional definition constructors compatible.
+
+## Goal
+
+Close the remaining quality-gate findings without expanding plugin behavior or generic orchestration scope.
+
+## Scope
+
+- Reject `不静音`, `不用静音`, `无需静音`, `no mute`, and equivalent negations as substantive invalid input with no LLM fallback or port call.
+- Reject decimal/grouped numeric phrases such as `40.5`, `10.5`, and `1,000` without truncating them into valid integers.
+- Canonicalize JSON Schema integral floats (`2.0`, `40.0`) to Python `int` for volume, search, and schedule numeric fields before ports/cache keys.
+- Preserve `used_fallback=True` when a post-fallback normalizer fails.
+- Use the standard `jsonschema` RFC3339 `date-time` checker; keep only the custom `iana-timezone` checker.
+- Move newly added dataclass fields to the end so existing positional `EventDefinition` and `ToolDefinition` construction keeps its historical meaning.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/models.py`
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `src/agent_lab/application/events/registry.py`
+- Modify: `src/agent_lab/application/events/builtin_plugins.py`
+- Modify: `src/agent_lab/application/tools.py`
+- Modify: `tests/test_event_kernel.py`
+- Modify: `tests/test_builtin_event_plugins.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED tests for all named negation and malformed numeric phrases; assert zero fallback and zero port calls.
+2. Add RED canonical-number tests proving ports/cache treat integral floats as integers and equivalent values share cache keys.
+3. Add RED post-fallback normalizer failure and positional-constructor compatibility tests.
+4. Add RED lowercase `t`/`z` RFC3339 acceptance while retaining invalid-calendar rejection.
+5. Implement the smallest parser/normalizer/kernel/dataclass changes and run final bounded reviews.
+
+### Observe / Update
+
+Close Todo 47 only when no Critical/Important findings remain and all external side effects are protected by canonical validated arguments.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_kernel.py tests/test_builtin_event_plugins.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 28 - 0
docs/plans/todo-47.3-bare-volume-numeric-guard.md

@@ -0,0 +1,28 @@
+# Todo 47.3 Bare Volume Numeric Guard Plan
+
+**Status:** done
+
+## Result
+
+Mirrored malformed-number protection across the existing bare `volume to`, `volume at`, and `volume=` syntax; final bounded quality gate approved.
+
+## Goal
+
+Close the final named volume-token gap by applying the existing malformed-number guard to bare `volume to`, `volume at`, and `volume=` forms.
+
+## Scope
+
+- Add RED tests for `volume to 40.5`, `volume at 1,000`, and `volume=40.5`.
+- Assert no fallback, no port call, and structured invalid arguments.
+- Preserve valid bare integer forms such as `volume to 40`, `volume at 40`, and `volume=40`.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/builtin_plugins.py`
+- Modify: `tests/test_builtin_event_plugins.py`
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_builtin_event_plugins.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 114 - 0
docs/plans/todo-48-parallel-event-batches.md

@@ -0,0 +1,114 @@
+# Todo 48 Parallel Event Batches and Result Policies Plan
+
+**Status:** done
+
+## Result
+
+Added a shared policy-driven `EventBatchExecutor` used by direct and dual modes. It provides bounded parallelism, conflict serialization, per-event/batch timeout handling, stable ordered results, failure isolation, scoped replay/deduplication, terminal-last execution, and generic policy aggregation for silent, template, LLM, and terminate outcomes. Final full verification: `372 passed`, one existing warning.
+
+## Goal
+
+Execute flat multi-event batches through one shared scheduler in both invocation modes, then apply plugin-declared result policies without any event-name branches.
+
+## Shared Batch Executor
+
+Add a generic `EventBatchExecutor` that owns batch mechanics around an `EventKernel`:
+
+- configurable global concurrency limit;
+- per-event timeout from `EventDefinition.timeout_seconds`;
+- overall batch deadline;
+- stable result ordering matching input order;
+- failure and timeout isolation;
+- conflict-key locking so mutually conflicting events do not overlap;
+- non-terminal events first, then terminal events after all accepted siblings reach a terminal result or timeout;
+- exact duplicate coalescing inside a batch;
+- replay protection scoped by caller-provided session/turn scope plus event ID and canonical request data.
+
+The executor remains generic and never checks built-in event names.
+
+## Result Policy Aggregation
+
+Runtime derives one continuation decision from ordered event results and definition metadata:
+
+- `TERMINATE` success: emit any deterministic sibling confirmations, finish the turn/session, and suppress another ChatAgent call.
+- `SILENT_SUCCESS` success: keep the already streamed first acknowledgement and do not call ChatAgent again.
+- `TEMPLATE_FOLLOW_UP` success: emit a plugin-owned deterministic user message, append it to active history, and do not call ChatAgent again unless another result requires LLM synthesis.
+- `LLM_FOLLOW_UP` success: continue ChatAgent exactly once with compact results.
+- Any non-terminal failure/timeout: request one aggregated corrective ChatAgent follow-up.
+- Mixed batches: deterministic templates first, then at most one LLM follow-up; successful termination suppresses LLM follow-up after siblings finish.
+
+Plugins own deterministic template text through generic definition metadata or a stable payload field; Runtime does not format by event name.
+
+## Runtime and EventAgent Flow
+
+- Replace unbounded `asyncio.gather` in EventAgent and direct Runtime with the same batch executor.
+- Keep the EventAgent worker boundary for `dual_agent`; it resolves arguments with deterministic-first/LLM fallback and returns ordered tool replies.
+- Stop unconditionally enqueueing the legacy EventAgent summary. Runtime builds one compact summary only when the aggregated policy requires an LLM follow-up.
+- Direct mode keeps assistant/tool provider transcript and uses the same ordered batch results.
+- Reuse `EventAgentParams` for shared batch configuration:
+  - `max_parallel_events` default `4`, constrained to `1..16`;
+  - `batch_timeout_seconds` default `15`, positive and bounded.
+- Scope replay protection by session/turn so parser-local short IDs do not collide across turns.
+
+## Built-in Acceptance
+
+- `session.terminate`: farewell first, accepted siblings finish or time out, termination executes last, no second ChatAgent call, reusable session emits terminal completion.
+- `device.volume.adjust`: successful action has no second answer; failure gets one corrective answer.
+- `calendar.schedule.create`: successful action emits a deterministic confirmation without a model call.
+- `knowledge.web.search`: first answer, search, then one distinct ChatAgent follow-up grounded in compact results.
+- A mixed batch can contain schedule + web search + volume + terminate while preserving stable result order and terminal semantics.
+
+## Scope
+
+- Add batch execution/result types and timeout status.
+- Add shared scheduler use in EventAgent and direct Runtime.
+- Add generic tool-reply/result-policy aggregation helpers.
+- Add deterministic schedule confirmation owned by the plugin.
+- Add audit/output evidence for batch start/results, timeouts, deduplication, policy decision, and terminal completion.
+- Add focused executor, EventAgent, Runtime, and built-in behavior tests.
+
+## Boundaries
+
+- No retry framework beyond existing single execution; web retry remains future work.
+- No dynamic DAG, workflow engine, distributed lock, or production idempotency store.
+- Do not implement UI, reconnect transcript persistence, or comparison metrics here.
+- Do not cancel successful siblings because another event fails.
+- Do not catch `BaseException`/cancellation as an ordinary event failure.
+
+## Files
+
+- Add: `src/agent_lab/application/events/batch.py`
+- Modify: `src/agent_lab/application/events/models.py`
+- Modify: `src/agent_lab/application/events/__init__.py`
+- Modify: `src/agent_lab/application/contracts.py`
+- Modify: `src/agent_lab/application/tools.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/application/events/builtin_plugins.py`
+- Add: `tests/test_event_batch.py`
+- Modify: `tests/test_event_agent.py`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `tests/test_builtin_event_plugins.py`
+- Modify: `tests/test_websocket_api.py` for config validation/defaults if needed
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED unit tests for bounded overlap, stable order, conflict serialization, timeout isolation, duplicate coalescing, scoped replay, and terminal-last execution.
+2. Add RED EventAgent/direct parity tests proving both use the same scheduler and produce the same ordered payloads.
+3. Add RED policy aggregation tests for silent, template, LLM, failure, terminate, and mixed batches.
+4. Add RED reusable-session termination and web-search two-answer tests.
+5. Implement the minimum generic scheduler and aggregation helpers.
+6. Run focused/full verification and independent specification/quality reviews.
+
+### Observe / Update
+
+Add `48.x` rows for any review-driven correction. Mark complete only when both modes share the scheduler and all four built-in result policies work without name-specific Runtime logic.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_batch.py tests/test_event_agent.py tests/test_debug_runtime.py tests/test_builtin_event_plugins.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `UV_CACHE_DIR=.uv-cache uv run python -c "from agent_lab.main import app; print(app.title)"`
+- `git diff --check`

+ 75 - 0
docs/plans/todo-48.1-batch-canonical-replay-and-terminal-safety.md

@@ -0,0 +1,75 @@
+# Todo 48.1 Batch Canonical Replay and Terminal Safety Plan
+
+**Status:** done
+
+## Result
+
+Added canonical normalized keys, atomic in-flight sharing, bounded/releasable scopes, terminal grace after sibling timeout, off-event-loop sync handlers, deterministic fallback farewell, and dedicated direct/dual web-search two-answer tests.
+
+## Goal
+
+Close review-confirmed batch correctness, lifecycle, timeout, and user-visible termination gaps while keeping the scheduler and policy layer generic.
+
+## Canonical Dedupe and Replay
+
+- Split canonical request identity from event ID:
+  - batch coalescing uses event name, source, and deterministically normalized request arguments, excluding event ID;
+  - replay identity additionally includes caller scope and event ID.
+- Apply definition normalizers before forming provider/structured request keys. Text name-only events may coalesce within one batch, but Runtime scopes must include round so parser-local IDs cannot replay stale results across rounds before resolution.
+- Clone coalesced/replayed `EventResult` correlation fields for each requested event ID while retaining explicit deduplication provenance.
+- Add an in-flight future map guarded atomically so concurrent identical replay keys execute one handler.
+- Add explicit scope release and a defensive capacity bound; Runtime/EventAgent release round scopes after results are delivered.
+
+## Terminal Deadline
+
+- Non-terminal siblings use the configured batch deadline.
+- After siblings finish or become timeout results, terminal events receive a short independent grace window bounded by their own timeout.
+- Mark `deadline_exceeded=True` for the sibling phase while still allowing terminal execution.
+- Preserve terminal-last order and failure isolation.
+
+## Sync Handler Timeout
+
+- Run synchronous handlers off the event loop in async kernel execution so event/batch timeout can return promptly and unrelated events/session work remain responsive.
+- Keep `execute_sync()` behavior unchanged.
+- Do not swallow cancellation/BaseException.
+- Document/test that timeout stops awaiting the worker; production side-effect ports should remain cancellation/idempotency aware.
+
+## Farewell and Search Acceptance
+
+- Give `session.terminate` a plugin-owned deterministic farewell factory.
+- When a successful terminal batch has no visible ChatAgent content, emit that deterministic farewell before ending one-shot/reusable sessions.
+- If ChatAgent already streamed a farewell, do not duplicate it.
+- Add dedicated `knowledge.web.search` two-answer regressions for both modes: first visible response, search/tool execution, one grounded second ChatAgent response.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/batch.py`
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `src/agent_lab/application/events/models.py`
+- Modify: `src/agent_lab/application/events/builtin_plugins.py`
+- Modify: `src/agent_lab/application/tools.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `tests/test_event_batch.py`
+- Modify: `tests/test_event_kernel.py`
+- Modify: `tests/test_debug_runtime.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED normalized-equivalence, different-ID coalescing, cross-round stale replay, concurrent in-flight replay, and scope cleanup tests.
+2. Add RED sibling-timeout/terminal-grace and blocking-sync-handler timeout tests.
+3. Add RED tool-call-only terminate farewell and direct/dual search two-answer tests.
+4. Implement canonical keys, atomic in-flight sharing, bounded cleanup, terminal grace, async sync-handler dispatch, and policy-owned farewell.
+5. Run focused/full verification and renewed bounded reviews.
+
+### Observe / Update
+
+Close Todo 48 only when no Critical/Important findings remain and the named direct/dual user-visible flows are permanent regressions.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_batch.py tests/test_event_kernel.py tests/test_debug_runtime.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 58 - 0
docs/plans/todo-48.2-outcome-cache-and-sync-resource-lifecycle.md

@@ -0,0 +1,58 @@
+# Todo 48.2 Outcome Cache and Sync Resource Lifecycle Plan
+
+**Status:** done
+
+## Result
+
+Replay now preserves complete outcomes, released scopes cannot late-store, invalid arguments cannot inherit success, timed-out synchronous work retains conflict/concurrency leases until actual completion, and coalesced results retain correct IDs with one logical policy message.
+
+## Goal
+
+Close the remaining Important batch findings by preserving complete execution semantics in cache/replay and keeping physical concurrency resources held while timed-out synchronous work is still running.
+
+## Scope
+
+- Canonical key generation must tag non-JSON/normalizer-invalid arguments distinctly instead of collapsing them to `{}`.
+- Cache/replay `_ExecutionOutcome`, including `batch_timed_out`, rather than inferring phase semantics from error text.
+- Add per-scope generations:
+  - `release_scope()` advances the generation and clears completed/in-flight mappings for the released generation;
+  - late completion from an old generation cannot repopulate cache;
+  - immediate scope reuse operates on a new generation.
+- For synchronous handlers dispatched to a worker thread:
+  - return timeout promptly;
+  - keep semaphore/conflict locks leased until the underlying shielded kernel task actually completes;
+  - consume late exceptions and release resources in a completion callback;
+  - async handlers remain cancellable normally.
+- Add explicit `deduplicated_from` result provenance and clone correlation safely:
+  - outer `event_id` and tool reply ID match each requested call;
+  - top-level payload `event_id` is rewritten only when it equals the primary result ID;
+  - Runtime policy aggregation processes a coalesced logical result once, preventing duplicate farewells/templates.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/models.py`
+- Modify: `src/agent_lab/application/events/batch.py`
+- Modify: `src/agent_lab/application/tools.py`
+- Modify: `src/agent_lab/application/runtime.py` only if audit/provenance exposure needs alignment
+- Modify: `tests/test_event_batch.py`
+- Modify: `tests/test_debug_runtime.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED tests for invalid-vs-empty canonical keys, terminal timeout replay flags, release/reuse with late completion, and concurrent in-flight cleanup.
+2. Add RED sync timeout lease tests proving conflicting/new work cannot start until the worker actually finishes.
+3. Add RED different-ID payload correlation and single farewell/template tests.
+4. Implement outcome caching, scope generations, deferred resource release, and explicit dedupe provenance.
+5. Run focused/full verification and final bounded reviews.
+
+### Observe / Update
+
+Close Todo 48 only when all named concurrency/cache findings are permanent regressions and no Critical/Important issues remain.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_batch.py tests/test_debug_runtime.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 33 - 0
docs/plans/todo-48.3-strict-canonical-types-and-scope-token-cleanup.md

@@ -0,0 +1,33 @@
+# Todo 48.3 Strict Canonical Types and Scope Token Cleanup Plan
+
+**Status:** done
+
+## Result
+
+Canonical equality now uses strict recursive JSON types, and unique scope tokens are removed on release while still preventing old late tasks from storing into reused scopes. Final bounded gate approved.
+
+## Goal
+
+Close the final bounded batch findings: canonical identity must use strict JSON type equality, and releasing a scope must retain no permanent generation tombstone.
+
+## Scope
+
+- Compare JSON round-trip values recursively with exact types, matching kernel strictness; `IntEnum(1)`, `True`, `1`, and `1.0` must not be conflated unless a declared normalizer intentionally canonicalizes them.
+- Replace per-scope integer generations with globally unique generation tokens:
+  - first execution for a scope allocates a unique token;
+  - concurrent calls for the active scope share the token;
+  - `release_scope()` removes the scope token and associated maps;
+  - immediate reuse allocates a new token that can never equal an old late task token;
+  - late completion checks token equality and cannot store when the scope is absent or reused.
+- Assert thousands of execute/release cycles leave no scope-token, replay, or in-flight growth.
+
+## Files
+
+- Modify: `src/agent_lab/application/events/batch.py`
+- Modify: `tests/test_event_batch.py`
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_event_batch.py -q`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 75 - 0
docs/plans/todo-49-tool-mode-ui-session-restore.md

@@ -0,0 +1,75 @@
+# Todo 49 Tool Mode UI and Session Restore Plan
+
+**Status:** done
+
+## Goal
+
+Make invocation mode and batch configuration visible/configurable in the debug UI, and restore a persisted direct-mode conversation with a provider-valid assistant/tool transcript.
+
+## Observe / Update
+
+- UI request and workspace/session snapshots preserve invocation mode, parallelism, and timeout configuration.
+- Direct-mode protocol history is persisted and restored provider-validly; follow-up Todos 49.1-49.3 closed atomicity, legacy repair, migration, and asynchronous replay races.
+- Final verification after follow-ups: `388 passed`, `node --check` and `git diff --check` pass; one pre-existing Starlette deprecation warning remains.
+
+## UI
+
+- Add a mode selector with:
+  - `Dual Agent — fast reply + EventAgent` (default);
+  - `ChatAgent Tools — native provider tools`.
+- Add concise mode help/applicability text:
+  - dual mode: EventAgent model/system/extra-body may be used for missing argument fallback;
+  - direct mode: ChatAgent proposes provider-resolved arguments, so EventAgent model prompt settings are inactive; enabled tools, loop budget, batch concurrency, and timeout still apply.
+- Add `max_parallel_events` and `batch_timeout_seconds` controls.
+- Include mode and new settings in WebSocket requests and workspace snapshots.
+- Restore them from workspace snapshots and persisted session config, with backward-compatible defaults.
+- When loading a session, fetch `/api/sessions/{id}` together with replay/audit/usage and apply its saved config to the UI.
+- New manually created sessions save the current workspace config.
+
+## Persisted Transcript
+
+- Persist assistant `tool_calls` as JSON alongside role/content/name/tool-call ID.
+- Add a safe SQLite migration for existing databases.
+- Return `tool_calls` from the messages API and reconstruct `ChatMessage` values for Runtime session restoration.
+- On reusable session start with an existing `session_id`, load prior user/assistant/tool messages into active history after current system/pre-message context.
+- Persist direct-mode tool replies as messages; dual mode continues to persist only user-visible conversational history and summaries, not provider tool protocol messages.
+- Validate restored transcript ordering/IDs before the next provider call through existing client validation.
+
+## Boundaries
+
+- Do not change result-policy behavior or batch scheduling.
+- Do not migrate historical rows into synthetic tool calls; old rows default to `[]`.
+- Do not expose internal tool messages as normal assistant chat bubbles beyond the existing tool replay style.
+- Comparison metrics remain Todo 50.
+
+## Files
+
+- Modify: `src/agent_lab/infrastructure/sqlite_store.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/presentation/static/index.html`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify: `src/agent_lab/presentation/static/styles.css` only if mode help layout needs it
+- Modify: `tests/test_sqlite_store.py`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `tests/test_websocket_api.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED persistence/migration tests for assistant tool calls and tool replies.
+2. Add RED Runtime reconnect tests proving the next direct provider request receives restored valid transcript.
+3. Add RED static/request/snapshot/session-load tests for mode and batch config.
+4. Implement the smallest schema migration, restore helper, and UI controls.
+5. Run Python, JS syntax, full tests, and a local browser/static check if layout changes materially.
+
+### Observe / Update
+
+Add `49.x` rows for review findings. Mark complete only when old sessions remain loadable and direct-mode restored transcripts pass provider validation.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_sqlite_store.py tests/test_debug_runtime.py tests/test_websocket_api.py -q`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`

+ 53 - 0
docs/plans/todo-49.1-session-mode-and-atomic-transcript-safety.md

@@ -0,0 +1,53 @@
+# Todo 49.1 Session Mode and Atomic Transcript Safety Plan
+
+**Status:** done
+
+## Goal
+
+Prevent session restoration from changing architecture semantics or persisting invalid provider transcripts.
+
+## Scope
+
+- Before updating an existing session config, read its persisted `tool_invocation_mode` (default `dual_agent` for legacy config) and reject an incoming mismatch with a stable error; do not overwrite config.
+- Add `SessionStore.append_messages()` and implement one SQLite transaction for a direct assistant tool-call message plus all matching tool replies.
+- In direct mode with tool calls:
+  - keep the assistant/tool transcript in active memory immediately;
+  - persist the complete protocol group only after batch execution returns all replies;
+  - cancellation/disconnect before completion leaves no partial protocol group.
+- Restore defensively from sessions created by earlier experimental code: drop an incomplete trailing assistant tool-call group or orphan tool replies rather than sending an invalid transcript.
+- Make SQLite column migration safe across concurrent store/process initialization using serialized migration and/or duplicate-column recheck.
+- UI replay skips assistant records whose content is empty and whose only purpose is tool protocol; tool rows retain existing tool styling.
+
+## Files
+
+- Modify: `src/agent_lab/application/session_store.py`
+- Modify: `src/agent_lab/infrastructure/sqlite_store.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify: `tests/test_sqlite_store.py`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `tests/test_websocket_api.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED mode-mismatch tests proving config/history remain unchanged.
+2. Add RED cancellation tests proving no partial assistant/tool group is stored and reconnect remains valid.
+3. Add RED incomplete-tail repair and concurrent migration stress tests.
+4. Add RED static replay test for tool-call-only assistant suppression.
+5. Implement atomic persistence, restore repair, migration serialization, and UI filtering.
+6. Run focused/node/full verification and final reviews.
+
+## Verification
+
+- `UV_CACHE_DIR=.uv-cache uv run pytest tests/test_sqlite_store.py tests/test_debug_runtime.py tests/test_websocket_api.py -q`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `UV_CACHE_DIR=.uv-cache uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- Persisted mode mismatches fail before config overwrite.
+- Direct assistant tool calls and replies commit as one SQLite transaction; cancellation leaves no unresolved protocol group.
+- Concurrent migration, orphan/incomplete legacy transcript repair, focused tests (`138 passed`), and full suite (`385 passed` at this checkpoint) passed.

+ 42 - 0
docs/plans/todo-49.2-session-load-race-and-visible-repair.md

@@ -0,0 +1,42 @@
+# Todo 49.2 Session Load Race and Visible Repair Plan
+
+**Status:** done
+
+## Goal
+
+Close reviewed session restoration races without changing the completed Todo 49/49.1 records.
+
+## Scope
+
+- Guard asynchronous session replay with a monotonically increasing load token and the selected session id so stale responses cannot overwrite current UI state.
+- Ignore stale success and failure status updates from superseded loads.
+- Treat whitespace-only assistant content with `tool_calls` as protocol-only in replay.
+- When repairing an incomplete persisted tool-call group, retain non-blank assistant content as a provider-valid assistant message without `tool_calls`; continue dropping orphan tool replies.
+
+## Files
+
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify: `tests/test_debug_runtime.py`
+- Modify: `tests/test_websocket_api.py`
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED tests for out-of-order session replay, whitespace-only protocol messages, and incomplete tool rounds with visible text.
+2. Add the smallest load-token and transcript-repair changes.
+3. Run focused tests, JS syntax, full tests, diff check, and bounded review.
+
+## Verification
+
+- `uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py -q`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- Superseded replay success/failure responses cannot mutate the selected session UI.
+- Whitespace-only protocol assistant records are hidden, while incomplete protocol records with visible text are restored without `tool_calls`.
+- Focused tests passed (`132 passed`); full suite passed (`387 passed`) before the directional token follow-up.

+ 31 - 0
docs/plans/todo-49.3-stale-replay-token-order.md

@@ -0,0 +1,31 @@
+# Todo 49.3 Stale Replay Token Order Plan
+
+**Status:** done
+
+## Goal
+
+Make the replay generation guard directional: only a replay for the selected session may supersede the active replay.
+
+## Scope
+
+- Exit `loadSessionReplay()` before incrementing the generation token when `sessionId !== currentSessionId`.
+- Add an executable JS regression where the current replay starts first and a stale replay is invoked second.
+- Preserve the Todo 49.2 success/failure ordering coverage.
+
+## Files
+
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify: `tests/test_websocket_api.py`
+
+## Verification
+
+- `uv run pytest tests/test_websocket_api.py -q`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- A replay for a non-selected session now exits before advancing the load generation or starting fetches.
+- Reverse-order executable JS regression passes alongside Todo 49.2 race coverage.
+- WebSocket tests passed (`78 passed`); full suite passed (`388 passed`) with the existing Starlette warning.

+ 102 - 0
docs/plans/todo-5-tool-call-history.md

@@ -0,0 +1,102 @@
+# Todo 5 Tool-Call History Plan
+
+**Status:** done
+
+## Result
+
+Implemented by subagent and locally verified.
+
+Commands:
+
+```bash
+uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py
+uv run pytest
+```
+
+Results:
+
+- focused runtime/API tests: `7 passed, 1 warning in 0.23s`
+- full suite: `10 passed, 1 warning in 0.24s`
+
+Evaluation note: the remaining work is cleanup and closeout. The warning is from FastAPI/Starlette TestClient importing the currently installed httpx version and does not block the MVP.
+
+## Goal
+
+Make multi-turn tool-call history compatible with strict OpenAI Chat Completions compatible providers.
+
+## Problem
+
+After the model emits a tool call, the next Chat Completions request should include:
+
+1. the assistant message that contains `tool_calls`;
+2. the corresponding `tool` reply message.
+
+Current runtime appends assistant content and tool replies, but it does not preserve assistant `tool_calls`. Some providers reject this because a `tool` message lacks a preceding assistant tool call.
+
+## Scope
+
+This todo covers:
+
+- Representing assistant `tool_calls` in `ChatMessage`.
+- Preserving raw tool call id/name/arguments from `ToolCallEvent`.
+- Updating `DebugRuntime` to append assistant `tool_calls` before tool replies.
+- Updating OpenAI payload serialization to include `tool_calls`.
+- Tests proving strict provider-compatible history order.
+
+This todo does not cover:
+
+- New tools.
+- UI changes.
+- External queues.
+- Real network smoke.
+
+## Files
+
+- Modify: `src/agent_lab/domain/messages.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/infrastructure/chat_client.py` only if serialization needs adjustment.
+- Modify or add tests, preferably in `tests/test_debug_runtime.py` and/or `tests/test_websocket_api.py`.
+- Do not modify: `docs/plans/todos.md`.
+
+## Design
+
+Add optional `tool_calls` to `ChatMessage` as a list of OpenAI-compatible tool call dicts. Keep `content` as a string for simplicity; use the aggregated assistant text or `""` when a model emits only tool calls.
+
+For each `ToolCallEvent`, create this message shape:
+
+```python
+{
+    "id": event.id,
+    "type": "function",
+    "function": {
+        "name": event.name,
+        "arguments": event.raw_arguments,
+    },
+}
+```
+
+At the end of each model turn, append one assistant message with aggregated content and all tool calls from that turn, then append the generated tool replies.
+
+## Steps
+
+1. Write a failing test proving that the second ChatAgent call receives messages in this order:
+   - system
+   - user
+   - assistant with `tool_calls`
+   - tool with matching `tool_call_id`
+2. Add `tool_calls` to `ChatMessage`.
+3. Update `DebugRuntime.run()` to collect tool calls per model turn and append them to the assistant message before tool replies.
+4. Confirm `OpenAICompatibleChatClient` serializes `tool_calls` through `model_dump(exclude_none=True)`; adjust only if needed.
+5. Run:
+
+```bash
+uv run pytest tests/test_debug_runtime.py tests/test_websocket_api.py
+uv run pytest
+```
+
+## Verification
+
+Expected result:
+
+- Tests prove assistant `tool_calls` precede tool replies.
+- Full suite passes.

+ 73 - 0
docs/plans/todo-50-comparable-model-tool-metrics.md

@@ -0,0 +1,73 @@
+# Todo 50 Comparable Model and Tool Metrics Plan
+
+**Status:** done
+
+## Goal
+
+Make `dual_agent` and `chat_agent_tools` directly comparable without double-counting model tokens, parallel latency, or replayed events.
+
+## Metric Contract
+
+- Keep the existing `/api/sessions/{id}/usage` endpoint and `calls` / `turns` / `session` shape.
+- Extend the persisted call ledger with:
+  - `agent`: `chat_agent`, `event_agent`, or `event_kernel`;
+  - `call_kind`: `chat_completion`, `argument_fallback`, or `tool_execution`;
+  - `event_id`, `event_name`, `used_fallback`, and `tool_latency_ms`;
+  - deterministic `metric_key` for idempotent persistence.
+- Derive `tool_invocation_mode` from the immutable session config instead of duplicating it per row.
+- Add independent `turns.wall_time_ms`, measured once from the runtime monotonic clock.
+- Token totals include only real model calls. Tool rows carry zero tokens.
+- Turn/session wall time is calculated only from `turns.wall_time_ms`; model elapsed and concurrent tool latencies remain per-call diagnostics and are never summed as wall time.
+- Persist only logical event results (`deduplicated_from is None`) so coalesced/replayed clones do not inflate tool or fallback counts.
+
+## Runtime Scope
+
+- ChatAgent records model TTFT from the first provider stream item while preserving visible-text TTFT in existing round events.
+- EventAgent consumes the complete fallback stream so trailing usage is retained, and exposes one model-call metric per fallback attempt, including failed resolution attempts.
+- Event kernel records handler/tool latency on each result.
+- Direct mode persists ChatAgent model calls plus logical tool executions.
+- Dual mode additionally persists EventAgent fallback model calls.
+
+## Storage and UI Scope
+
+- Migrate legacy SQLite databases with safe additive columns/indexes; legacy rows default to `chat_agent` / `chat_completion`.
+- Extend call, turn, and session summaries with mode, call counts, fallback counts, tool counts, and independent wall time.
+- Keep legacy `elapsed_ms` fields for compatibility; UI labels architecture/call kind and prefers `turn_wall_time_ms` for totals.
+
+## Files
+
+- Modify: `src/agent_lab/domain/messages.py`
+- Modify: `src/agent_lab/application/session_store.py`
+- Modify: `src/agent_lab/application/event_agent.py`
+- Modify: `src/agent_lab/application/events/models.py`
+- Modify: `src/agent_lab/application/events/kernel.py`
+- Modify: `src/agent_lab/application/runtime.py`
+- Modify: `src/agent_lab/infrastructure/sqlite_store.py`
+- Modify: `src/agent_lab/presentation/static/app.js`
+- Modify focused tests for event kernel/agent, runtime, SQLite, and WebSocket/UI.
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add RED storage/API tests for typed call rows, legacy migration, mode derivation, idempotent metric keys, and wall-time aggregation.
+2. Add RED EventAgent/kernel tests for trailing usage, first-provider-item TTFT, fallback attempts, and tool latency.
+3. Add RED runtime parity tests proving direct and dual persist the same logical tool count while dual includes fallback model cost only when used.
+4. Implement the smallest additive schema, metric capture, and UI formatting changes.
+5. Run focused tests, full suite, JS syntax, migration stress, diff check, then specification and quality review.
+
+## Verification
+
+- `uv run pytest tests/test_event_kernel.py tests/test_event_agent.py tests/test_sqlite_store.py tests/test_debug_runtime.py tests/test_websocket_api.py -q`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `uv run pytest`
+- repeated legacy/concurrent SQLite migration test
+- `git diff --check`
+
+## Observe / Update
+
+- `/usage` now reports mode-aware ChatAgent, EventAgent fallback, and Event Kernel call rows while preserving its existing top-level structure.
+- Model token totals exclude tool rows; logical event results exclude deduplicated clones; turn/session wall time comes only from the monotonic turn record, not overlapping spans.
+- EventAgent consumes trailing usage and records fallback TTFT/elapsed; Event Kernel records handler latency; UI labels mode/agent/call kind and prefers wall time.
+- Review found one legacy duplicate-key migration issue, closed by Todo 50.1.
+- Final verification after the follow-up: `405 passed`, `node --check` and `git diff --check` pass; one existing Starlette deprecation warning remains.

+ 32 - 0
docs/plans/todo-50.1-metric-key-migration-dedup.md

@@ -0,0 +1,32 @@
+# Todo 50.1 Metric Key Migration Dedup Plan
+
+**Status:** done
+
+## Goal
+
+Allow additive metrics migration to open an experimental database that already contains duplicate non-null metric keys.
+
+## Scope
+
+- Before creating `usage_stats_metric_key_uq`, delete duplicate rows for the same `(session_id, metric_key)` while retaining the earliest row by `id`.
+- Never deduplicate `NULL` metric keys used by legacy usage rows.
+- Keep cleanup and unique-index creation inside the serialized migration transaction.
+- Cover concurrent store initialization after duplicate cleanup.
+
+## Files
+
+- Modify: `src/agent_lab/infrastructure/sqlite_store.py`
+- Modify: `tests/test_sqlite_store.py`
+
+## Verification
+
+- `uv run pytest tests/test_sqlite_store.py -q`
+- repeat concurrent migration test
+- `uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- Migration retains `MIN(id)` for each duplicate non-null `(session_id, metric_key)` and preserves all legacy `NULL` keys.
+- Cleanup and unique-index creation run inside the serialized `BEGIN IMMEDIATE` migration.
+- SQLite focused tests passed (`12 passed`), concurrent initialization passed 20 repeated runs, and the full suite passed (`405 passed`).

+ 110 - 0
docs/plans/todo-51-comparison-matrix-closeout.md

@@ -0,0 +1,110 @@
+# Todo 51 Comparison Matrix Closeout Plan
+
+**Status:** done
+
+## Goal
+
+Close the service experiment with one repeatable behavior matrix, current operational documentation, explicit comparison controls, and bounded conclusions.
+
+## Scenario Matrix
+
+Run the same six business scenarios in both `dual_agent` and `chat_agent_tools` modes (12 parameterized runs):
+
+1. ordinary chat with all built-ins enabled and no event;
+2. absolute device-volume adjustment with silent success;
+3. fixed RFC3339 schedule creation with one deterministic confirmation;
+4. web search with first answer, tool result, then exactly one second answer;
+5. session termination with exactly one farewell and terminal completion;
+6. concurrent volume plus schedule, bounded at two workers, preserving input result order and one schedule template.
+
+The matrix compares shared user-visible semantics and normalized business results. It deliberately allows provider transcript and event-selection protocol differences.
+
+## Fair Comparison Controls
+
+- Same model parameters, enabled event set, user inputs, output budget, registry, plugin schemas, handlers, event budget, concurrency, and timeouts.
+- Direct-mode provider tool protocol tokens remain counted.
+- Token totals include only real model calls; tool rows have zero tokens.
+- `turn_wall_time_ms` is the only turn/session wall-clock total; overlapping model/tool spans are not summed.
+- Deduplicated/replayed event clones count as one logical tool execution.
+- Web search requires two ChatAgent answers in both modes.
+- Provider TTFT and visible-text TTFT remain distinct.
+
+## Documentation Scope
+
+- Rewrite `README.md` as the operational entry for architecture, built-ins, modes, configuration, UI/WebSocket/API use, metrics, verification, and current adapter limitations.
+- Calibrate `docs/plans/2026-07-12-chat-event-agent-production-events-design.md` to implemented namespaced event names and link this closeout; keep it as the architecture fact source.
+- Record matrix evidence, conclusions, and remaining risks in this file.
+- Update `docs/plans/todos.md` only when final verification passes.
+
+## Files
+
+- Add: `tests/test_tool_invocation_comparison.py`
+- Modify: `README.md`
+- Modify: `docs/plans/2026-07-12-chat-event-agent-production-events-design.md`
+- Modify: this file and `docs/plans/todos.md` during final Update.
+
+## Requirement Execution Loop
+
+### Plan
+
+1. Add the 12-run RED comparison matrix with small local scripted clients and injected production plugin ports.
+2. Fix only genuine shared-contract gaps exposed by the matrix; use Todo 51.x for any post-execution repair.
+3. Update README and architecture design names/status from the verified implementation.
+4. Run matrix, focused, full, JS syntax, app import, and diff checks.
+5. Perform specification and quality review, then record evidence and bounded experiment conclusions.
+
+## Verification
+
+- `uv run pytest tests/test_tool_invocation_comparison.py -q`
+- `uv run pytest tests/test_debug_runtime.py tests/test_builtin_event_plugins.py tests/test_sqlite_store.py tests/test_websocket_api.py -q`
+- `uv run pytest`
+- `node --check src/agent_lab/presentation/static/app.js`
+- `uv run python -c "from agent_lab.main import app; print(app.title)"`
+- `git diff --check`
+
+## Observe
+
+- The shared matrix runs 6 scenarios in both modes (`12 passed`): ordinary chat, volume, schedule, web search, termination, and parallel volume plus schedule.
+- Review required Todo 51.1 because the first matrix did not enforce fast first replies or inspect the actual mode-specific model request surface.
+- After Todo 51.1, each event scenario proves the first ChatAgent delta precedes tool completion; web search has exactly two model answers; schedule/parallel add one deterministic template; silent/terminate flows add no unintended second model answer.
+- `dual_agent` proves compact generated event catalog plus no provider tools; `chat_agent_tools` proves exact enabled schemas plus no generated catalog. Audit source is mode-correct.
+- Parallel cases require both handlers to start, release in reverse completion order, and still emit tool results in accepted input order.
+
+## Verification Evidence
+
+- Matrix: `12 passed in 0.22s`.
+- Focused runtime/plugin/storage/WebSocket regression: `231 passed`, one existing Starlette deprecation warning.
+- Full suite: `417 passed`, one existing Starlette deprecation warning.
+- `node --check src/agent_lab/presentation/static/app.js`: passed.
+- App import printed `Agent Lab`.
+- `git diff --check`: passed.
+
+## Experiment Conclusions
+
+Confirmed by this implementation and matrix:
+
+- one generic event kernel and flat built-in plugin registry serve both invocation modes;
+- both modes preserve the same normalized event execution and user-visible result-policy contract;
+- fast first replies, web-search two-answer ordering, multi-event parallelism, terminal behavior, session restoration, and comparable metric accounting have repeatable regression coverage;
+- token totals, logical tool counts, fallback rows, and turn wall time are separated so the two architectures can be measured without known double-counting.
+
+Not established by this closeout:
+
+- which mode has lower real-provider TTFT, turn latency, token cost, or monetary cost;
+- which mode produces better natural-language quality or event accuracy on live stochastic models;
+- real device, calendar, dialogue termination, and internet-search reliability.
+
+The current matrix uses scripted model streams and injected/default in-memory adapters. A production decision still requires paired live-provider and service-adapter benchmarks using fresh databases and the same comparison controls.
+
+## Remaining Risks
+
+- Some providers may suppress visible content when emitting native tool calls, weakening direct mode's first-reply behavior.
+- Fallback frequency and schema/prompt growth depend on the real capability set and user distribution.
+- Model randomness, provider usage reporting, and real search latency can dominate local deterministic results.
+- Reusing historical SQLite sessions can contaminate paired comparisons; benchmark runs should use isolated databases.
+- Monetary cost is not stored because it depends on provider/model pricing; estimate it from model call tokens outside the runtime.
+
+## Workflow Evolution Update
+
+- The requirement/todo loop, isolated worktree, per-todo commits, TDD, and follow-up `x.x` rows successfully contained review findings without rewriting completed todos.
+- Subagent model overrides and the default user-level uv cache were environment-specific friction. The main session used inherited subagent routing, explicit verification, and controlled Git commits. No repository workflow rule was added because these are not stable project requirements.

+ 34 - 0
docs/plans/todo-51.1-first-reply-and-protocol-matrix.md

@@ -0,0 +1,34 @@
+# Todo 51.1 First Reply and Protocol Matrix Plan
+
+**Status:** done
+
+## Goal
+
+Make the closeout matrix enforce the experiment's primary latency contract and the intended mode-specific protocol surfaces.
+
+## Scope
+
+- Add a ChatAgent `message_delta` before events for volume, schedule, terminate, web search, and parallel volume/schedule scenarios.
+- Assert first visible reply precedes the first tool result; silent/terminate flows do not add an unintended second model answer; schedule/parallel add exactly one deterministic template after execution.
+- Record each scripted model request.
+- For `dual_agent`, assert the initial ChatAgent request has no provider tools and includes the compact enabled event catalog.
+- For `chat_agent_tools`, assert the initial request contains exactly the enabled provider schemas and no generated event catalog.
+- Assert detected event audit source is `text_event` for dual mode and `provider_resolved` for direct mode.
+
+## Files
+
+- Modify: `tests/test_tool_invocation_comparison.py`
+
+## Verification
+
+- `uv run pytest tests/test_tool_invocation_comparison.py -q`
+- repeat parallel cases
+- `uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- Side-effect scenarios now stream a natural ChatAgent acknowledgement/farewell before event completion.
+- Matrix request capture proves dual mode sends no provider tools and injects exactly the enabled compact event catalog; direct mode sends exactly the enabled schemas and no generated catalog.
+- Detected event audits report `text_event` for dual mode and `provider_resolved` for direct mode.
+- Matrix passed (`12 passed`), parallel scenarios passed 20 repeated runs, and the full suite passed (`417 passed`).

+ 128 - 0
docs/plans/todo-52-agent-benchmark-runner.md

@@ -0,0 +1,128 @@
+# Agent Benchmark Runner Implementation Plan
+
+**Status:** done
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build a packaged benchmark CLI that runs deterministic mock or live OpenAI-compatible comparisons for both invocation modes and writes secret-safe JSON/Markdown reports.
+
+**Architecture:** Add a benchmark application module for config, case catalog, run models, timing and aggregation; a reporting infrastructure module for redaction/rendering/atomic writes; and a thin presentation CLI. Each run uses `DebugRuntime.start_session()` with a temporary SQLite store and timing chat-client wrapper.
+
+**Tech Stack:** Python 3.11+, asyncio, Pydantic, argparse, httpx-based existing OpenAI-compatible client, SQLite, pytest, uv.
+
+---
+
+### Task 1: Benchmark configuration and case catalog
+
+**Files:**
+- Create: `src/agent_lab/application/benchmark.py`
+- Create: `tests/test_benchmark_config.py`
+- Modify: `tests/test_tool_invocation_comparison.py`
+
+- [x] Write failing tests for strict schema version, URL validation, known case/mode IDs, positive runs, and rejection of secret fields.
+- [x] Verify RED with `uv run pytest tests/test_benchmark_config.py -q`.
+- [x] Implement strict Pydantic models `BenchmarkConfig`, `BenchmarkTarget`, `BenchmarkCase`, `BenchmarkExpectation`, and immutable built-in case catalog.
+- [x] Implement `build_benchmark_request(case, mode, model)` using the existing `DebugRunRequest`, identical params, and built-in enabled tools.
+- [x] Expose deterministic mock rounds from the production case catalog and refactor the existing comparison test to reuse the catalog instead of duplicating prompts/expectations.
+- [x] Run config and comparison tests, then commit with `feat: add benchmark case catalog`.
+
+### Task 2: Timing wrapper and isolated benchmark runner
+
+**Files:**
+- Modify: `src/agent_lab/application/benchmark.py`
+- Create: `tests/test_benchmark_runner.py`
+
+- [x] Write failing tests for provider TTFT versus visible TTFT, per-call elapsed/usage, persisted wall time, detected events/sources, tool/fallback counts, semantic failures, partial-failure continuation, and temporary-database isolation.
+- [x] Verify RED with `uv run pytest tests/test_benchmark_runner.py -q`.
+- [x] Implement `TimingChatClient` around the existing stream protocol, recording first item, first message delta, usage, elapsed, and fallback classification.
+- [x] Implement deterministic `MockBenchmarkChatClient` that never constructs HTTP transport and emits mode-correct streams for the six cases.
+- [x] Implement `BenchmarkRunner.run()` over case → mode → iteration, creating/closing one runtime, chat client, and temporary SQLite store per run.
+- [x] Implement semantic evaluation from output, audit, and usage ledgers and return ordered `BenchmarkRunResult` records without stopping after an individual failure.
+- [x] Run runner/config/comparison tests, then commit with `feat: run isolated agent benchmarks`.
+
+### Task 3: Aggregation, reporting, and CLI
+
+**Files:**
+- Create: `src/agent_lab/infrastructure/benchmark_reporting.py`
+- Create: `src/agent_lab/presentation/benchmark_cli.py`
+- Create: `tests/test_benchmark_reporting.py`
+- Create: `tests/test_benchmark_cli.py`
+- Modify: `pyproject.toml`
+
+- [x] Write failing tests for linear-interpolated p50/p95, failed-sample exclusion, low-sample warnings, redaction, JSON/Markdown consistency, atomic writes, CLI overrides, mock/live API-key requirements, exit codes 0/1/2, and continued report writing after run failures.
+- [x] Verify RED with `uv run pytest tests/test_benchmark_reporting.py tests/test_benchmark_cli.py -q`.
+- [x] Implement aggregation into one typed `BenchmarkReport` model shared by JSON and Markdown.
+- [x] Implement centralized redaction and 2 KiB error truncation before any rendered output.
+- [x] Implement one atomically published report bundle containing matching `report.json` and `report.md` files.
+- [x] Implement argparse with `--config`, `--base-url`, `--model`, repeatable `--case`/`--mode`, `--runs`, `--output-dir`, and `--mock`; apply `CLI > JSON` and environment-only API key.
+- [x] Register `agent-lab-benchmark = agent_lab.presentation.benchmark_cli:main` and verify `--help`.
+- [x] Run reporting/CLI/runner tests, then commit with `feat: add benchmark reports and cli`.
+
+### Task 4: Examples, documentation, and final verification
+
+**Files:**
+- Create: `benchmarks/single-target.example.json`
+- Modify: `.gitignore`
+- Modify: `README.md`
+- Modify: `docs/plans/todo-52-agent-benchmark-runner.md`
+- Modify: `docs/plans/todos.md`
+
+- [x] Add a secret-free example covering both modes and all six cases; ignore `outputs/benchmarks/`.
+- [x] Document `.env`, config copy/edit, mock command, live command, output fields, exit codes, fairness controls, and the limits of scripted/mock adapters.
+- [x] Run `agent-lab-benchmark --config benchmarks/single-target.example.json --mock --output-dir /tmp/agent-lab-benchmark` and inspect both reports.
+- [x] Run focused benchmark tests, `uv run pytest`, CLI help, app import, `node --check`, and `git diff --check`.
+- [x] Perform specification review then code-quality review; add Todo 52.x rows for post-execution fixes.
+- [x] Record evidence and Workflow Evolution findings, mark Todo 52 done, and commit with `docs: close benchmark runner todo`.
+
+## Final Evidence
+
+Verified on 2026-07-14 (Asia/Shanghai):
+
+- benchmark suite: `196 passed in 1.19s`;
+- full suite: `601 passed in 3.67s`, with one existing Starlette/httpx deprecation warning;
+- `agent-lab-benchmark --help`: exit `0`;
+- application import prints `Agent Lab`;
+- `node --check src/agent_lab/presentation/static/app.js`: exit `0`;
+- `git diff --check`: exit `0`;
+- zero-network mock CLI: `12 attempted / 12 passed / 0 failed`;
+- final JSON: `/tmp/agent-lab-benchmark-final-20260714/20260713T194850Z/report.json`;
+- final Markdown: `/tmp/agent-lab-benchmark-final-20260714/20260713T194850Z/report.md`;
+- both parallel-mode runs report `parallel_events_overlapped=true`;
+- both web-search runs report two model calls and one tool execution;
+- no mock run has missing token usage.
+
+Specification and code-quality reviews found no remaining Critical or Important
+issues after Todos 52.1-52.13. The remaining non-blocking risks are documented
+below.
+
+## Residual Risks and Limits
+
+- No live provider request was run because no real benchmark target or credential
+  was supplied; live latency, usage support, semantic adherence, and provider
+  tool-stream compatibility still require operator verification.
+- The example uses one run per case, so its 60 p95 warnings are expected and the
+  mock percentile values are not suitable for ranking architectures.
+- Atomic bundle publication uses a relative directory symlink and is verified on
+  macOS; Windows symlink behavior is not verified.
+- An interruption inside `mkdtemp()` may leave an empty hidden bundle directory.
+  A close failure during a narrow consumer-pause cancellation window may be
+  logged as a secondary async-generator exception. Neither case changes the
+  primary cancellation result or exposes a mixed public report.
+
+## Workflow Evolution Update
+
+The requirement loop exposed three durable workflow lessons:
+
+- paired filesystem artifacts need one explicit atomic publication boundary;
+  repeated two-file rollback fixes were replaced by a single report bundle;
+- benchmark plans must define clock boundaries, missing-data semantics, and
+  observable concurrency proof before implementation, not only metric names and
+  event counts;
+- parallel subagents need shared-contract integration checks when one task changes
+  an output layout consumed by another task's tests.
+
+These lessons are recorded in this task report and the benchmark design. No
+`AGENTS.md` change was made because the repository's existing PAOU, todo-first,
+TDD, per-todo commit, and post-review follow-up rules already caused the issues to
+be found and corrected; adding more global workflow rules would duplicate the
+current core process.

+ 29 - 0
docs/plans/todo-52.1-benchmark-url-control-characters.md

@@ -0,0 +1,29 @@
+# Todo 52.1 Benchmark URL Control Characters Plan
+
+**Status:** done
+
+## Goal
+
+Keep malformed benchmark targets inside the configuration error boundary instead of leaking an `httpx.InvalidURL` during live client construction.
+
+## Scope
+
+- Reject ASCII and Unicode characters whose category is control/format, including NUL, CR, LF, and tab.
+- Retain the existing HTTP(S), no-userinfo, no-query, and no-fragment requirements.
+- Add focused valid/invalid URL regression tests.
+
+## Files
+
+- Modify: `src/agent_lab/application/benchmark.py`
+- Modify: `tests/test_benchmark_config.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_config.py tests/test_tool_invocation_comparison.py -q`
+- `uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- Control-category characters are rejected before transport construction while printable internationalized URLs remain covered.
+- Focused tests passed (`67 passed`) and the full suite passed at this checkpoint (`472 passed`).

+ 29 - 0
docs/plans/todo-52.10-benchmark-bundle-cli-contract.md

@@ -0,0 +1,29 @@
+# Todo 52.10 Benchmark Bundle CLI Contract Plan
+
+**Status:** done
+
+## Goal
+
+Align CLI integration tests and report discovery with the atomic bundle layout introduced by Todo 52.9.
+
+## Observation
+
+The CLI still returns the report paths supplied by `write_benchmark_report()`, but four tests discover reports independently with root-level `*.json`/`*.md` globs or attempt to read every output-directory entry as a file. The new layout intentionally exposes a timestamped bundle symlink containing `report.json` and `report.md`, plus a private backing directory.
+
+## Scope
+
+- Update CLI tests to use the exact `json_report` and `markdown_report` paths printed by the command, or inspect the single published bundle symlink.
+- Do not inspect private dot-prefixed bundle backing directories as public reports.
+- Preserve assertions for CLI overrides, exit codes, cardinality system errors, report redaction, and matching JSON/Markdown contents.
+- Avoid adding compatibility copies at the output root because that would restore the two-path publication race fixed by Todo 52.9.
+
+## Files
+
+- Modify: `tests/test_benchmark_cli.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_cli.py -q`
+- `uv run pytest tests/test_benchmark_reporting.py tests/test_benchmark_cli.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 32 - 0
docs/plans/todo-52.11-bundle-interrupt-and-path-safety.md

@@ -0,0 +1,32 @@
+# Todo 52.11 Bundle Interrupt and Path Safety Plan
+
+**Status:** done
+
+## Goal
+
+Close the post-symlink interruption window and constrain report bundle names to the configured output directory.
+
+## Observation
+
+- A `KeyboardInterrupt` can arrive after `os.symlink()` creates the public bundle entry but before the implementation records publication. Cleanup then removes the backing bundle and leaves a dangling public symlink.
+- The optional `timestamp` argument is used as a path component without basename validation, so an absolute path or separator-containing value can escape `output_dir`.
+- Existing fsync tests count calls but do not prove that both report files, the bundle directory, and the output directory are the objects being synced.
+
+## Scope
+
+- Add a regression that injects interruption immediately after successful symlink creation and prove the public bundle remains complete rather than dangling.
+- Once public publication is attempted, retain the already-complete private bundle on ambiguous interruption/error paths; cleanup only before publication begins.
+- Validate an explicit timestamp as a bounded safe basename using a conservative ASCII identifier character set; reject absolute paths, separators, dot segments, controls, and empty values before filesystem writes.
+- Strengthen fsync tests to identify the two report files and both directories, not only count invocations.
+
+## Files
+
+- Modify: `src/agent_lab/infrastructure/benchmark_reporting.py`
+- Modify: `tests/test_benchmark_reporting.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_reporting.py -q`
+- `uv run pytest tests/test_benchmark_reporting.py tests/test_benchmark_cli.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 38 - 0
docs/plans/todo-52.12-benchmark-metric-integrity-and-parallel-proof.md

@@ -0,0 +1,38 @@
+# Todo 52.12 Benchmark Metric Integrity and Parallel Proof Plan
+
+**Status:** done
+
+## Goal
+
+Ensure benchmark latency, token-cost, and multi-event concurrency results measure the claimed behavior rather than consumer overhead, persistence defaults, or event cardinality alone.
+
+## Observation
+
+- `TimingChatClient.elapsed_ms` spans the outer generator's `yield`, so downstream Runtime/audit/queue work and consumer pauses inflate the value described as pure model-call elapsed.
+- Successful calls without a provider usage chunk are persisted as zero-token rows and currently become a false `0 tokens` benchmark result.
+- The parallel case verifies two successful events but does not prove their handlers overlap; a serial executor can still pass the benchmark.
+
+## Scope
+
+- Measure model elapsed as the accumulated time awaiting the wrapped provider iterator, excluding time while the caller processes yielded items.
+- Preserve provider TTFT and first-visible timing semantics independently from provider-await elapsed.
+- Require complete usage coverage across all model/fallback calls before reporting token totals. Missing usage produces nullable token metrics plus an explicit semantic failure instead of zero or a partial sum.
+- Add a benchmark-owned execution probe for the parallel volume/schedule case that deterministically requires both handlers to start before either completes.
+- Record the parallel-overlap result in `BenchmarkRunResult`, include it in report output, and fail the parallel case if overlap is absent.
+- Keep mock and live runs on the same probe/registry path and keep non-parallel case behavior unchanged.
+
+## Files
+
+- Modify: `src/agent_lab/application/benchmark.py`
+- Modify: `src/agent_lab/infrastructure/benchmark_reporting.py`
+- Modify: `tests/test_benchmark_runner.py`
+- Modify: `tests/test_benchmark_reporting.py`
+- Modify: `README.md`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_runner.py tests/test_benchmark_reporting.py -q`
+- `uv run pytest tests/test_benchmark_config.py tests/test_benchmark_runner.py tests/test_benchmark_reporting.py tests/test_benchmark_cli.py tests/test_tool_invocation_comparison.py -q`
+- zero-network mock CLI still reports 12 attempted runs and validates parallel overlap
+- `uv run pytest`
+- `git diff --check`

+ 30 - 0
docs/plans/todo-52.13-provider-cleanup-exception-priority.md

@@ -0,0 +1,30 @@
+# Todo 52.13 Provider Cleanup Exception Priority Plan
+
+**Status:** done
+
+## Goal
+
+Preserve the original provider failure, cancellation, or generator-close signal when cleanup of the wrapped provider iterator also fails.
+
+## Observation
+
+`TimingChatClient.stream_chat()` awaits `iterator.aclose()` in `finally`. If a provider exception or `CancelledError` is already propagating and `aclose()` raises another exception, the cleanup exception replaces the original. This can hide the real provider failure and convert cancellation into a normal runtime error instead of preserving interruption semantics.
+
+## Scope
+
+- Add regressions for provider exception plus close failure and cancellation plus close failure.
+- Keep the active provider/cancellation/generator-close exception as the primary propagated exception.
+- Propagate `aclose()` failure when no other exception is active.
+- Always append the timing attempt without changing provider-await elapsed, usage completeness, or parallel-probe behavior.
+
+## Files
+
+- Modify: `src/agent_lab/application/benchmark.py`
+- Modify: `tests/test_benchmark_runner.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_runner.py -q`
+- `uv run pytest tests/test_benchmark_runner.py tests/test_benchmark_reporting.py -q`
+- `uv run pytest`
+- `git diff --check`

+ 29 - 0
docs/plans/todo-52.2-benchmark-url-transport-compatibility.md

@@ -0,0 +1,29 @@
+# Todo 52.2 Benchmark URL Transport Compatibility Plan
+
+**Status:** done
+
+## Goal
+
+Ensure every accepted benchmark base URL can be constructed by the production httpx transport.
+
+## Scope
+
+- Validate the complete URL with `httpx.URL` after structural and control-character checks.
+- Convert `httpx.InvalidURL` and invalid port/IDNA cases into stable Pydantic configuration errors.
+- Keep valid HTTP(S) Unicode hostnames that httpx accepts.
+
+## Files
+
+- Modify: `src/agent_lab/application/benchmark.py`
+- Modify: `tests/test_benchmark_config.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_config.py tests/test_tool_invocation_comparison.py -q`
+- `uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- Config validation now uses the production httpx URL semantics for IDNA and port compatibility.
+- Focused tests passed (`72 passed`) and the full suite passed at this checkpoint (`477 passed`).

+ 31 - 0
docs/plans/todo-52.3-benchmark-case-mode-single-source.md

@@ -0,0 +1,31 @@
+# Todo 52.3 Benchmark Case and Mode Single Source Plan
+
+**Status:** done
+
+## Goal
+
+Prevent benchmark configuration and the executable case catalog from drifting as new cases or modes are added.
+
+## Scope
+
+- Replace separate `Literal` declarations with `StrEnum` definitions for case IDs and invocation modes.
+- Derive exported ordered choice tuples from the enums.
+- Type catalog keys and config lists with those enums while preserving JSON string serialization and existing runtime request values.
+- Assert every case enum has exactly one catalog entry.
+
+## Files
+
+- Modify: `src/agent_lab/application/benchmark.py`
+- Modify: `tests/test_benchmark_config.py`
+- Modify: `tests/test_tool_invocation_comparison.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_config.py tests/test_tool_invocation_comparison.py -q`
+- `uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- `BenchmarkMode` and `BenchmarkCaseId` enums now drive config validation, exported choices, catalog keys, request construction, and the 12-run comparison matrix.
+- Specification and code-quality reviews approved the result; focused tests passed (`76 passed`) and the full suite passed (`481 passed`).

+ 32 - 0
docs/plans/todo-52.4-benchmark-visible-ttft-and-call-counts.md

@@ -0,0 +1,32 @@
+# Todo 52.4 Benchmark Visible TTFT and Call Counts Plan
+
+**Status:** done
+
+## Goal
+
+Prevent benchmark runs from passing with hidden extra model cost or an artificially early visible-response timestamp.
+
+## Scope
+
+- Record call-level and run-level visible TTFT only for a `message_delta` containing non-whitespace content.
+- Add expected ChatAgent call count and expected fallback count to each benchmark case expectation.
+- Compare both persisted ledger counts and timing-wrapper counts against the case expectation.
+- Keep the six current cases at zero fallback; web search expects two ChatAgent calls and all other cases expect one.
+
+## Files
+
+- Modify: `src/agent_lab/application/benchmark.py`
+- Modify: `tests/test_benchmark_config.py`
+- Modify: `tests/test_benchmark_runner.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_runner.py tests/test_benchmark_config.py -q`
+- `uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- Visible TTFT now ignores blank/whitespace deltas at both call and run boundaries.
+- Each case declares expected ChatAgent/fallback counts; ledger and timing attempts are checked against those expectations and each other.
+- Focused tests passed (`111 passed`) and the full suite passed at this checkpoint (`516 passed`).

+ 32 - 0
docs/plans/todo-52.5-benchmark-ledger-event-identity.md

@@ -0,0 +1,32 @@
+# Todo 52.5 Benchmark Ledger Event Identity Plan
+
+**Status:** done
+
+## Goal
+
+Prevent independent benchmark ledgers from agreeing on counts while referring to different events, and keep failed-run attempt counts truthful.
+
+## Scope
+
+- Preserve event names from `event_batch_results` and `tool_execution` usage rows in `BenchmarkRunResult`.
+- Require detected event names, batch-result event names, and tool-metric event names to equal the case's expected ordered event list.
+- Keep normalized status checks paired with the batch event identity.
+- Report ChatAgent/fallback attempt counts from timing records; continue checking timing counts against persisted ledger counts when persistence exists.
+- For pre-ledger failures, retain timing-derived attempt counts instead of `None`/zero.
+
+## Files
+
+- Modify: `src/agent_lab/application/benchmark.py`
+- Modify: `tests/test_benchmark_runner.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_runner.py tests/test_benchmark_config.py -q`
+- `uv run pytest`
+- `git diff --check`
+
+## Observe / Update
+
+- Detected, batch-result, and tool-metric event names are preserved and independently matched to the expected ordered event list.
+- Failed streams retain timing-derived model/fallback attempts and any usage already returned.
+- Final Task 2 review approved the implementation; focused tests passed (`115 passed`) and the full suite passed (`520 passed`).

+ 29 - 0
docs/plans/todo-52.6-benchmark-report-auditability.md

@@ -0,0 +1,29 @@
+# Todo 52.6 Benchmark Report Auditability Plan
+
+**Status:** done
+
+## Goal
+
+Make benchmark reports complete and self-explanatory even under concurrent writers or orchestration-level failures.
+
+## Scope
+
+- Aggregate successful model-call elapsed samples and tool handler latency samples with p50/p95 and low-sample warnings.
+- Reserve each output basename using an exclusive lock/reservation file before choosing paths, so concurrent processes cannot publish mixed JSON/Markdown pairs.
+- Add redacted runner/system errors to the report model; count a catastrophic runner failure in overall attempted/failed values even when no case result exists.
+- Emit low-confidence warnings for zero as well as 1–19 successful samples.
+- Redact first, then truncate errors to at most 2048 UTF-8 bytes without producing invalid text.
+
+## Files
+
+- Modify: `src/agent_lab/infrastructure/benchmark_reporting.py`
+- Modify: `src/agent_lab/presentation/benchmark_cli.py`
+- Modify: `tests/test_benchmark_reporting.py`
+- Modify: `tests/test_benchmark_cli.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_reporting.py tests/test_benchmark_cli.py -q`
+- concurrent writer stress test
+- `uv run pytest`
+- `git diff --check`

+ 28 - 0
docs/plans/todo-52.7-benchmark-output-reservation-and-cardinality.md

@@ -0,0 +1,28 @@
+# Todo 52.7 Benchmark Output Reservation and Cardinality Plan
+
+**Status:** done
+
+## Goal
+
+Eliminate remaining report publication races and prevent an incomplete benchmark execution from being reported as successful.
+
+## Scope
+
+- Reserve both final report paths with exclusive file creation before writing temporary content; never overwrite a path created by another process.
+- Remove lock/reservation sidecar files entirely; clean only placeholders/temporary/published files owned by the current attempt after failure.
+- Replace argparse error details with a generic safe message so malformed values cannot be echoed to stderr.
+- Require `len(results) == len(cases) * len(modes) * runs_per_case`; mismatches become redacted system errors in the report and exit code 2.
+
+## Files
+
+- Modify: `src/agent_lab/infrastructure/benchmark_reporting.py`
+- Modify: `src/agent_lab/presentation/benchmark_cli.py`
+- Modify: `tests/test_benchmark_reporting.py`
+- Modify: `tests/test_benchmark_cli.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_reporting.py tests/test_benchmark_cli.py -q`
+- concurrent reservation stress test
+- `uv run pytest`
+- `git diff --check`

+ 27 - 0
docs/plans/todo-52.8-benchmark-no-overwrite-publication.md

@@ -0,0 +1,27 @@
+# Todo 52.8 Benchmark No-Overwrite Publication Plan
+
+**Status:** done
+
+## Goal
+
+Remove the final check-then-replace race from concurrent benchmark report publication.
+
+## Scope
+
+- Fully write and fsync JSON/Markdown temporary files in the destination directory.
+- Publish each file using `os.link(temp, final)`, whose destination creation is atomic and fails if the path already exists.
+- If either candidate path conflicts, remove only links whose inode matches the current temporary file, choose the next suffix, and retry the pair.
+- Remove temporary files after both links succeed; retain no lock or placeholder files.
+- Test a competitor creating/replacing destinations immediately before each link call.
+
+## Files
+
+- Modify: `src/agent_lab/infrastructure/benchmark_reporting.py`
+- Modify: `tests/test_benchmark_reporting.py`
+
+## Verification
+
+- `uv run pytest tests/test_benchmark_reporting.py -q`
+- repeated concurrent/competitor stress tests
+- `uv run pytest`
+- `git diff --check`

+ 36 - 0
docs/plans/todo-52.9-atomic-report-bundles-and-audit-parity.md

@@ -0,0 +1,36 @@
+# Todo 52.9 Atomic Report Bundles and Audit Parity Plan
+
+**Status:** done
+
+## Goal
+
+Replace the fundamentally non-transactional two-path report publication with one atomic bundle publication boundary, and close the remaining report/CLI audit gaps found after Todo 52.8.
+
+## Root Cause
+
+Publishing flat JSON and Markdown paths requires two independent filesystem operations. If the second operation conflicts, rolling back the first requires a check-then-unlink sequence that cannot atomically prove pathname ownership. A competing process can replace the first path between the inode check and `unlink()`, so the writer may delete another process's file or return a mixed pair.
+
+## Scope
+
+- Fully write and `fsync` `report.json` and `report.md` inside one private, uniquely named bundle directory.
+- Publish the complete bundle through one atomic, no-overwrite directory symlink named from the timestamp; a collision chooses the next numeric suffix without deleting or replacing the competitor.
+- Return the two report paths through the published bundle path and leave no flat two-file rollback path.
+- Clean only unpublished private bundle state. Never inspect-then-delete a published competitor-controlled pathname.
+- Treat mutation after successful publication as out of scope: a process with write access to the output directory can replace any published filesystem entry after the command returns.
+- Render runtime errors and semantic failures together in Markdown when both exist, preserving the facts present in JSON.
+- Map summary/error stream failures to exit code `2` without exposing secrets or confusing them with benchmark sample failures.
+
+## Files
+
+- Modify: `src/agent_lab/infrastructure/benchmark_reporting.py`
+- Modify: `src/agent_lab/presentation/benchmark_cli.py`
+- Modify: `tests/test_benchmark_reporting.py`
+- Modify: `tests/test_benchmark_cli.py`
+
+## Verification
+
+- Reproduce the Todo 52.8 inode-check-to-unlink race before implementation.
+- `uv run pytest tests/test_benchmark_reporting.py tests/test_benchmark_cli.py -q`
+- concurrent process/thread bundle publication and collision stress tests
+- `uv run pytest`
+- `git diff --check`

+ 402 - 0
docs/plans/todo-53-professional-debug-console-ui.md

@@ -0,0 +1,402 @@
+# Professional Debug Console UI Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Turn the static Agent Lab page into a professional responsive debug console and make EventPrompt deterministically follow invocation mode, selected tools, and restored state.
+
+**Architecture:** Preserve the FastAPI/static-JavaScript boundary and backend contracts. Consolidate tool/mode-derived state in one refresh path, then reorganize the page into a collapsible control rail, primary chat workspace, and optional Audit/Usage drawer with browser-only presentation persistence.
+
+**Tech Stack:** HTML5, CSS, browser-native JavaScript, FastAPI static assets, pytest source-contract tests, Node syntax checking, local browser QA.
+
+---
+
+## File Map
+
+- Modify `src/agent_lab/presentation/static/app.js`: EventPrompt derivation, restore cleanup, mode-aware prompt collection, collapsible sections, debug drawer, status indicators.
+- Modify `src/agent_lab/presentation/static/index.html`: console layout, status bar, collapsible control sections, right debug drawer, mode-dependent EventAgent groups.
+- Modify `src/agent_lab/presentation/static/styles.css`: design tokens, console shell, messages, controls, drawer, responsive layout.
+- Modify `tests/test_websocket_api.py`: focused frontend source-contract regressions.
+- Modify `README.md`: operator-facing console and EventPrompt behavior.
+- Modify `docs/plans/todos.md`: close Todo 53 only after all verification succeeds.
+
+### Task 1: Make EventPrompt a mode-derived singleton
+
+**Files:**
+
+- Modify: `tests/test_websocket_api.py:1364-1517`
+- Modify: `src/agent_lab/presentation/static/app.js:85-93,259-320,413-450,477-677,867-897`
+- Modify: `src/agent_lab/presentation/static/index.html:212-233`
+
+- [x] **Step 1: Write failing source-contract tests**
+
+Add these assertions in focused tests:
+
+```python
+assert 'toolInvocationMode.addEventListener("change", refreshToolModeState)' in js
+assert 'checkbox.addEventListener("change", refreshToolModeState)' in js
+assert "function refreshToolModeState()" in js
+assert "function isGeneratedEventPromptContent(" in js
+assert "function removeEventPromptItems()" in js
+assert 'toolInvocationMode.value === "chat_agent_tools"' in update_prompt
+assert 'toolInvocationMode.value !== "chat_agent_tools"' in collect_prompts
+assert 'prompt.prompt_kind !== "event_agent_system"' in normalize_prompts
+assert "!isGeneratedEventPromptContent(prompt.content)" in normalize_prompts
+assert 'id="event-agent-model-settings"' in html
+```
+
+Slice `update_prompt`, `collect_prompts`, and `normalize_prompts` using the existing `js.index(...)` test style so each assertion is scoped to the correct function.
+
+- [x] **Step 2: Run tests and verify RED**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "event_prompt or prompt_restore or tool_mode" -q
+```
+
+Expected: new assertions fail because there is no unified refresh, generated-content filter, or model-settings container.
+
+- [x] **Step 3: Add conservative legacy-generated-prompt detection**
+
+Add:
+
+```javascript
+const EVENT_PROMPT_PREFIX = "You may request EventAgent work using this text protocol.";
+
+function isGeneratedEventPromptContent(content) {
+  const normalized = String(content || "").trim();
+  return normalized.startsWith(EVENT_PROMPT_PREFIX)
+    && normalized.includes("<agent_events>")
+    && normalized.includes("Use exact event names only");
+}
+
+function removeEventPromptItems() {
+  [...systemPrompts.querySelectorAll(".prompt-item")].forEach((item) => {
+    const content = item.querySelector(".system-prompt")?.value || "";
+    if (item.dataset.promptKind === "event_agent_system"
+      || isGeneratedEventPromptContent(content)) {
+      item.remove();
+    }
+  });
+}
+```
+
+Change `normalizePromptItems()` to discard typed EventPrompt entries and legacy generated EventPrompt text. Insert one blank custom prompt only when no custom prompt remains; do not insert EventPrompt there.
+
+- [x] **Step 4: Centralize mode/tool refresh and outbound filtering**
+
+Implement:
+
+```javascript
+function refreshToolModeState() {
+  updateToolCount();
+  updateToolModeHelp();
+  updateEventAgentConfigAvailability();
+  updateEventPromptItem(availableTools);
+}
+
+function updateEventPromptItem(tools) {
+  removeEventPromptItems();
+  if (toolInvocationMode.value === "chat_agent_tools") {
+    return;
+  }
+  const selected = new Set(selectedTools());
+  const eventPrompt = ensureEventPromptItem();
+  eventPrompt.querySelector(".system-prompt").value = buildAvailableEventsPrompt(
+    tools.filter((tool) => selected.has(tool.name)),
+  );
+}
+```
+
+Bind mode change, checkbox change, All/None, bootstrap, `applySelectedTools()`, and `restoreWorkspaceSnapshot()` to `refreshToolModeState()`. Restore mode/prompts/tools first, then refresh once.
+
+Implement defensive collection:
+
+```javascript
+function collectSystemPrompts() {
+  const includeEventPrompt = toolInvocationMode.value !== "chat_agent_tools";
+  return buildPromptItems()
+    .filter((item) => item.prompt_kind !== "event_agent_system" || includeEventPrompt)
+    .filter((item) => includeEventPrompt || !isGeneratedEventPromptContent(item.content))
+    .map((item) => item.content)
+    .filter(Boolean);
+}
+```
+
+Wrap EventAgent model, temperature, max tokens, system prompt, and extra-body controls in `#event-agent-model-settings`. Disable that fieldset only in `chat_agent_tools`; keep loop, parallelism, and timeout controls outside it and enabled.
+
+- [x] **Step 5: Verify GREEN**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "event_prompt or prompt_restore or tool_mode or tools_ui" -q
+node --check src/agent_lab/presentation/static/app.js
+git diff --check
+```
+
+Expected: focused tests pass; Node and diff checks are clean.
+
+- [x] **Step 6: Commit Task 1**
+
+```bash
+git add tests/test_websocket_api.py src/agent_lab/presentation/static/app.js src/agent_lab/presentation/static/index.html
+git commit -m "fix: synchronize EventPrompt with tool mode" -m "Problem: EventPrompt could remain stale, duplicate after restore, or be sent in chat_agent_tools mode.\n\nRisk: legacy generated prompts are matched conservatively by established protocol markers."
+```
+
+### Task 2: Build the professional console layout and visual system
+
+**Files:**
+
+- Modify: `tests/test_websocket_api.py:1334-1607`
+- Modify: `src/agent_lab/presentation/static/index.html:7-247`
+- Modify: `src/agent_lab/presentation/static/styles.css:1-end`
+
+- [x] **Step 1: Write failing layout tests**
+
+Require these IDs and structural markers while retaining every existing runtime control ID:
+
+```python
+for control_id in [
+    "current-session-label",
+    "current-mode-label",
+    "toggle-debug-drawer",
+    "debug-drawer",
+    "session-controls",
+    "tool-controls",
+    "agent-config-controls",
+    "snapshot-controls",
+]:
+    assert f'id="{control_id}"' in html
+assert html.count('class="control-section-toggle"') == 4
+assert 'aria-controls="debug-drawer"' in html
+assert "--surface:" in css
+assert ".layout.debug-drawer-closed" in css
+assert "@media (max-width: 900px)" in css
+```
+
+Update the existing fixed-layout test to expect the new left/workspace/drawer shell instead of `.workspace-body` containing a permanent replay column.
+
+- [x] **Step 2: Run tests and verify RED**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "professional_console or layout_keeps_controls" -q
+```
+
+Expected: failure because the new shell and CSS tokens do not exist.
+
+- [x] **Step 3: Restructure the HTML shell**
+
+Use this hierarchy:
+
+```html
+<header class="topbar">
+  <div class="brand-block">
+    <h1>Agent Lab</h1>
+    <span>Chat and event runtime console</span>
+  </div>
+  <div class="top-status" aria-label="Runtime status">
+    <span id="current-session-label" class="status-badge">No session</span>
+    <span id="current-mode-label" class="status-badge">Dual Agent</span>
+    <span id="connection-status" class="status-badge status-neutral">Idle</span>
+    <button id="toggle-debug-drawer" type="button"
+      aria-expanded="true" aria-controls="debug-drawer">Hide Debug</button>
+  </div>
+</header>
+```
+
+Immediately after the header, create `main#console-layout.layout` with exactly three direct children in order: `aside.controls[aria-label="Runtime controls"]`, `section.workspace[aria-label="Conversation workspace"]`, and `aside#debug-drawer.debug-drawer[aria-label="Audit and usage"]`. Create four `.control-section` blocks in the controls child. Each toggle uses `data-collapse-target`, `aria-expanded`, and `aria-controls`; Session and Tools start expanded, Agent Config and Snapshot collapsed. Move Audit Replay and Session Usage into the drawer. Keep metrics above the anchored composer, preserve dialogs and all current IDs, and version both assets as `20260714-professional-console`.
+
+- [x] **Step 4: Implement the visual system and responsive layout**
+
+Start with these tokens and layout rules:
+
+```css
+:root {
+  --canvas: #f3f5f7;
+  --surface: #ffffff;
+  --surface-subtle: #f8fafc;
+  --border: #d9e0e7;
+  --text: #17212b;
+  --muted: #647180;
+  --accent: #176b87;
+  --accent-strong: #0f536a;
+  --danger: #b42318;
+  --radius: 10px;
+  --shadow: 0 8px 24px rgb(23 33 43 / 8%);
+}
+
+.layout {
+  display: grid;
+  grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) minmax(300px, 380px);
+  height: calc(100vh - 64px);
+  min-height: 0;
+  overflow: hidden;
+}
+
+.layout.debug-drawer-closed {
+  grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) 0;
+}
+```
+
+Add explicit primary/secondary/danger buttons, `:focus-visible`, compact selected tool cards, restrained role-specific messages, metrics/composer boundaries, drawer overflow, and `[hidden]` collapse rules. At `max-width: 900px`, allow body scrolling, stack the three regions, remove locked heights, and prevent horizontal overflow.
+
+- [x] **Step 5: Verify GREEN**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "static_ui or professional_console or layout or audit_replay or agent_config" -q
+node --check src/agent_lab/presentation/static/app.js
+git diff --check
+```
+
+Expected: focused static tests and checks pass.
+
+- [x] **Step 6: Commit Task 2**
+
+```bash
+git add tests/test_websocket_api.py src/agent_lab/presentation/static/index.html src/agent_lab/presentation/static/styles.css
+git commit -m "feat: redesign the debug console layout" -m "Problem: chat lacked visual priority and Audit/Usage permanently consumed workspace width.\n\nRisk: existing DOM IDs are retained to preserve runtime bindings."
+```
+
+### Task 3: Persist presentation state and synchronize top status
+
+**Files:**
+
+- Modify: `tests/test_websocket_api.py:1396-1620`
+- Modify: `src/agent_lab/presentation/static/app.js:1-120 and session lifecycle functions`
+
+- [x] **Step 1: Write failing persistence/status tests**
+
+```python
+assert 'const CONSOLE_UI_STORAGE_KEY = "agent-lab-console-ui-v1"' in js
+assert "function initializeConsoleUi()" in js
+assert "function setDebugDrawerOpen(" in js
+assert "function setControlSectionExpanded(" in js
+assert "localStorage.getItem(CONSOLE_UI_STORAGE_KEY)" in js
+assert "localStorage.setItem(CONSOLE_UI_STORAGE_KEY" in js
+assert "function updateTopbarStatus()" in js
+assert "current-session-label" in js
+assert "current-mode-label" in js
+assert "CONSOLE_UI_STORAGE_KEY" not in build_request
+```
+
+- [x] **Step 2: Run tests and verify RED**
+
+```bash
+uv run pytest tests/test_websocket_api.py -k "presentation_state or topbar_status" -q
+```
+
+Expected: failure because the presentation-state functions do not exist.
+
+- [x] **Step 3: Implement resilient browser-only UI state**
+
+```javascript
+const CONSOLE_UI_STORAGE_KEY = "agent-lab-console-ui-v1";
+
+function readConsoleUiState() {
+  try {
+    return JSON.parse(localStorage.getItem(CONSOLE_UI_STORAGE_KEY) || "{}");
+  } catch (_error) {
+    return {};
+  }
+}
+
+function writeConsoleUiState(state) {
+  try {
+    localStorage.setItem(CONSOLE_UI_STORAGE_KEY, JSON.stringify(state));
+  } catch (_error) {
+    // Presentation preferences must never block the runtime.
+  }
+}
+```
+
+`setControlSectionExpanded(button, expanded, { persist = true } = {})` updates `aria-expanded`, target `hidden`, and `sections[targetId]`. `setDebugDrawerOpen(open, { persist = true } = {})` updates layout class, drawer `hidden`, button text/ARIA, and `debug_drawer_open`. `initializeConsoleUi()` applies stored state over markup defaults and binds controls. None of these values enter snapshots or requests.
+
+- [x] **Step 4: Synchronize topbar session/mode context**
+
+```javascript
+function updateTopbarStatus() {
+  document.querySelector("#current-session-label").textContent = currentSessionId
+    ? `Session ${currentSessionId.slice(0, 8)}`
+    : "No session";
+  document.querySelector("#current-mode-label").textContent =
+    toolInvocationMode.value === "chat_agent_tools"
+      ? "ChatAgent Tools"
+      : "Dual Agent";
+}
+```
+
+Add `updateTopbarStatus();` to `refreshToolModeState()`, then call it after bootstrap, session creation/load, `session_started`, and any path replacing `currentSessionId`. Preserve active-turn and stale-socket guards.
+
+- [x] **Step 5: Verify GREEN**
+
+```bash
+uv run pytest tests/test_websocket_api.py -q
+node --check src/agent_lab/presentation/static/app.js
+git diff --check
+```
+
+Expected: all WebSocket/static tests pass with only the existing deprecation warning.
+
+- [x] **Step 6: Commit Task 3**
+
+```bash
+git add tests/test_websocket_api.py src/agent_lab/presentation/static/app.js
+git commit -m "feat: persist debug console presentation state" -m "Problem: drawer, control-section, session, and mode context were not synchronized.\n\nRisk: localStorage failures are ignored so UI preferences cannot block runtime requests."
+```
+
+### Task 4: Document, visually verify, and close Todo 53
+
+**Files:**
+
+- Modify: `README.md:94-102`
+- Modify: `docs/plans/todos.md:117`
+- Modify: `docs/plans/todo-53-professional-debug-console-ui.md`
+
+- [x] **Step 1: Update README operator guidance**
+
+Document the left control rail, primary chat workspace, anchored metrics/composer, collapsible Audit/Usage drawer, browser-only presentation state, and these rules:
+
+```markdown
+In `dual_agent`, the read-only EventPrompt is generated from selected tools and
+appears once in Chat Agent Prompt List. In `chat_agent_tools`, EventPrompt is
+hidden and excluded from `system_prompts`; mode changes and session/snapshot
+restores regenerate the correct state. EventAgent model settings are unavailable
+in direct mode, while shared loop, concurrency, and timeout controls remain active.
+```
+
+- [x] **Step 2: Run the complete automated gate**
+
+```bash
+uv run pytest
+node --check src/agent_lab/presentation/static/app.js
+git diff --check
+uv run python -c "from agent_lab.main import app; print(app.title)"
+```
+
+Expected: full pytest passes with only the known Starlette/httpx warning; Node/diff/import checks succeed.
+
+- [x] **Step 3: Run desktop browser QA (waived by user on 2026-07-15)**
+
+Start `uv run uvicorn agent_lab.main:app --host 127.0.0.1 --port 8000`, then verify section defaults/persistence, drawer width recovery, tools/All/None/count, mode badge, exactly one selected-tool EventPrompt in dual mode, no EventPrompt in direct mode, model-only disabled state, restore deduplication, chat streaming, audit dialog, and usage rendering.
+
+- [x] **Step 4: Run narrow browser QA (waived by user on 2026-07-15)**
+
+At width `<= 900px`, verify vertical stacking, no horizontal scrollbar, usable messages/metrics/composer/tools/modals, and removal of the drawer region when closed.
+
+- [x] **Step 5: Observe and update Todo 53**
+
+If a defect exists, keep 53 `in_progress` and add a `53.x` row directly below it with a focused plan. Otherwise mark 53 `done`, record exact test counts and browser evidence, and mark this plan's checkboxes complete.
+
+- [x] **Step 6: Commit Task 4**
+
+```bash
+git add README.md docs/plans/todos.md docs/plans/todo-53-professional-debug-console-ui.md
+git commit -m "docs: close professional console todo" -m "Problem: the new console and EventPrompt contract need durable guidance and verification evidence."
+```
+
+- [x] **Step 7: Final clean-state check and local merge**
+
+Run `git status --short` and `git log -5 --oneline`. Merge the verified feature branch locally into `codex/agent-lab-mvp`, then rerun the full automated gate on the merged branch.
+
+## Workflow Evolution Observation
+
+- Scoped subagents were effective for TDD implementation and independent review, but some could not write shared Git metadata. The main session should preserve their verified unstaged/staged changes and perform the scoped commit rather than waiting indefinitely.
+- Browser availability is an execution-environment capability. When it is unavailable, record the exact automated/HTTP evidence and an explicit user waiver; never report visual QA as passed.
+- These findings are recorded in this task report rather than expanding `AGENTS.md`: the existing plan-act-observe-update and per-todo commit rules already cover the durable workflow, while the observed limitations are tool-specific.

+ 38 - 0
docs/plans/todo-53.1-event-prompt-order-preservation.md

@@ -0,0 +1,38 @@
+# Todo 53.1: EventPrompt Order Preservation
+
+## Problem
+
+Commit `cf481ab` correctly made EventPrompt derived state, but `removeEventPromptItems()` deletes it and `ensureEventPromptItem()` appends the replacement. Tool changes, mode round trips, and restored prompt lists therefore move EventPrompt to the end and change serialized system-message order.
+
+## Scope
+
+- Preserve the first generated EventPrompt's position relative to retained custom prompts.
+- Remember that insertion position while `chat_agent_tools` hides EventPrompt.
+- Restore the saved typed/legacy EventPrompt position when loading prompt items.
+- Continue removing duplicate generated EventPrompt items and preserving ordinary custom prompts.
+- Do not add frontend dependencies or change backend contracts.
+
+## TDD Steps
+
+1. Add a failing regression in `tests/test_websocket_api.py` that requires an explicit EventPrompt insertion-position state/helper, captures position before removal, and reinserts with `insertBefore` instead of unconditional append.
+2. Run the focused test and confirm it fails for the current append-only implementation.
+3. Implement the smallest position-preserving logic in `src/agent_lab/presentation/static/app.js`.
+4. Verify tool refresh, `dual_agent -> chat_agent_tools -> dual_agent`, and restore paths use the same remembered position.
+5. Run:
+
+   ```bash
+   uv run pytest tests/test_websocket_api.py -k "event_prompt or prompt_restore or tool_mode" -q
+   node --check src/agent_lab/presentation/static/app.js
+   git diff --check
+   ```
+
+6. Commit with the solved problem and ordering risk, then repeat spec and code-quality reviews.
+
+## Acceptance
+
+- EventPrompt content and visibility remain mode/tool-derived.
+- At most one EventPrompt exists.
+- Refreshing tools does not move EventPrompt.
+- Hiding and showing EventPrompt restores its previous insertion position.
+- Restoring typed or legacy prompt data derives the insertion position from the first generated EventPrompt while discarding duplicates.
+- Custom prompt order and outbound request contracts remain unchanged.

+ 33 - 0
docs/plans/todo-53.2-console-interaction-state.md

@@ -0,0 +1,33 @@
+# Todo 53.2: Console Interaction State
+
+## Problem
+
+Commit `fd9c31e` introduced the professional console shell, but the new collapsed Agent Config/Snapshot sections and Hide Debug button have no JavaScript handlers. The intermediate UI therefore makes controls unreachable and cannot close the drawer.
+
+## Scope
+
+- Bind all `.control-section-toggle` buttons and update `aria-expanded` plus target `hidden` state.
+- Bind `#toggle-debug-drawer`, update `#console-layout.debug-drawer-closed`, drawer `hidden`, button text, and ARIA state.
+- Persist section and drawer preferences in a dedicated browser-only localStorage record.
+- Treat storage parse/write failures as non-fatal presentation failures.
+- Update `#current-session-label` and `#current-mode-label` from existing runtime state.
+- Keep presentation state out of workspace snapshots and WebSocket requests.
+- Preserve EventPrompt, active-turn, stale-socket, session, and backend contracts.
+
+## TDD Steps
+
+1. Add failing tests in `tests/test_websocket_api.py` that execute the interaction helpers with lightweight DOM/localStorage fakes, not only source substring checks.
+2. Confirm RED for unreachable sections/drawer and missing topbar synchronization.
+3. Add the smallest interaction state implementation to `src/agent_lab/presentation/static/app.js`.
+4. Verify bootstrap, mode refresh, session create/load/start, and session reset paths update topbar status.
+5. Run focused/full WebSocket tests, Node syntax, and diff checks.
+6. Commit and repeat specification plus code-quality review.
+
+## Acceptance
+
+- Agent Config and Snapshot can be expanded from their default collapsed state.
+- Drawer open/closed state changes layout and visibility consistently on desktop and narrow layouts.
+- Reload restores the last section/drawer preferences when storage is valid.
+- Invalid or unavailable storage does not block the runtime.
+- Topbar mode and abbreviated session labels stay current.
+- No console UI preference appears in request or snapshot payloads.

+ 28 - 0
docs/plans/todo-53.3-event-prompt-payload-singleton.md

@@ -0,0 +1,28 @@
+# Todo 53.3: EventPrompt Payload Singleton
+
+## Problem
+
+The DOM refresh path removes duplicate generated EventPrompt items, but `collectSystemPrompts()` in `dual_agent` currently serializes every prompt item. A stale or user-edited custom item containing the complete generated EventPrompt signature can therefore be sent alongside the managed EventPrompt.
+
+## Scope
+
+- At the outbound collection boundary, discard generated-signature custom items in both invocation modes.
+- In `dual_agent`, retain only the managed `event_agent_system` item plus ordinary custom prompts.
+- In `chat_agent_tools`, retain ordinary custom prompts only and send no EventPrompt.
+- Preserve prompt order and content for ordinary custom items.
+- Do not alter restore, EventPrompt positioning, backend contracts, or UI layout.
+
+## TDD Steps
+
+1. Change the existing Node behavior test so `dual_agent` expects exactly one generated prompt instead of accepting a generated-signature custom duplicate.
+2. Run the focused test and confirm RED against the current collector.
+3. Apply the minimal filter in `collectSystemPrompts()`.
+4. Verify ordinary custom prompts and managed EventPrompt order remain unchanged.
+5. Run focused/full WebSocket tests, Node syntax, diff checks, and independent review.
+
+## Acceptance
+
+- `dual_agent` serializes ordinary custom prompts and at most one managed EventPrompt.
+- `chat_agent_tools` serializes ordinary custom prompts and zero EventPrompt content.
+- A generated-signature custom item cannot bypass the outbound boundary.
+- EventPrompt visibility, ordering, restore deduplication, and tool-derived content remain unchanged.

+ 20 - 0
docs/plans/todo-53.4-managed-event-prompt-test-identity.md

@@ -0,0 +1,20 @@
+# Todo 53.4: Managed EventPrompt Test Identity
+
+## Problem
+
+The Todo 53.3 behavior test gives the managed EventPrompt and generated-signature custom impostor identical content. Its expected output therefore proves only that one string remains, not that the collector retained the managed item.
+
+## Scope
+
+- Change only `tests/test_websocket_api.py`.
+- Use two different strings that both satisfy the conservative generated EventPrompt signature.
+- Assign one to the custom impostor and the other to the managed `event_agent_system` item.
+- Assert `dual_agent` retains the managed content and rejects the impostor.
+- Assert `chat_agent_tools` rejects both generated contents while preserving ordinary custom prompts.
+- Do not change production code.
+
+## Verification
+
+- Run the focused EventPrompt test and the full WebSocket test file.
+- Run Node syntax and diff checks.
+- Repeat specification and code-quality review for Todo 53.3/53.4 together.

+ 19 - 0
docs/plans/todo-53.5-event-prompt-order-independent-identity.md

@@ -0,0 +1,19 @@
+# Todo 53.5: EventPrompt Order-Independent Identity
+
+## Problem
+
+Todo 53.4 distinguishes managed and impostor content, but tests only the ordering where the impostor precedes the managed item. An incorrect collector that keeps the last generated-signature item could still pass.
+
+## Scope
+
+- Change only the EventPrompt payload behavior test.
+- Run the same assertions with managed-before-impostor and managed-after-impostor prompt arrays.
+- In both orderings, `dual_agent` must retain ordinary custom prompts plus the managed content.
+- In both orderings, `chat_agent_tools` must retain ordinary custom prompts only.
+- Do not change production code.
+
+## Verification
+
+- Focused payload test passes for both orderings.
+- Full WebSocket test file, Node syntax, and diff checks pass.
+- Repeat the bounded code-quality review.

+ 41 - 0
docs/plans/todo-53.6-browser-visual-qa.md

@@ -0,0 +1,41 @@
+# Todo 53.6: Browser Visual QA
+
+**Status:** waived by user on 2026-07-15
+
+## Original Blocker
+
+The local application starts and serves the new assets, but the current Codex browser runtime reports an empty browser list. Desktop and narrow-screen visual/interaction QA therefore cannot be performed in this session.
+
+Chrome was checked separately on 2026-07-15: Chrome was running, the ChatGPT Chrome Extension was installed and enabled, and the Native Host manifest was correct, but extension communication remained unavailable. The user then explicitly instructed the task to skip browser acceptance. No browser result is represented as having passed.
+
+## Verified Before Blocker
+
+- Full suite: `617 passed`, one existing Starlette/httpx deprecation warning.
+- `node --check src/agent_lab/presentation/static/app.js` passes.
+- `git diff --check` passes.
+- Application import prints `Agent Lab`.
+- HTTP smoke at `http://127.0.0.1:8010/` returns the new `console-layout`, `debug-drawer`, `20260714-professional-console` assets, and console interaction functions.
+
+## Required Browser Checks
+
+### Desktop
+
+- Session and Tools start expanded; Agent Config and Snapshot start collapsed and can be opened.
+- Drawer opens/closes, releases chat width, and persists after reload.
+- Tool selection, All/None, count, mode badge, and EventPrompt remain synchronized.
+- `dual_agent` shows exactly one selected-tool EventPrompt.
+- `chat_agent_tools` hides EventPrompt and disables only EventAgent model-specific settings.
+- Switching back restores exactly one EventPrompt at its prior position.
+- Session/snapshot restore cannot duplicate EventPrompt.
+- Chat send, streaming, audit dialog, and usage rendering remain usable.
+
+### Narrow screen (`<= 900px`)
+
+- Controls, workspace, and open drawer stack vertically.
+- No horizontal page scrollbar appears.
+- Messages, metrics, composer, tool selection, and modal controls remain usable.
+- Closing the drawer removes its stacked region.
+
+## Closeout Decision
+
+The desktop and narrow-screen checks above were waived rather than executed. Todo 53 may close using the fresh automated gate, HTTP asset smoke, and independent code reviews as the accepted evidence. Any later visual defect must be recorded as a new follow-up todo instead of rewriting the completed Todo 53 history.

+ 75 - 0
docs/plans/todo-6-cleanup-closeout.md

@@ -0,0 +1,75 @@
+# Todo 6 Cleanup and Closeout Plan
+
+**Status:** done
+
+## Goal
+
+Clean local generated files from the deliverable surface, align contributor docs with the implemented project, and run final verification.
+
+## Scope
+
+This todo covers:
+
+- `.gitignore` updates for local-only files.
+- Removing or ignoring temporary `.superpowers/` browser-companion files from the deliverable set.
+- Updating `AGENTS.md` from the old empty-repo guide to the actual FastAPI/uv project.
+- Final test/import/help verification.
+- Final `docs/plans/todos.md` status update.
+
+This todo does not cover:
+
+- New runtime behavior.
+- New UI features.
+- Live LLM network calls.
+- Creating commits unless explicitly requested.
+
+## Files
+
+- Modify: `.gitignore`
+- Modify or create: `AGENTS.md`
+- Modify: `docs/plans/todos.md`
+- Modify: `docs/plans/todo-6-cleanup-closeout.md`
+- Do not modify production code unless final verification exposes a bug.
+
+## Steps
+
+1. Inspect `.gitignore` and current git status.
+2. Add local-only ignores:
+   - `.env`
+   - `.venv/`
+   - `.pytest_cache/`
+   - `.superpowers/`
+   - `__pycache__/`
+3. Update `AGENTS.md` to describe the actual repo:
+   - `src/agent_lab/` package layout.
+   - `tests/`.
+   - `scripts/ws_smoke.py`.
+   - `docs/plans/`.
+   - `uv` commands.
+   - FastAPI/WebSocket and pydantic-settings conventions.
+4. Run verification:
+
+```bash
+uv run pytest
+uv run python -c "from agent_lab.main import app; print(app.title)"
+uv run python scripts/ws_smoke.py --help
+git status --porcelain=v2 --branch
+```
+
+5. Update `docs/plans/todos.md` and this file with results.
+
+## Verification
+
+Completed result:
+
+- `.gitignore` now ignores `.env`, `.venv/`, `.pytest_cache/`, and `.superpowers/`; `__pycache__/` was already covered.
+- `AGENTS.md` was created for the implemented FastAPI, WebSocket, uv, DDD, and pydantic-settings project shape.
+- `uv run pytest` passed: `10 passed, 1 warning`.
+- `uv run python -c "from agent_lab.main import app; print(app.title)"` passed and printed `Agent Lab`.
+- `uv run python scripts/ws_smoke.py --help` passed and showed the expected CLI options.
+- `git status --porcelain=v2 --branch` no longer lists `.superpowers/`, `.venv/`, `.pytest_cache/`, or `.env`.
+
+Notes:
+
+- The remaining warning is the existing FastAPI/Starlette `TestClient` deprecation warning for the installed httpx integration.
+- The two `uv run python ...` verification commands initially hit sandbox permission limits while reading `~/.cache/uv`; they passed after rerunning with approval.

部分文件因文件數量過多而無法顯示