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
88 changes: 87 additions & 1 deletion .opencode/context/adr/001-sarthi-v4-architecture-evolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The IterateSwarm Sarthi agent platform underwent a major architecture evolution

## Decision

We made seven architectural decisions to address these issues. Each is documented below.
We made ten architectural decisions to address these issues. Each is documented below.

### Decision 1: HTMX SSE over WebSocket / Raw JS EventSource

Expand Down Expand Up @@ -121,6 +121,84 @@ var specialistRoutes = map[string]specialistRoute{

**Rationale**: The removed types have real implementations in `apps/ai/src/` (Python) or `apps/core/internal/agents/` (Go). Keeping stubs created confusion — developers had to check whether a type was a stub or the real implementation. Removing them means "if it's in stubs.go, it's a real convenience type used here." This reduces cognitive load and prevents stale stubs from diverging from real implementations.

### Decision 8: SSEHub Event-Type Filtering (2026-06-28)

**What**: Introduced a dedicated SSEHub with event-type filtering to replace the raw `chatBroadcast chan fiber.Map` for chat-only SSE. Event filtering enables domain-specific SSE streams (chat, mission state, HITL, session events).

**How**:
```go
type SSEEvent struct {
Type string `json:"type"`
Payload string `json:"payload"`
}

type Subscription struct {
ID string
Channel chan []byte
Types []string // empty = all events
}
```
- `SSEHub.Subscribe(tenantID, "chat")` returns a subscription filtered to chat events only.
- `SSEHub.Broadcast(tenantID, SSEEvent{Type: "chat", ...})` only delivers to subscribers whose `Types` filter matches (or whose filter is empty = all events).
- Non-blocking fan-out with `select/default` on per-subscriber channels (buffered to 64).
- Backward-compatible: `tryBroadcast()` also pushes to `sseHub.Broadcast("default", SSEEvent{Type: "chat", ...})` alongside the legacy `chatBroadcast` channel.
- The `Subscription.Types` filter eliminates the need for separate SSE channels per event domain — one hub, typed routing.

**Rationale**: The original `chatBroadcast` channel was chat-only. As the SSE surface grows to include mission state updates, HITL approval signals, and session events, a single channel with event-type filtering prevents channel explosion (one channel per event type) while keeping the fan-out pattern simple. Per-subscriber channels (buffered 64) replace the single shared buffer (100), reducing head-of-line blocking.

### Decision 9: Tool Calling Surface — ToolRegistry + HITL Tier Mapping (2026-06-28)

**What**: Created a tool calling surface where agent actions are defined as standalone `ToolDef` entries, registered in a global `TOOL_REGISTRY`, and wired to the HITL manager for automatic tier assignment.

**How**:
```python
# apps/ai/src/agents/tools/__init__.py
@dataclass
class ToolDef:
name: str
description: str
hitl_tier: str # "auto" | "review" | "approve" | "blocked"
fn: Callable[..., Awaitable[dict]]
trigger_patterns: list[str] # e.g., ["FG-05", "BG-04"]
```
- 4 tool implementations in `apps/ai/src/agents/tools/`:
- `pause_failed_payment_retry` — tier: review, triggered by FG-05
- `draft_investor_update` — tier: approve, triggered by scheduled/manual
- `schedule_customer_checkin` — tier: auto, triggered by FG-03, BG-04
- `flag_churn_risk_customer` — tier: auto, triggered by BG-06, BG-04
- Tools auto-register on import via `register_tool(ToolDef(...))` at module bottom.
- `get_tools_for_tier(tier)` returns tools matching a HITL routing decision.
- `get_tools_for_patterns(pattern_ids)` returns tools matching triggered alert patterns.
- HITL tier strings are shared with `HITLManager.route()` output: `"auto" | "review" | "approve" | "blocked"`.

**Rationale**: Previously there was no explicit connection between alert patterns (FG-05, BG-04) and executable actions. Tools lived inside agent graphs with no centralized registry for discovery. The ToolDef pattern makes each tool self-documenting (name, description, tier, trigger patterns), and the global registry enables the HITL manager to suggest tools based on fired patterns. Each tool's `hitl_tier` is a first-class field, so routing decisions propagate from the HITL manager to tool execution without extra branching.

### Decision 10: Slack Consolidation — SocketMode Extension, Not Bolt (2026-06-28)

**What**: Extended the existing `SlackClient` with SocketMode listener support (`SocketModeClient` from `slack_sdk.socket_mode`) instead of adopting Slack's Bolt framework. Wired ACE (Acknowledge-Consequence-Evaluate) reflector loop into `slack_buttons.py` for feedback scoring.

**How**:
```python
# apps/ai/src/integrations/slack_client.py
from slack_sdk import WebClient
from slack_sdk.socket_mode import SocketModeClient

class SlackClient:
def __init__(self):
self.client = WebClient(token=os.getenv("SLACK_BOT_TOKEN"))
self.socket_client = SocketModeClient(
app_token=os.getenv("SLACK_APP_TOKEN"),
web_client=self.client
)
```
- SocketModeClient receives interactive payloads via Slack's Socket Mode (WebSocket), avoiding public HTTPS endpoints for Slack events.
- `slack_buttons.py` routes five button actions: `acknowledge`, `dispute`, `show_breakdown`, `log_decision`.
- Buttons wire to ACE reflector loop via `_send_feedback_signal()` → `score_from_button()` (Reflector) + `update_strategy_confidence()` (Curator).
- Button signals (`+1.0` for acknowledge, `-1.0` for dispute) update the trust battery and strategy confidence scores.
- Decision logging modal (`open_decision_modal`) captures structured decision data (decision, alternatives, reasoning) and persists via `log_decision` activity.

**Rationale**: Adding SocketMode to the existing `SlackClient` avoids the Bolt framework dependency and its opinionated middleware stack. The Socket Mode WebSocket connection eliminates the need for a public Slack Events API endpoint, simplifying deployment (no ngrok, no public URL for dev). The ACE loop wiring in `slack_buttons.py` closes the feedback loop: user button clicks → Reflector scores → strategy confidence updates — all without leaving the existing `slack_client.py` class.

---

## Consequences
Expand All @@ -136,6 +214,9 @@ var specialistRoutes = map[string]specialistRoute{
| Server-rendered bubbles | Single source of truth for HTML. XSS-safe via html.EscapeString. No client templates. |
| MissionState POST | Clear write path. Dashboard is a dumb renderer. No client state hydration. |
| Remove stubs | 40 fewer lines of stale dead code. Clear signal: "if it's here, it's real." |
| SSEHub event filtering | Event-type routing eliminates channel explosion. Per-subscriber channels reduce head-of-line blocking. |
| ToolRegistry + HITL mapping | Self-documenting tools with explicit tier. Central registry enables pattern-driven tool suggestion. |
| Slack SocketMode | No Bolt dependency. No public Slack Events URL needed. ACE loop closes feedback via reflector. |

### Tradeoffs

Expand All @@ -148,13 +229,18 @@ var specialistRoutes = map[string]specialistRoute{
| Server-rendered bubbles | Harder to add interactive elements (copy buttons, actions) — would need JS event delegation. |
| MissionState POST | Tight coupling between Python worker and Go schema. Schema changes require coordinated deploys. |
| Remove stubs | `DiscordApprovalInput` still exists as a convenience type — could become stale if Discord integration changes. |
| SSEHub event filtering | Per-subscriber channels (64 buffer) per client increase memory under high concurrent connections. |
| ToolRegistry | Tools are auto-registered on import — import order dependencies could cause registration race at startup. |
| Slack SocketMode | Requires `SLACK_APP_TOKEN` (Socket Mode token) in addition to `SLACK_BOT_TOKEN`. WebSocket dependency for event ingestion. |

### Risks

1. **SSE channel saturation** — `tryBroadcast` drops messages when the channel is full (buffered to 100). Under high concurrency, users may miss status updates. Mitigation: monitor `chatBroadcast` channel fill rate; increase buffer or add per-client channels if needed.
2. **Goroutine lifetime** — Long-lived goroutines (5-min timeout) hold references to request context. If the client disconnects, the goroutine continues until timeout or completion. Mitigation: check `ctx.Done()` in the goroutine loop pattern for future streaming responses.
3. **Specialist routing fragility** — Workflow type strings bypass compile-time checking. Mitigation: add a test that iterates all routes and verifies workflow names match registered Temporal workflow types.
4. **Signal ID mismatch** — The workflow ID used in the approval action must match the workflow ID that created the `planned_actions` row. If they diverge, signals are lost. Mitigation: store `workflow_id` in the `planned_actions` table and use it for signaling.
5. **Tool tier mismatch** — A tool's `hitl_tier` could diverge from the HITL manager's routing decision for a given pattern. Mitigation: `get_tools_for_tier()` provides a single source of truth — tools should be registered with tiers that match the HITL manager output.
6. **Slack SocketMode reconnection** — If the Socket Mode WebSocket drops, Slack button interactions are lost until reconnection. Mitigation: SocketModeClient auto-reconnects; monitor reconnection events via the `on_socket_connected` callback.

---

Expand Down
72 changes: 66 additions & 6 deletions .opencode/context/domain/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,20 @@
└─────────────────────────────────────────────────────────────────┘
```

> **Architecture decisions documented in:** [ADR-001: Sarthi v4.0 Architecture Evolution](../adr/001-sarthi-v4-architecture-evolution.md)
> **Architecture decisions documented in:** [ADR-001: Sarthi v4.0 Architecture Evolution](../adr/001-sarthi-v4-architecture-evolution.md) (10 decisions, incl. SSEHub, ToolRegistry, Slack SocketMode)

## Key Design Decisions

### Go Modular Monolith (apps/core/)

- **Framework**: Fiber v2 (`github.com/gofiber/fiber/v2`)
- **HTMX**: Server-rendered HTML partials with `hx-trigger`, `hx-swap`, `hx-target`
- **SSE**: Fiber v2 `SetBodyStreamWriter` + `*bufio.Writer` for streaming (see [sse.go](/apps/core/internal/web/sse.go))
- **SSE**: Fiber v2 `SetBodyStreamWriter` + `*bufio.Writer` for streaming
- **SSEHub** (`sse_hub.go`): Event-type filtered fan-out hub with per-subscriber channels
- `Subscribe(tenantID, eventTypes...)` creates typed subscriptions
- `Broadcast(tenantID, SSEEvent)` delivers only to matching subscribers (non-blocking)
- Backward-compatible: `tryBroadcast()` pushes to both legacy `chatBroadcast` channel and SSEHub
- Legacy polling-based SSE in [sse.go](/apps/core/internal/web/sse.go) (deprecated for new endpoints)
- **Templates**: Embedded via `//go:embed` in `internal/web/templates/`
- **Database**: Direct `database/sql` + `lib/pq` (not sqlc for command center — raw SQL queries)
- **Temporal SDK**: `go.temporal.io/sdk` v1.39.0 for workflow dispatch and signaling
Expand Down Expand Up @@ -120,10 +125,54 @@

### Mission State System

- **Python AI layer** writes compiled operational state to `mission_state` table
- **Go handlers** read `mission_state` for dashboard KPIs, signals, health score
- Fields: mrr, burn_rate, runway_days, burn_alert, burn_severity, trust_score, churn_rate, error_spike, active_alerts, founder_focus
- **Python AI layer** writes compiled operational state to `mission_states` table (migration 004 reconciled schema drift `mission_state` → `mission_states`)
- **Go handlers** read `mission_states` for dashboard KPIs, signals, health score
- Core fields: mrr, burn_rate, runway_days, burn_alert, burn_severity, trust_score, churn_rate, error_spike, active_alerts, founder_focus
- **New cognitive offloading fields (2026-06-28):**
- `prepared_brief` — LLM-generated brief prepopulated for founder context before a decision
- `pending_decisions` — JSONB array of open decisions awaiting founder action
- `last_updated_by` — Which agent/specialist last wrote to MissionState (traceability)
- Context budget: 800 tokens max for mission state compilation
- Dataclass in: `apps/ai/src/session/mission_state.py`
- Table references: `mission_states` (plural, not `mission_state`)

### Tool Calling Surface (ToolRegistry + HITL Tier Mapping)

New in 2026-06-28. Agent actions are defined as standalone `ToolDef` entries in a global `TOOL_REGISTRY` and wired to the HITL manager for automatic tier assignment.

```
ToolDef {
name: str # snake_case unique tool name
description: str # human-readable
hitl_tier: str # "auto" | "review" | "approve" | "blocked"
fn: Callable # async execute function
trigger_patterns: list # alert patterns that suggest this tool
}
```

**4 registered tools:**

| Tool | Tier | Pattern | Description |
|------|------|---------|-------------|
| `pause_failed_payment_retry` | review | FG-05 | Pause Stripe retry on 3+ failed payments |
| `draft_investor_update` | approve | schedule/manual | Draft investor email for approval |
| `schedule_customer_checkin` | auto | FG-03, BG-04 | Auto-schedule at-risk customer reminder |
| `flag_churn_risk_customer` | auto | BG-06, BG-04 | Flag segment for churn monitoring |

- Tools auto-register via `register_tool(ToolDef(...))` on module import
- `get_tools_for_tier(tier)` returns tools matching a routing decision
- `get_tools_for_patterns(pattern_ids)` returns tools matching triggered alerts
- Location: `apps/ai/src/agents/tools/__init__.py` + 4 tool modules

### Slack Consolidation — SocketMode + ACE Loop

New in 2026-06-28. The `SlackClient` was extended with `SocketModeClient` (WebSocket-based event ingestion) to handle interactive button payloads without a public HTTP endpoint.

- **SocketMode**: WebSocket connection via `slack_sdk.socket_mode.SocketModeClient` — no Bolt framework
- **Button routing** (`slack_buttons.py`): `acknowledge`, `dispute`, `show_breakdown`, `log_decision`
- **ACE loop**: Button clicks → `_send_feedback_signal()` → `score_from_button()` (Reflector) + `update_strategy_confidence()` (Curator)
- **Decision modal**: `open_decision_modal()` captures structured decision data (decision, alternatives, reasoning)
- Location: `apps/ai/src/integrations/slack_client.py`, `apps/ai/src/integrations/slack_buttons.py`

### Goroutine Safety

Expand All @@ -143,10 +192,21 @@
| `/apps/core/internal/web/templates/command_center.html` | Main dashboard template (HTMX + SSE + Chart.js) |
| `/apps/core/internal/web/templates/partials/command_chat.html` | Chat panel with SSE extension |
| `/apps/core/internal/web/templates/partials/command_approvals.html` | Approval queue UI (approve/hold) |
| `/apps/core/internal/web/command_center_test.go` | Test suite (52 tests covering chat, approvals, mission state, SSE) |
| `/apps/core/internal/web/command_center_test.go` | Test suite (19+ command center tests, part of 74+ web tests) |
| `/apps/core/internal/web/sse_hub.go` | SSEHub fan-out hub with event-type filtering (Subscribe/Broadcast) |
| `/apps/ai/src/workflows/finance_workflow.py` | Finance specialist Temporal workflow |
| `/apps/ai/src/workflows/data_workflow.py` | Data specialist Temporal workflow |
| `/apps/ai/src/workflows/ops_workflow.py` | Ops specialist Temporal workflow |
| `/apps/ai/src/agents/tools/__init__.py` | ToolRegistry with ToolDef dataclass, auto-registration, tier queries |
| `/apps/ai/src/agents/tools/pause_payment_retry.py` | Tool: Pause Stripe retry (tier: review) |
| `/apps/ai/src/agents/tools/draft_investor_update.py` | Tool: Draft investor email (tier: approve) |
| `/apps/ai/src/agents/tools/schedule_customer_checkin.py` | Tool: Schedule at-risk checkin (tier: auto) |
| `/apps/ai/src/agents/tools/flag_churn_risk.py` | Tool: Flag churn risk segment (tier: auto) |
| `/apps/ai/src/integrations/slack_client.py` | Slack WebClient + SocketModeClient for interactive messages |
| `/apps/ai/src/integrations/slack_buttons.py` | ACE button routing (acknowledge, dispute, breakdown, log_decision) |
| `/apps/ai/src/hitl/manager.py` | HITLManager with route_extended for guardrail-aware tier routing |
| `/apps/ai/src/hitl/confidence.py` | Confidence scoring (pattern_seen_before, data_quality, volatility) |
| `/apps/ai/src/hitl/approval_queue.py` | Approval request/response with Slack notification |

## Architecture Evolution (2026-06-28)

Expand Down
26 changes: 18 additions & 8 deletions .opencode/context/domain/chat-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,14 @@ func (h *Handler) APICommandChatEvents(c *fiber.Ctx) error {

Key patterns:
- **Fiber v2 `SetBodyStreamWriter`**: Proper SSE streaming support
- **`tryBroadcast()`**: Non-blocking send with `select/default` to prevent goroutine leaks
- **Two SSE endpoints**: `APICommandChatEvents` (chat-specific) and `APICommandEvents` (dashboard heartbeats)
- **`tryBroadcast()`**: Non-blocking send with `select/default` to prevent goroutine leaks. Also pushes to `sseHub.Broadcast("default", SSEEvent{Type: "chat", ...})` for fan-out.
- **SSEHub event-type filtering**: The SSEHub (sse_hub.go) manages per-subscriber channels with optional `Types` filter. Subscribing with `sseHub.Subscribe(tenantID, "chat")` only receives events with `Type == "chat"`.
- **Three SSE event domains** (2026-06-28):
- `event: chat` — Chat bubble HTML fragments (for `sse-swap="chat"`)
- `event: mission` — Mission state updates (prepared_brief, pending_decisions changes)
- `event: hitl` — HITL approval signals (new pending, approved, rejected)
- `event: session` — Session events (connection state, context updates)
- **SSE endpoints**: `APICommandChatEvents` (chat-specific via SSEHub), `APICommandEvents` (dashboard heartbeats), plus new typed subscribe endpoints for mission/hitl/session event types.
- **`recover()`**: Deferred in stream writer to prevent panic from closed connections

### 5. HTMX SSE Swap
Expand Down Expand Up @@ -173,12 +179,16 @@ Agent color classes: `agent-sarthi` (blue), `agent-finance` (green), `agent-data

| Scenario | Behavior | File Reference |
|----------|----------|----------------|
| No DB | Returns empty string | handler.go:1222-1224 |
| No Temporal | Skips workflow dispatch | handler.go:1272 |
| Workflow error | Broadcasts error bubble | handler.go:1309 |
| Channel full | Drops message, logs warning | handler.go:1415-1419 |
| Empty message | Returns empty string | handler.go:1201-1203 |
| Missing mention | No workflow dispatch, just saves message | handler.go:1259-1270 |
| No DB | Returns empty string | handler.go |
| No Temporal | Skips workflow dispatch | handler.go |
| Workflow error | Broadcasts error bubble | handler.go |
| Channel full | Drops message, logs warning | handler.go |
| Empty message | Returns empty string | handler.go |
| Missing mention | No workflow dispatch, just saves message | handler.go |
| SSEHub Subscribe with type filter | Only receives matching event types | sse_hub.go:39-53 |
| SSEHub Broadcast no matching subs | Event silently dropped (no subscriber with matching type filter) | sse_hub.go:75-93 |
| SSEHub subscriber channel full | Event dropped per sub (non-blocking per-subscriber channel) | sse_hub.go:89-92 |
| SSEHub Unsubscribe | Channel closed and removed from subscriber map | sse_hub.go:56-64 |

### 8. JavaScript Integration

Expand Down
Loading
Loading