From 9982b6607af42965ccc340a5bd7b3f2d4c90f692 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Thu, 5 Mar 2026 17:08:59 -0600 Subject: [PATCH 1/4] Agents revisions --- rust/src/client.rs | 145 ++--------- rust/src/types.rs | 122 +-------- rust/tests/agent_integration.rs | 331 ++++++------------------- src/lib/api.ts | 224 +++-------------- src/lib/index.ts | 28 +-- src/lib/test/integration/agent.test.ts | 245 ++++-------------- 6 files changed, 195 insertions(+), 900 deletions(-) diff --git a/rust/src/client.rs b/rust/src/client.rs index d0d0c52..76f456f 100644 --- a/rust/src/client.rs +++ b/rust/src/client.rs @@ -13,7 +13,6 @@ use reqwest::{ }; use serde::{de::DeserializeOwned, Serialize}; use serde_cbor::Value as CborValue; -use serde_json::Value; use std::sync::{Arc, RwLock}; use uuid::Uuid; @@ -1319,13 +1318,9 @@ impl OpenSecretClient { Ok(Box::pin(event_stream)) } - // Agent API Methods - - /// Sends a message to the agent and returns a stream of SSE events. - /// The agent processes the message through a multi-step tool loop and - /// delivers messages via SSE (agent.message, agent.done, agent.error events). - pub async fn agent_chat( + async fn agent_chat_stream( &self, + endpoint: String, input: &str, ) -> Result> + Send>>> { use eventsource_stream::Eventsource; @@ -1341,7 +1336,7 @@ impl OpenSecretClient { ) })?; - let url = format!("{}/v1/agent/chat", self.base_url); + let url = format!("{}{}", self.base_url, endpoint); let json = serde_json::to_string(&request)?; let encrypted = crypto::encrypt_data(&session.session_key, json.as_bytes())?; @@ -1475,135 +1470,41 @@ impl OpenSecretClient { Ok(Box::pin(event_stream)) } - /// Gets the agent configuration for the current user - pub async fn get_agent_config(&self) -> Result { - self.encrypted_api_call("/v1/agent/config", "GET", None::<()>) - .await - } + // Agent API Methods - /// Updates the agent configuration - pub async fn update_agent_config( + /// Sends a message to the main agent and returns a stream of SSE events. + pub async fn agent_chat( &self, - request: UpdateAgentConfigRequest, - ) -> Result { - self.encrypted_api_call("/v1/agent/config", "PUT", Some(request)) - .await - } - - /// Lists all memory blocks for the current user's agent - pub async fn list_memory_blocks(&self) -> Result> { - self.encrypted_api_call("/v1/agent/memory/blocks", "GET", None::<()>) + input: &str, + ) -> Result> + Send>>> { + self.agent_chat_stream("/v1/agent/chat".to_string(), input) .await } - /// Gets a specific memory block by label - pub async fn get_memory_block(&self, label: &str) -> Result { - let encoded = utf8_percent_encode(label, NON_ALPHANUMERIC).to_string(); - self.encrypted_api_call( - &format!("/v1/agent/memory/blocks/{}", encoded), - "GET", - None::<()>, - ) - .await - } - - /// Updates a specific memory block by label - pub async fn update_memory_block( + /// Creates a new subagent for the current user. + pub async fn create_subagent( &self, - label: &str, - request: UpdateMemoryBlockRequest, - ) -> Result { - let encoded = utf8_percent_encode(label, NON_ALPHANUMERIC).to_string(); - self.encrypted_api_call( - &format!("/v1/agent/memory/blocks/{}", encoded), - "PUT", - Some(request), - ) - .await - } - - /// Inserts a new archival memory entry - pub async fn insert_archival_memory( - &self, - text: &str, - metadata: Option, - ) -> Result { - let request = InsertArchivalRequest { - text: text.to_string(), - metadata, - }; - self.encrypted_api_call("/v1/agent/memory/archival", "POST", Some(request)) + request: CreateSubagentRequest, + ) -> Result { + self.encrypted_api_call("/v1/agent/subagents", "POST", Some(request)) .await } - /// Deletes an archival memory entry by UUID - pub async fn delete_archival_memory(&self, id: Uuid) -> Result { - self.encrypted_api_call( - &format!("/v1/agent/memory/archival/{}", id), - "DELETE", - None::<()>, - ) - .await - } - - /// Searches agent memory (archival + recall) - pub async fn search_agent_memory( + /// Sends a message to a specific subagent and returns a stream of SSE events. + pub async fn subagent_chat( &self, - request: MemorySearchRequest, - ) -> Result { - self.encrypted_api_call("/v1/agent/memory/search", "POST", Some(request)) + id: Uuid, + input: &str, + ) -> Result> + Send>>> { + self.agent_chat_stream(format!("/v1/agent/subagents/{}/chat", id), input) .await } - /// Lists agent conversations - pub async fn list_agent_conversations(&self) -> Result { - self.encrypted_api_call("/v1/agent/conversations", "GET", None::<()>) + /// Deletes a subagent by UUID. + pub async fn delete_subagent(&self, id: Uuid) -> Result { + self.encrypted_api_call(&format!("/v1/agent/subagents/{}", id), "DELETE", None::<()>) .await } - - /// Gets items from an agent conversation - pub async fn list_agent_conversation_items( - &self, - conversation_id: &str, - limit: Option, - after: Option<&str>, - order: Option<&str>, - ) -> Result { - let encoded_id = utf8_percent_encode(conversation_id, NON_ALPHANUMERIC).to_string(); - let mut url = format!("/v1/agent/conversations/{}/items", encoded_id); - let mut params = Vec::new(); - if let Some(l) = limit { - params.push(format!("limit={}", l)); - } - if let Some(a) = after { - params.push(format!( - "after={}", - utf8_percent_encode(a, NON_ALPHANUMERIC) - )); - } - if let Some(o) = order { - params.push(format!("order={}", o)); - } - if !params.is_empty() { - url.push('?'); - url.push_str(¶ms.join("&")); - } - self.encrypted_api_call(&url, "GET", None::<()>).await - } - - /// Deletes an agent conversation - pub async fn delete_agent_conversation( - &self, - conversation_id: &str, - ) -> Result { - let encoded_id = utf8_percent_encode(conversation_id, NON_ALPHANUMERIC).to_string(); - self.encrypted_api_call( - &format!("/v1/agent/conversations/{}", encoded_id), - "DELETE", - None::<()>, - ) - .await - } } #[cfg(test)] diff --git a/rust/src/types.rs b/rust/src/types.rs index 4f15e86..cf2e8e8 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -568,91 +568,21 @@ pub struct AgentChatRequest { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentConfigResponse { - pub enabled: bool, - pub model: String, - pub max_context_tokens: i32, - pub compaction_threshold: f32, - #[serde(skip_serializing_if = "Option::is_none")] - pub system_prompt: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub conversation_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateAgentConfigRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub enabled: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_context_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub compaction_threshold: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub system_prompt: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemoryBlockResponse { - pub label: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - pub value: String, - pub char_limit: i32, - pub read_only: bool, - pub version: i32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateMemoryBlockRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub value: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub char_limit: Option, +pub struct CreateSubagentRequest { #[serde(skip_serializing_if = "Option::is_none")] - pub read_only: Option, + pub display_name: Option, + pub purpose: String, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InsertArchivalRequest { - pub text: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InsertArchivalResponse { +pub struct SubagentResponse { pub id: Uuid, - pub source_type: String, - pub embedding_model: String, - pub token_count: i32, - pub created_at: DateTime, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemorySearchRequest { - pub query: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub top_k: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub source_types: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemorySearchResult { - pub content: String, - pub score: f32, - pub token_count: i32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemorySearchResponse { - pub results: Vec, + pub object: String, + pub conversation_id: Uuid, + pub display_name: String, + pub purpose: String, + pub created_by: String, + pub created_at: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -687,35 +617,3 @@ pub enum AgentSseEvent { Done(AgentDoneEvent), Error(AgentErrorEvent), } - -// Agent conversations reuse existing conversation types -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentConversationListResponse { - pub object: String, - pub data: Vec, - pub has_more: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub first_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentConversation { - pub id: String, - pub object: String, - pub created_at: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentConversationItemsResponse { - pub object: String, - pub data: Vec, - pub has_more: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub first_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_id: Option, -} diff --git a/rust/tests/agent_integration.rs b/rust/tests/agent_integration.rs index 3d98099..43527bf 100644 --- a/rust/tests/agent_integration.rs +++ b/rust/tests/agent_integration.rs @@ -1,9 +1,5 @@ use futures::StreamExt; -use opensecret::{ - AgentSseEvent, MemorySearchRequest, OpenSecretClient, Result, UpdateAgentConfigRequest, - UpdateMemoryBlockRequest, -}; -use serde_json::json; +use opensecret::{AgentSseEvent, CreateSubagentRequest, OpenSecretClient, Result}; use std::env; use uuid::Uuid; @@ -41,294 +37,106 @@ async fn setup_authenticated_client() -> Result { Ok(client) } -#[tokio::test] -#[ignore = "Requires agent API on server"] -async fn test_get_agent_config() { - let client = setup_authenticated_client() - .await - .expect("Failed to setup client"); - - let config = client - .get_agent_config() - .await - .expect("Failed to get agent config"); - - assert!(!config.model.is_empty(), "Model should not be empty"); - assert!( - config.max_context_tokens > 0, - "Max context tokens should be positive" - ); - assert!( - config.compaction_threshold > 0.0 && config.compaction_threshold <= 1.0, - "Compaction threshold should be between 0 and 1" - ); - - println!( - "Agent config: model={}, enabled={}", - config.model, config.enabled - ); -} - -#[tokio::test] -#[ignore = "Requires agent API on server"] -async fn test_update_agent_config() { - let client = setup_authenticated_client() - .await - .expect("Failed to setup client"); - - let original = client - .get_agent_config() - .await - .expect("Failed to get original config"); - - let update = UpdateAgentConfigRequest { - enabled: Some(true), - model: None, - max_context_tokens: Some(80_000), - compaction_threshold: None, - system_prompt: Some("You are a test assistant.".to_string()), - }; - - let updated = client - .update_agent_config(update) - .await - .expect("Failed to update agent config"); - - assert!(updated.enabled); - assert_eq!(updated.max_context_tokens, 80_000); - assert_eq!( - updated.system_prompt.as_deref(), - Some("You are a test assistant.") - ); - - // Restore original - let restore = UpdateAgentConfigRequest { - enabled: Some(original.enabled), - model: Some(original.model), - max_context_tokens: Some(original.max_context_tokens), - compaction_threshold: Some(original.compaction_threshold), - system_prompt: original.system_prompt, - }; - client - .update_agent_config(restore) - .await - .expect("Failed to restore config"); -} - -#[tokio::test] -#[ignore = "Requires agent API on server"] -async fn test_list_memory_blocks() { - let client = setup_authenticated_client() - .await - .expect("Failed to setup client"); - - // Trigger agent config init (which creates default blocks) - let _ = client.get_agent_config().await; - - let blocks = client - .list_memory_blocks() - .await - .expect("Failed to list memory blocks"); - - assert!( - blocks.len() >= 2, - "Should have at least persona and human blocks" - ); - - let labels: Vec<&str> = blocks.iter().map(|b| b.label.as_str()).collect(); - assert!(labels.contains(&"persona"), "Should have persona block"); - assert!(labels.contains(&"human"), "Should have human block"); - - for block in &blocks { - assert!(!block.label.is_empty()); - assert!(block.char_limit > 0); - } - - println!("Found {} memory blocks", blocks.len()); -} - -#[tokio::test] -#[ignore = "Requires agent API on server"] -async fn test_get_memory_block() { - let client = setup_authenticated_client() - .await - .expect("Failed to setup client"); - - let _ = client.get_agent_config().await; - - let block = client - .get_memory_block("persona") - .await - .expect("Failed to get persona block"); - - assert_eq!(block.label, "persona"); - assert!( - !block.value.is_empty(), - "Persona block should have a default value" - ); - assert!(block.char_limit > 0); - - println!("Persona block: {}", block.value); -} - -#[tokio::test] -#[ignore = "Requires agent API on server"] -async fn test_update_memory_block() { - let client = setup_authenticated_client() - .await - .expect("Failed to setup client"); - - let _ = client.get_agent_config().await; - - let original = client - .get_memory_block("human") - .await - .expect("Failed to get human block"); - - let update = UpdateMemoryBlockRequest { - description: None, - value: Some("Test user info from Rust SDK integration test.".to_string()), - char_limit: None, - read_only: None, - }; - - let updated = client - .update_memory_block("human", update) - .await - .expect("Failed to update human block"); - - assert_eq!(updated.label, "human"); - assert_eq!( - updated.value, - "Test user info from Rust SDK integration test." - ); +async fn create_test_subagent(client: &OpenSecretClient) -> Result { + let suffix = Uuid::new_v4(); - // Restore original - let restore = UpdateMemoryBlockRequest { - description: None, - value: Some(original.value), - char_limit: None, - read_only: None, - }; client - .update_memory_block("human", restore) + .create_subagent(CreateSubagentRequest { + display_name: Some(format!("Rust SDK Test {}", suffix)), + purpose: format!("Rust SDK integration test subagent {}", suffix), + }) .await - .expect("Failed to restore human block"); } #[tokio::test] #[ignore = "Requires agent API on server"] -async fn test_archival_memory_insert_and_delete() { +async fn test_create_and_delete_subagent() { let client = setup_authenticated_client() .await .expect("Failed to setup client"); - let inserted = client - .insert_archival_memory( - "Rust SDK test: The capital of France is Paris.", - Some(json!({"tags": ["test", "geography"]})), - ) + let subagent = create_test_subagent(&client) .await - .expect("Failed to insert archival memory"); - - assert_eq!(inserted.source_type, "archival"); - assert!(inserted.token_count > 0); + .expect("Failed to create subagent"); - println!( - "Inserted archival memory: id={}, model={}", - inserted.id, inserted.embedding_model - ); + assert_eq!(subagent.object, "agent.subagent"); + assert!(!subagent.display_name.is_empty()); + assert!(!subagent.purpose.is_empty()); let deleted = client - .delete_archival_memory(inserted.id) + .delete_subagent(subagent.id) .await - .expect("Failed to delete archival memory"); + .expect("Failed to delete subagent"); assert!(deleted.deleted); - assert_eq!(deleted.id, inserted.id); + assert_eq!(deleted.id, subagent.id); + assert_eq!(deleted.object, "agent.subagent.deleted"); } #[tokio::test] #[ignore = "Requires agent API on server"] -async fn test_memory_search() { +async fn test_agent_chat_sse() { let client = setup_authenticated_client() .await .expect("Failed to setup client"); - // Insert something searchable first - let inserted = client - .insert_archival_memory("Rust SDK search test: quantum computing uses qubits.", None) + let mut stream = client + .agent_chat("Hello, please respond with just the word 'pong'.") .await - .expect("Failed to insert archival memory"); - - // Search for it - let search = MemorySearchRequest { - query: "quantum computing qubits".to_string(), - top_k: Some(5), - max_tokens: None, - source_types: Some(vec!["archival".to_string()]), - }; + .expect("Failed to start agent chat"); - let results = client - .search_agent_memory(search) - .await - .expect("Failed to search agent memory"); + let mut got_message = false; + let mut got_done = false; + let mut all_messages: Vec = Vec::new(); - println!("Search returned {} results", results.results.len()); + while let Some(result) = stream.next().await { + match result { + Ok(event) => match event { + AgentSseEvent::Message(msg) => { + got_message = true; + all_messages.extend(msg.messages); + println!("Agent message at step {}: {:?}", msg.step, all_messages); + } + AgentSseEvent::Done(done) => { + got_done = true; + println!( + "Agent done: {} steps, {} messages", + done.total_steps, done.total_messages + ); + } + AgentSseEvent::Error(err) => { + panic!("Agent error: {}", err.error); + } + }, + Err(e) => { + panic!("Stream error: {:?}", e); + } + } + } - // Clean up - let _ = client.delete_archival_memory(inserted.id).await; + assert!( + got_message, + "Should have received at least one message event" + ); + assert!(got_done, "Should have received a done event"); + assert!(!all_messages.is_empty(), "Should have at least one message"); } #[tokio::test] #[ignore = "Requires agent API on server"] -async fn test_agent_conversations() { +async fn test_subagent_chat_sse() { let client = setup_authenticated_client() .await .expect("Failed to setup client"); - let conversations = client - .list_agent_conversations() + let subagent = create_test_subagent(&client) .await - .expect("Failed to list agent conversations"); - - assert_eq!(conversations.object, "list"); - - println!("Agent has {} conversations", conversations.data.len()); - - if let Some(conv) = conversations.data.first() { - let items = client - .list_agent_conversation_items(&conv.id, Some(10), None, None) - .await - .expect("Failed to list conversation items"); - - assert_eq!(items.object, "list"); - println!("First conversation has {} items (page)", items.data.len()); - } -} - -#[tokio::test] -#[ignore = "Requires agent API on server"] -async fn test_agent_chat_sse() { - let client = setup_authenticated_client() - .await - .expect("Failed to setup client"); - - // Ensure agent is enabled - let _ = client - .update_agent_config(UpdateAgentConfigRequest { - enabled: Some(true), - model: None, - max_context_tokens: None, - compaction_threshold: None, - system_prompt: None, - }) - .await; + .expect("Failed to create subagent"); let mut stream = client - .agent_chat("Hello, please respond with just the word 'pong'.") + .subagent_chat(subagent.id, "Please reply with the word 'subpong'.") .await - .expect("Failed to start agent chat"); + .expect("Failed to start subagent chat"); let mut got_message = false; let mut got_done = false; @@ -340,17 +148,17 @@ async fn test_agent_chat_sse() { AgentSseEvent::Message(msg) => { got_message = true; all_messages.extend(msg.messages); - println!("Agent message at step {}: {:?}", msg.step, all_messages); + println!("Subagent message at step {}: {:?}", msg.step, all_messages); } AgentSseEvent::Done(done) => { got_done = true; println!( - "Agent done: {} steps, {} messages", + "Subagent done: {} steps, {} messages", done.total_steps, done.total_messages ); } AgentSseEvent::Error(err) => { - panic!("Agent error: {}", err.error); + panic!("Subagent error: {}", err.error); } }, Err(e) => { @@ -359,10 +167,19 @@ async fn test_agent_chat_sse() { } } + let deleted = client + .delete_subagent(subagent.id) + .await + .expect("Failed to delete subagent after chat"); + + assert!(deleted.deleted); assert!( got_message, - "Should have received at least one message event" + "Should have received at least one subagent message" + ); + assert!(got_done, "Should have received a subagent done event"); + assert!( + !all_messages.is_empty(), + "Should have at least one subagent message" ); - assert!(got_done, "Should have received a done event"); - assert!(!all_messages.is_empty(), "Should have at least one message"); } diff --git a/src/lib/api.ts b/src/lib/api.ts index 5788d54..ce7649b 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1571,7 +1571,7 @@ export type ResponsesRetrieveResponse = { }; total_tokens: number; }; - output?: string | any[]; + output?: string | unknown[]; }; export type ThreadListItem = { @@ -1615,15 +1615,15 @@ export type Conversation = { id: string; object: "conversation"; created_at: number; - metadata?: Record; + metadata?: Record; }; export type ConversationCreateRequest = { - metadata?: Record; + metadata?: Record; }; export type ConversationUpdateRequest = { - metadata?: Record; + metadata?: Record; }; export type ConversationItemsResponse = { @@ -1820,7 +1820,7 @@ export type ResponsesCreateRequest = { conversation?: string | { id: string }; previous_response_id?: string; // Deprecated but still supported stream?: boolean; - metadata?: Record; + metadata?: Record; }; /** @@ -1846,7 +1846,9 @@ export type ResponsesCreateRequest = { * * @deprecated Use openai.conversations.create() instead */ -export async function createConversation(metadata?: Record): Promise { +export async function createConversation( + metadata?: Record +): Promise { const requestData: ConversationCreateRequest = metadata ? { metadata } : {}; return authenticatedApiCall( @@ -1902,7 +1904,7 @@ export async function getConversation(conversationId: string): Promise + metadata: Record ): Promise { const requestData: ConversationUpdateRequest = { metadata }; @@ -2182,8 +2184,10 @@ export async function listConversations(params?: { * }); * ``` */ -export async function createResponse(request: ResponsesCreateRequest): Promise { - return authenticatedApiCall( +export async function createResponse( + request: ResponsesCreateRequest +): Promise { + return authenticatedApiCall( `${apiUrl}/v1/responses`, "POST", request, @@ -2473,67 +2477,19 @@ export async function setDefaultInstruction(instructionId: string): Promise; +export type CreateSubagentRequest = { + display_name?: string; + purpose: string; }; -export type InsertArchivalResponse = { +export type SubagentResponse = { id: string; - source_type: string; - embedding_model: string; - token_count: number; - created_at: string; -}; - -export type MemorySearchRequest = { - query: string; - top_k?: number; - max_tokens?: number; - source_types?: string[]; -}; - -export type MemorySearchResult = { - content: string; - score: number; - token_count: number; -}; - -export type MemorySearchResponse = { - results: MemorySearchResult[]; + object: "agent.subagent"; + conversation_id: string; + display_name: string; + purpose: string; + created_by: string; + created_at: number; }; export type AgentDeletedObjectResponse = { @@ -2556,144 +2512,24 @@ export type AgentErrorEvent = { error: string; }; -export type AgentConversationListResponse = { - object: "list"; - data: Conversation[]; - has_more: boolean; - first_id?: string; - last_id?: string; -}; - // ============================================================================ // Agent API Functions // ============================================================================ -export async function getAgentConfig(): Promise { - return authenticatedApiCall( - `${apiUrl}/v1/agent/config`, - "GET", - undefined, - "Failed to get agent config" - ); -} - -export async function updateAgentConfig( - request: UpdateAgentConfigRequest -): Promise { - return authenticatedApiCall( - `${apiUrl}/v1/agent/config`, - "PUT", - request, - "Failed to update agent config" - ); -} - -export async function listMemoryBlocks(): Promise { - return authenticatedApiCall( - `${apiUrl}/v1/agent/memory/blocks`, - "GET", - undefined, - "Failed to list memory blocks" - ); -} - -export async function getMemoryBlock(label: string): Promise { - return authenticatedApiCall( - `${apiUrl}/v1/agent/memory/blocks/${encodeURIComponent(label)}`, - "GET", - undefined, - "Failed to get memory block" - ); -} - -export async function updateMemoryBlock( - label: string, - request: UpdateMemoryBlockRequest -): Promise { - return authenticatedApiCall( - `${apiUrl}/v1/agent/memory/blocks/${encodeURIComponent(label)}`, - "PUT", - request, - "Failed to update memory block" - ); -} - -export async function insertArchivalMemory( - request: InsertArchivalRequest -): Promise { - return authenticatedApiCall( - `${apiUrl}/v1/agent/memory/archival`, +export async function createSubagent(request: CreateSubagentRequest): Promise { + return authenticatedApiCall( + `${apiUrl}/v1/agent/subagents`, "POST", request, - "Failed to insert archival memory" - ); -} - -export async function deleteArchivalMemory(id: string): Promise { - return authenticatedApiCall( - `${apiUrl}/v1/agent/memory/archival/${encodeURIComponent(id)}`, - "DELETE", - undefined, - "Failed to delete archival memory" - ); -} - -export async function searchAgentMemory( - request: MemorySearchRequest -): Promise { - return authenticatedApiCall( - `${apiUrl}/v1/agent/memory/search`, - "POST", - request, - "Failed to search agent memory" - ); -} - -export async function listAgentConversations(): Promise { - return authenticatedApiCall( - `${apiUrl}/v1/agent/conversations`, - "GET", - undefined, - "Failed to list agent conversations" - ); -} - -export async function listAgentConversationItems( - conversationId: string, - params?: { limit?: number; after?: string; order?: string } -): Promise { - let url = `${apiUrl}/v1/agent/conversations/${encodeURIComponent(conversationId)}/items`; - const queryParams: string[] = []; - - if (params?.limit !== undefined) { - queryParams.push(`limit=${params.limit}`); - } - if (params?.after) { - queryParams.push(`after=${encodeURIComponent(params.after)}`); - } - if (params?.order) { - queryParams.push(`order=${encodeURIComponent(params.order)}`); - } - - if (queryParams.length > 0) { - url += `?${queryParams.join("&")}`; - } - - return authenticatedApiCall( - url, - "GET", - undefined, - "Failed to list agent conversation items" + "Failed to create subagent" ); } -export async function deleteAgentConversation( - conversationId: string -): Promise { +export async function deleteSubagent(id: string): Promise { return authenticatedApiCall( - `${apiUrl}/v1/agent/conversations/${encodeURIComponent(conversationId)}`, + `${apiUrl}/v1/agent/subagents/${encodeURIComponent(id)}`, "DELETE", undefined, - "Failed to delete agent conversation" + "Failed to delete subagent" ); } diff --git a/src/lib/index.ts b/src/lib/index.ts index fd3914f..31c3c7c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -29,39 +29,19 @@ export type { BatchDeleteConversationsRequest, BatchDeleteItemResult, BatchDeleteConversationsResponse, - AgentConfigResponse, - UpdateAgentConfigRequest, - MemoryBlockResponse, - UpdateMemoryBlockRequest, - InsertArchivalRequest, - InsertArchivalResponse, - MemorySearchRequest, - MemorySearchResult, - MemorySearchResponse, + CreateSubagentRequest, + SubagentResponse, AgentDeletedObjectResponse, AgentMessageEvent, AgentDoneEvent, - AgentErrorEvent, - AgentConversationListResponse + AgentErrorEvent } from "./api"; // Export API key management functions export { createApiKey, listApiKeys, deleteApiKey } from "./api"; // Export Agent API functions -export { - getAgentConfig, - updateAgentConfig, - listMemoryBlocks, - getMemoryBlock, - updateMemoryBlock, - insertArchivalMemory, - deleteArchivalMemory, - searchAgentMemory, - listAgentConversations, - listAgentConversationItems, - deleteAgentConversation -} from "./api"; +export { createSubagent, deleteSubagent } from "./api"; // Export AI customization options export { createCustomFetch, type CustomFetchOptions } from "./ai"; diff --git a/src/lib/test/integration/agent.test.ts b/src/lib/test/integration/agent.test.ts index 1867bf4..f3106ed 100644 --- a/src/lib/test/integration/agent.test.ts +++ b/src/lib/test/integration/agent.test.ts @@ -1,18 +1,6 @@ import { expect, test } from "bun:test"; -import { - fetchLogin, - fetchSignUp, - getAgentConfig, - updateAgentConfig, - listMemoryBlocks, - getMemoryBlock, - updateMemoryBlock, - insertArchivalMemory, - deleteArchivalMemory, - searchAgentMemory, - listAgentConversations, - listAgentConversationItems -} from "../../api"; +import { createCustomFetch } from "../../ai"; +import { createSubagent, deleteSubagent, fetchLogin, fetchSignUp } from "../../api"; const TEST_EMAIL = process.env.VITE_TEST_EMAIL; const TEST_PASSWORD = process.env.VITE_TEST_PASSWORD; @@ -32,7 +20,7 @@ async function setupTestUser() { ); window.localStorage.setItem("access_token", access_token); window.localStorage.setItem("refresh_token", refresh_token); - } catch (error) { + } catch { console.log("Login failed, attempting signup"); await fetchSignUp(TEST_EMAIL!, TEST_PASSWORD!, "", TEST_CLIENT_ID!, "Test User"); const { access_token, refresh_token } = await fetchLogin( @@ -45,206 +33,81 @@ async function setupTestUser() { } } -test.skip("Get agent config", async () => { - await setupTestUser(); - - const config = await getAgentConfig(); - - expect(config).toBeDefined(); - expect(typeof config.enabled).toBe("boolean"); - expect(config.model).toBeDefined(); - expect(config.model.length).toBeGreaterThan(0); - expect(config.max_context_tokens).toBeGreaterThan(0); - expect(config.compaction_threshold).toBeGreaterThan(0); - expect(config.compaction_threshold).toBeLessThanOrEqual(1); +async function createTestSubagent() { + const suffix = Date.now(); - console.log("Agent config:", JSON.stringify(config)); -}); - -test.skip("Update agent config", async () => { - await setupTestUser(); - - const original = await getAgentConfig(); - - const updated = await updateAgentConfig({ - enabled: true, - max_context_tokens: 80000, - system_prompt: "You are a test assistant from TypeScript SDK." + return createSubagent({ + display_name: `SDK Test ${suffix}`, + purpose: `TypeScript SDK integration test subagent ${suffix}` }); +} - expect(updated.enabled).toBe(true); - expect(updated.max_context_tokens).toBe(80000); - expect(updated.system_prompt).toBe("You are a test assistant from TypeScript SDK."); - - // Restore original - await updateAgentConfig({ - enabled: original.enabled, - model: original.model, - max_context_tokens: original.max_context_tokens, - compaction_threshold: original.compaction_threshold, - system_prompt: original.system_prompt ?? undefined - }); -}); - -test.skip("List memory blocks", async () => { - await setupTestUser(); - - // Trigger config init which creates default blocks - await getAgentConfig(); - - const blocks = await listMemoryBlocks(); - - expect(blocks).toBeDefined(); - expect(Array.isArray(blocks)).toBe(true); - expect(blocks.length).toBeGreaterThanOrEqual(2); - - const labels = blocks.map((b) => b.label); - expect(labels).toContain("persona"); - expect(labels).toContain("human"); - - for (const block of blocks) { - expect(block.label.length).toBeGreaterThan(0); - expect(block.char_limit).toBeGreaterThan(0); - expect(typeof block.read_only).toBe("boolean"); - expect(typeof block.version).toBe("number"); - } +async function readAgentStream(url: string, input: string): Promise { + const customFetch = createCustomFetch(); - console.log(`Found ${blocks.length} memory blocks`); -}); + const response = await customFetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream" + }, + body: JSON.stringify({ input }) + }); -test.skip("Get memory block by label", async () => { - await setupTestUser(); - await getAgentConfig(); + expect(response.ok).toBe(true); - const block = await getMemoryBlock("persona"); + const text = await response.text(); + expect(text.length).toBeGreaterThan(0); - expect(block).toBeDefined(); - expect(block.label).toBe("persona"); - expect(block.value.length).toBeGreaterThan(0); - expect(block.char_limit).toBeGreaterThan(0); + const hasAgentEvents = + text.includes("agent.message") || text.includes("agent.done") || text.includes("messages"); + expect(hasAgentEvents).toBe(true); - console.log("Persona block value:", block.value); -}); + return text; +} -test.skip("Update memory block", async () => { +test.skip("Create and delete subagent", async () => { await setupTestUser(); - await getAgentConfig(); - - const original = await getMemoryBlock("human"); - - const updated = await updateMemoryBlock("human", { - value: "Test user info from TypeScript SDK integration test." - }); - expect(updated.label).toBe("human"); - expect(updated.value).toBe("Test user info from TypeScript SDK integration test."); + const subagent = await createTestSubagent(); - // Restore original - await updateMemoryBlock("human", { value: original.value }); -}); - -test.skip("Insert and delete archival memory", async () => { - await setupTestUser(); - - const inserted = await insertArchivalMemory({ - text: "TypeScript SDK test: The speed of light is approximately 299,792 km/s.", - metadata: { tags: ["test", "physics"] } - }); + expect(subagent.id).toBeDefined(); + expect(subagent.object).toBe("agent.subagent"); + expect(subagent.conversation_id).toBeDefined(); + expect(subagent.display_name.length).toBeGreaterThan(0); + expect(subagent.purpose.length).toBeGreaterThan(0); - expect(inserted).toBeDefined(); - expect(inserted.id).toBeDefined(); - expect(inserted.source_type).toBe("archival"); - expect(inserted.token_count).toBeGreaterThan(0); - expect(inserted.embedding_model).toBeDefined(); - - console.log(`Inserted archival memory: id=${inserted.id}, model=${inserted.embedding_model}`); - - const deleted = await deleteArchivalMemory(inserted.id); + const deleted = await deleteSubagent(subagent.id); expect(deleted.deleted).toBe(true); - expect(deleted.id).toBe(inserted.id); + expect(deleted.id).toBe(subagent.id); + expect(deleted.object).toBe("agent.subagent.deleted"); }); -test.skip("Search agent memory", async () => { +test.skip("Agent chat via SSE (using createCustomFetch)", async () => { await setupTestUser(); - // Insert something searchable - const inserted = await insertArchivalMemory({ - text: "TypeScript SDK search test: machine learning uses neural networks for pattern recognition." - }); - - const results = await searchAgentMemory({ - query: "neural networks machine learning", - top_k: 5, - source_types: ["archival"] - }); + const text = await readAgentStream( + `${API_URL}/v1/agent/chat`, + "Hello, please respond with just the word 'pong'." + ); - expect(results).toBeDefined(); - expect(results.results).toBeDefined(); - expect(Array.isArray(results.results)).toBe(true); - - console.log(`Search returned ${results.results.length} results`); - - // Clean up - await deleteArchivalMemory(inserted.id); -}); + console.log("Agent chat SSE completed, response length:", text.length); +}, 60000); -test.skip("List agent conversations", async () => { +test.skip("Subagent chat via SSE (using createCustomFetch)", async () => { await setupTestUser(); - const conversations = await listAgentConversations(); + const subagent = await createTestSubagent(); - expect(conversations).toBeDefined(); - expect(conversations.object).toBe("list"); - expect(Array.isArray(conversations.data)).toBe(true); - - console.log(`Agent has ${conversations.data.length} conversations`); - - if (conversations.data.length > 0) { - const conv = conversations.data[0]; - expect(conv.id).toBeDefined(); - - const items = await listAgentConversationItems(conv.id, { limit: 10 }); - - expect(items).toBeDefined(); - expect(items.object).toBe("list"); - expect(Array.isArray(items.data)).toBe(true); + try { + const text = await readAgentStream( + `${API_URL}/v1/agent/subagents/${encodeURIComponent(subagent.id)}/chat`, + "Please reply with the word 'subpong'." + ); - console.log(`First conversation has ${items.data.length} items (page)`); + console.log("Subagent chat SSE completed, response length:", text.length); + } finally { + await deleteSubagent(subagent.id); } -}); - -test.skip("Agent chat via SSE (using createCustomFetch)", async () => { - await setupTestUser(); - - // Ensure agent is enabled - await updateAgentConfig({ enabled: true }); - - const { createCustomFetch } = await import("../../ai"); - - const customFetch = createCustomFetch(); - - const response = await customFetch(`${API_URL}/v1/agent/chat`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream" - }, - body: JSON.stringify({ - input: "Hello, please respond with just the word 'pong'." - }) - }); - - expect(response.ok).toBe(true); - - // Read the full response text (createCustomFetch decrypts SSE events inline) - const text = await response.text(); - expect(text.length).toBeGreaterThan(0); - - // The decrypted SSE stream should contain agent event types and JSON data - const hasAgentEvents = - text.includes("agent.message") || text.includes("agent.done") || text.includes("messages"); - expect(hasAgentEvents).toBe(true); - - console.log("Agent chat SSE completed, response length:", text.length); }, 60000); From 778d59206b1ec585181bcf418eccdbbcdad008e5 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Thu, 5 Mar 2026 20:00:57 -0600 Subject: [PATCH 2/4] Agent api pagination and filtering --- rust/src/client.rs | 120 +++++++++++++++++ rust/src/types.rs | 55 ++++++++ rust/tests/agent_integration.rs | 108 +++++++++++++++- src/lib/api.ts | 170 ++++++++++++++++++++++++- src/lib/index.ts | 17 ++- src/lib/test/integration/agent.test.ts | 68 +++++++++- 6 files changed, 534 insertions(+), 4 deletions(-) diff --git a/rust/src/client.rs b/rust/src/client.rs index 76f456f..a65f09c 100644 --- a/rust/src/client.rs +++ b/rust/src/client.rs @@ -24,6 +24,67 @@ pub struct OpenSecretClient { server_public_key: Arc>>>, // Store server's public key from attestation } +fn append_query_param(query: &mut Vec, key: &str, value: impl ToString) { + let encoded = utf8_percent_encode(&value.to_string(), NON_ALPHANUMERIC).to_string(); + query.push(format!("{}={}", key, encoded)); +} + +fn build_agent_items_endpoint(base: &str, params: Option<&AgentItemsListParams>) -> String { + let mut endpoint = base.to_string(); + let mut query = Vec::new(); + + if let Some(params) = params { + if let Some(limit) = params.limit { + append_query_param(&mut query, "limit", limit); + } + if let Some(after) = params.after { + append_query_param(&mut query, "after", after); + } + if let Some(order) = ¶ms.order { + append_query_param(&mut query, "order", order); + } + if let Some(include) = ¶ms.include { + for include_value in include { + append_query_param(&mut query, "include", include_value); + } + } + } + + if !query.is_empty() { + endpoint.push('?'); + endpoint.push_str(&query.join("&")); + } + + endpoint +} + +fn build_subagents_endpoint(params: Option<&ListSubagentsParams>) -> String { + let mut endpoint = "/v1/agent/subagents".to_string(); + let mut query = Vec::new(); + + if let Some(params) = params { + if let Some(limit) = params.limit { + append_query_param(&mut query, "limit", limit); + } + if let Some(after) = params.after { + append_query_param(&mut query, "after", after); + } + if let Some(order) = ¶ms.order { + append_query_param(&mut query, "order", order); + } + if let Some(created_by) = ¶ms.created_by { + append_query_param(&mut query, "created_by", created_by); + } + } + + if !query.is_empty() { + endpoint.push('?'); + endpoint.push_str(&query.join("&")); + } + + endpoint +} + impl OpenSecretClient { pub fn new(base_url: impl Into) -> Result { let base_url = base_url.into(); @@ -1472,6 +1533,27 @@ impl OpenSecretClient { // Agent API Methods + /// Fetches the current user's main agent. + pub async fn get_main_agent(&self) -> Result { + self.encrypted_api_call("/v1/agent", "GET", None::<()>) + .await + } + + /// Lists items in the main agent conversation. + pub async fn list_main_agent_items( + &self, + params: Option, + ) -> Result { + let endpoint = build_agent_items_endpoint("/v1/agent/items", params.as_ref()); + self.encrypted_api_call(&endpoint, "GET", None::<()>).await + } + + /// Fetches a single item from the main agent conversation. + pub async fn get_main_agent_item(&self, item_id: Uuid) -> Result { + self.encrypted_api_call(&format!("/v1/agent/items/{}", item_id), "GET", None::<()>) + .await + } + /// Sends a message to the main agent and returns a stream of SSE events. pub async fn agent_chat( &self, @@ -1490,6 +1572,21 @@ impl OpenSecretClient { .await } + /// Lists subagents for the current user with pagination and filtering. + pub async fn list_subagents( + &self, + params: Option, + ) -> Result { + let endpoint = build_subagents_endpoint(params.as_ref()); + self.encrypted_api_call(&endpoint, "GET", None::<()>).await + } + + /// Fetches a single subagent by UUID. + pub async fn get_subagent(&self, id: Uuid) -> Result { + self.encrypted_api_call(&format!("/v1/agent/subagents/{}", id), "GET", None::<()>) + .await + } + /// Sends a message to a specific subagent and returns a stream of SSE events. pub async fn subagent_chat( &self, @@ -1500,6 +1597,29 @@ impl OpenSecretClient { .await } + /// Lists items in a subagent conversation. + pub async fn list_subagent_items( + &self, + id: Uuid, + params: Option, + ) -> Result { + let endpoint = build_agent_items_endpoint( + &format!("/v1/agent/subagents/{}/items", id), + params.as_ref(), + ); + self.encrypted_api_call(&endpoint, "GET", None::<()>).await + } + + /// Fetches a single item from a subagent conversation. + pub async fn get_subagent_item(&self, id: Uuid, item_id: Uuid) -> Result { + self.encrypted_api_call( + &format!("/v1/agent/subagents/{}/items/{}", id, item_id), + "GET", + None::<()>, + ) + .await + } + /// Deletes a subagent by UUID. pub async fn delete_subagent(&self, id: Uuid) -> Result { self.encrypted_api_call(&format!("/v1/agent/subagents/{}", id), "DELETE", None::<()>) diff --git a/rust/src/types.rs b/rust/src/types.rs index cf2e8e8..b16a3b3 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -574,15 +574,70 @@ pub struct CreateSubagentRequest { pub purpose: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MainAgentResponse { + pub id: Uuid, + pub object: String, + pub kind: String, + pub conversation_id: Uuid, + pub display_name: String, + pub created_at: i64, + pub updated_at: i64, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SubagentResponse { pub id: Uuid, pub object: String, + pub kind: String, pub conversation_id: Uuid, pub display_name: String, pub purpose: String, pub created_by: String, pub created_at: i64, + pub updated_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct AgentItemsListParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub order: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ListSubagentsParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub order: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub created_by: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentItemsListResponse { + pub object: String, + pub data: Vec, + pub first_id: Option, + pub last_id: Option, + pub has_more: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SubagentListResponse { + pub object: String, + pub data: Vec, + pub first_id: Option, + pub last_id: Option, + pub has_more: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/rust/tests/agent_integration.rs b/rust/tests/agent_integration.rs index 43527bf..47d2dd2 100644 --- a/rust/tests/agent_integration.rs +++ b/rust/tests/agent_integration.rs @@ -1,5 +1,8 @@ use futures::StreamExt; -use opensecret::{AgentSseEvent, CreateSubagentRequest, OpenSecretClient, Result}; +use opensecret::{ + AgentItemsListParams, AgentSseEvent, CreateSubagentRequest, ListSubagentsParams, + OpenSecretClient, Result, +}; use std::env; use uuid::Uuid; @@ -73,6 +76,83 @@ async fn test_create_and_delete_subagent() { assert_eq!(deleted.object, "agent.subagent.deleted"); } +#[tokio::test] +#[ignore = "Requires agent API on server"] +async fn test_get_main_agent_and_items() { + let client = setup_authenticated_client() + .await + .expect("Failed to setup client"); + + let main_agent = client + .get_main_agent() + .await + .expect("Failed to get main agent"); + + assert_eq!(main_agent.object, "agent.main"); + assert_eq!(main_agent.kind, "main"); + assert!(!main_agent.display_name.is_empty()); + + let items = client + .list_main_agent_items(Some(AgentItemsListParams { + limit: Some(10), + order: Some("desc".to_string()), + ..Default::default() + })) + .await + .expect("Failed to list main agent items"); + + assert_eq!(items.object, "list"); + + if let Some(first_item) = items.data.first() { + let item_id = first_item + .get("id") + .and_then(|value| value.as_str()) + .expect("Expected item id string"); + + client + .get_main_agent_item(item_id.parse().expect("Invalid item UUID")) + .await + .expect("Failed to get main agent item"); + } +} + +#[tokio::test] +#[ignore = "Requires agent API on server"] +async fn test_list_and_get_subagents() { + let client = setup_authenticated_client() + .await + .expect("Failed to setup client"); + + let subagent = create_test_subagent(&client) + .await + .expect("Failed to create subagent"); + + let list = client + .list_subagents(Some(ListSubagentsParams { + limit: Some(10), + created_by: Some("user".to_string()), + ..Default::default() + })) + .await + .expect("Failed to list subagents"); + + assert_eq!(list.object, "list"); + assert!(list.data.iter().any(|item| item.id == subagent.id)); + + let fetched = client + .get_subagent(subagent.id) + .await + .expect("Failed to get subagent"); + + assert_eq!(fetched.id, subagent.id); + assert_eq!(fetched.kind, "subagent"); + + client + .delete_subagent(subagent.id) + .await + .expect("Failed to delete subagent"); +} + #[tokio::test] #[ignore = "Requires agent API on server"] async fn test_agent_chat_sse() { @@ -167,6 +247,32 @@ async fn test_subagent_chat_sse() { } } + let items = client + .list_subagent_items( + subagent.id, + Some(AgentItemsListParams { + limit: Some(10), + order: Some("desc".to_string()), + ..Default::default() + }), + ) + .await + .expect("Failed to list subagent items"); + + assert_eq!(items.object, "list"); + + if let Some(first_item) = items.data.first() { + let item_id = first_item + .get("id") + .and_then(|value| value.as_str()) + .expect("Expected item id string"); + + client + .get_subagent_item(subagent.id, item_id.parse().expect("Invalid item UUID")) + .await + .expect("Failed to get subagent item"); + } + let deleted = client .delete_subagent(subagent.id) .await diff --git a/src/lib/api.ts b/src/lib/api.ts index ce7649b..85ecdb6 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -2482,14 +2482,50 @@ export type CreateSubagentRequest = { purpose: string; }; +export type AgentCreatedBy = "user" | "agent"; + +export type MainAgentResponse = { + id: string; + object: "agent.main"; + kind: "main"; + conversation_id: string; + display_name: string; + created_at: number; + updated_at: number; +}; + export type SubagentResponse = { id: string; object: "agent.subagent"; + kind: "subagent"; conversation_id: string; display_name: string; purpose: string; - created_by: string; + created_by: AgentCreatedBy; created_at: number; + updated_at: number; +}; + +export type AgentItemsListParams = { + limit?: number; + after?: string; + order?: string; + include?: string[]; +}; + +export type ListSubagentsParams = { + limit?: number; + after?: string; + order?: string; + created_by?: AgentCreatedBy; +}; + +export type SubagentListResponse = { + object: "list"; + data: SubagentResponse[]; + first_id?: string; + last_id?: string; + has_more: boolean; }; export type AgentDeletedObjectResponse = { @@ -2516,6 +2552,95 @@ export type AgentErrorEvent = { // Agent API Functions // ============================================================================ +export async function getMainAgent(): Promise { + return authenticatedApiCall( + `${apiUrl}/v1/agent`, + "GET", + undefined, + "Failed to get main agent" + ); +} + +export async function listMainAgentItems( + params?: AgentItemsListParams +): Promise { + let url = `${apiUrl}/v1/agent/items`; + const queryParams: string[] = []; + + if (params?.limit !== undefined) { + queryParams.push(`limit=${params.limit}`); + } + if (params?.after) { + queryParams.push(`after=${encodeURIComponent(params.after)}`); + } + if (params?.order) { + queryParams.push(`order=${encodeURIComponent(params.order)}`); + } + if (params?.include) { + for (const includeValue of params.include) { + queryParams.push(`include=${encodeURIComponent(includeValue)}`); + } + } + + if (queryParams.length > 0) { + url += `?${queryParams.join("&")}`; + } + + return authenticatedApiCall( + url, + "GET", + undefined, + "Failed to list main agent items" + ); +} + +export async function getMainAgentItem(itemId: string): Promise { + return authenticatedApiCall( + `${apiUrl}/v1/agent/items/${encodeURIComponent(itemId)}`, + "GET", + undefined, + "Failed to get main agent item" + ); +} + +export async function listSubagents(params?: ListSubagentsParams): Promise { + let url = `${apiUrl}/v1/agent/subagents`; + const queryParams: string[] = []; + + if (params?.limit !== undefined) { + queryParams.push(`limit=${params.limit}`); + } + if (params?.after) { + queryParams.push(`after=${encodeURIComponent(params.after)}`); + } + if (params?.order) { + queryParams.push(`order=${encodeURIComponent(params.order)}`); + } + if (params?.created_by) { + queryParams.push(`created_by=${encodeURIComponent(params.created_by)}`); + } + + if (queryParams.length > 0) { + url += `?${queryParams.join("&")}`; + } + + return authenticatedApiCall( + url, + "GET", + undefined, + "Failed to list subagents" + ); +} + +export async function getSubagent(id: string): Promise { + return authenticatedApiCall( + `${apiUrl}/v1/agent/subagents/${encodeURIComponent(id)}`, + "GET", + undefined, + "Failed to get subagent" + ); +} + export async function createSubagent(request: CreateSubagentRequest): Promise { return authenticatedApiCall( `${apiUrl}/v1/agent/subagents`, @@ -2533,3 +2658,46 @@ export async function deleteSubagent(id: string): Promise { + let url = `${apiUrl}/v1/agent/subagents/${encodeURIComponent(id)}/items`; + const queryParams: string[] = []; + + if (params?.limit !== undefined) { + queryParams.push(`limit=${params.limit}`); + } + if (params?.after) { + queryParams.push(`after=${encodeURIComponent(params.after)}`); + } + if (params?.order) { + queryParams.push(`order=${encodeURIComponent(params.order)}`); + } + if (params?.include) { + for (const includeValue of params.include) { + queryParams.push(`include=${encodeURIComponent(includeValue)}`); + } + } + + if (queryParams.length > 0) { + url += `?${queryParams.join("&")}`; + } + + return authenticatedApiCall( + url, + "GET", + undefined, + "Failed to list subagent items" + ); +} + +export async function getSubagentItem(id: string, itemId: string): Promise { + return authenticatedApiCall( + `${apiUrl}/v1/agent/subagents/${encodeURIComponent(id)}/items/${encodeURIComponent(itemId)}`, + "GET", + undefined, + "Failed to get subagent item" + ); +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 31c3c7c..17244c9 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -29,8 +29,13 @@ export type { BatchDeleteConversationsRequest, BatchDeleteItemResult, BatchDeleteConversationsResponse, + AgentCreatedBy, + MainAgentResponse, CreateSubagentRequest, + AgentItemsListParams, + ListSubagentsParams, SubagentResponse, + SubagentListResponse, AgentDeletedObjectResponse, AgentMessageEvent, AgentDoneEvent, @@ -41,7 +46,17 @@ export type { export { createApiKey, listApiKeys, deleteApiKey } from "./api"; // Export Agent API functions -export { createSubagent, deleteSubagent } from "./api"; +export { + getMainAgent, + listMainAgentItems, + getMainAgentItem, + listSubagents, + getSubagent, + createSubagent, + deleteSubagent, + listSubagentItems, + getSubagentItem +} from "./api"; // Export AI customization options export { createCustomFetch, type CustomFetchOptions } from "./ai"; diff --git a/src/lib/test/integration/agent.test.ts b/src/lib/test/integration/agent.test.ts index f3106ed..e8dbdc2 100644 --- a/src/lib/test/integration/agent.test.ts +++ b/src/lib/test/integration/agent.test.ts @@ -1,6 +1,18 @@ import { expect, test } from "bun:test"; import { createCustomFetch } from "../../ai"; -import { createSubagent, deleteSubagent, fetchLogin, fetchSignUp } from "../../api"; +import { + createSubagent, + deleteSubagent, + fetchLogin, + fetchSignUp, + getMainAgent, + getMainAgentItem, + getSubagent, + getSubagentItem, + listMainAgentItems, + listSubagentItems, + listSubagents +} from "../../api"; const TEST_EMAIL = process.env.VITE_TEST_EMAIL; const TEST_PASSWORD = process.env.VITE_TEST_PASSWORD; @@ -84,6 +96,50 @@ test.skip("Create and delete subagent", async () => { expect(deleted.object).toBe("agent.subagent.deleted"); }); +test.skip("Get main agent and list main agent items", async () => { + await setupTestUser(); + + const mainAgent = await getMainAgent(); + + expect(mainAgent.id).toBeDefined(); + expect(mainAgent.object).toBe("agent.main"); + expect(mainAgent.kind).toBe("main"); + expect(mainAgent.conversation_id).toBeDefined(); + expect(mainAgent.display_name.length).toBeGreaterThan(0); + + const items = await listMainAgentItems({ limit: 10, order: "desc" }); + + expect(items.object).toBe("list"); + expect(Array.isArray(items.data)).toBe(true); + + if (items.data.length > 0) { + const item = await getMainAgentItem(items.data[0].id); + expect(item.id).toBe(items.data[0].id); + } +}); + +test.skip("List and get subagents", async () => { + await setupTestUser(); + + const subagent = await createTestSubagent(); + + try { + const list = await listSubagents({ limit: 10, created_by: "user" }); + + expect(list.object).toBe("list"); + expect(Array.isArray(list.data)).toBe(true); + expect(list.data.some((item) => item.id === subagent.id)).toBe(true); + + const fetched = await getSubagent(subagent.id); + + expect(fetched.id).toBe(subagent.id); + expect(fetched.object).toBe("agent.subagent"); + expect(fetched.kind).toBe("subagent"); + } finally { + await deleteSubagent(subagent.id); + } +}); + test.skip("Agent chat via SSE (using createCustomFetch)", async () => { await setupTestUser(); @@ -106,6 +162,16 @@ test.skip("Subagent chat via SSE (using createCustomFetch)", async () => { "Please reply with the word 'subpong'." ); + const items = await listSubagentItems(subagent.id, { limit: 10, order: "desc" }); + + expect(items.object).toBe("list"); + expect(Array.isArray(items.data)).toBe(true); + + if (items.data.length > 0) { + const item = await getSubagentItem(subagent.id, items.data[0].id); + expect(item.id).toBe(items.data[0].id); + } + console.log("Subagent chat SSE completed, response length:", text.length); } finally { await deleteSubagent(subagent.id); From d848a2346d283278f3b653736d595556926783f3 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Thu, 5 Mar 2026 20:38:49 -0600 Subject: [PATCH 3/4] Add typing for agents api --- rust/src/client.rs | 20 ++++++++- rust/src/types.rs | 75 ++++++++++++++++++++++++++++++++- rust/tests/agent_integration.rs | 43 ++++++++++++------- 3 files changed, 119 insertions(+), 19 deletions(-) diff --git a/rust/src/client.rs b/rust/src/client.rs index a65f09c..3ec1faf 100644 --- a/rust/src/client.rs +++ b/rust/src/client.rs @@ -1480,6 +1480,22 @@ impl OpenSecretClient { })), } } + "agent.typing" => { + match serde_json::from_str::( + &json_str, + ) { + Ok(typing) => { + Some(Ok(AgentSseEvent::Typing(typing))) + } + Err(e) => Some(Err(Error::Api { + status: 0, + message: format!( + "Failed to parse agent typing: {}", + e + ), + })), + } + } "agent.done" => { match serde_json::from_str::(&json_str) { @@ -1549,7 +1565,7 @@ impl OpenSecretClient { } /// Fetches a single item from the main agent conversation. - pub async fn get_main_agent_item(&self, item_id: Uuid) -> Result { + pub async fn get_main_agent_item(&self, item_id: Uuid) -> Result { self.encrypted_api_call(&format!("/v1/agent/items/{}", item_id), "GET", None::<()>) .await } @@ -1611,7 +1627,7 @@ impl OpenSecretClient { } /// Fetches a single item from a subagent conversation. - pub async fn get_subagent_item(&self, id: Uuid, item_id: Uuid) -> Result { + pub async fn get_subagent_item(&self, id: Uuid, item_id: Uuid) -> Result { self.encrypted_api_call( &format!("/v1/agent/subagents/{}/items/{}", id, item_id), "GET", diff --git a/rust/src/types.rs b/rust/src/types.rs index b16a3b3..9ad4046 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -622,10 +622,77 @@ pub struct ListSubagentsParams { pub created_by: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum ConversationContent { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "input_text")] + InputText { text: String }, + #[serde(rename = "output_text")] + OutputText { text: String }, + #[serde(rename = "input_image")] + InputImage { image_url: String }, + #[serde(rename = "input_file")] + InputFile { filename: String }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum ReasoningContentItem { + #[serde(rename = "text")] + Text { text: String }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum ConversationItem { + #[serde(rename = "message")] + Message { + id: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + status: Option, + role: String, + content: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + created_at: Option, + }, + #[serde(rename = "function_call")] + FunctionToolCall { + id: Uuid, + call_id: Uuid, + name: String, + arguments: String, + #[serde(skip_serializing_if = "Option::is_none")] + status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + created_at: Option, + }, + #[serde(rename = "function_call_output")] + FunctionToolCallOutput { + id: Uuid, + call_id: Uuid, + output: String, + #[serde(skip_serializing_if = "Option::is_none")] + status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + created_at: Option, + }, + #[serde(rename = "reasoning")] + Reasoning { + id: Uuid, + content: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + created_at: Option, + }, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentItemsListResponse { pub object: String, - pub data: Vec, + pub data: Vec, pub first_id: Option, pub last_id: Option, pub has_more: bool, @@ -654,6 +721,11 @@ pub struct AgentMessageEvent { pub step: usize, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentTypingEvent { + pub step: usize, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentDoneEvent { pub total_steps: usize, @@ -669,6 +741,7 @@ pub struct AgentErrorEvent { #[derive(Debug, Clone)] pub enum AgentSseEvent { Message(AgentMessageEvent), + Typing(AgentTypingEvent), Done(AgentDoneEvent), Error(AgentErrorEvent), } diff --git a/rust/tests/agent_integration.rs b/rust/tests/agent_integration.rs index 47d2dd2..7f30bea 100644 --- a/rust/tests/agent_integration.rs +++ b/rust/tests/agent_integration.rs @@ -1,11 +1,20 @@ use futures::StreamExt; use opensecret::{ - AgentItemsListParams, AgentSseEvent, CreateSubagentRequest, ListSubagentsParams, - OpenSecretClient, Result, + AgentItemsListParams, AgentSseEvent, ConversationItem, CreateSubagentRequest, + ListSubagentsParams, OpenSecretClient, Result, }; use std::env; use uuid::Uuid; +fn conversation_item_id(item: &ConversationItem) -> Uuid { + match item { + ConversationItem::Message { id, .. } + | ConversationItem::FunctionToolCall { id, .. } + | ConversationItem::FunctionToolCallOutput { id, .. } + | ConversationItem::Reasoning { id, .. } => *id, + } +} + async fn setup_authenticated_client() -> Result { let env_path = std::path::Path::new("../.env.local"); if env_path.exists() { @@ -104,15 +113,13 @@ async fn test_get_main_agent_and_items() { assert_eq!(items.object, "list"); if let Some(first_item) = items.data.first() { - let item_id = first_item - .get("id") - .and_then(|value| value.as_str()) - .expect("Expected item id string"); - - client - .get_main_agent_item(item_id.parse().expect("Invalid item UUID")) + let item_id = conversation_item_id(first_item); + let item = client + .get_main_agent_item(item_id) .await .expect("Failed to get main agent item"); + + assert_eq!(conversation_item_id(&item), item_id); } } @@ -177,6 +184,9 @@ async fn test_agent_chat_sse() { all_messages.extend(msg.messages); println!("Agent message at step {}: {:?}", msg.step, all_messages); } + AgentSseEvent::Typing(typing) => { + println!("Agent typing at step {}", typing.step); + } AgentSseEvent::Done(done) => { got_done = true; println!( @@ -230,6 +240,9 @@ async fn test_subagent_chat_sse() { all_messages.extend(msg.messages); println!("Subagent message at step {}: {:?}", msg.step, all_messages); } + AgentSseEvent::Typing(typing) => { + println!("Subagent typing at step {}", typing.step); + } AgentSseEvent::Done(done) => { got_done = true; println!( @@ -262,15 +275,13 @@ async fn test_subagent_chat_sse() { assert_eq!(items.object, "list"); if let Some(first_item) = items.data.first() { - let item_id = first_item - .get("id") - .and_then(|value| value.as_str()) - .expect("Expected item id string"); - - client - .get_subagent_item(subagent.id, item_id.parse().expect("Invalid item UUID")) + let item_id = conversation_item_id(first_item); + let item = client + .get_subagent_item(subagent.id, item_id) .await .expect("Failed to get subagent item"); + + assert_eq!(conversation_item_id(&item), item_id); } let deleted = client From 506c52931ef4f5a595bf9d130461b21d74057c8b Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Fri, 6 Mar 2026 14:54:38 -0600 Subject: [PATCH 4/4] Delete agent api's --- rust/src/client.rs | 6 ++++ rust/tests/agent_integration.rs | 43 ++++++++++++++++++++++++++ src/lib/api.ts | 9 ++++++ src/lib/index.ts | 1 + src/lib/test/integration/agent.test.ts | 20 ++++++++++++ 5 files changed, 79 insertions(+) diff --git a/rust/src/client.rs b/rust/src/client.rs index 3ec1faf..c317f9b 100644 --- a/rust/src/client.rs +++ b/rust/src/client.rs @@ -1555,6 +1555,12 @@ impl OpenSecretClient { .await } + /// Deletes the current user's main agent and resets shared agent state. + pub async fn delete_main_agent(&self) -> Result { + self.encrypted_api_call("/v1/agent", "DELETE", None::<()>) + .await + } + /// Lists items in the main agent conversation. pub async fn list_main_agent_items( &self, diff --git a/rust/tests/agent_integration.rs b/rust/tests/agent_integration.rs index 7f30bea..6721485 100644 --- a/rust/tests/agent_integration.rs +++ b/rust/tests/agent_integration.rs @@ -123,6 +123,49 @@ async fn test_get_main_agent_and_items() { } } +#[tokio::test] +#[ignore = "Requires agent API on server"] +async fn test_delete_main_agent_resets_agent_tree() { + let client = setup_authenticated_client() + .await + .expect("Failed to setup client"); + + let main_agent = client + .get_main_agent() + .await + .expect("Failed to get main agent"); + + let subagent = create_test_subagent(&client) + .await + .expect("Failed to create subagent"); + + let deleted = client + .delete_main_agent() + .await + .expect("Failed to delete main agent"); + + assert!(deleted.deleted); + assert_eq!(deleted.id, main_agent.id); + assert_eq!(deleted.object, "agent.main.deleted"); + + let subagents = client + .list_subagents(Some(ListSubagentsParams { + limit: Some(10), + ..Default::default() + })) + .await + .expect("Failed to list subagents after main deletion"); + + assert!(!subagents.data.iter().any(|item| item.id == subagent.id)); + + let recreated = client + .get_main_agent() + .await + .expect("Failed to recreate main agent"); + + assert_eq!(recreated.object, "agent.main"); +} + #[tokio::test] #[ignore = "Requires agent API on server"] async fn test_list_and_get_subagents() { diff --git a/src/lib/api.ts b/src/lib/api.ts index 85ecdb6..b14a233 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -2561,6 +2561,15 @@ export async function getMainAgent(): Promise { ); } +export async function deleteMainAgent(): Promise { + return authenticatedApiCall( + `${apiUrl}/v1/agent`, + "DELETE", + undefined, + "Failed to delete main agent" + ); +} + export async function listMainAgentItems( params?: AgentItemsListParams ): Promise { diff --git a/src/lib/index.ts b/src/lib/index.ts index 17244c9..9cd1e9f 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -48,6 +48,7 @@ export { createApiKey, listApiKeys, deleteApiKey } from "./api"; // Export Agent API functions export { getMainAgent, + deleteMainAgent, listMainAgentItems, getMainAgentItem, listSubagents, diff --git a/src/lib/test/integration/agent.test.ts b/src/lib/test/integration/agent.test.ts index e8dbdc2..813a14b 100644 --- a/src/lib/test/integration/agent.test.ts +++ b/src/lib/test/integration/agent.test.ts @@ -2,6 +2,7 @@ import { expect, test } from "bun:test"; import { createCustomFetch } from "../../ai"; import { createSubagent, + deleteMainAgent, deleteSubagent, fetchLogin, fetchSignUp, @@ -118,6 +119,25 @@ test.skip("Get main agent and list main agent items", async () => { } }); +test.skip("Delete main agent resets the agent tree", async () => { + await setupTestUser(); + + const mainAgent = await getMainAgent(); + const subagent = await createTestSubagent(); + + const deleted = await deleteMainAgent(); + + expect(deleted.deleted).toBe(true); + expect(deleted.id).toBe(mainAgent.id); + expect(deleted.object).toBe("agent.main.deleted"); + + const subagents = await listSubagents({ limit: 10 }); + expect(subagents.data.some((item) => item.id === subagent.id)).toBe(false); + + const recreatedMainAgent = await getMainAgent(); + expect(recreatedMainAgent.id).toBeDefined(); +}); + test.skip("List and get subagents", async () => { await setupTestUser();