mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Keep extracting memories out of core and moving the write trigger in the app-server This is temporary and it should move at the client level as a follow-up This makes core fully independant from `codex-memories-write` --------- Co-authored-by: Codex <noreply@openai.com>
444 lines
16 KiB
Rust
444 lines
16 KiB
Rust
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
|
|
use chrono::DateTime;
|
|
use chrono::Utc;
|
|
use codex_git_utils::GitSha;
|
|
use codex_protocol::AgentPath;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
|
use codex_protocol::models::BaseInstructions;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_protocol::protocol::GitInfo;
|
|
use codex_protocol::protocol::RolloutItem;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::SubAgentSource;
|
|
use codex_protocol::protocol::ThreadMemoryMode;
|
|
|
|
use super::proto;
|
|
use crate::GitInfoPatch;
|
|
use crate::OptionalStringPatch;
|
|
use crate::SortDirection;
|
|
use crate::StoredThread;
|
|
use crate::StoredThreadHistory;
|
|
use crate::ThreadEventPersistenceMode;
|
|
use crate::ThreadMetadataPatch;
|
|
use crate::ThreadSortKey;
|
|
use crate::ThreadStoreError;
|
|
use crate::ThreadStoreResult;
|
|
|
|
pub(super) fn remote_status_to_error(status: tonic::Status) -> ThreadStoreError {
|
|
match status.code() {
|
|
tonic::Code::InvalidArgument => ThreadStoreError::InvalidRequest {
|
|
message: status.message().to_string(),
|
|
},
|
|
tonic::Code::AlreadyExists | tonic::Code::FailedPrecondition | tonic::Code::Aborted => {
|
|
ThreadStoreError::Conflict {
|
|
message: status.message().to_string(),
|
|
}
|
|
}
|
|
_ => ThreadStoreError::Internal {
|
|
message: format!("remote thread store request failed: {status}"),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub(super) fn remote_status_to_thread_error(
|
|
status: tonic::Status,
|
|
thread_id: ThreadId,
|
|
) -> ThreadStoreError {
|
|
if status.code() == tonic::Code::NotFound {
|
|
return ThreadStoreError::ThreadNotFound { thread_id };
|
|
}
|
|
remote_status_to_error(status)
|
|
}
|
|
|
|
pub(super) fn proto_thread_id_request(thread_id: ThreadId) -> proto::ThreadIdRequest {
|
|
proto::ThreadIdRequest {
|
|
thread_id: thread_id.to_string(),
|
|
}
|
|
}
|
|
|
|
pub(super) fn proto_sort_key(sort_key: ThreadSortKey) -> proto::ThreadSortKey {
|
|
match sort_key {
|
|
ThreadSortKey::CreatedAt => proto::ThreadSortKey::CreatedAt,
|
|
ThreadSortKey::UpdatedAt => proto::ThreadSortKey::UpdatedAt,
|
|
}
|
|
}
|
|
|
|
pub(super) fn proto_sort_direction(sort_direction: SortDirection) -> proto::SortDirection {
|
|
match sort_direction {
|
|
SortDirection::Asc => proto::SortDirection::Asc,
|
|
SortDirection::Desc => proto::SortDirection::Desc,
|
|
}
|
|
}
|
|
|
|
pub(super) fn proto_event_persistence_mode(
|
|
mode: ThreadEventPersistenceMode,
|
|
) -> proto::ThreadEventPersistenceMode {
|
|
match mode {
|
|
ThreadEventPersistenceMode::Limited => proto::ThreadEventPersistenceMode::Limited,
|
|
ThreadEventPersistenceMode::Extended => proto::ThreadEventPersistenceMode::Extended,
|
|
}
|
|
}
|
|
|
|
pub(super) fn proto_session_source(source: &SessionSource) -> proto::SessionSource {
|
|
match source {
|
|
SessionSource::Cli => proto_source(proto::SessionSourceKind::Cli),
|
|
SessionSource::VSCode => proto_source(proto::SessionSourceKind::Vscode),
|
|
SessionSource::Exec => proto_source(proto::SessionSourceKind::Exec),
|
|
SessionSource::Mcp => proto_source(proto::SessionSourceKind::AppServer),
|
|
SessionSource::Custom(custom) => proto::SessionSource {
|
|
kind: proto::SessionSourceKind::Custom.into(),
|
|
custom: Some(custom.clone()),
|
|
..Default::default()
|
|
},
|
|
SessionSource::SubAgent(SubAgentSource::Review) => {
|
|
proto_source(proto::SessionSourceKind::SubAgentReview)
|
|
}
|
|
SessionSource::SubAgent(SubAgentSource::Compact) => {
|
|
proto_source(proto::SessionSourceKind::SubAgentCompact)
|
|
}
|
|
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
|
parent_thread_id,
|
|
depth,
|
|
agent_path,
|
|
agent_nickname,
|
|
agent_role,
|
|
}) => proto::SessionSource {
|
|
kind: proto::SessionSourceKind::SubAgentThreadSpawn.into(),
|
|
sub_agent_parent_thread_id: Some(parent_thread_id.to_string()),
|
|
sub_agent_depth: Some(*depth),
|
|
sub_agent_path: agent_path.as_ref().map(|path| path.as_str().to_string()),
|
|
sub_agent_nickname: agent_nickname.clone(),
|
|
sub_agent_role: agent_role.clone(),
|
|
..Default::default()
|
|
},
|
|
SessionSource::SubAgent(SubAgentSource::MemoryConsolidation) => {
|
|
proto_source(proto::SessionSourceKind::SubAgentMemoryConsolidation)
|
|
}
|
|
SessionSource::SubAgent(SubAgentSource::Other(other)) => proto::SessionSource {
|
|
kind: proto::SessionSourceKind::SubAgentOther.into(),
|
|
sub_agent_other: Some(other.clone()),
|
|
..Default::default()
|
|
},
|
|
SessionSource::Internal(_) => proto_source(proto::SessionSourceKind::Unknown),
|
|
SessionSource::Unknown => proto_source(proto::SessionSourceKind::Unknown),
|
|
}
|
|
}
|
|
|
|
fn proto_source(kind: proto::SessionSourceKind) -> proto::SessionSource {
|
|
proto::SessionSource {
|
|
kind: kind.into(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub(super) fn serialize_json<T: serde::Serialize>(
|
|
value: &T,
|
|
field_name: &str,
|
|
) -> ThreadStoreResult<String> {
|
|
serde_json::to_string(value).map_err(|err| ThreadStoreError::InvalidRequest {
|
|
message: format!("failed to serialize {field_name} for remote thread store: {err}"),
|
|
})
|
|
}
|
|
|
|
fn deserialize_json<T: serde::de::DeserializeOwned>(
|
|
json: &str,
|
|
field_name: &str,
|
|
) -> ThreadStoreResult<T> {
|
|
serde_json::from_str(json).map_err(|err| ThreadStoreError::InvalidRequest {
|
|
message: format!("remote thread store returned invalid {field_name}: {err}"),
|
|
})
|
|
}
|
|
|
|
pub(super) fn serialize_json_vec<T: serde::Serialize>(
|
|
values: &[T],
|
|
field_name: &str,
|
|
) -> ThreadStoreResult<Vec<String>> {
|
|
values
|
|
.iter()
|
|
.map(|value| serialize_json(value, field_name))
|
|
.collect()
|
|
}
|
|
|
|
fn deserialize_json_vec<T: serde::de::DeserializeOwned>(
|
|
values: &[String],
|
|
field_name: &str,
|
|
) -> ThreadStoreResult<Vec<T>> {
|
|
values
|
|
.iter()
|
|
.map(|value| deserialize_json(value, field_name))
|
|
.collect()
|
|
}
|
|
|
|
pub(super) fn base_instructions_json(
|
|
base_instructions: &BaseInstructions,
|
|
) -> ThreadStoreResult<String> {
|
|
serialize_json(base_instructions, "base_instructions")
|
|
}
|
|
|
|
pub(super) fn dynamic_tools_json(
|
|
dynamic_tools: &[DynamicToolSpec],
|
|
) -> ThreadStoreResult<Vec<String>> {
|
|
serialize_json_vec(dynamic_tools, "dynamic_tool")
|
|
}
|
|
|
|
pub(super) fn rollout_items_json(items: &[RolloutItem]) -> ThreadStoreResult<Vec<String>> {
|
|
serialize_json_vec(items, "rollout_item")
|
|
}
|
|
|
|
pub(super) fn stored_thread_history_from_proto(
|
|
history: proto::StoredThreadHistory,
|
|
) -> ThreadStoreResult<StoredThreadHistory> {
|
|
let thread_id = ThreadId::from_string(&history.thread_id).map_err(|err| {
|
|
ThreadStoreError::InvalidRequest {
|
|
message: format!("remote thread store returned invalid history thread_id: {err}"),
|
|
}
|
|
})?;
|
|
Ok(StoredThreadHistory {
|
|
thread_id,
|
|
items: deserialize_json_vec(&history.items_json, "rollout_item")?,
|
|
})
|
|
}
|
|
|
|
pub(super) fn proto_metadata_patch(patch: ThreadMetadataPatch) -> proto::ThreadMetadataPatch {
|
|
proto::ThreadMetadataPatch {
|
|
name: patch.name,
|
|
memory_mode: patch.memory_mode.map(proto_memory_mode).map(Into::into),
|
|
git_info: patch.git_info.map(proto_git_info_patch),
|
|
}
|
|
}
|
|
|
|
fn proto_memory_mode(memory_mode: ThreadMemoryMode) -> proto::ThreadMemoryMode {
|
|
match memory_mode {
|
|
ThreadMemoryMode::Enabled => proto::ThreadMemoryMode::Enabled,
|
|
ThreadMemoryMode::Disabled => proto::ThreadMemoryMode::Disabled,
|
|
}
|
|
}
|
|
|
|
fn proto_git_info_patch(patch: GitInfoPatch) -> proto::GitInfoPatch {
|
|
proto::GitInfoPatch {
|
|
sha: Some(proto_optional_string_patch(patch.sha)),
|
|
branch: Some(proto_optional_string_patch(patch.branch)),
|
|
origin_url: Some(proto_optional_string_patch(patch.origin_url)),
|
|
}
|
|
}
|
|
|
|
fn proto_optional_string_patch(patch: OptionalStringPatch) -> proto::OptionalStringPatch {
|
|
match patch {
|
|
None => proto::OptionalStringPatch {
|
|
kind: proto::OptionalStringPatchKind::Unset.into(),
|
|
value: None,
|
|
},
|
|
Some(None) => proto::OptionalStringPatch {
|
|
kind: proto::OptionalStringPatchKind::Clear.into(),
|
|
value: None,
|
|
},
|
|
Some(Some(value)) => proto::OptionalStringPatch {
|
|
kind: proto::OptionalStringPatchKind::Set.into(),
|
|
value: Some(value),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub(super) fn stored_thread_from_proto(
|
|
thread: proto::StoredThread,
|
|
) -> ThreadStoreResult<StoredThread> {
|
|
// Keep this mapping boring: the proto mirrors StoredThread for remote-readable
|
|
// summary fields, except for Rust domain types that cross gRPC as stable scalar
|
|
// values. Local-only fields such as rollout_path intentionally stay local.
|
|
let source = thread
|
|
.source
|
|
.as_ref()
|
|
.map(session_source_from_proto)
|
|
.transpose()?
|
|
.unwrap_or(SessionSource::Unknown);
|
|
let thread_id = ThreadId::from_string(&thread.thread_id).map_err(|err| {
|
|
ThreadStoreError::InvalidRequest {
|
|
message: format!("remote thread store returned invalid thread_id: {err}"),
|
|
}
|
|
})?;
|
|
let forked_from_id = thread
|
|
.forked_from_id
|
|
.as_deref()
|
|
.map(ThreadId::from_string)
|
|
.transpose()
|
|
.map_err(|err| ThreadStoreError::InvalidRequest {
|
|
message: format!("remote thread store returned invalid forked_from_id: {err}"),
|
|
})?;
|
|
|
|
Ok(StoredThread {
|
|
thread_id,
|
|
rollout_path: thread.rollout_path.map(PathBuf::from),
|
|
forked_from_id,
|
|
preview: thread.preview,
|
|
name: thread.name,
|
|
model_provider: thread.model_provider,
|
|
model: thread.model,
|
|
reasoning_effort: thread
|
|
.reasoning_effort
|
|
.as_deref()
|
|
.map(parse_reasoning_effort)
|
|
.transpose()?,
|
|
created_at: datetime_from_unix(thread.created_at)?,
|
|
updated_at: datetime_from_unix(thread.updated_at)?,
|
|
archived_at: thread.archived_at.map(datetime_from_unix).transpose()?,
|
|
cwd: PathBuf::from(thread.cwd),
|
|
cli_version: thread.cli_version,
|
|
source,
|
|
agent_nickname: thread.agent_nickname,
|
|
agent_role: thread.agent_role,
|
|
agent_path: thread.agent_path,
|
|
git_info: thread.git_info.map(git_info_from_proto),
|
|
approval_mode: thread
|
|
.approval_mode_json
|
|
.as_deref()
|
|
.map(|json| deserialize_json(json, "approval_mode"))
|
|
.transpose()?
|
|
.unwrap_or(AskForApproval::OnRequest),
|
|
sandbox_policy: thread
|
|
.sandbox_policy_json
|
|
.as_deref()
|
|
.map(|json| deserialize_json(json, "sandbox_policy"))
|
|
.transpose()?
|
|
.unwrap_or_else(SandboxPolicy::new_read_only_policy),
|
|
token_usage: thread
|
|
.token_usage_json
|
|
.as_deref()
|
|
.map(|json| deserialize_json(json, "token_usage"))
|
|
.transpose()?,
|
|
first_user_message: thread.first_user_message,
|
|
history: thread
|
|
.history
|
|
.map(stored_thread_history_from_proto)
|
|
.transpose()?,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(super) fn stored_thread_to_proto(thread: StoredThread) -> proto::StoredThread {
|
|
proto::StoredThread {
|
|
thread_id: thread.thread_id.to_string(),
|
|
forked_from_id: thread.forked_from_id.map(|thread_id| thread_id.to_string()),
|
|
preview: thread.preview,
|
|
name: thread.name,
|
|
model_provider: thread.model_provider,
|
|
model: thread.model,
|
|
created_at: thread.created_at.timestamp(),
|
|
updated_at: thread.updated_at.timestamp(),
|
|
archived_at: thread.archived_at.map(|timestamp| timestamp.timestamp()),
|
|
cwd: thread.cwd.to_string_lossy().into_owned(),
|
|
cli_version: thread.cli_version,
|
|
source: Some(proto_session_source(&thread.source)),
|
|
git_info: thread.git_info.map(git_info_to_proto),
|
|
agent_nickname: thread.agent_nickname,
|
|
agent_role: thread.agent_role,
|
|
agent_path: thread.agent_path,
|
|
reasoning_effort: thread.reasoning_effort.map(|effort| effort.to_string()),
|
|
first_user_message: thread.first_user_message,
|
|
rollout_path: thread
|
|
.rollout_path
|
|
.map(|path| path.to_string_lossy().into_owned()),
|
|
approval_mode_json: Some(serialize_json(&thread.approval_mode, "approval_mode").unwrap()),
|
|
sandbox_policy_json: Some(
|
|
serialize_json(&thread.sandbox_policy, "sandbox_policy").unwrap(),
|
|
),
|
|
token_usage_json: thread
|
|
.token_usage
|
|
.as_ref()
|
|
.map(|usage| serialize_json(usage, "token_usage").unwrap()),
|
|
history: thread.history.map(stored_thread_history_to_proto),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn stored_thread_history_to_proto(history: StoredThreadHistory) -> proto::StoredThreadHistory {
|
|
proto::StoredThreadHistory {
|
|
thread_id: history.thread_id.to_string(),
|
|
items_json: rollout_items_json(&history.items).unwrap(),
|
|
}
|
|
}
|
|
|
|
fn datetime_from_unix(timestamp: i64) -> ThreadStoreResult<DateTime<Utc>> {
|
|
DateTime::from_timestamp(timestamp, 0).ok_or_else(|| ThreadStoreError::InvalidRequest {
|
|
message: format!("remote thread store returned invalid timestamp: {timestamp}"),
|
|
})
|
|
}
|
|
|
|
fn session_source_from_proto(source: &proto::SessionSource) -> ThreadStoreResult<SessionSource> {
|
|
let kind = proto::SessionSourceKind::try_from(source.kind).unwrap_or_default();
|
|
Ok(match kind {
|
|
proto::SessionSourceKind::Unknown => SessionSource::Unknown,
|
|
proto::SessionSourceKind::Cli => SessionSource::Cli,
|
|
proto::SessionSourceKind::Vscode => SessionSource::VSCode,
|
|
proto::SessionSourceKind::Exec => SessionSource::Exec,
|
|
proto::SessionSourceKind::AppServer => SessionSource::Mcp,
|
|
proto::SessionSourceKind::Custom => {
|
|
SessionSource::Custom(source.custom.clone().unwrap_or_default())
|
|
}
|
|
proto::SessionSourceKind::SubAgentReview => SessionSource::SubAgent(SubAgentSource::Review),
|
|
proto::SessionSourceKind::SubAgentCompact => {
|
|
SessionSource::SubAgent(SubAgentSource::Compact)
|
|
}
|
|
proto::SessionSourceKind::SubAgentThreadSpawn => {
|
|
let parent_thread_id = source
|
|
.sub_agent_parent_thread_id
|
|
.as_deref()
|
|
.map(ThreadId::from_string)
|
|
.transpose()
|
|
.map_err(|err| ThreadStoreError::InvalidRequest {
|
|
message: format!(
|
|
"remote thread store returned invalid sub-agent parent thread id: {err}"
|
|
),
|
|
})?
|
|
.ok_or_else(|| ThreadStoreError::InvalidRequest {
|
|
message: "remote thread store omitted sub-agent parent thread id".to_string(),
|
|
})?;
|
|
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
|
parent_thread_id,
|
|
depth: source.sub_agent_depth.unwrap_or_default(),
|
|
agent_path: source
|
|
.sub_agent_path
|
|
.clone()
|
|
.map(AgentPath::from_string)
|
|
.transpose()
|
|
.map_err(|message| ThreadStoreError::InvalidRequest { message })?,
|
|
agent_nickname: source.sub_agent_nickname.clone(),
|
|
agent_role: source.sub_agent_role.clone(),
|
|
})
|
|
}
|
|
proto::SessionSourceKind::SubAgentMemoryConsolidation => {
|
|
SessionSource::SubAgent(SubAgentSource::MemoryConsolidation)
|
|
}
|
|
proto::SessionSourceKind::SubAgentOther => SessionSource::SubAgent(SubAgentSource::Other(
|
|
source.sub_agent_other.clone().unwrap_or_default(),
|
|
)),
|
|
})
|
|
}
|
|
|
|
fn git_info_from_proto(info: proto::GitInfo) -> GitInfo {
|
|
GitInfo {
|
|
commit_hash: info.sha.as_deref().map(GitSha::new),
|
|
branch: info.branch,
|
|
repository_url: info.origin_url,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn git_info_to_proto(info: GitInfo) -> proto::GitInfo {
|
|
proto::GitInfo {
|
|
sha: info.commit_hash.map(|sha| sha.0),
|
|
branch: info.branch,
|
|
origin_url: info.repository_url,
|
|
}
|
|
}
|
|
|
|
fn parse_reasoning_effort(value: &str) -> ThreadStoreResult<ReasoningEffort> {
|
|
ReasoningEffort::from_str(value).map_err(|message| ThreadStoreError::InvalidRequest {
|
|
message: format!("remote thread store returned {message}"),
|
|
})
|
|
}
|