Pushing Results to the User

When background work produces a user-visible result — a research finding, a fetched document, a completed task list, a quote, a contract — the application has to decide two things: how the data reaches the user-facing surface, and what (if anything) the orchestrator is told about that having happened. Beach offers three patterns for the combination. None is the "right" one. Each is correct for a different kind of surface, and the choice is made per surface at design time.

The patterns differ along a single axis: how much of the rendered data the orchestrator's LLM sees. That single axis matters because LLMs, particularly under conversational pressure, will sometimes be helpful in ways the application would prefer they were not — restating a price slightly lower because the user is unhappy, paraphrasing a contract clause in plain English when the original wording was load-bearing, summarising a medical figure into a range when the patient asked what it was. The defence against that is not to remove the orchestrator from the loop. It is to think carefully about which values the orchestrator has in its context to restate.

Pattern 1 — Render with full inform

The conversation-shaped case. The data is consequential but its truth is not load-bearing on the conversation: a list of holiday options, a set of restaurant suggestions, a search result page. The orchestrator's awareness of the data is what makes the next conversational turn work. If the user replies "actually I want option three", the orchestrator that has seen the list can respond meaningfully; the orchestrator that has not, cannot.

await uiBridge.publishSurfaceUpdate(sessionId, renderResults(results));
await router.routeEvent({
  source: 'background',
  eventType: 'hotels_rendered',   // routing config maps this to the actor handler
  data: {
    sessionId,
    turnId: crypto.randomUUID(),
    inboundMessage: {
      role: 'user',
      content: `Twelve hotel options rendered. Top three by rating: ${summary}.`,
    },
  },
});

The orchestrator sees the summary. It can comment, recommend, narrow the search, or invite the user to refine. The LLM's tendency to be agreeable does no harm here because nothing it says revises the rendered facts on screen — the rendered list is the source of truth, the orchestrator is its conversational companion.

This is the right pattern when the data is shaped to invite conversation about it.

Pattern 2 — Render with positional inform only

The high-stakes case. Prices, contract terms, medical results, legal advice, signed booking confirmations, regulatory disclosures. The data must reach the user faithfully, with no LLM in the rendering path; and the orchestrator must not be tempted to restate or paraphrase it under conversational pressure. The routed message carries only the fact that rendering happened — not the values themselves.

await uiBridge.publishSurfaceUpdate(sessionId, renderQuote(quote));
await router.routeEvent({
  source: 'background',
  eventType: 'quote_rendered',    // routing config maps this to the actor handler
  data: {
    sessionId,
    turnId: crypto.randomUUID(),
    inboundMessage: {
      role: 'user',
      content:
        `A booking quote has been rendered for the user to review. ` +
        `Do not restate the price, the dates, or any contractual figures. ` +
        `If the user asks about the quote's contents, refer them to the ` +
        `displayed values; do not re-derive, summarise, or paraphrase them. ` +
        `If the user wants changes, trigger the appropriate quote workflow.`,
    },
  },
});

The orchestrator is told that something happened, and where, and the kind of thing it was. It is not given the figures. It cannot mangle what it does not have. If the user replies "can we make that cheaper?", the orchestrator's options are bounded — re-trigger the quote workflow with different parameters, or refer the user to the displayed values — but it cannot say "I've reduced it to £350" because £350 is not in its context to claim.

The defence here is twofold and works as a pair. The orchestrator's system prompt forbids restatement of the rendered values; the routed message does not contain them. A prompt alone is not sufficient — an LLM under enough conversational pressure may attempt restatement of values it has been told not to. The defence is that the values are not in its context.

A reusable prompt fragment for actors that operate alongside truth-load-bearing surfaces ships with @cool-ai/beach-llm as displayedValuesSnippet, joining respondToolSnippet and turnStatesSnippet:

When the user is shown structured data (a quote, an itinerary, a contract,
a medical result, a regulatory disclosure), you may be informed that the
data has been rendered. You are not given the underlying values. Do not
invent them. Do not restate, summarise, recompute, or paraphrase them.
Refer the user to the displayed surface. If the user asks for changes,
trigger the appropriate tool to produce new structured data; do not
adjust values yourself.

This is the right pattern when the data must reach the user faithfully and the orchestrator must remain part of the conversation without being able to revise what is on screen.

Pattern 3 — Render with no inform

The system-or-audit case. A live operations dashboard. A reminder fired at 9 a.m. by cron because the user scheduled it. A push notification triggered by an upstream webhook. There is no awaiting turn to route into; the orchestrator is not part of this signal at all; informing it would be a fabricated event.

await uiBridge.publishSurfaceUpdate(sessionId, renderActivityLog(events));
// No router.routeEvent(). The orchestrator does not know.

The diagnostic question: is there a turn waiting for this result? If the answer is no, the orchestrator is not in this loop. If the answer is yes, one of the first two patterns applies, and which one depends on the data.

This is the right pattern when the surface is genuinely orthogonal to the conversational track.

Choosing between the three

Two questions decide the pattern:

Is there an awaiting turn? If no, Pattern 3. The orchestrator is not informed because it is not part of this signal.

If yes, can the orchestrator afford to see the values? If the data is conversation-shaped — invites discussion, comparison, refinement, the user expects the agent to engage with it — Pattern 1. If the data is truth-load-bearing — the user must trust that what they see is what the application intends, and an LLM-paraphrased version would be worse — Pattern 2.

The same application typically uses all three. A travel-planning agent renders the discover-grid (twelve hotels) with full inform; renders the booking quote with positional inform only; renders the daily ops digest to an internal dashboard with no inform. The choice is per surface, decided at the point each renderer is wired, and is part of the application's design — not a Beach rule.

How the router delivers a result to the actor

When the orchestrator calls respond() with turnState: 'awaiting', its current invocation is complete — the actor has told the system it expects more work. When that background work finishes, the result is routed to the actor via router.routeEvent(). The router matches the event to the actor's handler via the routing configuration and starts a new actor invocation with the inboundMessage as the inbound. The actor sees it and decides what to do next — acknowledge, reply, or call another tool.

The routing rule that maps the background event to the actor sits in the application's routing configuration:

{ "source": "background", "eventType": "hotels_rendered", "handler": "my-orchestrator" }

The mechanism is the same across Patterns 1 and 2. What differs is what the inboundMessage contains.

Multiple parallel results — Assembly Manifest

When the orchestrator dispatches several research tasks in parallel and wants to react to all of them together, use an Assembly Manifest — a Manifest that accumulates named slots and fires onComplete when all are filled. The onComplete callback routes an event to the actor via router.routeEvent() with the assembled data as the inboundMessage. The same three patterns apply to the assembled result: full inform (Pattern 1), positional only (Pattern 2), or no inform (Pattern 3), depending on the surface the assembled data will reach.

See Manifests for the full Assembly Manifest pattern.

Pitfalls

Sending the full payload as a Pattern 1 inform. The orchestrator's context is finite. A 10MB research dump as the inboundMessage costs an enormous prompt and may exceed the model's context window. Summarise. Persist the full data to the missive store; send a short digest. Beach's truncate-and-stage support fetches more on demand when the orchestrator needs it.

Using Pattern 1 for truth-load-bearing data. The classic LLM-pleasing failure: a user pushes back on a price, and the orchestrator — having the price in its context — proposes a "small adjustment" the application never authorised. Pattern 2 prevents this by withholding the values from the orchestrator entirely. Audit the surfaces where prices, contractual terms, regulated language, or other truth-load-bearing values appear, and use Pattern 2 for each.

Using Pattern 2 without the prompt fragment. Withholding the values from the inbound message is necessary but not sufficient on its own. An orchestrator whose system prompt does not forbid restatement may, under pressure, infer or paraphrase. Ship displayedValuesSnippet (or the application's own equivalent) in every actor that operates alongside Pattern 2 surfaces.

Using Pattern 3 when there is an awaiting turn. Rendering without informing an awaiting orchestrator means the orchestrator's reply arrives next saying "I am still searching" while the user is already looking at the result. The two voices clash. If a turn is awaiting, choose Pattern 1 or Pattern 2; do not silently stop.

Routing the background result to a session that no longer exists. When the session has expired or the user has disconnected, the routing call will fail with a routing error or a warning if no session is open for the sessionId. Check whether the session is still active before routing background results back. A dropped delivery means the user is waiting on an actor that will not resume; escalate with a fallback event or an error log when drops appear.

Related

  • Manifests — Assembly Manifests in detail, for coordinating parallel research results.
  • Anti-patterns — render-without-inform when there is an awaiting turn (which is the genuine anti-pattern), and the inform-with-data anti-pattern for truth-load-bearing surfaces.
  • Using the starter scaffold — routing configuration and handler wiring.
  • Creating routing rules — how background events get routed to actor handlers.
  • Reference: respond-toolrespondToolSnippet, turnStatesSnippet, and displayedValuesSnippet.
  • Reference: design principles — principle 3.1 (background results dispatch immediately).