mirror of
https://github.com/openai/codex.git
synced 2026-10-27 00:00:53 +00:00
Add structured asynchronous user input requests (#42178)
## What changed - Replace `send_user_message_async` with `request_user_input_async`, accepting one or more questions with optional suggested answers while allowing the turn to continue. - Attach structured question metadata to asynchronous agent messages while retaining readable fallback text, and preserve it through app-server events, thread history, and generated schemas. - Continue enabling the new tool for model catalogs that advertise either the old or new tool name. ## Testing - Cover tool registration, question validation, emitted items, continued turn execution, history preservation, and backwards-compatible deserialization. GitOrigin-RevId: ffc49aebde2e854c3c50a122aa52805fec3fc6b3
This commit is contained in:
@@ -1028,6 +1028,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -522,6 +522,27 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AuthMode": {
|
||||
"description": "Authentication mode for OpenAI-backed providers.",
|
||||
"oneOf": [
|
||||
@@ -4513,6 +4534,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+31
@@ -7734,6 +7734,27 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AuthMode": {
|
||||
"description": "Authentication mode for OpenAI-backed providers.",
|
||||
"oneOf": [
|
||||
@@ -20973,6 +20994,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+31
@@ -1225,6 +1225,27 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AuthMode": {
|
||||
"description": "Authentication mode for OpenAI-backed providers.",
|
||||
"oneOf": [
|
||||
@@ -18687,6 +18708,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,27 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -806,6 +827,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,27 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -806,6 +827,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,27 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -994,6 +1015,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -92,6 +92,27 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1578,6 +1599,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,27 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -806,6 +827,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,27 @@
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1369,6 +1390,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+31
@@ -14,6 +14,27 @@
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1369,6 +1390,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,27 @@
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1369,6 +1390,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -92,6 +92,27 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1578,6 +1599,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,27 @@
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1369,6 +1390,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,27 @@
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1369,6 +1390,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -92,6 +92,27 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1578,6 +1599,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,27 @@
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1369,6 +1390,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,27 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -994,6 +1015,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,27 @@
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -1369,6 +1390,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,27 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -994,6 +1015,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,27 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -994,6 +1015,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,27 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AsyncUserInputQuestion": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
@@ -994,6 +1015,16 @@
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"questions": {
|
||||
"default": null,
|
||||
"items": {
|
||||
"$ref": "#/definitions/AsyncUserInputQuestion"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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 AsyncUserInputQuestion = { title: string, options: Array<string> | null, };
|
||||
@@ -10,6 +10,7 @@ import type { SleepItem } from "../SleepItem";
|
||||
import type { WebSearchItem } from "../WebSearchItem";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { AgentMessageDelivery } from "./AgentMessageDelivery";
|
||||
import type { AsyncUserInputQuestion } from "./AsyncUserInputQuestion";
|
||||
import type { CollabAgentState } from "./CollabAgentState";
|
||||
import type { CollabAgentTool } from "./CollabAgentTool";
|
||||
import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus";
|
||||
@@ -29,7 +30,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
import type { SubAgentActivityKind } from "./SubAgentActivityKind";
|
||||
import type { UserInput } from "./UserInput";
|
||||
|
||||
export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, questions: Array<AsyncUserInputQuestion> | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
/**
|
||||
* Trusted first-party plugin id when this command resolves to one plugin script.
|
||||
*/
|
||||
|
||||
@@ -41,6 +41,7 @@ export type { AppsListResponse } from "./AppsListResponse";
|
||||
export type { AppsReadParams } from "./AppsReadParams";
|
||||
export type { AppsReadResponse } from "./AppsReadResponse";
|
||||
export type { AskForApproval } from "./AskForApproval";
|
||||
export type { AsyncUserInputQuestion } from "./AsyncUserInputQuestion";
|
||||
export type { AttestationGenerateParams } from "./AttestationGenerateParams";
|
||||
export type { AttestationGenerateResponse } from "./AttestationGenerateResponse";
|
||||
export type { AuthRecoveryNotification } from "./AuthRecoveryNotification";
|
||||
|
||||
@@ -497,6 +497,7 @@ impl ThreadHistoryBuilder {
|
||||
phase: payload.phase.clone(),
|
||||
memory_citation: payload.memory_citation.clone().map(Into::into),
|
||||
delivery: payload.delivery,
|
||||
questions: payload.questions.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1657,6 +1658,7 @@ impl From<&PendingTurn> for Turn {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2::AgentMessageDelivery;
|
||||
use crate::protocol::v2::AsyncUserInputQuestion;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use codex_extension_items::ExtensionItem as CoreExtensionItem;
|
||||
use codex_extension_items::sleep::SleepItem as CoreSleepItem;
|
||||
@@ -1759,6 +1761,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent {
|
||||
text: "thinking".into(),
|
||||
@@ -1779,6 +1782,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1818,6 +1822,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1852,6 +1857,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2422,12 +2428,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_agent_message_phase_and_delivery_in_history() {
|
||||
fn preserves_agent_message_phase_delivery_and_questions_in_history() {
|
||||
let questions = vec![
|
||||
AsyncUserInputQuestion {
|
||||
title: "Which environment?".into(),
|
||||
options: Some(vec!["Staging".into(), "Production".into()]),
|
||||
},
|
||||
AsyncUserInputQuestion {
|
||||
title: "Anything else?".into(),
|
||||
options: None,
|
||||
},
|
||||
];
|
||||
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "Final reply".into(),
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: Some(questions.clone()),
|
||||
})];
|
||||
|
||||
let items = events
|
||||
@@ -2444,6 +2461,7 @@ mod tests {
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: Some(questions),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2554,6 +2572,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent {
|
||||
text: "second summary".into(),
|
||||
@@ -2603,6 +2622,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".into()),
|
||||
@@ -2624,6 +2644,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2656,6 +2677,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2681,6 +2703,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2701,6 +2724,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
@@ -2715,6 +2739,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
@@ -2730,6 +2755,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2761,6 +2787,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -2781,6 +2808,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -2802,6 +2830,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
@@ -2816,6 +2845,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }),
|
||||
];
|
||||
@@ -3849,6 +3879,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
@@ -4028,6 +4059,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -4295,6 +4327,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::Error(ErrorEvent {
|
||||
misalignment: None,
|
||||
|
||||
@@ -125,6 +125,7 @@ fn projects_completed_canonical_turn_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
});
|
||||
|
||||
let user_changes = project(item_completed(thread_id, "turn-1", user_item.clone()));
|
||||
|
||||
@@ -26,6 +26,7 @@ use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianA
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
pub use codex_protocol::items::AgentMessageDelivery;
|
||||
pub use codex_protocol::items::AsyncUserInputQuestion;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
|
||||
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
|
||||
@@ -255,6 +256,8 @@ pub enum ThreadItem {
|
||||
memory_citation: Option<MemoryCitation>,
|
||||
#[serde(default)]
|
||||
delivery: Option<AgentMessageDelivery>,
|
||||
#[serde(default)]
|
||||
questions: Option<Vec<AsyncUserInputQuestion>>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
@@ -881,6 +884,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
phase: agent.phase,
|
||||
memory_citation: agent.memory_citation.map(Into::into),
|
||||
delivery: agent.delivery,
|
||||
questions: agent.questions,
|
||||
}
|
||||
}
|
||||
CoreTurnItem::FunctionCallOutput(output) => ThreadItem::FunctionCallOutput {
|
||||
|
||||
@@ -3071,6 +3071,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@@ -3081,6 +3082,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3100,6 +3102,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
rollout_ids: vec!["rollout-1".to_string()],
|
||||
}),
|
||||
delivery: None,
|
||||
questions: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@@ -3118,9 +3121,42 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
thread_ids: vec!["rollout-1".to_string()],
|
||||
}),
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
|
||||
let async_item = ThreadItem::from(TurnItem::AgentMessage(AgentMessageItem {
|
||||
id: "async-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "Which?".to_string(),
|
||||
}],
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: Some(vec![AsyncUserInputQuestion {
|
||||
title: "Which?".to_string(),
|
||||
options: None,
|
||||
}]),
|
||||
}));
|
||||
assert_eq!(
|
||||
serde_json::to_value(&async_item).unwrap(),
|
||||
json!({
|
||||
"type": "agentMessage", "id": "async-1", "text": "Which?", "phase": "final_answer",
|
||||
"memoryCitation": null, "delivery": "async", "questions": [{"title": "Which?", "options": null}]
|
||||
})
|
||||
);
|
||||
let old_item: ThreadItem = serde_json::from_value(json!({
|
||||
"type": "agentMessage", "id": "old-1", "text": "An old message"
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
old_item,
|
||||
ThreadItem::AgentMessage {
|
||||
questions: None,
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
let reasoning_item = TurnItem::Reasoning(ReasoningItem {
|
||||
id: "reasoning-1".to_string(),
|
||||
summary_text: vec!["line one".to_string(), "line two".to_string()],
|
||||
|
||||
@@ -1824,7 +1824,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn
|
||||
|
||||
- `userMessage` — `{id, clientId, content}` where `clientId` is the optional `clientUserMessageId` supplied to `turn/start` or `turn/steer`, and `content` is a list of user inputs (`text`, `image`, `localImage`, `audio`, or `localAudio`).
|
||||
- `functionCallOutput` — `{id, name, namespace, output}` for a standalone function-call output without a `call_id`. `namespace` is nullable, and `output` is either a string or structured content items. Clients decide whether to render these tool-authority items; ordinary paired function-call outputs are not emitted separately.
|
||||
- `agentMessage` — `{id, text, phase, memoryCitation, delivery}` containing the accumulated agent reply. `delivery: "async"` identifies a user-visible message sent without ending the current turn; ordinary agent messages have `delivery: null`.
|
||||
- `agentMessage` — `{id, text, phase, memoryCitation, delivery, questions}` containing the accumulated agent reply. `delivery: "async"` identifies a user-visible message sent without ending the current turn. Async user-input requests also provide `questions`, an ordered array of `{title, options}`; `options: null` means free text only. `text` remains a readable fallback. Replies arrive as ordinary user messages. Ordinary agent messages have `delivery: null` and `questions: null`.
|
||||
- `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental).
|
||||
- `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
|
||||
- `commandExecution` — `{id, pluginId?, scriptPath?, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `pluginId` is present only for commands attributed to a trusted first-party plugin, newly attributed items also include `scriptPath` as a safe `/`-separated path relative to the trusted plugin root, older history may omit `scriptPath`, and `status` is `inProgress`, `completed`, `failed`, or `declined`. Ordinary execution items and their replay expose `command` and `commandActions` as redacted display values, not executable commands.
|
||||
|
||||
@@ -2291,6 +2291,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
})),
|
||||
];
|
||||
let stored_thread = StoredThread {
|
||||
@@ -3586,6 +3587,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: 0,
|
||||
@@ -3604,6 +3606,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: 0,
|
||||
|
||||
@@ -1031,6 +1031,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
|
||||
@@ -74,6 +74,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
ThreadItem::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
@@ -125,6 +126,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -178,6 +178,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TokenCount(TokenCountEvent {
|
||||
info: None,
|
||||
|
||||
@@ -2045,6 +2045,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -260,8 +260,8 @@ async fn history_notes_and_async_message_emit_control_tool_analytics() -> Result
|
||||
),
|
||||
(
|
||||
"functions",
|
||||
"send_user_message_async",
|
||||
json!({"message": "PRIVATE_MESSAGE"}),
|
||||
"request_user_input_async",
|
||||
json!({"questions": [{"title": "PRIVATE_MESSAGE"}]}),
|
||||
),
|
||||
(
|
||||
"notes",
|
||||
@@ -270,8 +270,8 @@ async fn history_notes_and_async_message_emit_control_tool_analytics() -> Result
|
||||
),
|
||||
(
|
||||
"functions",
|
||||
"send_user_message_async",
|
||||
json!({"message": " "}),
|
||||
"request_user_input_async",
|
||||
json!({"questions": [{"title": " "}]}),
|
||||
),
|
||||
];
|
||||
let server = responses::start_mock_server().await;
|
||||
|
||||
@@ -584,6 +584,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> {
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
),
|
||||
paginated_completed_item(
|
||||
@@ -597,6 +598,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> {
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
),
|
||||
paginated_turn_completed("turn-1"),
|
||||
@@ -1627,6 +1629,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
),
|
||||
paginated_completed_item(
|
||||
@@ -1679,6 +1682,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
],
|
||||
items_view: TurnItemsView::Full,
|
||||
@@ -1865,6 +1869,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
],
|
||||
items_view: TurnItemsView::Summary,
|
||||
@@ -2111,6 +2116,7 @@ fn append_agent_message(path: &Path, timestamp: &str, text: &str) -> anyhow::Res
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))?,
|
||||
})
|
||||
)?;
|
||||
|
||||
@@ -3836,6 +3836,7 @@ async fn thread_resume_token_usage_replay_ignores_stale_interrupted_tail_turn()
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))?,
|
||||
})
|
||||
.to_string(),
|
||||
@@ -3923,6 +3924,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))?,
|
||||
})
|
||||
.to_string(),
|
||||
@@ -4233,6 +4235,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))?,
|
||||
})
|
||||
.to_string(),
|
||||
|
||||
@@ -174,6 +174,7 @@ fn parse_agent_message(
|
||||
phase,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -222,6 +222,7 @@ fn promotes_backing_agent_artifacts_once_without_a_client_request() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}));
|
||||
assert!(state.observe(&completed).items.is_empty());
|
||||
|
||||
@@ -298,6 +299,7 @@ fn promotes_distinct_visualizations_once_and_ignores_markdown_fences() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}));
|
||||
assert!(state.observe(&completed).items.is_empty());
|
||||
}
|
||||
|
||||
@@ -2088,6 +2088,7 @@ async fn emit_agent_message_in_plan_mode(
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
})
|
||||
});
|
||||
sess.emit_turn_item_started(turn_context, &start_item).await;
|
||||
|
||||
@@ -2442,6 +2442,7 @@ fn completed_legacy_event_history_is_not_mid_turn() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
})),
|
||||
]);
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ mod request_permissions;
|
||||
mod request_plugin_install;
|
||||
pub(crate) mod request_plugin_install_spec;
|
||||
mod request_user_input;
|
||||
mod request_user_input_async;
|
||||
pub(crate) mod request_user_input_spec;
|
||||
mod send_user_message_async;
|
||||
pub(crate) mod shell_spec;
|
||||
mod sleep;
|
||||
mod test_sync;
|
||||
@@ -69,7 +69,7 @@ pub use plan::PlanHandler;
|
||||
pub use request_permissions::RequestPermissionsHandler;
|
||||
pub use request_plugin_install::RequestPluginInstallHandler;
|
||||
pub use request_user_input::RequestUserInputHandler;
|
||||
pub use send_user_message_async::SendUserMessageAsyncHandler;
|
||||
pub use request_user_input_async::RequestUserInputAsyncHandler;
|
||||
pub use sleep::SleepHandler;
|
||||
pub use test_sync::TestSyncHandler;
|
||||
pub(crate) use tool_search::ToolSearchHandlerCache;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::context::boxed_tool_output;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::AgentMessageDelivery;
|
||||
use codex_protocol::items::AgentMessageItem;
|
||||
use codex_protocol::items::AsyncUserInputQuestion;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_tools::JsonSchema;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const TOOL_NAME: &str = "request_user_input_async";
|
||||
|
||||
pub struct RequestUserInputAsyncHandler {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RequestUserInputAsyncArgs {
|
||||
questions: Vec<AsyncUserInputQuestion>,
|
||||
}
|
||||
|
||||
impl ToolExecutor<ToolInvocation> for RequestUserInputAsyncHandler {
|
||||
fn tool_name(&self) -> ToolName {
|
||||
ToolName::plain(TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> ToolSpec {
|
||||
let mut options = JsonSchema::array(
|
||||
JsonSchema::string(/*description*/ None),
|
||||
Some("Suggested answers, in display order. Put the recommended answer first; the first option is preselected by default. The user can select one option or enter a free-text answer. Do not include an Other option or a free-text placeholder; the UI provides free-text input automatically. Omit options for a free-text-only question.".to_string()),
|
||||
);
|
||||
options.min_items = Some(1);
|
||||
let question = JsonSchema::object(
|
||||
BTreeMap::from([
|
||||
("title".to_string(), JsonSchema::string(Some("The complete question shown to the user, including any context needed to answer it.".to_string()))),
|
||||
("options".to_string(), options),
|
||||
]),
|
||||
Some(vec!["title".to_string()]),
|
||||
/*additional_properties*/ Some(false.into()),
|
||||
);
|
||||
let mut questions = JsonSchema::array(
|
||||
question,
|
||||
Some(
|
||||
"One or more self-contained questions to present together, in display order."
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
questions.min_items = Some(1);
|
||||
let properties = BTreeMap::from([("questions".to_string(), questions)]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: TOOL_NAME.to_string(),
|
||||
description: self.description.clone().unwrap_or_else(|| {
|
||||
"Ask the user one or more questions during ongoing work. Use this tool only to request missing information, preferences, constraints, clarification, or approval. The tool returns immediately without ending the turn or waiting for a reply; any reply arrives asynchronously as a new user message. Keep questions concise, self-contained, and easy to understand, using a level of detail appropriate to the user and task. The UI always allows a free-text answer, including when suggested options are provided. A preselected option is not submitted automatically."
|
||||
.to_string()
|
||||
}),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(
|
||||
properties,
|
||||
Some(vec!["questions".to_string()]),
|
||||
/*additional_properties*/ Some(false.into()),
|
||||
),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle<'a>(&'a self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'a>
|
||||
where
|
||||
ToolInvocation: 'a,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
call_id,
|
||||
payload,
|
||||
..
|
||||
} = invocation;
|
||||
let ToolPayload::Function { arguments } = payload else {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"{TOOL_NAME} handler received unsupported payload"
|
||||
)));
|
||||
};
|
||||
let args: RequestUserInputAsyncArgs = parse_arguments(&arguments)?;
|
||||
if args.questions.is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"questions must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
let mut messages = Vec::with_capacity(args.questions.len());
|
||||
for question in &args.questions {
|
||||
if question.title.trim().is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"question titles must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
let mut lines = vec![question.title.clone()];
|
||||
if let Some(options) = &question.options {
|
||||
if options.is_empty() || options.iter().any(|option| option.trim().is_empty()) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"options must contain at least one non-empty answer".to_string(),
|
||||
));
|
||||
}
|
||||
lines.extend(options.iter().map(|option| format!("- {option}")));
|
||||
}
|
||||
messages.push(lines.join("\n"));
|
||||
}
|
||||
|
||||
let item = TurnItem::AgentMessage(AgentMessageItem {
|
||||
id: call_id,
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: messages.join("\n\n"),
|
||||
}],
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: Some(args.questions),
|
||||
});
|
||||
session.emit_turn_item_started(turn.as_ref(), &item).await;
|
||||
session.emit_turn_item_completed(turn.as_ref(), item).await;
|
||||
|
||||
Ok(boxed_tool_output(FunctionToolOutput::from_text(
|
||||
r#"{"accepted":true}"#.to_string(),
|
||||
/*success*/ Some(true),
|
||||
)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for RequestUserInputAsyncHandler {
|
||||
fn is_builtin_control_tool(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::context::boxed_tool_output;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::AgentMessageDelivery;
|
||||
use codex_protocol::items::AgentMessageItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_tools::JsonSchema;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const TOOL_NAME: &str = "send_user_message_async";
|
||||
|
||||
pub struct SendUserMessageAsyncHandler {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct SendUserMessageAsyncArgs {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl ToolExecutor<ToolInvocation> for SendUserMessageAsyncHandler {
|
||||
fn tool_name(&self) -> ToolName {
|
||||
ToolName::plain(TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> ToolSpec {
|
||||
let properties = BTreeMap::from([(
|
||||
"message".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"The concise question to send to the user.".to_string(),
|
||||
)),
|
||||
)]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: TOOL_NAME.to_string(),
|
||||
description: self.description.clone().unwrap_or_else(|| {
|
||||
"Send a concise message that needs the user's attention during ongoing work. The tool returns immediately without ending the turn or waiting for a reply; any reply arrives asynchronously as a new user message.\nOnly use this tool to ask for missing information, preferences, constraints, clarification, or approval. The message should be concise, easy to read and understand, and at the right level of abstraction that is appropriate for the user and task at hand."
|
||||
.to_string()
|
||||
}),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(
|
||||
properties,
|
||||
Some(vec!["message".to_string()]),
|
||||
/*additional_properties*/ Some(false.into()),
|
||||
),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle<'a>(&'a self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'a>
|
||||
where
|
||||
ToolInvocation: 'a,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
call_id,
|
||||
payload,
|
||||
..
|
||||
} = invocation;
|
||||
let ToolPayload::Function { arguments } = payload else {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"{TOOL_NAME} handler received unsupported payload"
|
||||
)));
|
||||
};
|
||||
let args: SendUserMessageAsyncArgs = parse_arguments(&arguments)?;
|
||||
let message = args.message.trim();
|
||||
if message.is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"message must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let item = TurnItem::AgentMessage(AgentMessageItem {
|
||||
id: call_id,
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: message.to_string(),
|
||||
}],
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
});
|
||||
session.emit_turn_item_started(turn.as_ref(), &item).await;
|
||||
session.emit_turn_item_completed(turn.as_ref(), item).await;
|
||||
|
||||
Ok(boxed_tool_output(FunctionToolOutput::from_text(
|
||||
r#"{"accepted":true}"#.to_string(),
|
||||
/*success*/ Some(true),
|
||||
)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for SendUserMessageAsyncHandler {
|
||||
fn is_builtin_control_tool(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ use crate::tools::handlers::PlanHandler;
|
||||
use crate::tools::handlers::ReadMcpResourceHandler;
|
||||
use crate::tools::handlers::RequestPermissionsHandler;
|
||||
use crate::tools::handlers::RequestPluginInstallHandler;
|
||||
use crate::tools::handlers::RequestUserInputAsyncHandler;
|
||||
use crate::tools::handlers::RequestUserInputHandler;
|
||||
use crate::tools::handlers::SendUserMessageAsyncHandler;
|
||||
use crate::tools::handlers::SleepHandler;
|
||||
use crate::tools::handlers::TestSyncHandler;
|
||||
use crate::tools::handlers::ToolSearchHandlerCache;
|
||||
@@ -1163,10 +1163,16 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
|
||||
.model_info
|
||||
.experimental_supported_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "send_user_message_async")
|
||||
// Existing model catalogs still advertise the previous name.
|
||||
.any(|tool| {
|
||||
matches!(
|
||||
tool.as_str(),
|
||||
"request_user_input_async" | "send_user_message_async"
|
||||
)
|
||||
})
|
||||
{
|
||||
registry.add_with_exposure(
|
||||
SendUserMessageAsyncHandler {
|
||||
RequestUserInputAsyncHandler {
|
||||
description: context
|
||||
.model_messages
|
||||
.and_then(|messages| messages.tools.as_ref())
|
||||
|
||||
@@ -66,6 +66,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))
|
||||
.await
|
||||
.is_some()
|
||||
@@ -78,6 +79,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))
|
||||
.await,
|
||||
None
|
||||
|
||||
@@ -136,6 +136,7 @@ mod request_permissions;
|
||||
mod request_permissions_tool;
|
||||
mod request_plugin_install;
|
||||
mod request_user_input;
|
||||
mod request_user_input_async;
|
||||
mod responses_api_proxy_headers;
|
||||
mod responses_lite;
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -151,7 +152,6 @@ mod rollout_list_find;
|
||||
mod safety_buffering;
|
||||
mod safety_check_downgrade;
|
||||
mod search_tool;
|
||||
mod send_user_message_async;
|
||||
mod settings_commits;
|
||||
mod settings_constraints;
|
||||
mod shell_snapshot;
|
||||
|
||||
+115
-19
@@ -4,6 +4,7 @@ use codex_core::TurnInputRequest;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::AgentMessageDelivery;
|
||||
use codex_protocol::items::AgentMessageItem;
|
||||
use codex_protocol::items::AsyncUserInputQuestion;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
@@ -112,26 +113,40 @@ async fn persistent_async_message_guidance_follows_tool_availability(
|
||||
.as_array()
|
||||
.expect("request tools")
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "send_user_message_async"),
|
||||
.any(|tool| tool["name"] == "request_user_input_async"),
|
||||
expect_tool_guidance,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(None; "fallback_description")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: None }); "missing_tool")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage::default()) }); "missing_description")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some("Catalog async message description.".to_string()) }) }); "catalog_description")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some(String::new()) }) }); "empty_description")]
|
||||
#[test_case(None, "send_user_message_async"; "fallback_description")]
|
||||
#[test_case(None, "request_user_input_async"; "current_catalog_name")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: None }), "send_user_message_async"; "missing_tool")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage::default()) }), "send_user_message_async"; "missing_description")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some("Catalog async message description.".to_string()) }) }), "send_user_message_async"; "catalog_description")]
|
||||
#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some(String::new()) }) }), "send_user_message_async"; "empty_description")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn send_user_message_async_emits_item_and_does_not_end_the_turn(
|
||||
async fn request_user_input_async_emits_item_and_does_not_end_the_turn(
|
||||
tool_messages: Option<ToolMessages>,
|
||||
catalog_tool_name: &'static str,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
const CALL_ID: &str = "async-message-call";
|
||||
const MESSAGE: &str = "Still investigating.";
|
||||
const TITLE: &str = "Which environment should I use?";
|
||||
const MESSAGE: &str =
|
||||
"Which environment should I use?\n- Staging\n- Production\n\nWhat deadline should I use?";
|
||||
let questions = vec![
|
||||
AsyncUserInputQuestion {
|
||||
title: TITLE.to_string(),
|
||||
options: Some(vec!["Staging".to_string(), "Production".to_string()]),
|
||||
},
|
||||
AsyncUserInputQuestion {
|
||||
title: "What deadline should I use?".to_string(),
|
||||
options: None,
|
||||
},
|
||||
];
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_sequence(
|
||||
@@ -142,8 +157,12 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn(
|
||||
ev_function_call_with_namespace(
|
||||
CALL_ID,
|
||||
"functions",
|
||||
"send_user_message_async",
|
||||
&json!({ "message": MESSAGE }).to_string(),
|
||||
"request_user_input_async",
|
||||
&json!({ "questions": [
|
||||
{"title": TITLE, "options": ["Staging", "Production"]},
|
||||
{"title": "What deadline should I use?"}
|
||||
] })
|
||||
.to_string(),
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
@@ -160,15 +179,18 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn(
|
||||
.and_then(|tools| tools.send_user_message_async.as_ref())
|
||||
.and_then(|tool| tool.description.as_deref())
|
||||
.unwrap_or(
|
||||
"Send a concise message that needs the user's attention during ongoing work. The tool returns immediately without ending the turn or waiting for a reply; any reply arrives asynchronously as a new user message.\nOnly use this tool to ask for missing information, preferences, constraints, clarification, or approval. The message should be concise, easy to read and understand, and at the right level of abstraction that is appropriate for the user and task at hand.",
|
||||
"Ask the user one or more questions during ongoing work. Use this tool only to request missing information, preferences, constraints, clarification, or approval. The tool returns immediately without ending the turn or waiting for a reply; any reply arrives asynchronously as a new user message. Keep questions concise, self-contained, and easy to understand, using a level of detail appropriate to the user and task. The UI always allows a free-text answer, including when suggested options are provided. A preselected option is not submitted automatically.",
|
||||
)
|
||||
.to_string();
|
||||
let test = test_codex()
|
||||
.with_model_info_override("gpt-5.2", move |model| {
|
||||
model.tool_mode = Some(ToolMode::CodeModeOnly);
|
||||
model.experimental_supported_tools.retain(|tool| {
|
||||
tool != "send_user_message_async" && tool != "request_user_input_async"
|
||||
});
|
||||
model
|
||||
.experimental_supported_tools
|
||||
.push("send_user_message_async".to_string());
|
||||
.push(catalog_tool_name.to_string());
|
||||
model
|
||||
.model_messages
|
||||
.as_mut()
|
||||
@@ -208,6 +230,7 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn(
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: Some(questions),
|
||||
})?
|
||||
);
|
||||
|
||||
@@ -238,22 +261,36 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn(
|
||||
assert_eq!(requests.len(), 2);
|
||||
for request in &requests {
|
||||
let body = request.body_json();
|
||||
let tool = body["tools"]
|
||||
.as_array()
|
||||
.expect("request tools")
|
||||
let tools = body["tools"].as_array().expect("request tools");
|
||||
let tool = tools
|
||||
.iter()
|
||||
.find(|tool| tool["type"] == "function" && tool["name"] == "send_user_message_async")
|
||||
.find(|tool| tool["type"] == "function" && tool["name"] == "request_user_input_async")
|
||||
.expect("the async message tool should be directly visible to the model");
|
||||
assert_eq!(tool["description"], expected_description);
|
||||
assert_eq!(tool["strict"], false);
|
||||
assert_eq!(tool["parameters"]["required"], json!(["questions"]));
|
||||
assert_eq!(tool["parameters"]["additionalProperties"], false);
|
||||
let schema = &tool["parameters"]["properties"]["questions"];
|
||||
assert_eq!(schema["minItems"], 1);
|
||||
assert_eq!(schema["items"]["required"], json!(["title"]));
|
||||
assert_eq!(schema["items"]["additionalProperties"], false);
|
||||
assert_eq!(schema["items"]["properties"]["options"]["minItems"], 1);
|
||||
assert_eq!(
|
||||
schema["items"]["properties"]["options"]["items"]["type"],
|
||||
"string"
|
||||
);
|
||||
assert!(
|
||||
tools
|
||||
.iter()
|
||||
.all(|tool| tool["name"] != "send_user_message_async")
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
requests[1].function_call_output_text(CALL_ID),
|
||||
Some(r#"{"accepted":true}"#.to_string())
|
||||
);
|
||||
let has_synthetic_assistant_message = requests[1].input().into_iter().any(|item| {
|
||||
item["type"] == "message"
|
||||
&& item["role"] == "assistant"
|
||||
&& item.to_string().contains(MESSAGE)
|
||||
item["type"] == "message" && item["role"] == "assistant" && item.to_string().contains(TITLE)
|
||||
});
|
||||
assert!(
|
||||
!has_synthetic_assistant_message,
|
||||
@@ -262,3 +299,62 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(json!({"questions": []}), "questions must not be empty"; "no_questions")]
|
||||
#[test_case(json!({"questions": [{"title": " "}]}), "question titles must not be empty"; "blank_title")]
|
||||
#[test_case(json!({"questions": [{"title": "Which?", "options": []}]}), "options must contain at least one non-empty answer"; "empty_options")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn invalid_async_questions_do_not_emit_an_item(
|
||||
arguments: serde_json::Value,
|
||||
expected_error: &str,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call_with_namespace(
|
||||
"invalid",
|
||||
"functions",
|
||||
"request_user_input_async",
|
||||
&arguments.to_string(),
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_model_info_override("gpt-5.2", |model| {
|
||||
model.tool_mode = Some(ToolMode::CodeModeOnly);
|
||||
model
|
||||
.experimental_supported_tools
|
||||
.push("request_user_input_async".to_string());
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
test.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "Get clarification.".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
wait_for_event(test.codex.as_ref(), |event| {
|
||||
let item = match event {
|
||||
EventMsg::ItemStarted(event) => Some(&event.item),
|
||||
EventMsg::ItemCompleted(event) => Some(&event.item),
|
||||
_ => None,
|
||||
};
|
||||
assert!(!matches!(item, Some(TurnItem::AgentMessage(message)) if message.delivery == Some(AgentMessageDelivery::Async)));
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
}).await;
|
||||
assert_eq!(
|
||||
responses.requests()[1].function_call_output_text("invalid"),
|
||||
Some(expected_error.to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1134,7 +1134,7 @@ async fn model_activation_uses_destination_metadata_defaults(
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn send_user_message_async_description_follows_mid_turn_model_changes() -> Result<()> {
|
||||
async fn request_user_input_async_description_follows_mid_turn_model_changes() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -1201,7 +1201,7 @@ async fn send_user_message_async_description_follows_mid_turn_model_changes() ->
|
||||
.as_array()
|
||||
.expect("request tools")
|
||||
.iter()
|
||||
.find(|tool| tool["name"] == "send_user_message_async")
|
||||
.find(|tool| tool["name"] == "request_user_input_async")
|
||||
.expect("async message tool");
|
||||
json!({"model": body["model"], "description": tool["description"]})
|
||||
})
|
||||
|
||||
@@ -251,6 +251,7 @@ fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
@@ -262,6 +263,7 @@ fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -321,6 +323,7 @@ fn turn_completed_recovers_final_message_from_turn_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -370,6 +373,7 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
|
||||
@@ -19,6 +19,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
|
||||
@@ -389,6 +389,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -420,6 +421,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}])
|
||||
);
|
||||
assert_eq!(turn_items_for_thread(&thread, "missing-turn"), None);
|
||||
|
||||
@@ -306,6 +306,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -928,6 +929,7 @@ fn agent_message_item_updates_final_message() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -964,6 +966,7 @@ fn agent_message_item_started_is_ignored() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -1315,6 +1318,7 @@ fn turn_completion_recovers_final_message_from_turn_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -1443,6 +1447,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -1462,6 +1467,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -1495,6 +1501,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -1542,6 +1549,7 @@ fn failed_turn_clears_stale_final_message() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
|
||||
@@ -136,6 +136,7 @@ pub(super) fn rollout_items_from_messages(messages: Vec<ConversationMessage>) ->
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
)));
|
||||
items.push(RolloutItem::ResponseItem(response_item(message).into()));
|
||||
@@ -158,6 +159,7 @@ fn external_session_imported_marker_item() -> RolloutItem {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -254,6 +256,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -287,6 +290,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
})
|
||||
);
|
||||
let last_turn_complete = imported
|
||||
|
||||
@@ -133,6 +133,14 @@ pub enum AgentMessageDelivery {
|
||||
Async,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AsyncUserInputQuestion {
|
||||
pub title: String,
|
||||
pub options: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
/// Assistant-authored message payload used in turn-item streams.
|
||||
///
|
||||
@@ -155,6 +163,9 @@ pub struct AgentMessageItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub delivery: Option<AgentMessageDelivery>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub questions: Option<Vec<AsyncUserInputQuestion>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
|
||||
@@ -106,6 +106,7 @@ impl AgentMessageItem {
|
||||
phase: self.phase.clone(),
|
||||
memory_citation: self.memory_citation.clone(),
|
||||
delivery: self.delivery,
|
||||
questions: self.questions.clone(),
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -34,6 +34,7 @@ use crate::dynamic_tools::DynamicToolResponse;
|
||||
use crate::dynamic_tools::DynamicToolSpec;
|
||||
use crate::error::Result as CodexResult;
|
||||
use crate::items::AgentMessageDelivery;
|
||||
use crate::items::AsyncUserInputQuestion;
|
||||
use crate::items::TurnItem;
|
||||
use crate::mcp::CallToolResult;
|
||||
use crate::mcp::RequestId;
|
||||
@@ -2496,6 +2497,9 @@ pub struct AgentMessageEvent {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub delivery: Option<AgentMessageDelivery>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub questions: Option<Vec<AsyncUserInputQuestion>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -67,6 +67,7 @@ fn agent_message_item(message: &str) -> RolloutItem {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -650,6 +651,7 @@ async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result<
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
))])
|
||||
.await?;
|
||||
@@ -905,6 +907,7 @@ async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::i
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
))])
|
||||
.await?;
|
||||
@@ -957,6 +960,7 @@ async fn writer_state_retries_write_error_before_reporting_flush_success() -> st
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
))]);
|
||||
|
||||
|
||||
@@ -1043,6 +1043,7 @@ mod tests {
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
})),
|
||||
RolloutItem::ResponseItem(
|
||||
ResponseItem::FunctionCallOutput {
|
||||
|
||||
@@ -95,6 +95,7 @@ pub(super) fn completed_item(
|
||||
phase: event.phase.clone(),
|
||||
memory_citation: event.memory_citation.clone(),
|
||||
delivery: event.delivery,
|
||||
questions: event.questions.clone(),
|
||||
}),
|
||||
None,
|
||||
)),
|
||||
|
||||
@@ -160,6 +160,7 @@ fn agent_message(text: &str) -> RolloutItem {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,7 @@ async fn split_homes_support_backfill_listing_and_paginated_history() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
),
|
||||
turn_completed("turn-1"),
|
||||
@@ -325,6 +326,7 @@ async fn paginated_live_append_materializes_turn_items_and_state() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
),
|
||||
turn_completed("turn-1"),
|
||||
@@ -480,6 +482,7 @@ async fn paginated_realtime_items_materialize_separately_in_rollout_order() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
),
|
||||
RolloutItem::RealtimeItem(RealtimeItem {
|
||||
@@ -2669,6 +2672,7 @@ fn agent_message(id: &str, phase: MessagePhase) -> TurnItem {
|
||||
phase: Some(phase),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ pub struct JsonSchema {
|
||||
pub enum_values: Option<Vec<JsonValue>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub items: Option<Box<JsonSchema>>,
|
||||
#[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
|
||||
pub min_items: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub properties: Option<BTreeMap<String, JsonSchema>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -38,6 +38,7 @@ fn agent_status_uses_bounded_buffered_activity() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-child".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
|
||||
@@ -3733,6 +3733,7 @@ async fn active_thread_file_change_approval_recovers_buffered_changes() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
@@ -7552,6 +7553,7 @@ async fn replay_thread_snapshot_replays_turn_history_in_order() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
],
|
||||
status: TurnStatus::Completed,
|
||||
|
||||
@@ -271,6 +271,7 @@ async fn active_patch_approval_preserves_deferred_startup_protection() -> Result
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
|
||||
@@ -1515,6 +1515,7 @@ async fn older_pagination_reconciles_review_prompts_across_page_boundaries() ->
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
})
|
||||
}));
|
||||
items.extend([
|
||||
@@ -1704,6 +1705,7 @@ async fn transcript_home_loads_every_older_history_page() -> Result<()> {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
started_at_ms: None,
|
||||
completed_at_ms: 0,
|
||||
@@ -2102,6 +2104,7 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
started_at_ms: None,
|
||||
completed_at_ms: 0,
|
||||
|
||||
@@ -479,5 +479,6 @@ fn title_agent_message(id: &str, text: &str, phase: Option<MessagePhase>) -> Thr
|
||||
phase,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3674,6 +3674,7 @@ mod tests {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
],
|
||||
status: TurnStatus::Completed,
|
||||
|
||||
@@ -95,6 +95,7 @@ impl ChatWidget {
|
||||
phase,
|
||||
memory_citation,
|
||||
delivery,
|
||||
questions,
|
||||
..
|
||||
} => {
|
||||
self.on_agent_message_item_completed(
|
||||
@@ -120,6 +121,7 @@ impl ChatWidget {
|
||||
}
|
||||
}),
|
||||
delivery,
|
||||
questions,
|
||||
},
|
||||
&turn_id,
|
||||
from_replay,
|
||||
|
||||
@@ -625,6 +625,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
};
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
@@ -1302,6 +1303,7 @@ async fn live_app_server_turn_completion_repairs_dropped_message_deltas() {
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}];
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnCompleted(TurnCompletedNotification {
|
||||
|
||||
@@ -771,6 +771,7 @@ pub(super) fn replay_agent_message(
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
"turn-1".to_string(),
|
||||
replay_kind,
|
||||
@@ -924,6 +925,7 @@ pub(super) fn complete_assistant_message(
|
||||
phase,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
|
||||
@@ -378,6 +378,7 @@ async fn replayed_nested_review_prompts_do_not_render_or_seed_composer_history()
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
],
|
||||
..app_server_turn(
|
||||
|
||||
@@ -832,6 +832,7 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() {
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
status: AppServerTurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -871,6 +872,7 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
status: AppServerTurnStatus::Completed,
|
||||
error: None,
|
||||
|
||||
@@ -6338,6 +6338,7 @@ session_picker_view = "dense"
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
id: String::from("plan-1"),
|
||||
|
||||
@@ -42,6 +42,7 @@ fn rollout_agent_message(text: &str) -> RolloutItem {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -269,6 +270,7 @@ fn transcript_preview_reverse_scan_stops_before_older_items() {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
};
|
||||
let mut lines = Vec::new();
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ fn agent_message_notification(turn_id: &str, text: &str) -> ServerNotification {
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user