context_management: handle summarization in UI (#2377)

This commit is contained in:
Lily Delalande
2025-04-30 16:55:23 -04:00
committed by GitHub
parent cb6fca2e1d
commit 67aa019489
17 changed files with 1395 additions and 127 deletions

View File

@@ -3,8 +3,16 @@ use goose::agents::extension::ToolInfo;
use goose::agents::ExtensionConfig; use goose::agents::ExtensionConfig;
use goose::config::permission::PermissionLevel; use goose::config::permission::PermissionLevel;
use goose::config::ExtensionEntry; use goose::config::ExtensionEntry;
use goose::message::{
ContextLengthExceeded, FrontendToolRequest, Message, MessageContent, RedactedThinkingContent,
ThinkingContent, ToolConfirmationRequest, ToolRequest, ToolResponse,
};
use goose::permission::permission_confirmation::PrincipalType; use goose::permission::permission_confirmation::PrincipalType;
use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata}; use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata};
use mcp_core::content::{Annotations, Content, EmbeddedResource, ImageContent, TextContent};
use mcp_core::handler::ToolResultSchema;
use mcp_core::resource::ResourceContents;
use mcp_core::role::Role;
use mcp_core::tool::{Tool, ToolAnnotations}; use mcp_core::tool::{Tool, ToolAnnotations};
use utoipa::OpenApi; use utoipa::OpenApi;
@@ -25,6 +33,7 @@ use utoipa::OpenApi;
super::routes::config_management::upsert_permissions, super::routes::config_management::upsert_permissions,
super::routes::agent::get_tools, super::routes::agent::get_tools,
super::routes::reply::confirm_permission, super::routes::reply::confirm_permission,
super::routes::context::manage_context, // Added this path
), ),
components(schemas( components(schemas(
super::routes::config_management::UpsertConfigQuery, super::routes::config_management::UpsertConfigQuery,
@@ -37,6 +46,25 @@ use utoipa::OpenApi;
super::routes::config_management::ToolPermission, super::routes::config_management::ToolPermission,
super::routes::config_management::UpsertPermissionsQuery, super::routes::config_management::UpsertPermissionsQuery,
super::routes::reply::PermissionConfirmationRequest, super::routes::reply::PermissionConfirmationRequest,
super::routes::context::ContextManageRequest,
super::routes::context::ContextManageResponse,
Message,
MessageContent,
Content,
EmbeddedResource,
ImageContent,
Annotations,
TextContent,
ToolResponse,
ToolRequest,
ToolResultSchema,
ToolConfirmationRequest,
ThinkingContent,
RedactedThinkingContent,
FrontendToolRequest,
ResourceContents,
ContextLengthExceeded,
Role,
ProviderMetadata, ProviderMetadata,
ExtensionEntry, ExtensionEntry,
ExtensionConfig, ExtensionConfig,

View File

@@ -9,23 +9,43 @@ use axum::{
use goose::message::Message; use goose::message::Message;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use utoipa::ToSchema;
// Direct message serialization for context mgmt request /// Request payload for context management operations
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ContextManageRequest { pub struct ContextManageRequest {
messages: Vec<Message>, /// Collection of messages to be managed
manage_action: String, pub messages: Vec<Message>,
/// Operation to perform: "truncation" or "summarize"
pub manage_action: String,
} }
// Direct message serialization for context mgmt request /// Response from context management operations
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ContextManageResponse { pub struct ContextManageResponse {
messages: Vec<Message>, /// Processed messages after the operation
token_counts: Vec<usize>, pub messages: Vec<Message>,
/// Token counts for each processed message
pub token_counts: Vec<usize>,
} }
#[utoipa::path(
post,
path = "/context/manage",
request_body = ContextManageRequest,
responses(
(status = 200, description = "Context managed successfully", body = ContextManageResponse),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 412, description = "Precondition failed - Agent not available"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Context Management"
)]
async fn manage_context( async fn manage_context(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
headers: HeaderMap, headers: HeaderMap,
@@ -40,7 +60,8 @@ async fn manage_context(
let mut processed_messages: Vec<Message> = vec![]; let mut processed_messages: Vec<Message> = vec![];
let mut token_counts: Vec<usize> = vec![]; let mut token_counts: Vec<usize> = vec![];
if request.manage_action == "trunction" {
if request.manage_action == "truncation" {
(processed_messages, token_counts) = agent (processed_messages, token_counts) = agent
.truncate_context(&request.messages) .truncate_context(&request.messages)
.await .await

View File

@@ -16,14 +16,17 @@ use mcp_core::role::Role;
use mcp_core::tool::ToolCall; use mcp_core::tool::ToolCall;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use utoipa::ToSchema;
mod tool_result_serde; mod tool_result_serde;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[derive(ToSchema)]
pub struct ToolRequest { pub struct ToolRequest {
pub id: String, pub id: String,
#[serde(with = "tool_result_serde")] #[serde(with = "tool_result_serde")]
#[schema(value_type = Object)]
pub tool_call: ToolResult<ToolCall>, pub tool_call: ToolResult<ToolCall>,
} }
@@ -45,14 +48,17 @@ impl ToolRequest {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[derive(ToSchema)]
pub struct ToolResponse { pub struct ToolResponse {
pub id: String, pub id: String,
#[serde(with = "tool_result_serde")] #[serde(with = "tool_result_serde")]
#[schema(value_type = Object)]
pub tool_result: ToolResult<Vec<Content>>, pub tool_result: ToolResult<Vec<Content>>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[derive(ToSchema)]
pub struct ToolConfirmationRequest { pub struct ToolConfirmationRequest {
pub id: String, pub id: String,
pub tool_name: String, pub tool_name: String,
@@ -60,31 +66,33 @@ pub struct ToolConfirmationRequest {
pub prompt: Option<String>, pub prompt: Option<String>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ThinkingContent { pub struct ThinkingContent {
pub thinking: String, pub thinking: String,
pub signature: String, pub signature: String,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct RedactedThinkingContent { pub struct RedactedThinkingContent {
pub data: String, pub data: String,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[derive(ToSchema)]
pub struct FrontendToolRequest { pub struct FrontendToolRequest {
pub id: String, pub id: String,
#[serde(with = "tool_result_serde")] #[serde(with = "tool_result_serde")]
#[schema(value_type = Object)]
pub tool_call: ToolResult<ToolCall>, pub tool_call: ToolResult<ToolCall>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ContextLengthExceeded { pub struct ContextLengthExceeded {
pub msg: String, pub msg: String,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
/// Content passed inside a message, which can be both simple content and tool content /// Content passed inside a message, which can be both simple content and tool content
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum MessageContent { pub enum MessageContent {
@@ -273,7 +281,7 @@ impl From<PromptMessage> for Message {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
/// A message to or from an LLM /// A message to or from an LLM
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Message { pub struct Message {

View File

@@ -5,8 +5,9 @@ use super::role::Role;
use crate::resource::ResourceContents; use crate::resource::ResourceContents;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Annotations { pub struct Annotations {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -14,6 +15,8 @@ pub struct Annotations {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f32>, pub priority: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = String, format = "date-time", example = "2023-01-01T00:00:00Z")]
// for openapi
pub timestamp: Option<DateTime<Utc>>, pub timestamp: Option<DateTime<Utc>>,
} }
@@ -33,7 +36,7 @@ impl Annotations {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TextContent { pub struct TextContent {
pub text: String, pub text: String,
@@ -41,7 +44,7 @@ pub struct TextContent {
pub annotations: Option<Annotations>, pub annotations: Option<Annotations>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ImageContent { pub struct ImageContent {
pub data: String, pub data: String,
@@ -50,7 +53,7 @@ pub struct ImageContent {
pub annotations: Option<Annotations>, pub annotations: Option<Annotations>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct EmbeddedResource { pub struct EmbeddedResource {
pub resource: ResourceContents, pub resource: ResourceContents,
@@ -67,7 +70,7 @@ impl EmbeddedResource {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum Content { pub enum Content {
Text(TextContent), Text(TextContent),

View File

@@ -1,8 +1,11 @@
use async_trait::async_trait; use async_trait::async_trait;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[allow(unused_imports)] // this is used in schema below
use serde_json::json;
use serde_json::Value; use serde_json::Value;
use thiserror::Error; use thiserror::Error;
use utoipa::ToSchema;
#[non_exhaustive] #[non_exhaustive]
#[derive(Error, Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Error, Debug, Clone, Deserialize, Serialize, PartialEq)]
@@ -19,6 +22,18 @@ pub enum ToolError {
pub type ToolResult<T> = std::result::Result<T, ToolError>; pub type ToolResult<T> = std::result::Result<T, ToolError>;
// Define schema manually without generics issues
#[derive(ToSchema)]
#[schema(example = json!({"success": true, "data": {}}))]
pub struct ToolResultSchema {
#[schema(example = "Operation completed successfully")]
pub message: Option<String>,
#[schema(example = true)]
pub success: bool,
#[schema(value_type = Object)]
pub data: Option<serde_json::Value>,
}
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum ResourceError { pub enum ResourceError {
#[error("Execution failed: {0}")] #[error("Execution failed: {0}")]

View File

@@ -1,10 +1,10 @@
use crate::content::Annotations;
/// Resources that servers provide to clients /// Resources that servers provide to clients
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
use utoipa::ToSchema;
use crate::content::Annotations;
const EPSILON: f32 = 1e-6; // Tolerance for floating point comparison const EPSILON: f32 = 1e-6; // Tolerance for floating point comparison
@@ -28,6 +28,7 @@ pub struct Resource {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase", untagged)] #[serde(rename_all = "camelCase", untagged)]
#[derive(ToSchema)]
pub enum ResourceContents { pub enum ResourceContents {
TextResourceContents { TextResourceContents {
uri: String, uri: String,

View File

@@ -1,7 +1,8 @@
/// Roles to describe the origin/ownership of content /// Roles to describe the origin/ownership of content
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Role { pub enum Role {
User, User,

View File

@@ -10,7 +10,7 @@
"license": { "license": {
"name": "Apache-2.0" "name": "Apache-2.0"
}, },
"version": "1.0.20" "version": "1.0.21"
}, },
"paths": { "paths": {
"/agent/tools": { "/agent/tools": {
@@ -408,10 +408,76 @@
} }
} }
} }
},
"/context/manage": {
"post": {
"tags": [
"Context Management"
],
"operationId": "manage_context",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ContextManageRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Context managed successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ContextManageResponse"
}
}
}
},
"401": {
"description": "Unauthorized - Invalid or missing API key"
},
"412": {
"description": "Precondition failed - Agent not available"
},
"500": {
"description": "Internal server error"
}
},
"security": [
{
"api_key": []
}
]
}
} }
}, },
"components": { "components": {
"schemas": { "schemas": {
"Annotations": {
"type": "object",
"properties": {
"audience": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Role"
},
"nullable": true
},
"priority": {
"type": "number",
"format": "float",
"nullable": true
},
"timestamp": {
"type": "string",
"format": "date-time",
"example": "2023-01-01T00:00:00Z"
}
}
},
"ConfigKey": { "ConfigKey": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -462,6 +528,152 @@
} }
} }
}, },
"Content": {
"oneOf": [
{
"allOf": [
{
"$ref": "#/components/schemas/TextContent"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"text"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/ImageContent"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"image"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/EmbeddedResource"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"resource"
]
}
}
}
]
}
],
"discriminator": {
"propertyName": "type"
}
},
"ContextLengthExceeded": {
"type": "object",
"required": [
"msg"
],
"properties": {
"msg": {
"type": "string"
}
}
},
"ContextManageRequest": {
"type": "object",
"description": "Request payload for context management operations",
"required": [
"messages",
"manageAction"
],
"properties": {
"manageAction": {
"type": "string",
"description": "Operation to perform: \"truncation\" or \"summarize\""
},
"messages": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Message"
},
"description": "Collection of messages to be managed"
}
}
},
"ContextManageResponse": {
"type": "object",
"description": "Response from context management operations",
"required": [
"messages",
"tokenCounts"
],
"properties": {
"messages": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Message"
},
"description": "Processed messages after the operation"
},
"tokenCounts": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0
},
"description": "Token counts for each processed message"
}
}
},
"EmbeddedResource": {
"type": "object",
"required": [
"resource"
],
"properties": {
"annotations": {
"allOf": [
{
"$ref": "#/components/schemas/Annotations"
}
],
"nullable": true
},
"resource": {
"$ref": "#/components/schemas/ResourceContents"
}
}
},
"Envs": { "Envs": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
@@ -704,6 +916,265 @@
} }
} }
}, },
"FrontendToolRequest": {
"type": "object",
"required": [
"id",
"toolCall"
],
"properties": {
"id": {
"type": "string"
},
"toolCall": {
"type": "object"
}
}
},
"ImageContent": {
"type": "object",
"required": [
"data",
"mimeType"
],
"properties": {
"annotations": {
"allOf": [
{
"$ref": "#/components/schemas/Annotations"
}
],
"nullable": true
},
"data": {
"type": "string"
},
"mimeType": {
"type": "string"
}
}
},
"Message": {
"type": "object",
"description": "A message to or from an LLM",
"required": [
"role",
"created",
"content"
],
"properties": {
"content": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MessageContent"
}
},
"created": {
"type": "integer",
"format": "int64"
},
"role": {
"$ref": "#/components/schemas/Role"
}
}
},
"MessageContent": {
"oneOf": [
{
"allOf": [
{
"$ref": "#/components/schemas/TextContent"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"text"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/ImageContent"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"image"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/ToolRequest"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"toolRequest"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/ToolResponse"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"toolResponse"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/ToolConfirmationRequest"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"toolConfirmationRequest"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/FrontendToolRequest"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"frontendToolRequest"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/ThinkingContent"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"thinking"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/RedactedThinkingContent"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"redactedThinking"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/ContextLengthExceeded"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"contextLengthExceeded"
]
}
}
}
]
}
],
"description": "Content passed inside a message, which can be both simple content and tool content",
"discriminator": {
"propertyName": "type"
}
},
"ModelInfo": { "ModelInfo": {
"type": "object", "type": "object",
"description": "Information about a model's capabilities", "description": "Information about a model's capabilities",
@@ -841,6 +1312,100 @@
} }
} }
}, },
"RedactedThinkingContent": {
"type": "object",
"required": [
"data"
],
"properties": {
"data": {
"type": "string"
}
}
},
"ResourceContents": {
"oneOf": [
{
"type": "object",
"required": [
"uri",
"text"
],
"properties": {
"mime_type": {
"type": "string",
"nullable": true
},
"text": {
"type": "string"
},
"uri": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"uri",
"blob"
],
"properties": {
"blob": {
"type": "string"
},
"mime_type": {
"type": "string",
"nullable": true
},
"uri": {
"type": "string"
}
}
}
]
},
"Role": {
"type": "string",
"enum": [
"user",
"assistant"
]
},
"TextContent": {
"type": "object",
"required": [
"text"
],
"properties": {
"annotations": {
"allOf": [
{
"$ref": "#/components/schemas/Annotations"
}
],
"nullable": true
},
"text": {
"type": "string"
}
}
},
"ThinkingContent": {
"type": "object",
"required": [
"thinking",
"signature"
],
"properties": {
"signature": {
"type": "string"
},
"thinking": {
"type": "string"
}
}
},
"Tool": { "Tool": {
"type": "object", "type": "object",
"description": "A tool that can be used by a model.", "description": "A tool that can be used by a model.",
@@ -898,6 +1463,27 @@
} }
} }
}, },
"ToolConfirmationRequest": {
"type": "object",
"required": [
"id",
"toolName",
"arguments"
],
"properties": {
"arguments": {},
"id": {
"type": "string"
},
"prompt": {
"type": "string",
"nullable": true
},
"toolName": {
"type": "string"
}
}
},
"ToolInfo": { "ToolInfo": {
"type": "object", "type": "object",
"description": "Information about the tool used for building prompts", "description": "Information about the tool used for building prompts",
@@ -945,6 +1531,61 @@
} }
} }
}, },
"ToolRequest": {
"type": "object",
"required": [
"id",
"toolCall"
],
"properties": {
"id": {
"type": "string"
},
"toolCall": {
"type": "object"
}
}
},
"ToolResponse": {
"type": "object",
"required": [
"id",
"toolResult"
],
"properties": {
"id": {
"type": "string"
},
"toolResult": {
"type": "object"
}
}
},
"ToolResultSchema": {
"type": "object",
"required": [
"success",
"data"
],
"properties": {
"data": {
"type": "object"
},
"message": {
"type": "string",
"example": "Operation completed successfully",
"nullable": true
},
"success": {
"type": "boolean",
"example": true
}
},
"example": {
"data": {},
"success": true
}
},
"UpsertConfigQuery": { "UpsertConfigQuery": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch';
import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, BackupConfigData, BackupConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ConfirmPermissionData } from './types.gen'; import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, BackupConfigData, BackupConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ConfirmPermissionData, ManageContextData, ManageContextResponse } from './types.gen';
import { client as _heyApiClient } from './client.gen'; import { client as _heyApiClient } from './client.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & { export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
@@ -132,3 +132,14 @@ export const confirmPermission = <ThrowOnError extends boolean = false>(options:
} }
}); });
}; };
export const manageContext = <ThrowOnError extends boolean = false>(options: Options<ManageContextData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).post<ManageContextResponse, unknown, ThrowOnError>({
url: '/context/manage',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};

View File

@@ -1,5 +1,11 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
export type Annotations = {
audience?: Array<Role> | null;
priority?: number | null;
timestamp?: string;
};
export type ConfigKey = { export type ConfigKey = {
default?: string | null; default?: string | null;
name: string; name: string;
@@ -16,6 +22,51 @@ export type ConfigResponse = {
config: {}; config: {};
}; };
export type Content = (TextContent & {
type: 'text';
}) | (ImageContent & {
type: 'image';
}) | (EmbeddedResource & {
type: 'resource';
});
export type ContextLengthExceeded = {
msg: string;
};
/**
* Request payload for context management operations
*/
export type ContextManageRequest = {
/**
* Operation to perform: "truncation" or "summarize"
*/
manageAction: string;
/**
* Collection of messages to be managed
*/
messages: Array<Message>;
};
/**
* Response from context management operations
*/
export type ContextManageResponse = {
/**
* Processed messages after the operation
*/
messages: Array<Message>;
/**
* Token counts for each processed message
*/
tokenCounts: Array<number>;
};
export type EmbeddedResource = {
annotations?: Annotations | null;
resource: ResourceContents;
};
export type Envs = { export type Envs = {
[key: string]: string; [key: string]: string;
}; };
@@ -102,6 +153,51 @@ export type ExtensionResponse = {
extensions: Array<ExtensionEntry>; extensions: Array<ExtensionEntry>;
}; };
export type FrontendToolRequest = {
id: string;
toolCall: {
[key: string]: unknown;
};
};
export type ImageContent = {
annotations?: Annotations | null;
data: string;
mimeType: string;
};
/**
* A message to or from an LLM
*/
export type Message = {
content: Array<MessageContent>;
created: number;
role: Role;
};
/**
* Content passed inside a message, which can be both simple content and tool content
*/
export type MessageContent = (TextContent & {
type: 'text';
}) | (ImageContent & {
type: 'image';
}) | (ToolRequest & {
type: 'toolRequest';
}) | (ToolResponse & {
type: 'toolResponse';
}) | (ToolConfirmationRequest & {
type: 'toolConfirmationRequest';
}) | (FrontendToolRequest & {
type: 'frontendToolRequest';
}) | (ThinkingContent & {
type: 'thinking';
}) | (RedactedThinkingContent & {
type: 'redactedThinking';
}) | (ContextLengthExceeded & {
type: 'contextLengthExceeded';
});
/** /**
* Information about a model's capabilities * Information about a model's capabilities
*/ */
@@ -180,6 +276,32 @@ export type ProvidersResponse = {
providers: Array<ProviderDetails>; providers: Array<ProviderDetails>;
}; };
export type RedactedThinkingContent = {
data: string;
};
export type ResourceContents = {
mime_type?: string | null;
text: string;
uri: string;
} | {
blob: string;
mime_type?: string | null;
uri: string;
};
export type Role = 'user' | 'assistant';
export type TextContent = {
annotations?: Annotations | null;
text: string;
};
export type ThinkingContent = {
signature: string;
thinking: string;
};
/** /**
* A tool that can be used by a model. * A tool that can be used by a model.
*/ */
@@ -249,6 +371,13 @@ export type ToolAnnotations = {
title?: string | null; title?: string | null;
}; };
export type ToolConfirmationRequest = {
arguments: unknown;
id: string;
prompt?: string | null;
toolName: string;
};
/** /**
* Information about the tool used for building prompts * Information about the tool used for building prompts
*/ */
@@ -267,6 +396,28 @@ export type ToolPermission = {
tool_name: string; tool_name: string;
}; };
export type ToolRequest = {
id: string;
toolCall: {
[key: string]: unknown;
};
};
export type ToolResponse = {
id: string;
toolResult: {
[key: string]: unknown;
};
};
export type ToolResultSchema = {
data: {
[key: string]: unknown;
};
message?: string | null;
success: boolean;
};
export type UpsertConfigQuery = { export type UpsertConfigQuery = {
is_secret: boolean; is_secret: boolean;
key: string; key: string;
@@ -593,6 +744,37 @@ export type ConfirmPermissionResponses = {
200: unknown; 200: unknown;
}; };
export type ManageContextData = {
body: ContextManageRequest;
path?: never;
query?: never;
url: '/context/manage';
};
export type ManageContextErrors = {
/**
* Unauthorized - Invalid or missing API key
*/
401: unknown;
/**
* Precondition failed - Agent not available
*/
412: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type ManageContextResponses = {
/**
* Context managed successfully
*/
200: ContextManageResponse;
};
export type ManageContextResponse = ManageContextResponses[keyof ManageContextResponses];
export type ClientOptions = { export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {}); baseUrl: `${string}://${string}` | (string & {});
}; };

View File

@@ -16,11 +16,12 @@ import { createRecipe } from '../recipe';
import { AgentHeader } from './AgentHeader'; import { AgentHeader } from './AgentHeader';
import LayingEggLoader from './LayingEggLoader'; import LayingEggLoader from './LayingEggLoader';
import { fetchSessionDetails } from '../sessions'; import { fetchSessionDetails } from '../sessions';
// import { configureRecipeExtensions } from '../utils/recipeExtensions';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { useMessageStream } from '../hooks/useMessageStream'; import { useMessageStream } from '../hooks/useMessageStream';
import { SessionSummaryModal } from './context_management/SessionSummaryModal'; import { SessionSummaryModal } from './context_management/SessionSummaryModal';
import { Recipe } from '../recipe'; import { Recipe } from '../recipe';
import { ContextManagerProvider, useChatContextManager } from './context_management/ContextManager';
import { ContextLengthExceededHandler } from './context_management/ContextLengthExceededHandler';
import { import {
Message, Message,
createUserMessage, createUserMessage,
@@ -30,7 +31,6 @@ import {
ToolResponseMessageContent, ToolResponseMessageContent,
ToolConfirmationRequestMessageContent, ToolConfirmationRequestMessageContent,
} from '../types/message'; } from '../types/message';
import { manageContext } from './context_management';
export interface ChatType { export interface ChatType {
id: string; id: string;
@@ -63,6 +63,30 @@ export default function ChatView({
setView: (view: View, viewOptions?: ViewOptions) => void; setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void; setIsGoosehintsModalOpen: (isOpen: boolean) => void;
}) { }) {
return (
<ContextManagerProvider>
<ChatContent
chat={chat}
setChat={setChat}
setView={setView}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
/>
</ContextManagerProvider>
);
}
function ChatContent({
chat,
setChat,
setView,
setIsGoosehintsModalOpen,
}: {
chat: ChatType;
setChat: (chat: ChatType) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
}) {
// Disabled askAi calls to save costs // Disabled askAi calls to save costs
// const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({}); // const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
const [hasMessages, setHasMessages] = useState(false); const [hasMessages, setHasMessages] = useState(false);
@@ -72,15 +96,24 @@ export default function ChatView({
const [sessionTokenCount, setSessionTokenCount] = useState<number>(0); const [sessionTokenCount, setSessionTokenCount] = useState<number>(0);
const scrollRef = useRef<ScrollAreaHandle>(null); const scrollRef = useRef<ScrollAreaHandle>(null);
const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false); const {
const [summaryContent, setSummaryContent] = useState(''); summaryContent,
const [summarizedThread, setSummarizedThread] = useState<Message[]>([]); summarizedThread,
isSummaryModalOpen,
resetMessagesWithSummary,
closeSummaryModal,
updateSummary,
hasContextLengthExceededContent,
} = useChatContextManager();
// Add this function to handle opening the summary modal with content useEffect(() => {
const handleViewSummary = (summary: string) => { // Log all messages when the component first mounts
setSummaryContent(summary); console.log('Initial messages when resuming session:', chat.messages);
setIsSummaryModalOpen(true); window.electron.logInfo(
}; 'Initial messages when resuming session: ' + JSON.stringify(chat.messages, null, 2)
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Empty dependency array means this runs once on mount;
// Get recipeConfig directly from appConfig // Get recipeConfig directly from appConfig
const recipeConfig = window.appConfig.get('recipeConfig') as Recipe | null; const recipeConfig = window.appConfig.get('recipeConfig') as Recipe | null;
@@ -103,6 +136,12 @@ export default function ChatView({
onFinish: async (_message, _reason) => { onFinish: async (_message, _reason) => {
window.electron.stopPowerSaveBlocker(); window.electron.stopPowerSaveBlocker();
setTimeout(() => {
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
}, 300);
// Disabled askAi calls to save costs // Disabled askAi calls to save costs
// const messageText = getTextContent(message); // const messageText = getTextContent(message);
// const fetchResponses = await askAi(messageText); // const fetchResponses = await askAi(messageText);
@@ -118,11 +157,6 @@ export default function ChatView({
}); });
} }
}, },
onToolCall: (toolCall: string) => {
// Handle tool calls if needed
console.log('Tool call received:', toolCall);
// Implement tool call handling logic here
},
}); });
// Listen for make-agent-from-chat event // Listen for make-agent-from-chat event
@@ -201,11 +235,27 @@ export default function ChatView({
window.electron.startPowerSaveBlocker(); window.electron.startPowerSaveBlocker();
const customEvent = e as CustomEvent; const customEvent = e as CustomEvent;
const content = customEvent.detail?.value || ''; const content = customEvent.detail?.value || '';
if (content.trim()) { if (content.trim()) {
setLastInteractionTime(Date.now()); setLastInteractionTime(Date.now());
append(createUserMessage(content));
if (scrollRef.current?.scrollToBottom) { if (process.env.ALPHA && summarizedThread.length > 0) {
scrollRef.current.scrollToBottom(); // First reset the messages with the summary
resetMessagesWithSummary(messages, setMessages);
// Then append the new user message
setTimeout(() => {
append(createUserMessage(content));
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
}, 150); // Small delay to ensure state updates properly
} else {
// Normal flow - just append the message
append(createUserMessage(content));
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
} }
} }
}; };
@@ -300,55 +350,12 @@ export default function ChatView({
} }
}; };
// Add this function to ChatView.tsx to detect if a message contains ContextLengthExceededContent
const hasContextLengthExceededContent = (message: Message): boolean => {
return message.content.some((content) => content.type === 'contextLengthExceeded');
};
const handleContextLengthExceeded = async () => {
// If we already have a summary, use that
if (summaryContent) {
return summaryContent;
}
// Otherwise, generate a summary
const response = await manageContext({ messages: messages, manageAction: 'summarize' });
setSummarizedThread(response.messages);
return response.messages[0].text;
};
const SummarizedNotification = ({
onViewSummary,
}: {
onViewSummary: (summaryContent: string) => void;
}) => {
const handleViewSummary = async () => {
// Await the result to get a string
const summary = summaryContent || (await handleContextLengthExceeded());
onViewSummary(summary); // Now always passing a string
};
return (
<div className="flex flex-col items-start mt-1 pl-4">
<span className="text-xs text-gray-400 italic">Session summarized</span>
<button
onClick={handleViewSummary}
className="text-xs text-textStandard cursor-pointer hover:text-textSubtle transition-colors mt-1"
>
View or edit summary
</button>
</div>
);
};
// Filter out standalone tool response messages for rendering // Filter out standalone tool response messages for rendering
// They will be shown as part of the tool invocation in the assistant message // They will be shown as part of the tool invocation in the assistant message
const filteredMessages = messages.filter((message) => { const filteredMessages = messages.filter((message) => {
// TODO: use this summarized thread in the chat window // Only filter out when display is explicitly false
if (summarizedThread.length > 0) { if (message.display === false) return false;
// we have a summarized thread
console.log('summarized thread has been created --', summarizedThread);
}
// Keep all assistant messages and user messages that aren't just tool responses // Keep all assistant messages and user messages that aren't just tool responses
if (message.role === 'assistant') return true; if (message.role === 'assistant') return true;
@@ -440,8 +447,10 @@ export default function ChatView({
<> <>
{/* Only render GooseMessage if it's not a CLE message (and we are not in alpha mode) */} {/* Only render GooseMessage if it's not a CLE message (and we are not in alpha mode) */}
{process.env.ALPHA && hasContextLengthExceededContent(message) ? ( {process.env.ALPHA && hasContextLengthExceededContent(message) ? (
// Render the summarized notification for CLE messages only in alpha mode <ContextLengthExceededHandler
<SummarizedNotification onViewSummary={handleViewSummary} /> messages={messages}
messageId={message.id ?? message.created.toString()}
/>
) : ( ) : (
<GooseMessage <GooseMessage
messageHistoryIndex={chat?.messageHistoryIndex} messageHistoryIndex={chat?.messageHistoryIndex}
@@ -502,11 +511,10 @@ export default function ChatView({
{process.env.ALPHA && ( {process.env.ALPHA && (
<SessionSummaryModal <SessionSummaryModal
isOpen={isSummaryModalOpen} isOpen={isSummaryModalOpen}
onClose={() => setIsSummaryModalOpen(false)} onClose={closeSummaryModal}
onSave={(editedContent) => { onSave={(editedContent) => {
console.log('Saving summary...'); updateSummary(editedContent);
setSummaryContent(editedContent); closeSummaryModal();
setIsSummaryModalOpen(false);
}} }}
summaryContent={summaryContent} summaryContent={summaryContent}
/> />

View File

@@ -0,0 +1,77 @@
import React, { useState, useRef, useEffect } from 'react';
import { Message } from '../../types/message';
import { useChatContextManager } from './ContextManager';
interface ContextLengthExceededHandlerProps {
messages: Message[];
messageId: string;
}
export const ContextLengthExceededHandler: React.FC<ContextLengthExceededHandlerProps> = ({
messages,
messageId,
}) => {
const { fetchSummary, summaryContent, isLoadingSummary, errorLoadingSummary, openSummaryModal } =
useChatContextManager();
const [hasFetchStarted, setHasFetchStarted] = useState(false);
// Find the relevant message to check if it's the latest
const isCurrentMessageLatest =
messageId === messages[messages.length - 1].id ||
messageId === messages[messages.length - 1].created.toString();
// Only allow interaction for the most recent context length exceeded event
const shouldAllowSummaryInteraction = isCurrentMessageLatest;
// Use a ref to track if we've started the fetch
const fetchStartedRef = useRef(false);
useEffect(() => {
// Automatically fetch summary if conditions are met
if (
!summaryContent &&
!hasFetchStarted &&
shouldAllowSummaryInteraction &&
!fetchStartedRef.current
) {
setHasFetchStarted(true);
fetchStartedRef.current = true;
fetchSummary(messages);
}
}, [fetchSummary, hasFetchStarted, messages, shouldAllowSummaryInteraction, summaryContent]);
// Handle retry
const handleRetry = () => {
if (!shouldAllowSummaryInteraction) return;
fetchSummary(messages);
};
// Render the notification UI
return (
<div className="flex flex-col items-start mt-1 pl-4">
{isLoadingSummary && shouldAllowSummaryInteraction ? (
// Only show loading indicator during loading state
<div className="flex items-center text-xs text-gray-400">
<span className="mr-2">Preparing summary...</span>
<span className="animate-spin h-3 w-3 border-2 border-gray-400 rounded-full border-t-transparent"></span>
</div>
) : (
// Show different UI based on whether it's already handled
<>
<span className="text-xs text-gray-400 italic">{'Session summarized'}</span>
{/* Only show the button if its last message */}
{shouldAllowSummaryInteraction && (
<button
onClick={() => (errorLoadingSummary ? handleRetry() : openSummaryModal())}
className="text-xs text-textStandard hover:text-textSubtle transition-colors mt-1 flex items-center"
>
{errorLoadingSummary ? 'Retry loading summary' : 'View or edit summary'}
</button>
)}
</>
)}
</div>
);
};

View File

@@ -0,0 +1,170 @@
import React, { createContext, useContext, useState } from 'react';
import { Message } from '../../types/message';
import { manageContextFromBackend, convertApiMessageToFrontendMessage } from './index';
// Define the context management interface
interface ContextManagerState {
summaryContent: string;
summarizedThread: Message[];
isSummaryModalOpen: boolean;
isLoadingSummary: boolean;
errorLoadingSummary: boolean;
}
interface ContextManagerActions {
fetchSummary: (messages: Message[]) => Promise<void>;
updateSummary: (newSummaryContent: string) => void;
resetMessagesWithSummary: (
messages: Message[],
setMessages: (messages: Message[]) => void
) => void;
openSummaryModal: () => void;
closeSummaryModal: () => void;
hasContextLengthExceededContent: (message: Message) => boolean;
}
// Create the context
const ContextManagerContext = createContext<
(ContextManagerState & ContextManagerActions) | undefined
>(undefined);
// Create the provider component
export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [summaryContent, setSummaryContent] = useState<string>('');
const [summarizedThread, setSummarizedThread] = useState<Message[]>([]);
const [isSummaryModalOpen, setIsSummaryModalOpen] = useState<boolean>(false);
const [isLoadingSummary, setIsLoadingSummary] = useState<boolean>(false);
const [errorLoadingSummary, setErrorLoadingSummary] = useState<boolean>(false);
const fetchSummary = async (messages: Message[]) => {
setIsLoadingSummary(true);
setErrorLoadingSummary(false);
try {
const response = await manageContextFromBackend({
messages: messages,
manageAction: 'summarize',
});
// Convert API messages to frontend messages
const convertedMessages = response.messages.map((apiMessage) =>
convertApiMessageToFrontendMessage(apiMessage)
);
// Extract the summary text from the first message
const summaryMessage = convertedMessages[0].content[0];
if (summaryMessage.type === 'text') {
const summary = summaryMessage.text;
setSummaryContent(summary);
setSummarizedThread(convertedMessages);
}
setIsLoadingSummary(false);
} catch (err) {
console.error('Error fetching summary:', err);
setErrorLoadingSummary(true);
setIsLoadingSummary(false);
}
};
const updateSummary = (newSummaryContent: string) => {
// Update the summary content
setSummaryContent(newSummaryContent);
// Update the thread if it exists
if (summarizedThread.length > 0) {
// Create a deep copy of the thread
const updatedThread = [...summarizedThread];
// Create a copy of the first message
const firstMessage = { ...updatedThread[0] };
// Create a copy of the content array
const updatedContent = [...firstMessage.content];
// Update the summary text in the first content item
if (updatedContent[0] && updatedContent[0].type === 'text') {
updatedContent[0] = {
...updatedContent[0],
text: newSummaryContent,
};
}
// Update the message with the new content
firstMessage.content = updatedContent;
updatedThread[0] = firstMessage;
// Update the thread
setSummarizedThread(updatedThread);
}
};
const resetMessagesWithSummary = (
messages: Message[],
setMessages: (messages: Message[]) => void
) => {
// Update summarizedThread with metadata
const updatedSummarizedThread = summarizedThread.map((msg) => ({
...msg,
display: false,
sendToLLM: true,
}));
// Update list of messages with other metadata
const updatedMessages = messages.map((msg) => ({
...msg,
display: true,
sendToLLM: false,
}));
// Make a copy that combines both
const newMessages = [...updatedMessages, ...updatedSummarizedThread];
// Update the messages state
setMessages(newMessages);
// Clear the summarized thread and content
setSummarizedThread([]);
setSummaryContent('');
};
const hasContextLengthExceededContent = (message: Message): boolean => {
return message.content.some((content) => content.type === 'contextLengthExceeded');
};
const openSummaryModal = () => {
setIsSummaryModalOpen(true);
};
const closeSummaryModal = () => {
setIsSummaryModalOpen(false);
};
const value = {
// State
summaryContent,
summarizedThread,
isSummaryModalOpen,
isLoadingSummary,
errorLoadingSummary,
// Actions
fetchSummary,
updateSummary,
resetMessagesWithSummary,
openSummaryModal,
closeSummaryModal,
hasContextLengthExceededContent,
};
return <ContextManagerContext.Provider value={value}>{children}</ContextManagerContext.Provider>;
};
// Create a hook to use the context
export const useChatContextManager = () => {
const context = useContext(ContextManagerContext);
if (context === undefined) {
throw new Error('useContextManager must be used within a ContextManagerProvider');
}
return context;
};

View File

@@ -1,37 +1,119 @@
import { Message } from '../../types/message'; import {
import { getApiUrl, getSecretKey } from '../../config'; Message as FrontendMessage,
Content as FrontendContent,
MessageContent as FrontendMessageContent,
ToolCallResult,
ToolCall,
} from '../../types/message';
import {
ContextManageRequest,
ContextManageResponse,
manageContext,
Message as ApiMessage,
MessageContent as ApiMessageContent,
} from '../../api';
import { generateId } from 'ai';
export async function manageContext({ export async function manageContextFromBackend({
messages, messages,
manageAction, manageAction,
}: { }: {
messages: Message[]; messages: FrontendMessage[];
manageAction: 'trunction' | 'summarize'; manageAction: 'truncation' | 'summarize';
}) { }): Promise<ContextManageResponse> {
const response = await fetch(getApiUrl('/context/manage'), { try {
method: 'POST', const contextManagementRequest = { manageAction, messages };
headers: {
'Content-Type': 'application/json', // Cast to the API-expected type
'X-Secret-Key': getSecretKey(), const result = await manageContext({
}, body: contextManagementRequest as unknown as ContextManageRequest,
body: JSON.stringify({ });
messages,
manageAction, // Check for errors in the result
}), if (result.error) {
}); throw new Error(`Context management failed: ${result.error}`);
if (!response.ok) {
if (!response.ok) {
// Get the status text or a default message
const errorText = await response.text().catch(() => 'Unknown error');
// log error with status and details
console.error(
`Context management failed: ${response.status} ${response.statusText} - ${errorText}`
);
throw new Error(
`Context management failed: ${response.status} ${response.statusText} - ${errorText}\n\nStart a new session.`
);
} }
// Extract the actual data from the result
if (!result.data) {
throw new Error('Context management returned no data');
}
return result.data;
} catch (error) {
console.error(`Context management failed: ${error || 'Unknown error'}`);
throw new Error(
`Context management failed: ${error || 'Unknown error'}\n\nStart a new session.`
);
} }
const data = await response.json(); }
return data;
// Function to convert API Message to frontend Message
export function convertApiMessageToFrontendMessage(apiMessage: ApiMessage): FrontendMessage {
return {
display: false,
sendToLLM: false,
id: generateId(),
role: apiMessage.role,
created: apiMessage.created,
content: apiMessage.content
.map((apiContent) => mapApiContentToFrontendMessageContent(apiContent))
.filter((content): content is FrontendMessageContent => content !== null),
};
}
// Function to convert API MessageContent to frontend MessageContent
function mapApiContentToFrontendMessageContent(
apiContent: ApiMessageContent
): FrontendMessageContent | null {
// Handle each content type specifically based on its "type" property
if (apiContent.type === 'text') {
return {
type: 'text',
text: apiContent.text,
annotations: apiContent.annotations as Record<string, unknown> | undefined,
};
} else if (apiContent.type === 'image') {
return {
type: 'image',
data: apiContent.data,
mimeType: apiContent.mimeType,
annotations: apiContent.annotations as Record<string, unknown> | undefined,
};
} else if (apiContent.type === 'toolRequest') {
// Ensure the toolCall has the correct type structure
const toolCall = apiContent.toolCall as unknown as ToolCallResult<ToolCall>;
return {
type: 'toolRequest',
id: apiContent.id,
toolCall: toolCall,
};
} else if (apiContent.type === 'toolResponse') {
// Ensure the toolResult has the correct type structure
const toolResult = apiContent.toolResult as unknown as ToolCallResult<FrontendContent[]>;
return {
type: 'toolResponse',
id: apiContent.id,
toolResult: toolResult,
};
} else if (apiContent.type === 'toolConfirmationRequest') {
return {
type: 'toolConfirmationRequest',
id: apiContent.id,
toolName: apiContent.toolName,
arguments: apiContent.arguments as Record<string, unknown>,
prompt: apiContent.prompt === null ? undefined : apiContent.prompt,
};
} else if (apiContent.type === 'contextLengthExceeded') {
return {
type: 'contextLengthExceeded',
msg: apiContent.msg,
};
}
// For types that exist in API but not in frontend, either skip or convert
console.warn(`Skipping unsupported content type: ${apiContent.type}`);
return null;
} }

View File

@@ -215,11 +215,26 @@ export function useMessageStream({
const parsedEvent = JSON.parse(data) as MessageEvent; const parsedEvent = JSON.parse(data) as MessageEvent;
switch (parsedEvent.type) { switch (parsedEvent.type) {
case 'Message': case 'Message': {
// Create a new message object with the properties preserved or defaulted
const newMessage = {
...parsedEvent.message,
// Only set to true if it's undefined (preserve false values)
display:
parsedEvent.message.display === undefined
? true
: parsedEvent.message.display,
sendToLLM:
parsedEvent.message.sendToLLM === undefined
? true
: parsedEvent.message.sendToLLM,
};
// Update messages with the new message // Update messages with the new message
currentMessages = [...currentMessages, parsedEvent.message]; currentMessages = [...currentMessages, newMessage];
mutate(currentMessages, false); mutate(currentMessages, false);
break; break;
}
case 'Error': case 'Error':
throw new Error(parsedEvent.error); throw new Error(parsedEvent.error);
@@ -268,9 +283,12 @@ export function useMessageStream({
const abortController = new AbortController(); const abortController = new AbortController();
abortControllerRef.current = abortController; abortControllerRef.current = abortController;
// Filter out messages where sendToLLM is explicitly false
const filteredMessages = requestMessages.filter((message) => message.sendToLLM !== false);
// Log request details for debugging // Log request details for debugging
console.log('Request details:', { console.log('Request details:', {
messages: requestMessages, messages: filteredMessages,
body: extraMetadataRef.current.body, body: extraMetadataRef.current.body,
}); });

View File

@@ -91,6 +91,8 @@ export interface Message {
role: Role; role: Role;
created: number; created: number;
content: MessageContent[]; content: MessageContent[];
display: boolean;
sendToLLM: boolean;
} }
// Helper functions to create messages // Helper functions to create messages