mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Merge ba16f4bfb1 into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -2775,6 +2775,95 @@
|
||||
],
|
||||
"title": "CollabCloseEndEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume begin.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_begin"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume end.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the receiver agent reported to the sender agent after resume."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_end"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"status",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsg",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7399,6 +7488,95 @@
|
||||
],
|
||||
"title": "CollabCloseEndEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume begin.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_begin"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume end.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the receiver agent reported to the sender agent after resume."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_end"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"status",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsg",
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"title": "EventMsg"
|
||||
|
||||
@@ -617,6 +617,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
@@ -3353,6 +3354,95 @@
|
||||
],
|
||||
"title": "CollabCloseEndEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume begin.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_begin"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume end.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the receiver agent reported to the sender agent after resume."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_end"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"status",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsg",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -4782,6 +4782,95 @@
|
||||
],
|
||||
"title": "CollabCloseEndEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume begin.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_begin"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume end.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the receiver agent reported to the sender agent after resume."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_end"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"status",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsg",
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"title": "EventMsg"
|
||||
@@ -10234,6 +10323,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
@@ -12348,6 +12438,84 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkRequirements": {
|
||||
"properties": {
|
||||
"allowLocalBinding": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowUnixSockets": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowUpstreamProxy": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedDomains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackAdmin": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackProxy": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"deniedDomains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"httpPort": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"socksPort": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"OverriddenMetadata": {
|
||||
"properties": {
|
||||
"effectiveValue": true,
|
||||
|
||||
@@ -2775,6 +2775,95 @@
|
||||
],
|
||||
"title": "CollabCloseEndEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume begin.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_begin"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume end.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the receiver agent reported to the sender agent after resume."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_end"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"status",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsg",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2775,6 +2775,95 @@
|
||||
],
|
||||
"title": "CollabCloseEndEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume begin.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_begin"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume end.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the receiver agent reported to the sender agent after resume."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_end"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"status",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsg",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2775,6 +2775,95 @@
|
||||
],
|
||||
"title": "CollabCloseEndEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume begin.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_begin"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeBeginEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Collab interaction: resume end.",
|
||||
"properties": {
|
||||
"call_id": {
|
||||
"description": "Identifier for the collab tool call.",
|
||||
"type": "string"
|
||||
},
|
||||
"receiver_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the receiver."
|
||||
},
|
||||
"sender_thread_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
}
|
||||
],
|
||||
"description": "Thread ID of the sender."
|
||||
},
|
||||
"status": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AgentStatus"
|
||||
}
|
||||
],
|
||||
"description": "Last known status of the receiver agent reported to the sender agent after resume."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"collab_resume_end"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"call_id",
|
||||
"receiver_thread_id",
|
||||
"sender_thread_id",
|
||||
"status",
|
||||
"type"
|
||||
],
|
||||
"title": "CollabResumeEndEventMsg",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -52,6 +52,84 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkRequirements": {
|
||||
"properties": {
|
||||
"allowLocalBinding": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowUnixSockets": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowUpstreamProxy": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedDomains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackAdmin": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackProxy": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"deniedDomains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"httpPort": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"socksPort": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ResidencyRequirement": {
|
||||
"enum": [
|
||||
"us"
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"enum": [
|
||||
"spawnAgent",
|
||||
"sendInput",
|
||||
"resumeAgent",
|
||||
"wait",
|
||||
"closeAgent"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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.
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type CollabResumeBeginEvent = {
|
||||
/**
|
||||
* Identifier for the collab tool call.
|
||||
*/
|
||||
call_id: string,
|
||||
/**
|
||||
* Thread ID of the sender.
|
||||
*/
|
||||
sender_thread_id: ThreadId,
|
||||
/**
|
||||
* Thread ID of the receiver.
|
||||
*/
|
||||
receiver_thread_id: ThreadId, };
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.
|
||||
import type { AgentStatus } from "./AgentStatus";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type CollabResumeEndEvent = {
|
||||
/**
|
||||
* Identifier for the collab tool call.
|
||||
*/
|
||||
call_id: string,
|
||||
/**
|
||||
* Thread ID of the sender.
|
||||
*/
|
||||
sender_thread_id: ThreadId,
|
||||
/**
|
||||
* Thread ID of the receiver.
|
||||
*/
|
||||
receiver_thread_id: ThreadId,
|
||||
/**
|
||||
* Last known status of the receiver agent reported to the sender agent after
|
||||
* resume.
|
||||
*/
|
||||
status: AgentStatus, };
|
||||
@@ -17,6 +17,8 @@ import type { CollabAgentSpawnBeginEvent } from "./CollabAgentSpawnBeginEvent";
|
||||
import type { CollabAgentSpawnEndEvent } from "./CollabAgentSpawnEndEvent";
|
||||
import type { CollabCloseBeginEvent } from "./CollabCloseBeginEvent";
|
||||
import type { CollabCloseEndEvent } from "./CollabCloseEndEvent";
|
||||
import type { CollabResumeBeginEvent } from "./CollabResumeBeginEvent";
|
||||
import type { CollabResumeEndEvent } from "./CollabResumeEndEvent";
|
||||
import type { CollabWaitingBeginEvent } from "./CollabWaitingBeginEvent";
|
||||
import type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent";
|
||||
import type { ContextCompactedEvent } from "./ContextCompactedEvent";
|
||||
@@ -72,4 +74,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent";
|
||||
* Response event from the agent
|
||||
* NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
|
||||
*/
|
||||
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent;
|
||||
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent;
|
||||
|
||||
@@ -37,6 +37,8 @@ export type { CollabAgentSpawnBeginEvent } from "./CollabAgentSpawnBeginEvent";
|
||||
export type { CollabAgentSpawnEndEvent } from "./CollabAgentSpawnEndEvent";
|
||||
export type { CollabCloseBeginEvent } from "./CollabCloseBeginEvent";
|
||||
export type { CollabCloseEndEvent } from "./CollabCloseEndEvent";
|
||||
export type { CollabResumeBeginEvent } from "./CollabResumeBeginEvent";
|
||||
export type { CollabResumeEndEvent } from "./CollabResumeEndEvent";
|
||||
export type { CollabWaitingBeginEvent } from "./CollabWaitingBeginEvent";
|
||||
export type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent";
|
||||
export type { CollaborationMode } from "./CollaborationMode";
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CollabAgentTool = "spawnAgent" | "sendInput" | "wait" | "closeAgent";
|
||||
export type CollabAgentTool = "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent";
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { AskForApproval } from "./AskForApproval";
|
||||
import type { ResidencyRequirement } from "./ResidencyRequirement";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
|
||||
export type ConfigRequirements = { allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, enforceResidency: ResidencyRequirement | null, };
|
||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, enforceResidency: ResidencyRequirement | null};
|
||||
|
||||
@@ -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 NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, allowedDomains: Array<string> | null, deniedDomains: Array<string> | null, allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
|
||||
@@ -37,5 +37,8 @@ personality?: Personality | null, /**
|
||||
outputSchema?: JsonValue | null, /**
|
||||
* EXPERIMENTAL - Set a pre-set collaboration mode.
|
||||
* Takes precedence over model, reasoning_effort, and developer instructions if set.
|
||||
*
|
||||
* For `collaboration_mode.settings.developer_instructions`, `null` means
|
||||
* "use the built-in instructions for the selected mode".
|
||||
*/
|
||||
collaborationMode?: CollaborationMode | null};
|
||||
|
||||
@@ -91,6 +91,7 @@ export type { Model } from "./Model";
|
||||
export type { ModelListParams } from "./ModelListParams";
|
||||
export type { ModelListResponse } from "./ModelListResponse";
|
||||
export type { NetworkAccess } from "./NetworkAccess";
|
||||
export type { NetworkRequirements } from "./NetworkRequirements";
|
||||
export type { OverriddenMetadata } from "./OverriddenMetadata";
|
||||
export type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
export type { PatchChangeKind } from "./PatchChangeKind";
|
||||
|
||||
@@ -527,7 +527,7 @@ pub struct ConfigReadResponse {
|
||||
pub layers: Option<Vec<ConfigLayer>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ConfigRequirements {
|
||||
@@ -535,6 +535,24 @@ pub struct ConfigRequirements {
|
||||
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
||||
pub enforce_residency: Option<ResidencyRequirement>,
|
||||
#[experimental("configRequirements/read.network")]
|
||||
pub network: Option<NetworkRequirements>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct NetworkRequirements {
|
||||
pub enabled: Option<bool>,
|
||||
pub http_port: Option<u16>,
|
||||
pub socks_port: Option<u16>,
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -2071,6 +2089,9 @@ pub struct TurnStartParams {
|
||||
|
||||
/// EXPERIMENTAL - Set a pre-set collaboration mode.
|
||||
/// Takes precedence over model, reasoning_effort, and developer instructions if set.
|
||||
///
|
||||
/// For `collaboration_mode.settings.developer_instructions`, `null` means
|
||||
/// "use the built-in instructions for the selected mode".
|
||||
#[experimental("turn/start.collaborationMode")]
|
||||
#[ts(optional = nullable)]
|
||||
pub collaboration_mode: Option<CollaborationMode>,
|
||||
@@ -2500,6 +2521,7 @@ pub enum CommandExecutionStatus {
|
||||
pub enum CollabAgentTool {
|
||||
SpawnAgent,
|
||||
SendInput,
|
||||
ResumeAgent,
|
||||
Wait,
|
||||
CloseAgent,
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ Example (from OpenAI's official VSCode extension):
|
||||
- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success.
|
||||
- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications.
|
||||
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications.
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
|
||||
- `turn/steer` — add user input to an already in-flight turn without starting a new turn; returns the active `turnId` that accepted the input.
|
||||
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
|
||||
- `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review.
|
||||
@@ -116,7 +116,7 @@ Example (from OpenAI's official VSCode extension):
|
||||
- `config/read` — fetch the effective config on disk after resolving config layering.
|
||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
|
||||
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk.
|
||||
- `configRequirements/read` — fetch the loaded requirements allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`) and `enforceResidency` from `requirements.toml` and/or MDM (or `null` if none are configured).
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), `enforceResidency`, and `network` constraints.
|
||||
|
||||
### Example: Start or resume a thread
|
||||
|
||||
@@ -497,7 +497,7 @@ Today both notifications carry an empty `items` array even when item events were
|
||||
- `commandExecution` — `{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`.
|
||||
- `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`, `wait`, `close_agent`); `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`.
|
||||
- `webSearch` — `{id, query, action?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion.
|
||||
- `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool.
|
||||
- `enteredReviewMode` — `{id, review}` sent when the reviewer starts; `review` is a short user-facing label such as `"current changes"` or the requested target description.
|
||||
|
||||
@@ -596,6 +596,28 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.send_server_notification(ServerNotification::ItemCompleted(notification))
|
||||
.await;
|
||||
}
|
||||
EventMsg::CollabResumeBegin(begin_event) => {
|
||||
let item = collab_resume_begin_item(begin_event);
|
||||
let notification = ItemStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
item,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemStarted(notification))
|
||||
.await;
|
||||
}
|
||||
EventMsg::CollabResumeEnd(end_event) => {
|
||||
let item = collab_resume_end_item(end_event);
|
||||
let notification = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
item,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(notification))
|
||||
.await;
|
||||
}
|
||||
EventMsg::AgentMessageContentDelta(event) => {
|
||||
let codex_protocol::protocol::AgentMessageContentDeltaEvent { item_id, delta, .. } =
|
||||
event;
|
||||
@@ -1758,6 +1780,44 @@ async fn on_command_execution_request_approval_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn collab_resume_begin_item(
|
||||
begin_event: codex_core::protocol::CollabResumeBeginEvent,
|
||||
) -> ThreadItem {
|
||||
ThreadItem::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: V2CollabToolCallStatus::InProgress,
|
||||
sender_thread_id: begin_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![begin_event.receiver_thread_id.to_string()],
|
||||
prompt: None,
|
||||
agents_states: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn collab_resume_end_item(end_event: codex_core::protocol::CollabResumeEndEvent) -> ThreadItem {
|
||||
let status = match &end_event.status {
|
||||
codex_protocol::protocol::AgentStatus::Errored(_)
|
||||
| codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed,
|
||||
_ => V2CollabToolCallStatus::Completed,
|
||||
};
|
||||
let receiver_id = end_event.receiver_thread_id.to_string();
|
||||
let agents_states = [(
|
||||
receiver_id.clone(),
|
||||
V2CollabAgentStatus::from(end_event.status),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
ThreadItem::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status,
|
||||
sender_thread_id: end_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id],
|
||||
prompt: None,
|
||||
agents_states,
|
||||
}
|
||||
}
|
||||
|
||||
/// similar to handle_mcp_tool_call_begin in exec
|
||||
async fn construct_mcp_tool_call_notification(
|
||||
begin_event: McpToolCallBeginEvent,
|
||||
@@ -1838,6 +1898,8 @@ mod tests {
|
||||
use anyhow::anyhow;
|
||||
use anyhow::bail;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
use codex_core::protocol::CollabResumeBeginEvent;
|
||||
use codex_core::protocol::CollabResumeEndEvent;
|
||||
use codex_core::protocol::CreditsSnapshot;
|
||||
use codex_core::protocol::McpInvocation;
|
||||
use codex_core::protocol::RateLimitSnapshot;
|
||||
@@ -1882,6 +1944,55 @@ mod tests {
|
||||
assert_eq!(completion_status, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collab_resume_begin_maps_to_item_started_resume_agent() {
|
||||
let event = CollabResumeBeginEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
sender_thread_id: ThreadId::new(),
|
||||
receiver_thread_id: ThreadId::new(),
|
||||
};
|
||||
|
||||
let item = collab_resume_begin_item(event.clone());
|
||||
let expected = ThreadItem::CollabAgentToolCall {
|
||||
id: event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: V2CollabToolCallStatus::InProgress,
|
||||
sender_thread_id: event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![event.receiver_thread_id.to_string()],
|
||||
prompt: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
assert_eq!(item, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collab_resume_end_maps_to_item_completed_resume_agent() {
|
||||
let event = CollabResumeEndEvent {
|
||||
call_id: "call-2".to_string(),
|
||||
sender_thread_id: ThreadId::new(),
|
||||
receiver_thread_id: ThreadId::new(),
|
||||
status: codex_protocol::protocol::AgentStatus::NotFound,
|
||||
};
|
||||
|
||||
let item = collab_resume_end_item(event.clone());
|
||||
let receiver_id = event.receiver_thread_id.to_string();
|
||||
let expected = ThreadItem::CollabAgentToolCall {
|
||||
id: event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: V2CollabToolCallStatus::Failed,
|
||||
sender_thread_id: event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id.clone()],
|
||||
prompt: None,
|
||||
agents_states: [(
|
||||
receiver_id,
|
||||
V2CollabAgentStatus::from(codex_protocol::protocol::AgentStatus::NotFound),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
assert_eq!(item, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_error_records_message() -> Result<()> {
|
||||
let conversation_id = ThreadId::new();
|
||||
|
||||
@@ -206,6 +206,7 @@ use codex_login::ServerOptions as LoginServerOptions;
|
||||
use codex_login::ShutdownHandle;
|
||||
use codex_login::run_login_server;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
@@ -397,6 +398,27 @@ impl CodexMessageProcessor {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// If a client sends `developer_instructions: null` during a mode switch,
|
||||
/// use the built-in instructions for that mode.
|
||||
fn normalize_turn_start_collaboration_mode(
|
||||
&self,
|
||||
mut collaboration_mode: CollaborationMode,
|
||||
) -> CollaborationMode {
|
||||
if collaboration_mode.settings.developer_instructions.is_none()
|
||||
&& let Some(instructions) = self
|
||||
.thread_manager
|
||||
.list_collaboration_modes()
|
||||
.into_iter()
|
||||
.find(|preset| preset.mode == Some(collaboration_mode.mode))
|
||||
.and_then(|preset| preset.developer_instructions.flatten())
|
||||
.filter(|instructions| !instructions.is_empty())
|
||||
{
|
||||
collaboration_mode.settings.developer_instructions = Some(instructions);
|
||||
}
|
||||
|
||||
collaboration_mode
|
||||
}
|
||||
|
||||
fn review_request_from_target(
|
||||
target: ApiReviewTarget,
|
||||
) -> Result<(ReviewRequest, String), JSONRPCErrorError> {
|
||||
@@ -4554,6 +4576,10 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
let collaboration_mode = params
|
||||
.collaboration_mode
|
||||
.map(|mode| self.normalize_turn_start_collaboration_mode(mode));
|
||||
|
||||
// Map v2 input items to core input items.
|
||||
let mapped_items: Vec<CoreInputItem> = params
|
||||
.input
|
||||
@@ -4567,7 +4593,7 @@ impl CodexMessageProcessor {
|
||||
|| params.model.is_some()
|
||||
|| params.effort.is_some()
|
||||
|| params.summary.is_some()
|
||||
|| params.collaboration_mode.is_some()
|
||||
|| collaboration_mode.is_some()
|
||||
|| params.personality.is_some();
|
||||
|
||||
// If any overrides are provided, update the session turn context first.
|
||||
@@ -4581,7 +4607,7 @@ impl CodexMessageProcessor {
|
||||
model: params.model,
|
||||
effort: params.effort.map(Some),
|
||||
summary: params.summary,
|
||||
collaboration_mode: params.collaboration_mode,
|
||||
collaboration_mode,
|
||||
personality: params.personality,
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_app_server_protocol::ConfigValueWriteParams;
|
||||
use codex_app_server_protocol::ConfigWriteErrorCode;
|
||||
use codex_app_server_protocol::ConfigWriteResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::NetworkRequirements;
|
||||
use codex_app_server_protocol::SandboxMode;
|
||||
use codex_core::config::ConfigService;
|
||||
use codex_core::config::ConfigServiceError;
|
||||
@@ -129,6 +130,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
||||
enforce_residency: requirements
|
||||
.enforce_residency
|
||||
.map(map_residency_requirement_to_api),
|
||||
network: requirements.network.map(map_network_requirements_to_api),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +151,23 @@ fn map_residency_requirement_to_api(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_network_requirements_to_api(
|
||||
network: codex_core::config_loader::NetworkRequirementsToml,
|
||||
) -> NetworkRequirements {
|
||||
NetworkRequirements {
|
||||
enabled: network.enabled,
|
||||
http_port: network.http_port,
|
||||
socks_port: network.socks_port,
|
||||
allow_upstream_proxy: network.allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin: network.dangerously_allow_non_loopback_admin,
|
||||
allowed_domains: network.allowed_domains,
|
||||
denied_domains: network.denied_domains,
|
||||
allow_unix_sockets: network.allow_unix_sockets,
|
||||
allow_local_binding: network.allow_local_binding,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(err: ConfigServiceError) -> JSONRPCErrorError {
|
||||
if let Some(code) = err.write_error_code() {
|
||||
return config_write_error(code, err.to_string());
|
||||
@@ -174,6 +193,7 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) ->
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_core::config_loader::NetworkRequirementsToml as CoreNetworkRequirementsToml;
|
||||
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -194,6 +214,18 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: Some(CoreResidencyRequirement::Us),
|
||||
network: Some(CoreNetworkRequirementsToml {
|
||||
enabled: Some(true),
|
||||
http_port: Some(8080),
|
||||
socks_port: Some(1080),
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_admin: Some(false),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["example.com".to_string()]),
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
allow_local_binding: Some(true),
|
||||
}),
|
||||
};
|
||||
|
||||
let mapped = map_requirements_toml_to_api(requirements);
|
||||
@@ -217,6 +249,21 @@ mod tests {
|
||||
mapped.enforce_residency,
|
||||
Some(codex_app_server_protocol::ResidencyRequirement::Us),
|
||||
);
|
||||
assert_eq!(
|
||||
mapped.network,
|
||||
Some(NetworkRequirements {
|
||||
enabled: Some(true),
|
||||
http_port: Some(8080),
|
||||
socks_port: Some(1080),
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_admin: Some(false),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["example.com".to_string()]),
|
||||
allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]),
|
||||
allow_local_binding: Some(true),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -228,6 +275,7 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
};
|
||||
|
||||
let mapped = map_requirements_toml_to_api(requirements);
|
||||
|
||||
@@ -408,6 +408,8 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> {
|
||||
let request = response_mock.single_request();
|
||||
let payload = request.body_json();
|
||||
assert_eq!(payload["model"].as_str(), Some("mock-model-collab"));
|
||||
let payload_text = payload.to_string();
|
||||
assert!(payload_text.contains("The `request_user_input` tool is unavailable in Default mode."));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -385,6 +385,7 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -426,6 +427,7 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -470,6 +472,7 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 2);
|
||||
|
||||
@@ -5,7 +5,9 @@ use crate::error::Result as CodexResult;
|
||||
use crate::thread_manager::ThreadManagerState;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Weak;
|
||||
use tokio::sync::watch;
|
||||
@@ -39,7 +41,7 @@ impl AgentControl {
|
||||
&self,
|
||||
config: crate::config::Config,
|
||||
prompt: String,
|
||||
session_source: Option<codex_protocol::protocol::SessionSource>,
|
||||
session_source: Option<SessionSource>,
|
||||
) -> CodexResult<ThreadId> {
|
||||
let state = self.upgrade()?;
|
||||
let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
@@ -65,6 +67,32 @@ impl AgentControl {
|
||||
Ok(new_thread.thread_id)
|
||||
}
|
||||
|
||||
/// Resume an existing agent thread from a recorded rollout file.
|
||||
pub(crate) async fn resume_agent_from_rollout(
|
||||
&self,
|
||||
config: crate::config::Config,
|
||||
rollout_path: PathBuf,
|
||||
session_source: SessionSource,
|
||||
) -> CodexResult<ThreadId> {
|
||||
let state = self.upgrade()?;
|
||||
let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
|
||||
let resumed_thread = state
|
||||
.resume_thread_from_rollout_with_source(
|
||||
config,
|
||||
rollout_path,
|
||||
self.clone(),
|
||||
session_source,
|
||||
)
|
||||
.await?;
|
||||
reservation.commit(resumed_thread.thread_id);
|
||||
// Resumed threads are re-registered in-memory and need the same listener
|
||||
// attachment path as freshly spawned threads.
|
||||
state.notify_thread_created(resumed_thread.thread_id);
|
||||
|
||||
Ok(resumed_thread.thread_id)
|
||||
}
|
||||
|
||||
/// Send a `user` prompt to an existing agent thread.
|
||||
pub(crate) async fn send_prompt(
|
||||
&self,
|
||||
@@ -287,6 +315,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_agent_errors_when_manager_dropped() {
|
||||
let control = AgentControl::default();
|
||||
let (_home, config) = test_config().await;
|
||||
let err = control
|
||||
.resume_agent_from_rollout(
|
||||
config,
|
||||
PathBuf::from("/tmp/missing-rollout.jsonl"),
|
||||
SessionSource::Exec,
|
||||
)
|
||||
.await
|
||||
.expect_err("resume_agent should fail without a manager");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"unsupported operation: thread manager dropped"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_prompt_errors_when_thread_missing() {
|
||||
let harness = AgentControlHarness::new().await;
|
||||
@@ -518,4 +564,88 @@ mod tests {
|
||||
.await
|
||||
.expect("shutdown agent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_agent_respects_max_threads_limit() {
|
||||
let max_threads = 1usize;
|
||||
let (_home, config) = test_config_with_cli_overrides(vec![(
|
||||
"agents.max_threads".to_string(),
|
||||
TomlValue::Integer(max_threads as i64),
|
||||
)])
|
||||
.await;
|
||||
let manager = ThreadManager::with_models_provider_and_home(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.clone(),
|
||||
);
|
||||
let control = manager.agent_control();
|
||||
|
||||
let resumable_id = control
|
||||
.spawn_agent(config.clone(), "hello".to_string(), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let rollout_path = manager
|
||||
.get_thread(resumable_id)
|
||||
.await
|
||||
.expect("thread should exist")
|
||||
.rollout_path()
|
||||
.expect("rollout path should exist");
|
||||
let _ = control
|
||||
.shutdown_agent(resumable_id)
|
||||
.await
|
||||
.expect("shutdown resumable thread");
|
||||
|
||||
let active_id = control
|
||||
.spawn_agent(config.clone(), "occupy".to_string(), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed for active slot");
|
||||
|
||||
let err = control
|
||||
.resume_agent_from_rollout(config, rollout_path, SessionSource::Exec)
|
||||
.await
|
||||
.expect_err("resume should respect max threads");
|
||||
let CodexErr::AgentLimitReached {
|
||||
max_threads: seen_max_threads,
|
||||
} = err
|
||||
else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
};
|
||||
assert_eq!(seen_max_threads, max_threads);
|
||||
|
||||
let _ = control
|
||||
.shutdown_agent(active_id)
|
||||
.await
|
||||
.expect("shutdown active thread");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_agent_releases_slot_after_resume_failure() {
|
||||
let max_threads = 1usize;
|
||||
let (_home, config) = test_config_with_cli_overrides(vec![(
|
||||
"agents.max_threads".to_string(),
|
||||
TomlValue::Integer(max_threads as i64),
|
||||
)])
|
||||
.await;
|
||||
let manager = ThreadManager::with_models_provider_and_home(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.clone(),
|
||||
);
|
||||
let control = manager.agent_control();
|
||||
|
||||
let missing_rollout = config.codex_home.join("sessions/missing-rollout.jsonl");
|
||||
let _ = control
|
||||
.resume_agent_from_rollout(config.clone(), missing_rollout, SessionSource::Exec)
|
||||
.await
|
||||
.expect_err("resume should fail for missing rollout path");
|
||||
|
||||
let resumed_id = control
|
||||
.spawn_agent(config, "hello".to_string(), None)
|
||||
.await
|
||||
.expect("spawn should succeed after failed resume");
|
||||
let _ = control
|
||||
.shutdown_agent(resumed_id)
|
||||
.await
|
||||
.expect("shutdown resumed thread");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1653,6 +1653,7 @@ impl Config {
|
||||
mcp_servers,
|
||||
exec_policy: _,
|
||||
enforce_residency,
|
||||
network: _network_requirements,
|
||||
} = requirements;
|
||||
|
||||
apply_requirement_constrained_value(
|
||||
@@ -4379,6 +4380,7 @@ model_verbosity = "high"
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
};
|
||||
let requirement_source = crate::config_loader::RequirementSource::Unknown;
|
||||
let requirement_source_for_error = requirement_source.clone();
|
||||
@@ -4936,6 +4938,7 @@ mcp_oauth_callback_port = 5678
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
};
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
|
||||
@@ -81,6 +81,8 @@ pub struct ConfigRequirements {
|
||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||
pub(crate) exec_policy: Option<Sourced<RequirementsExecPolicy>>,
|
||||
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
|
||||
/// Managed network constraints derived from requirements.
|
||||
pub network: Option<Sourced<NetworkConstraints>>,
|
||||
}
|
||||
|
||||
impl Default for ConfigRequirements {
|
||||
@@ -101,6 +103,7 @@ impl Default for ConfigRequirements {
|
||||
mcp_servers: None,
|
||||
exec_policy: None,
|
||||
enforce_residency: ConstrainedWithSource::new(Constrained::allow_any(None), None),
|
||||
network: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,6 +126,64 @@ pub struct McpServerRequirement {
|
||||
pub identity: McpServerIdentity,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct NetworkRequirementsToml {
|
||||
pub enabled: Option<bool>,
|
||||
pub http_port: Option<u16>,
|
||||
pub socks_port: Option<u16>,
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
/// Normalized network constraints derived from requirements TOML.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct NetworkConstraints {
|
||||
pub enabled: Option<bool>,
|
||||
pub http_port: Option<u16>,
|
||||
pub socks_port: Option<u16>,
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
fn from(value: NetworkRequirementsToml) -> Self {
|
||||
let NetworkRequirementsToml {
|
||||
enabled,
|
||||
http_port,
|
||||
socks_port,
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin,
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
allow_local_binding,
|
||||
} = value;
|
||||
Self {
|
||||
enabled,
|
||||
http_port,
|
||||
socks_port,
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin,
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
allow_local_binding,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum WebSearchModeRequirement {
|
||||
@@ -170,6 +231,8 @@ pub struct ConfigRequirementsToml {
|
||||
pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
|
||||
pub rules: Option<RequirementsExecPolicyToml>,
|
||||
pub enforce_residency: Option<ResidencyRequirement>,
|
||||
#[serde(rename = "experimental_network")]
|
||||
pub network: Option<NetworkRequirementsToml>,
|
||||
}
|
||||
|
||||
/// Value paired with the requirement source it came from, for better error
|
||||
@@ -202,6 +265,7 @@ pub struct ConfigRequirementsWithSources {
|
||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
|
||||
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
|
||||
pub network: Option<Sourced<NetworkRequirementsToml>>,
|
||||
}
|
||||
|
||||
impl ConfigRequirementsWithSources {
|
||||
@@ -236,6 +300,7 @@ impl ConfigRequirementsWithSources {
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
network,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -248,6 +313,7 @@ impl ConfigRequirementsWithSources {
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
network,
|
||||
} = self;
|
||||
ConfigRequirementsToml {
|
||||
allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
|
||||
@@ -256,6 +322,7 @@ impl ConfigRequirementsWithSources {
|
||||
mcp_servers: mcp_servers.map(|sourced| sourced.value),
|
||||
rules: rules.map(|sourced| sourced.value),
|
||||
enforce_residency: enforce_residency.map(|sourced| sourced.value),
|
||||
network: network.map(|sourced| sourced.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,6 +368,7 @@ impl ConfigRequirementsToml {
|
||||
&& self.mcp_servers.is_none()
|
||||
&& self.rules.is_none()
|
||||
&& self.enforce_residency.is_none()
|
||||
&& self.network.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +383,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
network,
|
||||
} = toml;
|
||||
|
||||
let approval_policy = match allowed_approval_policies {
|
||||
@@ -471,6 +540,10 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
}
|
||||
None => ConstrainedWithSource::new(Constrained::allow_any(None), None),
|
||||
};
|
||||
let network = network.map(|sourced_network| {
|
||||
let Sourced { value, source } = sourced_network;
|
||||
Sourced::new(NetworkConstraints::from(value), source)
|
||||
});
|
||||
Ok(ConfigRequirements {
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
@@ -478,6 +551,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
mcp_servers,
|
||||
exec_policy,
|
||||
enforce_residency,
|
||||
network,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -506,6 +580,7 @@ mod tests {
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
network,
|
||||
} = toml;
|
||||
ConfigRequirementsWithSources {
|
||||
allowed_approval_policies: allowed_approval_policies
|
||||
@@ -518,6 +593,7 @@ mod tests {
|
||||
rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
enforce_residency: enforce_residency
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
network: network.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,6 +623,7 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: Some(enforce_residency),
|
||||
network: None,
|
||||
};
|
||||
|
||||
target.merge_unset_fields(source.clone(), other);
|
||||
@@ -566,6 +643,7 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
|
||||
network: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -597,6 +675,7 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
@@ -636,6 +715,7 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
@@ -951,6 +1031,50 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_requirements_are_preserved_as_constraints_with_source() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
[experimental_network]
|
||||
enabled = true
|
||||
allow_upstream_proxy = false
|
||||
allowed_domains = ["api.example.com", "*.openai.com"]
|
||||
denied_domains = ["blocked.example.com"]
|
||||
allow_unix_sockets = ["/tmp/example.sock"]
|
||||
allow_local_binding = false
|
||||
"#;
|
||||
|
||||
let source = RequirementSource::CloudRequirements;
|
||||
let mut requirements_with_sources = ConfigRequirementsWithSources::default();
|
||||
requirements_with_sources.merge_unset_fields(source.clone(), from_str(toml_str)?);
|
||||
|
||||
let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
|
||||
let sourced_network = requirements
|
||||
.network
|
||||
.expect("network requirements should be preserved as constraints");
|
||||
|
||||
assert_eq!(sourced_network.source, source);
|
||||
assert_eq!(sourced_network.value.enabled, Some(true));
|
||||
assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false));
|
||||
assert_eq!(
|
||||
sourced_network.value.allowed_domains.as_ref(),
|
||||
Some(&vec![
|
||||
"api.example.com".to_string(),
|
||||
"*.openai.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.denied_domains.as_ref(),
|
||||
Some(&vec!["blocked.example.com".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
sourced_network.value.allow_unix_sockets.as_ref(),
|
||||
Some(&vec!["/tmp/example.sock".to_string()])
|
||||
);
|
||||
assert_eq!(sourced_network.value.allow_local_binding, Some(false));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_mcp_server_requirements() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
|
||||
@@ -37,6 +37,8 @@ pub use config_requirements::ConfigRequirementsToml;
|
||||
pub use config_requirements::ConstrainedWithSource;
|
||||
pub use config_requirements::McpServerIdentity;
|
||||
pub use config_requirements::McpServerRequirement;
|
||||
pub use config_requirements::NetworkConstraints;
|
||||
pub use config_requirements::NetworkRequirementsToml;
|
||||
pub use config_requirements::RequirementSource;
|
||||
pub use config_requirements::ResidencyRequirement;
|
||||
pub use config_requirements::SandboxModeRequirement;
|
||||
|
||||
@@ -568,6 +568,7 @@ allowed_approval_policies = ["on-request"]
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -615,6 +616,7 @@ allowed_approval_policies = ["on-request"]
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
},
|
||||
);
|
||||
load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?;
|
||||
@@ -651,6 +653,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
};
|
||||
let expected = requirements.clone();
|
||||
let cloud_requirements = CloudRequirementsLoader::new(async move { Some(requirements) });
|
||||
|
||||
@@ -109,6 +109,8 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool {
|
||||
| EventMsg::CollabWaitingBegin(_)
|
||||
| EventMsg::CollabWaitingEnd(_)
|
||||
| EventMsg::CollabCloseBegin(_)
|
||||
| EventMsg::CollabCloseEnd(_) => false,
|
||||
| EventMsg::CollabCloseEnd(_)
|
||||
| EventMsg::CollabResumeBegin(_)
|
||||
| EventMsg::CollabResumeEnd(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,6 +405,25 @@ impl ThreadManagerState {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn resume_thread_from_rollout_with_source(
|
||||
&self,
|
||||
config: Config,
|
||||
rollout_path: PathBuf,
|
||||
agent_control: AgentControl,
|
||||
session_source: SessionSource,
|
||||
) -> CodexResult<NewThread> {
|
||||
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
|
||||
self.spawn_thread_with_source(
|
||||
config,
|
||||
initial_history,
|
||||
Arc::clone(&self.auth_manager),
|
||||
agent_control,
|
||||
session_source,
|
||||
Vec::new(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Spawn a new thread with optional history and register it with the manager.
|
||||
pub(crate) async fn spawn_thread(
|
||||
&self,
|
||||
|
||||
@@ -22,8 +22,12 @@ use codex_protocol::protocol::CollabAgentSpawnBeginEvent;
|
||||
use codex_protocol::protocol::CollabAgentSpawnEndEvent;
|
||||
use codex_protocol::protocol::CollabCloseBeginEvent;
|
||||
use codex_protocol::protocol::CollabCloseEndEvent;
|
||||
use codex_protocol::protocol::CollabResumeBeginEvent;
|
||||
use codex_protocol::protocol::CollabResumeEndEvent;
|
||||
use codex_protocol::protocol::CollabWaitingBeginEvent;
|
||||
use codex_protocol::protocol::CollabWaitingEndEvent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -71,6 +75,7 @@ impl ToolHandler for CollabHandler {
|
||||
match tool_name.as_str() {
|
||||
"spawn_agent" => spawn::handle(session, turn, call_id, arguments).await,
|
||||
"send_input" => send_input::handle(session, turn, call_id, arguments).await,
|
||||
"resume_agent" => resume_agent::handle(session, turn, call_id, arguments).await,
|
||||
"wait" => wait::handle(session, turn, call_id, arguments).await,
|
||||
"close_agent" => close_agent::handle(session, turn, call_id, arguments).await,
|
||||
other => Err(FunctionCallError::RespondToModel(format!(
|
||||
@@ -86,8 +91,6 @@ mod spawn {
|
||||
|
||||
use crate::agent::exceeds_thread_spawn_depth_limit;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -148,10 +151,7 @@ mod spawn {
|
||||
.spawn_agent(
|
||||
config,
|
||||
prompt.clone(),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: session.conversation_id,
|
||||
depth: child_depth,
|
||||
})),
|
||||
Some(thread_spawn_source(session.conversation_id, child_depth)),
|
||||
)
|
||||
.await
|
||||
.map_err(collab_spawn_error);
|
||||
@@ -279,6 +279,152 @@ mod send_input {
|
||||
}
|
||||
}
|
||||
|
||||
mod resume_agent {
|
||||
use super::*;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::rollout::find_thread_path_by_id_str;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ResumeAgentArgs {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub(super) struct ResumeAgentResult {
|
||||
pub(super) status: AgentStatus,
|
||||
}
|
||||
|
||||
pub async fn handle(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
call_id: String,
|
||||
arguments: String,
|
||||
) -> Result<ToolOutput, FunctionCallError> {
|
||||
let args: ResumeAgentArgs = parse_arguments(&arguments)?;
|
||||
let receiver_thread_id = agent_id(&args.id)?;
|
||||
let child_depth = next_thread_spawn_depth(&turn.session_source);
|
||||
if exceeds_thread_spawn_depth_limit(child_depth) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"Agent depth limit reached. Solve the task yourself.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabResumeBeginEvent {
|
||||
call_id: call_id.clone(),
|
||||
sender_thread_id: session.conversation_id,
|
||||
receiver_thread_id,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut status = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_status(receiver_thread_id)
|
||||
.await;
|
||||
let error = if matches!(status, AgentStatus::NotFound) {
|
||||
// If the thread is no longer active, attempt to restore it from rollout.
|
||||
match try_resume_closed_agent(
|
||||
&session,
|
||||
&turn,
|
||||
receiver_thread_id,
|
||||
&args.id,
|
||||
child_depth,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(resumed_status) => {
|
||||
status = resumed_status;
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
status = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_status(receiver_thread_id)
|
||||
.await;
|
||||
Some(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabResumeEndEvent {
|
||||
call_id,
|
||||
sender_thread_id: session.conversation_id,
|
||||
receiver_thread_id,
|
||||
status: status.clone(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(err) = error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let content = serde_json::to_string(&ResumeAgentResult { status }).map_err(|err| {
|
||||
FunctionCallError::Fatal(format!("failed to serialize resume_agent result: {err}"))
|
||||
})?;
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::Text(content),
|
||||
success: Some(true),
|
||||
})
|
||||
}
|
||||
|
||||
async fn try_resume_closed_agent(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
receiver_thread_id: ThreadId,
|
||||
receiver_id: &str,
|
||||
child_depth: i32,
|
||||
) -> Result<AgentStatus, FunctionCallError> {
|
||||
let rollout_path = find_thread_path_by_id_str(
|
||||
turn.config.codex_home.as_path(),
|
||||
receiver_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"tool failed: failed to locate rollout for agent {receiver_thread_id}: {err}"
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"agent with id {receiver_thread_id} not found"
|
||||
))
|
||||
})?;
|
||||
|
||||
let config = build_agent_resume_config(turn.as_ref(), child_depth)?;
|
||||
let resumed_thread_id = session
|
||||
.services
|
||||
.agent_control
|
||||
.resume_agent_from_rollout(
|
||||
config,
|
||||
rollout_path,
|
||||
thread_spawn_source(session.conversation_id, child_depth),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| collab_agent_error(receiver_thread_id, err))?;
|
||||
|
||||
Ok(session
|
||||
.services
|
||||
.agent_control
|
||||
.get_status(resumed_thread_id)
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
mod wait {
|
||||
use super::*;
|
||||
use crate::agent::status::is_final;
|
||||
@@ -585,14 +731,39 @@ fn collab_agent_error(agent_id: ThreadId, err: CodexErr) -> FunctionCallError {
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_spawn_source(parent_thread_id: ThreadId, depth: i32) -> SessionSource {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_agent_spawn_config(
|
||||
base_instructions: &BaseInstructions,
|
||||
turn: &TurnContext,
|
||||
child_depth: i32,
|
||||
) -> Result<Config, FunctionCallError> {
|
||||
let mut config = build_agent_shared_config(turn, child_depth)?;
|
||||
config.base_instructions = Some(base_instructions.text.clone());
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn build_agent_resume_config(
|
||||
turn: &TurnContext,
|
||||
child_depth: i32,
|
||||
) -> Result<Config, FunctionCallError> {
|
||||
let mut config = build_agent_shared_config(turn, child_depth)?;
|
||||
// For resume, keep base instructions sourced from rollout/session metadata.
|
||||
config.base_instructions = None;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn build_agent_shared_config(
|
||||
turn: &TurnContext,
|
||||
child_depth: i32,
|
||||
) -> Result<Config, FunctionCallError> {
|
||||
let base_config = turn.config.clone();
|
||||
let mut config = (*base_config).clone();
|
||||
config.base_instructions = Some(base_instructions.text.clone());
|
||||
config.model = Some(turn.model_info.slug.clone());
|
||||
config.model_provider = turn.provider.clone();
|
||||
config.model_reasoning_effort = turn.reasoning_effort;
|
||||
@@ -883,6 +1054,193 @@ mod tests {
|
||||
.expect("shutdown should submit");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_agent_rejects_invalid_id() {
|
||||
let (session, turn) = make_session_and_context().await;
|
||||
let invocation = invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"resume_agent",
|
||||
function_payload(json!({"id": "not-a-uuid"})),
|
||||
);
|
||||
let Err(err) = CollabHandler.handle(invocation).await else {
|
||||
panic!("invalid id should be rejected");
|
||||
};
|
||||
let FunctionCallError::RespondToModel(msg) = err else {
|
||||
panic!("expected respond-to-model error");
|
||||
};
|
||||
assert!(msg.starts_with("invalid agent id not-a-uuid:"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_agent_reports_missing_agent() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
let agent_id = ThreadId::new();
|
||||
let invocation = invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"resume_agent",
|
||||
function_payload(json!({"id": agent_id.to_string()})),
|
||||
);
|
||||
let Err(err) = CollabHandler.handle(invocation).await else {
|
||||
panic!("missing agent should be reported");
|
||||
};
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel(format!("agent with id {agent_id} not found"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_agent_noops_for_active_agent() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
let config = turn.config.as_ref().clone();
|
||||
let thread = manager.start_thread(config).await.expect("start thread");
|
||||
let agent_id = thread.thread_id;
|
||||
let status_before = manager.agent_control().get_status(agent_id).await;
|
||||
let invocation = invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"resume_agent",
|
||||
function_payload(json!({"id": agent_id.to_string()})),
|
||||
);
|
||||
|
||||
let output = CollabHandler
|
||||
.handle(invocation)
|
||||
.await
|
||||
.expect("resume_agent should succeed");
|
||||
let ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::Text(content),
|
||||
success,
|
||||
..
|
||||
} = output
|
||||
else {
|
||||
panic!("expected function output");
|
||||
};
|
||||
let result: resume_agent::ResumeAgentResult =
|
||||
serde_json::from_str(&content).expect("resume_agent result should be json");
|
||||
assert_eq!(result.status, status_before);
|
||||
assert_eq!(success, Some(true));
|
||||
|
||||
let thread_ids = manager.list_thread_ids().await;
|
||||
assert_eq!(thread_ids, vec![agent_id]);
|
||||
|
||||
let _ = thread
|
||||
.thread
|
||||
.submit(Op::Shutdown {})
|
||||
.await
|
||||
.expect("shutdown should submit");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_agent_restores_closed_agent_and_accepts_send_input() {
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
let config = turn.config.as_ref().clone();
|
||||
let thread = manager.start_thread(config).await.expect("start thread");
|
||||
let agent_id = thread.thread_id;
|
||||
let _ = manager
|
||||
.agent_control()
|
||||
.shutdown_agent(agent_id)
|
||||
.await
|
||||
.expect("shutdown agent");
|
||||
assert_eq!(
|
||||
manager.agent_control().get_status(agent_id).await,
|
||||
AgentStatus::NotFound
|
||||
);
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
let resume_invocation = invocation(
|
||||
session.clone(),
|
||||
turn.clone(),
|
||||
"resume_agent",
|
||||
function_payload(json!({"id": agent_id.to_string()})),
|
||||
);
|
||||
let output = CollabHandler
|
||||
.handle(resume_invocation)
|
||||
.await
|
||||
.expect("resume_agent should succeed");
|
||||
let ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::Text(content),
|
||||
success,
|
||||
..
|
||||
} = output
|
||||
else {
|
||||
panic!("expected function output");
|
||||
};
|
||||
let result: resume_agent::ResumeAgentResult =
|
||||
serde_json::from_str(&content).expect("resume_agent result should be json");
|
||||
assert_ne!(result.status, AgentStatus::NotFound);
|
||||
assert_eq!(success, Some(true));
|
||||
|
||||
let send_invocation = invocation(
|
||||
session,
|
||||
turn,
|
||||
"send_input",
|
||||
function_payload(json!({"id": agent_id.to_string(), "message": "hello"})),
|
||||
);
|
||||
let output = CollabHandler
|
||||
.handle(send_invocation)
|
||||
.await
|
||||
.expect("send_input should succeed after resume");
|
||||
let ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::Text(content),
|
||||
success,
|
||||
..
|
||||
} = output
|
||||
else {
|
||||
panic!("expected function output");
|
||||
};
|
||||
let result: serde_json::Value =
|
||||
serde_json::from_str(&content).expect("send_input result should be json");
|
||||
let submission_id = result
|
||||
.get("submission_id")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
assert!(!submission_id.is_empty());
|
||||
assert_eq!(success, Some(true));
|
||||
|
||||
let _ = manager
|
||||
.agent_control()
|
||||
.shutdown_agent(agent_id)
|
||||
.await
|
||||
.expect("shutdown resumed agent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_agent_rejects_when_depth_limit_exceeded() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
|
||||
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: session.conversation_id,
|
||||
depth: MAX_THREAD_SPAWN_DEPTH,
|
||||
});
|
||||
|
||||
let invocation = invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
"resume_agent",
|
||||
function_payload(json!({"id": ThreadId::new().to_string()})),
|
||||
);
|
||||
let Err(err) = CollabHandler.handle(invocation).await else {
|
||||
panic!("resume should fail when depth limit exceeded");
|
||||
};
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel(
|
||||
"Agent depth limit reached. Solve the task yourself.".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
struct WaitResult {
|
||||
status: HashMap<ThreadId, AgentStatus>,
|
||||
@@ -1259,4 +1617,35 @@ mod tests {
|
||||
|
||||
assert_eq!(config.user_instructions, base_config.user_instructions);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_agent_resume_config_clears_base_instructions() {
|
||||
let (_session, mut turn) = make_session_and_context().await;
|
||||
let mut base_config = (*turn.config).clone();
|
||||
base_config.base_instructions = Some("caller-base".to_string());
|
||||
turn.config = Arc::new(base_config);
|
||||
|
||||
let config = build_agent_resume_config(&turn, 0).expect("resume config");
|
||||
|
||||
let mut expected = (*turn.config).clone();
|
||||
expected.base_instructions = None;
|
||||
expected.model = Some(turn.model_info.slug.clone());
|
||||
expected.model_provider = turn.provider.clone();
|
||||
expected.model_reasoning_effort = turn.reasoning_effort;
|
||||
expected.model_reasoning_summary = turn.reasoning_summary;
|
||||
expected.developer_instructions = turn.developer_instructions.clone();
|
||||
expected.compact_prompt = turn.compact_prompt.clone();
|
||||
expected.shell_environment_policy = turn.shell_environment_policy.clone();
|
||||
expected.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone();
|
||||
expected.cwd = turn.cwd.clone();
|
||||
expected
|
||||
.approval_policy
|
||||
.set(turn.approval_policy)
|
||||
.expect("approval policy set");
|
||||
expected
|
||||
.sandbox_policy
|
||||
.set(turn.sandbox_policy)
|
||||
.expect("sandbox policy set");
|
||||
assert_eq!(config, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,10 @@ pub(crate) fn build_command_spec(
|
||||
/// the original script:
|
||||
///
|
||||
/// shell -lc "<script>"
|
||||
/// => shell -c ". SNAPSHOT && <script>"
|
||||
/// => user_shell -c ". SNAPSHOT (best effort); exec shell -c <script>"
|
||||
///
|
||||
/// On non-POSIX shells or non-matching commands this is a no-op.
|
||||
/// This wrapper script uses POSIX constructs (`if`, `.`, `exec`) so it can
|
||||
/// be run by Bash/Zsh/sh. On non-matching commands this is a no-op.
|
||||
pub(crate) fn maybe_wrap_shell_lc_with_snapshot(
|
||||
command: &[String],
|
||||
session_shell: &Shell,
|
||||
@@ -72,10 +73,148 @@ pub(crate) fn maybe_wrap_shell_lc_with_snapshot(
|
||||
}
|
||||
|
||||
let snapshot_path = snapshot.path.to_string_lossy();
|
||||
let rewritten_script = format!(". \"{snapshot_path}\" && {}", command[2]);
|
||||
let shell_path = session_shell.shell_path.to_string_lossy();
|
||||
let original_shell = shell_single_quote(&command[0]);
|
||||
let original_script = shell_single_quote(&command[2]);
|
||||
let snapshot_path = shell_single_quote(snapshot_path.as_ref());
|
||||
let trailing_args = command[3..]
|
||||
.iter()
|
||||
.map(|arg| format!(" '{}'", shell_single_quote(arg)))
|
||||
.collect::<String>();
|
||||
let rewritten_script = format!(
|
||||
"if . '{snapshot_path}' >/dev/null 2>&1; then :; fi; exec '{original_shell}' -c '{original_script}'{trailing_args}"
|
||||
);
|
||||
|
||||
let mut rewritten = command.to_vec();
|
||||
rewritten[1] = "-c".to_string();
|
||||
rewritten[2] = rewritten_script;
|
||||
rewritten
|
||||
vec![shell_path.to_string(), "-c".to_string(), rewritten_script]
|
||||
}
|
||||
|
||||
fn shell_single_quote(input: &str) -> String {
|
||||
input.replace('\'', r#"'"'"'"#)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shell::ShellType;
|
||||
use crate::shell_snapshot::ShellSnapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
use tokio::sync::watch;
|
||||
|
||||
fn shell_with_snapshot(
|
||||
shell_type: ShellType,
|
||||
shell_path: &str,
|
||||
snapshot_path: PathBuf,
|
||||
) -> Shell {
|
||||
let (_tx, shell_snapshot) = watch::channel(Some(Arc::new(ShellSnapshot {
|
||||
path: snapshot_path,
|
||||
})));
|
||||
Shell {
|
||||
shell_type,
|
||||
shell_path: PathBuf::from(shell_path),
|
||||
shell_snapshot,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_bootstraps_in_user_shell() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let snapshot_path = dir.path().join("snapshot.sh");
|
||||
std::fs::write(&snapshot_path, "# Snapshot file\n").expect("write snapshot");
|
||||
let session_shell = shell_with_snapshot(ShellType::Zsh, "/bin/zsh", snapshot_path);
|
||||
let command = vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"echo hello".to_string(),
|
||||
];
|
||||
|
||||
let rewritten = maybe_wrap_shell_lc_with_snapshot(&command, &session_shell);
|
||||
|
||||
assert_eq!(rewritten[0], "/bin/zsh");
|
||||
assert_eq!(rewritten[1], "-c");
|
||||
assert!(rewritten[2].contains("if . '"));
|
||||
assert!(rewritten[2].contains("exec '/bin/bash' -c 'echo hello'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_escapes_single_quotes() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let snapshot_path = dir.path().join("snapshot.sh");
|
||||
std::fs::write(&snapshot_path, "# Snapshot file\n").expect("write snapshot");
|
||||
let session_shell = shell_with_snapshot(ShellType::Zsh, "/bin/zsh", snapshot_path);
|
||||
let command = vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"echo 'hello'".to_string(),
|
||||
];
|
||||
|
||||
let rewritten = maybe_wrap_shell_lc_with_snapshot(&command, &session_shell);
|
||||
|
||||
assert!(rewritten[2].contains(r#"exec '/bin/bash' -c 'echo '"'"'hello'"'"''"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_uses_bash_bootstrap_shell() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let snapshot_path = dir.path().join("snapshot.sh");
|
||||
std::fs::write(&snapshot_path, "# Snapshot file\n").expect("write snapshot");
|
||||
let session_shell = shell_with_snapshot(ShellType::Bash, "/bin/bash", snapshot_path);
|
||||
let command = vec![
|
||||
"/bin/zsh".to_string(),
|
||||
"-lc".to_string(),
|
||||
"echo hello".to_string(),
|
||||
];
|
||||
|
||||
let rewritten = maybe_wrap_shell_lc_with_snapshot(&command, &session_shell);
|
||||
|
||||
assert_eq!(rewritten[0], "/bin/bash");
|
||||
assert_eq!(rewritten[1], "-c");
|
||||
assert!(rewritten[2].contains("if . '"));
|
||||
assert!(rewritten[2].contains("exec '/bin/zsh' -c 'echo hello'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_uses_sh_bootstrap_shell() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let snapshot_path = dir.path().join("snapshot.sh");
|
||||
std::fs::write(&snapshot_path, "# Snapshot file\n").expect("write snapshot");
|
||||
let session_shell = shell_with_snapshot(ShellType::Sh, "/bin/sh", snapshot_path);
|
||||
let command = vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"echo hello".to_string(),
|
||||
];
|
||||
|
||||
let rewritten = maybe_wrap_shell_lc_with_snapshot(&command, &session_shell);
|
||||
|
||||
assert_eq!(rewritten[0], "/bin/sh");
|
||||
assert_eq!(rewritten[1], "-c");
|
||||
assert!(rewritten[2].contains("if . '"));
|
||||
assert!(rewritten[2].contains("exec '/bin/bash' -c 'echo hello'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_preserves_trailing_args() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let snapshot_path = dir.path().join("snapshot.sh");
|
||||
std::fs::write(&snapshot_path, "# Snapshot file\n").expect("write snapshot");
|
||||
let session_shell = shell_with_snapshot(ShellType::Zsh, "/bin/zsh", snapshot_path);
|
||||
let command = vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"printf '%s %s' \"$0\" \"$1\"".to_string(),
|
||||
"arg0".to_string(),
|
||||
"arg1".to_string(),
|
||||
];
|
||||
|
||||
let rewritten = maybe_wrap_shell_lc_with_snapshot(&command, &session_shell);
|
||||
|
||||
assert!(
|
||||
rewritten[2].contains(
|
||||
r#"exec '/bin/bash' -c 'printf '"'"'%s %s'"'"' "$0" "$1"' 'arg0' 'arg1'"#
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,6 +522,29 @@ fn create_send_input_tool() -> ToolSpec {
|
||||
})
|
||||
}
|
||||
|
||||
fn create_resume_agent_tool() -> ToolSpec {
|
||||
let mut properties = BTreeMap::new();
|
||||
properties.insert(
|
||||
"id".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Agent id to resume.".to_string()),
|
||||
},
|
||||
);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "resume_agent".to_string(),
|
||||
description:
|
||||
"Resume a previously closed agent by id so it can receive send_input and wait calls."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["id".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn create_wait_tool() -> ToolSpec {
|
||||
let mut properties = BTreeMap::new();
|
||||
properties.insert(
|
||||
@@ -1410,10 +1433,12 @@ pub(crate) fn build_specs(
|
||||
let collab_handler = Arc::new(CollabHandler);
|
||||
builder.push_spec(create_spawn_agent_tool());
|
||||
builder.push_spec(create_send_input_tool());
|
||||
builder.push_spec(create_resume_agent_tool());
|
||||
builder.push_spec(create_wait_tool());
|
||||
builder.push_spec(create_close_agent_tool());
|
||||
builder.register_handler("spawn_agent", collab_handler.clone());
|
||||
builder.register_handler("send_input", collab_handler.clone());
|
||||
builder.register_handler("resume_agent", collab_handler.clone());
|
||||
builder.register_handler("wait", collab_handler.clone());
|
||||
builder.register_handler("close_agent", collab_handler);
|
||||
}
|
||||
@@ -1670,7 +1695,13 @@ mod tests {
|
||||
let (tools, _) = build_specs(&tools_config, None, &[]).build();
|
||||
assert_contains_tool_names(
|
||||
&tools,
|
||||
&["spawn_agent", "send_input", "wait", "close_agent"],
|
||||
&[
|
||||
"spawn_agent",
|
||||
"send_input",
|
||||
"resume_agent",
|
||||
"wait",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,8 @@ Fetch the built-in collaboration mode presets with `collaborationMode/list`. Thi
|
||||
- `data` – ordered list of collaboration mode masks (partial settings to apply on top of the base mode)
|
||||
- For tri-state fields like `reasoning_effort` and `developer_instructions`, omit the field to keep the current value, set it to `null` to clear it, or set a concrete value to update it.
|
||||
|
||||
When sending `turn/start` with `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
|
||||
|
||||
## Event stream
|
||||
|
||||
While a conversation runs, the server sends notifications:
|
||||
|
||||
@@ -796,6 +796,8 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
| EventMsg::UndoStarted(_)
|
||||
| EventMsg::ThreadRolledBack(_)
|
||||
| EventMsg::RequestUserInput(_)
|
||||
| EventMsg::CollabResumeBegin(_)
|
||||
| EventMsg::CollabResumeEnd(_)
|
||||
| EventMsg::DynamicToolCallRequest(_) => {}
|
||||
}
|
||||
CodexStatus::Running
|
||||
|
||||
@@ -681,13 +681,41 @@ mod tests {
|
||||
}
|
||||
|
||||
impl RecordingReporter {
|
||||
fn wait_for_complete(&self, timeout: Duration) -> bool {
|
||||
let completes = self.complete_times.lock().unwrap();
|
||||
if !completes.is_empty() {
|
||||
return true;
|
||||
fn wait_until<T, F>(
|
||||
&self,
|
||||
mutex: &Mutex<T>,
|
||||
cv: &Condvar,
|
||||
timeout: Duration,
|
||||
mut predicate: F,
|
||||
) -> bool
|
||||
where
|
||||
F: FnMut(&T) -> bool,
|
||||
{
|
||||
let deadline = Instant::now() + timeout;
|
||||
let mut state = mutex.lock().unwrap();
|
||||
loop {
|
||||
if predicate(&state) {
|
||||
return true;
|
||||
}
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
return false;
|
||||
}
|
||||
let (next_state, wait_result) = cv.wait_timeout(state, remaining).unwrap();
|
||||
state = next_state;
|
||||
if wait_result.timed_out() {
|
||||
return predicate(&state);
|
||||
}
|
||||
}
|
||||
let (completes, _) = self.complete_cv.wait_timeout(completes, timeout).unwrap();
|
||||
!completes.is_empty()
|
||||
}
|
||||
|
||||
fn wait_for_complete(&self, timeout: Duration) -> bool {
|
||||
self.wait_until(
|
||||
&self.complete_times,
|
||||
&self.complete_cv,
|
||||
timeout,
|
||||
|completes| !completes.is_empty(),
|
||||
)
|
||||
}
|
||||
fn clear(&self) {
|
||||
self.updates.lock().unwrap().clear();
|
||||
@@ -699,12 +727,9 @@ mod tests {
|
||||
}
|
||||
|
||||
fn wait_for_updates_at_least(&self, min_len: usize, timeout: Duration) -> bool {
|
||||
let updates = self.updates.lock().unwrap();
|
||||
if updates.len() >= min_len {
|
||||
return true;
|
||||
}
|
||||
let (updates, _) = self.update_cv.wait_timeout(updates, timeout).unwrap();
|
||||
updates.len() >= min_len
|
||||
self.wait_until(&self.updates, &self.update_cv, timeout, |updates| {
|
||||
updates.len() >= min_len
|
||||
})
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> FileSearchSnapshot {
|
||||
|
||||
@@ -364,6 +364,8 @@ async fn run_codex_tool_session_inner(
|
||||
| EventMsg::CollabWaitingEnd(_)
|
||||
| EventMsg::CollabCloseBegin(_)
|
||||
| EventMsg::CollabCloseEnd(_)
|
||||
| EventMsg::CollabResumeBegin(_)
|
||||
| EventMsg::CollabResumeEnd(_)
|
||||
| EventMsg::DeprecationNotice(_) => {
|
||||
// For now, we do not do anything extra for these
|
||||
// events. Note that
|
||||
|
||||
@@ -505,6 +505,9 @@ base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
|
||||
[features]
|
||||
remote_models = false
|
||||
"#
|
||||
),
|
||||
)
|
||||
|
||||
@@ -879,6 +879,10 @@ pub enum EventMsg {
|
||||
CollabCloseBegin(CollabCloseBeginEvent),
|
||||
/// Collab interaction: close end.
|
||||
CollabCloseEnd(CollabCloseEndEvent),
|
||||
/// Collab interaction: resume begin.
|
||||
CollabResumeBegin(CollabResumeBeginEvent),
|
||||
/// Collab interaction: resume end.
|
||||
CollabResumeEnd(CollabResumeEndEvent),
|
||||
}
|
||||
|
||||
impl From<CollabAgentSpawnBeginEvent> for EventMsg {
|
||||
@@ -929,6 +933,18 @@ impl From<CollabCloseEndEvent> for EventMsg {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CollabResumeBeginEvent> for EventMsg {
|
||||
fn from(event: CollabResumeBeginEvent) -> Self {
|
||||
EventMsg::CollabResumeBegin(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CollabResumeEndEvent> for EventMsg {
|
||||
fn from(event: CollabResumeEndEvent) -> Self {
|
||||
EventMsg::CollabResumeEnd(event)
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent lifecycle status, derived from emitted events.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -2476,6 +2492,29 @@ pub struct CollabCloseEndEvent {
|
||||
pub status: AgentStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
pub struct CollabResumeBeginEvent {
|
||||
/// Identifier for the collab tool call.
|
||||
pub call_id: String,
|
||||
/// Thread ID of the sender.
|
||||
pub sender_thread_id: ThreadId,
|
||||
/// Thread ID of the receiver.
|
||||
pub receiver_thread_id: ThreadId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
pub struct CollabResumeEndEvent {
|
||||
/// Identifier for the collab tool call.
|
||||
pub call_id: String,
|
||||
/// Thread ID of the sender.
|
||||
pub sender_thread_id: ThreadId,
|
||||
/// Thread ID of the receiver.
|
||||
pub receiver_thread_id: ThreadId,
|
||||
/// Last known status of the receiver agent reported to the sender agent after
|
||||
/// resume.
|
||||
pub status: AgentStatus,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -4024,6 +4024,8 @@ impl ChatWidget {
|
||||
EventMsg::CollabWaitingEnd(ev) => self.on_collab_event(collab::waiting_end(ev)),
|
||||
EventMsg::CollabCloseBegin(_) => {}
|
||||
EventMsg::CollabCloseEnd(ev) => self.on_collab_event(collab::close_end(ev)),
|
||||
EventMsg::CollabResumeBegin(ev) => self.on_collab_event(collab::resume_begin(ev)),
|
||||
EventMsg::CollabResumeEnd(ev) => self.on_collab_event(collab::resume_end(ev)),
|
||||
EventMsg::ThreadRolledBack(_) => {}
|
||||
EventMsg::RawResponseItem(_)
|
||||
| EventMsg::ItemStarted(_)
|
||||
|
||||
@@ -5,6 +5,8 @@ use codex_core::protocol::AgentStatus;
|
||||
use codex_core::protocol::CollabAgentInteractionEndEvent;
|
||||
use codex_core::protocol::CollabAgentSpawnEndEvent;
|
||||
use codex_core::protocol::CollabCloseEndEvent;
|
||||
use codex_core::protocol::CollabResumeBeginEvent;
|
||||
use codex_core::protocol::CollabResumeEndEvent;
|
||||
use codex_core::protocol::CollabWaitingBeginEvent;
|
||||
use codex_core::protocol::CollabWaitingEndEvent;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -97,6 +99,34 @@ pub(crate) fn close_end(ev: CollabCloseEndEvent) -> PlainHistoryCell {
|
||||
collab_event("Agent closed", details)
|
||||
}
|
||||
|
||||
pub(crate) fn resume_begin(ev: CollabResumeBeginEvent) -> PlainHistoryCell {
|
||||
let CollabResumeBeginEvent {
|
||||
call_id,
|
||||
sender_thread_id: _,
|
||||
receiver_thread_id,
|
||||
} = ev;
|
||||
let details = vec![
|
||||
detail_line("call", call_id),
|
||||
detail_line("receiver", receiver_thread_id.to_string()),
|
||||
];
|
||||
collab_event("Resuming agent", details)
|
||||
}
|
||||
|
||||
pub(crate) fn resume_end(ev: CollabResumeEndEvent) -> PlainHistoryCell {
|
||||
let CollabResumeEndEvent {
|
||||
call_id,
|
||||
sender_thread_id: _,
|
||||
receiver_thread_id,
|
||||
status,
|
||||
} = ev;
|
||||
let details = vec![
|
||||
detail_line("call", call_id),
|
||||
detail_line("receiver", receiver_thread_id.to_string()),
|
||||
status_line(&status),
|
||||
];
|
||||
collab_event("Agent resumed", details)
|
||||
}
|
||||
|
||||
fn collab_event(title: impl Into<String>, details: Vec<Line<'static>>) -> PlainHistoryCell {
|
||||
let title = title.into();
|
||||
let mut lines: Vec<Line<'static>> =
|
||||
|
||||
@@ -3,6 +3,7 @@ use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::ConfigLayerStack;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::config_loader::NetworkConstraints;
|
||||
use codex_core::config_loader::RequirementSource;
|
||||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
@@ -115,6 +116,14 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(network) = requirements.network.as_ref() {
|
||||
requirement_lines.push(requirement_line(
|
||||
"experimental_network",
|
||||
format_network_constraints(&network.value),
|
||||
Some(&network.source),
|
||||
));
|
||||
}
|
||||
|
||||
if requirement_lines.is_empty() {
|
||||
lines.push(" <none>".dim().into());
|
||||
} else {
|
||||
@@ -199,6 +208,63 @@ fn format_residency_requirement(requirement: ResidencyRequirement) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_network_constraints(network: &NetworkConstraints) -> String {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
let NetworkConstraints {
|
||||
enabled,
|
||||
http_port,
|
||||
socks_port,
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin,
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
allow_unix_sockets,
|
||||
allow_local_binding,
|
||||
} = network;
|
||||
|
||||
if let Some(enabled) = enabled {
|
||||
parts.push(format!("enabled={enabled}"));
|
||||
}
|
||||
if let Some(http_port) = http_port {
|
||||
parts.push(format!("http_port={http_port}"));
|
||||
}
|
||||
if let Some(socks_port) = socks_port {
|
||||
parts.push(format!("socks_port={socks_port}"));
|
||||
}
|
||||
if let Some(allow_upstream_proxy) = allow_upstream_proxy {
|
||||
parts.push(format!("allow_upstream_proxy={allow_upstream_proxy}"));
|
||||
}
|
||||
if let Some(dangerously_allow_non_loopback_proxy) = dangerously_allow_non_loopback_proxy {
|
||||
parts.push(format!(
|
||||
"dangerously_allow_non_loopback_proxy={dangerously_allow_non_loopback_proxy}"
|
||||
));
|
||||
}
|
||||
if let Some(dangerously_allow_non_loopback_admin) = dangerously_allow_non_loopback_admin {
|
||||
parts.push(format!(
|
||||
"dangerously_allow_non_loopback_admin={dangerously_allow_non_loopback_admin}"
|
||||
));
|
||||
}
|
||||
if let Some(allowed_domains) = allowed_domains {
|
||||
parts.push(format!("allowed_domains=[{}]", allowed_domains.join(", ")));
|
||||
}
|
||||
if let Some(denied_domains) = denied_domains {
|
||||
parts.push(format!("denied_domains=[{}]", denied_domains.join(", ")));
|
||||
}
|
||||
if let Some(allow_unix_sockets) = allow_unix_sockets {
|
||||
parts.push(format!(
|
||||
"allow_unix_sockets=[{}]",
|
||||
allow_unix_sockets.join(", ")
|
||||
));
|
||||
}
|
||||
if let Some(allow_local_binding) = allow_local_binding {
|
||||
parts.push(format!("allow_local_binding={allow_local_binding}"));
|
||||
}
|
||||
|
||||
join_or_empty(parts)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::render_debug_config_lines;
|
||||
@@ -211,6 +277,7 @@ mod tests {
|
||||
use codex_core::config_loader::ConstrainedWithSource;
|
||||
use codex_core::config_loader::McpServerIdentity;
|
||||
use codex_core::config_loader::McpServerRequirement;
|
||||
use codex_core::config_loader::NetworkConstraints;
|
||||
use codex_core::config_loader::RequirementSource;
|
||||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
@@ -323,6 +390,14 @@ mod tests {
|
||||
Constrained::allow_any(WebSearchMode::Cached),
|
||||
Some(RequirementSource::CloudRequirements),
|
||||
);
|
||||
requirements.network = Some(Sourced::new(
|
||||
NetworkConstraints {
|
||||
enabled: Some(true),
|
||||
allowed_domains: Some(vec!["example.com".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
RequirementSource::CloudRequirements,
|
||||
));
|
||||
|
||||
let requirements_toml = ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
@@ -338,6 +413,7 @@ mod tests {
|
||||
)])),
|
||||
rules: None,
|
||||
enforce_residency: Some(ResidencyRequirement::Us),
|
||||
network: None,
|
||||
};
|
||||
|
||||
let user_file = if cfg!(windows) {
|
||||
@@ -375,6 +451,9 @@ mod tests {
|
||||
);
|
||||
assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))"));
|
||||
assert!(rendered.contains("enforce_residency: us (source: cloud requirements)"));
|
||||
assert!(rendered.contains(
|
||||
"experimental_network: enabled=true, allowed_domains=[example.com] (source: cloud requirements)"
|
||||
));
|
||||
assert!(!rendered.contains(" - rules:"));
|
||||
}
|
||||
|
||||
@@ -393,6 +472,7 @@ mod tests {
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
};
|
||||
|
||||
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
|
||||
|
||||
@@ -93,6 +93,57 @@ async fn collect_output_until_exit(
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_python_repl_ready(
|
||||
writer: &tokio::sync::mpsc::Sender<Vec<u8>>,
|
||||
output_rx: &mut tokio::sync::broadcast::Receiver<Vec<u8>>,
|
||||
timeout_ms: u64,
|
||||
newline: &str,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let mut collected = Vec::new();
|
||||
let marker = "__codex_pty_ready__";
|
||||
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
|
||||
let probe_window = tokio::time::Duration::from_millis(if cfg!(windows) { 750 } else { 250 });
|
||||
|
||||
while tokio::time::Instant::now() < deadline {
|
||||
writer
|
||||
.send(format!("print('{marker}'){newline}").into_bytes())
|
||||
.await?;
|
||||
|
||||
let probe_deadline = tokio::time::Instant::now() + probe_window;
|
||||
loop {
|
||||
let now = tokio::time::Instant::now();
|
||||
if now >= deadline || now >= probe_deadline {
|
||||
break;
|
||||
}
|
||||
let remaining = std::cmp::min(
|
||||
deadline.saturating_duration_since(now),
|
||||
probe_deadline.saturating_duration_since(now),
|
||||
);
|
||||
match tokio::time::timeout(remaining, output_rx.recv()).await {
|
||||
Ok(Ok(chunk)) => {
|
||||
collected.extend_from_slice(&chunk);
|
||||
if String::from_utf8_lossy(&collected).contains(marker) {
|
||||
return Ok(collected);
|
||||
}
|
||||
}
|
||||
Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
|
||||
Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
|
||||
anyhow::bail!(
|
||||
"PTY output closed while waiting for Python REPL readiness: {:?}",
|
||||
String::from_utf8_lossy(&collected)
|
||||
);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!(
|
||||
"timed out waiting for Python REPL readiness in PTY: {:?}",
|
||||
String::from_utf8_lossy(&collected)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn pty_python_repl_emits_output_and_exits() -> anyhow::Result<()> {
|
||||
let Some(python) = find_python() else {
|
||||
@@ -103,15 +154,20 @@ async fn pty_python_repl_emits_output_and_exits() -> anyhow::Result<()> {
|
||||
let env_map: HashMap<String, String> = std::env::vars().collect();
|
||||
let spawned = spawn_pty_process(&python, &[], Path::new("."), &env_map, &None).await?;
|
||||
let writer = spawned.session.writer_sender();
|
||||
let mut output_rx = spawned.output_rx;
|
||||
let newline = if cfg!(windows) { "\r\n" } else { "\n" };
|
||||
let startup_timeout_ms = if cfg!(windows) { 10_000 } else { 5_000 };
|
||||
let mut output =
|
||||
wait_for_python_repl_ready(&writer, &mut output_rx, startup_timeout_ms, newline).await?;
|
||||
writer
|
||||
.send(format!("print('hello from pty'){newline}").into_bytes())
|
||||
.await?;
|
||||
writer.send(format!("exit(){newline}").into_bytes()).await?;
|
||||
|
||||
let timeout_ms = if cfg!(windows) { 10_000 } else { 5_000 };
|
||||
let (output, code) =
|
||||
collect_output_until_exit(spawned.output_rx, spawned.exit_rx, timeout_ms).await;
|
||||
let (remaining_output, code) =
|
||||
collect_output_until_exit(output_rx, spawned.exit_rx, timeout_ms).await;
|
||||
output.extend_from_slice(&remaining_output);
|
||||
let text = String::from_utf8_lossy(&output);
|
||||
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user