Commit 365c5b9
refactor: typed AgentEvent + edit-diff + tool streaming + pi-coding-agent backend + web renderer registry (#138)
* feat(core/events): extract events_common.py shared base variants (Pi Step 1a)
Pydantic discriminated union variants — MessageStart/Update/End,
ToolExecutionStart/Update/End — that both ChatEvent and the upcoming
AgentEvent compose. ChatEvent migrated to import these from
events_common; chat-specific naming aliases retained for back-compat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core): typed AgentEvent union for agent task sessions (Pi Step 1b)
core/agent_events.py defines AgentStart, AgentEnd, TurnStart, TurnEnd,
CompactionOccurred plus shared variants from events_common, and all
legacy SessionEventType-mapped variants. core/_events.py gains
emit_typed() method accepting AgentEvent instances, settlement-rule
wait_idle() / register_agent_end_subscriber() / notify_agent_end_handled()
APIs, and dual-mode coalescing/terminal detection for both legacy uppercase
and new lowercase kind strings.
SessionEvent.event_type relaxed from SessionEventType enum to str so new
kind-based events persist without coercion. All is-identity comparisons
on event_type replaced with equality comparisons.
Alembic revision 5041f8573a34 drops and recreates task_events with CASCADE
FK (alpha hard cut). AGENT_STATUS retained — zero-cost removal deferred
until all TUI/server consumers migrate in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(events): emit AgentEvent through wire generator (Pi Step 1c)
scripts/generate_wire_types.py extended with static AgentEvent TS section
(33 interface definitions + AgentEvent discriminated union type). Wire
output regenerated to packages/shared/api-client/src/wire.ts. Wire-drift
gate confirms sync. Settlement rule wired into Events aggregate:
register_agent_end_subscriber / notify_agent_end_handled / wait_idle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp/fs): port edit-diff CRLF/BOM/overlap handling from pi-coding-agent (Pi Step 7)
Direct port of references/pi-mono/packages/coding-agent/src/core/tools/edit-diff.ts.
New module mcp/toolsets/_edit_diff.py provides normalize_line_endings,
detect_bom, reapply_line_endings, merge_overlapping_edits, plus the Edit
dataclass and EditConflict exception. Also includes apply_edits_to_normalized_content
(core text-hunk replacement engine with fuzzy matching), and LineEdit for
line-range overlap merging.
mcp/toolsets/fs.py is a new toolset with three tools:
- fs_read_file (WORKER+): read file bytes, detect BOM/EOL, return metadata
- fs_write_file (ORCHESTRATOR): fresh create/overwrite, no BOM preservation
- fs_edit_file (ORCHESTRATOR): old->new text replacement preserving BOM + EOL
All file-mutation tools route through apply_edits() which:
1. Reads bytes; detects BOM (UTF-8-sig, UTF-16-LE/BE, UTF-32-LE/BE).
2. Decodes using detected encoding (UTF-8 fallback).
3. Normalizes EOLs to LF; records original eol_style.
4. Applies edits to LF-normalized content (fuzzy match for smart quotes/
dashes/special spaces -- same algorithm as pi-mono).
5. Reapplies original EOL style.
6. Re-encodes and prepends original BOM if present.
Audit of existing write_text calls:
- src/kagan/core/_launchers.py: writes .mcp.json, startup prompt, attach
context -- generated tool configs, not user-edited content. Left alone.
- src/kagan/core/_agent.py: writes .mcp.json -- same reason. Left alone.
- src/kagan/core/chat/_factories.py: writes .mcp.json. Left alone.
- src/kagan/core/_preflight.py: probe write. Left alone.
- src/kagan/core/_prompt_export.py: writes exported prompt YAML.
Not user-edited content (export artifact). Left alone.
No existing naïve write_text calls touch user-edited source files, so no
migration was needed beyond the new fs toolset.
Policy: fs_read_file -> WORKER+; fs_write_file, fs_edit_file -> ORCHESTRATOR.
Contract tests (41): CRLF round-trip, BOM preservation, overlapping-edit
merge correctness, fuzzy match, all error paths.
Behavioral tests (15): full MCP tool protocol round-trips with real files
in tmp_path -- BOM+CRLF compound round-trips, missing-file error, overlapping
error, multi-edit, trailing-newline edge case.
Pi-mono integration step 7 of 6 (Steps 1, 7, 2, 4, 3, 5 land on this
branch before one consolidated PR).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: pre-sweep — remove legacy / dead / backcompat / shim code (pi-mono)
Before remaining pi-mono steps (2, 4, R6a, 5) land, sweep soft-cut
residue from R1-R5 and Pi Step 1.
B — Settlement-rule subscribers wired in core/_sessions.py:
register_agent_end_subscriber() called before ACP agent spawns;
notify_agent_end_handled() released in _handle_acp_done() finally
block so wait_idle() unblocks on both success and error paths.
New unit test file tests/core/unit/test_settlement_rule.py (7 tests)
covering: immediate return, timeout, blocking until notify,
multi-subscriber drain, error-path release.
_FakeEvents in tests/unit/test_sessions_shutdown.py extended with
publish_board / notify_agent_end_handled / register_agent_end_subscriber
stubs to match the expanded Events protocol.
C — R3 thin re-export shim TS files deleted:
packages/shared/api-client/src/types.ts — 461-LOC hand-written file
that was never imported by anything (not in index.ts); dead since R3
promoted everything to wire.ts.
packages/vscode/src/api/types.ts — 153-LOC re-export shim deleted;
all 22 vscode consumers (providers/, commands/, extension.ts,
api/client.ts, api/sse.ts, api/local.ts) now import directly from
@kagan/shared-api-client.
packages/web/src/lib/api/types.ts — 168-LOC re-export shim deleted;
all 61 web consumers now import directly from @kagan/shared-api-client.
DoctorReportResponse added to web/src/lib/api/client.ts top-level
import block (was an inline import type referencing the deleted shim).
Deferrals (reported):
A — AGENT_STATUS / SessionEventType full retirement: enum has 20
active values; migration touches _acp.py, _sessions.py, _events.py
and TUI screens (not in eng-core lane). Deferred to pi-step-2 or
dedicated sweep task.
D — _transitions.py deletion: validate_move() is still called from
Tasks.set_status() (line 619) and validate_merge_move() from
merge_task(); Tasks.set_status() is still an active code path for
reject_review and detached-agent completion. Deleting _transitions.py
requires first migrating Tasks.set_status() to the transitions.py
funnel. Deferred.
F — Comment debt: "legacy" comments in _events.py describe real
dual-format DB behaviour (uppercase enum vs lowercase kind strings
for historical rows). Not stale — legitimately deferred until the
DB migration that rewrites old rows runs.
H — Vulture: clean (0 findings).
I — Test fixtures: _session_picker module is still live (phase 6
extraction intact), so fixture imports are valid.
Net: 91 files changed, +109 / -870 LOC.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: complete pi-mono pre-sweep — retire SessionEventType + delete _transitions.py
Round-2 sweep finishes the two categories deferred in dae507d:
A. SessionEventType enum deleted from core/enums.py and removed from
core/__init__.py public surface. All ~25 call sites across
core/_acp, core/_sessions, core/_events, core/_reviews,
core/_tasks, core/transitions, core/_agent_monitor,
server/mcp/toolsets/sessions, cli/doctor, cli/chat/controller,
plus 4 TUI screen files (session_dashboard, _chat_runner,
task_screen, doctor_modal) and task_event_handler + task_workspace_helpers
migrated to kind string comparisons (e.g. "output_chunk",
"task_status_changed"). The SESSION_EVENT_TYPE_TO_KIND bridge map
in agent_events.py is deleted. _events.emit() signature changed
from SessionEventType to str. wire.ts regenerated with EVENT_TYPE
constants now derived from AgentEvent kind strings instead of the
removed enum.
D. core/_transitions.py deleted. validate_move / validate_merge_move
guards folded into callers:
- Tasks.set_status now enforces the direct-move matrix inline
(REVIEW->DONE excluded, same invariant as before).
- merge_task uses an inline InvalidTransitionError guard instead of
validate_merge_move.
- reject_review emits "task_status_changed" kind string directly.
tests/core/unit/test_transitions.py rewritten to test the same
invariants without importing the deleted module.
Net delete: ~459 LOC (130 from _transitions.py, 23 from SessionEventType
enum body, 27 from SESSION_EVENT_TYPE_TO_KIND map, rest call site cleanups).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): opt-in tool partial-result streaming for bash_exec + terminal_run (Pi Step 2)
bash_exec and terminal_run toolsets are introduced with a keyword-only
on_update callback. When the MCP server has a bound task context
(task_id + session_id), the dispatcher wires the callback to emit typed
ToolExecutionUpdate events on the per-task stream for each stdout/stderr
line, correlated with the tool_id from ToolExecutionStart. Without task
context the callback degrades gracefully (output buffered, no events).
Other ~28 toolsets are sub-second one-shots and stay unchanged.
The run_bash() helper in bash.py is shared by terminal_run and provides
the asyncio subprocess execution primitive with a streaming _merge_streams
coroutine that drains stdout and stderr concurrently.
terminal_run adds a max_output_lines cap for high-frequency output and
a 10-minute default timeout (vs 5 minutes for bash_exec).
Both tools are ORCHESTRATOR-role only in _policy.py.
Contract tests (13 new) verify:
- on_update callback receives each stdout/stderr line during execution
- Default None callback buffers output unchanged
- Timeout fires correctly (timed_out=True)
- Non-zero exit codes propagate
- MCP tool registration and result shape for both tools
- No error when no task context is available (graceful degradation)
Spinner placeholder rendering for bash_exec/terminal_run: N/A — tools
are brand new so no placeholder rendering existed. Pi Step 5 will add
the renderer registry that consumes ToolExecutionUpdate.partial_result.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core): add pi-coding-agent as 15th backend (R6a)
New BackendSpec("pi-coding-agent", ...) entry uses pi's JSONL-framed
RPC mode instead of ACP. New module core/adapters/pi_rpc.py provides
PiRpcClient + translate_pi_rpc_message (JSONL → AgentEvent).
BackendCapability.PI_RPC_STREAMING added as a sibling to ACP_STREAMING.
Backend selection can identify pi-rpc backends via has_capability().
Node >= 20.6 gated via check_node_version() + _parse_node_version() in
_environment_checks.py.
Translator splits into per-domain helpers dispatched via a static dict
to keep McCabe complexity within the project limit (≤ 20).
Smoke test asserts subprocess lifecycle (start + close) when npx is
on PATH with Node >= 20.6; skipped otherwise.
Unit test for the message translator covers each pi RPC frame shape
(36 assertions across 41 test cases).
Net add: 627 LOC core/adapters/pi_rpc.py + 21 LOC _agent.py +
36 LOC _environment_checks.py + 235 LOC tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(web): tool renderer registry + artifacts panel + RAF-batched streaming (Pi Step 5)
packages/web/src/lib/tool-renderers/ holds a plain Map<string, React.FC>
of tool renderers. getToolRenderer(name) falls back to DefaultRenderer
for unregistered tools. Hardcoded StreamToolPill replaced by StreamToolCard
which delegates to the registry.
ArtifactsPanel.tsx renders sandboxed iframes (sandbox="", srcDoc) for
HTML / SVG / Markdown artifacts. PDF/DOCX/XLSX deferred. jotai
artifactsAtom backs the panel.
streaming-message.ts ports pi-web-ui's RAF-batching pattern to React 19
+ jotai. Per-token CHAT_CHUNK SSE frames coalesce to one setState per
frame, fixing per-token rendering jank.
No runtime registerToolRenderer() API — YAGNI.
Net: 6 new source files, 3 new test files.
Registered tool renderers: bash_exec, bash, terminal_run, js_repl,
edit_file, str_replace_editor, read_file, apply_diff, patch.
vitest: 351 passed (54 test files, +20 new test cases).
Build: clean (pnpm run build, tsc --noEmit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: scrub internal pi-mono references from Kagan code
User-facing docs naming pi-coding-agent as a backend stay. Internal
module docstrings and code comments that referenced the sister project
as a heritage source are rewritten to be self-describing.
Affected files:
- server/mcp/toolsets/_edit_diff.py -- module docstring (removed
"Ported from references/pi-mono/...") replaced with a description of
the functionality; inline comment "mirrors pi-mono detectLineEnding"
removed from normalize_line_endings docstring
The package constant PI_CODING_AGENT_BACKEND = "pi-coding-agent" and
the @mariozechner/pi-coding-agent npm name are unchanged (they describe
the external tool, not internal heritage).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address Greptile P1 + 3 P2 on consolidated pi-mono PR
P1 (event ordering — bash_exec / terminal_run):
Fire-and-forget on_update emit tasks were not awaited before the
tool_execution_end event shipped. Clients saw end → late updates,
breaking the start→updates→end ordering renderers depend on.
Both tools now collect pending update Tasks and asyncio.gather them
before emitting end.
P2 (_DIRECT_MOVES rebuilt every set_status call):
Moved the frozenset literal from inside Tasks.set_status to module-
level in _tasks.py. Built once at import; same invariant.
P2 (turn_index hardcoded to 0):
PiRpcClient now maintains a per-prompt _turn_counter incremented at
the start of each _run_prompt. translate_pi_rpc_message accepts a
turn_index kwarg and prefers a pi-supplied frame value when present;
falls back to the caller's counter. All translator helpers gain the
kwarg for dispatch consistency.
P2 (_cumulative_bytes not reset between prompt() calls):
Counter now resets at the start of _run_prompt. The 500 MB cap is a
per-prompt safety bound, not a session-lifetime budget — long-lived
clients across many prompts no longer drift toward the ceiling.
1687 tests pass (was 1687 — flat). Lint, typecheck, deadcode clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: boundary typing — kill dict[str,Any] + isinstance ladders
Pattern across the codebase: external JSON-shaped data crossed the
Python boundary as dict[str, Any] and every consumer did
raw = obj.get("field"); if isinstance(raw, T): ... Replace with
Pydantic models at the boundary so downstream code reads typed
attributes.
Sites swept:
1. core/adapters/pi_rpc.py — 13 _translate_X helpers + _TRANSLATOR_DISPATCH
dict consolidated into one match statement over typed PiRpcMessage
variants. New pi_rpc_messages.py adds boundary models for all 19 known
pi frame types with extra="allow" and frozen=True. parse_pi_rpc_message
does O(1) type-tag lookup + model_validate instead of a union traversal.
Per-handler turn_index pollution gone; pi's real protocol has no
turn_index field on turn_start/turn_end frames (verified against
references/pi-mono/packages/agent/src/types.ts). _update_assembled_text
and _track_message_state merged into one pass per message.
2. server/_chat_routes.py::_parse_attachments — Attachment + AttachmentBody
models replace hand-rolled isinstance(a, dict) + str(a.get(...)) loop.
Attachment lives in core/_io/sessions.py and is re-exported from
kagan.core. Downstream dict consumers (SpawnPerTurnACPFactory,
engine.push_user) receive .model_dump() dicts; wire shape unchanged.
3. MCP toolset arg shaping — audit complete; all toolsets (tasks,
sessions, projects, review, bash, terminal_run, _edit_diff) already
use typed Python signatures or core/_io/ models from R2. No changes
needed.
4. server _session_to_wire / chat_session_to_legacy_dict — kept as-is;
both involve explicit field selection / datetime formatting logic that
legitimately belongs in a function, not a mechanical model_validate.
5. TUI event consumers — verified clean post Pi Step 1. No leftover
event.payload[X] dict reads on the pi RPC path.
Net: pi_rpc.py -207 LOC, _chat_routes.py +20 LOC (clearer intent),
+2 new files (pi_rpc_messages.py, test_pi_rpc_messages.py,
test_attachment_model.py). Tests: +60 new assertions across 3 files.
All 1727 existing tests pass; 0 new failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent dce2635 commit 365c5b9
163 files changed
Lines changed: 6345 additions & 1321 deletions
File tree
- packages
- vscode/src
- api
- commands
- providers
- web/src
- components
- board
- chat
- home
- layout
- mentions
- session
- settings
- welcome
- workspace
- lib
- api
- atoms
- chat
- commands
- hooks
- tool-renderers
- utils
- pages
- test
- scripts
- src/kagan
- cli
- chat
- core
- _io
- adapters
- db/migrations/versions
- chat
- server
- mcp
- toolsets
- tui
- screens
- widgets
- tests
- core
- unit
- mcp
- contract
- tui
- unit
- core
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
This file was deleted.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | 11 | | |
13 | | - | |
| 12 | + | |
14 | 13 | | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
36 | 46 | | |
37 | 47 | | |
38 | 48 | | |
| |||
318 | 328 | | |
319 | 329 | | |
320 | 330 | | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
321 | 525 | | |
322 | 526 | | |
323 | 527 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
0 commit comments