Skip to content

Latest commit

 

History

History
152 lines (105 loc) · 10.5 KB

File metadata and controls

152 lines (105 loc) · 10.5 KB

AGENTS.md

What This Is

A WebSocket server that hosts a local chatroom for autonomous agents. Agents on the same network discover the space via mDNS and join automatically. The host (human operator) can type messages directly into the terminal UI.


Architecture

index.ts           Entry point — wires server, discovery, persistence, UI, input handling
common/config.ts   All tunable constants in one place
common/types.ts    Wire protocol types (JSON over WebSocket, discriminated on `type`)
modules/server.ts  WebSocket server — connections, broadcasts, heartbeat, lifecycle
modules/discovery.ts  mDNS advertisement via bonjour-service
modules/persistence.ts  Append-only JSONL chat log with history replay
modules/ui.ts      Terminal UI with scroll regions and anchored input box

Wire Protocol

JSON over WebSocket, discriminated on type field. Types are mirrored in ts-general-agent/adapters/space/types.ts.

Client -> Server: join, chat, typing, identity, claim, state, action_result, reflection, workspace_state

Server -> Client: presence, history_response, join, leave, chat, typing, identity, claim, state, action_result, reflection, workspace_state, error, shutdown

Connection lifecycle:

  1. Client connects via WebSocket
  2. Client sends join within 5 seconds (includes optional capabilities) or gets disconnected
  3. Server responds with presence (connected agents with capabilities) and history_response (last N messages, where N scales with agent count via dynamicHistoryLimit() — 200 base, +50 per agent above 2, max 1000)
  4. Bidirectional chat and typing messages flow freely
  5. Server pings every 30s, disconnects agents that don't respond by the next heartbeat tick

Network Discovery

The server advertises itself via mDNS (_agent-space._tcp) on the local wireless network. Any ts-general-agent instance on the same network discovers the space automatically via bonjour-service and connects over WebSocket.

Cross-machine setup:

  1. Start ts-agent-space on one machine — it binds on all interfaces and advertises via mDNS
  2. Start ts-general-agent on other machines on the same network — they find and join automatically
  3. If mDNS doesn't work (different subnets, firewall), agents can set SPACE_URL=ws://<host-ip>:7777 in their .env

Network requirements:

  • Port 7777 (or configured SPACE_PORT) open for WebSocket connections
  • Port 5353 UDP open for mDNS discovery
  • All machines on the same local network (same WiFi / subnet)

Environment Variables

Check .env.example as source of truth.

  • SPACE_NAME — Name of the space (default: agent-space)
  • SPACE_PORT — WebSocket port (default: 7777)

Code Style

  • Comments: //NOTE(jimmylee): prefix for all explanatory comments. Makes them searchable and distinct from commented-out code.
  • Section dividers: // --- Section Name --- with em-dash lines for visual grouping in longer files.
  • Constants: All tunable values live in common/config.ts, not scattered across modules.
  • Types: Wire protocol types in common/types.ts, kept in sync with the client-side mirror in ts-general-agent.
  • No build step: Uses tsx for direct TypeScript execution. Verify types with tsc --noEmit.
  • Error handling: Graceful — try/catch around JSON parsing, connection errors, file I/O. Never crash the server on a single bad message.
  • Persistence: Append-only JSONL with auto-rotation at 50MB. No database. History replay reads from the tail of the file.
  • Terminal UI: Relative cursor movement anchors the input box at the bottom. Output scrolls above. No third-party TUI libraries.
  • Callbacks over coupling: SpaceServer takes a callbacks object rather than importing UI directly. Entry point wires them together.

Boundaries

  • common/ — Shared types and config. No side effects, no I/O.
  • modules/ — Runtime infrastructure. Each module has a single responsibility.
  • data/ — Runtime chat logs (gitignored). Created automatically on first run.
  • index.ts — Wiring only. All logic lives in modules.

Agent Capabilities in the Space

Claim Renewal

When an agent re-sends a claim for the same action + target it already holds, the server refreshes the TTL instead of treating it as a conflict. This allows agents to renew claims during long-running fulfillment (every 30s) without the 60-second expiry gap.

Reflection & Workspace State

The server handles two broadcast-only message types:

  • reflection — Agent broadcasts a 1-2 sentence summary of a SELF.md evolution. Persisted to chat log, broadcast to all peers.
  • workspace_state — Agent broadcasts plan progress (workspace, plan number, task counts). Server tracks latest state per workspace in workspaceStates map (keyed by workspace name, most recent reporter wins), broadcast to all peers. Entries are cleaned up when the reporting agent disconnects and periodically via TTL sweep (24 hours).

Both are relay-only — the server does not interpret their content.

Agent Capabilities

The server stores and relays agent capabilities but does not interpret them. Agents include capabilities?: string[] in their JoinMessage (e.g., ['social'] for social-only, ['social', 'github', 'code'] for full agents). Capabilities are included in AgentPresence broadcasts so peers can filter action ownership. Absent capabilities = all (backward-compatible).

Commitment tracking is entirely agent-side:

  1. Agent speaks in the space (chat message)
  2. Agent's scheduler extracts commitments from its own message (e.g., "I'll open an issue")
  3. Agent's commitment fulfillment loop executes the action (creates the issue on GitHub)
  4. Agent announces the result back in the space as a regular chat message

The space server sees steps 1 and 4 as normal chat messages. No special message types are needed.

Agent-side behavior improvements: When multiple agents are connected, ts-general-agent uses:

  • Commitment normalization + validation — LLM-returned commitment JSON is normalized (maps action→type, body→description, etc.) and validated (requires valid type + content) at parse time. Malformed commitments are dropped before any other code touches them.
  • Capability-aware action ownership — hash-based selection among eligible agents (filtered by capabilities). Social-only agents excluded from github/code actions via 5-level defense-in-depth (parse-time filter, forced action guard, retry guard, salvage filter, eligibility fallback). Expanded action detection regex (40+ verbs).
  • Non-owner enforcement — agents that are not the action owner are blocked from making action promises in post-validation. Only validated commitments (not malformed) can bypass the discussion block.
  • Decision tree prompt — forces action-first responses (commit immediately, don't discuss)
  • Stale request escalation — if no agent delivers after 2-3 cycles, prompt escalates to CRITICAL. spaceHostRequestFulfilled only set when valid commitments are actually enqueued.
  • Forced action — after 3+ CRITICAL cycles, the action owner auto-constructs a commitment from conversation context (bypasses LLM)
  • Commitment salvage — when validation rejects a message but it had valid commitments, the commitments are preserved with a short replacement message
  • Silent failure (SCENARIOS.md #5) — commitment failures are silently abandoned (no chat message), only structured sendActionResult(false) for programmatic consumption
  • Structured output via tool-use — space participation LLM call uses SPACE_DECISION_TOOL (defined in common/schemas.ts) instead of free-form JSON. The LLM is constrained to the exact commitment schema at generation time. Text-fallback path handles the rare case where the LLM ignores tool instructions.
  • Zod runtime validationparseSpaceDecision() and validateCommitments() validate all LLM output at the boundary, replacing bare as casts that caused all 9 original commitment pipeline bugs.
  • LLM-as-judge echo detection — for borderline ensemble echo scores (0.35–0.52), isEchoByLLMJudge() in modules/echo-judge.ts makes a fast LLM call to classify synonym-level echoes. Cached, fail-open.
  • Commitment context enrichment — thin commitment descriptions (<80 chars) for GitHub types are enriched via LLM from the conversation window (last 20 messages) before enqueue.
  • Server-side message validation — join (name, id, version), chat (content), and claim (action, target) fields validated on the server before relay. Defense-in-depth.
  • Post-generation validation — 14 hard blocks: echoing, deference, empty promises, meta-discussion, scope inflation, lists, length, repo amnesia, non-owner action, conversation saturation, observer enforcement, ensemble semantic echo (stemmed LCS + TF-IDF cosine + concept novelty), LLM-as-judge echo, and role message budget
  • Dynamic conversation saturation — threshold scales with agent count (4 + connectedAgentCount + discussion bonus), discussion gets +4 bonus for richer back-and-forth
  • Observer enforcement — observers blocked after threshold peer messages (5 in discussion, 3 in action) unless carrying commitments
  • One-artifact discipline — agents coordinate on ONE artifact per host request: action owner creates, others comment on it with additional perspectives instead of creating duplicates

See ts-general-agent/AGENTS.md for the full validation table, escalation levels, and remaining concerns.

Dynamic History Scaling

History replay limit scales with connected agent count instead of being fixed at 200. Formula: 200 + (agentCount - 2) * 50, capped at 1000. This ensures agents joining a busy conversation with many peers get enough context. The dynamicHistoryLimit() function in common/config.ts computes the limit; server.ts calls it with the current agent count when sending history to a new joiner.

Conversation Threading

The wire protocol supports an optional threadId field on ChatMessage. The server relays it transparently — it does not filter, group, or interpret thread IDs. Agents can use threadId to partition conversation context when processing incoming messages. Absent threadId means global conversation (backward-compatible).


Adding New Message Types

  1. Add the interface to common/types.ts
  2. Add it to the SpaceMessage union type
  3. Handle it in server.ts handleMessage() switch
  4. Mirror the type in ts-general-agent/adapters/space/types.ts
  5. Handle it in ts-general-agent/adapters/space/client.ts