Ephemeral attached subagents
Bind a model to the child, then run the parent:
import { SubagentRuntime } from "@effect-agent/capabilities/Subagent";
import { SubagentReservationsMemoryLive } from "@effect-agent/capabilities/SubagentReservations";
import { IdGenerator } from "@effect-agent/core/IdGenerator";
import * as AgentRuntime from "@effect-agent/engine/AgentRuntime";
import { RunContextPreparationPassthrough } from "@effect-agent/engine/RunOptions";
import { ThreadHistory } from "@effect-agent/engine/ThreadHistory";
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai";
import { Config, Effect, Layer } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import { Coordinator } from "./coordinator.ts";
import { Research, ResearchFailed } from "./delegation.ts";
import { TravelToolsLive } from "./tools.ts";
const ResearchLive = SubagentRuntime.layer(Research, OpenAiLanguageModel.model("gpt-4.1-mini"), {
mapChildFailure: (error) => ResearchFailed.make({ reason: error._tag }),
}).pipe(Layer.provide(TravelToolsLive));
export const program = AgentRuntime.run(Coordinator, { city: "Lisbon" }).pipe(
Effect.provide(ResearchLive),
Effect.provide(OpenAiLanguageModel.model("gpt-4.1-mini")),
Effect.provide(SubagentReservationsMemoryLive),
Effect.provide(ThreadHistory.layerTransient),
Effect.provide(RunContextPreparationPassthrough),
Effect.provide(IdGenerator.layer),
Effect.provide(OpenAiClient.layerConfig({ apiKey: Config.redacted("OPENAI_API_KEY") })),
Effect.provide(FetchHttpClient.layer),
);Coordinator calls the Research tool, waits for its findings, and builds an itinerary. SubagentRuntime.layer supplies the child's model and tool handlers. The parent and child can use different models.
The files below define Research, Coordinator, and the sample activity tools. Save them beside delegation-live.ts.
Define the child
import * as Agent from "@effect-agent/core/Agent";
import { Schema } from "effect";
import { TravelTools } from "./tools.ts";
export const Researcher = Agent.make("activity-researcher", {
input: Schema.Struct({ city: Schema.String, focus: Schema.String }),
output: Schema.Struct({
activities: Schema.Array(Schema.String),
researchNotes: Schema.String,
}),
instructions: ({ city, focus }) =>
`Use search_activities to find activities in ${city}. Focus on ${focus}. ` +
"Return matching activities and notes explaining your selection.",
toolkit: TravelTools,
policy: {
maxToolCalls: 8,
},
});import { Effect, Schema } from "effect";
import { Tool, Toolkit } from "effect/unstable/ai";
const SearchActivities = Tool.make("search_activities", {
description: "Find activities in a city.",
parameters: Schema.Struct({ city: Schema.String }),
success: Schema.Array(Schema.String),
});
export const TravelTools = Toolkit.make(SearchActivities);
// Sample data. A real handler can query your database or a travel API.
const activities = [
{ city: "Lisbon", name: "Riverside walk" },
{ city: "Lisbon", name: "Food market" },
{ city: "Lisbon", name: "City museum" },
];
export const TravelToolsLive = TravelTools.toLayer({
search_activities: ({ city }) =>
Effect.succeed(activities.filter((a) => a.city === city).map((a) => a.name)),
});Researcher receives a city and focus. Its search_activities tool uses sample data, so only the model needs an API key. The child's tool calls stay in its own conversation.
Expose the child as a tool
import * as Subagent from "@effect-agent/capabilities/Subagent";
import { SubagentPolicy } from "@effect-agent/capabilities/Subagent";
import { Effect, Schema } from "effect";
import { Researcher } from "./researcher.ts";
export class ResearchFailed extends Schema.TaggedError<ResearchFailed>()("ResearchFailed", {
reason: Schema.String,
}) {}
export const Research = Subagent.make("delegate_research_activities", {
description: "Delegate activity research for one city and focus. Returns a shortlist.",
target: Researcher,
success: Schema.Struct({
activities: Schema.Array(Schema.String),
partial: Schema.Boolean,
}),
failure: ResearchFailed,
failureMode: "error",
projectResult: (output, { budgetExhausted }) =>
Effect.succeed({
activities: output.activities,
partial: budgetExhausted,
// researchNotes stays in the child's thread.
}),
policy: SubagentPolicy.make({
maxChildren: 2,
maxConcurrency: 2,
maxTurns: 4,
maxToolCalls: 4,
maxDuration: "30 seconds",
maxResultBytes: 4_096,
}),
});Subagent.make uses the child's input Schema as its tool parameters. This example customizes the result: projectResult returns the activities and a partial flag, leaving research notes in the child's thread. The policy bounds each research task.
For default input and output mapping, only name and { target: Researcher } are needed. See the minimal declaration or mapping reference.
Give the parent the delegation tool
import * as Agent from "@effect-agent/core/Agent";
import { AgentPolicy } from "@effect-agent/core/AgentPolicy";
import { Schema } from "effect";
import { Toolkit } from "effect/unstable/ai";
import { Research } from "./delegation.ts";
export const Coordinator = Agent.make("trip-coordinator", {
input: Schema.Struct({ city: Schema.String }),
output: Schema.Struct({ itinerary: Schema.Array(Schema.String) }),
instructions:
"Call delegate_research_activities for the requested city with a focus on food and walking. " +
"Build an itinerary from the returned activities. If partial is true, use only confirmed findings.",
toolkit: Toolkit.make(Research.tool),
policy: AgentPolicy.make({
maxTurns: 6,
maxToolCalls: 2,
maxDuration: "2 minutes",
toolConcurrency: 2,
}),
});The parent sees the projected result as the tool's answer:
{ "activities": ["Riverside walk", "Food market"], "partial": false }Run it
import { NodeRuntime } from "@effect/platform-node";
import { Console, Effect } from "effect";
import { program } from "./delegation-live.ts";
NodeRuntime.runMain(program.pipe(Effect.tap(({ output }) => Console.log(output))));export OPENAI_API_KEY="your-api-key"
node --experimental-transform-types delegation-main.tsThe child shares the parent's Scope. Interruption stops both; a process restart loses active execution. Use durable attached when that work needs recovery. Stored history alone does not make execution durable.
Failure and limits
One delegation counts as one parent tool call. The child consumes its own reserved allowance. Set failureMode: "return" to give expected child failures to the parent model as data; defects and interruption retain their Effect meaning.
See budgets and permissions, or switch to background workers so the parent can continue while children work.