mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
1b24ba912a
## Why
When a subagent exhausts its retries, it emits an `Error`, but the
generic task lifecycle then emits `TurnComplete(None)`. That completion
used to overwrite the subagent's `Errored` status with
`Completed(None)`, so the parent received an empty completion
notification.
This made a failed child look indistinguishable from a child that
completed without an answer. In unattended or long-running multi-agent
work, the root could silently continue without knowing that delegated
work failed or how to restart it.
## Behavior
Before, a terminal stream failure was reduced to an empty completion:
```text
<subagent_notification>
{"agent_path":"/root/worker","status":{"completed":null}}
</subagent_notification>
```
Now the parent receives the actual terminal error, bounded to 1,000
tokens, together with an actionable recovery hint:
```text
<subagent_notification>
{
"agent_path": "/root/worker",
"status": {
"errored": "stream disconnected before completion: stream closed before response.completed"
},
"next_action": "This agent's turn failed. If you still need this agent, use `followup_task` to give it another task."
}
</subagent_notification>
```
The notification remains queue-only: it does not wake the root or replay
the failed request. The root sees it at the next sampling boundary and
can use `followup_task` to start a new turn for that agent.
## What changed
- Added terminal-error precedence to the [agent status
reducer](https://github.com/openai/codex/blob/e95fcfe2bb6a02f1a75650afa20048859f556511/codex-rs/core/src/agent/status.rs#L23-L34),
so a closing `TurnComplete` cannot erase an immediately preceding
`Errored` status.
- Made MultiAgentV2 completion forwarding use the retained session
status instead of re-deriving `Completed(None)` from the final event.
- Extended the [subagent notification
fragment](https://github.com/openai/codex/blob/e95fcfe2bb6a02f1a75650afa20048859f556511/codex-rs/core/src/context/subagent_notification.rs#L6-L60)
with a `next_action` for terminal errors and a hard cap on model-visible
error text.
- Kept successful completions and interrupted turns unchanged.
## Verification
- Added a status-reducer test proving that `Errored` survives the
trailing `TurnComplete`.
- Added an integration test that exhausts a subagent's stream retries
and verifies the exact `agent_message` delivered to the parent,
including the error and `followup_task` guidance.
- Re-ran the existing successful-completion and interrupted-turn
notification tests.
59 lines
2.2 KiB
Rust
59 lines
2.2 KiB
Rust
use codex_protocol::AgentPath;
|
|
use codex_protocol::protocol::AgentStatus;
|
|
use codex_utils_output_truncation::TruncationPolicy;
|
|
use codex_utils_output_truncation::truncate_text;
|
|
|
|
use crate::context::ContextualUserFragment;
|
|
use crate::context::InterAgentCompletionMessage;
|
|
use crate::context::SubagentNotification;
|
|
|
|
const COMPLETION_MESSAGE_MAX_TOKENS: usize = 1_000;
|
|
const COMPLETION_MESSAGE_ENVELOPE_TOKEN_RESERVE: usize = 100;
|
|
const ERROR_MAX_TOKENS: usize =
|
|
COMPLETION_MESSAGE_MAX_TOKENS - COMPLETION_MESSAGE_ENVELOPE_TOKEN_RESERVE;
|
|
const ERROR_NEXT_ACTION: &str = "This agent's turn failed. If you still need this agent, use the available collaboration tools to give it another task.";
|
|
|
|
// Helpers for model-visible session state markers that are stored in user-role
|
|
// messages but are not user intent.
|
|
|
|
// TODO(jif) unify with structured schema
|
|
pub(crate) fn format_subagent_notification_message(
|
|
agent_reference: &str,
|
|
status: &AgentStatus,
|
|
) -> String {
|
|
SubagentNotification::new(agent_reference, status.clone()).render()
|
|
}
|
|
|
|
pub(crate) fn format_inter_agent_completion_message(
|
|
task_name: AgentPath,
|
|
sender: AgentPath,
|
|
status: &AgentStatus,
|
|
) -> Option<String> {
|
|
let payload = match status {
|
|
AgentStatus::Completed(Some(message)) => message.clone(),
|
|
AgentStatus::Completed(None) => String::new(),
|
|
AgentStatus::Errored(error) => {
|
|
let error = truncate_text(error, TruncationPolicy::Tokens(ERROR_MAX_TOKENS));
|
|
format!("Agent errored: {error}\n\n{ERROR_NEXT_ACTION}")
|
|
}
|
|
AgentStatus::Shutdown => "Agent shut down.".to_string(),
|
|
AgentStatus::NotFound => "Agent was not found.".to_string(),
|
|
AgentStatus::PendingInit | AgentStatus::Running | AgentStatus::Interrupted => return None,
|
|
};
|
|
Some(InterAgentCompletionMessage::new(task_name, sender, payload).render())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "session_prefix_tests.rs"]
|
|
mod tests;
|
|
|
|
pub(crate) fn format_subagent_context_line(
|
|
agent_reference: &str,
|
|
agent_nickname: Option<&str>,
|
|
) -> String {
|
|
match agent_nickname.filter(|nickname| !nickname.is_empty()) {
|
|
Some(agent_nickname) => format!("- {agent_reference}: {agent_nickname}"),
|
|
None => format!("- {agent_reference}"),
|
|
}
|
|
}
|