Skip to content

Conceptual overview of how protoWorkstacean's components connect and why they are designed the way they are.


System overview

protoWorkstacean is a switchboard. Triggers come in from the outside world or from a scheduler, the router resolves them to a skill, the dispatcher routes them to the right executor, and the executor runs the skill (locally in a DeepAgent, remotely over A2A, or as a function handler).

mermaid
flowchart TD
    subgraph Surfaces["External Surfaces"]
        GH[GitHub webhooks]
        DC[Discord gateway]
        LN[Linear webhooks]
        GG[Google Gmail/Calendar polling]
        HTTP["HTTP API :3000\n/publish · /api/*"]
        MCP["MCP Server\n(Claude Code agents)"]
    end

    BUS[("Event Bus\nin-process pub/sub\nhierarchical topics")]

    subgraph Adapters["Integration Plugins"]
        GHP[GitHubPlugin]
        DCP[DiscordPlugin]
        LNP[LinearPlugin]
        GGP[GooglePlugin]
    end

    subgraph Schedulers["Schedulers"]
        SP["SchedulerPlugin\nworkspace/crons/*.yaml"]
        CP["CeremonyPlugin\nworkspace/ceremonies/*.yaml"]
    end

    subgraph Routing["Skill Routing"]
        RTP["RouterPlugin\nmessage.inbound.# + cron.#\n→ agent.skill.request"]
        SDP["SkillDispatcherPlugin\nagent.skill.request subscriber\n(sole consumer)\nchokepoint invariants live here"]
        REG["ExecutorRegistry\nresolve(skill, targets)\nhealth-weighted dispatch"]
    end

    subgraph Registrars["Registrars (install-time only)"]
        ART["AgentRuntimePlugin\nworkspace/agents/*.yaml\n→ DeepAgentExecutor"]
        SKB["SkillBrokerPlugin\nworkspace/agents.yaml\n→ A2AExecutor"]
        ASE["AlertSkillExecutorPlugin\n→ FunctionExecutor for alert.*"]
        CSE["CeremonySkillExecutorPlugin\n→ FunctionExecutor for ceremony.*"]
    end

    subgraph Executors["Executor implementations"]
        DAE["DeepAgentExecutor\nLangGraph createReactAgent\nin-process (Ava, protoBot)"]
        A2AE["A2AExecutor\nHTTP JSON-RPC 2.0 + SSE\n(Quinn, Researcher, Jon, protoPen)"]
        FE["FunctionExecutor\ninline function\n(alert.*, ceremony.*)"]
    end

    GH --> GHP
    DC --> DCP
    LN --> LNP
    GG --> GGP
    HTTP --> BUS
    MCP --> BUS

    GHP & DCP & LNP & GGP --> BUS
    SP --> BUS
    CP --> BUS

    BUS -- "message.inbound.#\ncron.#\nceremony.*.execute" --> RTP
    RTP -- "agent.skill.request" --> BUS
    BUS -- "agent.skill.request" --> SDP
    SDP --> REG
    REG --> DAE & A2AE & FE

    ART -- "register DeepAgentExecutor" --> REG
    SKB -- "register A2AExecutor" --> REG
    ASE & CSE -- "register FunctionExecutor" --> REG

The event bus

The bus is the only communication channel. No plugin talks directly to another plugin — everything goes through bus.publish() and bus.subscribe(). Topic matching is hierarchical: # matches anything, * matches one segment.

This constraint is what makes the system composable. Adding Discord support doesn't touch the GitHub plugin. Adding a new executor type doesn't touch the routing logic. Plugins are independently installable and testable.


Executor layer

The executor layer is the unified dispatch path for all agent skill calls. It separates registration from dispatch:

  • Registrars (AgentRuntimePlugin, SkillBrokerPlugin, plus the function-executor registrars for alert.* / ceremony.*) — register executors into ExecutorRegistry at install() time. No bus subscriptions.
  • Dispatcher (SkillDispatcherPlugin) — sole subscriber to agent.skill.request, delegates to the registry.
agent.skill.request
  → SkillDispatcherPlugin
    ├── chokepoint invariants (cooldown, target-registry guard;
    │   synthetic-actor filter lives downstream at fleet-health)
    └── ExecutorRegistry.resolve(skill, targets?)
       1. Named target: any registration whose agentName ∈ targets[]
       2. Skill match: highest priority registration where skill matches
          (health-weighted random selection when fleet metrics available)
       3. Default executor
       4. null → error response, message dropped
    → executor.execute(SkillRequest)
      → result published to replyTopic

SkillRequest carries correlationId (trace-id) and parentId (parent span-id), set by SkillDispatcherPlugin from the triggering bus message.


Scheduling

Two related but distinct primitives:

  • Crons (workspace/crons/*.yaml, served by SchedulerPlugin) — generic time-based triggers. Each yaml defines schedule (cron expression or ISO datetime for one-shot), topic, payload. Hot-reloaded every 5 seconds.
  • Ceremonies (workspace/ceremonies/*.yaml, served by CeremonyPlugin) — named, observable rituals. Each ceremony has a schedule, a skill, targets, optional Discord notification, and persistent outcome history. Can also be triggered on-demand by a skill caller via the ceremony.* FunctionExecutor.

Use a cron when you just need a topic fired on a schedule. Use a ceremony when you want the schedule to dispatch a specific skill on a specific agent and have the outcomes show up in /api/ceremonies.


Distributed tracing

Every BusMessage carries:

FieldRoleChanges?
correlationIdW3C trace-id — links every message in a request treeNever
parentIdParent span-id — = triggering message's idAt each hop

RouterPlugin sets parentId when translating inbound messages to agent.skill.request. A2AExecutor forwards both as X-Correlation-Id and X-Parent-Id HTTP headers. External A2A agents propagate X-Correlation-Id into their internal chat calls. When LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY are set, DeepAgent calls produce spans in Langfuse.

See Distributed Tracing.


Message routing conventions

message.inbound.github.<owner>.<repo>.<event>.<number>   — inbound from GitHub
message.outbound.github.<owner>.<repo>.<number>          — outbound to GitHub comment
message.inbound.discord.<channelId>                      — inbound from Discord
message.outbound.discord.<channelId>                     — outbound to Discord
message.inbound.linear.<event>                           — inbound from Linear webhook
message.inbound.google.<gmail|calendar>                  — inbound from Google polling
agent.skill.request                                      — route to agent via SkillDispatcher
agent.skill.response.<correlationId>                     — default reply topic
ceremony.<id>.execute                                    — trigger named ceremony
autonomous.outcome.<systemActor>.<skill>                 — terminal outcome for any skill dispatch

What this app is NOT

  • It is not a GOAP system. There is no world-state engine, no goal evaluator, no planner. Earlier versions had one; it was a baroque polling loop and was removed. If you're tempted to add a "world model" or "planner" or "goal," stop and ask whether a cron or a webhook would do the same job. It almost always will.
  • It is not an agent itself. It hosts agents (DeepAgent in-process, A2A remote) but the routing and scheduling layer has no agency.
  • It is not a workflow engine. Ceremonies are scheduled fleet rituals, not orchestrated multi-step workflows.

protoWorkstacean — a switchboard, not an agent.