wip: phase 4 planning complete, ready to execute
This commit is contained in:
parent
0e132849cc
commit
6923b801a3
7 changed files with 956 additions and 140 deletions
|
|
@ -1,97 +1,76 @@
|
|||
{
|
||||
"version": "1.0",
|
||||
"timestamp": "2026-04-07T23:54:30.473Z",
|
||||
"phase": "02-prototype",
|
||||
"phase_name": "matrix-direct-agent-prototype",
|
||||
"phase_dir": ".planning/phases/02-prototype",
|
||||
"plan": 1,
|
||||
"task": 4,
|
||||
"total_tasks": 4,
|
||||
"timestamp": "2026-04-17T12:34:43.144Z",
|
||||
"phase": "04",
|
||||
"phase_name": "Matrix MVP: shared agent context and context management commands",
|
||||
"phase_dir": ".planning/phases/04-matrix-mvp-shared-agent-context-and-context-management-comma",
|
||||
"plan": null,
|
||||
"task": null,
|
||||
"total_tasks": null,
|
||||
"status": "paused",
|
||||
"completed_tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Add Direct Agent Session Transport (sdk/agent_session.py)",
|
||||
"status": "done",
|
||||
"commit": "de20ff6"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Add Local Prototype State (sdk/prototype_state.py)",
|
||||
"status": "done",
|
||||
"commit": "2fad1aa"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Implement RealPlatformClient (sdk/real.py)",
|
||||
"status": "done",
|
||||
"commit": "9784ca6"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Wire Matrix Runtime to Real Backend (adapter/matrix/bot.py)",
|
||||
"status": "done",
|
||||
"commit": "94bdb44"
|
||||
}
|
||||
{"id": 1, "name": "Phase 4 CONTEXT.md — design decisions from session", "status": "done"},
|
||||
{"id": 2, "name": "Phase 4 RESEARCH.md — AgentApi lifecycle, platform findings", "status": "done"},
|
||||
{"id": 3, "name": "Phase 4 planning — 3 PLAN.md files (planner + checker + revision)", "status": "done"}
|
||||
],
|
||||
"remaining_tasks": [],
|
||||
"blockers": [
|
||||
{
|
||||
"description": "Backend/provider errors can still escape as PlatformError and crash the Matrix surface instead of degrading into a user-facing reply.",
|
||||
"type": "technical",
|
||||
"workaround": "Catch PlatformError in the message path or dispatcher boundary and emit a normal OutgoingMessage while logging the root cause."
|
||||
},
|
||||
{
|
||||
"description": "The required thread_id patch lives only in the local external/platform-agent clone and is not yet upstreamed.",
|
||||
"type": "external",
|
||||
"workaround": "Push or reapply external/platform-agent commit 1dca2c1 in the platform-agent repo before broader handoff."
|
||||
}
|
||||
"remaining_tasks": [
|
||||
{"id": 4, "name": "Execute 04-01: Replace AgentSessionClient with AgentApi + AgentApiWrapper", "status": "not_started"},
|
||||
{"id": 5, "name": "Execute 04-02: !save/!load/!reset/!context commands + PrototypeStateStore extensions", "status": "not_started"},
|
||||
{"id": 6, "name": "Execute 04-03: Dockerfile + docker-compose.yml + .env.example", "status": "not_started"}
|
||||
],
|
||||
"blockers": [],
|
||||
"human_actions_pending": [
|
||||
{
|
||||
"action": "Push or upstream the local external/platform-agent patch that adds WebSocket thread_id support.",
|
||||
"context": "The Matrix prototype depends on external/platform-agent commit 1dca2c1, but that change is only in the local clone under external/ and is not part of surfaces.git.",
|
||||
"action": "Request platform team to add POST /reset endpoint to platform-agent",
|
||||
"context": "!reset needs POST {AGENT_BASE_URL}/reset to reinitialize AgentService singleton. Currently returns unavailable. ~3 lines on their side.",
|
||||
"blocking": false
|
||||
},
|
||||
{
|
||||
"action": "Rotate exposed credentials used during manual testing.",
|
||||
"context": "Matrix password, provider key, and Telegram token were pasted into the session during bring-up and should be considered compromised.",
|
||||
"action": "Rotate credentials used during manual testing",
|
||||
"context": "Matrix password and OpenRouter API key sk-or-v1-d27c07... were shared in chat session.",
|
||||
"blocking": false
|
||||
}
|
||||
],
|
||||
"decisions": [
|
||||
{
|
||||
"decision": "Keep the prototype in this repo on its own branch rather than creating a separate Matrix spike repo.",
|
||||
"rationale": "This reuses the existing Matrix adapter and tests and keeps the migration path to future surfaces inside the same SDK boundary.",
|
||||
"phase": "02-prototype"
|
||||
"decision": "Wrap AgentApi in AgentApiWrapper (sdk/agent_api_wrapper.py) to add last_tokens_used tracking",
|
||||
"rationale": "AgentApi.send_message() drops MsgEventEnd without yielding it. Wrapper subclasses AgentApi and overrides _listen() to capture tokens_used. Avoids modifying external/ platform package.",
|
||||
"phase": "04"
|
||||
},
|
||||
{
|
||||
"decision": "Use a split backend boundary: AgentSessionClient plus PrototypeStateStore behind RealPlatformClient.",
|
||||
"rationale": "This keeps Matrix logic stable while allowing later replacement of local state with a real control plane.",
|
||||
"phase": "02-prototype"
|
||||
"decision": "Remove build_thread_key and thread_id from WS URL entirely",
|
||||
"rationale": "platform-agent origin/main does not support thread_id query param. Architecture: one container = one chat = isolated context by design.",
|
||||
"phase": "04"
|
||||
},
|
||||
{
|
||||
"decision": "Patch only platform-agent for per-chat memory and keep agent_api unchanged.",
|
||||
"rationale": "Reading thread_id from the WebSocket query string minimizes rebase surface and avoids rewriting the message payload contract.",
|
||||
"phase": "02-prototype"
|
||||
"decision": "!reset calls POST /AGENT_BASE_URL/reset, returns unavailable message if 404",
|
||||
"rationale": "MemorySaver is in-memory — endpoint reinitializes singleton. Endpoint not yet in platform-agent origin/main.",
|
||||
"phase": "04"
|
||||
},
|
||||
{
|
||||
"decision": "Use collision-safe serialized thread keys rather than the raw spec example matrix:user:chat format.",
|
||||
"rationale": "Matrix IDs contain colons, so the raw concatenation could collide across distinct user/chat tuples.",
|
||||
"phase": "02-prototype"
|
||||
"decision": "!save/!load are agent-mediated via formatted text messages to the agent",
|
||||
"rationale": "Agent has write_file/read_file tools for /workspace/contexts/. No direct FS access from surfaces-bot to agent container.",
|
||||
"phase": "04"
|
||||
},
|
||||
{
|
||||
"decision": "Treat repeat Matrix invites as join-only if the user was already provisioned.",
|
||||
"rationale": "Provisioning is one-time per locally known user; repeat invites should not recreate Space/chat trees but must still join the room.",
|
||||
"phase": "02-prototype"
|
||||
"decision": "!load numeric selection intercepted in on_room_message before dispatcher.dispatch()",
|
||||
"rationale": "Numeric input arrives as IncomingMessage not IncomingCommand. Keeps dispatcher clean.",
|
||||
"phase": "04"
|
||||
}
|
||||
],
|
||||
"uncommitted_files": [
|
||||
".planning/STATE.md",
|
||||
".planning/HANDOFF.json",
|
||||
".planning/phases/01.1-matrix-restart-reconciliation-and-dev-reset-workflow/.continue-here.md",
|
||||
".planning/phases/02-prototype/.continue-here.md",
|
||||
"docs/superpowers/plans/2026-04-08-matrix-direct-agent-prototype.md"
|
||||
".planning/phases/04-matrix-mvp-shared-agent-context-and-context-management-comma/04-CONTEXT.md",
|
||||
".planning/phases/04-matrix-mvp-shared-agent-context-and-context-management-comma/04-RESEARCH.md",
|
||||
".planning/phases/04-matrix-mvp-shared-agent-context-and-context-management-comma/04-01-PLAN.md",
|
||||
".planning/phases/04-matrix-mvp-shared-agent-context-and-context-management-comma/04-02-PLAN.md",
|
||||
".planning/phases/04-matrix-mvp-shared-agent-context-and-context-management-comma/04-03-PLAN.md",
|
||||
"adapter/matrix/bot.py",
|
||||
"sdk/agent_session.py",
|
||||
"tests/adapter/matrix/test_dispatcher.py",
|
||||
"tests/platform/test_agent_session.py"
|
||||
],
|
||||
"next_action": "Resume by implementing graceful degradation for backend/provider failures so Matrix surface errors do not crash the process, then decide whether to upstream external/platform-agent commit 1dca2c1 and create a PR from feat/matrix-direct-agent-prototype.",
|
||||
"context_notes": "The direct-agent Matrix prototype is working end-to-end on branch feat/matrix-direct-agent-prototype and was manually validated against a live Matrix homeserver plus a locally running patched external/platform-agent. surfaces.git branch contains transport, local state, RealPlatformClient, runtime wiring, invite fix, and Russian runbook docs. Manual bring-up uncovered three real-world issues that were resolved: homeserver TLS trust on macOS/Python, repeat invites returning before join(), and provider/model auth mismatches. There is still one quality gap: backend errors currently bubble up and can kill the bot process. A local OpenRouter-backed external/platform-agent process was last seen listening on port 8000 (PID 13499) during pause."
|
||||
"next_action": "Pull platform-agent origin/main (git -C external/platform-agent pull), then execute Phase 4: /gsd-execute-phase 4. Wave 1: 04-01 alone. Wave 2: 04-02 + 04-03 in parallel.",
|
||||
"context_notes": "Phase 4 planning complete and verified (1 checker revision round). Plans are ready to execute. Key gotcha: lambda_agent_api pyproject.toml says requires-python>=3.14 but runs on 3.11 — Dockerfile needs uv pip install --ignore-requires-python. platform-agent local clone is 11 commits behind origin/main — must pull before execution. Wave structure: 04-01 (Wave 1, alone) → 04-02 + 04-03 (Wave 2, parallel). All old thread_id/AgentSessionClient logic gets replaced — sdk/agent_session.py becomes mostly dead code or deleted."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: — Production-ready surfaces
|
||||
status: Phase 01 Complete
|
||||
last_updated: "2026-04-03T09:35:39Z"
|
||||
status: Ready to execute
|
||||
last_updated: "2026-04-17T12:34:33.578Z"
|
||||
progress:
|
||||
total_phases: 3
|
||||
total_phases: 5
|
||||
completed_phases: 1
|
||||
total_plans: 6
|
||||
total_plans: 12
|
||||
completed_plans: 6
|
||||
percent: 50
|
||||
---
|
||||
|
||||
# State
|
||||
|
|
@ -52,6 +53,7 @@ Phase 1 is complete. Phase 2 remains blocked until the Lambda platform SDK is av
|
|||
### Roadmap Evolution
|
||||
|
||||
- Phase 01.1 inserted after Phase 01: Matrix restart reconciliation and dev reset workflow (URGENT)
|
||||
- Phase 4 added: Matrix MVP: shared agent context and context management command
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
context: phase
|
||||
phase: 04-matrix-mvp-shared-agent-context-and-context-management-comma
|
||||
task: null
|
||||
total_tasks: null
|
||||
status: ready_to_execute
|
||||
last_updated: 2026-04-17T12:34:43.144Z
|
||||
---
|
||||
|
||||
<current_state>
|
||||
Phase 4 planning is COMPLETE. 3 plans written, verified by checker, revised once.
|
||||
Ready to execute — no blockers.
|
||||
|
||||
Before executing: pull platform-agent origin/main:
|
||||
git -C external/platform-agent pull
|
||||
</current_state>
|
||||
|
||||
<completed_work>
|
||||
|
||||
- CONTEXT.md — all design decisions from 2026-04-16 session
|
||||
- RESEARCH.md — AgentApi lifecycle, platform-agent origin/main state, store patterns
|
||||
- 04-01-PLAN.md — Replace AgentSessionClient with AgentApiWrapper (Wave 1)
|
||||
- 04-02-PLAN.md — !save/!load/!reset/!context commands (Wave 2)
|
||||
- 04-03-PLAN.md — Dockerfile + docker-compose (Wave 2, parallel with 04-02)
|
||||
- Checker passed after 1 revision (3 blockers fixed: tag rename, missing return, external/ modification)
|
||||
</completed_work>
|
||||
|
||||
<remaining_work>
|
||||
|
||||
- Execute Wave 1: 04-01 (AgentApi migration)
|
||||
- Execute Wave 2: 04-02 + 04-03 in parallel
|
||||
</remaining_work>
|
||||
|
||||
<decisions_made>
|
||||
|
||||
- AgentApi wrapped in AgentApiWrapper (sdk/agent_api_wrapper.py) — subclasses AgentApi, overrides _listen() to capture MsgEventEnd.tokens_used. Do NOT modify external/platform-agent_api/
|
||||
- build_thread_key and thread_id in WS URL removed entirely — architecture is one container = one chat
|
||||
- !reset → POST {AGENT_BASE_URL}/reset; returns "unavailable" if 404 (endpoint not yet in platform-agent)
|
||||
- !save/!load are agent-mediated: bot sends text message to agent, agent uses write_file/read_file in /workspace/contexts/
|
||||
- !load numeric selection intercepted in on_room_message before dispatcher.dispatch()
|
||||
- lambda_agent_api install needs --ignore-requires-python (pyproject.toml says >=3.14, runs fine on 3.11)
|
||||
</decisions_made>
|
||||
|
||||
<blockers>
|
||||
None blocking execution.
|
||||
|
||||
Pending (non-blocking):
|
||||
- POST /reset endpoint needs to be requested from platform team
|
||||
- Credentials rotation (Matrix password, OpenRouter key sk-or-v1-d27c07...)
|
||||
</blockers>
|
||||
|
||||
## Required Reading (in order)
|
||||
1. `04-CONTEXT.md` — locked decisions
|
||||
2. `04-RESEARCH.md` — AgentApi interface details, platform-agent findings
|
||||
3. `external/platform-agent_api/lambda_agent_api/agent_api.py` — AgentApi source (read before implementing wrapper)
|
||||
|
||||
## Infrastructure State
|
||||
- platform-agent local clone: 11 commits BEHIND origin/main — pull before executing
|
||||
- surfaces-bot branch: feat/matrix-direct-agent-prototype
|
||||
- platform-agent branch: main (local has our old thread_id patch on top)
|
||||
|
||||
<context>
|
||||
Phase 4 is the main MVP delivery phase. The core insight: platform-agent uses thread_id="default" as a singleton by design — one container per chat, isolation at container level. We stop fighting this and embrace it. AgentSessionClient (our custom WS client) gets replaced by the platform team's AgentApi, wrapped to capture tokens_used. Four context management commands added: !save (agent writes summary to file), !load (agent reads file, user picks by number), !reset (POST /reset endpoint), !context (show session info).
|
||||
</context>
|
||||
|
||||
<next_action>
|
||||
1. git -C external/platform-agent pull (sync to origin/main)
|
||||
2. /clear
|
||||
3. /gsd-execute-phase 4
|
||||
</next_action>
|
||||
|
|
@ -5,6 +5,7 @@ type: execute
|
|||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- sdk/agent_api_wrapper.py
|
||||
- sdk/agent_session.py
|
||||
- sdk/real.py
|
||||
- adapter/matrix/bot.py
|
||||
|
|
@ -18,17 +19,20 @@ requirements:
|
|||
|
||||
must_haves:
|
||||
truths:
|
||||
- "RealPlatformClient uses AgentApi, not AgentSessionClient"
|
||||
- "AgentApi is connected before sync_forever and closed in finally block of main()"
|
||||
- "RealPlatformClient uses AgentApiWrapper (wraps AgentApi), not AgentSessionClient"
|
||||
- "AgentApiWrapper is connected before sync_forever and closed in finally block of main()"
|
||||
- "build_thread_key and AgentSessionClient are gone from sdk/"
|
||||
- "stream_message() yields MessageChunk objects including a final chunk with tokens_used from last_tokens_used"
|
||||
- "AGENT_WS_URL is used unchanged (no thread_id query param)"
|
||||
- "MATRIX_PLATFORM_BACKEND=real still works end-to-end without test crash"
|
||||
- "All existing tests pass after the swap"
|
||||
artifacts:
|
||||
- path: "sdk/agent_api_wrapper.py"
|
||||
provides: "AgentApiWrapper subclass of AgentApi with last_tokens_used tracking"
|
||||
contains: "AgentApiWrapper"
|
||||
- path: "sdk/real.py"
|
||||
provides: "RealPlatformClient wrapping AgentApi"
|
||||
contains: "AgentApi"
|
||||
provides: "RealPlatformClient wrapping AgentApiWrapper"
|
||||
contains: "AgentApiWrapper"
|
||||
- path: "adapter/matrix/bot.py"
|
||||
provides: "main() awaits agent_api.connect() and agent_api.close()"
|
||||
contains: "agent_api.connect"
|
||||
|
|
@ -46,18 +50,23 @@ must_haves:
|
|||
---
|
||||
|
||||
<objective>
|
||||
Replace the custom per-request AgentSessionClient with the persistent AgentApi from
|
||||
lambda_agent_api. Remove build_thread_key and AgentSessionClient entirely. Wire
|
||||
AgentApi connect/close into bot.py main(). Update all tests that referenced the
|
||||
old client.
|
||||
Replace the custom per-request AgentSessionClient with a thin AgentApiWrapper that
|
||||
subclasses AgentApi from lambda_agent_api and adds last_tokens_used tracking. Remove
|
||||
build_thread_key and AgentSessionClient entirely. Wire AgentApiWrapper connect/close
|
||||
into bot.py main(). Update all tests that referenced the old client.
|
||||
|
||||
Do NOT modify any file under external/. The external/ directory is managed by the
|
||||
platform team. All customisation goes in sdk/agent_api_wrapper.py.
|
||||
|
||||
Purpose: The existing AgentSessionClient creates a new WebSocket per message and
|
||||
injects thread_id into the URL — both incompatible with origin/main platform-agent.
|
||||
AgentApi maintains a single persistent WS connection managed via connect()/close()
|
||||
and exposes send_message() as an AsyncIterator.
|
||||
and exposes send_message() as an AsyncIterator. We capture tokens_used in a thin
|
||||
subclass so sdk/real.py can include it in the final MessageChunk without touching
|
||||
the upstream library.
|
||||
|
||||
Output: sdk/real.py, sdk/agent_session.py (deleted/emptied), adapter/matrix/bot.py
|
||||
updated, tests green.
|
||||
Output: sdk/agent_api_wrapper.py (new), sdk/real.py (rewritten), sdk/agent_session.py
|
||||
(stubbed), adapter/matrix/bot.py updated, tests green.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
|
@ -74,8 +83,9 @@ updated, tests green.
|
|||
|
||||
<interfaces>
|
||||
<!-- Key types the executor needs. Read from source before touching anything. -->
|
||||
<!-- IMPORTANT: external/ files are READ-ONLY — do not modify them. -->
|
||||
|
||||
From external/platform-agent_api/lambda_agent_api/agent_api.py:
|
||||
From external/platform-agent_api/lambda_agent_api/agent_api.py (READ ONLY):
|
||||
```python
|
||||
class AgentApi:
|
||||
def __init__(self, agent_id: str, url: str,
|
||||
|
|
@ -84,14 +94,16 @@ class AgentApi:
|
|||
async def close(self) -> None: ... # cancels _listen, closes WS+session
|
||||
async def send_message(self, text: str) -> AsyncIterator[AgentEventUnion]:
|
||||
# yields MsgEventTextChunk only; breaks on MsgEventEnd (does NOT yield it)
|
||||
# MsgEventEnd.tokens_used is consumed internally but NOT stored — executor
|
||||
# MUST add self.last_tokens_used: int = 0 to AgentApi and set it at the
|
||||
# break point, OR store it in a thin wrapper on RealPlatformClient.
|
||||
# MsgEventEnd.tokens_used is consumed internally at the break point
|
||||
...
|
||||
async def _listen(self) -> None:
|
||||
# internal task: receives WS frames, puts AgentEventUnion into self._queue
|
||||
# on MsgEventEnd: puts it in queue then breaks
|
||||
...
|
||||
# AgentEventUnion = Union[MsgEventTextChunk, MsgEventEnd] per server.py
|
||||
```
|
||||
|
||||
From external/platform-agent_api/lambda_agent_api/server.py:
|
||||
From external/platform-agent_api/lambda_agent_api/server.py (READ ONLY):
|
||||
```python
|
||||
class MsgEventTextChunk(BaseModel):
|
||||
type: Literal[EServerMessage.AGENT_EVENT_TEXT_CHUNK]
|
||||
|
|
@ -102,6 +114,22 @@ class MsgEventEnd(BaseModel):
|
|||
tokens_used: int
|
||||
```
|
||||
|
||||
New file to create — sdk/agent_api_wrapper.py:
|
||||
```python
|
||||
class AgentApiWrapper(AgentApi):
|
||||
"""Thin subclass of AgentApi that captures tokens_used from MsgEventEnd.
|
||||
|
||||
AgentApi.send_message() yields only MsgEventTextChunk and breaks silently
|
||||
on MsgEventEnd without storing tokens_used. This wrapper overrides _listen()
|
||||
to intercept MsgEventEnd and store tokens_used before it is discarded.
|
||||
"""
|
||||
last_tokens_used: int = 0
|
||||
|
||||
async def _listen(self) -> None:
|
||||
# Override: same as parent, but capture MsgEventEnd.tokens_used
|
||||
...
|
||||
```
|
||||
|
||||
From sdk/interface.py (unchanged):
|
||||
```python
|
||||
class MessageChunk(BaseModel):
|
||||
|
|
@ -119,39 +147,89 @@ class PlatformClient(Protocol):
|
|||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Replace AgentSessionClient with AgentApi in sdk/real.py, delete sdk/agent_session.py, patch tokens_used capture</name>
|
||||
<name>Task 1: Create sdk/agent_api_wrapper.py; rewrite sdk/real.py; stub sdk/agent_session.py</name>
|
||||
|
||||
<read_first>
|
||||
- sdk/real.py (full file — being replaced)
|
||||
- sdk/agent_session.py (full file — being deleted)
|
||||
- external/platform-agent_api/lambda_agent_api/agent_api.py (lines 134–216 — send_message generator + finally block)
|
||||
- sdk/agent_session.py (full file — being stubbed)
|
||||
- external/platform-agent_api/lambda_agent_api/agent_api.py (full file — READ ONLY, inspect _listen and send_message to understand override point)
|
||||
- external/platform-agent_api/lambda_agent_api/server.py (full file — READ ONLY, MsgEventEnd.tokens_used)
|
||||
- sdk/interface.py (MessageChunk, PlatformClient Protocol)
|
||||
</read_first>
|
||||
|
||||
<files>sdk/real.py, sdk/agent_session.py, external/platform-agent_api/lambda_agent_api/agent_api.py</files>
|
||||
<files>sdk/agent_api_wrapper.py, sdk/real.py, sdk/agent_session.py</files>
|
||||
|
||||
<behavior>
|
||||
- RealPlatformClient.__init__ accepts agent_api: AgentApi (not AgentSessionClient), prototype_state: PrototypeStateStore, platform: str = "matrix"
|
||||
- Create sdk/agent_api_wrapper.py with class AgentApiWrapper(AgentApi):
|
||||
- __init__: calls super().__init__(...) and adds self.last_tokens_used: int = 0
|
||||
- Override _listen(): copy the parent _listen() logic verbatim, then at the point where MsgEventEnd is received (before or as it is put into the queue), set self.last_tokens_used = chunk.tokens_used
|
||||
- Do NOT modify agent_api.py in external/ — subclass only
|
||||
- RealPlatformClient.__init__ accepts agent_api: AgentApiWrapper, prototype_state: PrototypeStateStore, platform: str = "matrix"
|
||||
- RealPlatformClient exposes agent_api as property self.agent_api so bot.py main() can call connect/close
|
||||
- stream_message() iterates agent_api.send_message(text) yielding MessageChunk per MsgEventTextChunk chunk; after loop yields final MessageChunk(finished=True, delta="", tokens_used=agent_api.last_tokens_used)
|
||||
- send_message() collects all chunks from stream_message() and returns MessageResponse
|
||||
- No thread_key, no build_thread_key references anywhere in sdk/real.py
|
||||
- AgentApi.last_tokens_used: int = 0 added as instance attribute in __init__; set inside send_message() generator at the "if isinstance(chunk, MsgEventEnd): break" line — change that line to "self.last_tokens_used = chunk.tokens_used; break"
|
||||
- sdk/agent_session.py: delete file contents and replace with single comment "# Deleted in Phase 4 — replaced by AgentApi from lambda_agent_api" (keep file to avoid import errors in test_real.py until tests are updated in Task 2)
|
||||
- sdk/agent_session.py: replace file contents with single comment stub (keep file to avoid import errors until tests are updated in Task 2)
|
||||
</behavior>
|
||||
|
||||
<action>
|
||||
1. Edit external/platform-agent_api/lambda_agent_api/agent_api.py:
|
||||
- In __init__: add `self.last_tokens_used: int = 0`
|
||||
- In send_message() at line ~172 (`if isinstance(chunk, MsgEventEnd): break`):
|
||||
replace with:
|
||||
```python
|
||||
if isinstance(chunk, MsgEventEnd):
|
||||
self.last_tokens_used = chunk.tokens_used
|
||||
break
|
||||
```
|
||||
1. Read external/platform-agent_api/lambda_agent_api/agent_api.py fully to find the exact _listen() implementation and the line where MsgEventEnd is handled.
|
||||
|
||||
2. Rewrite sdk/real.py entirely:
|
||||
2. Create sdk/agent_api_wrapper.py:
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure lambda_agent_api is importable (same sys.path trick as bot.py)
|
||||
_api_root = Path(__file__).resolve().parents[1] / "external" / "platform-agent_api"
|
||||
if str(_api_root) not in sys.path:
|
||||
sys.path.insert(0, str(_api_root))
|
||||
|
||||
from lambda_agent_api.agent_api import AgentApi
|
||||
from lambda_agent_api.server import MsgEventEnd
|
||||
|
||||
|
||||
class AgentApiWrapper(AgentApi):
|
||||
"""Thin subclass of AgentApi that captures tokens_used from MsgEventEnd.
|
||||
|
||||
AgentApi.send_message() yields MsgEventTextChunk events and breaks on
|
||||
MsgEventEnd without storing tokens_used. This wrapper overrides _listen()
|
||||
to intercept MsgEventEnd and set self.last_tokens_used before the event
|
||||
is discarded, so RealPlatformClient can include it in the final MessageChunk.
|
||||
|
||||
Do NOT modify external/platform-agent_api — subclass only.
|
||||
"""
|
||||
|
||||
def __init__(self, agent_id: str, url: str, **kwargs) -> None:
|
||||
super().__init__(agent_id=agent_id, url=url, **kwargs)
|
||||
self.last_tokens_used: int = 0
|
||||
|
||||
async def _listen(self) -> None:
|
||||
# Copy parent _listen() logic.
|
||||
# Read external/platform-agent_api/lambda_agent_api/agent_api.py _listen()
|
||||
# and reproduce it here, adding:
|
||||
# if isinstance(event, MsgEventEnd):
|
||||
# self.last_tokens_used = event.tokens_used
|
||||
# at the point where MsgEventEnd is processed.
|
||||
#
|
||||
# IMPORTANT: after reading agent_api.py, replace this entire method body
|
||||
# with the exact parent implementation + the tokens_used capture line.
|
||||
# Do not call super()._listen() — the parent creates a task; we need the
|
||||
# override to run in the same task context.
|
||||
raise NotImplementedError(
|
||||
"Executor: replace this body with the copied _listen() from AgentApi "
|
||||
"plus `self.last_tokens_used = event.tokens_used` at the MsgEventEnd branch."
|
||||
)
|
||||
```
|
||||
|
||||
IMPORTANT NOTE FOR EXECUTOR: The `_listen()` body above is a placeholder.
|
||||
After reading agent_api.py, copy the actual _listen() implementation from AgentApi
|
||||
into AgentApiWrapper._listen() and insert `self.last_tokens_used = event.tokens_used`
|
||||
at the MsgEventEnd branch. The final file must NOT contain the NotImplementedError.
|
||||
|
||||
3. Rewrite sdk/real.py entirely:
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -161,13 +239,13 @@ from sdk.interface import Attachment, MessageChunk, MessageResponse, PlatformCli
|
|||
from sdk.prototype_state import PrototypeStateStore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lambda_agent_api.agent_api import AgentApi
|
||||
from sdk.agent_api_wrapper import AgentApiWrapper
|
||||
|
||||
|
||||
class RealPlatformClient(PlatformClient):
|
||||
def __init__(
|
||||
self,
|
||||
agent_api: "AgentApi",
|
||||
agent_api: "AgentApiWrapper",
|
||||
prototype_state: PrototypeStateStore,
|
||||
platform: str = "matrix",
|
||||
) -> None:
|
||||
|
|
@ -176,7 +254,7 @@ class RealPlatformClient(PlatformClient):
|
|||
self._platform = platform
|
||||
|
||||
@property
|
||||
def agent_api(self) -> "AgentApi":
|
||||
def agent_api(self) -> "AgentApiWrapper":
|
||||
return self._agent_api
|
||||
|
||||
async def get_or_create_user(
|
||||
|
|
@ -241,9 +319,9 @@ class RealPlatformClient(PlatformClient):
|
|||
await self._prototype_state.update_settings(user_id, action)
|
||||
```
|
||||
|
||||
3. Replace sdk/agent_session.py content with:
|
||||
4. Replace sdk/agent_session.py content with:
|
||||
```python
|
||||
# Deleted in Phase 4 — replaced by AgentApi from lambda_agent_api
|
||||
# Deleted in Phase 4 — replaced by AgentApiWrapper from sdk/agent_api_wrapper.py
|
||||
# File kept as stub to avoid import errors during migration; remove after test_agent_session.py is updated.
|
||||
```
|
||||
</action>
|
||||
|
|
@ -253,16 +331,19 @@ class RealPlatformClient(PlatformClient):
|
|||
</verify>
|
||||
|
||||
<done>
|
||||
- sdk/real.py imports AgentApi (not AgentSessionClient), exposes self.agent_api property
|
||||
- sdk/agent_api_wrapper.py exists with AgentApiWrapper(AgentApi), __init__ sets self.last_tokens_used = 0, _listen() override captures MsgEventEnd.tokens_used
|
||||
- sdk/real.py imports AgentApiWrapper (not AgentSessionClient or AgentApi directly), exposes self.agent_api property
|
||||
- sdk/real.py stream_message yields final chunk with tokens_used from agent_api.last_tokens_used
|
||||
- agent_api.py __init__ has self.last_tokens_used = 0 and send_message sets it before break
|
||||
- external/ directory has NO modifications
|
||||
- sdk/agent_session.py contains only a comment stub (no class definitions)
|
||||
- `python -c "from sdk.real import RealPlatformClient"` exits 0
|
||||
- `grep "AgentApiWrapper" sdk/real.py` returns a match
|
||||
- `grep "last_tokens_used" sdk/agent_api_wrapper.py` returns a match
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Wire AgentApi lifecycle into bot.py main(); update all broken tests</name>
|
||||
<name>Task 2: Wire AgentApiWrapper lifecycle into bot.py main(); update all broken tests</name>
|
||||
|
||||
<read_first>
|
||||
- adapter/matrix/bot.py (full file — _build_platform_from_env and main() need changes)
|
||||
|
|
@ -274,21 +355,21 @@ class RealPlatformClient(PlatformClient):
|
|||
<files>adapter/matrix/bot.py, tests/platform/test_agent_session.py, tests/platform/test_real.py, tests/adapter/matrix/test_dispatcher.py</files>
|
||||
|
||||
<behavior>
|
||||
- _build_platform_from_env() returns a RealPlatformClient with an unconnected AgentApi (connect() NOT called here — called in main())
|
||||
- _build_platform_from_env() returns a RealPlatformClient with an unconnected AgentApiWrapper (connect() NOT called here — called in main())
|
||||
- main() calls await runtime.platform.agent_api.connect() after build_runtime() (only when backend is "real"; mock has no agent_api); wrap in `if hasattr(runtime.platform, "agent_api")` guard
|
||||
- main() finally block: await agent_api.close() before await client.close()
|
||||
- AGENT_WS_URL env var is passed unchanged to AgentApi(url=ws_url) — no query param manipulation
|
||||
- test_agent_session.py: completely rewritten — remove all build_thread_key tests, remove AgentSessionClient tests, remove process_message tests (those tested our platform-agent patch which is being discarded); replace with 2 tests: (1) import check for lambda_agent_api module, (2) stub test that documents the deletion
|
||||
- AGENT_WS_URL env var is passed unchanged to AgentApiWrapper(url=ws_url) — no query param manipulation
|
||||
- test_agent_session.py: completely rewritten — remove all build_thread_key tests, remove AgentSessionClient tests; replace with 2 tests: (1) import check for lambda_agent_api module, (2) stub test that documents the deletion
|
||||
- test_real.py: FakeAgentSessionClient replaced with FakeAgentApi that has send_message(text: str) -> AsyncIterator and last_tokens_used: int = 0; tests updated to construct RealPlatformClient(agent_api=FakeAgentApi(), prototype_state=PrototypeStateStore()); test_send_message no longer checks thread_key in message_id (now uses user_id); test_stream_message checks final chunk tokens_used comes from FakeAgentApi.last_tokens_used
|
||||
- test_dispatcher.py: test_build_runtime_uses_real_platform_when_matrix_backend_is_real must NOT call agent_api.connect() (build_runtime only constructs, does not connect); update test to mock AgentApi so it does not attempt a real WS connection; assert isinstance(runtime.platform, RealPlatformClient) still passes
|
||||
- test_dispatcher.py: test_build_runtime_uses_real_platform_when_matrix_backend_is_real must NOT call agent_api.connect() (build_runtime only constructs, does not connect); update test to mock AgentApiWrapper so it does not attempt a real WS connection; assert isinstance(runtime.platform, RealPlatformClient) still passes
|
||||
</behavior>
|
||||
|
||||
<action>
|
||||
1. Edit adapter/matrix/bot.py:
|
||||
|
||||
a. Remove imports: `from sdk.agent_session import AgentSessionClient, AgentSessionConfig`
|
||||
|
||||
b. Add import at top: `import sys; sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "external" / "platform-agent_api"))` — NO, instead add lambda_agent_api to sys.path only in bot.py startup, or better: install the package. In _build_platform_from_env(), do a lazy import:
|
||||
|
||||
b. In _build_platform_from_env(), use AgentApiWrapper with lazy import:
|
||||
```python
|
||||
def _build_platform_from_env() -> PlatformClient:
|
||||
backend = os.environ.get("MATRIX_PLATFORM_BACKEND", "mock").strip().lower()
|
||||
|
|
@ -297,9 +378,9 @@ class RealPlatformClient(PlatformClient):
|
|||
_api_root = Path(__file__).resolve().parents[2] / "external" / "platform-agent_api"
|
||||
if str(_api_root) not in sys.path:
|
||||
sys.path.insert(0, str(_api_root))
|
||||
from lambda_agent_api.agent_api import AgentApi
|
||||
from sdk.agent_api_wrapper import AgentApiWrapper
|
||||
ws_url = os.environ["AGENT_WS_URL"]
|
||||
agent_api = AgentApi(agent_id="matrix-bot", url=ws_url)
|
||||
agent_api = AgentApiWrapper(agent_id="matrix-bot", url=ws_url)
|
||||
return RealPlatformClient(
|
||||
agent_api=agent_api,
|
||||
prototype_state=PrototypeStateStore(),
|
||||
|
|
@ -326,7 +407,7 @@ class RealPlatformClient(PlatformClient):
|
|||
test_agent_session.py — stub after Phase 4 migration.
|
||||
|
||||
AgentSessionClient and build_thread_key were removed in Phase 4.
|
||||
The platform client is now AgentApi from lambda_agent_api.
|
||||
The platform client is now AgentApiWrapper wrapping AgentApi from lambda_agent_api.
|
||||
See tests/platform/test_real.py for RealPlatformClient tests.
|
||||
"""
|
||||
import sys
|
||||
|
|
@ -373,7 +454,7 @@ from lambda_agent_api.server import MsgEventTextChunk, EServerMessage # noqa: E
|
|||
|
||||
|
||||
class FakeAgentApi:
|
||||
"""Minimal fake for AgentApi — no real WebSocket."""
|
||||
"""Minimal fake for AgentApiWrapper — no real WebSocket."""
|
||||
def __init__(self) -> None:
|
||||
self.last_tokens_used: int = 0
|
||||
self.send_calls: list[str] = []
|
||||
|
|
@ -446,7 +527,7 @@ async def test_real_platform_client_settings_are_local():
|
|||
|
||||
4. Edit tests/adapter/matrix/test_dispatcher.py — update `test_build_runtime_uses_real_platform_when_matrix_backend_is_real`:
|
||||
- Add sys.path setup for lambda_agent_api (same pattern as above)
|
||||
- Mock AgentApi so it does not open a real WS:
|
||||
- Mock AgentApiWrapper so it does not open a real WS:
|
||||
```python
|
||||
async def test_build_runtime_uses_real_platform_when_matrix_backend_is_real(monkeypatch):
|
||||
import sys
|
||||
|
|
@ -458,16 +539,16 @@ async def test_real_platform_client_settings_are_local():
|
|||
monkeypatch.setenv("MATRIX_PLATFORM_BACKEND", "real")
|
||||
monkeypatch.setenv("AGENT_WS_URL", "ws://agent.example/agent_ws/")
|
||||
|
||||
# Patch AgentApi to avoid real WS connection during build_runtime
|
||||
import lambda_agent_api.agent_api as _mod
|
||||
class _FakeAgentApi:
|
||||
# Patch AgentApiWrapper to avoid real WS connection during build_runtime
|
||||
import sdk.agent_api_wrapper as _mod
|
||||
class _FakeAgentApiWrapper:
|
||||
def __init__(self, agent_id, url, **kw):
|
||||
self.last_tokens_used = 0
|
||||
async def connect(self): pass
|
||||
async def close(self): pass
|
||||
async def send_message(self, text):
|
||||
return; yield # empty async generator
|
||||
monkeypatch.setattr(_mod, "AgentApi", _FakeAgentApi)
|
||||
monkeypatch.setattr(_mod, "AgentApiWrapper", _FakeAgentApiWrapper)
|
||||
|
||||
from adapter.matrix.bot import build_runtime
|
||||
from sdk.real import RealPlatformClient
|
||||
|
|
@ -485,6 +566,7 @@ async def test_real_platform_client_settings_are_local():
|
|||
- main() in bot.py has agent_api.connect() call guarded by hasattr check
|
||||
- main() finally block closes agent_api before matrix client
|
||||
- grep confirms no "AgentSessionClient" or "build_thread_key" remain in sdk/real.py or adapter/matrix/bot.py
|
||||
- grep confirms no modifications to any file under external/
|
||||
</done>
|
||||
</task>
|
||||
|
||||
|
|
@ -502,7 +584,7 @@ async def test_real_platform_client_settings_are_local():
|
|||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-01-01 | Tampering | AgentApi.send_message() text | accept | Single-user prototype; text originates from authenticated Matrix user |
|
||||
| T-04-01-01 | Tampering | AgentApiWrapper.send_message() text | accept | Single-user prototype; text originates from authenticated Matrix user |
|
||||
| T-04-01-02 | Denial of Service | AgentBusyException from concurrent sends | mitigate | AgentApi._request_lock already prevents concurrent sends; bot must surface error to user instead of crashing |
|
||||
| T-04-01-03 | Information Disclosure | AGENT_WS_URL in env | accept | Internal service URL; not exposed to users |
|
||||
</threat_model>
|
||||
|
|
@ -519,11 +601,14 @@ Grep checks:
|
|||
# No old imports should remain
|
||||
grep -r "AgentSessionClient\|build_thread_key" sdk/ adapter/ tests/ --include="*.py" | grep -v "stub\|Deleted\|removed"
|
||||
|
||||
# AgentApi wired in bot.py
|
||||
# AgentApiWrapper wired in bot.py
|
||||
grep "agent_api.connect\|agent_api.close" adapter/matrix/bot.py
|
||||
|
||||
# last_tokens_used set in agent_api.py
|
||||
grep "last_tokens_used" external/platform-agent_api/lambda_agent_api/agent_api.py
|
||||
# last_tokens_used set in wrapper
|
||||
grep "last_tokens_used" sdk/agent_api_wrapper.py
|
||||
|
||||
# No external/ files modified
|
||||
git diff --name-only external/
|
||||
```
|
||||
</verification>
|
||||
|
||||
|
|
@ -532,7 +617,8 @@ grep "last_tokens_used" external/platform-agent_api/lambda_agent_api/agent_api.p
|
|||
- `grep -r "AgentSessionClient" sdk/ adapter/` returns empty (or only the stub comment)
|
||||
- `grep -r "build_thread_key" sdk/ adapter/` returns empty
|
||||
- `grep "agent_api.connect" adapter/matrix/bot.py` returns a match
|
||||
- `grep "last_tokens_used" external/platform-agent_api/lambda_agent_api/agent_api.py` returns the assignment line
|
||||
- `grep "last_tokens_used" sdk/agent_api_wrapper.py` returns the assignment line
|
||||
- `git diff --name-only external/` returns empty (external/ untouched)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ services:
|
|||
Read .env.example first to see what's there, then write the full updated file.
|
||||
</action>
|
||||
|
||||
<acceptance_criteria>
|
||||
<done>
|
||||
- `grep "python:3.11-slim" Dockerfile` returns a match
|
||||
- `grep "ignore-requires-python" Dockerfile` returns a match (lambda_agent_api install)
|
||||
- `grep "PYTHONPATH=/app" Dockerfile` returns a match
|
||||
|
|
@ -142,17 +142,14 @@ services:
|
|||
- `grep "env_file" docker-compose.yml` returns a match
|
||||
- `grep "AGENT_BASE_URL" .env.example` returns a match
|
||||
- `grep "MATRIX_PLATFORM_BACKEND" .env.example` returns a match
|
||||
</acceptance_criteria>
|
||||
|
||||
<verify>
|
||||
<automated>grep "python:3.11-slim" /Users/a/MAI/sem2/lambda/surfaces-bot/Dockerfile && grep "ignore-requires-python" /Users/a/MAI/sem2/lambda/surfaces-bot/Dockerfile && grep "AGENT_BASE_URL" /Users/a/MAI/sem2/lambda/surfaces-bot/.env.example && echo "All checks passed"</automated>
|
||||
</verify>
|
||||
|
||||
<done>
|
||||
- Dockerfile exists with python:3.11-slim, uv install, lambda_agent_api pip install --ignore-requires-python, PYTHONPATH=/app, CMD python -m adapter.matrix.bot
|
||||
- docker-compose.yml exists with matrix-bot service, env_file: .env, restart: unless-stopped
|
||||
- .env.example contains AGENT_WS_URL, AGENT_BASE_URL, MATRIX_PLATFORM_BACKEND=real
|
||||
</done>
|
||||
|
||||
<verify>
|
||||
<automated>grep "python:3.11-slim" /Users/a/MAI/sem2/lambda/surfaces-bot/Dockerfile && grep "ignore-requires-python" /Users/a/MAI/sem2/lambda/surfaces-bot/Dockerfile && grep "AGENT_BASE_URL" /Users/a/MAI/sem2/lambda/surfaces-bot/.env.example && echo "All checks passed"</automated>
|
||||
</verify>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
# Phase 4: Matrix MVP — Agent Context + Context Management — Context
|
||||
|
||||
**Gathered:** 2026-04-16
|
||||
**Status:** Ready for planning
|
||||
**Source:** Conversation context (2026-04-16 design session)
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Привести Matrix-бот к рабочему состоянию для MVP-деплоя в контейнер:
|
||||
- Убрать наш кастомный `AgentSessionClient` и thread_id патч из `platform-agent`, перейти на актуальный origin/main платформы с `AgentApi` из `lambda_agent_api`
|
||||
- Добавить 4 команды управления контекстом агента: `!save`, `!load`, `!reset`, `!context`
|
||||
- Упаковать Matrix-бот в Docker-контейнер
|
||||
|
||||
НЕ входит в фазу:
|
||||
- Изменения в platform-agent (это задача команды платформы)
|
||||
- Telegram адаптер
|
||||
- E2EE
|
||||
- Skills system (ждём платформу)
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Архитектура платформы (locked)
|
||||
|
||||
- **Один контейнер = один чат**: `AgentService` с `thread_id = "default"` — намеренная архитектура. Изоляция на уровне контейнеров, не thread_id. Не менять.
|
||||
- **Убрать thread_id патч**: наш коммит `1dca2c1` в `external/platform-agent` удаляем. Переходим на `origin/main` platform-agent.
|
||||
- **Удалить `build_thread_key`**: функция больше не нужна. Убрать из `sdk/agent_session.py` и `sdk/real.py`.
|
||||
- **Заменить `AgentSessionClient` на `AgentApi`**: использовать `AgentApi` из `external/platform-agent_api/lambda_agent_api/agent_api.py`. Он уже правильно обрабатывает все event-типы (неизвестные → `logger.warning`, без краша).
|
||||
|
||||
### !save (locked)
|
||||
|
||||
- Синтаксис: `!save` (автоимя по дате/времени) или `!save [имя]`
|
||||
- Механизм: Matrix-бот посылает агенту текстовое сообщение "Summarize our conversation and save to /workspace/contexts/[name].md. Reply only with: Saved: [name]"
|
||||
- Имена сохранений хранятся в `PrototypeStateStore` (список для `!load`)
|
||||
- Агент сам пишет файл через свои инструменты (`write_file`)
|
||||
|
||||
### !load (locked)
|
||||
|
||||
- `!load` без аргументов → бот показывает нумерованный список сохранений
|
||||
- Пользователь вводит **число** (1, 2, 3...) для выбора
|
||||
- Выход из состояния: `0` или `!cancel`
|
||||
- После выбора: бот посылает агенту "Load context from /workspace/contexts/[name].md and use it as background for our conversation. Reply: Loaded: [name]"
|
||||
- Состояние ожидания выбора хранится в Matrix store (аналогично pending_confirm)
|
||||
|
||||
### !reset (locked)
|
||||
|
||||
- Показывает confirmation-диалог:
|
||||
```
|
||||
Сбросить контекст агента? Выбери:
|
||||
!yes — сбросить
|
||||
!save [имя] — сохранить и сбросить
|
||||
!no — отмена
|
||||
```
|
||||
- `!yes` → вызвать `POST {AGENT_BASE_URL}/reset` (resets AgentService singleton)
|
||||
- `!save имя` → сначала выполняется логика !save, затем POST /reset
|
||||
- `!no` → отмена
|
||||
- Fallback если `/reset` endpoint недоступен (404): вернуть пользователю "Reset endpoint недоступен. Обратитесь к администратору."
|
||||
- `AGENT_BASE_URL` — новая env переменная (HTTP base URL агента, отдельно от `AGENT_WS_URL`)
|
||||
|
||||
### !context (locked)
|
||||
|
||||
- Показывает: имя текущей сессии (если загружали через `!load`), токены из последнего ответа (`tokens_used` из `MsgEventEnd`), список сохранений (имена + даты)
|
||||
- Не делает никаких вызовов к агенту
|
||||
|
||||
### Dockerfile + docker-compose (locked)
|
||||
|
||||
- `Dockerfile` для Matrix-бота (`adapter/matrix/bot.py`)
|
||||
- `docker-compose.yml` с сервисом `matrix-bot`
|
||||
- Env переменные через `.env` файл
|
||||
- Platform-agent запускается отдельно (не входит в compose этой фазы)
|
||||
|
||||
### Claude's Discretion
|
||||
|
||||
- Структура хранения saved sessions в PrototypeStateStore (dict name→timestamp)
|
||||
- Формат автоимени для !save без аргументов
|
||||
- HTTP клиент для POST /reset (aiohttp или httpx)
|
||||
- Точный формат промптов к агенту для save/load
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Platform клиент (заменяем)
|
||||
- `sdk/agent_session.py` — текущий AgentSessionClient, УДАЛЯЕМ/ЗАМЕНЯЕМ
|
||||
- `sdk/real.py` — RealPlatformClient, обновляем под AgentApi
|
||||
- `external/platform-agent_api/lambda_agent_api/agent_api.py` — новый клиент AgentApi
|
||||
- `external/platform-agent_api/lambda_agent_api/server.py` — типы сообщений (MsgStatus, MsgEventTextChunk, MsgEventEnd, etc.)
|
||||
- `external/platform-agent_api/lambda_agent_api/client.py` — MsgUserMessage
|
||||
|
||||
### Matrix адаптер (расширяем)
|
||||
- `adapter/matrix/bot.py` — точка входа, MatrixBot, build_runtime
|
||||
- `adapter/matrix/handlers/` — существующие обработчики команд
|
||||
- `adapter/matrix/store.py` — get_room_meta, set_pending_confirm (паттерн для !load state)
|
||||
- `sdk/prototype_state.py` — PrototypeStateStore, расширяем для saved sessions
|
||||
|
||||
### Состояние платформы
|
||||
- `.planning/threads/matrix-dev-prototype-agent-platform-state.md` — исследование от 2026-04-14
|
||||
|
||||
### Существующая архитектура команд
|
||||
- `core/protocol.py` — IncomingCommand, OutgoingMessage, OutgoingUI
|
||||
- `core/handlers/` — паттерны регистрации обработчиков
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- `AgentApi` требует явного `connect()` при старте и `close()` при завершении — lifecycle нужно встроить в `MatrixBot`
|
||||
- `AgentApi.send_message()` — AsyncIterator, возвращает `MsgEventTextChunk` чанки и `MsgEventEnd`
|
||||
- Для `!load` состояние "ожидаем число" хранить по ключу `load_pending:{matrix_user_id}:{room_id}` в store (аналог pending_confirm)
|
||||
- `AGENT_BASE_URL` — HTTP URL, например `http://127.0.0.1:8000`; `AGENT_WS_URL` = `ws://127.0.0.1:8000/agent_ws/`
|
||||
- platform-agent origin/main: `POST /reset` эндпоинта нет — это нужно запросить у команды платформы. До тех пор `!reset` возвращает "Reset endpoint недоступен"
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- Замена `PrototypeStateStore` на реальный control-plane из platform-master (Phase 3)
|
||||
- Skills интеграция через SkillsMiddleware (ждём платформу)
|
||||
- E2EE для Matrix
|
||||
- `!reset` через docker restart (заменяется на /reset endpoint когда платформа добавит)
|
||||
- Суммаризация контекста (агент сам решает как писать в файл)
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 04-matrix-mvp-shared-agent-context-and-context-management-comma*
|
||||
*Context gathered: 2026-04-16 via conversation design session*
|
||||
|
|
@ -0,0 +1,546 @@
|
|||
# Phase 4: Matrix MVP — Shared Agent Context + Context Management — Research
|
||||
|
||||
**Researched:** 2026-04-16
|
||||
**Domain:** Matrix bot, AgentApi WebSocket client, context management commands, Docker packaging
|
||||
**Confidence:** HIGH (all findings verified against actual source files in this repo)
|
||||
|
||||
---
|
||||
|
||||
<user_constraints>
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
|
||||
**Архитектура платформы:**
|
||||
- Один контейнер = один чат: `AgentService` с `thread_id = "default"` — намеренная архитектура. Не менять.
|
||||
- Убрать thread_id патч: наш коммит `1dca2c1` в `external/platform-agent` удаляем. Переходим на `origin/main` platform-agent.
|
||||
- Удалить `build_thread_key`: функция больше не нужна. Убрать из `sdk/agent_session.py` и `sdk/real.py`.
|
||||
- Заменить `AgentSessionClient` на `AgentApi`: использовать `AgentApi` из `external/platform-agent_api/lambda_agent_api/agent_api.py`.
|
||||
|
||||
**!save:** Синтаксис `!save` (автоимя) или `!save [имя]`. Механизм: посылаем агенту текстовое сообщение. Имена сохранений хранятся в `PrototypeStateStore`.
|
||||
|
||||
**!load:** `!load` без аргументов → нумерованный список. Пользователь вводит число. Выход: `0` или `!cancel`. После выбора — посылаем агенту текстовое сообщение. Состояние ожидания в Matrix store.
|
||||
|
||||
**!reset:** Confirmation-диалог с `!yes`/`!save имя`/`!no`. `!yes` → `POST {AGENT_BASE_URL}/reset`. Fallback если 404: сообщение пользователю.
|
||||
|
||||
**!context:** Показывает имя сессии, токены, список сохранений. Без вызовов агента.
|
||||
|
||||
**Dockerfile + docker-compose:** Для Matrix-бота. Env через `.env`. Platform-agent — отдельно.
|
||||
|
||||
### Claude's Discretion
|
||||
|
||||
- Структура хранения saved sessions в PrototypeStateStore (dict name→timestamp)
|
||||
- Формат автоимени для !save без аргументов
|
||||
- HTTP клиент для POST /reset (aiohttp или httpx)
|
||||
- Точный формат промптов к агенту для save/load
|
||||
|
||||
### Deferred Ideas (OUT OF SCOPE)
|
||||
|
||||
- Замена `PrototypeStateStore` на реальный control-plane из platform-master
|
||||
- Skills интеграция через SkillsMiddleware
|
||||
- E2EE для Matrix
|
||||
- `!reset` через docker restart
|
||||
- Суммаризация контекста
|
||||
</user_constraints>
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 4 replaces the custom `AgentSessionClient` with the production `AgentApi` from `lambda_agent_api`, adds four context management commands to the Matrix bot, and packages it in Docker. All findings are verified directly against source files.
|
||||
|
||||
**Primary recommendation:** Wire `AgentApi` as a persistent connection in `MatrixBot.__init__` (connect on start, close in finally block of `main()`). Expose it through `RealPlatformClient`. The four commands follow the existing handler registration pattern in `adapter/matrix/handlers/__init__.py`.
|
||||
|
||||
The `platform-agent` at `origin/main` already works with `AgentApi` — it does NOT require `thread_id` query param. Our local patch (`1dca2c1`) must be discarded and `external/platform-agent` reset to `origin/main`.
|
||||
|
||||
---
|
||||
|
||||
## Project Constraints (from CLAUDE.md)
|
||||
|
||||
- **Tech stack:** matrix-nio for Matrix — do not change without discussion
|
||||
- **Platform client:** connected only via `sdk/interface.py` Protocol — core/ and adapters untouched when swapping implementation
|
||||
- **No E2EE** — matrix-nio without python-olm
|
||||
- **Hotfixes < 20 lines** → Claude Code directly; implementation → Codex via GSD
|
||||
- **MATRIX_PLATFORM_BACKEND env var** controls mock vs real
|
||||
|
||||
---
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core (verified)
|
||||
| Library | Version | Purpose | Source |
|
||||
|---------|---------|---------|--------|
|
||||
| `lambda_agent_api` | local (external/platform-agent_api) | AgentApi WebSocket client | [VERIFIED: file read] |
|
||||
| `aiohttp` | >=3.9 (surfaces-bot), >=3.13.4 (agent_api) | WebSocket transport inside AgentApi | [VERIFIED: pyproject.toml] |
|
||||
| `pydantic` | >=2.5 | Message serialization (MsgUserMessage, MsgEventEnd, etc.) | [VERIFIED: server.py/client.py] |
|
||||
| `httpx` | >=0.27 | HTTP client for POST /reset (already in deps) | [VERIFIED: pyproject.toml] |
|
||||
| `structlog` | >=24.1 | Logging (existing pattern) | [VERIFIED: pyproject.toml] |
|
||||
|
||||
### Supporting
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| `aiohttp` | already a dep | Alternative HTTP for POST /reset | Could use instead of httpx — both available |
|
||||
|
||||
**Installation:** No new packages needed. `lambda_agent_api` is installed as a local path package (currently accessed via `sys.path` injection in tests; for production use, add to pyproject.toml as path dep or install via `pip install -e external/platform-agent_api`).
|
||||
|
||||
**Critical:** `lambda_agent_api` requires Python >=3.14 per its own `pyproject.toml`. The surfaces-bot requires Python >=3.11. [VERIFIED: pyproject.toml of both]. This is a **version mismatch** — see Pitfalls.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### AgentApi Constructor (verified)
|
||||
|
||||
```python
|
||||
# Source: external/platform-agent_api/lambda_agent_api/agent_api.py
|
||||
AgentApi(
|
||||
agent_id: str, # arbitrary string ID, used in logs
|
||||
url: str, # WebSocket URL, e.g. "ws://127.0.0.1:8000/agent_ws/"
|
||||
callback: Optional[Callable[[ServerMessage], None]] = None, # for orphaned msgs
|
||||
on_disconnect: Optional[Callable[['AgentApi'], None]] = None # called on WS close
|
||||
)
|
||||
```
|
||||
|
||||
### AgentApi Lifecycle (verified)
|
||||
|
||||
```python
|
||||
# Source: external/platform-agent_api/lambda_agent_api/agent_api.py
|
||||
agent = AgentApi(agent_id="matrix-bot", url=ws_url)
|
||||
await agent.connect() # opens WS, waits for MsgStatus, starts _listen() task
|
||||
# ... use agent ...
|
||||
await agent.close() # cancels _listen task, closes WS and session
|
||||
```
|
||||
|
||||
`connect()` blocks until `MsgStatus` is received from server (5s timeout). After `connect()`, a background `_listen()` asyncio task runs continuously, routing server messages to an internal `asyncio.Queue`.
|
||||
|
||||
### AgentApi.send_message() semantics (verified)
|
||||
|
||||
```python
|
||||
# Source: external/platform-agent_api/lambda_agent_api/agent_api.py, line 134
|
||||
async def send_message(self, text: str) -> AsyncIterator[AgentEventUnion]:
|
||||
```
|
||||
|
||||
- `AgentEventUnion = Union[MsgEventTextChunk, MsgEventEnd]` — **but** the generator `yield`s only `MsgEventTextChunk` chunks; it `break`s (stops) on `MsgEventEnd` without yielding it.
|
||||
- `MsgEventEnd` carries `tokens_used: int` — to capture this, the caller must intercept the queue or handle `MsgEventEnd` in the `_listen` loop. **Currently `send_message` discards `tokens_used`.** This affects `!context` which needs tokens.
|
||||
|
||||
**Resolution:** In `RealPlatformClient.stream_message()`, after iterating through `send_message()`, `tokens_used` won't be directly available. Options:
|
||||
1. Store `tokens_used` in a shared attribute after each response (add `self._last_tokens_used` to `AgentApi` or a wrapper).
|
||||
2. Use the `callback` parameter to capture `MsgEventEnd` events from the `_listen` loop.
|
||||
|
||||
[ASSUMED] The simplest approach: wrap `AgentApi` in a thin `AgentApiAdapter` class that intercepts `_listen` output and exposes `last_tokens_used`. Or: store tokens in `PrototypeStateStore` after each message.
|
||||
|
||||
### AgentApi concurrency constraint (verified)
|
||||
|
||||
`AgentApi._request_lock` prevents parallel `send_message()` calls — second call raises `AgentBusyException`. In the single-user Matrix prototype this is acceptable. The bot must not dispatch two messages concurrently to the same agent.
|
||||
|
||||
### Wiring AgentApi into MatrixBot (integration pattern)
|
||||
|
||||
The `AgentApi` must be a persistent connection (not per-message connect/disconnect) because:
|
||||
1. `_listen()` task runs in background and routes server push events.
|
||||
2. Per-message connect/disconnect would recreate the aiohttp session each time and discard LangGraph thread state.
|
||||
|
||||
**Recommended wiring:**
|
||||
|
||||
```python
|
||||
# adapter/matrix/bot.py — main() function
|
||||
agent_api = AgentApi(agent_id="matrix-bot", url=ws_url)
|
||||
await agent_api.connect()
|
||||
runtime = build_runtime(store=SQLiteStore(db_path), client=client, agent_api=agent_api)
|
||||
try:
|
||||
await client.sync_forever(timeout=30000, since=since_token)
|
||||
finally:
|
||||
await client.close()
|
||||
await agent_api.close()
|
||||
```
|
||||
|
||||
`_build_platform_from_env()` currently instantiates everything synchronously. It must be refactored to `async` or split: construct `AgentApi` synchronously, call `connect()` in `main()` before starting sync loop.
|
||||
|
||||
### RealPlatformClient updates
|
||||
|
||||
`RealPlatformClient` currently imports `AgentSessionClient` and calls `build_thread_key`. Both are removed. The updated class:
|
||||
|
||||
```python
|
||||
class RealPlatformClient(PlatformClient):
|
||||
def __init__(
|
||||
self,
|
||||
agent_api: AgentApi, # replaces agent_sessions: AgentSessionClient
|
||||
prototype_state: PrototypeStateStore,
|
||||
platform: str = "matrix",
|
||||
) -> None:
|
||||
```
|
||||
|
||||
`send_message()` and `stream_message()` call `agent_api.send_message(text)` directly — no `thread_key` needed.
|
||||
|
||||
### platform-agent origin/main: what changes (verified)
|
||||
|
||||
Our patch `1dca2c1` added `thread_id` query param handling to `external/platform-agent/src/api/external.py`. On `origin/main`, the `process_message()` function does NOT use `thread_id` — it calls `agent_service.astream(msg.text)` without `thread_id`. The WS URL becomes simply `ws://host:port/agent_ws/` — no query params.
|
||||
|
||||
### Existing command registration pattern (verified)
|
||||
|
||||
```python
|
||||
# adapter/matrix/handlers/__init__.py — register_matrix_handlers()
|
||||
dispatcher.register(IncomingCommand, "new", make_handle_new_chat(client, store))
|
||||
dispatcher.register(IncomingCommand, "settings", handle_settings)
|
||||
dispatcher.register(IncomingCallback, "confirm", make_handle_confirm(store))
|
||||
```
|
||||
|
||||
Handler signature (all existing handlers follow this):
|
||||
```python
|
||||
async def handle_X(
|
||||
event: IncomingCommand,
|
||||
auth_mgr,
|
||||
platform,
|
||||
chat_mgr,
|
||||
settings_mgr,
|
||||
) -> list[OutgoingEvent]:
|
||||
```
|
||||
|
||||
New context commands need access to `agent_api` (for `!save`, `!load`) and `store` (for `!context`, `!load` pending state). Pattern: use `make_handle_X(agent_api, store)` closures — same as `make_handle_new_chat(client, store)`.
|
||||
|
||||
### !load pending state pattern (verified)
|
||||
|
||||
Existing `PENDING_CONFIRM_PREFIX = "matrix_pending_confirm:"` in `adapter/matrix/store.py`.
|
||||
|
||||
New key for load pending state:
|
||||
```python
|
||||
LOAD_PENDING_PREFIX = "matrix_load_pending:"
|
||||
|
||||
def _load_pending_key(user_id: str, room_id: str) -> str:
|
||||
return f"{LOAD_PENDING_PREFIX}{user_id}:{room_id}"
|
||||
```
|
||||
|
||||
Stored data structure:
|
||||
```python
|
||||
{
|
||||
"saves": [{"name": "my-save", "ts": "2026-04-16T12:00:00Z"}, ...],
|
||||
"display": "1. my-save (2026-04-16)\n2. other..."
|
||||
}
|
||||
```
|
||||
|
||||
The numeric input `1`, `2`, etc. is intercepted in `MatrixBot.on_room_message()` BEFORE dispatching as `IncomingMessage` — check if `load_pending` exists for this user+room, resolve to save name, dispatch the load command internally.
|
||||
|
||||
**Alternative (recommended):** Handle numeric input in the `IncomingMessage` handler via a pre-dispatch interceptor, or register a special numeric-input check in the dispatcher for messages that are pure integers.
|
||||
|
||||
### !reset confirmation dialog pattern
|
||||
|
||||
!reset reuses the `OutgoingUI` + `pending_confirm` mechanism or a simpler custom state. Since the dialog options are `!yes`, `!save имя`, `!no` (not just yes/no), it cannot reuse `pending_confirm` directly without extension.
|
||||
|
||||
Simplest approach: store `reset_pending:{user_id}:{room_id}` key (boolean) and check for `!yes`/`!no`/`!save` commands from the `IncomingCommand` dispatcher when reset_pending is set.
|
||||
|
||||
### saved sessions storage in PrototypeStateStore
|
||||
|
||||
New dict attribute on `PrototypeStateStore`:
|
||||
```python
|
||||
self._saved_sessions: dict[str, list[dict]] = {}
|
||||
# Key: matrix_user_id
|
||||
# Value: [{"name": "my-save", "created_at": "2026-04-16T12:00:00Z"}, ...]
|
||||
```
|
||||
|
||||
Methods to add:
|
||||
```python
|
||||
async def add_saved_session(self, user_id: str, name: str) -> None: ...
|
||||
async def list_saved_sessions(self, user_id: str) -> list[dict]: ...
|
||||
```
|
||||
|
||||
### !context tokens_used tracking
|
||||
|
||||
`MsgEventEnd.tokens_used: int` is available from `server.py`. Since `AgentApi.send_message()` drops it, the planner must decide how to surface it. Recommended: store in `PrototypeStateStore` as `_last_tokens_used: dict[str, int]` keyed by user_id, updated after each successful agent response in `RealPlatformClient`.
|
||||
|
||||
### Prompts for !save / !load (Claude's Discretion)
|
||||
|
||||
```python
|
||||
# !save
|
||||
SAVE_PROMPT = (
|
||||
"Summarize our conversation and save to /workspace/contexts/{name}.md. "
|
||||
"Reply only with: Saved: {name}"
|
||||
)
|
||||
|
||||
# !load
|
||||
LOAD_PROMPT = (
|
||||
"Load context from /workspace/contexts/{name}.md and use it as background "
|
||||
"for our conversation. Reply: Loaded: {name}"
|
||||
)
|
||||
```
|
||||
|
||||
Auto-name format (Claude's Discretion): `context-{YYYYMMDD-HHMMSS}` (UTC, no spaces, no special chars, safe as filename).
|
||||
|
||||
### POST /reset endpoint
|
||||
|
||||
Confirmed absent in `origin/main` platform-agent. Only endpoint is `GET /agent_ws/` (WebSocket). The `main.py` has no HTTP routes beyond what FastAPI provides by default (`/docs`, `/openapi.json`).
|
||||
|
||||
`!reset` with `!yes` → `POST {AGENT_BASE_URL}/reset` → expect 404 → return "Reset endpoint недоступен. Обратитесь к администратору."
|
||||
|
||||
HTTP client for this: **httpx** (already in `pyproject.toml`):
|
||||
```python
|
||||
import httpx
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(f"{agent_base_url}/reset", timeout=5.0)
|
||||
if response.status_code == 404:
|
||||
return [OutgoingMessage(chat_id=..., text="Reset endpoint недоступен...")]
|
||||
```
|
||||
|
||||
### Dockerfile
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml .
|
||||
RUN pip install -e .
|
||||
COPY . .
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
CMD ["python", "-m", "adapter.matrix.bot"]
|
||||
```
|
||||
|
||||
`lambda_agent_api` must be installed in the container. Options:
|
||||
1. `COPY external/platform-agent_api /app/external/platform-agent_api` + `pip install -e /app/external/platform-agent_api`
|
||||
2. Include `lambda_agent_api` package directly in `surfaces-bot` package (copy source files)
|
||||
|
||||
Option 1 is cleaner.
|
||||
|
||||
### docker-compose.yml structure
|
||||
|
||||
```yaml
|
||||
services:
|
||||
matrix-bot:
|
||||
build: .
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Platform-agent runs separately — not in this compose file.
|
||||
|
||||
---
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| WebSocket lifecycle with reconnect | Custom WS manager | `AgentApi` from `lambda_agent_api` | Already handles connect/close/listen loop, error routing, queue management |
|
||||
| Message deserialization | Custom JSON parsing | `ServerMessage.validate_json()` (Pydantic TypeAdapter) | Discriminated union handles all message types |
|
||||
| HTTP async client | `aiohttp.ClientSession` directly | `httpx.AsyncClient` | Already in deps, cleaner API for one-shot POST |
|
||||
| Concurrent request guard | Custom lock | `AgentApi._request_lock` | Already implemented, raises `AgentBusyException` |
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: lambda_agent_api Python version mismatch
|
||||
|
||||
**What goes wrong:** `lambda_agent_api/pyproject.toml` declares `requires-python = ">=3.14"`. The surfaces-bot runs on Python 3.11+. If `pip install -e external/platform-agent_api` is run with Python 3.11 it may fail or emit warnings.
|
||||
|
||||
**Why it happens:** The `lambda_agent_api` was developed under Python 3.14 (seen in `.venv` path: `python3.14`). The code itself uses no 3.14-specific syntax — it is pure aiohttp + pydantic which run on 3.11.
|
||||
|
||||
**How to avoid:** Change `requires-python = ">=3.11"` in `external/platform-agent_api/pyproject.toml` before building the Docker image, or install with `--ignore-requires-python`. Alternatively, copy the three source files directly into the surfaces-bot package.
|
||||
|
||||
**Warning signs:** `pip install` failure with "requires Python >=3.14".
|
||||
|
||||
### Pitfall 2: AgentApi.send_message() drops MsgEventEnd (tokens_used lost)
|
||||
|
||||
**What goes wrong:** The generator yields only `MsgEventTextChunk` objects and breaks on `MsgEventEnd` without yielding it. Any downstream code that tries to get `tokens_used` from the iterator gets nothing.
|
||||
|
||||
**Why it happens:** The generator `break`s on `MsgEventEnd` (line 172 of agent_api.py) without yielding it. This is intentional for streaming UX but loses token info.
|
||||
|
||||
**How to avoid:** Before streaming, set `self._last_tokens_used = 0`. In `_listen()`, `MsgEventEnd` is put into `_current_queue` (line 241). The `send_message()` generator reads from that queue but does `break` — the `MsgEventEnd` object is consumed but not returned to caller. The only way to capture it is to subclass `AgentApi` or read from `_current_queue` directly before the break.
|
||||
|
||||
**Practical fix:** Add `self.last_tokens_used: int = 0` to `AgentApi` and intercept the queue in the `finally` block of `send_message()` — or store it in a wrapper class.
|
||||
|
||||
### Pitfall 3: AgentApi persistent connection vs sync_forever loop
|
||||
|
||||
**What goes wrong:** If `agent_api.connect()` is called inside `_build_platform_from_env()` (sync function), it creates an `asyncio.Task` for `_listen()` outside the event loop context.
|
||||
|
||||
**Why it happens:** `_build_platform_from_env()` is called synchronously from `build_runtime()`. `connect()` is a coroutine.
|
||||
|
||||
**How to avoid:** Do NOT call `agent_api.connect()` inside `_build_platform_from_env()`. Instead:
|
||||
1. `_build_platform_from_env()` creates `RealPlatformClient` with an unconnected `AgentApi`
|
||||
2. `main()` awaits `agent_api.connect()` explicitly after constructing runtime
|
||||
|
||||
Expose `agent_api` from `RealPlatformClient` via a property so `main()` can call `connect()` on it.
|
||||
|
||||
### Pitfall 4: !load numeric input interception
|
||||
|
||||
**What goes wrong:** When user types `1` in response to `!load` menu, it is dispatched as `IncomingMessage` (not a command) and routed to the platform — the agent receives "1" as a user message.
|
||||
|
||||
**Why it happens:** The Matrix converter (`from_room_event`) produces `IncomingMessage` for plain text, `IncomingCommand` only for `!`-prefixed text.
|
||||
|
||||
**How to avoid:** In `MatrixBot.on_room_message()`, before calling `dispatcher.dispatch()`, check if `load_pending` state exists for this user+room. If yes and the message text is a digit (or `0`/`!cancel`), handle it as a load selection instead of routing to agent.
|
||||
|
||||
### Pitfall 5: platform-agent thread_id removal breaks existing tests
|
||||
|
||||
**What goes wrong:** `tests/platform/test_agent_session.py` imports `build_thread_key` and tests `process_message` with `thread_id` in query params. After the patch is removed, these tests will fail.
|
||||
|
||||
**Why it happens:** Tests were written against our patched `external.py`.
|
||||
|
||||
**How to avoid:** The plan must include updating `test_agent_session.py` — remove `build_thread_key` tests, update `process_message` tests to reflect origin/main signature (no `thread_id` param).
|
||||
|
||||
### Pitfall 6: !reset dialog conflicts with existing !yes/!no flow
|
||||
|
||||
**What goes wrong:** The existing `pending_confirm` flow uses `!yes`/`!no`. If both `reset_pending` and `pending_confirm` are active simultaneously, `!yes` could trigger the wrong handler.
|
||||
|
||||
**Why it happens:** Both flows listen for the same commands.
|
||||
|
||||
**How to avoid:** `!reset` dialog uses a separate state key `reset_pending:{user_id}:{room_id}`. The handler for `!yes` must check `reset_pending` first, then `pending_confirm`. Document priority in handler code.
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Invoking AgentApi.send_message() in stream_message
|
||||
```python
|
||||
# Source: external/platform-agent_api/lambda_agent_api/agent_api.py
|
||||
async def stream_message(self, user_id: str, chat_id: str, text: str, ...) -> AsyncIterator[MessageChunk]:
|
||||
async for event in self._agent_api.send_message(text):
|
||||
if isinstance(event, MsgEventTextChunk):
|
||||
yield MessageChunk(
|
||||
message_id=user_id,
|
||||
delta=event.text,
|
||||
finished=False,
|
||||
)
|
||||
# After loop ends, MsgEventEnd was consumed internally
|
||||
yield MessageChunk(message_id=user_id, delta="", finished=True, tokens_used=self._agent_api.last_tokens_used)
|
||||
```
|
||||
|
||||
### Handler registration pattern
|
||||
```python
|
||||
# Source: adapter/matrix/handlers/__init__.py
|
||||
def register_matrix_handlers(dispatcher: EventDispatcher, client=None, store=None, agent_api=None) -> None:
|
||||
# existing...
|
||||
dispatcher.register(IncomingCommand, "save", make_handle_save(agent_api, store))
|
||||
dispatcher.register(IncomingCommand, "load", make_handle_load(agent_api, store))
|
||||
dispatcher.register(IncomingCommand, "reset", make_handle_reset(store))
|
||||
dispatcher.register(IncomingCommand, "context", make_handle_context(store))
|
||||
```
|
||||
|
||||
### !load pending key
|
||||
```python
|
||||
# New in adapter/matrix/store.py
|
||||
LOAD_PENDING_PREFIX = "matrix_load_pending:"
|
||||
|
||||
async def get_load_pending(store: StateStore, user_id: str, room_id: str) -> dict | None:
|
||||
return await store.get(f"{LOAD_PENDING_PREFIX}{user_id}:{room_id}")
|
||||
|
||||
async def set_load_pending(store: StateStore, user_id: str, room_id: str, data: dict) -> None:
|
||||
await store.set(f"{LOAD_PENDING_PREFIX}{user_id}:{room_id}", data)
|
||||
|
||||
async def clear_load_pending(store: StateStore, user_id: str, room_id: str) -> None:
|
||||
await store.delete(f"{LOAD_PENDING_PREFIX}{user_id}:{room_id}")
|
||||
```
|
||||
|
||||
### platform-agent origin/main process_message (no thread_id)
|
||||
```python
|
||||
# Source: git show origin/main:src/api/external.py in external/platform-agent
|
||||
async def process_message(ws: WebSocket, msg, agent_service: AgentService):
|
||||
match msg:
|
||||
case MsgUserMessage():
|
||||
async for chunk in agent_service.astream(msg.text): # no thread_id arg
|
||||
await ws.send_text(chunk.model_dump_json())
|
||||
await ws.send_text(MsgEventEnd(tokens_used=0).model_dump_json())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Assumptions Log
|
||||
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
|---|-------|---------|---------------|
|
||||
| A1 | `tokens_used` can be captured by storing in `AgentApi.last_tokens_used` attribute during `_listen()` before it's queued | Architecture Patterns | If `_listen` timing means value is read before queue, token count would be wrong — low risk, easy to test |
|
||||
| A2 | Python 3.11 can run `lambda_agent_api` despite `>=3.14` constraint in pyproject.toml | Standard Stack | If code uses 3.14-specific syntax, would fail at runtime — actual code inspected: no 3.14 syntax found |
|
||||
| A3 | httpx is preferred over aiohttp for POST /reset (one-shot HTTP) | Standard Stack | Either works; httpx already in deps |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **tokens_used capture from AgentApi**
|
||||
- What we know: `MsgEventEnd.tokens_used` is put into `_current_queue` but consumed (not yielded) by `send_message()` generator
|
||||
- What's unclear: Cleanest interception point without modifying `lambda_agent_api` source
|
||||
- Recommendation: Add `last_tokens_used: int = 0` attribute to `AgentApi` and set it in `send_message()`'s `finally` block when draining orphan queue, OR set it in `_listen()` before putting `MsgEventEnd` in queue
|
||||
|
||||
2. **!load numeric input dispatch**
|
||||
- What we know: Plain text `1`, `2` arrives as `IncomingMessage`, not `IncomingCommand`
|
||||
- What's unclear: Where to intercept — in `on_room_message()` (bot layer) or in dispatcher pre-hook
|
||||
- Recommendation: Intercept in `MatrixBot.on_room_message()` before `dispatcher.dispatch()`. Keeps dispatcher clean.
|
||||
|
||||
3. **lambda_agent_api install in Docker**
|
||||
- What we know: It's a local package in `external/platform-agent_api/`
|
||||
- What's unclear: Whether to install as editable or copy sources
|
||||
- Recommendation: `COPY external/platform-agent_api /build/lambda_agent_api && pip install /build/lambda_agent_api` in Dockerfile
|
||||
|
||||
---
|
||||
|
||||
## Environment Availability
|
||||
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
|------------|-------------|-----------|---------|----------|
|
||||
| Python 3.11+ | All | ✓ | System | — |
|
||||
| aiohttp | AgentApi WS | ✓ | >=3.9 in deps | — |
|
||||
| httpx | POST /reset | ✓ | >=0.27 in deps | aiohttp |
|
||||
| matrix-nio | Matrix bot | ✓ | >=0.21 in deps | — |
|
||||
| lambda_agent_api | AgentApi | local only | 0.1.0 | — |
|
||||
| Docker | Container build | [ASSUMED] standard dev env | — | — |
|
||||
| platform-agent (running) | Integration test | local clone | origin/main needed | — |
|
||||
|
||||
---
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Test Framework
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | pytest + pytest-asyncio (asyncio_mode = "auto") |
|
||||
| Config file | pyproject.toml `[tool.pytest.ini_options]` |
|
||||
| Quick run command | `pytest tests/platform/test_real.py tests/adapter/matrix/test_dispatcher.py -v` |
|
||||
| Full suite command | `pytest tests/ -v` |
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Req | Behavior | Test Type | File |
|
||||
|-----|----------|-----------|------|
|
||||
| Remove build_thread_key | Function gone from sdk/ | unit | `tests/platform/test_agent_session.py` — update/remove |
|
||||
| AgentApi replaces AgentSessionClient | `RealPlatformClient` uses `AgentApi` | unit | `tests/platform/test_real.py` — update |
|
||||
| !save sends prompt to agent | Command dispatches agent message | unit | `tests/adapter/matrix/test_dispatcher.py` — add |
|
||||
| !load shows list | Command returns numbered list | unit | `tests/adapter/matrix/test_dispatcher.py` — add |
|
||||
| !load numeric select | Bot intercepts digit, sends load prompt | unit | `tests/adapter/matrix/test_dispatcher.py` — add |
|
||||
| !reset shows dialog | Command returns confirmation UI | unit | `tests/adapter/matrix/test_dispatcher.py` — add |
|
||||
| !context returns snapshot | Command returns session info | unit | `tests/adapter/matrix/test_dispatcher.py` — add |
|
||||
| PrototypeStateStore saved sessions | add/list saved sessions | unit | `tests/platform/test_prototype_state.py` — add |
|
||||
|
||||
### Wave 0 Gaps
|
||||
- [ ] `tests/platform/test_agent_api_integration.py` — unit tests for `RealPlatformClient` with mocked `AgentApi`
|
||||
- [ ] `tests/adapter/matrix/test_context_commands.py` — dedicated module for !save/!load/!reset/!context handlers
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence — verified by file read in this session)
|
||||
- `external/platform-agent_api/lambda_agent_api/agent_api.py` — AgentApi constructor, connect/close/send_message, _listen loop
|
||||
- `external/platform-agent_api/lambda_agent_api/server.py` — MsgEventTextChunk, MsgEventEnd, MsgStatus, AgentEventUnion types
|
||||
- `external/platform-agent_api/lambda_agent_api/client.py` — MsgUserMessage type
|
||||
- `external/platform-agent/src/api/external.py` — current (patched) and origin/main versions verified via git show
|
||||
- `adapter/matrix/handlers/__init__.py` — handler registration pattern
|
||||
- `adapter/matrix/store.py` — pending_confirm key pattern
|
||||
- `adapter/matrix/bot.py` — MatrixBot, build_runtime, _build_platform_from_env
|
||||
- `sdk/agent_session.py` — current AgentSessionClient (to be replaced)
|
||||
- `sdk/real.py` — RealPlatformClient (to be updated)
|
||||
- `sdk/prototype_state.py` — PrototypeStateStore (to be extended)
|
||||
- `core/protocol.py` — IncomingCommand, OutgoingMessage types
|
||||
- `pyproject.toml` — dependency versions
|
||||
- `external/platform-agent_api/pyproject.toml` — Python version constraint
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- Docker best practices for Python apps [ASSUMED] — standard industry pattern
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- AgentApi interface: HIGH — read source directly
|
||||
- platform-agent origin/main diff: HIGH — verified via `git show origin/main`
|
||||
- handler registration pattern: HIGH — read all handler files
|
||||
- pending_confirm key pattern: HIGH — read store.py directly
|
||||
- tokens_used interception: MEDIUM — pattern clear but implementation needs care
|
||||
- Docker/docker-compose: MEDIUM — standard pattern, not verified against specific matrix-nio requirements
|
||||
|
||||
**Research date:** 2026-04-16
|
||||
**Valid until:** 2026-05-16 (lambda_agent_api is local — stable until platform team updates it)
|
||||
Loading…
Add table
Add a link
Reference in a new issue