mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
Encrypt multi-agent v2 message payloads (#26210)
## Why
Multi-agent v2 currently routes agent instructions through normal tool
arguments and inter-agent context. That means the parent model can emit
plaintext task text, Codex can persist it in history/rollouts, and the
recipient can receive it as ordinary assistant-message JSON.
This changes the v2 path so agent instructions stay encrypted between
model calls: Responses encrypts the `message` argument returned by the
model, Codex forwards only that ciphertext, and Responses decrypts it
internally for the recipient model.
## What changed
- Mark the v2 `message` parameter as encrypted for `spawn_agent`,
`send_message`, and `followup_task`.
- Treat multi-agent v2 tool `message` values as ciphertext
unconditionally.
- Store v2 inter-agent task text in
`InterAgentCommunication.encrypted_content` with empty plaintext
`content`.
- Convert encrypted inter-agent communications into the Responses
`agent_message` input item before sending the child request.
- Preserve `agent_message` items across history, rollout, compaction,
telemetry, and app-server schema paths.
- Leave multi-agent v1 unchanged.
## Message shape
The model still calls the v2 tools with a `message` argument, but that
value is now ciphertext:
```json
{
"name": "spawn_agent",
"arguments": {
"task_name": "worker",
"message": "<ciphertext>"
}
}
```
Codex stores the task as encrypted inter-agent communication:
```json
{
"author": "/root",
"recipient": "/root/worker",
"content": "",
"encrypted_content": "<ciphertext>",
"trigger_turn": true
}
```
When Codex builds the recipient request, it forwards the ciphertext
using the new Responses input item:
```json
{
"type": "agent_message",
"author": "/root",
"recipient": "/root/worker",
"content": [
{
"type": "encrypted_content",
"encrypted_content": "<ciphertext>"
}
]
}
```
Responses decrypts that item internally for the recipient model.
## Context impact
- Parent context no longer carries plaintext v2 agent task instructions
from these tool arguments.
- Codex rollout/history stores ciphertext for v2 agent instructions.
- Recipient requests receive an `agent_message` item instead of
assistant commentary JSON for encrypted task delivery.
- Plaintext completion/status notifications are still plaintext because
they are Codex-generated status messages, not encrypted model tool
arguments.
## Validation
- `just test -p codex-tools`
- `just test -p codex-protocol`
- `just test -p codex-rollout`
- `just test -p codex-rollout-trace`
- `just test -p codex-otel`
- `just write-app-server-schema`
This commit is contained in:
34 files changed
+674
-59
No files matched your search
@@ -34,6 +34,30 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"AgentMessageInputContent": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"encrypted_content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"encrypted_content"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContentType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"encrypted_content",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContent",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"ApprovalsReviewer": {
|
"ApprovalsReviewer": {
|
||||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
|
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -2120,6 +2144,37 @@
|
|||||||
"title": "MessageResponseItem",
|
"title": "MessageResponseItem",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/AgentMessageInputContent"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"agent_message"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItemType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"author",
|
||||||
|
"content",
|
||||||
|
"recipient",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItem",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
|
|||||||
+3
-3
@@ -288,8 +288,8 @@
|
|||||||
"environmentId": {
|
"environmentId": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"type": [
|
"type": [
|
||||||
"null",
|
"string",
|
||||||
"string"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"itemId": {
|
"itemId": {
|
||||||
@@ -326,4 +326,4 @@
|
|||||||
],
|
],
|
||||||
"title": "PermissionsRequestApprovalParams",
|
"title": "PermissionsRequestApprovalParams",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
+3
-3
@@ -1593,8 +1593,8 @@
|
|||||||
"environmentId": {
|
"environmentId": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"type": [
|
"type": [
|
||||||
"null",
|
"string",
|
||||||
"string"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"itemId": {
|
"itemId": {
|
||||||
@@ -2005,4 +2005,4 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "ServerRequest"
|
"title": "ServerRequest"
|
||||||
}
|
}
|
||||||
+55
@@ -5900,6 +5900,30 @@
|
|||||||
"title": "AgentMessageDeltaNotification",
|
"title": "AgentMessageDeltaNotification",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"AgentMessageInputContent": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"encrypted_content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"encrypted_content"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContentType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"encrypted_content",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContent",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"AgentPath": {
|
"AgentPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -14050,6 +14074,37 @@
|
|||||||
"title": "MessageResponseItem",
|
"title": "MessageResponseItem",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/v2/AgentMessageInputContent"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"agent_message"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItemType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"author",
|
||||||
|
"content",
|
||||||
|
"recipient",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItem",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
|
|||||||
+55
@@ -265,6 +265,30 @@
|
|||||||
"title": "AgentMessageDeltaNotification",
|
"title": "AgentMessageDeltaNotification",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"AgentMessageInputContent": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"encrypted_content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"encrypted_content"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContentType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"encrypted_content",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContent",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"AgentPath": {
|
"AgentPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -10572,6 +10596,37 @@
|
|||||||
"title": "MessageResponseItem",
|
"title": "MessageResponseItem",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/AgentMessageInputContent"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"agent_message"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItemType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"author",
|
||||||
|
"content",
|
||||||
|
"recipient",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItem",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
|
|||||||
+55
@@ -1,6 +1,30 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"AgentMessageInputContent": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"encrypted_content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"encrypted_content"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContentType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"encrypted_content",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContent",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"ContentItem": {
|
"ContentItem": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
@@ -369,6 +393,37 @@
|
|||||||
"title": "MessageResponseItem",
|
"title": "MessageResponseItem",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/AgentMessageInputContent"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"agent_message"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItemType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"author",
|
||||||
|
"content",
|
||||||
|
"recipient",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItem",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"AgentMessageInputContent": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"encrypted_content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"encrypted_content"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContentType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"encrypted_content",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "EncryptedContentAgentMessageInputContent",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"ApprovalsReviewer": {
|
"ApprovalsReviewer": {
|
||||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
|
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -436,6 +460,37 @@
|
|||||||
"title": "MessageResponseItem",
|
"title": "MessageResponseItem",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/AgentMessageInputContent"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"agent_message"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItemType",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"author",
|
||||||
|
"content",
|
||||||
|
"recipient",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"title": "AgentMessageResponseItem",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// 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 AgentMessageInputContent = { "type": "encrypted_content", encrypted_content: string, };
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
// 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.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { AgentMessageInputContent } from "./AgentMessageInputContent";
|
||||||
import type { ContentItem } from "./ContentItem";
|
import type { ContentItem } from "./ContentItem";
|
||||||
import type { FunctionCallOutputBody } from "./FunctionCallOutputBody";
|
import type { FunctionCallOutputBody } from "./FunctionCallOutputBody";
|
||||||
import type { LocalShellAction } from "./LocalShellAction";
|
import type { LocalShellAction } from "./LocalShellAction";
|
||||||
@@ -10,7 +11,7 @@ import type { ReasoningItemContent } from "./ReasoningItemContent";
|
|||||||
import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
|
import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
|
||||||
import type { WebSearchAction } from "./WebSearchAction";
|
import type { WebSearchAction } from "./WebSearchAction";
|
||||||
|
|
||||||
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
|
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, phase?: MessagePhase, } | { "type": "agent_message", author: string, recipient: string, content: Array<AgentMessageInputContent>, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
|
||||||
/**
|
/**
|
||||||
* Set when using the Responses API.
|
* Set when using the Responses API.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||||
|
|
||||||
export type { AbsolutePathBuf } from "./AbsolutePathBuf";
|
export type { AbsolutePathBuf } from "./AbsolutePathBuf";
|
||||||
|
export type { AgentMessageInputContent } from "./AgentMessageInputContent";
|
||||||
export type { AgentPath } from "./AgentPath";
|
export type { AgentPath } from "./AgentPath";
|
||||||
export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
|
export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
|
||||||
export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse";
|
export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse";
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
|
|||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
RolloutItem::ResponseItem(
|
RolloutItem::ResponseItem(
|
||||||
ResponseItem::Reasoning { .. }
|
ResponseItem::AgentMessage { .. }
|
||||||
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
| ResponseItem::ToolSearchCall { .. }
|
| ResponseItem::ToolSearchCall { .. }
|
||||||
@@ -715,7 +716,12 @@ impl AgentControl {
|
|||||||
agent_id: ThreadId,
|
agent_id: ThreadId,
|
||||||
initial_operation: Op,
|
initial_operation: Op,
|
||||||
) -> CodexResult<String> {
|
) -> CodexResult<String> {
|
||||||
let last_task_message = render_input_preview(&initial_operation);
|
let last_task_message = match &initial_operation {
|
||||||
|
Op::InterAgentCommunication { communication } => {
|
||||||
|
last_task_message_from_communication(communication)
|
||||||
|
}
|
||||||
|
_ => non_empty_task_message(render_input_preview(&initial_operation)),
|
||||||
|
};
|
||||||
let state = self.upgrade()?;
|
let state = self.upgrade()?;
|
||||||
let result = self
|
let result = self
|
||||||
.handle_thread_request_result(
|
.handle_thread_request_result(
|
||||||
@@ -725,8 +731,12 @@ impl AgentControl {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
self.state
|
match last_task_message {
|
||||||
.update_last_task_message(agent_id, last_task_message);
|
Some(last_task_message) => self
|
||||||
|
.state
|
||||||
|
.update_last_task_message(agent_id, last_task_message),
|
||||||
|
None => self.state.clear_last_task_message(agent_id),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -736,7 +746,7 @@ impl AgentControl {
|
|||||||
agent_id: ThreadId,
|
agent_id: ThreadId,
|
||||||
communication: InterAgentCommunication,
|
communication: InterAgentCommunication,
|
||||||
) -> CodexResult<String> {
|
) -> CodexResult<String> {
|
||||||
let last_task_message = communication.content.clone();
|
let last_task_message = last_task_message_from_communication(&communication);
|
||||||
let state = self.upgrade()?;
|
let state = self.upgrade()?;
|
||||||
let result = self
|
let result = self
|
||||||
.handle_thread_request_result(
|
.handle_thread_request_result(
|
||||||
@@ -748,8 +758,12 @@ impl AgentControl {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
self.state
|
match last_task_message {
|
||||||
.update_last_task_message(agent_id, last_task_message);
|
Some(last_task_message) => self
|
||||||
|
.state
|
||||||
|
.update_last_task_message(agent_id, last_task_message),
|
||||||
|
None => self.state.clear_last_task_message(agent_id),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -1329,6 +1343,17 @@ pub(crate) fn render_input_preview(initial_operation: &Op) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn last_task_message_from_communication(communication: &InterAgentCommunication) -> Option<String> {
|
||||||
|
if communication.encrypted_content.is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
non_empty_task_message(communication.content.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn non_empty_task_message(message: String) -> Option<String> {
|
||||||
|
(!message.is_empty()).then_some(message)
|
||||||
|
}
|
||||||
|
|
||||||
fn thread_spawn_depth(session_source: &SessionSource) -> Option<i32> {
|
fn thread_spawn_depth(session_source: &SessionSource) -> Option<i32> {
|
||||||
match session_source {
|
match session_source {
|
||||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => Some(*depth),
|
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => Some(*depth),
|
||||||
|
|||||||
@@ -527,6 +527,62 @@ async fn send_inter_agent_communication_without_turn_queues_message_without_trig
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn encrypted_inter_agent_communication_clears_existing_last_task_message() {
|
||||||
|
let harness = AgentControlHarness::new().await;
|
||||||
|
let (parent_thread_id, _) = harness.start_thread().await;
|
||||||
|
let agent_path = AgentPath::try_from("/root/worker").expect("agent path");
|
||||||
|
let spawned_agent = harness
|
||||||
|
.control
|
||||||
|
.spawn_agent_with_metadata(
|
||||||
|
harness.config.clone(),
|
||||||
|
text_input("old plaintext task"),
|
||||||
|
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||||
|
parent_thread_id,
|
||||||
|
depth: 1,
|
||||||
|
agent_path: Some(agent_path.clone()),
|
||||||
|
agent_nickname: None,
|
||||||
|
agent_role: None,
|
||||||
|
})),
|
||||||
|
SpawnAgentOptions {
|
||||||
|
parent_thread_id: Some(parent_thread_id),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("spawn_agent should succeed");
|
||||||
|
assert_eq!(
|
||||||
|
harness
|
||||||
|
.control
|
||||||
|
.state
|
||||||
|
.agent_metadata_for_thread(spawned_agent.thread_id)
|
||||||
|
.and_then(|metadata| metadata.last_task_message),
|
||||||
|
Some("old plaintext task".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
let communication = InterAgentCommunication::new_encrypted(
|
||||||
|
AgentPath::root(),
|
||||||
|
agent_path,
|
||||||
|
Vec::new(),
|
||||||
|
"encrypted-task".to_string(),
|
||||||
|
/*trigger_turn*/ true,
|
||||||
|
);
|
||||||
|
harness
|
||||||
|
.control
|
||||||
|
.send_inter_agent_communication(spawned_agent.thread_id, communication)
|
||||||
|
.await
|
||||||
|
.expect("send_inter_agent_communication should succeed");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
harness
|
||||||
|
.control
|
||||||
|
.state
|
||||||
|
.agent_metadata_for_thread(spawned_agent.thread_id)
|
||||||
|
.and_then(|metadata| metadata.last_task_message),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spawn_agent_creates_thread_and_sends_prompt() {
|
async fn spawn_agent_creates_thread_and_sends_prompt() {
|
||||||
let harness = AgentControlHarness::new().await;
|
let harness = AgentControlHarness::new().await;
|
||||||
|
|||||||
@@ -180,6 +180,20 @@ impl AgentRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_last_task_message(&self, thread_id: ThreadId) {
|
||||||
|
let mut active_agents = self
|
||||||
|
.active_agents
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
if let Some(metadata) = active_agents
|
||||||
|
.agent_tree
|
||||||
|
.values_mut()
|
||||||
|
.find(|metadata| metadata.agent_id == Some(thread_id))
|
||||||
|
{
|
||||||
|
metadata.last_task_message = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn register_spawned_thread(&self, agent_metadata: AgentMetadata) {
|
fn register_spawned_thread(&self, agent_metadata: AgentMetadata) {
|
||||||
let Some(thread_id) = agent_metadata.agent_id else {
|
let Some(thread_id) = agent_metadata.agent_id else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use codex_config::types::Personality;
|
|||||||
use codex_protocol::error::Result;
|
use codex_protocol::error::Result;
|
||||||
use codex_protocol::models::BaseInstructions;
|
use codex_protocol::models::BaseInstructions;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
|
use codex_protocol::protocol::InterAgentCommunication;
|
||||||
use codex_tools::ToolSpec;
|
use codex_tools::ToolSpec;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -53,7 +54,22 @@ impl Default for Prompt {
|
|||||||
|
|
||||||
impl Prompt {
|
impl Prompt {
|
||||||
pub(crate) fn get_formatted_input(&self) -> Vec<ResponseItem> {
|
pub(crate) fn get_formatted_input(&self) -> Vec<ResponseItem> {
|
||||||
self.input.clone()
|
self.input
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|item| {
|
||||||
|
let ResponseItem::Message { role, content, .. } = &item else {
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
if role != "assistant" {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
InterAgentCommunication::from_message_content(content)
|
||||||
|
.filter(|communication| communication.encrypted_content.is_some())
|
||||||
|
.map(|communication| communication.to_model_input_item())
|
||||||
|
.unwrap_or(item)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
|||||||
}
|
}
|
||||||
ResponseItem::Message { role, .. } if role == "assistant" => true,
|
ResponseItem::Message { role, .. } if role == "assistant" => true,
|
||||||
ResponseItem::Message { .. } => false,
|
ResponseItem::Message { .. } => false,
|
||||||
|
ResponseItem::AgentMessage { .. } => true,
|
||||||
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
|
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
|
||||||
ResponseItem::CompactionTrigger => false,
|
ResponseItem::CompactionTrigger => false,
|
||||||
ResponseItem::Reasoning { .. }
|
ResponseItem::Reasoning { .. }
|
||||||
|
|||||||
@@ -386,6 +386,7 @@ impl ContextManager {
|
|||||||
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
||||||
},
|
},
|
||||||
ResponseItem::Message { .. }
|
ResponseItem::Message { .. }
|
||||||
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::Reasoning { .. }
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
@@ -474,7 +475,8 @@ pub(crate) fn truncate_function_output_payload(
|
|||||||
fn is_api_message(message: &ResponseItem) -> bool {
|
fn is_api_message(message: &ResponseItem) -> bool {
|
||||||
match message {
|
match message {
|
||||||
ResponseItem::Message { role, .. } => role.as_str() != "system",
|
ResponseItem::Message { role, .. } => role.as_str() != "system",
|
||||||
ResponseItem::FunctionCallOutput { .. }
|
ResponseItem::AgentMessage { .. }
|
||||||
|
| ResponseItem::FunctionCallOutput { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
| ResponseItem::ToolSearchCall { .. }
|
| ResponseItem::ToolSearchCall { .. }
|
||||||
| ResponseItem::ToolSearchOutput { .. }
|
| ResponseItem::ToolSearchOutput { .. }
|
||||||
@@ -720,11 +722,15 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
|
|||||||
ResponseItem::FunctionCallOutput { .. }
|
ResponseItem::FunctionCallOutput { .. }
|
||||||
| ResponseItem::ToolSearchOutput { .. }
|
| ResponseItem::ToolSearchOutput { .. }
|
||||||
| ResponseItem::CustomToolCallOutput { .. }
|
| ResponseItem::CustomToolCallOutput { .. }
|
||||||
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::Other => false,
|
| ResponseItem::Other => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_user_turn_boundary(item: &ResponseItem) -> bool {
|
pub(crate) fn is_user_turn_boundary(item: &ResponseItem) -> bool {
|
||||||
|
if matches!(item, ResponseItem::AgentMessage { .. }) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
let ResponseItem::Message { role, content, .. } = item else {
|
let ResponseItem::Message { role, content, .. } = item else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1924,6 +1924,7 @@ async fn try_run_sampling_request(
|
|||||||
role == "assistant" && matches!(phase, Some(MessagePhase::Commentary))
|
role == "assistant" && matches!(phase, Some(MessagePhase::Commentary))
|
||||||
}
|
}
|
||||||
ResponseItem::Reasoning { .. } => true,
|
ResponseItem::Reasoning { .. } => true,
|
||||||
|
ResponseItem::AgentMessage { .. } => false,
|
||||||
ResponseItem::LocalShellCall { .. }
|
ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
| ResponseItem::ToolSearchCall { .. }
|
| ResponseItem::ToolSearchCall { .. }
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ pub fn create_send_message_tool() -> ToolSpec {
|
|||||||
"message".to_string(),
|
"message".to_string(),
|
||||||
JsonSchema::string(Some(
|
JsonSchema::string(Some(
|
||||||
"Message text to queue on the target agent.".to_string(),
|
"Message text to queue on the target agent.".to_string(),
|
||||||
)),
|
))
|
||||||
|
.with_encrypted(),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -199,7 +200,8 @@ pub fn create_followup_task_tool() -> ToolSpec {
|
|||||||
"message".to_string(),
|
"message".to_string(),
|
||||||
JsonSchema::string(Some(
|
JsonSchema::string(Some(
|
||||||
"Message text to send to the target agent.".to_string(),
|
"Message text to send to the target agent.".to_string(),
|
||||||
)),
|
))
|
||||||
|
.with_encrypted(),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -595,7 +597,10 @@ fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<St
|
|||||||
BTreeMap::from([
|
BTreeMap::from([
|
||||||
(
|
(
|
||||||
"message".to_string(),
|
"message".to_string(),
|
||||||
JsonSchema::string(Some("Initial plain-text task for the new agent.".to_string())),
|
JsonSchema::string(Some(
|
||||||
|
"Initial plain-text task for the new agent.".to_string(),
|
||||||
|
))
|
||||||
|
.with_encrypted(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"agent_type".to_string(),
|
"agent_type".to_string(),
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
|||||||
assert!(!description.contains("hidden-model"));
|
assert!(!description.contains("hidden-model"));
|
||||||
assert!(properties.contains_key("task_name"));
|
assert!(properties.contains_key("task_name"));
|
||||||
assert!(properties.contains_key("message"));
|
assert!(properties.contains_key("message"));
|
||||||
|
assert_eq!(
|
||||||
|
properties
|
||||||
|
.get("message")
|
||||||
|
.and_then(|schema| schema.encrypted),
|
||||||
|
Some(true)
|
||||||
|
);
|
||||||
assert!(properties.contains_key("fork_turns"));
|
assert!(properties.contains_key("fork_turns"));
|
||||||
assert!(!properties.contains_key("items"));
|
assert!(!properties.contains_key("items"));
|
||||||
assert!(!properties.contains_key("fork_context"));
|
assert!(!properties.contains_key("fork_context"));
|
||||||
@@ -141,6 +147,12 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
|
|||||||
|
|
||||||
assert!(properties.contains_key("fork_context"));
|
assert!(properties.contains_key("fork_context"));
|
||||||
assert!(!properties.contains_key("fork_turns"));
|
assert!(!properties.contains_key("fork_turns"));
|
||||||
|
assert_eq!(
|
||||||
|
properties
|
||||||
|
.get("message")
|
||||||
|
.and_then(|schema| schema.encrypted),
|
||||||
|
None
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
properties
|
properties
|
||||||
.get("model")
|
.get("model")
|
||||||
@@ -259,6 +271,12 @@ fn send_message_tool_requires_message_and_has_no_output_schema() {
|
|||||||
.expect("send_message should use object params");
|
.expect("send_message should use object params");
|
||||||
assert!(properties.contains_key("target"));
|
assert!(properties.contains_key("target"));
|
||||||
assert!(properties.contains_key("message"));
|
assert!(properties.contains_key("message"));
|
||||||
|
assert_eq!(
|
||||||
|
properties
|
||||||
|
.get("message")
|
||||||
|
.and_then(|schema| schema.encrypted),
|
||||||
|
Some(true)
|
||||||
|
);
|
||||||
assert!(!properties.contains_key("interrupt"));
|
assert!(!properties.contains_key("interrupt"));
|
||||||
assert!(!properties.contains_key("items"));
|
assert!(!properties.contains_key("items"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -296,6 +314,12 @@ fn followup_task_tool_requires_message_and_has_no_output_schema() {
|
|||||||
.expect("followup_task should use object params");
|
.expect("followup_task should use object params");
|
||||||
assert!(properties.contains_key("target"));
|
assert!(properties.contains_key("target"));
|
||||||
assert!(properties.contains_key("message"));
|
assert!(properties.contains_key("message"));
|
||||||
|
assert_eq!(
|
||||||
|
properties
|
||||||
|
.get("message")
|
||||||
|
.and_then(|schema| schema.encrypted),
|
||||||
|
Some(true)
|
||||||
|
);
|
||||||
assert!(!properties.contains_key("items"));
|
assert!(!properties.contains_key("items"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parameters.required.as_ref(),
|
parameters.required.as_ref(),
|
||||||
|
|||||||
@@ -1152,7 +1152,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
|||||||
turn.clone(),
|
turn.clone(),
|
||||||
"spawn_agent",
|
"spawn_agent",
|
||||||
function_payload(json!({
|
function_payload(json!({
|
||||||
"message": "inspect this repo",
|
"message": "encrypted-spawn-message",
|
||||||
"task_name": "test_process"
|
"task_name": "test_process"
|
||||||
})),
|
})),
|
||||||
))
|
))
|
||||||
@@ -1188,7 +1188,8 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
|||||||
if communication.author == AgentPath::root()
|
if communication.author == AgentPath::root()
|
||||||
&& communication.recipient.as_str() == "/root/test_process"
|
&& communication.recipient.as_str() == "/root/test_process"
|
||||||
&& communication.other_recipients.is_empty()
|
&& communication.other_recipients.is_empty()
|
||||||
&& communication.content == "inspect this repo"
|
&& communication.content.is_empty()
|
||||||
|
&& communication.encrypted_content.as_deref() == Some("encrypted-spawn-message")
|
||||||
&& communication.trigger_turn
|
&& communication.trigger_turn
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
@@ -1200,7 +1201,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
|||||||
"send_message",
|
"send_message",
|
||||||
function_payload(json!({
|
function_payload(json!({
|
||||||
"target": "test_process",
|
"target": "test_process",
|
||||||
"message": "continue"
|
"message": "encrypted-send-message"
|
||||||
})),
|
})),
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
@@ -1214,7 +1215,8 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
|||||||
if communication.author == AgentPath::root()
|
if communication.author == AgentPath::root()
|
||||||
&& communication.recipient.as_str() == "/root/test_process"
|
&& communication.recipient.as_str() == "/root/test_process"
|
||||||
&& communication.other_recipients.is_empty()
|
&& communication.other_recipients.is_empty()
|
||||||
&& communication.content == "continue"
|
&& communication.content.is_empty()
|
||||||
|
&& communication.encrypted_content.as_deref() == Some("encrypted-send-message")
|
||||||
&& !communication.trigger_turn
|
&& !communication.trigger_turn
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
@@ -1396,7 +1398,7 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
|
|||||||
"send_message",
|
"send_message",
|
||||||
function_payload(json!({
|
function_payload(json!({
|
||||||
"target": "/root",
|
"target": "/root",
|
||||||
"message": "done"
|
"message": "encrypted-done"
|
||||||
})),
|
})),
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
@@ -1410,7 +1412,8 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
|
|||||||
if communication.author == child_path
|
if communication.author == child_path
|
||||||
&& communication.recipient == AgentPath::root()
|
&& communication.recipient == AgentPath::root()
|
||||||
&& communication.other_recipients.is_empty()
|
&& communication.other_recipients.is_empty()
|
||||||
&& communication.content == "done"
|
&& communication.content.is_empty()
|
||||||
|
&& communication.encrypted_content.as_deref() == Some("encrypted-done")
|
||||||
&& !communication.trigger_turn
|
&& !communication.trigger_turn
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
@@ -1500,7 +1503,7 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_message() {
|
async fn multi_agent_v2_list_agents_returns_completed_status_without_encrypted_spawn_preview() {
|
||||||
let (mut session, mut turn) = make_session_and_context().await;
|
let (mut session, mut turn) = make_session_and_context().await;
|
||||||
let manager = thread_manager();
|
let manager = thread_manager();
|
||||||
let root = manager
|
let root = manager
|
||||||
@@ -1586,10 +1589,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
|
|||||||
.find(|agent| agent.agent_name == "/root/worker")
|
.find(|agent| agent.agent_name == "/root/worker")
|
||||||
.expect("worker agent should be listed");
|
.expect("worker agent should be listed");
|
||||||
assert_eq!(worker.agent_status, json!({"completed": "done"}));
|
assert_eq!(worker.agent_status, json!({"completed": "done"}));
|
||||||
assert_eq!(
|
assert_eq!(worker.last_task_message, None);
|
||||||
worker.last_task_message.as_deref(),
|
|
||||||
Some("inspect this repo")
|
|
||||||
);
|
|
||||||
assert_eq!(success, Some(true));
|
assert_eq!(success, Some(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1868,7 +1868,8 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
|
|||||||
if communication.author == AgentPath::root()
|
if communication.author == AgentPath::root()
|
||||||
&& communication.recipient.as_str() == "/root/worker"
|
&& communication.recipient.as_str() == "/root/worker"
|
||||||
&& communication.other_recipients.is_empty()
|
&& communication.other_recipients.is_empty()
|
||||||
&& communication.content == "continue"
|
&& communication.content.is_empty()
|
||||||
|
&& communication.encrypted_content.as_deref() == Some("continue")
|
||||||
&& !communication.trigger_turn
|
&& !communication.trigger_turn
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use codex_protocol::protocol::CollabCloseBeginEvent;
|
|||||||
use codex_protocol::protocol::CollabCloseEndEvent;
|
use codex_protocol::protocol::CollabCloseEndEvent;
|
||||||
use codex_protocol::protocol::CollabWaitingBeginEvent;
|
use codex_protocol::protocol::CollabWaitingBeginEvent;
|
||||||
use codex_protocol::protocol::CollabWaitingEndEvent;
|
use codex_protocol::protocol::CollabWaitingEndEvent;
|
||||||
|
use codex_protocol::protocol::InterAgentCommunication;
|
||||||
use codex_protocol::user_input::UserInput;
|
use codex_protocol::user_input::UserInput;
|
||||||
use codex_tools::ToolName;
|
use codex_tools::ToolName;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -42,3 +43,17 @@ mod message_tool;
|
|||||||
mod send_message;
|
mod send_message;
|
||||||
mod spawn;
|
mod spawn;
|
||||||
pub(crate) mod wait;
|
pub(crate) mod wait;
|
||||||
|
|
||||||
|
pub(super) fn communication_from_tool_message(
|
||||||
|
author: AgentPath,
|
||||||
|
recipient: AgentPath,
|
||||||
|
message: String,
|
||||||
|
) -> InterAgentCommunication {
|
||||||
|
InterAgentCommunication::new_encrypted(
|
||||||
|
author,
|
||||||
|
recipient,
|
||||||
|
Vec::new(),
|
||||||
|
message,
|
||||||
|
/*trigger_turn*/ true,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Shared argument parsing and dispatch for the v2 text-only agent messaging tools.
|
//! Shared argument parsing and dispatch for the v2 agent messaging tools.
|
||||||
//!
|
//!
|
||||||
//! `send_message` and `followup_task` share the same submission path and differ only in whether the
|
//! `send_message` and `followup_task` share the same submission path and differ only in whether the
|
||||||
//! resulting `InterAgentCommunication` should wake the target immediately.
|
//! resulting `InterAgentCommunication` should wake the target immediately.
|
||||||
@@ -55,20 +55,21 @@ fn message_content(message: String) -> Result<String, FunctionCallError> {
|
|||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the shared MultiAgentV2 plain-text message flow for both `send_message` and `followup_task`.
|
/// Handles the shared MultiAgentV2 message flow for both `send_message` and `followup_task`.
|
||||||
pub(crate) async fn handle_message_string_tool(
|
pub(crate) async fn handle_message_string_tool(
|
||||||
invocation: ToolInvocation,
|
invocation: ToolInvocation,
|
||||||
mode: MessageDeliveryMode,
|
mode: MessageDeliveryMode,
|
||||||
target: String,
|
target: String,
|
||||||
message: String,
|
message: String,
|
||||||
) -> Result<FunctionToolOutput, FunctionCallError> {
|
) -> Result<FunctionToolOutput, FunctionCallError> {
|
||||||
let prompt = message_content(message)?;
|
let message = message_content(message)?;
|
||||||
let ToolInvocation {
|
let ToolInvocation {
|
||||||
session,
|
session,
|
||||||
turn,
|
turn,
|
||||||
call_id,
|
call_id,
|
||||||
..
|
..
|
||||||
} = invocation;
|
} = invocation;
|
||||||
|
let prompt = String::new();
|
||||||
let receiver_thread_id = resolve_agent_target(&session, &turn, &target).await?;
|
let receiver_thread_id = resolve_agent_target(&session, &turn, &target).await?;
|
||||||
let receiver_agent = session
|
let receiver_agent = session
|
||||||
.services
|
.services
|
||||||
@@ -101,15 +102,11 @@ pub(crate) async fn handle_message_string_tool(
|
|||||||
let receiver_agent_path = receiver_agent.agent_path.clone().ok_or_else(|| {
|
let receiver_agent_path = receiver_agent.agent_path.clone().ok_or_else(|| {
|
||||||
FunctionCallError::RespondToModel("target agent is missing an agent_path".to_string())
|
FunctionCallError::RespondToModel("target agent is missing an agent_path".to_string())
|
||||||
})?;
|
})?;
|
||||||
let communication = InterAgentCommunication::new(
|
let author = turn
|
||||||
turn.session_source
|
.session_source
|
||||||
.get_agent_path()
|
.get_agent_path()
|
||||||
.unwrap_or_else(AgentPath::root),
|
.unwrap_or_else(AgentPath::root);
|
||||||
receiver_agent_path,
|
let communication = communication_from_tool_message(author, receiver_agent_path, message);
|
||||||
Vec::new(),
|
|
||||||
prompt.clone(),
|
|
||||||
/*trigger_turn*/ true,
|
|
||||||
);
|
|
||||||
let result = session
|
let result = session
|
||||||
.services
|
.services
|
||||||
.agent_control
|
.agent_control
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::agent::control::SpawnAgentForkMode;
|
use crate::agent::control::SpawnAgentForkMode;
|
||||||
use crate::agent::control::SpawnAgentOptions;
|
use crate::agent::control::SpawnAgentOptions;
|
||||||
use crate::agent::control::render_input_preview;
|
|
||||||
use crate::agent::next_thread_spawn_depth;
|
use crate::agent::next_thread_spawn_depth;
|
||||||
use crate::agent::role::DEFAULT_ROLE_NAME;
|
use crate::agent::role::DEFAULT_ROLE_NAME;
|
||||||
use crate::agent::role::apply_role_to_config;
|
use crate::agent::role::apply_role_to_config;
|
||||||
@@ -9,7 +8,6 @@ use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
|
|||||||
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
|
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
|
||||||
use crate::turn_timing::now_unix_timestamp_ms;
|
use crate::turn_timing::now_unix_timestamp_ms;
|
||||||
use codex_protocol::AgentPath;
|
use codex_protocol::AgentPath;
|
||||||
use codex_protocol::protocol::InterAgentCommunication;
|
|
||||||
use codex_protocol::protocol::Op;
|
use codex_protocol::protocol::Op;
|
||||||
use codex_tools::ToolSpec;
|
use codex_tools::ToolSpec;
|
||||||
|
|
||||||
@@ -61,8 +59,9 @@ async fn handle_spawn_agent(
|
|||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|role| !role.is_empty());
|
.filter(|role| !role.is_empty());
|
||||||
|
|
||||||
|
let message = args.message.clone();
|
||||||
let initial_operation = parse_collab_input(Some(args.message), /*items*/ None)?;
|
let initial_operation = parse_collab_input(Some(args.message), /*items*/ None)?;
|
||||||
let prompt = render_input_preview(&initial_operation);
|
let prompt = String::new();
|
||||||
|
|
||||||
let session_source = turn.session_source.clone();
|
let session_source = turn.session_source.clone();
|
||||||
let child_depth = next_thread_spawn_depth(&session_source);
|
let child_depth = next_thread_spawn_depth(&session_source);
|
||||||
@@ -129,17 +128,12 @@ async fn handle_spawn_agent(
|
|||||||
.iter()
|
.iter()
|
||||||
.all(|item| matches!(item, UserInput::Text { .. })) =>
|
.all(|item| matches!(item, UserInput::Text { .. })) =>
|
||||||
{
|
{
|
||||||
Op::InterAgentCommunication {
|
let author = turn
|
||||||
communication: InterAgentCommunication::new(
|
.session_source
|
||||||
turn.session_source
|
.get_agent_path()
|
||||||
.get_agent_path()
|
.unwrap_or_else(AgentPath::root);
|
||||||
.unwrap_or_else(AgentPath::root),
|
let communication = communication_from_tool_message(author, recipient, message);
|
||||||
recipient,
|
Op::InterAgentCommunication { communication }
|
||||||
Vec::new(),
|
|
||||||
prompt.clone(),
|
|
||||||
/*trigger_turn*/ true,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(_, initial_operation) => initial_operation,
|
(_, initial_operation) => initial_operation,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1047,6 +1047,30 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multi_agent_v2_message_schemas_are_encrypted() {
|
||||||
|
let plan = probe(|turn| {
|
||||||
|
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
for tool_name in ["spawn_agent", "send_message", "followup_task"] {
|
||||||
|
let ToolSpec::Function(tool) = plan.visible_spec(tool_name) else {
|
||||||
|
panic!("expected {tool_name} function spec");
|
||||||
|
};
|
||||||
|
let properties = tool
|
||||||
|
.parameters
|
||||||
|
.properties
|
||||||
|
.as_ref()
|
||||||
|
.expect("tool should use object params");
|
||||||
|
assert_eq!(
|
||||||
|
properties
|
||||||
|
.get("message")
|
||||||
|
.and_then(|schema| schema.encrypted),
|
||||||
|
Some(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn tool_mode_selector_overrides_feature_flags() {
|
async fn tool_mode_selector_overrides_feature_flags() {
|
||||||
let direct = probe(|turn| {
|
let direct = probe(|turn| {
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ fn response_item_records_turn_ttft(item: &ResponseItem) -> bool {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
ResponseItem::AgentMessage { .. } => false,
|
||||||
ResponseItem::LocalShellCall { .. }
|
ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
| ResponseItem::CustomToolCall { .. }
|
| ResponseItem::CustomToolCall { .. }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use core_test_support::hooks::trust_discovered_hooks;
|
|||||||
use core_test_support::responses::ResponsesRequest;
|
use core_test_support::responses::ResponsesRequest;
|
||||||
use core_test_support::responses::ev_assistant_message;
|
use core_test_support::responses::ev_assistant_message;
|
||||||
use core_test_support::responses::ev_completed;
|
use core_test_support::responses::ev_completed;
|
||||||
|
use core_test_support::responses::ev_function_call;
|
||||||
use core_test_support::responses::ev_function_call_with_namespace;
|
use core_test_support::responses::ev_function_call_with_namespace;
|
||||||
use core_test_support::responses::ev_response_created;
|
use core_test_support::responses::ev_response_created;
|
||||||
use core_test_support::responses::ev_tool_search_call;
|
use core_test_support::responses::ev_tool_search_call;
|
||||||
@@ -1026,6 +1027,80 @@ async fn spawned_multi_agent_v2_child_inherits_parent_developer_context() -> Res
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result<()> {
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let encrypted_message = "opaque-encrypted-message";
|
||||||
|
let spawn_args = serde_json::to_string(&json!({
|
||||||
|
"message": encrypted_message,
|
||||||
|
"task_name": "worker",
|
||||||
|
}))?;
|
||||||
|
mount_sse_once_match(
|
||||||
|
&server,
|
||||||
|
|req: &wiremock::Request| body_contains(req, TURN_1_PROMPT),
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created("resp-parent-1"),
|
||||||
|
ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
|
||||||
|
ev_completed("resp-parent-1"),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let child_request_log = mount_sse_once_match(
|
||||||
|
&server,
|
||||||
|
|req: &wiremock::Request| body_contains(req, "\"type\":\"agent_message\""),
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created("resp-child-1"),
|
||||||
|
ev_completed("resp-child-1"),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
mount_sse_once_match(
|
||||||
|
&server,
|
||||||
|
|req: &wiremock::Request| {
|
||||||
|
body_contains(req, SPAWN_CALL_ID) && !body_contains(req, "\"type\":\"agent_message\"")
|
||||||
|
},
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created("resp-parent-2"),
|
||||||
|
ev_assistant_message("msg-parent-2", "done"),
|
||||||
|
ev_completed("resp-parent-2"),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut builder = test_codex().with_model("koffing").with_config(|config| {
|
||||||
|
config
|
||||||
|
.features
|
||||||
|
.enable(Feature::Collab)
|
||||||
|
.expect("test config should allow feature update");
|
||||||
|
config
|
||||||
|
.features
|
||||||
|
.enable(Feature::MultiAgentV2)
|
||||||
|
.expect("test config should allow feature update");
|
||||||
|
});
|
||||||
|
let test = builder.build(&server).await?;
|
||||||
|
|
||||||
|
test.submit_turn(TURN_1_PROMPT).await?;
|
||||||
|
|
||||||
|
let child_request = wait_for_requests(&child_request_log)
|
||||||
|
.await?
|
||||||
|
.pop()
|
||||||
|
.expect("child request");
|
||||||
|
assert_eq!(
|
||||||
|
child_request.inputs_of_type("agent_message"),
|
||||||
|
vec![json!({
|
||||||
|
"type": "agent_message",
|
||||||
|
"author": "/root",
|
||||||
|
"recipient": "/root/worker",
|
||||||
|
"content": [{
|
||||||
|
"type": "encrypted_content",
|
||||||
|
"encrypted_content": encrypted_message,
|
||||||
|
}],
|
||||||
|
})]
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn skills_toggle_skips_instructions_for_parent_and_spawned_child() -> Result<()> {
|
async fn skills_toggle_skips_instructions_for_parent_and_spawned_child() -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ fn edit_images(history: &[ResponseItem]) -> Vec<ImageUrl> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
ResponseItem::Message { .. }
|
ResponseItem::Message { .. }
|
||||||
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::Reasoning { .. }
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
|
|||||||
@@ -1217,6 +1217,7 @@ impl SessionTelemetry {
|
|||||||
fn responses_item_type(item: &ResponseItem) -> String {
|
fn responses_item_type(item: &ResponseItem) -> String {
|
||||||
match item {
|
match item {
|
||||||
ResponseItem::Message { role, .. } => format!("message_from_{role}"),
|
ResponseItem::Message { role, .. } => format!("message_from_{role}"),
|
||||||
|
ResponseItem::AgentMessage { .. } => "agent_message".into(),
|
||||||
ResponseItem::Reasoning { .. } => "reasoning".into(),
|
ResponseItem::Reasoning { .. } => "reasoning".into(),
|
||||||
ResponseItem::LocalShellCall { .. } => "local_shell_call".into(),
|
ResponseItem::LocalShellCall { .. } => "local_shell_call".into(),
|
||||||
ResponseItem::FunctionCall { .. } => "function_call".into(),
|
ResponseItem::FunctionCall { .. } => "function_call".into(),
|
||||||
|
|||||||
@@ -715,6 +715,12 @@ pub enum ContentItem {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum AgentMessageInputContent {
|
||||||
|
EncryptedContent { encrypted_content: String },
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ImageDetail {
|
pub enum ImageDetail {
|
||||||
@@ -758,6 +764,11 @@ pub enum ResponseItem {
|
|||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
phase: Option<MessagePhase>,
|
phase: Option<MessagePhase>,
|
||||||
},
|
},
|
||||||
|
AgentMessage {
|
||||||
|
author: String,
|
||||||
|
recipient: String,
|
||||||
|
content: Vec<AgentMessageInputContent>,
|
||||||
|
},
|
||||||
Reasoning {
|
Reasoning {
|
||||||
#[serde(default, skip_serializing)]
|
#[serde(default, skip_serializing)]
|
||||||
#[ts(skip)]
|
#[ts(skip)]
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use crate::mcp::CallToolResult;
|
|||||||
use crate::mcp::RequestId;
|
use crate::mcp::RequestId;
|
||||||
use crate::memory_citation::MemoryCitation;
|
use crate::memory_citation::MemoryCitation;
|
||||||
use crate::models::ActivePermissionProfile;
|
use crate::models::ActivePermissionProfile;
|
||||||
|
use crate::models::AgentMessageInputContent;
|
||||||
use crate::models::BaseInstructions;
|
use crate::models::BaseInstructions;
|
||||||
use crate::models::ContentItem;
|
use crate::models::ContentItem;
|
||||||
use crate::models::ImageDetail;
|
use crate::models::ImageDetail;
|
||||||
@@ -690,6 +691,9 @@ pub struct InterAgentCommunication {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub other_recipients: Vec<AgentPath>,
|
pub other_recipients: Vec<AgentPath>,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[ts(optional)]
|
||||||
|
pub encrypted_content: Option<String>,
|
||||||
pub trigger_turn: bool,
|
pub trigger_turn: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,6 +710,24 @@ impl InterAgentCommunication {
|
|||||||
recipient,
|
recipient,
|
||||||
other_recipients,
|
other_recipients,
|
||||||
content,
|
content,
|
||||||
|
encrypted_content: None,
|
||||||
|
trigger_turn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_encrypted(
|
||||||
|
author: AgentPath,
|
||||||
|
recipient: AgentPath,
|
||||||
|
other_recipients: Vec<AgentPath>,
|
||||||
|
encrypted_content: String,
|
||||||
|
trigger_turn: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
author,
|
||||||
|
recipient,
|
||||||
|
other_recipients,
|
||||||
|
content: String::new(),
|
||||||
|
encrypted_content: Some(encrypted_content),
|
||||||
trigger_turn,
|
trigger_turn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -720,6 +742,19 @@ impl InterAgentCommunication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_model_input_item(&self) -> ResponseItem {
|
||||||
|
match &self.encrypted_content {
|
||||||
|
Some(encrypted_content) => ResponseItem::AgentMessage {
|
||||||
|
author: self.author.to_string(),
|
||||||
|
recipient: self.recipient.to_string(),
|
||||||
|
content: vec![AgentMessageInputContent::EncryptedContent {
|
||||||
|
encrypted_content: encrypted_content.clone(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
None => self.to_response_input_item().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_message_content(content: &[ContentItem]) -> bool {
|
pub fn is_message_content(content: &[ContentItem]) -> bool {
|
||||||
Self::from_message_content(content).is_some()
|
Self::from_message_content(content).is_some()
|
||||||
}
|
}
|
||||||
@@ -4063,6 +4098,7 @@ mod tests {
|
|||||||
recipient: AgentPath::root().join("reviewer").expect("recipient path"),
|
recipient: AgentPath::root().join("reviewer").expect("recipient path"),
|
||||||
other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],
|
other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],
|
||||||
content: "review the diff".to_string(),
|
content: "review the diff".to_string(),
|
||||||
|
encrypted_content: None,
|
||||||
trigger_turn: true,
|
trigger_turn: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -613,7 +613,12 @@ fn inter_agent_message_fields(item: &ConversationItem) -> Option<(String, String
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let communication = serde_json::from_str::<InterAgentCommunication>(text).ok()?;
|
let communication = serde_json::from_str::<InterAgentCommunication>(text).ok()?;
|
||||||
Some((communication.recipient.to_string(), communication.content))
|
Some((
|
||||||
|
communication.recipient.to_string(),
|
||||||
|
communication
|
||||||
|
.encrypted_content
|
||||||
|
.unwrap_or(communication.content),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub fn persisted_rollout_items(items: &[RolloutItem]) -> Vec<RolloutItem> {
|
|||||||
pub fn should_persist_response_item(item: &ResponseItem) -> bool {
|
pub fn should_persist_response_item(item: &ResponseItem) -> bool {
|
||||||
match item {
|
match item {
|
||||||
ResponseItem::Message { .. }
|
ResponseItem::Message { .. }
|
||||||
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::Reasoning { .. }
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
@@ -60,7 +61,8 @@ pub fn should_persist_response_item_for_memories(item: &ResponseItem) -> bool {
|
|||||||
| ResponseItem::CustomToolCall { .. }
|
| ResponseItem::CustomToolCall { .. }
|
||||||
| ResponseItem::CustomToolCallOutput { .. }
|
| ResponseItem::CustomToolCallOutput { .. }
|
||||||
| ResponseItem::WebSearchCall { .. } => true,
|
| ResponseItem::WebSearchCall { .. } => true,
|
||||||
ResponseItem::Reasoning { .. }
|
ResponseItem::AgentMessage { .. }
|
||||||
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::ImageGenerationCall { .. }
|
| ResponseItem::ImageGenerationCall { .. }
|
||||||
| ResponseItem::Compaction { .. }
|
| ResponseItem::Compaction { .. }
|
||||||
| ResponseItem::CompactionTrigger
|
| ResponseItem::CompactionTrigger
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ pub struct JsonSchema {
|
|||||||
pub schema_type: Option<JsonSchemaType>,
|
pub schema_type: Option<JsonSchemaType>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
/// Responses-only marker for reviewed encrypted tool parameters.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub encrypted: Option<bool>,
|
||||||
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
|
||||||
pub enum_values: Option<Vec<JsonValue>>,
|
pub enum_values: Option<Vec<JsonValue>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -90,6 +93,11 @@ impl JsonSchema {
|
|||||||
Self::typed(JsonSchemaPrimitiveType::String, description)
|
Self::typed(JsonSchemaPrimitiveType::String, description)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_encrypted(mut self) -> Self {
|
||||||
|
self.encrypted = Some(true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn number(description: Option<String>) -> Self {
|
pub fn number(description: Option<String>) -> Self {
|
||||||
Self::typed(JsonSchemaPrimitiveType::Number, description)
|
Self::typed(JsonSchemaPrimitiveType::Number, description)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,20 @@ fn parse_tool_input_schema_coerces_boolean_schemas() {
|
|||||||
assert_eq!(schema, JsonSchema::string(/*description*/ None));
|
assert_eq!(schema, JsonSchema::string(/*description*/ None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_schema_serializes_encrypted_marker() {
|
||||||
|
let schema = JsonSchema::string(Some("Secret value".to_string())).with_encrypted();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(schema).expect("serialize schema"),
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "string",
|
||||||
|
"description": "Secret value",
|
||||||
|
"encrypted": true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_tool_input_schema_infers_object_shape_and_defaults_properties() {
|
fn parse_tool_input_schema_infers_object_shape_and_defaults_properties() {
|
||||||
// Example schema shape:
|
// Example schema shape:
|
||||||
|
|||||||
Reference in new issue
Block a user