feat(agent): add durable agent invocations and spawning - #1065
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
2f3b985 to
b3aea1f
Compare
|
Review exercise result: the current recovery path still has one merge blocker. An empty resume wake sets |
Empty resume wakes never become final delivery attempts, so stranded running children with no model or resumable boundary must fail their session immediately and project onto the invocation instead of throwing and requeueing forever. Co-Authored-By: David Cramer <david@sentry.io>
Resolve conflicts with current main while keeping spawn/agent-invocation wiring, provider conversation bindings, scheduled-task heartbeats, and destination-aware slack work. Renumber the agent invocation schema migration to 0020_sour_vargas after main's newer conversation history and reporting migrations. Co-Authored-By: David Cramer <david@sentry.io>
|
Addressed the stranded-invocation merge blocker and brought the branch current with Fix
Main integration
Local integration verification could not run here (no Docker/Postgres). Relying on CI for |
Main renamed juniorToolResultSchema to juniorToolOutputSchema and dropped ok/status envelope fields. Update spawnAgent and its unit expectations, and pass the required Slack destination after requireSlackDestination. Co-Authored-By: David Cramer <david@sentry.io>
Main's local Source requires visibility and dropped type. Align agent runner and retention fixtures, and expect one more core migration after 0020_sour_vargas.
Bring the branch current with main so the agent-invocation schema can be regenerated after 0020_restrict_task_execution_kinds. Co-Authored-By: David Cramer <david@sentry.io>
Main claimed 0020_restrict_task_execution_kinds. Drop the stale 0020_sour_vargas agent migration and regenerate as 0021_living_groot with a snapshot chain rooted on main's latest migration. Co-Authored-By: David Cramer <david@sentry.io>
|
Refreshed the branch against current Migration
Note CI should re-run on this push. |
There was a problem hiding this comment.
Design review of the subagent model, not just the machinery.
The durable spine is in good shape: named binding → child conversation + history, invocation → one retry-safe task + authority snapshot + terminal result, mailbox/lease/session reuse, stranded recovery, retention. That part matches Junior's conversation runtime well.
The gap is the product identity of a subagent. Right now a "named agent" is really:
- a stable child conversation id
- optional sticky
reasoningLevel - busy-locking across invocations
That is thinner than both the Codex-shaped mental model and #880 (spawnAgent for named/ephemeral agents with optional model/reasoning profiles, plus result subscription / getAgentResult).
1. Agent policy is half-defined
spawnAgent accepts task + optional name + optional reasoning_level. Bindings persist only reasoning. Model profile is chosen later by the shared runner (standard → route/handoff) and lives on conversation projection / turn route / session — never on binding or invocation.
So at run time a child always has profile+reasoning, but only reasoning is agent policy. If named agents are persistent child agents, sticky policy should be coherent: either binding owns full execution policy (modelProfile + reasoningLevel), or binding is only a history handle and reasoning should not be special-cased there either. The current split makes "what is this agent?" hard to answer.
Related inconsistency: children still get handoff. A child can change model mid-run, and profile handoff can override reasoning via profile_reasoning_override, while the binding continues to claim a sticky reasoning policy. Pick one: pin child execution policy, or treat profile/reasoning as fully runtime-owned.
2. Model-facing spawn is incomplete as a subagent API
This slice returns only { invocation_id }. There is no parent mailbox injection, no auto-subscribe on terminal result, and no getAgentResult. The invocation stores a terminal result, but nothing closes the loop to the parent turn.
That means the exposed tool teaches the parent a fire-and-forget contract. In a Codex-like subagent flow, spawn without a result path is not yet a usable subagent — it's durable background work with an opaque handle. Either:
- keep this PR as #879 runtime-only and don't expose
spawnAgentto the model yet, or - land the minimal result path (
getAgentResultand/or parent terminal notification) before calling the model surface done.
Shipping spawn alone will create parent agents that "successfully" delegate and then never observe the work.
3. Named agent continuity is transcript-only
Reusing a name reuses history. It does not establish:
- a model profile
- agent instructions / role
- tool policy beyond "no recursive spawn"
- workspace/artifact isolation beyond whatever the shared child state happens to accumulate
- a stable authority identity (actor/credentials/destination are snapshotted per invocation, not per binding)
That may be fine for v1, but then docs/tool copy should say named persistent child conversation, not imply a full agent config object. Codex-style "researcher/implementer with its own model" is not what this implements yet.
4. Child capability surface is "almost parent"
Child runs through the same AgentRunner with:
agentSpawning: "disabled"✅authorizationFlowMode: "disabled"✅- destinationless conversation ✅
- otherwise normal tools, including
handoff
If subagents are constrained workers, this is too open. If they are full peer agents with continuity, missing policy/result wiring is the problem. Either framing is defensible; the code currently sits between them.
5. Smaller concrete follow-through
- Local CLI wires spawn + in-process worker but not the mailbox-append heartbeat repair path production uses. A crash between create and append can leave a named agent busy with no local repair.
- Terminology/docs should explicitly define: binding = named child identity (+ policy if we keep it), invocation = one task attempt, session = resumable execution including resolved model. Right now README says binding owns "reasoning policy" while #880 talks about model/reasoning profiles — align those before merge so the next slice doesn't invent a third shape.
What I'd lock before merge
- Decide the noun: is a named agent (a) history handle, or (b) sticky execution policy + history? Implement policy fields accordingly (at least stop the reasoning-only special case unless we commit to it).
- Decide the model surface for this PR: runtime-only proof, or spawn+result. Avoid shipping spawn without any parent-visible completion path.
- Decide child autonomy: keep/remove
handoffon children; if sticky reasoning stays, handoff must not silently override binding policy.
The execution/recovery work is real and mostly right. I would not merge the model-facing half until the subagent identity and result contract are intentional rather than residual from the slice cut.
Named and unnamed children are the same work shape; a name only keeps the child conversation id so later inputs continue history. Drop sticky reasoning from bindings, keep per-invocation reasoning, and pin child runs so they cannot hand off or otherwise override execution policy. Co-Authored-By: David Cramer <david@sentry.io>
|
Addressed the design decisions from review: Named vs unnamed
Child policy is fixed
Schema
Local |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bb5365c. Configure here.
The new assertion was creating another named researcher invocation while the previous one was still non-terminal, so it hit busy instead of validating per-task reasoning on the reused child.

Adds durable agent invocations and a
spawnAgenttool so a parent agent can schedule named or one-off child work through Junior's existing conversation mailbox and lease worker.Named and unnamed children are the same work shape. A name only keeps the same child conversation id so later inputs continue that child's history. Unnamed invocations get an invocation-scoped child. Different named agents can run concurrently, repeated tool calls are idempotent, overlapping work for one name is rejected, and one child's failure does not affect its siblings.
Child execution policy
Child runs keep fixed policy for the invocation: no recursive spawn, no model handoff, and no adaptive profile override. Optional
reasoning_levelis per-invocation only; bindings do not store sticky reasoning or model profile. Model selection stays on the shared runner/session path under that fixed policy.The model supplies only the task, optional name, and optional reasoning level. The runtime keeps parent identity, actor, credentials, routing, and idempotency bound to the active run, and the tool returns only a durable invocation handle. Child creation shares the root conversation mutation lock with retention purge, and bindings and invocations are deleted with the conversation tree.
Child work uses the existing provider-neutral mailbox worker. Local chat processes child wakes in-process, while production continues to use Vercel Queues. The schema change is migration
0021_first_warhawkafter main's0020_restrict_task_execution_kinds.Unrecoverable stranded running children (missing model or no resumable boundary) fail the session immediately and project onto the invocation, so empty resume wakes cannot requeue forever or permanently occupy a named agent.
SQL-backed regression coverage exercises isolated and named agents, history reuse with per-task reasoning, concurrent replay, overlap rejection, sibling failure isolation, validation failure, recovery (including unrecoverable stranded sessions), and retention cleanup. This slice returns invocation handles; parent result delivery, deterministic result recovery, cancellation, and recursive children remain follow-up work on #880.
Fixes #879
Refs #880