Yoke.sh
Agents

Processor actor

Event-driven single-step LLM processor — one short inference step per conversation.message.posted.

The processor is the LLM brain of the agent mesh. Unlike a traditional blocking agent loop, it runs one short step per event — each persisted message re-queues the processor via conversation.message.posted.

The processor does not decide what the user sees. It persists monologue messages and moves on. Egress reads those posts and forwards agent text to the forum.

Design

/**
 * Event-driven LLM processor: one short step per `conversation.message.posted`.
 *
 * - user text → optional compaction → single infer → persist outputs (type: message)
 * - agent tool_call → execute tools → persist tool_result
 * - user tool_result → single infer → persist outputs
 *
 * Each persisted message re-queues the processor via the same event type.
 * User visibility is handled by egress, not here.
 */

This means the processor never blocks on multi-round tool loops. Tool execution and follow-up inference happen across separate event deliveries.

Trigger filter

filterFromConfig: () => (event) => {
  if (event.type !== "conversation.message.posted") return false;
  // Ignore forum traffic (ingestion handles copy-in) and legacy type: reply.
  return event.payload.conversationId !== config.sourceConversationId
    && event.payload.messageType !== "reply";
}

The processor handles monologue messages only. It does not filter on whether output is user-visible — that is egress's job.

State

const processorStateSchema = z.object({
  monologueId: z.string().optional(),
  agentParticipantId: z.string().optional(),
  processed: z.array(z.string()).default([]), // message ids already handled
});

Config is seeded on first run from instance config (monologueId, agentParticipantId). After compaction, state is repointed to the new monologue.

Decision tree

flowchart TD
  E[conversation.message.posted] --> C{conversationId === monologueId?}
  C -->|no| X[ignore]
  C -->|yes| D{already processed?}
  D -->|yes| X
  D -->|no| T{trigger part types}

  T -->|tool_call| TC[Execute tools in parallel]
  TC --> TR[Post tool_result as system author]

  T -->|tool_result| TI[inferOnce → post outputs]

  T -->|user text| COMP{compaction changed list?}
  COMP -->|yes| SWAP[Swap monologue + hydrate]
  SWAP --> INF[inferOnce → post outputs]
  COMP -->|no| INF

  TR --> E
  INF --> E
  INF --> EG[Egress forwards agent text to forum]

User text (role user)

  1. Read full monologue transcript → InferenceMessage[]
  2. Optionally run compaction — if the list changes, swap to a fresh monologue
  3. Call ctx.llm.infer once via inferOnce
  4. Persist outputs as type: "message" (text, tool_call, or both)

Agent tool_call (role agent)

  1. Resolve tool registry (static or lazy)
  2. Validate inputs and execute all tools in parallel
  3. Post tool_result parts to monologue as system author
  4. Mark message processed — the tool_result post triggers the next processor step

User tool_result (role user)

  1. Call inferOnce on the full transcript including the new tool results
  2. Persist outputs
  3. Mark processed

Configuration

const processorConfigSchema = z.object({
  sourceConversationId: z.string(),  // forum id
  forumParticipantId: z.string(),
  agentActorId: z.string(),
  monologueId: z.string(),           // initial — copied to state
  agentParticipantId: z.string(),
  model: z.string().default("fixture-echo"),
});

Created by joinConversation with the chosen model id.

Registration

defineProcessorActor({
  configSchema: processorConfigSchema,
  stateSchema: processorStateSchema,
  compaction: agentStack.compaction,  // optional
  tools: agentStack.tools,            // optional
});

Contrast with runInferenceLoop

ProcessorrunInferenceLoop
TriggerStream eventsDirect function call
Tool roundsOne round per eventUp to 8 rounds in one call
Use casePrimary agent meshEphemeral actors (guardian runner)
User visibilityDoes not decideDoes not decide (egress handles mesh agents)

The processor uses inferOnce + event re-queuing. Guardian runners use runInferenceLoop for a complete pass in one delivery.

Queue semantics

queue: { mode: "fifo", concurrency: 1 }

FIFO preserves monologue ordering. Concurrency 1 prevents parallel inference on the same monologue stint.

  • Egress — user-visible copy-out from monologue to forum
  • Inference — ctx.llm.infer and providers
  • Tools — tool registration and execution
  • Compaction — transcript compaction before infer

On this page