Durable background subagents
Give the parent tools to start and steer a researcher while it keeps chatting:
import * as Subagent from "@effect-agent/capabilities/Subagent";
import { Researcher } from "./researcher.ts";
const background = Subagent.background(Researcher, {
start: true,
followUp: true,
reportToParent: true,
});A start returns the worker reference and input receipt. The parent keeps responding, and a WorkerCompletion message arrives when the child run ends. It contains the projected result or a bounded failure, the worker and run identities, and a budget-exhaustion flag. Finishing or aborting the parent run leaves the worker and pending report running.
Reports join an active parent run at an input boundary or start a later run in the same thread. The framework delivers them separately from the parent's application input: no report tags, mapper, input union, or extra host registration is required. Existing callers must opt in.
Send intermediate findings
Declare the update Schema on the Agent once, then enable parent reporting:
import { Schema } from "effect";
import { Agent, Subagent } from "effect-agent";
import { Toolkit } from "effect/unstable/ai";
export const HotelRequest = Schema.Struct({
city: Schema.String,
area: Schema.String,
sources: Schema.Array(Schema.Struct({ url: Schema.String, notes: Schema.String })),
});
export const AreaConcern = Schema.TaggedStruct("AreaConcern", {
area: Schema.String,
finding: Schema.String,
sources: Schema.Array(Schema.String),
});
export const HotelResearcher = Agent.make("hotel-researcher", {
input: HotelRequest,
updates: AreaConcern,
output: Schema.Struct({ hotels: Schema.Array(Schema.String), summary: Schema.String }),
instructions:
"Review the supplied source notes for hotel options. Use emit_update to share a material " +
"area concern as soon as you find one, citing only supplied sources. Continue the research " +
"after emitting. Treat concerns as provisional and incorporate follow-up preferences.",
toolkit: Toolkit.empty,
policy: { maxTurns: 6, maxToolCalls: 4, maxDuration: "2 minutes" },
});
export const hotels = Subagent.background(HotelResearcher, {
start: true,
followUp: true,
reportToParent: true,
});Save as background-updates.ts. This example reviews source notes supplied in its input; add your research tools to its toolkit for live retrieval. The native emit_update tool accepts { value: AreaConcern }. Its acknowledgement retains the finding and lets the child continue. An update is provisional information, independent of the final hotel result.
Give a coordinator hotels.toolkit and provide hotels.layer. Register the exact HotelResearcher definition alongside that coordinator, using the host setup below. With reportToParent: true, the parent receives both WorkerUpdate and WorkerCompletion without an application input union, mapper, or reporting entry. Agents without updates continue to send only completion.
The parent consumes the finding at a safe input boundary or in a later run. It can explain the concern, ask the user how to proceed, and use follow-up tools to redirect the hotel worker and other workers to Rosebank. Emission does not wait for a user decision or stop the child. See update delivery guarantees for ordering, backpressure, and recovery.
Give the parent its tools
import * as Subagent from "@effect-agent/capabilities/Subagent";
import * as Agent from "@effect-agent/core/Agent";
import { Schema } from "effect";
import { CoordinatorInput } from "./background-input.ts";
import { Researcher } from "./researcher.ts";
export const ResearchBackground = Subagent.background(Researcher, {
start: true,
followUp: true,
reportToParent: true,
});
export const BackgroundCoordinator = Agent.make("background-trip-coordinator", {
input: CoordinatorInput,
output: Schema.String,
toolkit: ResearchBackground.toolkit,
instructions:
"Help the user plan a trip. Start activity research in the background when needed. " +
"Keep discussing their preferences while research runs. Send changed preferences " +
"to the existing worker with follow_up. When WorkerCompletion arrives, explain " +
"the findings and flag partial results. On failure or cancellation, help choose a next step. " +
"Do not start another search just because a research report arrived.",
policy: { maxTurns: 6, maxToolCalls: 4, maxDuration: "2 minutes", toolConcurrency: 2 },
});Save as background-coordinator.ts. This uses the activity researcher directly. The default result is { output, budgetExhausted }. Use an explicit Subagent.make declaration when the parent should receive a custom result projection.
Define the parent's input
import { Schema } from "effect";
export const CoordinatorInput = Schema.Struct({ text: Schema.String });Save as background-input.ts. Instructions and host policy keep the original admitted application input as their context. For a completion, the framework renders the typed message instead of calling the application's inputPrompt again.
Connect the host
import { NodeDurableHost } from "@effect-agent/platform-node";
import { Layer } from "effect";
import { WorkerAccessLive } from "./background-access.ts";
import { BackgroundCoordinator, ResearchBackground } from "./background-coordinator.ts";
import { definitions, ModelLive, OpenAiLive } from "./node-agent.ts";
import { Researcher } from "./researcher.ts";
import { TravelToolsLive } from "./tools.ts";
export const HostLive = NodeDurableHost.layer(
[
{
agent: BackgroundCoordinator,
model: ModelLive,
definitions,
},
{ agent: Researcher, model: ModelLive, definitions },
],
{
filename: "./agents.sqlite",
deploymentId: "background-research",
producerId: "worker-start-001",
workerConcurrency: 4,
},
).pipe(
Layer.provide(ResearchBackground.layer),
Layer.provide(TravelToolsLive),
Layer.provide(WorkerAccessLive),
Layer.provide(OpenAiLive),
);Save as background-host.ts. Each entry registers an agent and its code versions with the host. The host discovers reporting from the coordinator's background tools. The Layers supply tool handlers, provider credentials, and worker access.
The host recovers accepted work and pending reports after restarts. Keep report preparation free of external side effects: recovery may repeat it before its decision is recorded.
Authorize the conversation
import { ThreadId } from "@effect-agent/core/Identifiers";
import { WorkerError } from "@effect-agent/core/Worker";
import { Principal } from "@effect-agent/thread/SubmissionLedger";
import { WorkerHostAuthorizer } from "@effect-agent/thread/WorkerHost";
import { Effect, Layer, Schema } from "effect";
// This local example permits one user to manage workers from one conversation.
export const principal = Schema.decodeSync(Principal)("travel-user");
export const threadId = Schema.decodeSync(ThreadId)("travel-chat");
export const WorkerAccessLive = Layer.succeed(WorkerHostAuthorizer)({
authorize: (request) =>
request.principal === principal && request.sourceThreadId === threadId
? Effect.succeed(principal)
: WorkerError.make({ operation: request.operation, reason: "denied" }),
});Save as background-access.ts. Worker access denies by default; this local example permits one user and conversation. In an application, check authenticated identity and thread ownership.
Run it
import { NodeDurableHost } from "@effect-agent/platform-node";
import { NodeRuntime } from "@effect/platform-node";
import { Effect } from "effect";
import { HostLive } from "./background-host.ts";
NodeRuntime.runMain(NodeDurableHost.run.pipe(Effect.provide(HostLive)));Save as background-main.ts and run with node --experimental-transform-types background-main.ts. Use the Node.js setup to submit BackgroundCoordinator with the exported principal, threadId, and this input:
{ "text": "Find food and walking activities in Lisbon." }Keep the host running so research and report delivery can progress. The Cloudflare runtime supports the same contracts.
Follow up and cancel
A follow-up joins an active worker run at a safe input boundary or starts a later run. Opt in to inspect, list, or cancel tools when needed; inspection reads a saved result. Cancellation targets one input's receipt; it does not close the worker. Several inputs joining one run produce one logical report. An input cancelled before it starts a run produces no completion message.
Workers share a bounded allocation from their source by default. Host lifetime and concurrency limits still apply. See independent budgets for separately funded work.
For application-driven starts, see the programmatic API. For delivery failures and recovery, see report guarantees.