Propagate image generation asset pointers

This commit is contained in:
Jiayu Huang
2026-06-09 17:15:28 -07:00
parent 51fc4b0559
commit 6c30ed63af
41 changed files with 468 additions and 1 deletions
@@ -3811,6 +3811,8 @@ async fn turn_event_counts_completed_tool_items() {
status: "completed".to_string(),
revised_prompt: None,
result: "ok".to_string(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: None,
},
];
@@ -2536,9 +2536,23 @@
},
{
"properties": {
"asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"id": {
"type": "string"
},
"original_asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"result": {
"type": "string"
},
@@ -4207,9 +4207,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -14606,9 +14606,23 @@
},
{
"properties": {
"asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"id": {
"type": "string"
},
"original_asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"result": {
"type": "string"
},
@@ -17044,9 +17058,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -11108,9 +11108,23 @@
},
{
"properties": {
"asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"id": {
"type": "string"
},
"original_asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"result": {
"type": "string"
},
@@ -14861,9 +14875,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1089,9 +1089,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1089,9 +1089,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -755,9 +755,23 @@
},
{
"properties": {
"asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"id": {
"type": "string"
},
"original_asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"result": {
"type": "string"
},
@@ -1233,9 +1233,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1717,9 +1717,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1532,9 +1532,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1532,9 +1532,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1532,9 +1532,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -826,9 +826,23 @@
},
{
"properties": {
"asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"id": {
"type": "string"
},
"original_asset_pointer": {
"type": [
"string",
"null"
],
"writeOnly": true
},
"result": {
"type": "string"
},
@@ -1717,9 +1717,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1532,9 +1532,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1717,9 +1717,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1532,9 +1532,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1532,9 +1532,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1233,9 +1233,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1233,9 +1233,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -1233,9 +1233,21 @@
},
{
"properties": {
"assetPointer": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"originalAssetPointer": {
"type": [
"string",
"null"
]
},
"result": {
"type": "string"
},
@@ -99,4 +99,4 @@ reasoningEffort: ReasoningEffort | null,
/**
* Last known status of the target agents, when available.
*/
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: AbsolutePathBuf, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: AbsolutePathBuf, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, assetPointer: string | null, originalAssetPointer: string | null, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
@@ -594,6 +594,8 @@ impl ThreadHistoryBuilder {
status: String::new(),
revised_prompt: None,
result: String::new(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: None,
};
self.upsert_item_in_current_turn(item);
@@ -605,6 +607,8 @@ impl ThreadHistoryBuilder {
status: payload.status.clone(),
revised_prompt: payload.revised_prompt.clone(),
result: payload.result.clone(),
asset_pointer: payload.asset_pointer.clone(),
original_asset_pointer: payload.original_asset_pointer.clone(),
saved_path: payload.saved_path.clone(),
};
self.upsert_item_in_current_turn(item);
@@ -1577,6 +1581,8 @@ mod tests {
status: "completed".into(),
revised_prompt: Some("final prompt".into()),
result: "Zm9v".into(),
asset_pointer: Some("sediment://asset".into()),
original_asset_pointer: Some("sediment://original".into()),
saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()),
})),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
@@ -1614,6 +1620,8 @@ mod tests {
status: "completed".into(),
revised_prompt: Some("final prompt".into()),
result: "Zm9v".into(),
asset_pointer: Some("sediment://asset".into()),
original_asset_pointer: Some("sediment://original".into()),
saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()),
},
],
@@ -360,6 +360,8 @@ pub enum ThreadItem {
status: String,
revised_prompt: Option<String>,
result: String,
asset_pointer: Option<String>,
original_asset_pointer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
saved_path: Option<AbsolutePathBuf>,
@@ -839,6 +841,8 @@ impl From<CoreTurnItem> for ThreadItem {
status: image.status,
revised_prompt: image.revised_prompt,
result: image.result,
asset_pointer: image.asset_pointer,
original_asset_pointer: image.original_asset_pointer,
saved_path: image.saved_path,
},
CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange {
@@ -121,6 +121,32 @@ fn turn_defaults_legacy_missing_items_view_to_full() {
assert_eq!(turn.items_view, TurnItemsView::Full);
}
#[test]
fn image_generation_item_serializes_provenance_fields() {
let item = ThreadItem::ImageGeneration {
id: "ig_123".to_string(),
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
asset_pointer: Some("sediment://asset".to_string()),
original_asset_pointer: None,
saved_path: None,
};
assert_eq!(
serde_json::to_value(item).expect("serialize image generation item"),
json!({
"type": "imageGeneration",
"id": "ig_123",
"status": "completed",
"revisedPrompt": null,
"result": "Zm9v",
"assetPointer": "sediment://asset",
"originalAssetPointer": null,
})
);
}
#[test]
fn thread_turns_list_params_accepts_items_view() {
let params = serde_json::from_value::<ThreadTurnsListParams>(json!({
+1
View File
@@ -1275,6 +1275,7 @@ Today both notifications carry an empty `items` array even when item events were
- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`.
- `webSearch` — `{id, query, action?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion.
- `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool.
- `imageGeneration` — `{id, status, revisedPrompt, result, assetPointer, originalAssetPointer, savedPath?}` for an image-generation request. The nullable asset pointer fields preserve upstream image provenance when available.
- `enteredReviewMode` — `{id, review}` sent when the reviewer starts; `review` is a short user-facing label such as `"current changes"` or the requested target description.
- `exitedReviewMode` — `{id, review}` emitted when the reviewer finishes; `review` is the full plain-text review (usually, overall notes plus bullet point findings).
- `contextCompaction` — `{id}` emitted when codex compacts the conversation history. This can happen automatically.
@@ -96,6 +96,8 @@ mod tests {
status: "completed".to_string(),
revised_prompt: Some("revised".to_string()),
result: "base64-result".to_string(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
},
]);
@@ -804,6 +804,8 @@ fn append_resume_redaction_history(
status: "completed".to_string(),
revised_prompt: Some("secret revised prompt".to_string()),
result: "base64-image-result".to_string(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: Some(test_absolute_path("/tmp/ig-1.png")),
}),
]
@@ -517,6 +517,8 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
status: "generating".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
asset_pointer: None,
original_asset_pointer: None,
},
ResponseItem::Message {
id: None,
@@ -536,6 +538,8 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
status: "generating".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
asset_pointer: None,
original_asset_pointer: None,
},
ResponseItem::Message {
id: None,
@@ -565,6 +569,8 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
status: "completed".to_string(),
revised_prompt: Some("lobster".to_string()),
result: "Zm9v".to_string(),
asset_pointer: None,
original_asset_pointer: None,
},
]);
@@ -584,6 +590,8 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
status: "completed".to_string(),
revised_prompt: Some("lobster".to_string()),
result: String::new(),
asset_pointer: None,
original_asset_pointer: None,
},
]
);
+4
View File
@@ -196,12 +196,16 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
status,
revised_prompt,
result,
asset_pointer,
original_asset_pointer,
} => Some(TurnItem::ImageGeneration(
codex_protocol::items::ImageGenerationItem {
id: id.clone(),
status: status.clone(),
revised_prompt: revised_prompt.clone(),
result: result.clone(),
asset_pointer: asset_pointer.clone(),
original_asset_pointer: original_asset_pointer.clone(),
saved_path: None,
},
)),
+29
View File
@@ -4,6 +4,7 @@ use crate::context::InternalContextSource;
use crate::context::InternalModelContextFragment;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::HookPromptFragment;
use codex_protocol::items::ImageGenerationItem;
use codex_protocol::items::TurnItem;
use codex_protocol::items::WebSearchItem;
use codex_protocol::items::build_hook_prompt_message;
@@ -145,6 +146,34 @@ fn parses_assistant_message_input_text_for_backward_compatibility() {
}
}
#[test]
fn parses_image_generation_asset_pointers() {
let item = ResponseItem::ImageGenerationCall {
id: "ig_123".to_string(),
status: "completed".to_string(),
revised_prompt: Some("A small blue square".to_string()),
result: "Zm9v".to_string(),
asset_pointer: Some("sediment://asset".to_string()),
original_asset_pointer: Some("sediment://original".to_string()),
};
let Some(TurnItem::ImageGeneration(image)) = parse_turn_item(&item) else {
panic!("expected image generation turn item");
};
assert_eq!(
image,
ImageGenerationItem {
id: "ig_123".to_string(),
status: "completed".to_string(),
revised_prompt: Some("A small blue square".to_string()),
result: "Zm9v".to_string(),
asset_pointer: Some("sediment://asset".to_string()),
original_asset_pointer: Some("sediment://original".to_string()),
saved_path: None,
}
);
}
#[test]
fn skips_unnamed_image_label_text() {
let image_url = "data:image/png;base64,abc".to_string();
+6
View File
@@ -7573,6 +7573,8 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
status: "completed".to_string(),
revised_prompt: Some("a tiny blue square".to_string()),
result: "Zm9v".to_string(),
asset_pointer: None,
original_asset_pointer: None,
}],
/*reference_context_item*/ None,
)
@@ -7825,6 +7827,8 @@ async fn handle_output_item_done_records_image_save_history_message() {
status: "completed".to_string(),
revised_prompt: Some("a tiny blue square".to_string()),
result: "Zm9v".to_string(),
asset_pointer: None,
original_asset_pointer: None,
};
let mut ctx = HandleOutputCtx {
@@ -7880,6 +7884,8 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
status: "completed".to_string(),
revised_prompt: Some("broken payload".to_string()),
result: "_-8".to_string(),
asset_pointer: None,
original_asset_pointer: None,
};
let mut ctx = HandleOutputCtx {
@@ -410,6 +410,8 @@ fn completed_item_defers_mailbox_delivery_for_image_generation_calls() {
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
asset_pointer: None,
original_asset_pointer: None,
};
assert!(completed_item_defers_mailbox_delivery_to_next_turn(
@@ -459,6 +459,8 @@ mod tests {
status: "in_progress".to_string(),
revised_prompt: None,
result: String::new(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: None,
},
))
@@ -470,6 +472,8 @@ mod tests {
status: "completed".to_string(),
revised_prompt: Some("A tiny blue square".to_string()),
result: "cG5n".to_string(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: Some(test_path_buf("/tmp/extension-claimed.png").abs()),
},
))
@@ -532,6 +536,8 @@ mod tests {
status: "in_progress".to_string(),
revised_prompt: None,
result: String::new(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: None,
}
);
@@ -542,6 +548,8 @@ mod tests {
status: "completed".to_string(),
revised_prompt: Some("A tiny blue square".to_string()),
result: "cG5n".to_string(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: Some(expected_path.clone()),
}
);
@@ -103,6 +103,8 @@ fn recent_image_fallback_selects_newest_images_in_chronological_order() {
status: "completed".to_string(),
revised_prompt: None,
result: "generated".to_string(),
asset_pointer: None,
original_asset_pointer: None,
},
ResponseItem::FunctionCallOutput {
call_id: "orphan-call".to_string(),
@@ -105,6 +105,8 @@ impl ToolExecutor<ToolCall> for ImageGenerationTool {
status: "in_progress".to_string(),
revised_prompt: None,
result: String::new(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: None,
}))
.await;
@@ -126,6 +128,8 @@ impl ToolExecutor<ToolCall> for ImageGenerationTool {
status: "completed".to_string(),
revised_prompt: Some(args.prompt),
result: result.clone(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: None,
}))
.await;
+8
View File
@@ -150,6 +150,12 @@ pub struct ImageGenerationItem {
pub result: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub asset_pointer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub original_asset_pointer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub saved_path: Option<AbsolutePathBuf>,
}
@@ -500,6 +506,8 @@ impl ImageGenerationItem {
status: self.status.clone(),
revised_prompt: self.revised_prompt.clone(),
result: self.result.clone(),
asset_pointer: self.asset_pointer.clone(),
original_asset_pointer: self.original_asset_pointer.clone(),
saved_path: self.saved_path.clone(),
})
}
+48
View File
@@ -893,6 +893,12 @@ pub enum ResponseItem {
#[ts(optional)]
revised_prompt: Option<String>,
result: String,
#[serde(default, skip_serializing)]
#[ts(skip)]
asset_pointer: Option<String>,
#[serde(default, skip_serializing)]
#[ts(skip)]
original_asset_pointer: Option<String>,
},
#[serde(alias = "compaction_summary")]
Compaction {
@@ -1776,6 +1782,8 @@ mod tests {
"status": "completed",
"revised_prompt": "A small blue square",
"result": "Zm9v",
"asset_pointer": "sediment://asset",
"original_asset_pointer": "sediment://original",
}))
.expect("image generation item should deserialize");
@@ -1786,8 +1794,22 @@ mod tests {
status: "completed".to_string(),
revised_prompt: Some("A small blue square".to_string()),
result: "Zm9v".to_string(),
asset_pointer: Some("sediment://asset".to_string()),
original_asset_pointer: Some("sediment://original".to_string()),
}
);
let serialized =
serde_json::to_value(&item).expect("image generation item should serialize");
assert_eq!(
serialized,
serde_json::json!({
"type": "image_generation_call",
"id": "ig_123",
"status": "completed",
"revised_prompt": "A small blue square",
"result": "Zm9v",
})
);
}
#[test]
@@ -1807,6 +1829,32 @@ mod tests {
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
asset_pointer: None,
original_asset_pointer: None,
}
);
}
#[test]
fn response_item_parses_image_generation_call_without_original_asset_pointer() {
let item = serde_json::from_value::<ResponseItem>(serde_json::json!({
"id": "ig_123",
"type": "image_generation_call",
"status": "completed",
"result": "Zm9v",
"asset_pointer": "sediment://asset",
}))
.expect("image generation item should deserialize");
assert_eq!(
item,
ResponseItem::ImageGenerationCall {
id: "ig_123".to_string(),
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
asset_pointer: Some("sediment://asset".to_string()),
original_asset_pointer: None,
}
);
}
+15
View File
@@ -2338,6 +2338,12 @@ pub struct ImageGenerationEndEvent {
#[ts(optional)]
pub revised_prompt: Option<String>,
pub result: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub asset_pointer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub original_asset_pointer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub saved_path: Option<AbsolutePathBuf>,
@@ -4715,6 +4721,8 @@ mod tests {
status: "in_progress".into(),
revised_prompt: None,
result: String::new(),
asset_pointer: None,
original_asset_pointer: None,
saved_path: None,
}),
started_at_ms: 0,
@@ -4811,6 +4819,8 @@ mod tests {
status: "completed".into(),
revised_prompt: Some("A tiny blue square".into()),
result: "Zm9v".into(),
asset_pointer: Some("sediment://asset".into()),
original_asset_pointer: Some("sediment://original".into()),
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
}),
completed_at_ms: 0,
@@ -4824,6 +4834,11 @@ mod tests {
assert_eq!(event.status, "completed");
assert_eq!(event.revised_prompt.as_deref(), Some("A tiny blue square"));
assert_eq!(event.result, "Zm9v");
assert_eq!(event.asset_pointer.as_deref(), Some("sediment://asset"));
assert_eq!(
event.original_asset_pointer.as_deref(),
Some("sediment://original")
);
assert_eq!(
event.saved_path.as_ref().map(AbsolutePathBuf::as_path),
Some(test_path_buf("/tmp/ig-1.png").as_path())
@@ -707,6 +707,8 @@ pub(super) fn handle_image_generation_end(
status: "completed".to_string(),
revised_prompt,
result: String::new(),
asset_pointer: None,
original_asset_pointer: None,
saved_path,
},
}),