Subagent Parent Questions
May 20, 2026 ยท View on GitHub
Design note for adding a structured child-to-parent clarification path for Lemon subagents.
Last reviewed: 2026-05-15 Owner: @z80
Summary
Lemon already has strong parent-to-child control:
- parent/child run lineage in
CodingAgent.RunGraph - async task lifecycle and followup routing in
CodingAgent.Tools.Task - deferred routing via
CodingAgent.Session.follow_up/2
What Lemon does not have today is a structured reverse path where a spawned subagent can pause, ask its parent for a clarification or decision, and then resume execution after the parent answers.
This document proposes a narrow ask_parent tool for subagents plus a small parent-facing parent_question tool for resolving those requests. The goal is to preserve bounded delegation while giving children a safe escalation path for decisions that cannot be resolved locally.
Problem
Today a subagent has two unsatisfying options when it needs clarification:
- guess and continue
- stop early and return a partial result asking the parent to rerun or manually continue
Both are suboptimal.
Guessing hurts correctness. Returning early hurts orchestration because the parent must translate a partial result back into a new child run instead of answering a structured question and letting the same child continue.
At the same time, a general-purpose bidirectional chat channel between child and parent would be a mistake. It would encourage prompt ping-pong, reduce the value of delegation, and complicate lineage, telemetry, and failure handling.
The design target is therefore:
- narrow, structured escalation
- explicit pause/resume semantics
- visible lineage and telemetry
- minimal changes to the current task/session architecture
Existing Relevant Architecture
The current codebase already provides most of the primitives needed for this feature.
Existing capabilities
CodingAgent.Tools.Task.Executioncreates child task context includingrun_id,task_id,parent_run_id,session_key, andagent_id.CodingAgent.Tools.Task.Asyncemits task lifecycle events toLemonCore.BusandLemonCore.Introspection, and already broadcasts to both child and parent run topics.CodingAgent.Tools.Task.Followupcan route async completion text back into the live parent session or through router fallback.CodingAgent.Session.follow_up/2can queue a message into the parent session.CodingAgent.Sessionalready acceptsextra_tools, so child-only tooling can be injected without exposing it globally.CodingAgent.TaskStoreandCodingAgent.RunGraphalready persist bounded task/run state with DETS-backed recovery.
Relevant files
apps/coding_agent/lib/coding_agent/tools/task/execution.exapps/coding_agent/lib/coding_agent/tools/task/async.exapps/coding_agent/lib/coding_agent/tools/task/followup.exapps/coding_agent/lib/coding_agent/tools/task/params.exapps/coding_agent/lib/coding_agent/session.exapps/coding_agent/lib/coding_agent/session/state.exapps/coding_agent/lib/coding_agent/task_store.exapps/coding_agent/lib/coding_agent/run_graph.exapps/coding_agent/lib/coding_agent/tools.exapps/coding_agent/lib/coding_agent/tool_registry.ex
Goals
- Let a spawned subagent ask its parent a structured clarification question.
- Preserve the child run instead of forcing a new run after the answer arrives.
- Keep the mechanism narrow enough that it is used for decisions, not casual conversation.
- Expose request/answer lifecycle through the same bus and introspection patterns already used for task events.
- Keep Phase 1 limited to in-process parent/child task sessions.
Non-Goals
- full free-form parent/child chat
- arbitrary child-to-parent tool execution
- multi-hop conversation trees between sibling subagents
- cross-node or remote-agent support in Phase 1
- UI-first workflow before runtime semantics are stable
Proposed Tool
Add a built-in tool named ask_parent.
This tool should only be available inside subagent sessions that were launched with a live parent context. It should not be part of the default global tool set for normal top-level sessions.
Parameters
{
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "Concrete question for the parent."
},
"why_blocked": {
"type": "string",
"description": "Why the subagent cannot safely proceed without input."
},
"options": {
"type": "array",
"items": { "type": "string" },
"description": "Optional mutually exclusive choices the parent can pick from."
},
"recommended_option": {
"type": "string",
"description": "Optional recommended option from the child."
},
"can_continue_without_answer": {
"type": "boolean",
"description": "Whether the child may continue with a fallback if the parent does not answer."
},
"fallback": {
"type": "string",
"description": "What the child will do if the timeout is reached and continuation is allowed."
},
"timeout_ms": {
"type": "integer",
"description": "How long to wait for the parent answer before timing out."
}
},
"required": ["question", "why_blocked"]
}
Tool result
The tool should return a normal tool result whose visible text is short and operational:
Parent question submitted as request <request_id>. Waiting for answer.
details should include:
request_idstatusparent_run_idchild_run_idtask_idtimed_outanswered
Runtime Model
High-level flow
- A child session calls
ask_parent. - Lemon validates that the session has a live parent context and no open parent-question request for the same child run.
- Lemon persists a parent-question request record.
- Lemon emits
:parent_question_requestedto child and parent run topics and records introspection. - Lemon sends a structured follow-up message into the parent session.
- The child blocks waiting for an answer or timeout.
- The parent answers through a resolver path.
- Lemon persists the answer, emits
:parent_question_answered, and wakes the waitingask_parenttool execution. - The
ask_parenttool returns the parent answer as a normal tool result and the child continues.
Why the answer returns as a tool result
The simplest Phase 1 path is to have ask_parent wait and then return the answer as its own tool result:
- the child run is preserved
- the parent answer becomes explicit tool-result context for the child model
- no separate child resume protocol is needed
- the mechanism works for both sync and async child sessions
The returned tool text should be explicit and machine-friendly, for example:
Parent answer for request <request_id>:
Answer: Keep the current session model.
Rationale: Avoid widening the auth surface in this change.
New Persistent State
Add a new store module, likely CodingAgent.ParentQuestions, modeled after CodingAgent.TaskStore.
Record shape
%{
id: request_id,
status: :waiting | :answered | :timed_out | :cancelled | :error,
inserted_at: integer(),
updated_at: integer(),
parent_run_id: String.t(),
child_run_id: String.t(),
task_id: String.t() | nil,
parent_session_key: String.t() | nil,
parent_agent_id: String.t() | nil,
child_session_id: String.t() | nil,
child_session_pid: pid() | nil,
question: String.t(),
why_blocked: String.t(),
options: [String.t()],
recommended_option: String.t() | nil,
can_continue_without_answer: boolean(),
fallback: String.t() | nil,
timeout_ms: non_neg_integer() | nil,
answer: String.t() | nil,
answered_at: integer() | nil,
meta: map()
}
Events should be bounded the same way TaskStore events are bounded.
Why a dedicated store
This feature has state distinct from task completion:
- open request lifecycle
- answer payload
- timeout and resume semantics
- one-open-question-per-child-run enforcement
Overloading TaskStore would make polling and retention behavior harder to reason about.
Parent-Side Delivery
Phase 1 should deliver the question into the parent using the existing live-session path.
Parent delivery format
Use CodingAgent.Session.follow_up/2 on the parent session with a stable, structured message:
[subagent question <request_id>]
Child task: <description>
Blocked because: <why_blocked>
Question: <question>
Options:
- ...
Recommended: <recommended_option>
Use the `parent_question` tool to resolve request <request_id>.
This keeps the existing parent experience intact while preserving enough structure for future UI or control-plane affordances.
Resolver path
Phase 1 should expose a minimal parent-facing tool:
parent_questionwithaction="list"andaction="answer"
Under the hood that tool resolves requests through:
CodingAgent.ParentQuestions.answer(request_id, answer_text, opts \\ [])
Useful future surfaces:
- control-plane RPC method for resolving a request
- TUI/web affordance for answering from a request list
Child-Side Waiting Semantics
The ask_parent tool itself should block until one of these occurs:
- parent answer received
- timeout reached and
can_continue_without_answer == true - timeout reached and continuation is not allowed
- parent session disappears or cannot be reached
Success path
If answered:
- mark request
:answered - return an
AgentToolResultcontaining the parent answer so the tool call is part of normal child history
Timeout path
If timed out and continuation is allowed:
- mark request
:timed_out - return a non-error tool result with
status: "timed_out"
If timed out and continuation is not allowed:
- mark request
:timed_out - return an error result so the child can surface the block clearly
Parent unavailable
If parent session context is missing or dead:
- do not silently downgrade to best effort
- fail immediately with an explicit error
This should remain a true escalation primitive, not hidden retry behavior.
Event Model
Add new lifecycle events parallel to existing task lifecycle events.
Proposed event types
:parent_question_requested:parent_question_answered:parent_question_timed_out:parent_question_cancelled:parent_question_error
Event payload base
%{
request_id: request_id,
parent_run_id: parent_run_id,
child_run_id: child_run_id,
task_id: task_id,
session_key: parent_session_key,
agent_id: parent_agent_id,
question: question,
why_blocked: why_blocked,
options: options,
recommended_option: recommended_option,
can_continue_without_answer: can_continue_without_answer,
timeout_ms: timeout_ms,
meta: meta
}
Broadcast strategy should mirror task lifecycle broadcasting:
run:<child_run_id>run:<parent_run_id>when different
Introspection should also capture these events with run_id, parent_run_id, session_key, and agent_id.
Tool Availability
ask_parent should not be universally registered.
Phase 1 availability rule:
- inject as an
extra_toolonly whenCodingAgent.Tools.Tasklaunches an internal child session - require
parent_run_id - require live parent session context
- require a parent session module that exports
follow_up/2
The parent-side resolver tool can live in the default toolset because it is inert unless there are open requests for the current session.
This is the smallest safe scope and fits the existing extra_tools path already supported by CodingAgent.Session.
Concrete injection point
The natural place to add the tool is during child session option construction in:
CodingAgent.Tools.Task.Params.build_session_opts/3
That function already composes child-specific session options and can append extra_tools for child runs.
API Sketch
New store module
defmodule CodingAgent.ParentQuestions do
@spec new_request(map()) :: String.t()
@spec get(String.t()) :: {:ok, map(), [term()]} | {:error, :not_found}
@spec append_event(String.t(), term()) :: :ok
@spec mark_answered(String.t(), String.t(), map()) :: :ok
@spec mark_timed_out(String.t()) :: :ok
@spec mark_cancelled(String.t(), term()) :: :ok
@spec open_request_for_child_run(String.t()) :: {:ok, map()} | {:error, :not_found}
@spec answer(String.t(), String.t(), keyword()) :: :ok | {:error, term()}
end
New tool
defmodule CodingAgent.Tools.AskParent do
@spec tool(String.t(), keyword()) :: AgentCore.Types.AgentTool.t()
end
New runtime helper
defmodule CodingAgent.ParentQuestionCoordinator do
@spec request(map(), keyword()) :: AgentCore.Types.AgentToolResult.t() | {:error, term()}
@spec answer(String.t(), String.t(), keyword()) :: :ok | {:error, term()}
end
The coordinator layer is optional but likely useful for keeping store, bus, and session interactions out of the raw tool module.
Suggested Phase 1 Implementation Steps
- Add
CodingAgent.ParentQuestionsplus its owning server if separate ETS/DETS ownership is needed. - Add lifecycle event emission helpers parallel to
CodingAgent.Tools.Task.Async. - Add
CodingAgent.Tools.AskParent. - Inject the tool into eligible internal child sessions using
extra_tools. - Add internal answer API.
- Wire answer handling to
Session.steer/2for the child. - Add tests for request, answer, timeout, and missing-parent behavior.
- Add control-plane/UI affordances only after runtime semantics are stable.
Guardrails
The design only works if the feature stays narrow.
Required guardrails
- one open parent-question request per child run
- reject duplicate or near-duplicate open questions from the same child
- timeout required or defaulted
- no generic free-form conversation primitive
- no sibling-to-sibling communication path
- parent answers are text decisions, not executable actions
- every request and answer emits telemetry and introspection
Prompt guidance
The tool description should instruct the model to use ask_parent only when:
- a product or architectural decision is needed
- the child lacks authority to choose between options
- continuing without a decision would likely cause rework or correctness risk
The tool description should explicitly discourage use for:
- exploratory file discovery
- routine implementation choices
- status updates
- narrative discussion
Failure Modes and Recovery
Child asks too often
Risk:
- models may overuse the tool and collapse parallelism
Mitigation:
- strong tool description
- one-open-question limit
- follow-up evals that penalize unnecessary escalation
Parent never answers
Risk:
- child hangs indefinitely
Mitigation:
- explicit timeout
- optional fallback
- timeout event emission
Parent dies mid-request
Risk:
- request is left in limbo
Mitigation:
- monitor parent session pid when available
- convert to
:erroror:timed_outinstead of waiting forever
Child session dies before answer arrives
Risk:
- late answer cannot be delivered
Mitigation:
- mark request cancelled or errored
- retain record for diagnosis
Testing Plan
Add tests for at least the following:
- child creates a request and parent receives it
- parent answer resumes the same child run
- timeout with
can_continue_without_answer: true - timeout with
can_continue_without_answer: false - missing parent context
- parent session exits before answering
- duplicate open question rejection
- event broadcast to both child and parent run topics
- introspection records for request and answer
- tool is absent from top-level sessions and present only in eligible child sessions
Likely test areas:
apps/coding_agent/test/coding_agent/tools/apps/coding_agent/test/coding_agent/subagent_integration_test.exsapps/coding_agent/test/coding_agent/tools/task_async_test.exsapps/coding_agent/test/coding_agent/introspection_test.exs
Open Questions
These questions should not block Phase 1:
- Should the parent resolve requests through a dedicated tool, a control-plane method, or both?
- Should a child be able to continue doing local analysis while waiting, or should
ask_parentalways be a hard block? - Should parent-question requests appear in
TaskStoresummaries, or remain a separate view? - Do we want router-mediated delivery for non-live parent sessions in a later phase?
Recommended Phase 1 Decision
Implement ask_parent as a subagent-only extra tool for internal task sessions, backed by a dedicated ParentQuestions store and resolved by returning the parent answer as the ask_parent tool result after the parent answers through parent_question.
This uses Lemon's current architecture well:
- existing lineage from
RunGraph - existing live messaging from
Session.follow_up/2 - existing task lifecycle broadcasting patterns
- existing child-only tool injection through
extra_tools
It adds the missing reverse escalation path without turning subagents into general-purpose chat participants.