feat: add memory citation to agent message (#14821)

Client side to come
This commit is contained in:
jif-oai 2026-03-18 10:03:38 +00:00 committed by GitHub
parent 0f9484dc8a
commit a265d6043e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1420 additions and 40 deletions

View file

@ -1455,6 +1455,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -2181,6 +2229,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -8600,6 +8600,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/v2/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MergeStrategy": {
"enum": [
"replace",
@ -11841,6 +11889,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/v2/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -5388,6 +5388,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MergeStrategy": {
"enum": [
"replace",
@ -9601,6 +9649,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -289,6 +289,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -444,6 +492,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -289,6 +289,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -444,6 +492,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -403,6 +403,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -558,6 +606,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -488,6 +488,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -1038,6 +1086,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -426,6 +426,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -796,6 +844,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -426,6 +426,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -796,6 +844,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -426,6 +426,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -796,6 +844,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -488,6 +488,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -1038,6 +1086,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -426,6 +426,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -796,6 +844,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -488,6 +488,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -1038,6 +1086,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -426,6 +426,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -796,6 +844,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -426,6 +426,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -796,6 +844,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -403,6 +403,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -558,6 +606,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -403,6 +403,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -558,6 +606,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -403,6 +403,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@ -558,6 +606,17 @@
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{

View file

@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MemoryCitationEntry } from "./MemoryCitationEntry";
export type MemoryCitation = { entries: Array<MemoryCitationEntry>, threadIds: Array<string>, };

View file

@ -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 MemoryCitationEntry = { path: string, lineStart: number, lineEnd: number, note: string, };

View file

@ -15,11 +15,12 @@ import type { FileUpdateChange } from "./FileUpdateChange";
import type { McpToolCallError } from "./McpToolCallError";
import type { McpToolCallResult } from "./McpToolCallResult";
import type { McpToolCallStatus } from "./McpToolCallStatus";
import type { MemoryCitation } from "./MemoryCitation";
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": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | 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.
*/

View file

@ -174,6 +174,8 @@ export type { McpToolCallError } from "./McpToolCallError";
export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification";
export type { McpToolCallResult } from "./McpToolCallResult";
export type { McpToolCallStatus } from "./McpToolCallStatus";
export type { MemoryCitation } from "./MemoryCitation";
export type { MemoryCitationEntry } from "./MemoryCitationEntry";
export type { MergeStrategy } from "./MergeStrategy";
export type { Model } from "./Model";
export type { ModelAvailabilityNux } from "./ModelAvailabilityNux";

View file

@ -118,9 +118,11 @@ impl ThreadHistoryBuilder {
pub fn handle_event(&mut self, event: &EventMsg) {
match event {
EventMsg::UserMessage(payload) => self.handle_user_message(payload),
EventMsg::AgentMessage(payload) => {
self.handle_agent_message(payload.message.clone(), payload.phase.clone())
}
EventMsg::AgentMessage(payload) => self.handle_agent_message(
payload.message.clone(),
payload.phase.clone(),
payload.memory_citation.clone().map(Into::into),
),
EventMsg::AgentReasoning(payload) => self.handle_agent_reasoning(payload),
EventMsg::AgentReasoningRawContent(payload) => {
self.handle_agent_reasoning_raw_content(payload)
@ -208,15 +210,23 @@ impl ThreadHistoryBuilder {
self.current_turn = Some(turn);
}
fn handle_agent_message(&mut self, text: String, phase: Option<MessagePhase>) {
fn handle_agent_message(
&mut self,
text: String,
phase: Option<MessagePhase>,
memory_citation: Option<crate::protocol::v2::MemoryCitation>,
) {
if text.is_empty() {
return;
}
let id = self.next_item_id();
self.ensure_turn()
.items
.push(ThreadItem::AgentMessage { id, text, phase });
self.ensure_turn().items.push(ThreadItem::AgentMessage {
id,
text,
phase,
memory_citation,
});
}
fn handle_agent_reasoning(&mut self, payload: &AgentReasoningEvent) {
@ -1178,6 +1188,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "Hi there".into(),
phase: None,
memory_citation: None,
}),
EventMsg::AgentReasoning(AgentReasoningEvent {
text: "thinking".into(),
@ -1194,6 +1205,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "Reply two".into(),
phase: None,
memory_citation: None,
}),
];
@ -1229,6 +1241,7 @@ mod tests {
id: "item-2".into(),
text: "Hi there".into(),
phase: None,
memory_citation: None,
}
);
assert_eq!(
@ -1260,6 +1273,7 @@ mod tests {
id: "item-5".into(),
text: "Reply two".into(),
phase: None,
memory_citation: None,
}
);
}
@ -1318,6 +1332,7 @@ mod tests {
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
message: "Final reply".into(),
phase: Some(CoreMessagePhase::FinalAnswer),
memory_citation: None,
})];
let items = events
@ -1332,6 +1347,7 @@ mod tests {
id: "item-1".into(),
text: "Final reply".into(),
phase: Some(MessagePhase::FinalAnswer),
memory_citation: None,
}
);
}
@ -1354,6 +1370,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "interlude".into(),
phase: None,
memory_citation: None,
}),
EventMsg::AgentReasoning(AgentReasoningEvent {
text: "second summary".into(),
@ -1399,6 +1416,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "Working...".into(),
phase: None,
memory_citation: None,
}),
EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some("turn-1".into()),
@ -1413,6 +1431,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "Second attempt complete.".into(),
phase: None,
memory_citation: None,
}),
];
@ -1442,6 +1461,7 @@ mod tests {
id: "item-2".into(),
text: "Working...".into(),
phase: None,
memory_citation: None,
}
);
@ -1464,6 +1484,7 @@ mod tests {
id: "item-4".into(),
text: "Second attempt complete.".into(),
phase: None,
memory_citation: None,
}
);
}
@ -1480,6 +1501,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A1".into(),
phase: None,
memory_citation: None,
}),
EventMsg::UserMessage(UserMessageEvent {
message: "Second".into(),
@ -1490,6 +1512,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A2".into(),
phase: None,
memory_citation: None,
}),
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }),
EventMsg::UserMessage(UserMessageEvent {
@ -1501,6 +1524,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A3".into(),
phase: None,
memory_citation: None,
}),
];
@ -1529,6 +1553,7 @@ mod tests {
id: "item-2".into(),
text: "A1".into(),
phase: None,
memory_citation: None,
},
]
);
@ -1546,6 +1571,7 @@ mod tests {
id: "item-4".into(),
text: "A3".into(),
phase: None,
memory_citation: None,
},
]
);
@ -1563,6 +1589,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A1".into(),
phase: None,
memory_citation: None,
}),
EventMsg::UserMessage(UserMessageEvent {
message: "Two".into(),
@ -1573,6 +1600,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A2".into(),
phase: None,
memory_citation: None,
}),
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }),
];
@ -2209,6 +2237,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "still in b".into(),
phase: None,
memory_citation: None,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
@ -2263,6 +2292,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "still in b".into(),
phase: None,
memory_citation: None,
}),
];
@ -2497,6 +2527,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "done".into(),
phase: None,
memory_citation: None,
}),
EventMsg::Error(ErrorEvent {
message: "rollback failed".into(),

View file

@ -30,6 +30,8 @@ use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::mcp::Resource as McpResource;
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
use codex_protocol::mcp::Tool as McpTool;
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
use codex_protocol::models::MacOsAutomationPermission as CoreMacOsAutomationPermission;
use codex_protocol::models::MacOsContactsPermission as CoreMacOsContactsPermission;
@ -3568,6 +3570,44 @@ pub struct Turn {
pub error: Option<TurnError>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct MemoryCitation {
pub entries: Vec<MemoryCitationEntry>,
pub thread_ids: Vec<String>,
}
impl From<CoreMemoryCitation> for MemoryCitation {
fn from(value: CoreMemoryCitation) -> Self {
Self {
entries: value.entries.into_iter().map(Into::into).collect(),
thread_ids: value.rollout_ids,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct MemoryCitationEntry {
pub path: String,
pub line_start: u32,
pub line_end: u32,
pub note: String,
}
impl From<CoreMemoryCitationEntry> for MemoryCitationEntry {
fn from(value: CoreMemoryCitationEntry) -> Self {
Self {
path: value.path,
line_start: value.line_start,
line_end: value.line_end,
note: value.note,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Error)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@ -4068,6 +4108,8 @@ pub enum ThreadItem {
text: String,
#[serde(default)]
phase: Option<MessagePhase>,
#[serde(default)]
memory_citation: Option<MemoryCitation>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
@ -4317,6 +4359,7 @@ impl From<CoreTurnItem> for ThreadItem {
id: agent.id,
text,
phase: agent.phase,
memory_citation: agent.memory_citation.map(Into::into),
}
}
CoreTurnItem::Plan(plan) => ThreadItem::Plan {
@ -7393,6 +7436,7 @@ mod tests {
},
],
phase: None,
memory_citation: None,
});
assert_eq!(
@ -7401,6 +7445,7 @@ mod tests {
id: "agent-1".to_string(),
text: "Hello world".to_string(),
phase: None,
memory_citation: None,
}
);
@ -7410,6 +7455,15 @@ mod tests {
text: "final".to_string(),
}],
phase: Some(MessagePhase::FinalAnswer),
memory_citation: Some(CoreMemoryCitation {
entries: vec![CoreMemoryCitationEntry {
path: "MEMORY.md".to_string(),
line_start: 1,
line_end: 2,
note: "summary".to_string(),
}],
rollout_ids: vec!["rollout-1".to_string()],
}),
});
assert_eq!(
@ -7418,6 +7472,15 @@ mod tests {
id: "agent-2".to_string(),
text: "final".to_string(),
phase: Some(MessagePhase::FinalAnswer),
memory_citation: Some(MemoryCitation {
entries: vec![MemoryCitationEntry {
path: "MEMORY.md".to_string(),
line_start: 1,
line_end: 2,
note: "summary".to_string(),
}],
thread_ids: vec!["rollout-1".to_string()],
}),
}
);

View file

@ -450,6 +450,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
"payload": serde_json::to_value(EventMsg::AgentMessage(AgentMessageEvent {
message: "Still running".to_string(),
phase: None,
memory_citation: None,
}))?,
})
.to_string(),

View file

@ -6856,6 +6856,7 @@ async fn emit_agent_message_in_plan_mode(
id: agent_message_id.clone(),
content: Vec::new(),
phase: None,
memory_citation: None,
})
});
sess.emit_turn_item_started(turn_context, &start_item).await;

View file

@ -83,7 +83,12 @@ fn parse_agent_message(
}
}
let id = id.cloned().unwrap_or_else(|| Uuid::new_v4().to_string());
AgentMessageItem { id, content, phase }
AgentMessageItem {
id,
content,
phase,
memory_citation: None,
}
}
pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {

View file

@ -1,36 +1,89 @@
use codex_protocol::ThreadId;
use codex_protocol::memory_citation::MemoryCitation;
use codex_protocol::memory_citation::MemoryCitationEntry;
use std::collections::HashSet;
pub fn parse_memory_citation(citations: Vec<String>) -> Option<MemoryCitation> {
let mut entries = Vec::new();
let mut rollout_ids = Vec::new();
let mut seen_rollout_ids = HashSet::new();
pub fn get_thread_id_from_citations(citations: Vec<String>) -> Vec<ThreadId> {
let mut result = Vec::new();
for citation in citations {
let mut ids_block = None;
for (open, close) in [
("<thread_ids>", "</thread_ids>"),
("<rollout_ids>", "</rollout_ids>"),
] {
if let Some((_, rest)) = citation.split_once(open)
&& let Some((ids, _)) = rest.split_once(close)
{
ids_block = Some(ids);
break;
}
if let Some(entries_block) =
extract_block(&citation, "<citation_entries>", "</citation_entries>")
{
entries.extend(
entries_block
.lines()
.filter_map(parse_memory_citation_entry),
);
}
if let Some(ids_block) = ids_block {
if let Some(ids_block) = extract_ids_block(&citation) {
for id in ids_block
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
{
if let Ok(thread_id) = ThreadId::try_from(id) {
result.push(thread_id);
if seen_rollout_ids.insert(id.to_string()) {
rollout_ids.push(id.to_string());
}
}
}
}
if entries.is_empty() && rollout_ids.is_empty() {
None
} else {
Some(MemoryCitation {
entries,
rollout_ids,
})
}
}
pub fn get_thread_id_from_citations(citations: Vec<String>) -> Vec<ThreadId> {
let mut result = Vec::new();
if let Some(memory_citation) = parse_memory_citation(citations) {
for rollout_id in memory_citation.rollout_ids {
if let Ok(thread_id) = ThreadId::try_from(rollout_id.as_str()) {
result.push(thread_id);
}
}
}
result
}
fn parse_memory_citation_entry(line: &str) -> Option<MemoryCitationEntry> {
let line = line.trim();
if line.is_empty() {
return None;
}
let (location, note) = line.rsplit_once("|note=[")?;
let note = note.strip_suffix(']')?.trim().to_string();
let (path, line_range) = location.rsplit_once(':')?;
let (line_start, line_end) = line_range.split_once('-')?;
Some(MemoryCitationEntry {
path: path.trim().to_string(),
line_start: line_start.trim().parse().ok()?,
line_end: line_end.trim().parse().ok()?,
note,
})
}
fn extract_block<'a>(text: &'a str, open: &str, close: &str) -> Option<&'a str> {
let (_, rest) = text.split_once(open)?;
let (body, _) = rest.split_once(close)?;
Some(body)
}
fn extract_ids_block(text: &str) -> Option<&str> {
extract_block(text, "<rollout_ids>", "</rollout_ids>")
.or_else(|| extract_block(text, "<thread_ids>", "</thread_ids>"))
}
#[cfg(test)]
#[path = "citations_tests.rs"]
mod tests;

View file

@ -1,4 +1,5 @@
use super::get_thread_id_from_citations;
use super::parse_memory_citation;
use codex_protocol::ThreadId;
use pretty_assertions::assert_eq;
@ -24,3 +25,40 @@ fn get_thread_id_from_citations_supports_legacy_rollout_ids() {
assert_eq!(get_thread_id_from_citations(citations), vec![thread_id]);
}
#[test]
fn parse_memory_citation_extracts_entries_and_rollout_ids() {
let first = ThreadId::new();
let second = ThreadId::new();
let citations = vec![format!(
"<citation_entries>\nMEMORY.md:1-2|note=[summary]\nrollout_summaries/foo.md:10-12|note=[details]\n</citation_entries>\n<rollout_ids>\n{first}\n{second}\n{first}\n</rollout_ids>"
)];
let parsed = parse_memory_citation(citations).expect("memory citation should parse");
assert_eq!(
parsed
.entries
.iter()
.map(|entry| (
entry.path.clone(),
entry.line_start,
entry.line_end,
entry.note.clone(),
))
.collect::<Vec<_>>(),
vec![
("MEMORY.md".to_string(), 1, 2, "summary".to_string()),
(
"rollout_summaries/foo.md".to_string(),
10,
12,
"details".to_string()
),
]
);
assert_eq!(
parsed.rollout_ids,
vec![first.to_string(), second.to_string()]
);
}

View file

@ -85,6 +85,7 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
AgentMessageEvent {
message: "buffered-event".to_string(),
phase: None,
memory_citation: None,
},
))])
.await?;
@ -201,6 +202,7 @@ async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Resu
AgentMessageEvent {
message: "assistant text".to_string(),
phase: None,
memory_citation: None,
},
))])
.await?;
@ -251,6 +253,7 @@ async fn metadata_irrelevant_events_fall_back_to_upsert_when_thread_missing() ->
AgentMessageEvent {
message: "assistant text".to_string(),
phase: None,
memory_citation: None,
},
))];

View file

@ -15,6 +15,7 @@ use crate::error::CodexErr;
use crate::error::Result;
use crate::function_tool::FunctionCallError;
use crate::memories::citations::get_thread_id_from_citations;
use crate::memories::citations::parse_memory_citation;
use crate::parse_turn_item;
use crate::state_db;
use crate::tools::parallel::ToolCallRuntime;
@ -38,6 +39,22 @@ fn strip_hidden_assistant_markup(text: &str, plan_mode: bool) -> String {
}
}
fn strip_hidden_assistant_markup_and_parse_memory_citation(
text: &str,
plan_mode: bool,
) -> (
String,
Option<codex_protocol::memory_citation::MemoryCitation>,
) {
let (without_citations, citations) = strip_citations(text);
let visible_text = if plan_mode {
strip_proposed_plan_blocks(&without_citations)
} else {
without_citations
};
(visible_text, parse_memory_citation(citations))
}
pub(crate) fn raw_assistant_output_text_from_item(item: &ResponseItem) -> Option<String> {
if let ResponseItem::Message { role, content, .. } = item
&& role == "assistant"
@ -297,9 +314,11 @@ pub(crate) async fn handle_non_tool_response_item(
codex_protocol::items::AgentMessageContent::Text { text } => text.as_str(),
})
.collect::<String>();
let stripped = strip_hidden_assistant_markup(&combined, plan_mode);
let (stripped, memory_citation) =
strip_hidden_assistant_markup_and_parse_memory_citation(&combined, plan_mode);
agent_message.content =
vec![codex_protocol::items::AgentMessageContent::Text { text: stripped }];
agent_message.memory_citation = memory_citation;
}
if let TurnItem::ImageGeneration(image_item) = &mut turn_item {
match save_image_generation_result(&image_item.id, &image_item.result).await {

View file

@ -23,7 +23,9 @@ fn assistant_output_text(text: &str) -> ResponseItem {
#[tokio::test]
async fn handle_non_tool_response_item_strips_citations_from_assistant_message() {
let (session, turn_context) = make_session_and_context().await;
let item = assistant_output_text("hello<oai-mem-citation>doc1</oai-mem-citation> world");
let item = assistant_output_text(
"hello<oai-mem-citation><citation_entries>\nMEMORY.md:1-2|note=[x]\n</citation_entries>\n<rollout_ids>\n019cc2ea-1dff-7902-8d40-c8f6e5d83cc4\n</rollout_ids></oai-mem-citation> world",
);
let turn_item = handle_non_tool_response_item(&session, &turn_context, &item, false)
.await
@ -40,6 +42,15 @@ async fn handle_non_tool_response_item_strips_citations_from_assistant_message()
})
.collect::<String>();
assert_eq!(text, "hello world");
let memory_citation = agent_message
.memory_citation
.expect("memory citation should be parsed");
assert_eq!(memory_citation.entries.len(), 1);
assert_eq!(memory_citation.entries[0].path, "MEMORY.md");
assert_eq!(
memory_citation.rollout_ids,
vec!["019cc2ea-1dff-7902-8d40-c8f6e5d83cc4".to_string()]
);
}
#[test]

View file

@ -58,6 +58,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
id: "msg-1".to_string(),
content: Vec::new(),
phase: None,
memory_citation: None,
}))
.await
.is_some()
@ -68,6 +69,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() {
id: "msg-2".to_string(),
content: Vec::new(),
phase: None,
memory_citation: None,
}))
.await,
None

View file

@ -749,6 +749,7 @@ fn agent_message_produces_item_completed_agent_message() {
EventMsg::AgentMessage(AgentMessageEvent {
message: "hello".to_string(),
phase: None,
memory_citation: None,
}),
);
let out = ep.collect_thread_events(&ev);

View file

@ -1,3 +1,4 @@
use crate::memory_citation::MemoryCitation;
use crate::models::MessagePhase;
use crate::models::WebSearchAction;
use crate::protocol::AgentMessageEvent;
@ -58,6 +59,9 @@ pub struct AgentMessageItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub phase: Option<MessagePhase>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub memory_citation: Option<MemoryCitation>,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
@ -201,6 +205,7 @@ impl AgentMessageItem {
id: uuid::Uuid::new_v4().to_string(),
content: content.to_vec(),
phase: None,
memory_citation: None,
}
}
@ -211,6 +216,7 @@ impl AgentMessageItem {
AgentMessageContent::Text { text } => EventMsg::AgentMessage(AgentMessageEvent {
message: text.clone(),
phase: self.phase.clone(),
memory_citation: self.memory_citation.clone(),
}),
})
.collect()

View file

@ -7,6 +7,7 @@ pub mod custom_prompts;
pub mod dynamic_tools;
pub mod items;
pub mod mcp;
pub mod memory_citation;
pub mod message_history;
pub mod models;
pub mod num_format;

View file

@ -0,0 +1,20 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct MemoryCitation {
pub entries: Vec<MemoryCitationEntry>,
pub rollout_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct MemoryCitationEntry {
pub path: String,
pub line_start: u32,
pub line_end: u32,
pub note: String,
}

View file

@ -32,6 +32,7 @@ use crate::mcp::RequestId;
use crate::mcp::Resource as McpResource;
use crate::mcp::ResourceTemplate as McpResourceTemplate;
use crate::mcp::Tool as McpTool;
use crate::memory_citation::MemoryCitation;
use crate::message_history::HistoryEntry;
use crate::models::BaseInstructions;
use crate::models::ContentItem;
@ -1996,6 +1997,8 @@ pub struct AgentMessageEvent {
pub message: String,
#[serde(default)]
pub phase: Option<MessagePhase>,
#[serde(default)]
pub memory_citation: Option<MemoryCitation>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]

View file

@ -203,6 +203,7 @@ async fn resumed_initial_messages_render_history() {
EventMsg::AgentMessage(AgentMessageEvent {
message: "assistant reply".to_string(),
phase: None,
memory_citation: None,
}),
]),
network_proxy: None,
@ -251,6 +252,7 @@ async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() {
text: "assistant reply".to_string(),
}],
phase: None,
memory_citation: None,
}),
}),
});
@ -259,6 +261,7 @@ async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() {
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "assistant reply".to_string(),
phase: None,
memory_citation: None,
}),
});
@ -1547,6 +1550,7 @@ async fn live_agent_message_renders_during_review_mode() {
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "Review progress update".to_string(),
phase: None,
memory_citation: None,
}),
});
@ -1573,6 +1577,7 @@ async fn thread_snapshot_replay_preserves_agent_message_during_review_mode() {
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "Review progress update".to_string(),
phase: None,
memory_citation: None,
}),
});
@ -3551,6 +3556,7 @@ fn complete_assistant_message(
text: text.to_string(),
}],
phase,
memory_citation: None,
}),
}),
});
@ -4146,6 +4152,7 @@ async fn live_legacy_agent_message_after_item_completed_does_not_duplicate_assis
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "hello".into(),
phase: Some(MessagePhase::FinalAnswer),
memory_citation: None,
}),
});
@ -6010,6 +6017,7 @@ async fn slash_copy_is_unavailable_when_legacy_agent_message_is_not_repeated_on_
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "Legacy final message".into(),
phase: None,
memory_citation: None,
}),
});
let _ = drain_insert_history(&mut rx);
@ -10855,6 +10863,7 @@ async fn deltas_then_same_final_message_are_rendered_snapshot() {
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "Here is the result.".into(),
phase: None,
memory_citation: None,
}),
});

View file

@ -7007,6 +7007,7 @@ guardian_approval = true
id: "assistant-1".to_string(),
text: "restored response".to_string(),
phase: None,
memory_citation: None,
},
],
status: TurnStatus::Completed,
@ -7120,6 +7121,7 @@ guardian_approval = true
id: "assistant-1".to_string(),
text: "restored response".to_string(),
phase: None,
memory_citation: None,
},
],
status: TurnStatus::Completed,

View file

@ -668,13 +668,33 @@ fn thread_item_to_core(item: &ThreadItem) -> Option<TurnItem> {
.map(codex_app_server_protocol::UserInput::into_core)
.collect(),
})),
ThreadItem::AgentMessage { id, text, phase } => {
Some(TurnItem::AgentMessage(AgentMessageItem {
id: id.clone(),
content: vec![AgentMessageContent::Text { text: text.clone() }],
phase: phase.clone(),
}))
}
ThreadItem::AgentMessage {
id,
text,
phase,
memory_citation,
} => Some(TurnItem::AgentMessage(AgentMessageItem {
id: id.clone(),
content: vec![AgentMessageContent::Text { text: text.clone() }],
phase: phase.clone(),
memory_citation: memory_citation.clone().map(|citation| {
codex_protocol::memory_citation::MemoryCitation {
entries: citation
.entries
.into_iter()
.map(
|entry| codex_protocol::memory_citation::MemoryCitationEntry {
path: entry.path,
line_start: entry.line_start,
line_end: entry.line_end,
note: entry.note,
},
)
.collect(),
rollout_ids: citation.thread_ids,
}
}),
})),
ThreadItem::Plan { id, text } => Some(TurnItem::Plan(PlanItem {
id: id.clone(),
text: text.clone(),
@ -897,6 +917,7 @@ mod tests {
id: item_id,
text: "Hello from your coding assistant.".to_string(),
phase: Some(MessagePhase::FinalAnswer),
memory_citation: None,
},
thread_id: thread_id.clone(),
turn_id: turn_id.clone(),
@ -921,13 +942,19 @@ mod tests {
);
assert_eq!(completed.turn_id, turn_id);
match &completed.item {
TurnItem::AgentMessage(AgentMessageItem { id, content, phase }) => {
TurnItem::AgentMessage(AgentMessageItem {
id,
content,
phase,
memory_citation,
}) => {
assert_eq!(id, "msg_123");
let [AgentMessageContent::Text { text }] = content.as_slice() else {
panic!("expected a single text content item");
};
assert_eq!(text, "Hello from your coding assistant.");
assert_eq!(*phase, Some(MessagePhase::FinalAnswer));
assert_eq!(*memory_citation, None);
}
_ => panic!("expected bridged agent message item"),
}
@ -1111,6 +1138,7 @@ mod tests {
id: "assistant-1".to_string(),
text: "hi".to_string(),
phase: Some(MessagePhase::FinalAnswer),
memory_citation: None,
},
],
status: TurnStatus::Completed,

View file

@ -1117,6 +1117,7 @@ mod tests {
id: "assistant-1".to_string(),
text: "assistant reply".to_string(),
phase: None,
memory_citation: None,
},
],
status: TurnStatus::Completed,

View file

@ -202,6 +202,7 @@ async fn resumed_initial_messages_render_history() {
EventMsg::AgentMessage(AgentMessageEvent {
message: "assistant reply".to_string(),
phase: None,
memory_citation: None,
}),
]),
network_proxy: None,
@ -250,6 +251,7 @@ async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() {
text: "assistant reply".to_string(),
}],
phase: None,
memory_citation: None,
}),
}),
});
@ -258,6 +260,7 @@ async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() {
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "assistant reply".to_string(),
phase: None,
memory_citation: None,
}),
});
@ -1546,6 +1549,7 @@ async fn live_agent_message_renders_during_review_mode() {
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "Review progress update".to_string(),
phase: None,
memory_citation: None,
}),
});
@ -1572,6 +1576,7 @@ async fn thread_snapshot_replay_preserves_agent_message_during_review_mode() {
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "Review progress update".to_string(),
phase: None,
memory_citation: None,
}),
});
@ -3521,6 +3526,7 @@ fn complete_assistant_message(
text: text.to_string(),
}],
phase,
memory_citation: None,
}),
}),
});
@ -4110,6 +4116,7 @@ async fn live_legacy_agent_message_after_item_completed_does_not_duplicate_assis
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "hello".into(),
phase: Some(MessagePhase::FinalAnswer),
memory_citation: None,
}),
});
@ -5958,6 +5965,7 @@ async fn slash_copy_is_unavailable_when_legacy_agent_message_is_not_repeated_on_
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "Legacy final message".into(),
phase: None,
memory_citation: None,
}),
});
let _ = drain_insert_history(&mut rx);
@ -8497,11 +8505,8 @@ async fn permissions_selection_history_snapshot_full_access_to_default() {
.approval_policy
.set(AskForApproval::Never)
.expect("set approval policy");
chat.config
.permissions
.sandbox_policy
.set(SandboxPolicy::DangerFullAccess)
.expect("set sandbox policy");
chat.config.permissions.sandbox_policy =
Constrained::allow_any(SandboxPolicy::DangerFullAccess);
chat.open_permissions_popup();
let popup = render_bottom_popup(&chat, 120);
@ -10810,6 +10815,7 @@ async fn deltas_then_same_final_message_are_rendered_snapshot() {
msg: EventMsg::AgentMessage(AgentMessageEvent {
message: "Here is the result.".into(),
phase: None,
memory_citation: None,
}),
});