import type { SessionTokenBudget } from '../../store/budget/index.js' import type { TaskId } from '../ids/index.js' import type { AgentPersona } from '../persona/index.js' import type { CancelCause } from '../session/cancel-cause.js' import type { ChildSessionLifecycleEvent, SessionEventListener } from '../session/events.js' import type { AgentRuntimeContext, BaseAgentConfig, BaseAgentResult } from './base.js' import type { AgentTaskState } from './task.js' export interface TaskHandle { readonly taskId: TaskId readonly agentId: string readonly state: AgentTaskState readonly result?: BaseAgentResult readonly createdAt: number readonly completedAt?: number } /** * What a failed child means for the siblings still running. * * `'continue'` — the default, and deliberately so. Partial results are * usually worth having, and tearing down healthy siblings on any failure * would let one flaky child waste four good ones. * * `'cancel-siblings'` — stop the rest. For a fan-out whose parts only mean * something together: if one leg of a comparison dies, the others are * spending budget on an answer nobody can use. */ export type SiblingFailurePolicy = 'continue' | 'cancel-siblings' export interface CreateTaskOptions { /** * Revalidate host authority at actual admission, including after a capacity wait. * Queue retries may invoke this more than once; checks must tolerate repeated calls. */ readonly beforeStart?: () => Promise /** * Observe events from this one delegated session when the scheduler can expose * them. * * The built-in local scheduler supports this without replacing its * scheduler-wide listener. Delivery is observational and must not * backpressure or decide the child session; a remote/custom scheduler may omit * it when that transport has no event stream. Callers must therefore settle * their task view from the returned {@link TaskHandle} as well. */ readonly onEvent?: SessionEventListener /** * Span the spawned run should hang off — normally the executing tool's * own span, so the delegation shows up inside the turn that asked for * it rather than as a disconnected root trace. */ readonly parentSpan?: import('@opentelemetry/api').Span /** Approved plan edge for live worker/progress correlation. */ readonly planId?: string readonly planStepId?: string /** * Display grouping for the delegated child, carried onto its * `agent_pending` event so a consumer watching from outside this process * can group the child the way this caller meant. Reach, not durability: * that event goes straight to a host's listener and enters no session log, * so nothing here is persisted by the kernel. See the `agent_pending` * variant in `types/session/events.ts` for the full contract. * * These fields are display annotations only; they do not create * dependencies, barriers, or serial execution. The kernel reads none of * them — a caller wanting correlation a host may act on has * {@link planId} and {@link planStepId} for that. */ readonly workflow?: string /** Stage within {@link workflow}. Display-only on the same terms. */ readonly phase?: string /** Longer text explaining {@link phase}. Display-only on the same terms. */ readonly phaseDetail?: string /** Zero-based DISPLAY order for {@link phase}. Display-only on the same terms. */ readonly phaseOrder?: number agentId: string /** * Tools this ONE delegation may not use, on top of whatever the child * would otherwise have. * * Deny-only, and the shape is the point: a per-call scope that could * ADD a tool is a privilege-escalation surface wearing the word * "scope". Widening has to be unexpressible, not merely discouraged — * a caller who wants a child to have more tools changes the agent's * definition, where somebody can see it. * * Naming a tool the child never had is a no-op, not an error: the * result is still narrower, and refusing would make a caller's deny * list depend on which agent it happened to be talking to. */ readonly toolScope?: { readonly deny: readonly string[] } /** * Replace the child's assembled persona for this delegation only. * * Scoped to the call rather than the agent, so a supervisor can hand * one subtask a narrower brief without redefining an agent every other * delegation shares. */ readonly personaOverride?: AgentPersona prompt: string workingDirectory: string runtimeContext?: AgentRuntimeContext /** * Config the spawned run should be built with, overriding what the * agent's own definition supplies — the model it runs on, its iteration * ceiling, its thinking or effort settings. * * **This was accepted and dropped.** `LocalTaskScheduler.createTask` built * its own `configOverrides` object out of `parentSpan` alone and never * read this field, so a caller pinning a delegated session to a cheaper model * got the agent's default model and no indication otherwise. It is * forwarded now, with the dedicated {@link parentSpan} option winning if * both name a span, since that one is the specific field for the job. * * Typed as `Partial` rather than * `Record`: this lands on `SendMessageOptions`, which is * already that shape, and the loose type let a misspelled key type-check * and then do nothing — the same silence this field was already producing. */ configOverrides?: Partial } export interface TaskScheduler { /** Authority under which this scheduler reserves and meters child execution. */ readonly budget?: SessionTokenBudget createTask(options: CreateTaskOptions): Promise waitForTask(taskId: TaskId): Promise continueTask(taskId: TaskId, message: string): Promise /** * Stop one task. A cause is relative to the child: a blocking delegation * abandoned by the turn that launched it is cancelled by its `parent`. * * Optional so existing host schedulers remain structurally compatible. A * scheduler that can preserve the cause should carry it to the task's abort * signal; built-in local and foreign-delegate schedulers do. */ cancelTask(taskId: TaskId, cause?: CancelCause): void /** * The task's current state, or `undefined` if this gateway does not know * about it. * * **A task that has just settled should still be findable here.** The * kernel uses this to recover one specific race: `createTask` resolves a * microtask before its caller can record whose the task is, so a worker * that finishes inside that window is announced to a listener that cannot * yet place it. `CompletionInbox` buffers the announcement AND asks this * method, and the second is what covers the case the buffer could not * hold. * * This is a request, not a requirement, and the cost of not meeting it is * yours: a gateway that forgets a task the instant it completes still * works, but under a burst large enough to overflow the buffer a fast * worker's result can go unannounced. `LocalTaskScheduler` meets it for as * long as the manager holds the record. */ getTask(taskId: TaskId): TaskHandle | undefined listTasks(): TaskHandle[] onTaskCompleted(callback: (handle: TaskHandle) => void): () => void /** * Tell me when a task does something, not just when it finishes. * * This is what an idle bound is measured against. A wall clock says * nothing about whether a worker is working: an hour is long enough to * be useless as a stall detector, and short enough to kill a child that * is making steady progress at minute fifty-nine. Time-without-progress * is the quantity that separates "stuck" from "slow", and only the * gateway can see it. * * OPTIONAL, and the absence is meaningful rather than an oversight: a * gateway that cannot observe its children still works, and its waits * are bounded by the wall clock alone — which is exactly the behaviour * before this existed. It is optional because `TaskScheduler` is * implemented by hosts, and a required method would break every one of * them for a capability not all of them can provide. * * Anything the worker did counts: a tool call, an emitted token, a state * change. What must NOT count is the supervisor's own activity — the * point is to notice a child that has gone quiet, and a parent polling * about it is not the child speaking. */ onTaskProgress?(callback: (taskId: TaskId) => void): () => void /** * Tell me when a child session this gateway launched starts, messages or * goes idle, as the `child_session_*` events its manager emits. * * The parent turn records these into its own session log — the * `child_session_spawned` record, and `child_session_ended` when the * child goes idle — so a finished delegation can be listed and replayed * from the parent's log after the process is gone. A host listener given * to the gateway sees the same events for display; this is the channel * the kernel reads them on. * * OPTIONAL for the same reason as {@link onTaskProgress}: a host gateway * that cannot observe its children still works, and its parent log simply * names no children. */ onChildSessionEvent?(callback: (event: ChildSessionLifecycleEvent) => void): () => void }