Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lingtai/kernel/base_agent/ANATOMY.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@ Generic agent kernel. Single class `BaseAgent` with methods distributed across 6
- **`__init__.py` is large.** This is intentional — the constructor, properties, state machine, hooks, cross-cutting infrastructure (`_save_chat_history`, `_log`, notification sync, legacy tc_inbox drain), and pass-through stubs all live here. The package is not "thin shell + 6 leaves"; the remaining code is genuinely cross-cutting or bound to the class definition.
- **Pass-through pattern.** Each extracted method becomes a 2-line stub in `__init__.py` (lazy import + call). This preserves the `BaseAgent` class interface while the implementation lives in submodules. The `self` → `agent` conversion is mechanical but `_heartbeat_loop` (~264 lines, 15+ cross-cluster calls) deserves extra-careful review.
- **Subclass overrides stay on `__init__.py`.** `_activate_preset`, `_activate_default_preset`, `_build_launch_cmd`, `_pre_request`, `_post_request`, `_on_tool_result_hook`, `_cpr_agent` are all overridden by the `Agent` subclass in the wrapper package. They must remain as methods on `BaseAgent`.
- **Telegram Task Card hooks (turn-local, route B).** Hooks in `__init__.py` implement the batched Task Card lifecycle — a rolling window of the newest **N ordinary tool rows** (env-configurable, default 1), not a single transient current step. `_setup_telegram_task_card` (`base_agent/__init__.py:2870-2937`) captures the current Telegram inbound route (`data.previews[0].message_ref` → `account:chat_id`), resolves the Telegram MCP client from `_mcp_clients_by_tool`, and stores a fingerprint-deduped turn-local `_telegram_task_card_context` (rows / generation / monotonic `clock` / `wall_clock` / heartbeat). `_on_tool_pre_dispatch_hook` (`base_agent/__init__.py:2342-2410`) appends one row per tool call in actual pre-dispatch order, opens a new epoch when no tool row is active (bumps `generation` so a stale heartbeat can't overwrite the window; supersedes a lingering API-error row, since a fresh tool batch means the LLM responded), then caps ordinary tool rows in place via `_cap_task_card_tool_rows` (`base_agent/__init__.py:2412-2428`) to `_task_card_max_tool_rows()` (`base_agent/__init__.py:83`, reads `LINGTAI_TASK_CARD_MAX_TOOL_ROWS`; positive int N, else fail-safe to 1; N is not otherwise clamped, so a very large N can exceed the render budget/transport limit) — the API-error row is retained and never evicted by the tool cap. `_on_tool_result_hook` (`base_agent/__init__.py:2279`) → `_freeze_task_card_row` (`base_agent/__init__.py:2309`) freezes the matching row (final whole-second elapsed + done) while others keep ticking; a row already scrolled out of the window has no matching `call_id`, so its late result is a no-op and cannot mutate the window. Rendering (create lazily, else edit the same card) is `_render_task_card` (`base_agent/__init__.py:2443`); the 0.5s heartbeat `_task_card_heartbeat_tick` (`base_agent/__init__.py:2724`) / `_start_task_card_heartbeat` (`base_agent/__init__.py:2759`) refreshes only active tool-row elapsed (floored to whole seconds) under the lock and no-ops on a stale `generation`. Each render reverse-calls the **private, unlisted MCP tool name** `_TASK_CARD_TOOL` (`= "_lingtai_telegram_task_card"`, `base_agent/__init__.py:71`) via `call_tool(_TASK_CARD_TOOL, {"sub_action": ..., ...})` — it sends **no** `action`; the Telegram server forces `_task_card_update` server-side (see `tools/mcp/ANATOMY.md`). A recursion guard skips the hook for `_TASK_CARD_TOOL` itself. Because `call_tool` reports tool-level failures as an error *dict* (and can also raise), the hooks inspect the result so an error is never treated as a created card (no fake success), compute the `message_id` once, stay fail-open, and make both error-dicts and raised exceptions observable via content-free warnings (phase/tool + status-or-exception-class only — never reasoning, chat id, account, card id, or provider text). LLM/provider API failures are surfaced onto the same card as one stable sentinel row by `_report_task_card_api_error` (`base_agent/__init__.py:2648`). `_teardown_telegram_task_card` (`base_agent/__init__.py:2939`) freezes the card on its concrete last batch (the retained newest-N rows + `✓` markers + final elapsed) with no generic overall `DONE` subject, surfaces a finalize error/exception the same content-free way, and clears context in `finally`. Setup is called in `lifecycle.py:549` after `_sync_notifications()` inside the heartbeat loop. Ordinary-request teardown runs in `turn.py:1123` (finally block) and in `_post_request` at `__init__.py:2277` (normal path). tc-wake teardown runs in `turn.py:1332` (finally block around the wire-drive handler). The real turn→ToolExecutor execute-hook wiring is at `turn.py:1732-1734` (`on_pre_dispatch_hook=getattr(agent, "_on_tool_pre_dispatch_hook", None)`). The serial pre-future parallel hook block is at `tool_executor.py:1424-1432` (Phase 2, before `ThreadPoolExecutor` spawns futures), so parallel pre-dispatch order is deterministic. The MCP consumer side is `TelegramManager._handle_task_card_update` (`src/lingtai/mcp_servers/telegram/manager.py:1374`), a private action not in SCHEMA, dispatched via `manager.handle()` at `src/lingtai/mcp_servers/telegram/manager.py:466` → `action == "_task_card_update"`; its update-first resident-card singleton and single card-level `时间 HH:MM:SS UTC±HH` line are documented in `src/lingtai/mcp_servers/ANATOMY.md`.
- **Telegram Task Card hooks (turn-local, route B).** Hooks in `__init__.py` implement the batched Task Card lifecycle — a rolling window of the newest **N ordinary tool rows** (env-configurable, default 1), not a single transient current step. `_setup_telegram_task_card` (`base_agent/__init__.py:2884-2952`) captures the current Telegram inbound route (`data.previews[0].message_ref` → `account:chat_id`), resolves the Telegram MCP client from `_mcp_clients_by_tool`, and stores a fingerprint-deduped turn-local `_telegram_task_card_context` (rows / generation / monotonic `clock` / `wall_clock` / heartbeat). `_on_tool_pre_dispatch_hook` (`base_agent/__init__.py:2356-2423`) appends one row per tool call in actual pre-dispatch order, opens a new epoch when no tool row is active (bumps `generation` so a stale heartbeat can't overwrite the window; supersedes a lingering API-error row, since a fresh tool batch means the LLM responded), then caps ordinary tool rows in place via `_cap_task_card_tool_rows` (`base_agent/__init__.py:2426-2444`) to `_task_card_max_tool_rows()` (`base_agent/__init__.py:84`, reads `LINGTAI_TASK_CARD_MAX_TOOL_ROWS`; positive int N, else fail-safe to 1; N is not otherwise clamped, so a very large N can exceed the render budget/transport limit) — the API-error row is retained and never evicted by the tool cap. `_on_tool_result_hook` (`base_agent/__init__.py:2293`) → `_freeze_task_card_row` (`base_agent/__init__.py:2323`) freezes the matching row (final whole-second elapsed + done) while others keep ticking; a row already scrolled out of the window has no matching `call_id`, so its late result is a no-op and cannot mutate the window. Rendering (create lazily, else edit the same card) is `_render_task_card` (`base_agent/__init__.py:2457`); the 0.5s heartbeat `_task_card_heartbeat_tick` (`base_agent/__init__.py:2738`) / `_start_task_card_heartbeat` (`base_agent/__init__.py:2773`) refreshes only active tool-row elapsed (floored to whole seconds) under the lock and no-ops on a stale `generation`. Each render reverse-calls the **private, unlisted MCP tool name** `_TASK_CARD_TOOL` (`= "_lingtai_telegram_task_card"`, `base_agent/__init__.py:72`) via `call_tool(_TASK_CARD_TOOL, {"sub_action": ..., ...})` — it sends **no** `action`; the Telegram server forces `_task_card_update` server-side (see `tools/mcp/ANATOMY.md`). A recursion guard skips the hook for `_TASK_CARD_TOOL` itself. Because `call_tool` reports tool-level failures as an error *dict* (and can also raise), the hooks inspect the result so an error is never treated as a created card (no fake success), compute the `message_id` once, stay fail-open, and make both error-dicts and raised exceptions observable via content-free warnings (phase/tool + status-or-exception-class only — never reasoning, chat id, account, card id, or provider text). LLM/provider API failures are surfaced onto the same card as one stable sentinel row by `_report_task_card_api_error` (`base_agent/__init__.py:2662`). `_teardown_telegram_task_card` (`base_agent/__init__.py:2954`) freezes the card on its concrete last batch (the retained newest-N rows + `✓` markers + final elapsed) with no generic overall `DONE` subject, surfaces a finalize error/exception the same content-free way, and clears context in `finally`. Setup is called in `lifecycle.py:549` after `_sync_notifications()` inside the heartbeat loop. Ordinary-request teardown runs in `turn.py:1123` (finally block) and in `_post_request` at `__init__.py:2291` (normal path). tc-wake teardown runs in `turn.py:1332` (finally block around the wire-drive handler). The real turn→ToolExecutor execute-hook wiring is at `turn.py:1732-1734` (`on_pre_dispatch_hook=getattr(agent, "_on_tool_pre_dispatch_hook", None)`). The serial pre-future parallel hook block is at `tool_executor.py:1424-1432` (Phase 2, before `ThreadPoolExecutor` spawns futures), so parallel pre-dispatch order is deterministic. The MCP consumer side is `TelegramManager._handle_task_card_update` (`src/lingtai/mcp_servers/telegram/manager.py:1377`), a private action not in SCHEMA, dispatched via `manager.handle()` at `src/lingtai/mcp_servers/telegram/manager.py:472` → `action == "_task_card_update"`; its update-first resident-card singleton and single card-level `时间 HH:MM:SS UTC±HH` line are documented in `src/lingtai/mcp_servers/ANATOMY.md`.
Loading