Add Epiphany view lens wrapper

This commit is contained in:
Meta Vens 2026-05-12 03:51:17 +01:00
commit e551bbf6c5
8 changed files with 221 additions and 0 deletions

View file

@ -4749,6 +4749,14 @@
},
"type": "object"
},
"ThreadEpiphanyViewLens": {
"enum": [
"scene",
"jobs",
"pressure"
],
"type": "string"
},
"ThreadForkParams": {
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
"properties": {

View file

@ -17833,6 +17833,14 @@
},
"type": "object"
},
"ThreadEpiphanyViewLens": {
"enum": [
"scene",
"jobs",
"pressure"
],
"type": "string"
},
"ThreadForkParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",

View file

@ -15720,6 +15720,14 @@
},
"type": "object"
},
"ThreadEpiphanyViewLens": {
"enum": [
"scene",
"jobs",
"pressure"
],
"type": "string"
},
"ThreadForkParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",

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 ThreadEpiphanyViewLens = "scene" | "jobs" | "pressure";

View file

@ -387,6 +387,7 @@ export type { ThreadEpiphanyStateUpdatedField } from "./ThreadEpiphanyStateUpdat
export type { ThreadEpiphanyStateUpdatedNotification } from "./ThreadEpiphanyStateUpdatedNotification";
export type { ThreadEpiphanyStateUpdatedSource } from "./ThreadEpiphanyStateUpdatedSource";
export type { ThreadEpiphanyUpdatePatch } from "./ThreadEpiphanyUpdatePatch";
export type { ThreadEpiphanyViewLens } from "./ThreadEpiphanyViewLens";
export type { ThreadForkParams } from "./ThreadForkParams";
export type { ThreadForkResponse } from "./ThreadForkResponse";
export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams";

View file

@ -341,6 +341,11 @@ client_request_definitions! {
params: v2::ThreadEpiphanySceneParams,
response: v2::ThreadEpiphanySceneResponse,
},
#[experimental("thread/epiphany/view")]
ThreadEpiphanyView => "thread/epiphany/view" {
params: v2::ThreadEpiphanyViewParams,
response: v2::ThreadEpiphanyViewResponse,
},
#[experimental("thread/epiphany/jobs")]
ThreadEpiphanyJobs => "thread/epiphany/jobs" {
params: v2::ThreadEpiphanyJobsParams,
@ -1891,6 +1896,68 @@ mod tests {
Ok(())
}
#[test]
fn serialize_thread_epiphany_view_response() -> Result<()> {
let request = ClientRequest::ThreadEpiphanyView {
request_id: RequestId::Integer(8),
params: v2::ThreadEpiphanyViewParams {
thread_id: "thr_123".to_string(),
lenses: vec![v2::ThreadEpiphanyViewLens::Pressure],
},
};
assert_eq!(request.method(), "thread/epiphany/view");
let response = ClientResponse::ThreadEpiphanyView {
request_id: RequestId::Integer(8),
response: v2::ThreadEpiphanyViewResponse {
thread_id: "thr_123".to_string(),
lenses: vec![v2::ThreadEpiphanyViewLens::Pressure],
scene: None,
jobs: Vec::new(),
pressure: Some(v2::ThreadEpiphanyPressure {
status: v2::ThreadEpiphanyPressureStatus::Ready,
level: v2::ThreadEpiphanyPressureLevel::Low,
basis: v2::ThreadEpiphanyPressureBasis::ModelContextWindow,
used_tokens: Some(10),
model_context_window: Some(100),
model_auto_compact_token_limit: Some(100),
remaining_tokens: Some(90),
ratio_per_mille: Some(100),
should_prepare_compaction: false,
note: "Context window usage is below the compaction prep threshold."
.to_string(),
}),
},
};
assert_eq!(response.id(), &RequestId::Integer(8));
assert_eq!(response.method(), "thread/epiphany/view");
assert_eq!(
json!({
"method": "thread/epiphany/view",
"id": 8,
"response": {
"threadId": "thr_123",
"lenses": ["pressure"],
"pressure": {
"status": "ready",
"level": "low",
"basis": "modelContextWindow",
"usedTokens": 10,
"modelContextWindow": 100,
"modelAutoCompactTokenLimit": 100,
"remainingTokens": 90,
"ratioPerMille": 100,
"shouldPrepareCompaction": false,
"note": "Context window usage is below the compaction prep threshold."
}
}
}),
serde_json::to_value(&response)?,
);
Ok(())
}
#[test]
fn serialize_thread_epiphany_jobs_response() -> Result<()> {
let response = ClientResponse::ThreadEpiphanyJobs {

View file

@ -3940,6 +3940,41 @@ pub struct ThreadEpiphanySceneResponse {
pub scene: ThreadEpiphanyScene,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum ThreadEpiphanyViewLens {
Scene,
Jobs,
Pressure,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyViewParams {
pub thread_id: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lenses: Vec<ThreadEpiphanyViewLens>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyViewResponse {
pub thread_id: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lenses: Vec<ThreadEpiphanyViewLens>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub scene: Option<ThreadEpiphanyScene>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub jobs: Vec<ThreadEpiphanyJob>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub pressure: Option<ThreadEpiphanyPressure>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]

View file

@ -266,6 +266,9 @@ use codex_app_server_protocol::ThreadEpiphanyStateUpdatedSource;
use codex_app_server_protocol::ThreadEpiphanyUpdateParams;
use codex_app_server_protocol::ThreadEpiphanyUpdatePatch;
use codex_app_server_protocol::ThreadEpiphanyUpdateResponse;
use codex_app_server_protocol::ThreadEpiphanyViewLens;
use codex_app_server_protocol::ThreadEpiphanyViewParams;
use codex_app_server_protocol::ThreadEpiphanyViewResponse;
use codex_app_server_protocol::ThreadForkParams;
use codex_app_server_protocol::ThreadForkResponse;
use codex_app_server_protocol::ThreadIncrementElicitationParams;
@ -1120,6 +1123,10 @@ impl CodexMessageProcessor {
self.thread_epiphany_scene(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadEpiphanyView { request_id, params } => {
self.thread_epiphany_view(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadEpiphanyJobs { request_id, params } => {
self.thread_epiphany_jobs(to_connection_request_id(request_id), params)
.await;
@ -4290,6 +4297,88 @@ impl CodexMessageProcessor {
self.outgoing.send_response(request_id, response).await;
}
async fn thread_epiphany_view(
&self,
request_id: ConnectionRequestId,
params: ThreadEpiphanyViewParams,
) {
let ThreadEpiphanyViewParams { thread_id, lenses } = params;
let lenses = if lenses.is_empty() {
vec![
ThreadEpiphanyViewLens::Scene,
ThreadEpiphanyViewLens::Jobs,
ThreadEpiphanyViewLens::Pressure,
]
} else {
lenses
};
let thread_uuid = match ThreadId::from_string(&thread_id) {
Ok(id) => id,
Err(err) => {
self.send_invalid_request_error(request_id, format!("invalid thread id: {err}"))
.await;
return;
}
};
let loaded_thread = self.thread_manager.get_thread(thread_uuid).await.ok();
let loaded = loaded_thread.is_some();
let thread = match self.read_thread_view(thread_uuid, false).await {
Ok(thread) => thread,
Err(ThreadReadViewError::InvalidRequest(message)) => {
self.send_invalid_request_error(request_id, message).await;
return;
}
Err(ThreadReadViewError::Internal(message)) => {
self.send_internal_error(request_id, message).await;
return;
}
};
let retrieval_override = if lenses.contains(&ThreadEpiphanyViewLens::Jobs)
&& thread
.epiphany_state
.as_ref()
.and_then(|state| state.retrieval.as_ref())
.is_none()
{
if let Some(loaded_thread) = loaded_thread.as_ref() {
Some(loaded_thread.epiphany_retrieval_state().await)
} else {
None
}
} else {
None
};
let token_usage_info = if lenses.contains(&ThreadEpiphanyViewLens::Pressure) {
if let Some(loaded_thread) = loaded_thread.as_ref() {
loaded_thread.token_usage_info().await
} else {
None
}
} else {
None
};
let response = ThreadEpiphanyViewResponse {
thread_id,
scene: lenses
.contains(&ThreadEpiphanyViewLens::Scene)
.then(|| map_epiphany_scene(thread.epiphany_state.as_ref(), loaded)),
jobs: if lenses.contains(&ThreadEpiphanyViewLens::Jobs) {
map_epiphany_jobs(thread.epiphany_state.as_ref(), retrieval_override.as_ref())
} else {
Vec::new()
},
pressure: lenses
.contains(&ThreadEpiphanyViewLens::Pressure)
.then(|| map_epiphany_pressure(token_usage_info.as_ref())),
lenses,
};
self.outgoing.send_response(request_id, response).await;
}
async fn thread_epiphany_jobs(
&self,
request_id: ConnectionRequestId,