mirror of
https://github.com/openai/codex.git
synced 2026-03-18 04:34:00 +00:00
Compare commits
5 Commits
main
...
user_messa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
796438281e | ||
|
|
7b8c000a73 | ||
|
|
94d66dc421 | ||
|
|
e837ce9e02 | ||
|
|
9ad2b001f0 |
@@ -1180,6 +1180,24 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookRunStatus": {
|
||||
"enum": [
|
||||
"running",
|
||||
@@ -2168,6 +2186,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -7983,6 +7983,24 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookRunStatus": {
|
||||
"enum": [
|
||||
"running",
|
||||
@@ -11986,6 +12004,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -4727,6 +4727,24 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookRunStatus": {
|
||||
"enum": [
|
||||
"running",
|
||||
@@ -9746,6 +9764,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -257,6 +257,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -439,6 +457,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -257,6 +257,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -439,6 +457,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -371,6 +371,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -553,6 +571,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -456,6 +456,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1033,6 +1051,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -394,6 +394,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -791,6 +809,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -394,6 +394,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -791,6 +809,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -394,6 +394,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -791,6 +809,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -456,6 +456,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1033,6 +1051,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -394,6 +394,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -791,6 +809,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -456,6 +456,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -1033,6 +1051,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -394,6 +394,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -791,6 +809,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -394,6 +394,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -791,6 +809,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -371,6 +371,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -553,6 +571,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -371,6 +371,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -553,6 +571,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -371,6 +371,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookPromptFragment": {
|
||||
"properties": {
|
||||
"hookRunIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hookRunIds",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallError": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -553,6 +571,33 @@
|
||||
"title": "UserMessageThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"fragments": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookPromptFragment"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"hookPrompt"
|
||||
],
|
||||
"title": "HookPromptThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fragments",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "HookPromptThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -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 HookPromptFragment = { text: string, hookRunIds: Array<string>, };
|
||||
@@ -12,6 +12,7 @@ import type { CommandExecutionStatus } from "./CommandExecutionStatus";
|
||||
import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
|
||||
import type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
|
||||
import type { FileUpdateChange } from "./FileUpdateChange";
|
||||
import type { HookPromptFragment } from "./HookPromptFragment";
|
||||
import type { McpToolCallError } from "./McpToolCallError";
|
||||
import type { McpToolCallResult } from "./McpToolCallResult";
|
||||
import type { McpToolCallStatus } from "./McpToolCallStatus";
|
||||
@@ -19,7 +20,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
import type { UserInput } from "./UserInput";
|
||||
import type { WebSearchAction } from "./WebSearchAction";
|
||||
|
||||
export type ThreadItem = { "type": "userMessage", id: string, content: Array<UserInput>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, } | { "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, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
/**
|
||||
* The command to be executed.
|
||||
*/
|
||||
|
||||
@@ -126,6 +126,7 @@ export type { HookExecutionMode } from "./HookExecutionMode";
|
||||
export type { HookHandlerType } from "./HookHandlerType";
|
||||
export type { HookOutputEntry } from "./HookOutputEntry";
|
||||
export type { HookOutputEntryKind } from "./HookOutputEntryKind";
|
||||
export type { HookPromptFragment } from "./HookPromptFragment";
|
||||
export type { HookRunStatus } from "./HookRunStatus";
|
||||
export type { HookRunSummary } from "./HookRunSummary";
|
||||
export type { HookScope } from "./HookScope";
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::protocol::v2::TurnError;
|
||||
use crate::protocol::v2::TurnStatus;
|
||||
use crate::protocol::v2::UserInput;
|
||||
use crate::protocol::v2::WebSearchAction;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
use codex_protocol::protocol::AgentReasoningRawContentEvent;
|
||||
@@ -182,12 +183,37 @@ impl ThreadHistoryBuilder {
|
||||
match item {
|
||||
RolloutItem::EventMsg(event) => self.handle_event(event),
|
||||
RolloutItem::Compacted(payload) => self.handle_compacted(payload),
|
||||
RolloutItem::TurnContext(_)
|
||||
| RolloutItem::SessionMeta(_)
|
||||
| RolloutItem::ResponseItem(_) => {}
|
||||
RolloutItem::ResponseItem(item) => self.handle_response_item(item),
|
||||
RolloutItem::TurnContext(_) | RolloutItem::SessionMeta(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_response_item(&mut self, item: &codex_protocol::models::ResponseItem) {
|
||||
let codex_protocol::models::ResponseItem::Message {
|
||||
role, content, id, ..
|
||||
} = item
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if role != "user" {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(hook_prompt) = parse_hook_prompt_message(id.as_ref(), content) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.ensure_turn().items.push(ThreadItem::HookPrompt {
|
||||
id: hook_prompt.id,
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
.into_iter()
|
||||
.map(crate::protocol::v2::HookPromptFragment::from)
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_user_message(&mut self, payload: &UserMessageEvent) {
|
||||
// User messages should stay in explicitly opened turns. For backward
|
||||
// compatibility with older streams that did not open turns explicitly,
|
||||
@@ -271,6 +297,7 @@ impl ThreadHistoryBuilder {
|
||||
);
|
||||
}
|
||||
codex_protocol::items::TurnItem::UserMessage(_)
|
||||
| codex_protocol::items::TurnItem::HookPrompt(_)
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
@@ -291,6 +318,7 @@ impl ThreadHistoryBuilder {
|
||||
);
|
||||
}
|
||||
codex_protocol::items::TurnItem::UserMessage(_)
|
||||
| codex_protocol::items::TurnItem::HookPrompt(_)
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
@@ -1136,8 +1164,10 @@ mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::models::MessagePhase as CoreMessagePhase;
|
||||
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
@@ -2608,4 +2638,80 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_hook_prompt_items_from_rollout_response_items() {
|
||||
let hook_prompt = build_hook_prompt_message(&[
|
||||
CoreHookPromptFragment::from_single_hook("Retry with tests.", "hook-run-1"),
|
||||
CoreHookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"),
|
||||
])
|
||||
.expect("hook prompt message");
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "hello".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
})),
|
||||
RolloutItem::ResponseItem(hook_prompt),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
})),
|
||||
];
|
||||
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(turns[0].items.len(), 2);
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::HookPrompt {
|
||||
id: turns[0].items[1].id().to_string(),
|
||||
fragments: vec![
|
||||
crate::protocol::v2::HookPromptFragment {
|
||||
text: "Retry with tests.".into(),
|
||||
hook_run_ids: vec!["hook-run-1".into()],
|
||||
},
|
||||
crate::protocol::v2::HookPromptFragment {
|
||||
text: "Then summarize cleanly.".into(),
|
||||
hook_run_ids: vec!["hook-run-2".into()],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_plain_user_response_items_in_rollout_replay() {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
RolloutItem::ResponseItem(codex_protocol::models::ResponseItem::Message {
|
||||
id: Some("msg-1".into()),
|
||||
role: "user".into(),
|
||||
content: vec![codex_protocol::models::ContentItem::InputText {
|
||||
text: "plain text".into(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
})),
|
||||
];
|
||||
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert!(turns[0].items.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4128,6 +4128,12 @@ pub enum ThreadItem {
|
||||
UserMessage { id: String, content: Vec<UserInput> },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
HookPrompt {
|
||||
id: String,
|
||||
fragments: Vec<HookPromptFragment>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
AgentMessage {
|
||||
id: String,
|
||||
text: String,
|
||||
@@ -4257,10 +4263,19 @@ pub enum ThreadItem {
|
||||
ContextCompaction { id: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub struct HookPromptFragment {
|
||||
pub text: String,
|
||||
pub hook_run_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl ThreadItem {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
ThreadItem::UserMessage { id, .. }
|
||||
| ThreadItem::HookPrompt { id, .. }
|
||||
| ThreadItem::AgentMessage { id, .. }
|
||||
| ThreadItem::Plan { id, .. }
|
||||
| ThreadItem::Reasoning { id, .. }
|
||||
@@ -4370,6 +4385,14 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
id: user.id,
|
||||
content: user.content.into_iter().map(UserInput::from).collect(),
|
||||
},
|
||||
CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt {
|
||||
id: hook_prompt.id,
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
.into_iter()
|
||||
.map(HookPromptFragment::from)
|
||||
.collect(),
|
||||
},
|
||||
CoreTurnItem::AgentMessage(agent) => {
|
||||
let text = agent
|
||||
.content
|
||||
@@ -4411,6 +4434,15 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<codex_protocol::items::HookPromptFragment> for HookPromptFragment {
|
||||
fn from(value: codex_protocol::items::HookPromptFragment) -> Self {
|
||||
Self {
|
||||
text: value.text,
|
||||
hook_run_ids: value.hook_run_ids,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -110,6 +110,7 @@ use codex_core::sandboxing::intersect_permission_profiles;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
@@ -1482,6 +1483,14 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
EventMsg::RawResponseItem(raw_response_item_event) => {
|
||||
maybe_emit_hook_prompt_item_completed(
|
||||
api_version,
|
||||
conversation_id,
|
||||
&event_turn_id,
|
||||
&raw_response_item_event.item,
|
||||
&outgoing,
|
||||
)
|
||||
.await;
|
||||
maybe_emit_raw_response_item_completed(
|
||||
api_version,
|
||||
conversation_id,
|
||||
@@ -1980,6 +1989,49 @@ async fn maybe_emit_raw_response_item_completed(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn maybe_emit_hook_prompt_item_completed(
|
||||
api_version: ApiVersion,
|
||||
conversation_id: ThreadId,
|
||||
turn_id: &str,
|
||||
item: &codex_protocol::models::ResponseItem,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
) {
|
||||
let ApiVersion::V2 = api_version else {
|
||||
return;
|
||||
};
|
||||
|
||||
let codex_protocol::models::ResponseItem::Message {
|
||||
role, content, id, ..
|
||||
} = item
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if role != "user" {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(hook_prompt) = parse_hook_prompt_message(id.as_ref(), content) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let notification = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
item: ThreadItem::HookPrompt {
|
||||
id: hook_prompt.id,
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
.into_iter()
|
||||
.map(codex_app_server_protocol::HookPromptFragment::from)
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(notification))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn find_and_remove_turn_summary(
|
||||
_conversation_id: ThreadId,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
@@ -2750,6 +2802,8 @@ mod tests {
|
||||
use codex_app_server_protocol::GuardianApprovalReviewStatus;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
|
||||
@@ -3784,4 +3838,59 @@ mod tests {
|
||||
assert!(rx.try_recv().is_err(), "no messages expected");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hook_prompt_raw_response_emits_item_completed() -> Result<()> {
|
||||
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(tx));
|
||||
let conversation_id = ThreadId::new();
|
||||
let outgoing = ThreadScopedOutgoingMessageSender::new(
|
||||
outgoing,
|
||||
vec![ConnectionId(1)],
|
||||
conversation_id,
|
||||
);
|
||||
let item = build_hook_prompt_message(&[
|
||||
HookPromptFragment::from_single_hook("Retry with tests.", "hook-run-1"),
|
||||
HookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"),
|
||||
])
|
||||
.expect("hook prompt message");
|
||||
|
||||
maybe_emit_hook_prompt_item_completed(
|
||||
ApiVersion::V2,
|
||||
conversation_id,
|
||||
"turn-1",
|
||||
&item,
|
||||
&outgoing,
|
||||
)
|
||||
.await;
|
||||
|
||||
let msg = recv_broadcast_message(&mut rx).await?;
|
||||
match msg {
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::ItemCompleted(
|
||||
notification,
|
||||
)) => {
|
||||
assert_eq!(notification.thread_id, conversation_id.to_string());
|
||||
assert_eq!(notification.turn_id, "turn-1");
|
||||
assert_eq!(
|
||||
notification.item,
|
||||
ThreadItem::HookPrompt {
|
||||
id: notification.item.id().to_string(),
|
||||
fragments: vec![
|
||||
codex_app_server_protocol::HookPromptFragment {
|
||||
text: "Retry with tests.".into(),
|
||||
hook_run_ids: vec!["hook-run-1".into()],
|
||||
},
|
||||
codex_app_server_protocol::HookPromptFragment {
|
||||
text: "Then summarize cleanly.".into(),
|
||||
hook_run_ids: vec!["hook-run-2".into()],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
other => bail!("unexpected message: {other:?}"),
|
||||
}
|
||||
assert!(rx.try_recv().is_err(), "no extra messages expected");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::items::PlanItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -5812,13 +5813,12 @@ pub(crate) async fn run_turn(
|
||||
.await;
|
||||
}
|
||||
if stop_outcome.should_block {
|
||||
if let Some(continuation_prompt) = stop_outcome.continuation_prompt.clone()
|
||||
if let Some(hook_prompt_message) =
|
||||
build_hook_prompt_message(&stop_outcome.continuation_fragments)
|
||||
{
|
||||
let developer_message: ResponseItem =
|
||||
DeveloperInstructions::new(continuation_prompt).into();
|
||||
sess.record_conversation_items(
|
||||
&turn_context,
|
||||
std::slice::from_ref(&developer_message),
|
||||
std::slice::from_ref(&hook_prompt_message),
|
||||
)
|
||||
.await;
|
||||
stop_hook_active = true;
|
||||
|
||||
@@ -196,7 +196,7 @@ pub(crate) async fn process_compacted_history(
|
||||
/// - `developer` messages because remote output can include stale/duplicated
|
||||
/// instruction content.
|
||||
/// - non-user-content `user` messages (session prefix/instruction wrappers),
|
||||
/// keeping only real user messages as parsed by `parse_turn_item`.
|
||||
/// while preserving real user messages and persisted hook prompts.
|
||||
///
|
||||
/// This intentionally keeps:
|
||||
/// - `assistant` messages (future remote compaction models may emit them)
|
||||
@@ -208,7 +208,7 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
||||
ResponseItem::Message { role, .. } if role == "user" => {
|
||||
matches!(
|
||||
crate::event_mapping::parse_turn_item(item),
|
||||
Some(TurnItem::UserMessage(_))
|
||||
Some(TurnItem::UserMessage(_) | TurnItem::HookPrompt(_))
|
||||
)
|
||||
}
|
||||
ResponseItem::Message { role, .. } if role == "assistant" => true,
|
||||
|
||||
@@ -177,8 +177,7 @@ impl ContextManager {
|
||||
/// Returns true when a tool image was replaced, false otherwise.
|
||||
pub(crate) fn replace_last_turn_images(&mut self, placeholder: &str) -> bool {
|
||||
let Some(index) = self.items.iter().rposition(|item| {
|
||||
matches!(item, ResponseItem::FunctionCallOutput { .. })
|
||||
|| matches!(item, ResponseItem::Message { role, .. } if role == "user")
|
||||
matches!(item, ResponseItem::FunctionCallOutput { .. }) || is_user_turn_boundary(item)
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
@@ -200,7 +199,7 @@ impl ContextManager {
|
||||
}
|
||||
replaced
|
||||
}
|
||||
ResponseItem::Message { role, .. } if role == "user" => false,
|
||||
ResponseItem::Message { .. } => false,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -250,11 +249,7 @@ impl ContextManager {
|
||||
|
||||
fn get_non_last_reasoning_items_tokens(&self) -> i64 {
|
||||
// Get reasoning items excluding all the ones after the last user message.
|
||||
let Some(last_user_index) = self
|
||||
.items
|
||||
.iter()
|
||||
.rposition(|item| matches!(item, ResponseItem::Message { role, .. } if role == "user"))
|
||||
else {
|
||||
let Some(last_user_index) = self.items.iter().rposition(is_user_turn_boundary) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use codex_protocol::items::HookPromptItem;
|
||||
use codex_protocol::items::parse_hook_prompt_fragment;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
|
||||
@@ -94,10 +96,7 @@ const CONTEXTUAL_USER_FRAGMENTS: &[ContextualUserFragmentDefinition] = &[
|
||||
SUBAGENT_NOTIFICATION_FRAGMENT,
|
||||
];
|
||||
|
||||
pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
|
||||
let ContentItem::InputText { text } = content_item else {
|
||||
return false;
|
||||
};
|
||||
fn is_standard_contextual_user_text(text: &str) -> bool {
|
||||
CONTEXTUAL_USER_FRAGMENTS
|
||||
.iter()
|
||||
.any(|definition| definition.matches_text(text))
|
||||
@@ -118,6 +117,40 @@ pub(crate) fn is_memory_excluded_contextual_user_fragment(content_item: &Content
|
||||
AGENTS_MD_FRAGMENT.matches_text(text) || SKILL_FRAGMENT.matches_text(text)
|
||||
}
|
||||
|
||||
pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
|
||||
let ContentItem::InputText { text } = content_item else {
|
||||
return false;
|
||||
};
|
||||
parse_hook_prompt_fragment(text).is_some() || is_standard_contextual_user_text(text)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_visible_hook_prompt_message(
|
||||
id: Option<&String>,
|
||||
content: &[ContentItem],
|
||||
) -> Option<HookPromptItem> {
|
||||
let mut fragments = Vec::new();
|
||||
|
||||
for content_item in content {
|
||||
let ContentItem::InputText { text } = content_item else {
|
||||
return None;
|
||||
};
|
||||
if let Some(fragment) = parse_hook_prompt_fragment(text) {
|
||||
fragments.push(fragment);
|
||||
continue;
|
||||
}
|
||||
if is_standard_contextual_user_text(text) {
|
||||
continue;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if fragments.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(HookPromptItem::from_fragments(id, fragments))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "contextual_user_message_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
|
||||
#[test]
|
||||
fn detects_environment_context_fragment() {
|
||||
@@ -61,3 +63,36 @@ fn classifies_memory_excluded_fragments() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_hook_prompt_fragment_and_roundtrips_escaping() {
|
||||
let message = build_hook_prompt_message(&[HookPromptFragment::from_single_hook(
|
||||
r#"Retry with "waves" & <tides>"#,
|
||||
"hook-run-1",
|
||||
)])
|
||||
.expect("hook prompt message");
|
||||
|
||||
let ResponseItem::Message { content, .. } = message else {
|
||||
panic!("expected hook prompt response item");
|
||||
};
|
||||
|
||||
let [content_item] = content.as_slice() else {
|
||||
panic!("expected a single content item");
|
||||
};
|
||||
|
||||
assert!(is_contextual_user_fragment(content_item));
|
||||
|
||||
let ContentItem::InputText { text } = content_item else {
|
||||
panic!("expected input text content item");
|
||||
};
|
||||
let parsed =
|
||||
parse_visible_hook_prompt_message(None, content.as_slice()).expect("visible hook prompt");
|
||||
assert_eq!(
|
||||
parsed.fragments,
|
||||
vec![HookPromptFragment {
|
||||
text: r#"Retry with "waves" & <tides>"#.to_string(),
|
||||
hook_run_ids: vec!["hook-run-1".to_string()],
|
||||
}],
|
||||
);
|
||||
assert!(!text.contains(""waves" & <tides>"));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::contextual_user_message::is_contextual_user_fragment;
|
||||
use crate::contextual_user_message::parse_visible_hook_prompt_message;
|
||||
use crate::web_search::web_search_action_detail;
|
||||
|
||||
pub(crate) fn is_contextual_user_message_content(message: &[ContentItem]) -> bool {
|
||||
@@ -95,7 +96,9 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
||||
phase,
|
||||
..
|
||||
} => match role.as_str() {
|
||||
"user" => parse_user_message(content).map(TurnItem::UserMessage),
|
||||
"user" => parse_visible_hook_prompt_message(id.as_ref(), content)
|
||||
.map(TurnItem::HookPrompt)
|
||||
.or_else(|| parse_user_message(content).map(TurnItem::UserMessage)),
|
||||
"assistant" => Some(TurnItem::AgentMessage(parse_agent_message(
|
||||
id.as_ref(),
|
||||
content,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::parse_turn_item;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::WebSearchItem;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ReasoningItemContent;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
@@ -208,6 +210,67 @@ fn skips_user_instructions_and_env() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_hook_prompt_message_as_distinct_turn_item() {
|
||||
let item = build_hook_prompt_message(&[HookPromptFragment::from_single_hook(
|
||||
"Retry with exactly the phrase meow meow meow.",
|
||||
"hook-run-1",
|
||||
)])
|
||||
.expect("hook prompt message");
|
||||
|
||||
let turn_item = parse_turn_item(&item).expect("expected hook prompt turn item");
|
||||
|
||||
match turn_item {
|
||||
TurnItem::HookPrompt(hook_prompt) => {
|
||||
assert_eq!(hook_prompt.fragments.len(), 1);
|
||||
assert_eq!(
|
||||
hook_prompt.fragments[0],
|
||||
HookPromptFragment {
|
||||
text: "Retry with exactly the phrase meow meow meow.".to_string(),
|
||||
hook_run_ids: vec!["hook-run-1".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
other => panic!("expected TurnItem::HookPrompt, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_hook_prompt_and_hides_other_contextual_fragments() {
|
||||
let item = ResponseItem::Message {
|
||||
id: Some("msg-1".to_string()),
|
||||
role: "user".to_string(),
|
||||
content: vec![
|
||||
ContentItem::InputText {
|
||||
text: "<environment_context>ctx</environment_context>".to_string(),
|
||||
},
|
||||
ContentItem::InputText {
|
||||
text:
|
||||
"<hook_prompt hook_run_id=\"hook-run-1\">Retry with care & joy.</hook_prompt>"
|
||||
.to_string(),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
let turn_item = parse_turn_item(&item).expect("expected hook prompt turn item");
|
||||
|
||||
match turn_item {
|
||||
TurnItem::HookPrompt(hook_prompt) => {
|
||||
assert_eq!(hook_prompt.id, "msg-1");
|
||||
assert_eq!(
|
||||
hook_prompt.fragments,
|
||||
vec![HookPromptFragment {
|
||||
text: "Retry with care & joy.".to_string(),
|
||||
hook_run_ids: vec!["hook-run-1".to_string()],
|
||||
}]
|
||||
);
|
||||
}
|
||||
other => panic!("expected TurnItem::HookPrompt, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_agent_message() {
|
||||
let item = ResponseItem::Message {
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::Path;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_core::features::Feature;
|
||||
use codex_protocol::items::parse_hook_prompt_fragment;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
@@ -69,7 +70,49 @@ else:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollout_developer_texts(text: &str) -> Result<Vec<String>> {
|
||||
fn write_parallel_stop_hooks(home: &Path, prompts: &[&str]) -> Result<()> {
|
||||
let hook_entries = prompts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, prompt)| {
|
||||
let script_path = home.join(format!("stop_hook_{index}.py"));
|
||||
let script = format!(
|
||||
r#"import json
|
||||
import sys
|
||||
|
||||
payload = json.load(sys.stdin)
|
||||
if payload["stop_hook_active"]:
|
||||
print(json.dumps({{"systemMessage": "done"}}))
|
||||
else:
|
||||
print(json.dumps({{"decision": "block", "reason": {prompt:?}}}))
|
||||
"#
|
||||
);
|
||||
fs::write(&script_path, script).with_context(|| {
|
||||
format!(
|
||||
"write stop hook script fixture at {}",
|
||||
script_path.display()
|
||||
)
|
||||
})?;
|
||||
Ok(serde_json::json!({
|
||||
"type": "command",
|
||||
"command": format!("python3 {}", script_path.display()),
|
||||
}))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let hooks = serde_json::json!({
|
||||
"hooks": {
|
||||
"Stop": [{
|
||||
"hooks": hook_entries,
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
fs::write(home.join("hooks.json"), hooks.to_string()).context("write hooks.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollout_hook_prompt_texts(text: &str) -> Result<Vec<String>> {
|
||||
let mut texts = Vec::new();
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
@@ -78,11 +121,13 @@ fn rollout_developer_texts(text: &str) -> Result<Vec<String>> {
|
||||
}
|
||||
let rollout: RolloutLine = serde_json::from_str(trimmed).context("parse rollout line")?;
|
||||
if let RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) = rollout.item
|
||||
&& role == "developer"
|
||||
&& role == "user"
|
||||
{
|
||||
for item in content {
|
||||
if let ContentItem::InputText { text } = item {
|
||||
texts.push(text);
|
||||
if let ContentItem::InputText { text } = item
|
||||
&& let Some(fragment) = parse_hook_prompt_fragment(&text)
|
||||
{
|
||||
texts.push(fragment.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +135,16 @@ fn rollout_developer_texts(text: &str) -> Result<Vec<String>> {
|
||||
Ok(texts)
|
||||
}
|
||||
|
||||
fn request_hook_prompt_texts(
|
||||
request: &core_test_support::responses::ResponsesRequest,
|
||||
) -> Vec<String> {
|
||||
request
|
||||
.message_input_texts("user")
|
||||
.into_iter()
|
||||
.filter_map(|text| parse_hook_prompt_fragment(&text).map(|fragment| fragment.text))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_stop_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
fs::read_to_string(home.join("stop_hook_log.jsonl"))
|
||||
.context("read stop hook log")?
|
||||
@@ -147,23 +202,18 @@ async fn stop_hook_can_block_multiple_times_in_same_turn() -> Result<()> {
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
assert!(
|
||||
requests[1]
|
||||
.message_input_texts("developer")
|
||||
.contains(&FIRST_CONTINUATION_PROMPT.to_string()),
|
||||
"second request should include the first continuation prompt",
|
||||
assert_eq!(
|
||||
request_hook_prompt_texts(&requests[1]),
|
||||
vec![FIRST_CONTINUATION_PROMPT.to_string()],
|
||||
"second request should include the first continuation prompt as user hook context",
|
||||
);
|
||||
assert!(
|
||||
requests[2]
|
||||
.message_input_texts("developer")
|
||||
.contains(&FIRST_CONTINUATION_PROMPT.to_string()),
|
||||
"third request should retain the first continuation prompt from history",
|
||||
);
|
||||
assert!(
|
||||
requests[2]
|
||||
.message_input_texts("developer")
|
||||
.contains(&SECOND_CONTINUATION_PROMPT.to_string()),
|
||||
"third request should include the second continuation prompt",
|
||||
assert_eq!(
|
||||
request_hook_prompt_texts(&requests[2]),
|
||||
vec![
|
||||
FIRST_CONTINUATION_PROMPT.to_string(),
|
||||
SECOND_CONTINUATION_PROMPT.to_string(),
|
||||
],
|
||||
"third request should retain hook prompts in user history",
|
||||
);
|
||||
|
||||
let hook_inputs = read_stop_hook_inputs(test.codex_home_path())?;
|
||||
@@ -180,13 +230,13 @@ async fn stop_hook_can_block_multiple_times_in_same_turn() -> Result<()> {
|
||||
|
||||
let rollout_path = test.codex.rollout_path().expect("rollout path");
|
||||
let rollout_text = fs::read_to_string(&rollout_path)?;
|
||||
let developer_texts = rollout_developer_texts(&rollout_text)?;
|
||||
let hook_prompt_texts = rollout_hook_prompt_texts(&rollout_text)?;
|
||||
assert!(
|
||||
developer_texts.contains(&FIRST_CONTINUATION_PROMPT.to_string()),
|
||||
hook_prompt_texts.contains(&FIRST_CONTINUATION_PROMPT.to_string()),
|
||||
"rollout should persist the first continuation prompt",
|
||||
);
|
||||
assert!(
|
||||
developer_texts.contains(&SECOND_CONTINUATION_PROMPT.to_string()),
|
||||
hook_prompt_texts.contains(&SECOND_CONTINUATION_PROMPT.to_string()),
|
||||
"rollout should persist the second continuation prompt",
|
||||
);
|
||||
|
||||
@@ -260,11 +310,76 @@ async fn resumed_thread_keeps_stop_continuation_prompt_in_history() -> Result<()
|
||||
resumed.submit_turn("and now continue").await?;
|
||||
|
||||
let resumed_request = resumed_response.single_request();
|
||||
assert!(
|
||||
resumed_request
|
||||
.message_input_texts("developer")
|
||||
.contains(&FIRST_CONTINUATION_PROMPT.to_string()),
|
||||
"resumed request should keep the persisted continuation prompt in history",
|
||||
assert_eq!(
|
||||
request_hook_prompt_texts(&resumed_request),
|
||||
vec![FIRST_CONTINUATION_PROMPT.to_string()],
|
||||
"resumed request should keep the persisted continuation prompt in user history",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multiple_blocking_stop_hooks_persist_multiple_hook_prompt_fragments() -> 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_assistant_message("msg-1", "draft one"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-2", "final draft"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) = write_parallel_stop_hooks(
|
||||
home,
|
||||
&[FIRST_CONTINUATION_PROMPT, SECOND_CONTINUATION_PROMPT],
|
||||
) {
|
||||
panic!("failed to write parallel stop hook fixtures: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("hello again").await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert_eq!(
|
||||
request_hook_prompt_texts(&requests[1]),
|
||||
vec![
|
||||
FIRST_CONTINUATION_PROMPT.to_string(),
|
||||
SECOND_CONTINUATION_PROMPT.to_string(),
|
||||
],
|
||||
"second request should receive one user hook prompt message with both fragments",
|
||||
);
|
||||
|
||||
let rollout_path = test.codex.rollout_path().expect("rollout path");
|
||||
let rollout_text = fs::read_to_string(&rollout_path)?;
|
||||
assert_eq!(
|
||||
rollout_hook_prompt_texts(&rollout_text)?,
|
||||
vec![
|
||||
FIRST_CONTINUATION_PROMPT.to_string(),
|
||||
SECOND_CONTINUATION_PROMPT.to_string(),
|
||||
],
|
||||
"rollout should preserve both hook prompt fragments in order",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::protocol::HookCompletedEvent;
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
use codex_protocol::protocol::HookOutputEntry;
|
||||
@@ -34,7 +35,7 @@ pub struct StopOutcome {
|
||||
pub stop_reason: Option<String>,
|
||||
pub should_block: bool,
|
||||
pub block_reason: Option<String>,
|
||||
pub continuation_prompt: Option<String>,
|
||||
pub continuation_fragments: Vec<HookPromptFragment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
@@ -43,7 +44,7 @@ struct StopHandlerData {
|
||||
stop_reason: Option<String>,
|
||||
should_block: bool,
|
||||
block_reason: Option<String>,
|
||||
continuation_prompt: Option<String>,
|
||||
continuation_fragments: Vec<HookPromptFragment>,
|
||||
}
|
||||
|
||||
pub(crate) fn preview(
|
||||
@@ -77,7 +78,7 @@ pub(crate) async fn run(
|
||||
stop_reason: None,
|
||||
should_block: false,
|
||||
block_reason: None,
|
||||
continuation_prompt: None,
|
||||
continuation_fragments: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ pub(crate) async fn run(
|
||||
stop_reason: aggregate.stop_reason,
|
||||
should_block: aggregate.should_block,
|
||||
block_reason: aggregate.block_reason,
|
||||
continuation_prompt: aggregate.continuation_prompt,
|
||||
continuation_fragments: aggregate.continuation_fragments,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +241,14 @@ fn parse_completed(
|
||||
turn_id,
|
||||
run: dispatcher::completed_summary(handler, &run_result, status, entries),
|
||||
};
|
||||
let continuation_fragments = continuation_prompt
|
||||
.map(|prompt| {
|
||||
vec![HookPromptFragment::from_single_hook(
|
||||
prompt,
|
||||
completed.run.id.clone(),
|
||||
)]
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
dispatcher::ParsedHandler {
|
||||
completed,
|
||||
@@ -248,7 +257,7 @@ fn parse_completed(
|
||||
stop_reason,
|
||||
should_block,
|
||||
block_reason,
|
||||
continuation_prompt,
|
||||
continuation_fragments,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -267,12 +276,14 @@ fn aggregate_results<'a>(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let continuation_prompt = if should_block {
|
||||
join_block_text(results.iter().copied(), |result| {
|
||||
result.continuation_prompt.as_deref()
|
||||
})
|
||||
let continuation_fragments = if should_block {
|
||||
results
|
||||
.iter()
|
||||
.filter(|result| result.should_block)
|
||||
.flat_map(|result| result.continuation_fragments.clone())
|
||||
.collect()
|
||||
} else {
|
||||
None
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
StopHandlerData {
|
||||
@@ -280,7 +291,7 @@ fn aggregate_results<'a>(
|
||||
stop_reason,
|
||||
should_block,
|
||||
block_reason,
|
||||
continuation_prompt,
|
||||
continuation_fragments,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +347,7 @@ fn serialization_failure_outcome(
|
||||
stop_reason: None,
|
||||
should_block: false,
|
||||
block_reason: None,
|
||||
continuation_prompt: None,
|
||||
continuation_fragments: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +361,8 @@ mod tests {
|
||||
use codex_protocol::protocol::HookRunStatus;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
|
||||
use super::StopHandlerData;
|
||||
use super::aggregate_results;
|
||||
use super::parse_completed;
|
||||
@@ -375,7 +388,10 @@ mod tests {
|
||||
stop_reason: None,
|
||||
should_block: true,
|
||||
block_reason: Some("retry with tests".to_string()),
|
||||
continuation_prompt: Some("retry with tests".to_string()),
|
||||
continuation_fragments: vec![HookPromptFragment {
|
||||
text: "retry with tests".to_string(),
|
||||
hook_run_ids: vec![parsed.completed.run.id.clone()],
|
||||
}],
|
||||
}
|
||||
);
|
||||
assert_eq!(parsed.completed.run.status, HookRunStatus::Blocked);
|
||||
@@ -419,7 +435,7 @@ mod tests {
|
||||
stop_reason: Some("done".to_string()),
|
||||
should_block: false,
|
||||
block_reason: None,
|
||||
continuation_prompt: None,
|
||||
continuation_fragments: Vec::new(),
|
||||
}
|
||||
);
|
||||
assert_eq!(parsed.completed.run.status, HookRunStatus::Stopped);
|
||||
@@ -440,7 +456,10 @@ mod tests {
|
||||
stop_reason: None,
|
||||
should_block: true,
|
||||
block_reason: Some("retry with tests".to_string()),
|
||||
continuation_prompt: Some("retry with tests".to_string()),
|
||||
continuation_fragments: vec![HookPromptFragment {
|
||||
text: "retry with tests".to_string(),
|
||||
hook_run_ids: vec![parsed.completed.run.id.clone()],
|
||||
}],
|
||||
}
|
||||
);
|
||||
assert_eq!(parsed.completed.run.status, HookRunStatus::Blocked);
|
||||
@@ -509,14 +528,18 @@ mod tests {
|
||||
stop_reason: None,
|
||||
should_block: true,
|
||||
block_reason: Some("first".to_string()),
|
||||
continuation_prompt: Some("first".to_string()),
|
||||
continuation_fragments: vec![HookPromptFragment::from_single_hook(
|
||||
"first", "run-1",
|
||||
)],
|
||||
},
|
||||
&StopHandlerData {
|
||||
should_stop: false,
|
||||
stop_reason: None,
|
||||
should_block: true,
|
||||
block_reason: Some("second".to_string()),
|
||||
continuation_prompt: Some("second".to_string()),
|
||||
continuation_fragments: vec![HookPromptFragment::from_single_hook(
|
||||
"second", "run-2",
|
||||
)],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -527,7 +550,10 @@ mod tests {
|
||||
stop_reason: None,
|
||||
should_block: true,
|
||||
block_reason: Some("first\n\nsecond".to_string()),
|
||||
continuation_prompt: Some("first\n\nsecond".to_string()),
|
||||
continuation_fragments: vec![
|
||||
HookPromptFragment::from_single_hook("first", "run-1"),
|
||||
HookPromptFragment::from_single_hook("second", "run-2"),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::models::ContentItem;
|
||||
use crate::models::MessagePhase;
|
||||
use crate::models::ResponseItem;
|
||||
use crate::models::WebSearchAction;
|
||||
use crate::protocol::AgentMessageEvent;
|
||||
use crate::protocol::AgentReasoningEvent;
|
||||
@@ -21,6 +23,7 @@ use ts_rs::TS;
|
||||
#[ts(tag = "type")]
|
||||
pub enum TurnItem {
|
||||
UserMessage(UserMessageItem),
|
||||
HookPrompt(HookPromptItem),
|
||||
AgentMessage(AgentMessageItem),
|
||||
Plan(PlanItem),
|
||||
Reasoning(ReasoningItem),
|
||||
@@ -35,6 +38,25 @@ pub struct UserMessageItem {
|
||||
pub content: Vec<UserInput>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
|
||||
pub struct HookPromptItem {
|
||||
pub id: String,
|
||||
pub fragments: Vec<HookPromptFragment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
pub struct HookPromptFragment {
|
||||
pub text: String,
|
||||
pub hook_run_ids: Vec<String>,
|
||||
}
|
||||
|
||||
const HOOK_PROMPT_OPEN_TAG_PREFIX: &str = "<hook_prompt";
|
||||
const HOOK_PROMPT_CLOSE_TAG: &str = "</hook_prompt>";
|
||||
const HOOK_PROMPT_RUN_ID_ATTR: &str = "hook_run_id";
|
||||
const HOOK_PROMPT_RUN_IDS_ATTR: &str = "hook_run_ids";
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
#[ts(tag = "type")]
|
||||
@@ -195,6 +217,161 @@ impl UserMessageItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl HookPromptItem {
|
||||
pub fn from_fragments(id: Option<&String>, fragments: Vec<HookPromptFragment>) -> Self {
|
||||
Self {
|
||||
id: id
|
||||
.cloned()
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
|
||||
fragments,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HookPromptFragment {
|
||||
pub fn from_single_hook(text: impl Into<String>, hook_run_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
hook_run_ids: vec![hook_run_id.into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<ResponseItem> {
|
||||
let content = fragments
|
||||
.iter()
|
||||
.filter(|fragment| !fragment.hook_run_ids.is_empty())
|
||||
.filter_map(|fragment| {
|
||||
serialize_hook_prompt_fragment(&fragment.text, &fragment.hook_run_ids)
|
||||
.map(|text| ContentItem::InputText { text })
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ResponseItem::Message {
|
||||
id: Some(uuid::Uuid::new_v4().to_string()),
|
||||
role: "user".to_string(),
|
||||
content,
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_hook_prompt_message(
|
||||
id: Option<&String>,
|
||||
content: &[ContentItem],
|
||||
) -> Option<HookPromptItem> {
|
||||
let fragments = content
|
||||
.iter()
|
||||
.map(|content_item| {
|
||||
let ContentItem::InputText { text } = content_item else {
|
||||
return None;
|
||||
};
|
||||
parse_hook_prompt_fragment(text)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
|
||||
if fragments.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(HookPromptItem::from_fragments(id, fragments))
|
||||
}
|
||||
|
||||
pub fn parse_hook_prompt_fragment(text: &str) -> Option<HookPromptFragment> {
|
||||
let trimmed = text.trim();
|
||||
if !trimmed.starts_with(HOOK_PROMPT_OPEN_TAG_PREFIX)
|
||||
|| !trimmed.ends_with(HOOK_PROMPT_CLOSE_TAG)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let open_tag_end = trimmed.find('>')?;
|
||||
let open_tag = &trimmed[..=open_tag_end];
|
||||
let hook_run_ids = parse_hook_prompt_hook_run_ids(open_tag)?;
|
||||
if hook_run_ids.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let body_start = open_tag_end + 1;
|
||||
let body_end = trimmed.len().checked_sub(HOOK_PROMPT_CLOSE_TAG.len())?;
|
||||
if body_end < body_start {
|
||||
return None;
|
||||
}
|
||||
let body = &trimmed[body_start..body_end];
|
||||
|
||||
Some(HookPromptFragment {
|
||||
text: unescape_hook_prompt_xml(body),
|
||||
hook_run_ids,
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_hook_prompt_fragment(text: &str, hook_run_ids: &[String]) -> Option<String> {
|
||||
let escaped_text = escape_hook_prompt_xml(text);
|
||||
match hook_run_ids {
|
||||
[hook_run_id] => Some(format!(
|
||||
r#"<hook_prompt {HOOK_PROMPT_RUN_ID_ATTR}="{hook_run_id}">{escaped_text}</hook_prompt>"#,
|
||||
hook_run_id = escape_hook_prompt_xml(hook_run_id),
|
||||
)),
|
||||
_ => {
|
||||
let encoded_hook_run_ids = serde_json::to_string(hook_run_ids).ok()?;
|
||||
Some(format!(
|
||||
r#"<hook_prompt {HOOK_PROMPT_RUN_IDS_ATTR}="{hook_run_ids}">{escaped_text}</hook_prompt>"#,
|
||||
hook_run_ids = escape_hook_prompt_xml(&encoded_hook_run_ids),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hook_prompt_attribute(open_tag: &str, attribute_name: &str) -> Option<String> {
|
||||
let marker = format!(r#"{attribute_name}=""#);
|
||||
let start = open_tag.find(&marker)? + marker.len();
|
||||
let value_end = open_tag[start..].find('"')?;
|
||||
Some(unescape_hook_prompt_xml(
|
||||
&open_tag[start..start + value_end],
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_hook_prompt_hook_run_ids(open_tag: &str) -> Option<Vec<String>> {
|
||||
if let Some(encoded_hook_run_ids) =
|
||||
parse_hook_prompt_attribute(open_tag, HOOK_PROMPT_RUN_IDS_ATTR)
|
||||
{
|
||||
let hook_run_ids = serde_json::from_str::<Vec<String>>(&encoded_hook_run_ids).ok()?;
|
||||
if hook_run_ids
|
||||
.iter()
|
||||
.any(|hook_run_id| hook_run_id.trim().is_empty())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
return Some(hook_run_ids);
|
||||
}
|
||||
|
||||
let hook_run_id = parse_hook_prompt_attribute(open_tag, HOOK_PROMPT_RUN_ID_ATTR)?;
|
||||
if hook_run_id.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(vec![hook_run_id])
|
||||
}
|
||||
|
||||
fn escape_hook_prompt_xml(text: &str) -> String {
|
||||
text.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn unescape_hook_prompt_xml(text: &str) -> String {
|
||||
text.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", "\"")
|
||||
.replace("'", "'")
|
||||
.replace("&", "&")
|
||||
}
|
||||
|
||||
impl AgentMessageItem {
|
||||
pub fn new(content: &[AgentMessageContent]) -> Self {
|
||||
Self {
|
||||
@@ -266,6 +443,7 @@ impl TurnItem {
|
||||
pub fn id(&self) -> String {
|
||||
match self {
|
||||
TurnItem::UserMessage(item) => item.id.clone(),
|
||||
TurnItem::HookPrompt(item) => item.id.clone(),
|
||||
TurnItem::AgentMessage(item) => item.id.clone(),
|
||||
TurnItem::Plan(item) => item.id.clone(),
|
||||
TurnItem::Reasoning(item) => item.id.clone(),
|
||||
@@ -278,6 +456,7 @@ impl TurnItem {
|
||||
pub fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
|
||||
match self {
|
||||
TurnItem::UserMessage(item) => vec![item.as_legacy_event()],
|
||||
TurnItem::HookPrompt(_) => Vec::new(),
|
||||
TurnItem::AgentMessage(item) => item.as_legacy_events(),
|
||||
TurnItem::Plan(_) => Vec::new(),
|
||||
TurnItem::WebSearch(item) => vec![item.as_legacy_event()],
|
||||
@@ -287,3 +466,42 @@ impl TurnItem {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn hook_prompt_roundtrips_multiple_hook_run_ids() {
|
||||
let original = HookPromptFragment {
|
||||
text: "Retry with care & joy.".to_string(),
|
||||
hook_run_ids: vec!["hook-run-1".to_string(), "hook-run-2".to_string()],
|
||||
};
|
||||
let message =
|
||||
build_hook_prompt_message(std::slice::from_ref(&original)).expect("hook prompt");
|
||||
|
||||
let ResponseItem::Message { content, .. } = message else {
|
||||
panic!("expected hook prompt message");
|
||||
};
|
||||
|
||||
let parsed = parse_hook_prompt_message(None, &content).expect("parsed hook prompt");
|
||||
assert_eq!(parsed.fragments, vec![original]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hook_prompt_parses_legacy_single_hook_run_id() {
|
||||
let parsed = parse_hook_prompt_fragment(
|
||||
r#"<hook_prompt hook_run_id="hook-run-1">Retry with tests.</hook_prompt>"#,
|
||||
)
|
||||
.expect("legacy hook prompt");
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
HookPromptFragment {
|
||||
text: "Retry with tests.".to_string(),
|
||||
hook_run_ids: vec!["hook-run-1".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +497,7 @@ fn turn_snapshot_events(
|
||||
}),
|
||||
);
|
||||
}
|
||||
TurnItem::HookPrompt(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,6 +616,7 @@ fn thread_item_to_core(item: &ThreadItem) -> Option<TurnItem> {
|
||||
| ThreadItem::McpToolCall { .. }
|
||||
| ThreadItem::DynamicToolCall { .. }
|
||||
| ThreadItem::CollabAgentToolCall { .. }
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::ImageView { .. }
|
||||
| ThreadItem::EnteredReviewMode { .. }
|
||||
| ThreadItem::ExitedReviewMode { .. } => {
|
||||
|
||||
Reference in New Issue
Block a user