[Story-754] SQS consumer + Strands harness

Story 754Epic None15 files · +1413/−98

PR #1 · pramaan-io/pramaan-agents · merged 2026-05-21

How it flows

sequenceDiagram
  autonumber
  participant sqs as SQS
  participant rt as Runtime
  participant store as Store
  participant ws as WebSocket
  sqs->>rt: SQS event
  rt->>rt: validate payload
  rt->>store: start run
  store-->>rt: run id
  rt->>ws: stream tokens
  rt->>store: append turn
  rt->>store: complete / fail

store.py · +366 — new methods

Methodin → outwhat it does
AgentRunStore(class)
start_run(request, model_id) → UUID
store_body(content, firm_id, run_id, kind, threshold_bytes) → StoredBody

What changed, file by file

Agent run storage layer: RLS-aware, S3 offload for large bodies, RDS IAM auth, plus an in-memory variant for tests.

new classsignature
AgentRunStore(class)
start_run(request, model_id) → UUID
store_body(content, firm_id, run_id, kind, threshold_bytes) → StoredBody
_fallback_s3_offload(content, firm_id, run_id, kind, sha256, size_bytes) → tuple[str, str, str, int]
InMemoryAgentRunStore(class)
load_history(chat_session_id, limit) → Sequence[ChatMessage]
append_assistant_turn(request, run_id, content) → None
complete_run(run_id) → None
fail_run(run_id, error) → None
SqlAlchemyAgentRunStore(class)
_set_rls(conn, request) → None
_next_turn_index(conn, chat_session_id) → int
_ids(request) → dict[str, str]
_build_database_url(url_factory) → Any
_rds_iam_token(host, port, username, region) → str
New fields: preview : strs3_key : str | Nonesize_bytes : intsha256 : str

WebSocket sender interface and a TokenStreamer that batches chunks under a rate cap.

new classsignature
ConnectionGoneError(class)
WebSocketSender(class)
post(connection_id, payload) → None
TokenStreamer(sender, connection_id, run_id, max_posts_per_second, _disconnected, _buffer)
emit(content) → None
flush() → None
done() → None

Strictly typed request shape with explicit validation for required fields and UUIDs.

new classsignature
PayloadValidationError(class)
AgentRunRequest(connection_id, firm_id, matter_id, user_id, chat_session_id, message, request_id)
from_mapping(payload) → AgentRunRequest
_non_empty_string(value, field) → str
_uuid(value, field) → UUID

Wraps the Strands agent behind one run method, with a stub path for tests.

new classsignature
ChatMessage(role, content, StrandsHarness)
run(request, history, token_streamer) → str
_run_stub(token_streamer) → str
_run_strands(token_streamer) → str

Composes the runtime container; supplies a no-op websocket sender for non-streamed runs.

new classsignature
NullWebSocketSender(class)
post(connection_id, payload) → None
build_agent_runtime() → AgentRuntime

SQS event handler rewritten to delegate to the new runtime and return a structured response.

new methodsignature
handle_event(event, runtime) → dict[str, Any]

Tool call lifecycle hooks — a recorder and a no-op placeholder; guardrails land in Story-756.

new classsignature
ToolCallStart(run_id, tool_name, input_json, ToolCallFinish, tool_call_id, output_json, duration_ms, error_message)
ToolCallRecorder(class)
before_tool_call(event, event) → None: ... class NoopToolCallRecorder: """Placeholder hook implementation until Story-756 adds real guardrails.""" def before_t
after_tool_call(event) → None

AgentRuntime container holding the store, harness, websocket sender, and tool call recorder.

new classsignature
AgentRuntime(store, harness, websocket_sender, tool_call_recorder)

Failure modes closed off

start run is the only path in
RLS locks the firm boundary
rate cap on every websocket post
tool guardrails — coming in Story 756
alerting and retries — future work

Files changed (15)

File±
implementation-notes.html+458
pyproject.toml+1 −1
requirements.in+1 −1
src/pramaan_agents/db/store.py ★+366
src/pramaan_agents/handler.py+40 −39
src/pramaan_agents/runtime/app.py+37
src/pramaan_agents/runtime/factory.py+41
src/pramaan_agents/runtime/payloads.py ★+76
src/pramaan_agents/runtime/strands_harness.py+68
src/pramaan_agents/runtime/tool_hooks.py+39
src/pramaan_agents/runtime/websocket.py ★+87
tests/runtime/test_payloads.py+40
tests/runtime/test_websocket.py+52
tests/test_handler.py+3 −57
tests/test_runtime_handler.py+104