mirror of
https://github.com/openai/codex.git
synced 2026-05-02 18:37:01 +00:00
Make the morpheus agent (which is the phase 2 memories agent) follow the agent-v2 path system by naming it `/morpheus`. To maintain the path primitive this means moving it to a dedicated `AgentControl` Co-authored-by: Codex <noreply@openai.com>
241 lines
6.4 KiB
Rust
241 lines
6.4 KiB
Rust
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::fmt;
|
|
use std::ops::Deref;
|
|
use std::str::FromStr;
|
|
use ts_rs::TS;
|
|
|
|
#[derive(
|
|
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, TS,
|
|
)]
|
|
#[serde(try_from = "String", into = "String")]
|
|
#[schemars(with = "String")]
|
|
#[ts(type = "string")]
|
|
pub struct AgentPath(String);
|
|
|
|
impl AgentPath {
|
|
pub const ROOT: &str = "/root";
|
|
pub const MORPHEUS: &str = "/morpheus";
|
|
const ROOT_SEGMENT: &str = "root";
|
|
|
|
pub fn root() -> Self {
|
|
Self(Self::ROOT.to_string())
|
|
}
|
|
|
|
pub fn morpheus() -> Self {
|
|
Self(Self::MORPHEUS.to_string())
|
|
}
|
|
|
|
pub fn from_string(path: String) -> Result<Self, String> {
|
|
validate_absolute_path(path.as_str())?;
|
|
Ok(Self(path))
|
|
}
|
|
|
|
pub fn as_str(&self) -> &str {
|
|
self.0.as_str()
|
|
}
|
|
|
|
pub fn is_root(&self) -> bool {
|
|
self.as_str() == Self::ROOT
|
|
}
|
|
|
|
pub fn name(&self) -> &str {
|
|
if self.is_root() {
|
|
return Self::ROOT_SEGMENT;
|
|
}
|
|
self.as_str()
|
|
.rsplit('/')
|
|
.next()
|
|
.filter(|segment| !segment.is_empty())
|
|
.unwrap_or(Self::ROOT_SEGMENT)
|
|
}
|
|
|
|
pub fn join(&self, agent_name: &str) -> Result<Self, String> {
|
|
validate_agent_name(agent_name)?;
|
|
Self::from_string(format!("{self}/{agent_name}"))
|
|
}
|
|
|
|
pub fn resolve(&self, reference: &str) -> Result<Self, String> {
|
|
if reference.is_empty() {
|
|
return Err("agent path must not be empty".to_string());
|
|
}
|
|
if reference == Self::ROOT {
|
|
return Ok(Self::root());
|
|
}
|
|
if reference.starts_with('/') {
|
|
return Self::try_from(reference);
|
|
}
|
|
|
|
validate_relative_reference(reference)?;
|
|
Self::from_string(format!("{self}/{reference}"))
|
|
}
|
|
}
|
|
|
|
impl TryFrom<String> for AgentPath {
|
|
type Error = String;
|
|
|
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
Self::from_string(value)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&str> for AgentPath {
|
|
type Error = String;
|
|
|
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
Self::from_string(value.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<AgentPath> for String {
|
|
fn from(value: AgentPath) -> Self {
|
|
value.0
|
|
}
|
|
}
|
|
|
|
impl FromStr for AgentPath {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Self::try_from(s)
|
|
}
|
|
}
|
|
|
|
impl AsRef<str> for AgentPath {
|
|
fn as_ref(&self) -> &str {
|
|
self.as_str()
|
|
}
|
|
}
|
|
|
|
impl Deref for AgentPath {
|
|
type Target = str;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
self.as_str()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for AgentPath {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
fn validate_agent_name(agent_name: &str) -> Result<(), String> {
|
|
if agent_name.is_empty() {
|
|
return Err("agent_name must not be empty".to_string());
|
|
}
|
|
if agent_name == AgentPath::ROOT_SEGMENT {
|
|
return Err("agent_name `root` is reserved".to_string());
|
|
}
|
|
if agent_name == "." || agent_name == ".." {
|
|
return Err(format!("agent_name `{agent_name}` is reserved"));
|
|
}
|
|
if agent_name.contains('/') {
|
|
return Err("agent_name must not contain `/`".to_string());
|
|
}
|
|
if !agent_name
|
|
.chars()
|
|
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
|
|
{
|
|
return Err(
|
|
"agent_name must use only lowercase letters, digits, and underscores".to_string(),
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_absolute_path(path: &str) -> Result<(), String> {
|
|
if path == AgentPath::MORPHEUS {
|
|
return Ok(());
|
|
}
|
|
|
|
let Some(stripped) = path.strip_prefix('/') else {
|
|
return Err("absolute agent paths must start with `/root` or be `/morpheus`".to_string());
|
|
};
|
|
let mut segments = stripped.split('/');
|
|
let Some(root) = segments.next() else {
|
|
return Err("absolute agent path must not be empty".to_string());
|
|
};
|
|
if root != AgentPath::ROOT_SEGMENT {
|
|
return Err("absolute agent paths must start with `/root` or be `/morpheus`".to_string());
|
|
}
|
|
if stripped.ends_with('/') {
|
|
return Err("absolute agent path must not end with `/`".to_string());
|
|
}
|
|
for segment in segments {
|
|
validate_agent_name(segment)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_relative_reference(reference: &str) -> Result<(), String> {
|
|
if reference.ends_with('/') {
|
|
return Err("relative agent path must not end with `/`".to_string());
|
|
}
|
|
for segment in reference.split('/') {
|
|
validate_agent_name(segment)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::AgentPath;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn root_has_expected_name() {
|
|
let root = AgentPath::root();
|
|
assert_eq!(root.as_str(), AgentPath::ROOT);
|
|
assert_eq!(root.name(), "root");
|
|
assert!(root.is_root());
|
|
}
|
|
|
|
#[test]
|
|
fn morpheus_has_expected_name() {
|
|
let morpheus = AgentPath::morpheus();
|
|
assert_eq!(morpheus.as_str(), AgentPath::MORPHEUS);
|
|
assert_eq!(morpheus.name(), "morpheus");
|
|
assert!(!morpheus.is_root());
|
|
}
|
|
|
|
#[test]
|
|
fn join_builds_child_paths() {
|
|
let root = AgentPath::root();
|
|
let child = root.join("researcher").expect("child path");
|
|
assert_eq!(child.as_str(), "/root/researcher");
|
|
assert_eq!(child.name(), "researcher");
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_supports_relative_and_absolute_references() {
|
|
let current = AgentPath::try_from("/root/researcher").expect("path");
|
|
assert_eq!(
|
|
current.resolve("worker").expect("relative path"),
|
|
AgentPath::try_from("/root/researcher/worker").expect("path")
|
|
);
|
|
assert_eq!(
|
|
current.resolve("/root/other").expect("absolute path"),
|
|
AgentPath::try_from("/root/other").expect("path")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_names_and_paths_are_rejected() {
|
|
assert_eq!(
|
|
AgentPath::root().join("BadName"),
|
|
Err("agent_name must use only lowercase letters, digits, and underscores".to_string())
|
|
);
|
|
assert_eq!(
|
|
AgentPath::try_from("/not-root"),
|
|
Err("absolute agent paths must start with `/root` or be `/morpheus`".to_string())
|
|
);
|
|
assert_eq!(
|
|
AgentPath::root().resolve("../sibling"),
|
|
Err("agent_name `..` is reserved".to_string())
|
|
);
|
|
}
|
|
}
|