Compare commits

...
Author SHA1 Message Date
vimtor 6051a67bc2 feat(core): add session subagent API 2026-09-24 12:41:27 +02:00
20 changed files with 421 additions and 3 deletions
+16
View File
@@ -226,6 +226,20 @@ export type SessionForkInput = { readonly sessionID: Session.ID; readonly before
export type SessionForkOutput = Session.Info
export type SessionForkOperation<E = never> = (input: SessionForkInput) => Effect.Effect<SessionForkOutput, E>
export type SessionSubagentInput = {
readonly sessionID: Session.ID
readonly text: string
readonly description: string
readonly agent?: Agent.ID | undefined
readonly model?: Model.Ref | undefined
readonly fork?: boolean | undefined
readonly resume?: boolean | undefined
}
export type SessionSubagentOutput = Session.Info
export type SessionSubagentOperation<E = never> = (
input: SessionSubagentInput,
) => Effect.Effect<SessionSubagentOutput, E>
export type SessionSwitchAgentInput = { readonly sessionID: Session.ID; readonly agent: Agent.ID }
export type SessionSwitchAgentOutput = void
export type SessionSwitchAgentOperation<E = never> = (
@@ -589,6 +603,7 @@ export type SessionLogOutput =
readonly sessionID: Session.ID
readonly parentID: Session.ID
readonly boundary: Session.ForkBoundary
readonly child?: boolean | undefined
readonly instructions?:
| { readonly [x: string & Brand.Brand<"Instruction.Key">]: string & Brand.Brand<"Instruction.Hash"> }
| undefined
@@ -1409,6 +1424,7 @@ export interface SessionApi<E = never> {
readonly get: SessionGetOperation<E>
readonly remove: SessionRemoveOperation<E>
readonly fork: SessionForkOperation<E>
readonly subagent: SessionSubagentOperation<E>
readonly switchAgent: SessionSwitchAgentOperation<E>
readonly switchModel: SessionSwitchModelOperation<E>
readonly update: SessionUpdateOperation<E>
@@ -36,6 +36,8 @@ import type {
SessionRemoveOutput,
SessionForkInput,
SessionForkOutput,
SessionSubagentInput,
SessionSubagentOutput,
SessionSwitchAgentInput,
SessionSwitchAgentOutput,
SessionSwitchModelInput,
@@ -435,6 +437,24 @@ const EndpointSessionFork = (raw: RawClient["server.session"]) => (input: Sessio
),
)
const EndpointSessionSubagent = (raw: RawClient["server.session"]) => (input: SessionSubagentInput) =>
preserveEffect<SessionSubagentOutput>()(
raw["session.subagent"]({
params: { sessionID: input["sessionID"] },
payload: {
text: input["text"],
description: input["description"],
agent: input["agent"],
model: input["model"],
fork: input["fork"],
resume: input["resume"],
},
}).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
),
)
const EndpointSessionSwitchAgent = (raw: RawClient["server.session"]) => (input: SessionSwitchAgentInput) =>
preserveEffect<SessionSwitchAgentOutput>()(
raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe(
@@ -747,6 +767,7 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({
get: EndpointSessionGet(raw),
remove: EndpointSessionRemove(raw),
fork: EndpointSessionFork(raw),
subagent: EndpointSessionSubagent(raw),
switchAgent: EndpointSessionSwitchAgent(raw),
switchModel: EndpointSessionSwitchModel(raw),
update: EndpointSessionUpdate(raw),
@@ -30,6 +30,8 @@ import type {
SessionRemoveOutput,
SessionForkInput,
SessionForkOutput,
SessionSubagentInput,
SessionSubagentOutput,
SessionSwitchAgentInput,
SessionSwitchAgentOutput,
SessionSwitchModelInput,
@@ -637,6 +639,25 @@ export function make(options: ClientOptions) {
},
requestOptions,
).then((value) => value.data),
subagent: (input: SessionSubagentInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionSubagentOutput }>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/subagent`,
body: {
text: input["text"],
description: input["description"],
agent: input["agent"],
model: input["model"],
fork: input["fork"],
resume: input["resume"],
},
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
).then((value) => value.data),
switchAgent: (input: SessionSwitchAgentInput, requestOptions?: RequestOptions) =>
request<SessionSwitchAgentOutput>(
{
@@ -1811,6 +1811,7 @@ export type SessionForked = {
sessionID: string
parentID: string
boundary: SessionForkBoundary
child?: boolean
instructions?: { [x: string]: string }
instructionEntries?: InstructionEntrySnapshot
}
@@ -3938,6 +3939,60 @@ export type SessionForkInput = {
export type SessionForkOutput = { data: SessionInfo }["data"]
export type SessionSubagentInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly text: {
readonly text: string
readonly description: string
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly fork?: boolean | null
readonly resume?: boolean | null
}["text"]
readonly description: {
readonly text: string
readonly description: string
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly fork?: boolean | null
readonly resume?: boolean | null
}["description"]
readonly agent?: {
readonly text: string
readonly description: string
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly fork?: boolean | null
readonly resume?: boolean | null
}["agent"]
readonly model?: {
readonly text: string
readonly description: string
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly fork?: boolean | null
readonly resume?: boolean | null
}["model"]
readonly fork?: {
readonly text: string
readonly description: string
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly fork?: boolean | null
readonly resume?: boolean | null
}["fork"]
readonly resume?: {
readonly text: string
readonly description: string
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly fork?: boolean | null
readonly resume?: boolean | null
}["resume"]
}
export type SessionSubagentOutput = { data: SessionInfo }["data"]
export type SessionSwitchAgentInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly agent: { readonly agent: string }["agent"]
+2
View File
@@ -23,6 +23,8 @@ const Background = Schema.Struct({
childSessionID: SessionSchema.ID,
agent: Schema.String,
description: Schema.String,
/** False admits the completion notice without resuming the parent. */
resume: Schema.optionalKey(Schema.Boolean),
}),
]),
status: Schema.Literals(["running", "completed", "error", "cancelled"]),
+1
View File
@@ -542,6 +542,7 @@ export const make = Effect.fn("PluginHost.make")(function* (
}),
move: sessions.move,
synthetic: sessions.synthetic,
subagent: sessions.subagent,
interrupt: (input) =>
sessions
.interrupt(input.sessionID, { resume: input.resume })
+14
View File
@@ -26,6 +26,7 @@ import { SessionRunner } from "./session/runner/index.js"
import { SessionStore } from "./session/store.js"
import { SessionExecution } from "./session/execution.js"
import {
AgentNotFoundError,
AttachmentError,
BusyError,
CompactionConflictError,
@@ -45,6 +46,8 @@ import { SessionInbox } from "./session/inbox.js"
import { InstructionState } from "./session/instruction-state.js"
import { SessionGenerate } from "./session/generate.js"
import { SessionCommand } from "./session/command.js"
import { SessionSubagent } from "./session/subagent.js"
import { SubagentJob } from "./session/subagent-job.js"
import {
SessionMove,
DestinationNotFoundError,
@@ -95,9 +98,12 @@ type CompactInput = Parameters<Session.Handle["compact"]>[0] & { sessionID: Sess
type ForkInput = {
sessionID: SessionSchema.ID
before?: SessionMessage.ID
/** Makes the fork a child of the source session instead of a top-level session. */
child?: boolean
}
export {
AgentNotFoundError,
AttachmentError,
BusyError,
CompactionConflictError,
@@ -187,6 +193,10 @@ export interface Interface {
sessionID: SessionSchema.ID
prompt: string
}) => Effect.Effect<string, NotFoundError | SessionGenerate.Error>
/** Starts a background subagent in a child Session and delivers its outcome to the parent when it settles. */
readonly subagent: (
input: SessionSubagent.Input,
) => Effect.Effect<SessionSchema.Info, NotFoundError | AgentNotFoundError>
readonly command: (input: {
sessionID: SessionSchema.ID
command: string
@@ -341,6 +351,7 @@ const layer = Layer.effect(
sessionID,
parentID: parent.id,
boundary: { type: input.before ? "before" : "through", messageID: boundary.id },
...(input.child ? { child: true } : {}),
...inherited,
})
return yield* result.get(sessionID).pipe(Effect.orDie)
@@ -410,6 +421,8 @@ const layer = Layer.effect(
Effect.provideService(LLMClient.Service, llm),
)
}),
subagent: (input) =>
SessionSubagent.spawn(result, subagents, input).pipe(Effect.provideService(Instance.Service, instances)),
command: Effect.fn("Session.command")(function* (input) {
const session = yield* result.get(input.sessionID)
return yield* SessionCommand.execute({ ...input, session }).pipe(
@@ -454,6 +467,7 @@ const layer = Layer.effect(
commit: (sessionID) => sessions.forSession(sessionID).revert.commit(),
},
})
const subagents = yield* SubagentJob.make.pipe(Effect.provideService(Service, result))
return result
}),
+1 -1
View File
@@ -148,7 +148,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.insert(SessionTable)
.values({
id: event.data.sessionID,
parent_id: null,
parent_id: event.data.child ? event.data.parentID : null,
fork_session_id: event.data.parentID,
fork_boundary: event.data.boundary,
project_id: parent.project_id,
@@ -36,7 +36,7 @@ export const deliver = Effect.fnUntraced(function* (
yield* sessions.synthetic({
...(input.notificationID ? { id: input.notificationID } : {}),
sessionID: recovery.parentSessionID,
...(input.resume === false ? { resume: false } : {}),
...((input.resume ?? recovery.resume) === false ? { resume: false } : {}),
description: recovery.description,
text: `<subagent sessionID="${recovery.childSessionID}" state="${input.status}" description="${recovery.description}">\n${text}\n</subagent>`,
metadata: { source: "subagent", childID: recovery.childSessionID, agent: recovery.agent, state: input.status },
+1 -1
View File
@@ -7,7 +7,7 @@ import { SubagentCompletion } from "./subagent-completion.js"
type Recovery = Extract<Job.Recovery, { kind: "subagent" }>
interface Runner {
export interface Runner {
start: (recovery: Recovery) => Effect.Effect<Job.Info>
background: (recovery: Recovery) => Effect.Effect<void>
notify: (recovery: Recovery, startedAt: number) => Effect.Effect<void>
+85
View File
@@ -0,0 +1,85 @@
export * as SessionSubagent from "./subagent.js"
import { Effect } from "effect"
import { Agent } from "../agent.js"
import { Instance } from "../instance/service.js"
import type { Model } from "../model.js"
import { Plugin } from "../plugin/service.js"
import type { Session } from "../session.js"
import { AgentNotFoundError } from "./error.js"
import type { SessionSchema } from "./schema.js"
import type { SubagentJob } from "./subagent-job.js"
const preamble = "You are a subagent spawned by another session."
export type Input = {
readonly sessionID: SessionSchema.ID
readonly text: string
readonly description: string
readonly agent?: Agent.ID
readonly model?: Model.Ref
/** Copies the parent's settled history into the child instead of starting with fresh context. */
readonly fork?: boolean
/** False admits the completion notice to the parent without resuming it. */
readonly resume?: boolean
}
/** Starts a background child Session whose outcome is delivered to the parent when it settles. */
export const spawn = Effect.fn("SessionSubagent.spawn")(function* (
sessions: Session.Interface,
subagents: SubagentJob.Runner,
input: Input,
) {
const instances = yield* Instance.Service
const parent = yield* sessions.get(input.sessionID)
const selected = yield* Plugin.awaitActivation.pipe(
Effect.andThen(Agent.Service),
Effect.flatMap((agents) => agents.select(input.agent ?? parent.agent)),
instances.provide(parent),
)
if (input.agent !== undefined && selected.info === undefined)
return yield* new AgentNotFoundError({ sessionID: parent.id, agent: input.agent })
const create = sessions.create({
parentID: parent.id,
title: input.description,
agent: selected.id,
model: input.model ?? selected.info?.model ?? parent.model,
})
const child = input.fork
? yield* sessions.fork({ sessionID: parent.id, child: true }).pipe(
Effect.tap((forked) => {
// A fork inherits the parent's agent and model; an explicit model wins over the switched agent's model.
const switched = forked.agent !== selected.id
const model = input.model ?? (switched ? selected.info?.model : undefined)
return Effect.all(
[
sessions.rename({ sessionID: forked.id, title: input.description }),
switched ? sessions.switchAgent({ sessionID: forked.id, agent: selected.id }) : Effect.void,
model === undefined ? Effect.void : sessions.switchModel({ sessionID: forked.id, model }),
],
{ discard: true },
)
}),
Effect.catchTag("Session.ForkEmptyError", () => create),
// Only a `before` boundary can be missing.
Effect.catchTag("Session.MessageNotFoundError", Effect.die),
)
: yield* create
// A text-only prompt without an explicit ID cannot fail admission on the Session just created.
yield* sessions
.prompt({ sessionID: child.id, text: [preamble, input.text].join("\n"), resume: false })
.pipe(Effect.orDie)
const recovery = {
kind: "subagent" as const,
parentSessionID: parent.id,
childSessionID: child.id,
agent: selected.id,
description: input.description,
...(input.resume === false ? { resume: false } : {}),
}
yield* subagents.start(recovery)
yield* subagents.background(recovery)
return yield* sessions.get(child.id)
})
+1
View File
@@ -173,6 +173,7 @@ export function host(overrides: Overrides = {}): Plugin.Context {
update: overrides.session?.update ?? (() => Effect.die("unused session.update")),
move: overrides.session?.move ?? (() => Effect.die("unused session.move")),
synthetic: overrides.session?.synthetic ?? (() => Effect.die("unused session.synthetic")),
subagent: overrides.session?.subagent ?? (() => Effect.die("unused session.subagent")),
interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")),
wait: overrides.session?.wait ?? (() => Effect.die("unused session.wait")),
context: overrides.session?.context ?? (() => Effect.die("unused session.context")),
+149
View File
@@ -0,0 +1,149 @@
import { describe, expect } from "bun:test"
import path from "path"
import { Effect, Layer, Stream } from "effect"
import { LanguageModel } from "@opencode/ai"
import { OpenAIChat } from "@opencode/ai/protocols/openai-chat"
import { TestLLM } from "@opencode/ai/testing"
import { Agent } from "@opencode/core/agent"
import { Bus } from "@opencode/core/bus"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
import { LayerNodePlatform } from "@opencode/core/effect/app-node-platform"
import { Watcher } from "@opencode/core/filesystem/watcher"
import { LocationServiceMap } from "@opencode/core/location-service-map"
import { Model } from "@opencode/core/model"
import { Provider } from "@opencode/core/provider"
import { AbsolutePath } from "@opencode/core/schema"
import { Session } from "@opencode/core/session"
import { SessionRunnerModel } from "@opencode/core/session/runner/model"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { Global } from "@opencode/util/global"
import { tempGlobalLayer } from "./fixture/global"
import { offlineModels } from "./fixture/models"
import { tmpdirScoped } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
const llmLayer = TestLLM.testLayer({ fallback: TestLLM.text("Docs updated", "docs") })
const it = testEffect(
Layer.merge(
llmLayer,
AppNodeBuilder.build(LayerNode.group([Session.node, LocationServiceMap.node]), [
Global.node.replace(tempGlobalLayer),
offlineModels,
Watcher.node.replace(Watcher.configured({ enabled: false })),
LayerNodePlatform.llmClient.replace(llmLayer),
SessionRunnerModel.node.replace(
Layer.succeed(SessionRunnerModel.Service, {
resolve: (session) =>
Effect.succeed(
SessionRunnerModel.resolved(
LanguageModel.make({ id: session.model?.id ?? "parent", provider: "test", route: OpenAIChat.route }),
{
capabilities: { tools: true, input: ["text"], output: ["text"] },
cost: [],
limit: { context: 200_000, output: 32_000 },
},
),
),
}),
),
]),
),
)
const parentModel = Model.Ref.make({ id: Model.ID.make("parent"), providerID: Provider.ID.make("test") })
describe("session subagents", () => {
it.live("forks settled history into a child and admits its outcome without resuming the parent", () =>
Effect.gen(function* () {
const parent = yield* project()
const sessions = yield* Session.Service
const llm = yield* TestLLM.Test
yield* sessions.prompt({ sessionID: parent.id, text: "Earlier question" })
yield* sessions.wait(parent.id)
const gate = yield* llm.gate()
// This must return while the child's model is still blocked.
const child = yield* sessions.subagent({
sessionID: parent.id,
text: "Update the docs",
description: "Update docs",
fork: true,
resume: false,
})
yield* gate.started
expect(child).toMatchObject({
parentID: parent.id,
fork: { sessionID: parent.id },
title: "Update docs",
agent: "build",
model: parentModel,
})
expect((yield* sessions.list({ parentID: parent.id })).data.map((session) => session.id)).toEqual([child.id])
expect(
(yield* sessions.context(child.id)).flatMap((message) => (message.type === "user" ? [message.text] : [])),
).toEqual(["Earlier question", "You are a subagent spawned by another session.\nUpdate the docs"])
yield* gate.release
const notice = yield* sessions.log({ sessionID: parent.id, follow: true }).pipe(
Stream.filter(
(event) =>
!Bus.isSynced(event) && event.type === "session.inbox.enqueued" && event.data.item.type === "synthetic",
),
Stream.runHead,
)
expect(notice).toMatchObject({
_tag: "Some",
value: { data: { item: { metadata: { source: "subagent", childID: child.id, state: "completed" } } } },
})
expect(yield* sessions.inbox(parent.id)).toMatchObject([{ type: "synthetic" }])
expect((yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic")).toEqual([])
expect(yield* llm.requests()).toHaveLength(2)
}),
)
it.live("creates a fresh child with the requested agent when there is no history to fork", () =>
Effect.gen(function* () {
const parent = yield* project()
const sessions = yield* Session.Service
const llm = yield* TestLLM.Test
const gate = yield* llm.gate()
const child = yield* sessions.subagent({
sessionID: parent.id,
text: "Review the changes",
description: "Review changes",
agent: Agent.ID.make("reviewer"),
fork: true,
})
yield* gate.started
expect(child).toMatchObject({ parentID: parent.id, agent: "reviewer", model: { id: "child" } })
expect(child.fork).toBeUndefined()
yield* gate.release
expect(
yield* sessions
.subagent({ sessionID: parent.id, text: "x", description: "x", agent: Agent.ID.make("missing") })
.pipe(Effect.flip),
).toBeInstanceOf(Session.AgentNotFoundError)
}),
)
})
function project() {
return Effect.gen(function* () {
const tmp = yield* tmpdirScoped()
yield* Effect.promise(() =>
Bun.write(
path.join(tmp.path, "opencode.json"),
JSON.stringify({ agents: { reviewer: { mode: "subagent", model: "test/child" } } }),
),
)
const sessions = yield* Session.Service
return yield* sessions.create({
location: { directory: AbsolutePath.make(tmp.path) },
title: "Parent session",
agent: Agent.ID.make("build"),
model: parentModel,
})
})
}
+1
View File
@@ -160,6 +160,7 @@ export type SessionDomain = Pick<
| "generate"
| "command"
| "synthetic"
| "subagent"
| "interrupt"
| "update"
| "move"
+1
View File
@@ -586,6 +586,7 @@ export function fromPromise(plugin: Plugin) {
generate: adaptApiMethod(SessionEndpoints["session.generate"], host.session.generate),
command: adaptApiMethod(SessionEndpoints["session.command"], host.session.command),
synthetic: adaptApiMethod(SessionEndpoints["session.synthetic"], host.session.synthetic),
subagent: adaptApiMethod(SessionEndpoints["session.subagent"], host.session.subagent),
interrupt: adaptApiMethod(SessionEndpoints["session.interrupt"], host.session.interrupt),
update: adaptApiMethod(SessionEndpoints["session.update"], host.session.update),
move: adaptApiMethod(SessionEndpoints["session.move"], host.session.move),
+1
View File
@@ -160,6 +160,7 @@ export type SessionDomain = Pick<
| "generate"
| "command"
| "synthetic"
| "subagent"
| "interrupt"
| "update"
| "move"
+25
View File
@@ -17,6 +17,7 @@ import { Event } from "@opencode/schema/event"
import { Context, Effect, Encoding, Result, Schema, SchemaGetter, Struct } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import {
AgentNotFoundError,
ConflictError,
CommandExecutionError,
CommandNotFoundError,
@@ -322,6 +323,30 @@ export const makeSessionGroup = <
}),
),
)
.add(
HttpApiEndpoint.post("session.subagent", "/api/session/:sessionID/subagent", {
params: { sessionID: Session.ID },
payload: Schema.Struct({
text: Schema.String,
description: Schema.String,
agent: Agent.ID.pipe(Schema.optional),
model: Model.Ref.pipe(Schema.optional),
fork: Schema.Boolean.pipe(Schema.optional),
resume: Schema.Boolean.pipe(Schema.optional),
}),
success: Schema.Struct({ data: PublicSessionInfo }),
error: [SessionNotFoundError, AgentNotFoundError],
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "session.subagent",
summary: "Start subagent",
description:
"Start a background subagent in a child session and deliver its outcome to this session when it settles. Set fork to copy this session's settled history into the child. Set resume to false to admit the outcome without resuming this session.",
}),
),
)
.add(
HttpApiEndpoint.post("session.switchAgent", "/api/session/:sessionID/agent", {
params: { sessionID: Session.ID },
+2
View File
@@ -197,6 +197,8 @@ export const Forked = Event.durable({
...Base,
parentID: SessionID,
boundary: SessionFork.Boundary,
/** The fork is a child of its source session, such as a forked subagent. */
child: Schema.Boolean.pipe(optional),
instructions: Instruction.Values.pipe(optional),
instructionEntries: InstructionEntry.Snapshot.pipe(optional),
},
+15
View File
@@ -9,6 +9,7 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
import { SessionsCursor } from "@opencode/protocol/groups/session"
import {
AgentNotFoundError,
ConflictError,
CommandExecutionError,
CommandNotFoundError,
@@ -234,6 +235,20 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
}
}),
)
.handle(
"session.subagent",
Effect.fn(function* (ctx) {
return {
data: yield* session.subagent({ sessionID: ctx.params.sessionID, ...ctx.payload }).pipe(
Effect.catchTag("Session.NotFoundError", missingSession),
Effect.catchTag(
"Session.AgentNotFoundError",
(error) => new AgentNotFoundError({ agentID: error.agent, message: error.message }),
),
),
}
}),
)
.handle(
"session.switchAgent",
Effect.fn(function* (ctx) {
@@ -766,6 +766,13 @@ const command = await ctx.session.command({ sessionID, command: "review", argume
const synthetic = await ctx.session.synthetic({ sessionID, text: "Deployment completed" })
```
Start a background subagent in a child session. Set `fork` to copy the session's settled history into the child. The
subagent's outcome is added to the parent when it settles; set `resume` to `false` to add it without resuming the parent.
```ts
const child = await ctx.session.subagent({ sessionID, text: "Update the docs", description: "Update docs", fork: true })
```
Rename, interrupt, or wait for a session.
```ts
@@ -790,6 +797,7 @@ interface SessionContext {
generate(input: SessionGenerateInput, requestOptions?: RequestOptions): Promise<{ text: string }>
command(input: SessionCommandInput, requestOptions?: RequestOptions): Promise<SessionInboxUser>
synthetic(input: SessionSyntheticInput, requestOptions?: RequestOptions): Promise<SessionInboxSynthetic>
subagent(input: SessionSubagentInput, requestOptions?: RequestOptions): Promise<SessionInfo>
interrupt(input: SessionInterruptInput, requestOptions?: RequestOptions): Promise<void>
rename(input: SessionRenameInput, requestOptions?: RequestOptions): Promise<void>
wait(input: SessionWaitInput, requestOptions?: RequestOptions): Promise<void>