Compare commits

...

1 Commits

Author SHA1 Message Date
Francis Chalissery
be22c4072c Add command summary metadata for exec command lifecycle 2026-04-09 17:30:16 -07:00
40 changed files with 696 additions and 30 deletions

View File

@@ -792,6 +792,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"ConfigWarningNotification": {
"properties": {
"details": {
@@ -2739,6 +2756,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -6273,6 +6273,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"Config": {
"additionalProperties": true,
"properties": {
@@ -12921,6 +12938,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/v2/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -2921,6 +2921,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"Config": {
"additionalProperties": true,
"properties": {
@@ -10776,6 +10793,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -195,6 +195,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -664,6 +681,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -195,6 +195,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -664,6 +681,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -331,6 +331,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -807,6 +824,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -396,6 +396,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -1321,6 +1338,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -334,6 +334,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -1079,6 +1096,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -334,6 +334,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -1079,6 +1096,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -334,6 +334,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -1079,6 +1096,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -396,6 +396,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -1321,6 +1338,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -334,6 +334,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -1079,6 +1096,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -396,6 +396,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -1321,6 +1338,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -334,6 +334,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -1079,6 +1096,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -334,6 +334,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -1079,6 +1096,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -331,6 +331,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -807,6 +824,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -331,6 +331,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -807,6 +824,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -331,6 +331,23 @@
],
"type": "string"
},
"CommandSummary": {
"properties": {
"past": {
"description": "Past-tense prose summary, for example \"Pulled most recent code\".",
"type": "string"
},
"present": {
"description": "Present-tense prose summary, for example \"Pulling most recent code\".",
"type": "string"
}
},
"required": [
"past",
"present"
],
"type": "object"
},
"DynamicToolCallOutputContentItem": {
"oneOf": [
{
@@ -807,6 +824,17 @@
},
"type": "array"
},
"commandSummary": {
"anyOf": [
{
"$ref": "#/definitions/CommandSummary"
},
{
"type": "null"
}
],
"description": "Optional short prose summaries for non-technical surfaces."
},
"cwd": {
"description": "The command's working directory.",
"type": "string"

View File

@@ -0,0 +1,13 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CommandSummary = {
/**
* Present-tense prose summary, for example "Pulling most recent code".
*/
present: string,
/**
* Past-tense prose summary, for example "Pulled most recent code".
*/
past: string, };

View File

@@ -10,6 +10,7 @@ import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus";
import type { CommandAction } from "./CommandAction";
import type { CommandExecutionSource } from "./CommandExecutionSource";
import type { CommandExecutionStatus } from "./CommandExecutionStatus";
import type { CommandSummary } from "./CommandSummary";
import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
import type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
import type { FileUpdateChange } from "./FileUpdateChange";
@@ -44,15 +45,19 @@ commandActions: Array<CommandAction>,
/**
* The command's output, aggregated from stdout and stderr.
*/
aggregatedOutput: string | null,
aggregatedOutput: string | null,
/**
* The command's exit code.
*/
exitCode: number | null,
exitCode: number | null,
/**
* The duration of the command execution in milliseconds.
*/
durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array<FileUpdateChange>, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, result: McpToolCallResult | null, error: McpToolCallError | null,
durationMs: number | null,
/**
* Optional short prose summaries for non-technical surfaces.
*/
commandSummary: CommandSummary | null, } | { "type": "fileChange", id: string, changes: Array<FileUpdateChange>, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, result: McpToolCallResult | null, error: McpToolCallError | null,
/**
* The duration of the MCP tool call in milliseconds.
*/

View File

@@ -55,6 +55,7 @@ export type { CommandExecutionRequestApprovalParams } from "./CommandExecutionRe
export type { CommandExecutionRequestApprovalResponse } from "./CommandExecutionRequestApprovalResponse";
export type { CommandExecutionSource } from "./CommandExecutionSource";
export type { CommandExecutionStatus } from "./CommandExecutionStatus";
export type { CommandSummary } from "./CommandSummary";
export type { Config } from "./Config";
export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams";
export type { ConfigEdit } from "./ConfigEdit";

View File

@@ -82,6 +82,7 @@ pub fn build_command_execution_approval_request_item(
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
}
}
@@ -102,6 +103,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: payload.command_summary.clone().map(Into::into),
}
}
@@ -129,6 +131,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
aggregated_output,
exit_code: Some(payload.exit_code),
duration_ms: Some(duration_ms),
command_summary: payload.command_summary.clone().map(Into::into),
}
}
@@ -157,6 +160,7 @@ pub fn build_item_from_guardian_event(
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
})
}
GuardianAssessmentAction::Execve {
@@ -189,6 +193,7 @@ pub fn build_item_from_guardian_event(
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
})
}
GuardianAssessmentAction::ApplyPatch { .. }

View File

@@ -1791,6 +1791,7 @@ mod tests {
}],
source: ExecCommandSource::Agent,
interaction_input: None,
command_summary: None,
stdout: String::new(),
stderr: String::new(),
aggregated_output: "hello world\n".into(),
@@ -1844,6 +1845,7 @@ mod tests {
aggregated_output: Some("hello world\n".into()),
exit_code: Some(0),
duration_ms: Some(12),
command_summary: None,
}
);
assert_eq!(
@@ -2008,6 +2010,7 @@ mod tests {
parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }],
source: ExecCommandSource::Agent,
interaction_input: None,
command_summary: None,
stdout: String::new(),
stderr: "exec command rejected by user".into(),
aggregated_output: "exec command rejected by user".into(),
@@ -2056,6 +2059,7 @@ mod tests {
aggregated_output: Some("exec command rejected by user".into()),
exit_code: Some(-1),
duration_ms: Some(0),
command_summary: None,
}
);
assert_eq!(
@@ -2141,6 +2145,7 @@ mod tests {
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
}
);
}
@@ -2200,6 +2205,7 @@ mod tests {
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
}
);
}
@@ -2248,6 +2254,7 @@ mod tests {
}],
source: ExecCommandSource::Agent,
interaction_input: None,
command_summary: None,
stdout: "done\n".into(),
stderr: String::new(),
aggregated_output: "done\n".into(),
@@ -2289,6 +2296,7 @@ mod tests {
aggregated_output: Some("done\n".into()),
exit_code: Some(0),
duration_ms: Some(5),
command_summary: None,
}
);
}
@@ -2337,6 +2345,7 @@ mod tests {
}],
source: ExecCommandSource::Agent,
interaction_input: None,
command_summary: None,
stdout: "done\n".into(),
stderr: String::new(),
aggregated_output: "done\n".into(),

View File

@@ -4392,6 +4392,8 @@ pub enum ThreadItem {
/// The duration of the command execution in milliseconds.
#[ts(type = "number | null")]
duration_ms: Option<i64>,
/// Optional short prose summaries for non-technical surfaces.
command_summary: Option<CommandSummary>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
@@ -4920,6 +4922,25 @@ pub enum CommandExecutionStatus {
Declined,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct CommandSummary {
/// Present-tense prose summary, for example "Pulling most recent code".
pub present: String,
/// Past-tense prose summary, for example "Pulled most recent code".
pub past: String,
}
impl From<codex_protocol::protocol::CommandSummary> for CommandSummary {
fn from(value: codex_protocol::protocol::CommandSummary) -> Self {
Self {
present: value.present,
past: value.past,
}
}
}
impl From<CoreExecCommandStatus> for CommandExecutionStatus {
fn from(value: CoreExecCommandStatus) -> Self {
Self::from(&value)

View File

@@ -948,7 +948,7 @@ Today both notifications carry an empty `items` array even when item events were
- `agentMessage``{id, text}` containing the accumulated agent reply.
- `plan``{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental).
- `reasoning``{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
- `commandExecution``{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`.
- `commandExecution``{id, command, cwd, status, commandActions, commandSummary?, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`. `commandSummary`, when present, contains short `present` and `past` prose labels for non-technical surfaces.
- `fileChange``{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`.
- `mcpToolCall``{id, server, tool, status, arguments, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`.
- `collabToolCall``{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`.
@@ -987,7 +987,7 @@ There are additional item-specific events:
#### commandExecution
- `item/commandExecution/outputDelta` — streams stdout/stderr for the command; append deltas in order to render live output alongside `aggregatedOutput` in the final item.
Final `commandExecution` items include parsed `commandActions`, `status`, `exitCode`, and `durationMs` so the UI can summarize what ran and whether it succeeded.
Final `commandExecution` items include parsed `commandActions`, optional `commandSummary`, `status`, `exitCode`, and `durationMs` so the UI can summarize what ran and whether it succeeded.
#### fileChange

View File

@@ -1612,6 +1612,7 @@ pub(crate) async fn apply_bespoke_event_handling(
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: exec_command_begin_event.command_summary.map(Into::into),
};
let notification = ItemStartedNotification {
thread_id: conversation_id.to_string(),
@@ -1983,6 +1984,7 @@ async fn start_command_execution_item(
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
},
};
outgoing
@@ -2027,6 +2029,7 @@ async fn complete_command_execution_item(
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
};
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
@@ -3209,6 +3212,7 @@ mod tests {
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
}
);
}

View File

@@ -153,6 +153,7 @@ pub(crate) async fn execute_user_shell_command(
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
command_summary: None,
}),
)
.await;
@@ -221,6 +222,7 @@ pub(crate) async fn execute_user_shell_command(
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
command_summary: None,
stdout: String::new(),
stderr: aborted_message.clone(),
aggregated_output: aborted_message.clone(),
@@ -245,6 +247,7 @@ pub(crate) async fn execute_user_shell_command(
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
command_summary: None,
stdout: output.stdout.text.clone(),
stderr: output.stderr.text.clone(),
aggregated_output: output.aggregated_output.text.clone(),
@@ -289,6 +292,7 @@ pub(crate) async fn execute_user_shell_command(
parsed_cmd,
source: ExecCommandSource::UserShell,
interaction_input: None,
command_summary: None,
stdout: exec_output.stdout.text.clone(),
stderr: exec_output.stderr.text.clone(),
aggregated_output: exec_output.aggregated_output.text.clone(),

View File

@@ -7,6 +7,7 @@ use codex_protocol::error::CodexErr;
use codex_protocol::error::SandboxErr;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::CommandSummary;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandBeginEvent;
use codex_protocol::protocol::ExecCommandEndEvent;
@@ -61,27 +62,20 @@ pub(crate) enum ToolEventFailure {
Rejected(String),
}
pub(crate) async fn emit_exec_command_begin(
ctx: ToolEventCtx<'_>,
command: &[String],
cwd: &Path,
parsed_cmd: &[ParsedCommand],
source: ExecCommandSource,
interaction_input: Option<String>,
process_id: Option<&str>,
) {
async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: ExecCommandInput<'_>) {
ctx.session
.send_event(
ctx.turn,
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: ctx.call_id.to_string(),
process_id: process_id.map(str::to_owned),
process_id: exec_input.process_id.map(str::to_owned),
turn_id: ctx.turn.sub_id.clone(),
command: command.to_vec(),
cwd: cwd.to_path_buf(),
parsed_cmd: parsed_cmd.to_vec(),
source,
interaction_input,
command: exec_input.command.to_vec(),
cwd: exec_input.cwd.to_path_buf(),
parsed_cmd: exec_input.parsed_cmd.to_vec(),
source: exec_input.source,
interaction_input: exec_input.interaction_input.map(str::to_owned),
command_summary: exec_input.command_summary.cloned(),
}),
)
.await;
@@ -105,6 +99,7 @@ pub(crate) enum ToolEmitter {
source: ExecCommandSource,
parsed_cmd: Vec<ParsedCommand>,
process_id: Option<String>,
command_summary: Option<CommandSummary>,
},
}
@@ -137,6 +132,7 @@ impl ToolEmitter {
cwd: PathBuf,
source: ExecCommandSource,
process_id: Option<String>,
command_summary: Option<CommandSummary>,
) -> Self {
let parsed_cmd = parse_command(command);
Self::UnifiedExec {
@@ -145,6 +141,7 @@ impl ToolEmitter {
source,
parsed_cmd,
process_id,
command_summary,
}
}
@@ -169,6 +166,7 @@ impl ToolEmitter {
*source,
/*interaction_input*/ None,
/*process_id*/ None,
/*command_summary*/ None,
),
stage,
)
@@ -266,6 +264,7 @@ impl ToolEmitter {
source,
parsed_cmd,
process_id,
command_summary,
},
stage,
) => {
@@ -278,6 +277,7 @@ impl ToolEmitter {
*source,
/*interaction_input*/ None,
process_id.as_deref(),
command_summary.as_ref(),
),
stage,
)
@@ -370,6 +370,7 @@ struct ExecCommandInput<'a> {
source: ExecCommandSource,
interaction_input: Option<&'a str>,
process_id: Option<&'a str>,
command_summary: Option<&'a CommandSummary>,
}
impl<'a> ExecCommandInput<'a> {
@@ -380,6 +381,7 @@ impl<'a> ExecCommandInput<'a> {
source: ExecCommandSource,
interaction_input: Option<&'a str>,
process_id: Option<&'a str>,
command_summary: Option<&'a CommandSummary>,
) -> Self {
Self {
command,
@@ -388,6 +390,7 @@ impl<'a> ExecCommandInput<'a> {
source,
interaction_input,
process_id,
command_summary,
}
}
}
@@ -409,16 +412,7 @@ async fn emit_exec_stage(
) {
match stage {
ToolEventStage::Begin => {
emit_exec_command_begin(
ctx,
exec_input.command,
exec_input.cwd,
exec_input.parsed_cmd,
exec_input.source,
exec_input.interaction_input.map(str::to_owned),
exec_input.process_id,
)
.await;
emit_exec_command_begin(ctx, exec_input).await;
}
ToolEventStage::Success(output)
| ToolEventStage::Failure(ToolEventFailure::Output(output)) => {
@@ -483,6 +477,7 @@ async fn emit_exec_end(
parsed_cmd: exec_input.parsed_cmd.to_vec(),
source: exec_input.source,
interaction_input: exec_input.interaction_input.map(str::to_owned),
command_summary: exec_input.command_summary.cloned(),
stdout: exec_result.stdout,
stderr: exec_result.stderr,
aggregated_output: exec_result.aggregated_output,

View File

@@ -26,6 +26,7 @@ use codex_features::Feature;
use codex_otel::SessionTelemetry;
use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::CommandSummary;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::TerminalInteractionEvent;
use codex_shell_command::is_safe_command::is_known_safe_command;
@@ -59,6 +60,8 @@ pub(crate) struct ExecCommandArgs {
justification: Option<String>,
#[serde(default)]
prefix_rule: Option<Vec<String>>,
#[serde(default, rename = "commandSummary")]
command_summary: Option<CommandSummary>,
}
#[derive(Debug, Deserialize)]
@@ -217,6 +220,7 @@ impl ToolHandler for UnifiedExecHandler {
additional_permissions,
justification,
prefix_rule,
command_summary,
..
} = args;
@@ -325,6 +329,7 @@ impl ToolHandler for UnifiedExecHandler {
.permissions_preapproved,
justification,
prefix_rule,
command_summary,
},
&context,
)

View File

@@ -19,6 +19,7 @@ use crate::tools::events::ToolEventStage;
use crate::unified_exec::head_tail_buffer::HeadTailBuffer;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::exec_output::StreamOutput;
use codex_protocol::protocol::CommandSummary;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandOutputDeltaEvent;
use codex_protocol::protocol::ExecCommandSource;
@@ -114,6 +115,7 @@ pub(crate) fn spawn_exit_watcher(
process_id: i32,
transcript: Arc<Mutex<HeadTailBuffer>>,
started_at: Instant,
command_summary: Option<CommandSummary>,
) {
let exit_token = process.cancellation_token();
let output_drained = process.output_drained_notify();
@@ -134,6 +136,7 @@ pub(crate) fn spawn_exit_watcher(
transcript,
message,
duration,
command_summary,
)
.await;
} else {
@@ -149,6 +152,7 @@ pub(crate) fn spawn_exit_watcher(
String::new(),
exit_code,
duration,
command_summary,
)
.await;
}
@@ -202,6 +206,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
fallback_output: String,
exit_code: i32,
duration: Duration,
command_summary: Option<CommandSummary>,
) {
let aggregated_output = resolve_aggregated_output(&transcript, fallback_output).await;
let output = ExecToolCallOutput {
@@ -223,6 +228,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
cwd,
ExecCommandSource::UnifiedExecStartup,
process_id,
command_summary,
);
emitter
.emit(event_ctx, ToolEventStage::Success(output))
@@ -240,6 +246,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
transcript: Arc<Mutex<HeadTailBuffer>>,
message: String,
duration: Duration,
command_summary: Option<CommandSummary>,
) {
let stdout = resolve_aggregated_output(&transcript, String::new()).await;
let aggregated_output = if stdout.is_empty() {
@@ -266,6 +273,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
cwd,
ExecCommandSource::UnifiedExecStartup,
process_id,
command_summary,
);
emitter
.emit(

View File

@@ -29,6 +29,7 @@ use std::sync::Weak;
use codex_network_proxy::NetworkProxy;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::CommandSummary;
use codex_utils_absolute_path::AbsolutePathBuf;
use rand::Rng;
use rand::rng;
@@ -99,6 +100,7 @@ pub(crate) struct ExecCommandRequest {
pub additional_permissions_preapproved: bool,
pub justification: Option<String>,
pub prefix_rule: Option<Vec<String>>,
pub command_summary: Option<CommandSummary>,
}
#[derive(Debug)]

View File

@@ -192,6 +192,7 @@ impl UnifiedExecProcessManager {
cwd.to_path_buf(),
ExecCommandSource::UnifiedExecStartup,
Some(request.process_id.to_string()),
request.command_summary.clone(),
);
emitter.emit(event_ctx, ToolEventStage::Begin).await;
@@ -214,6 +215,7 @@ impl UnifiedExecProcessManager {
request.tty,
network_approval_id,
Arc::clone(&transcript),
request.command_summary.clone(),
)
.await;
}
@@ -260,6 +262,7 @@ impl UnifiedExecProcessManager {
Arc::clone(&transcript),
message.clone(),
wall_time,
request.command_summary.clone(),
)
.await;
}
@@ -304,6 +307,7 @@ impl UnifiedExecProcessManager {
text.clone(),
exit,
wall_time,
request.command_summary.clone(),
)
.await;
@@ -532,6 +536,7 @@ impl UnifiedExecProcessManager {
tty: bool,
network_approval_id: Option<String>,
transcript: Arc<tokio::sync::Mutex<HeadTailBuffer>>,
command_summary: Option<codex_protocol::protocol::CommandSummary>,
) {
let entry = ProcessEntry {
process: Arc::clone(&process),
@@ -576,6 +581,7 @@ impl UnifiedExecProcessManager {
process_id,
transcript,
started_at,
command_summary,
);
}

View File

@@ -2972,6 +2972,14 @@ pub enum ExecCommandStatus {
Declined,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct CommandSummary {
/// Present-tense prose summary, for example "Pulling most recent code".
pub present: String,
/// Past-tense prose summary, for example "Pulled most recent code".
pub past: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ExecCommandBeginEvent {
/// Identifier so this can be paired with the ExecCommandEnd event.
@@ -2994,6 +3002,10 @@ pub struct ExecCommandBeginEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub interaction_input: Option<String>,
/// Optional short prose summary of the command for non-technical surfaces.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub command_summary: Option<CommandSummary>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
@@ -3018,6 +3030,10 @@ pub struct ExecCommandEndEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub interaction_input: Option<String>,
/// Optional short prose summary of the command for non-technical surfaces.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub command_summary: Option<CommandSummary>,
/// Captured stdout
pub stdout: String,

View File

@@ -54,6 +54,33 @@ pub fn create_exec_command_tool(options: CommandToolOptions) -> ToolSpec {
"Maximum number of tokens to return. Excess output will be truncated.".to_string(),
)),
),
(
"commandSummary".to_string(),
JsonSchema {
description: Some(
"Optional short prose summaries for non-technical UI: present tense while running and past tense after completion, each 3-10 words.".to_string(),
),
..JsonSchema::object(
BTreeMap::from([
(
"present".to_string(),
JsonSchema::string(Some(
"Present-tense summary, e.g. \"Pulling most recent code\"."
.to_string(),
)),
),
(
"past".to_string(),
JsonSchema::string(Some(
"Past-tense summary, e.g. \"Pulled most recent code\".".to_string(),
)),
),
]),
Some(vec!["present".to_string(), "past".to_string()]),
Some(false.into()),
)
},
),
]);
if options.allow_login_shell {
properties.insert(

View File

@@ -897,6 +897,7 @@ fn command_execution_started_event(turn_id: &str, item: &ThreadItem) -> Option<V
process_id,
source,
command_actions,
command_summary,
..
} = item
else {
@@ -918,6 +919,12 @@ fn command_execution_started_event(turn_id: &str, item: &ThreadItem) -> Option<V
.collect(),
source: source.to_core(),
interaction_input: None,
command_summary: command_summary.clone().map(|summary| {
codex_protocol::protocol::CommandSummary {
present: summary.present,
past: summary.past,
}
}),
}),
}])
}
@@ -935,6 +942,7 @@ fn command_execution_completed_event(turn_id: &str, item: &ThreadItem) -> Option
aggregated_output,
exit_code,
duration_ms,
command_summary,
} = item
else {
return None;
@@ -978,6 +986,12 @@ fn command_execution_completed_event(turn_id: &str, item: &ThreadItem) -> Option
.collect(),
source: source.to_core(),
interaction_input: None,
command_summary: command_summary.clone().map(|summary| {
codex_protocol::protocol::CommandSummary {
present: summary.present,
past: summary.past,
}
}),
stdout: String::new(),
stderr: String::new(),
aggregated_output: aggregated_output.clone(),
@@ -1167,6 +1181,7 @@ mod tests {
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
};
let (_, started_events) = server_notification_thread_events(
@@ -1223,6 +1238,7 @@ mod tests {
aggregated_output: Some("hello world\n".to_string()),
exit_code: Some(0),
duration_ms: Some(5),
command_summary: None,
};
let (_, completed_events) = server_notification_thread_events(
ServerNotification::ItemCompleted(ItemCompletedNotification {
@@ -1258,6 +1274,7 @@ mod tests {
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
};
let events =
@@ -1308,6 +1325,7 @@ mod tests {
aggregated_output: Some("hello world\n".to_string()),
exit_code: Some(0),
duration_ms: Some(5),
command_summary: None,
}],
status: TurnStatus::Completed,
error: None,

View File

@@ -6121,6 +6121,7 @@ impl ChatWidget {
aggregated_output,
exit_code,
duration_ms,
command_summary,
} => {
if matches!(
status,
@@ -6138,6 +6139,12 @@ impl ChatWidget {
.collect(),
source: source.to_core(),
interaction_input: None,
command_summary: command_summary.map(|summary| {
codex_protocol::protocol::CommandSummary {
present: summary.present,
past: summary.past,
}
}),
});
} else {
let aggregated_output = aggregated_output.unwrap_or_default();
@@ -6153,6 +6160,12 @@ impl ChatWidget {
.collect(),
source: source.to_core(),
interaction_input: None,
command_summary: command_summary.map(|summary| {
codex_protocol::protocol::CommandSummary {
present: summary.present,
past: summary.past,
}
}),
stdout: String::new(),
stderr: String::new(),
aggregated_output: aggregated_output.clone(),
@@ -6712,6 +6725,7 @@ impl ChatWidget {
.collect(),
source: source.to_core(),
interaction_input: None,
command_summary: None,
});
}
ThreadItem::FileChange { id, changes, .. } => {

View File

@@ -201,6 +201,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
aggregated_output: None,
exit_code: None,
duration_ms: None,
command_summary: None,
},
}),
/*replay_kind*/ None,
@@ -222,6 +223,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
aggregated_output: Some("Hello, world!\n".to_string()),
exit_code: Some(0),
duration_ms: Some(5),
command_summary: None,
},
}),
/*replay_kind*/ None,

View File

@@ -367,6 +367,7 @@ async fn exec_end_without_begin_uses_event_command() {
parsed_cmd,
source: ExecCommandSource::Agent,
interaction_input: None,
command_summary: None,
stdout: "done".to_string(),
stderr: String::new(),
aggregated_output: "done".to_string(),

View File

@@ -489,6 +489,7 @@ pub(super) fn begin_exec_with_source(
parsed_cmd,
source,
interaction_input,
command_summary: None,
};
chat.handle_codex_event(Event {
id: call_id.to_string(),
@@ -514,6 +515,7 @@ pub(super) fn begin_unified_exec_startup(
parsed_cmd: Vec::new(),
source: ExecCommandSource::UnifiedExecStartup,
interaction_input: None,
command_summary: None,
};
chat.handle_codex_event(Event {
id: call_id.to_string(),
@@ -629,6 +631,7 @@ pub(super) fn end_exec(
source,
interaction_input,
process_id,
command_summary,
} = begin_event;
chat.handle_codex_event(Event {
id: call_id.clone(),
@@ -641,6 +644,7 @@ pub(super) fn end_exec(
parsed_cmd,
source,
interaction_input,
command_summary,
stdout: stdout.to_string(),
stderr: stderr.to_string(),
aggregated_output: aggregated.clone(),

View File

@@ -1462,6 +1462,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::Agent,
interaction_input: None,
command_summary: None,
}),
});
chat.handle_codex_event(Event {
@@ -1475,6 +1476,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
parsed_cmd,
source: ExecCommandSource::Agent,
interaction_input: None,
command_summary: None,
stdout: String::new(),
stderr: String::new(),
aggregated_output: String::new(),