Skip to content

Instantly share code, notes, and snippets.

@ianphil
Created April 4, 2026 14:06
Show Gist options
  • Select an option

  • Save ianphil/0601eef98b70a2b02354e63c2641d3e8 to your computer and use it in GitHub Desktop.

Select an option

Save ianphil/0601eef98b70a2b02354e63c2641d3e8 to your computer and use it in GitHub Desktop.
Dapr Actor-Based Agent Architecture Research

Actor-Based Agent Architecture Research

Research into evolving faux-foundation's agent architecture using Dapr actors, informed by pi-agent-core's design patterns.


Context

Current State

The chat UI runs the entire agent loop in the browser (Chat.tsx):

  1. Builds input items from conversation history
  2. POSTs to llm-proxy /v1/responses (streaming SSE)
  3. Parses tool calls from SSE events
  4. Executes tools against the tool-service via Dapr service invocation
  5. Appends results to input, re-submits to LLM
  6. Loops until the LLM responds with pure text (no tool calls)
  7. Persists to Dapr state store after each completed exchange

MacGyver runs as a separate bespoke agent with its own loop (star-poller → clone → reverse-spec → commit). There is no shared agent runtime between the chat UI and MacGyver.

Browser (Chat.tsx)                    MacGyver (agents/macgyver/)
  │                                     │
  ├─ POST /v1/responses (stream)        ├─ setInterval → star-poller
  ├─ parse tool calls                   ├─ clone repo
  ├─ POST /tools/web-fetch              ├─ CopilotClient.sendAndWait()
  ├─ append results, loop               ├─ commit spec
  └─ persist to state store             └─ (no events, no steering)

Reference Architecture: pi-agent-core

pi-agent-core (from badlogic/pi-mono) is a lightweight agent orchestration layer built on top of pi-ai (a unified LLM API). Key patterns:

  • Two-loop design: Inner loop handles tool calls + steering. Outer loop handles follow-ups after the agent would otherwise stop.
  • Event system: 10 event types across agent/turn/message/tool lifecycle for real-time UI updates.
  • Steering & follow-up queues: Mid-run injection ("stop, do this instead") and post-run injection ("also do this when done") with configurable drain modes.
  • Before/after tool hooks: Safety gates before execution, result modification after. Block dangerous calls with a reason.
  • Parallel tool execution: Sequential preflight (validation + beforeToolCall), concurrent execution, results emitted in source order.
  • Custom message types: App-specific messages in the transcript alongside LLM messages, filtered before LLM calls via convertToLlm.
  • Context transformation pipeline: Two-phase — transformContext (prune/inject) → convertToLlm (filter custom types) — runs before each LLM call.
  • Single dependency, five source files: Agent class, agent-loop, proxy, types, index.

llm-svc vs pi-ai

copilot-llm-svc is faux-foundation's equivalent of pi-ai — a local OpenAI-compatible proxy routing to GitHub Copilot's LLM API. Key capabilities in pi-ai that llm-svc could adopt:

  • Cost/usage tracking — token breakdown and per-request cost calculation
  • Thinking/reasoning support — unified thinking levels mapped to model-native mechanisms
  • Context overflow detection — pattern-matching across provider error formats
  • Prompt cache controlcacheRetention and sessionId for session-aware caching
  • Abort with partial preservation — clean cancellation keeping partial content
  • Streaming JSON parsing — safe partial tool-call argument parsing mid-stream

Proposed Architecture: Dapr Actors as Agent Runtime

Core Idea

Instead of building a custom Agent class with state management, single-run enforcement, HTTP endpoints, and event delivery — use Dapr actors. Each agent conversation becomes a virtual actor instance.

Dapr Building Blocks → pi-agent-core Concepts

pi-agent-core concept Dapr building block
Agent instance state Actors — virtual actor per session, state auto-persisted, single-threaded
Single-run enforcement Actors — one call processed at a time, guaranteed by runtime
Event system Pub/Sub — agent publishes to topic, UI and other agents subscribe
Steering / follow-up queues Actor methodssteer() / followUp() called on the actor
Triggers Input Bindings + Jobs API — cron, webhooks, event-driven
Tool execution Service Invocation — agent actor calls tool-service via Dapr (already exists)
Context persistence State Store — actor state manages conversation (already exists)

Actor Design

┌────────────────────────────────────────────────┐
│          Dapr Actor: AgentSession               │
│          (one per conversation)                 │
│                                                 │
│  Methods (replace custom endpoints):            │
│    prompt(text)    → starts the loop            │
│    steer(text)     → injects mid-run            │
│    followUp(text)  → queues for after           │
│    abort()         → cancels current run         │
│    getState()      → returns messages/status     │
│                                                 │
│  Actor guarantees:                              │
│    • one call at a time (no concurrent runs)    │
│    • state saved automatically after each call  │
│    • reactivated on demand, sleeps when idle    │
│    • survives restarts (state in store)         │
│                                                 │
│  Inside prompt():                               │
│    while (true):                                │
│      stream(llm-proxy)  → publish events        │
│      if tools: execute via service invocation   │
│      if steering queue: drain + loop            │
│      if follow-ups: drain + loop                │
│      else: break                                │
└────────────────────────────────────────────────┘

Multi-Agent via Config

One codebase (apps/agent/), N instances in dapr.yaml, differentiated by environment:

Env Var Purpose
MIND_REPO Git repo for SOUL.md + expertise files
AGENT_TOOLS Which tools this agent can use
TRIGGER What kicks off a run (star-poller, pr-watcher, cron, chat)
MODEL Default model preference
dapr.yaml
─────────
  agent:macgyver  :3200   MIND_REPO=ianphil/macgyver  TRIGGER=star-poller
  agent:jack      :3201   MIND_REPO=ianphil/jack      TRIGGER=pr-watcher
  agent:penny     :3202   MIND_REPO=ianphil/penny     TRIGGER=chat-only
  llm-proxy       :5100
  tool-service    :3100

The mind repo is the differentiation. Same runtime, many minds.

Target Flow

Browser (Chat.tsx)                     Agent Service (Dapr Actor)
  │                                      │
  ├─ POST actor.prompt("fix the bug")    │
  ├─ Subscribe to pub/sub events    ◄────┤─ publish: agent_start
  ├─ render text_delta              ◄────┤─ publish: text_delta
  ├─ render tool status             ◄────┤─ publish: tool_execution_start
  ├─ POST actor.steer("stop, ...")       │
  │                                      ├─ inject steering
  ├─ render final                   ◄────┤─ publish: agent_end
  │                                      │
  │                                      ▼
  │                              ┌──────────────┐  ┌────────────┐
  │                              │  LLM Proxy   │  │  Tool Svc  │
  │                              │  :5100       │  │  :3100     │
  │                              └──────────────┘  └────────────┘

What Dapr Provides (don't build)

  • No custom HTTP endpoints for prompt/steer/abort — actor methods
  • No event SSE endpoint — pub/sub topic
  • No state save/load code — actor state management
  • No single-run mutex — actors are single-threaded
  • No trigger endpoints — input bindings

What You Still Build

  • The agent loop logic itself (stream → tool calls → loop)
  • Tool hook logic (before/after)
  • Context transformation pipeline
  • Mind loading (already done in mind-session.mjs)
  • The thin chat UI event subscriber

What Already Works (reuse)

  • mind-session.mjsloadMindSnapshot(), resolveMindRoot(), bootstrapMind() are already generic
  • tool-service — web-fetch and web-search, called via Dapr service invocation
  • llm-proxy — OpenAI-compatible proxy with auto-routing and translation
  • Dapr state store — conversation persistence
  • Dapr Jobs API — scheduling
  • Three-tier dev loop (process → Kind → ACA)

Open Questions

  • Actor language: Dapr actors have first-class support in .NET, Java, Python, Go. The JS SDK has actor support but it's less mature. Evaluate JS actor SDK vs writing the agent service in .NET/Python.
  • Pub/sub for streaming: Pub/sub is message-based, not streaming. For text deltas, options are: batch deltas into small messages, use Dapr's streaming API if available, or keep SSE for the browser and pub/sub for agent-to-agent.
  • Long-running actor calls: The agent loop may run for minutes (tool execution, large context). Dapr actors have reentrancy and timeout considerations. May need actor reminders or a workflow pattern for very long runs.
  • Dapr Workflows alternative: Dapr Workflows (durable task framework) could model the agent loop as a workflow with activities. Gives retry, compensation, and observability. Trade-off: more ceremony, but better durability guarantees.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment