ctx-mgmt: ctx session management (dev mode only) (#2415)
This commit is contained in:
@@ -9,6 +9,8 @@ use goose::message::{
|
|||||||
};
|
};
|
||||||
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 goose::session::info::SessionInfo;
|
||||||
|
use goose::session::SessionMetadata;
|
||||||
use mcp_core::content::{Annotations, Content, EmbeddedResource, ImageContent, TextContent};
|
use mcp_core::content::{Annotations, Content, EmbeddedResource, ImageContent, TextContent};
|
||||||
use mcp_core::handler::ToolResultSchema;
|
use mcp_core::handler::ToolResultSchema;
|
||||||
use mcp_core::resource::ResourceContents;
|
use mcp_core::resource::ResourceContents;
|
||||||
@@ -33,7 +35,9 @@ 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
|
super::routes::context::manage_context,
|
||||||
|
super::routes::session::list_sessions,
|
||||||
|
super::routes::session::get_session_history
|
||||||
),
|
),
|
||||||
components(schemas(
|
components(schemas(
|
||||||
super::routes::config_management::UpsertConfigQuery,
|
super::routes::config_management::UpsertConfigQuery,
|
||||||
@@ -48,6 +52,8 @@ use utoipa::OpenApi;
|
|||||||
super::routes::reply::PermissionConfirmationRequest,
|
super::routes::reply::PermissionConfirmationRequest,
|
||||||
super::routes::context::ContextManageRequest,
|
super::routes::context::ContextManageRequest,
|
||||||
super::routes::context::ContextManageResponse,
|
super::routes::context::ContextManageResponse,
|
||||||
|
super::routes::session::SessionListResponse,
|
||||||
|
super::routes::session::SessionHistoryResponse,
|
||||||
Message,
|
Message,
|
||||||
MessageContent,
|
MessageContent,
|
||||||
Content,
|
Content,
|
||||||
@@ -76,6 +82,8 @@ use utoipa::OpenApi;
|
|||||||
PermissionLevel,
|
PermissionLevel,
|
||||||
PrincipalType,
|
PrincipalType,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
|
SessionInfo,
|
||||||
|
SessionMetadata,
|
||||||
))
|
))
|
||||||
)]
|
)]
|
||||||
pub struct ApiDoc;
|
pub struct ApiDoc;
|
||||||
|
|||||||
@@ -11,20 +11,41 @@ use axum::{
|
|||||||
use goose::message::Message;
|
use goose::message::Message;
|
||||||
use goose::session;
|
use goose::session;
|
||||||
use goose::session::info::{get_session_info, SessionInfo, SortOrder};
|
use goose::session::info::{get_session_info, SessionInfo, SortOrder};
|
||||||
|
use goose::session::SessionMetadata;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
struct SessionListResponse {
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SessionListResponse {
|
||||||
|
/// List of available session information objects
|
||||||
sessions: Vec<SessionInfo>,
|
sessions: Vec<SessionInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
struct SessionHistoryResponse {
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SessionHistoryResponse {
|
||||||
|
/// Unique identifier for the session
|
||||||
session_id: String,
|
session_id: String,
|
||||||
metadata: session::SessionMetadata,
|
/// Session metadata containing creation time and other details
|
||||||
|
metadata: SessionMetadata,
|
||||||
|
/// List of messages in the session conversation
|
||||||
messages: Vec<Message>,
|
messages: Vec<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/sessions",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "List of available sessions retrieved successfully", body = SessionListResponse),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("api_key" = [])
|
||||||
|
),
|
||||||
|
tag = "Session Management"
|
||||||
|
)]
|
||||||
// List all available sessions
|
// List all available sessions
|
||||||
async fn list_sessions(
|
async fn list_sessions(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
@@ -38,6 +59,23 @@ async fn list_sessions(
|
|||||||
Ok(Json(SessionListResponse { sessions }))
|
Ok(Json(SessionListResponse { sessions }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/sessions/{session_id}",
|
||||||
|
params(
|
||||||
|
("session_id" = String, Path, description = "Unique identifier for the session")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Session history retrieved successfully", body = SessionHistoryResponse),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||||
|
(status = 404, description = "Session not found"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("api_key" = [])
|
||||||
|
),
|
||||||
|
tag = "Session Management"
|
||||||
|
)]
|
||||||
// Get a specific session's history
|
// Get a specific session's history
|
||||||
async fn get_session_history(
|
async fn get_session_history(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
|
|||||||
@@ -77,9 +77,8 @@ pub struct RedactedThinkingContent {
|
|||||||
pub data: String,
|
pub data: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
|
||||||
#[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")]
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
use crate::session::{self, SessionMetadata};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::session::{self, SessionMetadata};
|
#[derive(Clone, Serialize, ToSchema)]
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
|
||||||
pub struct SessionInfo {
|
pub struct SessionInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use std::fs::{self, File};
|
|||||||
use std::io::{self, BufRead, Write};
|
use std::io::{self, BufRead, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
fn get_home_dir() -> PathBuf {
|
fn get_home_dir() -> PathBuf {
|
||||||
choose_app_strategy(crate::config::APP_STRATEGY.clone())
|
choose_app_strategy(crate::config::APP_STRATEGY.clone())
|
||||||
@@ -17,9 +18,10 @@ fn get_home_dir() -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata for a session, stored as the first line in the session file
|
/// Metadata for a session, stored as the first line in the session file
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
pub struct SessionMetadata {
|
pub struct SessionMetadata {
|
||||||
/// Working directory for the session
|
/// Working directory for the session
|
||||||
|
#[schema(value_type = String, example = "/home/user/sessions/session1")]
|
||||||
pub working_dir: PathBuf,
|
pub working_dir: PathBuf,
|
||||||
/// A short description of the session, typically 3 words or less
|
/// A short description of the session, typically 3 words or less
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ pub struct Resource {
|
|||||||
pub annotations: Option<Annotations>,
|
pub annotations: Option<Annotations>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(ToSchema, 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,
|
||||||
|
|||||||
@@ -452,6 +452,82 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/sessions": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Session Management"
|
||||||
|
],
|
||||||
|
"operationId": "list_sessions",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of available sessions retrieved successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SessionListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - Invalid or missing API key"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/sessions/{session_id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Session Management"
|
||||||
|
],
|
||||||
|
"operationId": "get_session_history",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "session_id",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Unique identifier for the session",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Session history retrieved successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SessionHistoryResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - Invalid or missing API key"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Session not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@@ -1372,6 +1448,129 @@
|
|||||||
"assistant"
|
"assistant"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"SessionHistoryResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"sessionId",
|
||||||
|
"metadata",
|
||||||
|
"messages"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"messages": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Message"
|
||||||
|
},
|
||||||
|
"description": "List of messages in the session conversation"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"$ref": "#/components/schemas/SessionMetadata"
|
||||||
|
},
|
||||||
|
"sessionId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique identifier for the session"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SessionInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"path",
|
||||||
|
"modified",
|
||||||
|
"metadata"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"$ref": "#/components/schemas/SessionMetadata"
|
||||||
|
},
|
||||||
|
"modified": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SessionListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"sessions"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"sessions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/SessionInfo"
|
||||||
|
},
|
||||||
|
"description": "List of available session information objects"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SessionMetadata": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Metadata for a session, stored as the first line in the session file",
|
||||||
|
"required": [
|
||||||
|
"working_dir",
|
||||||
|
"description",
|
||||||
|
"message_count"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"accumulated_input_tokens": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "The number of input tokens used in the session. Accumulated across all messages.",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"accumulated_output_tokens": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "The number of output tokens used in the session. Accumulated across all messages.",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"accumulated_total_tokens": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "The total number of tokens used in the session. Accumulated across all messages (useful for tracking cost over an entire session).",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A short description of the session, typically 3 words or less"
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "The number of input tokens used in the session. Retrieved from the provider's last usage.",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"message_count": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of messages in the session",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "The number of output tokens used in the session. Retrieved from the provider's last usage.",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"total_tokens": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "The total number of tokens used in the session. Retrieved from the provider's last usage.",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"working_dir": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Working directory for the session",
|
||||||
|
"example": "/home/user/sessions/session1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"TextContent": {
|
"TextContent": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -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, ManageContextData, ManageContextResponse } 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, ListSessionsData, ListSessionsResponse, GetSessionHistoryData, GetSessionHistoryResponse } 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> & {
|
||||||
@@ -142,4 +142,18 @@ export const manageContext = <ThrowOnError extends boolean = false>(options: Opt
|
|||||||
...options?.headers
|
...options?.headers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listSessions = <ThrowOnError extends boolean = false>(options?: Options<ListSessionsData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? _heyApiClient).get<ListSessionsResponse, unknown, ThrowOnError>({
|
||||||
|
url: '/sessions',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSessionHistory = <ThrowOnError extends boolean = false>(options: Options<GetSessionHistoryData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).get<GetSessionHistoryResponse, unknown, ThrowOnError>({
|
||||||
|
url: '/sessions/{session_id}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
};
|
};
|
||||||
@@ -292,6 +292,74 @@ export type ResourceContents = {
|
|||||||
|
|
||||||
export type Role = 'user' | 'assistant';
|
export type Role = 'user' | 'assistant';
|
||||||
|
|
||||||
|
export type SessionHistoryResponse = {
|
||||||
|
/**
|
||||||
|
* List of messages in the session conversation
|
||||||
|
*/
|
||||||
|
messages: Array<Message>;
|
||||||
|
metadata: SessionMetadata;
|
||||||
|
/**
|
||||||
|
* Unique identifier for the session
|
||||||
|
*/
|
||||||
|
sessionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessionInfo = {
|
||||||
|
id: string;
|
||||||
|
metadata: SessionMetadata;
|
||||||
|
modified: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessionListResponse = {
|
||||||
|
/**
|
||||||
|
* List of available session information objects
|
||||||
|
*/
|
||||||
|
sessions: Array<SessionInfo>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata for a session, stored as the first line in the session file
|
||||||
|
*/
|
||||||
|
export type SessionMetadata = {
|
||||||
|
/**
|
||||||
|
* The number of input tokens used in the session. Accumulated across all messages.
|
||||||
|
*/
|
||||||
|
accumulated_input_tokens?: number | null;
|
||||||
|
/**
|
||||||
|
* The number of output tokens used in the session. Accumulated across all messages.
|
||||||
|
*/
|
||||||
|
accumulated_output_tokens?: number | null;
|
||||||
|
/**
|
||||||
|
* The total number of tokens used in the session. Accumulated across all messages (useful for tracking cost over an entire session).
|
||||||
|
*/
|
||||||
|
accumulated_total_tokens?: number | null;
|
||||||
|
/**
|
||||||
|
* A short description of the session, typically 3 words or less
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* The number of input tokens used in the session. Retrieved from the provider's last usage.
|
||||||
|
*/
|
||||||
|
input_tokens?: number | null;
|
||||||
|
/**
|
||||||
|
* Number of messages in the session
|
||||||
|
*/
|
||||||
|
message_count: number;
|
||||||
|
/**
|
||||||
|
* The number of output tokens used in the session. Retrieved from the provider's last usage.
|
||||||
|
*/
|
||||||
|
output_tokens?: number | null;
|
||||||
|
/**
|
||||||
|
* The total number of tokens used in the session. Retrieved from the provider's last usage.
|
||||||
|
*/
|
||||||
|
total_tokens?: number | null;
|
||||||
|
/**
|
||||||
|
* Working directory for the session
|
||||||
|
*/
|
||||||
|
working_dir: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TextContent = {
|
export type TextContent = {
|
||||||
annotations?: Annotations | null;
|
annotations?: Annotations | null;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -775,6 +843,69 @@ export type ManageContextResponses = {
|
|||||||
|
|
||||||
export type ManageContextResponse = ManageContextResponses[keyof ManageContextResponses];
|
export type ManageContextResponse = ManageContextResponses[keyof ManageContextResponses];
|
||||||
|
|
||||||
|
export type ListSessionsData = {
|
||||||
|
body?: never;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/sessions';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListSessionsErrors = {
|
||||||
|
/**
|
||||||
|
* Unauthorized - Invalid or missing API key
|
||||||
|
*/
|
||||||
|
401: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListSessionsResponses = {
|
||||||
|
/**
|
||||||
|
* List of available sessions retrieved successfully
|
||||||
|
*/
|
||||||
|
200: SessionListResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListSessionsResponse = ListSessionsResponses[keyof ListSessionsResponses];
|
||||||
|
|
||||||
|
export type GetSessionHistoryData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the session
|
||||||
|
*/
|
||||||
|
session_id: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/sessions/{session_id}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetSessionHistoryErrors = {
|
||||||
|
/**
|
||||||
|
* Unauthorized - Invalid or missing API key
|
||||||
|
*/
|
||||||
|
401: unknown;
|
||||||
|
/**
|
||||||
|
* Session not found
|
||||||
|
*/
|
||||||
|
404: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetSessionHistoryResponses = {
|
||||||
|
/**
|
||||||
|
* Session history retrieved successfully
|
||||||
|
*/
|
||||||
|
200: SessionHistoryResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetSessionHistoryResponse = GetSessionHistoryResponses[keyof GetSessionHistoryResponses];
|
||||||
|
|
||||||
export type ClientOptions = {
|
export type ClientOptions = {
|
||||||
baseUrl: `${string}://${string}` | (string & {});
|
baseUrl: `${string}://${string}` | (string & {});
|
||||||
};
|
};
|
||||||
@@ -15,12 +15,15 @@ import { SearchView } from './conversation/SearchView';
|
|||||||
import { createRecipe } from '../recipe';
|
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, generateSessionId } from '../sessions';
|
||||||
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 {
|
||||||
|
ChatContextManagerProvider,
|
||||||
|
useChatContextManager,
|
||||||
|
} from './context_management/ContextManager';
|
||||||
import { ContextLengthExceededHandler } from './context_management/ContextLengthExceededHandler';
|
import { ContextLengthExceededHandler } from './context_management/ContextLengthExceededHandler';
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
@@ -64,14 +67,14 @@ export default function ChatView({
|
|||||||
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ContextManagerProvider>
|
<ChatContextManagerProvider>
|
||||||
<ChatContent
|
<ChatContent
|
||||||
chat={chat}
|
chat={chat}
|
||||||
setChat={setChat}
|
setChat={setChat}
|
||||||
setView={setView}
|
setView={setView}
|
||||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||||
/>
|
/>
|
||||||
</ContextManagerProvider>
|
</ChatContextManagerProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +96,9 @@ function ChatContent({
|
|||||||
const [showGame, setShowGame] = useState(false);
|
const [showGame, setShowGame] = useState(false);
|
||||||
const [isGeneratingRecipe, setIsGeneratingRecipe] = useState(false);
|
const [isGeneratingRecipe, setIsGeneratingRecipe] = useState(false);
|
||||||
const [sessionTokenCount, setSessionTokenCount] = useState<number>(0);
|
const [sessionTokenCount, setSessionTokenCount] = useState<number>(0);
|
||||||
|
const [ancestorMessages, setAncestorMessages] = useState<Message[]>([]);
|
||||||
const [droppedFiles, setDroppedFiles] = useState<string[]>([]);
|
const [droppedFiles, setDroppedFiles] = useState<string[]>([]);
|
||||||
|
|
||||||
const scrollRef = useRef<ScrollAreaHandle>(null);
|
const scrollRef = useRef<ScrollAreaHandle>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -108,7 +113,6 @@ function ChatContent({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log all messages when the component first mounts
|
// Log all messages when the component first mounts
|
||||||
console.log('Initial messages when resuming session:', chat.messages);
|
|
||||||
window.electron.logInfo(
|
window.electron.logInfo(
|
||||||
'Initial messages when resuming session: ' + JSON.stringify(chat.messages, null, 2)
|
'Initial messages when resuming session: ' + JSON.stringify(chat.messages, null, 2)
|
||||||
);
|
);
|
||||||
@@ -129,6 +133,7 @@ function ChatContent({
|
|||||||
setInput: _setInput,
|
setInput: _setInput,
|
||||||
handleInputChange: _handleInputChange,
|
handleInputChange: _handleInputChange,
|
||||||
handleSubmit: _submitMessage,
|
handleSubmit: _submitMessage,
|
||||||
|
updateMessageStreamBody,
|
||||||
} = useMessageStream({
|
} = useMessageStream({
|
||||||
api: getApiUrl('/reply'),
|
api: getApiUrl('/reply'),
|
||||||
initialMessages: chat.messages,
|
initialMessages: chat.messages,
|
||||||
@@ -159,6 +164,36 @@ function ChatContent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// for CLE events -- create a new session id for the next set of messages
|
||||||
|
useEffect(() => {
|
||||||
|
// If we're in a continuation session, update the chat ID
|
||||||
|
if (summarizedThread.length > 0) {
|
||||||
|
const newSessionId = generateSessionId();
|
||||||
|
|
||||||
|
// Update the session ID in the chat object
|
||||||
|
setChat({
|
||||||
|
...chat,
|
||||||
|
id: newSessionId!,
|
||||||
|
title: `Continued from ${chat.id}`,
|
||||||
|
messageHistoryIndex: summarizedThread.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the body used by useMessageStream to send future messages to the new session
|
||||||
|
if (summarizedThread.length > 0 && updateMessageStreamBody) {
|
||||||
|
updateMessageStreamBody({
|
||||||
|
session_id: newSessionId,
|
||||||
|
session_working_dir: window.appConfig.get('GOOSE_WORKING_DIR'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only update if summarizedThread length changes from 0 -> 1+
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
summarizedThread.length > 0,
|
||||||
|
]);
|
||||||
|
|
||||||
// Listen for make-agent-from-chat event
|
// Listen for make-agent-from-chat event
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMakeAgent = async () => {
|
const handleMakeAgent = async () => {
|
||||||
@@ -201,7 +236,8 @@ function ChatContent({
|
|||||||
window.electron.logInfo('Opening recipe editor window');
|
window.electron.logInfo('Opening recipe editor window');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.electron.logInfo('Failed to create recipe:');
|
window.electron.logInfo('Failed to create recipe:');
|
||||||
window.electron.logInfo(error.message);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
window.electron.logInfo(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingRecipe(false);
|
setIsGeneratingRecipe(false);
|
||||||
}
|
}
|
||||||
@@ -233,25 +269,33 @@ function ChatContent({
|
|||||||
// Handle submit
|
// Handle submit
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
window.electron.startPowerSaveBlocker();
|
window.electron.startPowerSaveBlocker();
|
||||||
const customEvent = e as CustomEvent;
|
const customEvent = e as unknown as CustomEvent;
|
||||||
const content = customEvent.detail?.value || '';
|
const content = customEvent.detail?.value || '';
|
||||||
|
|
||||||
if (content.trim()) {
|
if (content.trim()) {
|
||||||
setLastInteractionTime(Date.now());
|
setLastInteractionTime(Date.now());
|
||||||
|
|
||||||
if (process.env.ALPHA && summarizedThread.length > 0) {
|
if (summarizedThread.length > 0) {
|
||||||
// First reset the messages with the summary
|
// move current `messages` to `ancestorMessages` and `messages` to `summarizedThread`
|
||||||
resetMessagesWithSummary(messages, setMessages);
|
resetMessagesWithSummary(
|
||||||
|
messages,
|
||||||
|
setMessages,
|
||||||
|
ancestorMessages,
|
||||||
|
setAncestorMessages,
|
||||||
|
summaryContent
|
||||||
|
);
|
||||||
|
|
||||||
// Then append the new user message
|
// update the chat with new sessionId
|
||||||
|
|
||||||
|
// now call the llm
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
append(createUserMessage(content));
|
append(createUserMessage(content));
|
||||||
if (scrollRef.current?.scrollToBottom) {
|
if (scrollRef.current?.scrollToBottom) {
|
||||||
scrollRef.current.scrollToBottom();
|
scrollRef.current.scrollToBottom();
|
||||||
}
|
}
|
||||||
}, 150); // Small delay to ensure state updates properly
|
}, 150);
|
||||||
} else {
|
} else {
|
||||||
// Normal flow - just append the message
|
// Normal flow (existing code)
|
||||||
append(createUserMessage(content));
|
append(createUserMessage(content));
|
||||||
if (scrollRef.current?.scrollToBottom) {
|
if (scrollRef.current?.scrollToBottom) {
|
||||||
scrollRef.current.scrollToBottom();
|
scrollRef.current.scrollToBottom();
|
||||||
@@ -324,6 +368,8 @@ function ChatContent({
|
|||||||
// Create tool responses for all interrupted tool requests
|
// Create tool responses for all interrupted tool requests
|
||||||
|
|
||||||
let responseMessage: Message = {
|
let responseMessage: Message = {
|
||||||
|
display: true,
|
||||||
|
sendToLLM: true,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
content: [],
|
content: [],
|
||||||
@@ -352,7 +398,7 @@ function ChatContent({
|
|||||||
|
|
||||||
// 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 = [...ancestorMessages, ...messages].filter((message) => {
|
||||||
// Only filter out when display is explicitly false
|
// Only filter out when display is explicitly false
|
||||||
if (message.display === false) return false;
|
if (message.display === false) return false;
|
||||||
|
|
||||||
@@ -466,10 +512,13 @@ function ChatContent({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* 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.NODE_ENV === 'development' &&
|
||||||
|
hasContextLengthExceededContent(message) ? (
|
||||||
<ContextLengthExceededHandler
|
<ContextLengthExceededHandler
|
||||||
messages={messages}
|
messages={messages}
|
||||||
messageId={message.id ?? message.created.toString()}
|
messageId={message.id ?? message.created.toString()}
|
||||||
|
chatId={chat.id}
|
||||||
|
workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<GooseMessage
|
<GooseMessage
|
||||||
@@ -529,7 +578,7 @@ function ChatContent({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
|
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
|
||||||
{process.env.ALPHA && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<SessionSummaryModal
|
<SessionSummaryModal
|
||||||
isOpen={isSummaryModalOpen}
|
isOpen={isSummaryModalOpen}
|
||||||
onClose={closeSummaryModal}
|
onClose={closeSummaryModal}
|
||||||
|
|||||||
@@ -5,21 +5,30 @@ import { useChatContextManager } from './ContextManager';
|
|||||||
interface ContextLengthExceededHandlerProps {
|
interface ContextLengthExceededHandlerProps {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
chatId: string;
|
||||||
|
workingDir: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContextLengthExceededHandler: React.FC<ContextLengthExceededHandlerProps> = ({
|
export const ContextLengthExceededHandler: React.FC<ContextLengthExceededHandlerProps> = ({
|
||||||
messages,
|
messages,
|
||||||
messageId,
|
messageId,
|
||||||
|
chatId,
|
||||||
|
workingDir,
|
||||||
}) => {
|
}) => {
|
||||||
const { fetchSummary, summaryContent, isLoadingSummary, errorLoadingSummary, openSummaryModal } =
|
const {
|
||||||
useChatContextManager();
|
summaryContent,
|
||||||
|
isLoadingSummary,
|
||||||
|
errorLoadingSummary,
|
||||||
|
openSummaryModal,
|
||||||
|
handleContextLengthExceeded,
|
||||||
|
} = useChatContextManager();
|
||||||
|
|
||||||
const [hasFetchStarted, setHasFetchStarted] = useState(false);
|
const [hasFetchStarted, setHasFetchStarted] = useState(false);
|
||||||
|
|
||||||
// Find the relevant message to check if it's the latest
|
// Find the relevant message to check if it's the latest
|
||||||
const isCurrentMessageLatest =
|
const isCurrentMessageLatest =
|
||||||
messageId === messages[messages.length - 1].id ||
|
messageId === messages[messages.length - 1]?.id ||
|
||||||
messageId === messages[messages.length - 1].created.toString();
|
messageId === String(messages[messages.length - 1]?.created);
|
||||||
|
|
||||||
// Only allow interaction for the most recent context length exceeded event
|
// Only allow interaction for the most recent context length exceeded event
|
||||||
const shouldAllowSummaryInteraction = isCurrentMessageLatest;
|
const shouldAllowSummaryInteraction = isCurrentMessageLatest;
|
||||||
@@ -27,31 +36,60 @@ export const ContextLengthExceededHandler: React.FC<ContextLengthExceededHandler
|
|||||||
// Use a ref to track if we've started the fetch
|
// Use a ref to track if we've started the fetch
|
||||||
const fetchStartedRef = useRef(false);
|
const fetchStartedRef = useRef(false);
|
||||||
|
|
||||||
|
// Function to trigger the async operation properly
|
||||||
|
const triggerContextLengthExceeded = () => {
|
||||||
|
setHasFetchStarted(true);
|
||||||
|
fetchStartedRef.current = true;
|
||||||
|
|
||||||
|
// Call the async function without awaiting it in useEffect
|
||||||
|
handleContextLengthExceeded(messages, chatId, workingDir).catch((err) => {
|
||||||
|
console.error('Error handling context length exceeded:', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Automatically fetch summary if conditions are met
|
|
||||||
if (
|
if (
|
||||||
!summaryContent &&
|
!summaryContent &&
|
||||||
!hasFetchStarted &&
|
!hasFetchStarted &&
|
||||||
shouldAllowSummaryInteraction &&
|
shouldAllowSummaryInteraction &&
|
||||||
!fetchStartedRef.current
|
!fetchStartedRef.current
|
||||||
) {
|
) {
|
||||||
setHasFetchStarted(true);
|
// Use the wrapper function instead of calling the async function directly
|
||||||
fetchStartedRef.current = true;
|
triggerContextLengthExceeded();
|
||||||
fetchSummary(messages);
|
|
||||||
}
|
}
|
||||||
}, [fetchSummary, hasFetchStarted, messages, shouldAllowSummaryInteraction, summaryContent]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
hasFetchStarted,
|
||||||
|
messages,
|
||||||
|
shouldAllowSummaryInteraction,
|
||||||
|
summaryContent,
|
||||||
|
chatId,
|
||||||
|
workingDir,
|
||||||
|
]);
|
||||||
|
|
||||||
// Handle retry
|
// Handle retry - Call the async function properly
|
||||||
const handleRetry = () => {
|
const handleRetry = () => {
|
||||||
if (!shouldAllowSummaryInteraction) return;
|
if (!shouldAllowSummaryInteraction) return;
|
||||||
fetchSummary(messages);
|
|
||||||
|
// Reset states for retry
|
||||||
|
setHasFetchStarted(false);
|
||||||
|
fetchStartedRef.current = false;
|
||||||
|
|
||||||
|
// Trigger the process again
|
||||||
|
triggerContextLengthExceeded();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render the notification UI
|
// Render the notification UI
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start mt-1 pl-4">
|
<div className="flex flex-col items-start mt-1 pl-4">
|
||||||
|
{/* Horizontal line with text in the middle - shown regardless of loading state */}
|
||||||
|
<div className="relative flex items-center py-2 w-full">
|
||||||
|
<div className="flex-grow border-t border-gray-300"></div>
|
||||||
|
<div className="flex-grow border-t border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isLoadingSummary && shouldAllowSummaryInteraction ? (
|
{isLoadingSummary && shouldAllowSummaryInteraction ? (
|
||||||
// Only show loading indicator during loading state
|
// Show loading indicator during loading state
|
||||||
<div className="flex items-center text-xs text-gray-400">
|
<div className="flex items-center text-xs text-gray-400">
|
||||||
<span className="mr-2">Preparing summary...</span>
|
<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>
|
<span className="animate-spin h-3 w-3 border-2 border-gray-400 rounded-full border-t-transparent"></span>
|
||||||
@@ -59,15 +97,17 @@ export const ContextLengthExceededHandler: React.FC<ContextLengthExceededHandler
|
|||||||
) : (
|
) : (
|
||||||
// Show different UI based on whether it's already handled
|
// Show different UI based on whether it's already handled
|
||||||
<>
|
<>
|
||||||
<span className="text-xs text-gray-400 italic">{'Session summarized'}</span>
|
<span className="text-xs text-gray-400">{`Your conversation has exceeded the model's context capacity`}</span>
|
||||||
|
<span className="text-xs text-gray-400">{`Messages above this line remain viewable but are not included in the active context`}</span>
|
||||||
{/* Only show the button if its last message */}
|
{/* Only show the button if its last message */}
|
||||||
{shouldAllowSummaryInteraction && (
|
{shouldAllowSummaryInteraction && (
|
||||||
<button
|
<button
|
||||||
onClick={() => (errorLoadingSummary ? handleRetry() : openSummaryModal())}
|
onClick={() => (errorLoadingSummary ? handleRetry() : openSummaryModal())}
|
||||||
className="text-xs text-textStandard hover:text-textSubtle transition-colors mt-1 flex items-center"
|
className="text-xs text-textStandard hover:text-textSubtle transition-colors mt-1 flex items-center"
|
||||||
>
|
>
|
||||||
{errorLoadingSummary ? 'Retry loading summary' : 'View or edit summary'}
|
{errorLoadingSummary
|
||||||
|
? 'Retry loading summary'
|
||||||
|
: 'View or edit summary (you may continue your conversation based on the summary)'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Message } from '../../types/message';
|
|||||||
import { manageContextFromBackend, convertApiMessageToFrontendMessage } from './index';
|
import { manageContextFromBackend, convertApiMessageToFrontendMessage } from './index';
|
||||||
|
|
||||||
// Define the context management interface
|
// Define the context management interface
|
||||||
interface ContextManagerState {
|
interface ChatContextManagerState {
|
||||||
summaryContent: string;
|
summaryContent: string;
|
||||||
summarizedThread: Message[];
|
summarizedThread: Message[];
|
||||||
isSummaryModalOpen: boolean;
|
isSummaryModalOpen: boolean;
|
||||||
@@ -11,31 +11,73 @@ interface ContextManagerState {
|
|||||||
errorLoadingSummary: boolean;
|
errorLoadingSummary: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContextManagerActions {
|
interface ChatContextManagerActions {
|
||||||
fetchSummary: (messages: Message[]) => Promise<void>;
|
fetchSummary: (messages: Message[]) => Promise<void>;
|
||||||
updateSummary: (newSummaryContent: string) => void;
|
updateSummary: (newSummaryContent: string) => void;
|
||||||
resetMessagesWithSummary: (
|
resetMessagesWithSummary: (
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
setMessages: (messages: Message[]) => void
|
setMessages: (messages: Message[]) => void,
|
||||||
|
ancestorMessages: Message[],
|
||||||
|
setAncestorMessages: (messages: Message[]) => void,
|
||||||
|
summaryContent: string
|
||||||
) => void;
|
) => void;
|
||||||
openSummaryModal: () => void;
|
openSummaryModal: () => void;
|
||||||
closeSummaryModal: () => void;
|
closeSummaryModal: () => void;
|
||||||
hasContextLengthExceededContent: (message: Message) => boolean;
|
hasContextLengthExceededContent: (message: Message) => boolean;
|
||||||
|
handleContextLengthExceeded: (
|
||||||
|
messages: Message[],
|
||||||
|
chatId: string,
|
||||||
|
workingDir: string
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the context
|
// Create the context
|
||||||
const ContextManagerContext = createContext<
|
const ChatContextManagerContext = createContext<
|
||||||
(ContextManagerState & ContextManagerActions) | undefined
|
(ChatContextManagerState & ChatContextManagerActions) | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
// Create the provider component
|
// Create the provider component
|
||||||
export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const ChatContextManagerProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
const [summaryContent, setSummaryContent] = useState<string>('');
|
const [summaryContent, setSummaryContent] = useState<string>('');
|
||||||
const [summarizedThread, setSummarizedThread] = useState<Message[]>([]);
|
const [summarizedThread, setSummarizedThread] = useState<Message[]>([]);
|
||||||
const [isSummaryModalOpen, setIsSummaryModalOpen] = useState<boolean>(false);
|
const [isSummaryModalOpen, setIsSummaryModalOpen] = useState<boolean>(false);
|
||||||
const [isLoadingSummary, setIsLoadingSummary] = useState<boolean>(false);
|
const [isLoadingSummary, setIsLoadingSummary] = useState<boolean>(false);
|
||||||
const [errorLoadingSummary, setErrorLoadingSummary] = useState<boolean>(false);
|
const [errorLoadingSummary, setErrorLoadingSummary] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleContextLengthExceeded = async (messages: Message[]): Promise<void> => {
|
||||||
|
setIsLoadingSummary(true);
|
||||||
|
setErrorLoadingSummary(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Now get the summary from the backend
|
||||||
|
const summaryResponse = await manageContextFromBackend({
|
||||||
|
messages: messages,
|
||||||
|
manageAction: 'summarize',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert API messages to frontend messages
|
||||||
|
const convertedMessages = summaryResponse.messages.map(
|
||||||
|
(apiMessage) => convertApiMessageToFrontendMessage(apiMessage, false, true) // do not show to user but send to llm
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract summary 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 handling context length exceeded:', err);
|
||||||
|
setErrorLoadingSummary(true);
|
||||||
|
setIsLoadingSummary(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchSummary = async (messages: Message[]) => {
|
const fetchSummary = async (messages: Message[]) => {
|
||||||
setIsLoadingSummary(true);
|
setIsLoadingSummary(true);
|
||||||
setErrorLoadingSummary(false);
|
setErrorLoadingSummary(false);
|
||||||
@@ -47,8 +89,8 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Convert API messages to frontend messages
|
// Convert API messages to frontend messages
|
||||||
const convertedMessages = response.messages.map((apiMessage) =>
|
const convertedMessages = response.messages.map(
|
||||||
convertApiMessageToFrontendMessage(apiMessage)
|
(apiMessage) => convertApiMessageToFrontendMessage(apiMessage, false, true) // do not show to user but send to llm
|
||||||
);
|
);
|
||||||
|
|
||||||
// Extract the summary text from the first message
|
// Extract the summary text from the first message
|
||||||
@@ -101,27 +143,69 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
|
|||||||
|
|
||||||
const resetMessagesWithSummary = (
|
const resetMessagesWithSummary = (
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
setMessages: (messages: Message[]) => void
|
setMessages: (messages: Message[]) => void,
|
||||||
|
ancestorMessages: Message[],
|
||||||
|
setAncestorMessages: (messages: Message[]) => void,
|
||||||
|
summaryContent: string
|
||||||
) => {
|
) => {
|
||||||
// Update summarizedThread with metadata
|
// Create a copy of the summarized thread
|
||||||
const updatedSummarizedThread = summarizedThread.map((msg) => ({
|
const updatedSummarizedThread = [...summarizedThread];
|
||||||
...msg,
|
|
||||||
display: false,
|
|
||||||
sendToLLM: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Update list of messages with other metadata
|
// Make sure there's at least one message in the summarized thread
|
||||||
const updatedMessages = messages.map((msg) => ({
|
if (updatedSummarizedThread.length > 0) {
|
||||||
...msg,
|
// Get the first message
|
||||||
display: true,
|
const firstMessage = { ...updatedSummarizedThread[0] };
|
||||||
sendToLLM: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Make a copy that combines both
|
// Make a copy of the content array
|
||||||
const newMessages = [...updatedMessages, ...updatedSummarizedThread];
|
const contentCopy = [...firstMessage.content];
|
||||||
|
|
||||||
|
// Assuming the first content item is of type TextContent
|
||||||
|
if (contentCopy.length > 0 && 'text' in contentCopy[0]) {
|
||||||
|
// Update the text with the new summary content
|
||||||
|
contentCopy[0] = {
|
||||||
|
...contentCopy[0],
|
||||||
|
text: summaryContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the first message with the new content
|
||||||
|
firstMessage.content = contentCopy;
|
||||||
|
|
||||||
|
// Update the first message in the thread
|
||||||
|
updatedSummarizedThread[0] = firstMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update metadata for the summarized thread
|
||||||
|
const finalUpdatedThread = updatedSummarizedThread.map((msg, index) => ({
|
||||||
|
...msg,
|
||||||
|
display: index === 0, // First message has display: true, others false
|
||||||
|
sendToLLM: true, // All messages have sendToLLM: true
|
||||||
|
}));
|
||||||
|
|
||||||
// Update the messages state
|
// Update the messages state
|
||||||
setMessages(newMessages);
|
setMessages(finalUpdatedThread);
|
||||||
|
|
||||||
|
// If ancestorMessages already has items, extend it instead of replacing it
|
||||||
|
if (ancestorMessages.length > 0) {
|
||||||
|
// Convert current messages to ancestor format
|
||||||
|
const newAncestorMessages = messages.map((msg) => ({
|
||||||
|
...msg,
|
||||||
|
display: true,
|
||||||
|
sendToLLM: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Append new ancestor messages to existing ones
|
||||||
|
setAncestorMessages([...ancestorMessages, ...newAncestorMessages]);
|
||||||
|
} else {
|
||||||
|
// Initial set of ancestor messages
|
||||||
|
const newAncestorMessages = messages.map((msg) => ({
|
||||||
|
...msg,
|
||||||
|
display: true,
|
||||||
|
sendToLLM: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setAncestorMessages(newAncestorMessages);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the summarized thread and content
|
// Clear the summarized thread and content
|
||||||
setSummarizedThread([]);
|
setSummarizedThread([]);
|
||||||
@@ -155,14 +239,19 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
|
|||||||
openSummaryModal,
|
openSummaryModal,
|
||||||
closeSummaryModal,
|
closeSummaryModal,
|
||||||
hasContextLengthExceededContent,
|
hasContextLengthExceededContent,
|
||||||
|
handleContextLengthExceeded,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <ContextManagerContext.Provider value={value}>{children}</ContextManagerContext.Provider>;
|
return (
|
||||||
|
<ChatContextManagerContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ChatContextManagerContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a hook to use the context
|
// Create a hook to use the context
|
||||||
export const useChatContextManager = () => {
|
export const useChatContextManager = () => {
|
||||||
const context = useContext(ContextManagerContext);
|
const context = useContext(ChatContextManagerContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error('useContextManager must be used within a ContextManagerProvider');
|
throw new Error('useContextManager must be used within a ContextManagerProvider');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ export function SessionSummaryModal({
|
|||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
defaultValue={summaryContent}
|
defaultValue={summaryContent}
|
||||||
className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 text-sm w-full min-h-[200px] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 text-sm w-full min-h-[200px] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
style={{
|
||||||
|
textRendering: 'optimizeLegibility',
|
||||||
|
WebkitFontSmoothing: 'antialiased',
|
||||||
|
MozOsxFontSmoothing: 'grayscale',
|
||||||
|
transform: 'translateZ(0)', // Force hardware acceleration
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,10 +49,14 @@ export async function manageContextFromBackend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to convert API Message to frontend Message
|
// Function to convert API Message to frontend Message
|
||||||
export function convertApiMessageToFrontendMessage(apiMessage: ApiMessage): FrontendMessage {
|
export function convertApiMessageToFrontendMessage(
|
||||||
|
apiMessage: ApiMessage,
|
||||||
|
display?: boolean,
|
||||||
|
sendToLLM?: boolean
|
||||||
|
): FrontendMessage {
|
||||||
return {
|
return {
|
||||||
display: false,
|
display: display ?? true,
|
||||||
sendToLLM: false,
|
sendToLLM: sendToLLM ?? true,
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
role: apiMessage.role,
|
role: apiMessage.role,
|
||||||
created: apiMessage.created,
|
created: apiMessage.created,
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const SessionListView: React.FC<SessionListViewProps> = ({ setView, onSelectSess
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetchSessions();
|
const sessions = await fetchSessions();
|
||||||
setSessions(response.sessions);
|
setSessions(sessions);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load sessions:', err);
|
console.error('Failed to load sessions:', err);
|
||||||
setError('Failed to load sessions. Please try again later.');
|
setError('Failed to load sessions. Please try again later.');
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
|
|||||||
|
|
||||||
const handleResumeSession = () => {
|
const handleResumeSession = () => {
|
||||||
if (selectedSession) {
|
if (selectedSession) {
|
||||||
|
console.log('Selected session object:', JSON.stringify(selectedSession, null, 2));
|
||||||
|
|
||||||
// Get the working directory from the session metadata
|
// Get the working directory from the session metadata
|
||||||
const workingDir = selectedSession.metadata.working_dir;
|
const workingDir = selectedSession.metadata.working_dir;
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ export interface UseMessageStreamHelpers {
|
|||||||
|
|
||||||
/** Add a tool result to a tool call */
|
/** Add a tool result to a tool call */
|
||||||
addToolResult: ({ toolCallId, result }: { toolCallId: string; result: unknown }) => void;
|
addToolResult: ({ toolCallId, result }: { toolCallId: string; result: unknown }) => void;
|
||||||
|
|
||||||
|
/** Modify body (session id and/or work dir mid-stream) **/
|
||||||
|
updateMessageStreamBody?: (newBody: object) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,6 +151,14 @@ export function useMessageStream({
|
|||||||
fallbackData: initialMessages,
|
fallbackData: initialMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// expose a way to update the body so we can update the session id when CLE occurs
|
||||||
|
const updateMessageStreamBody = useCallback((newBody: object) => {
|
||||||
|
extraMetadataRef.current.body = {
|
||||||
|
...extraMetadataRef.current.body,
|
||||||
|
...newBody,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Keep the latest messages in a ref
|
// Keep the latest messages in a ref
|
||||||
const messagesRef = useRef<Message[]>(messages || []);
|
const messagesRef = useRef<Message[]>(messages || []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -505,5 +516,6 @@ export function useMessageStream({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
isLoading: isLoading || false,
|
isLoading: isLoading || false,
|
||||||
addToolResult,
|
addToolResult,
|
||||||
|
updateMessageStreamBody,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-41
@@ -1,5 +1,6 @@
|
|||||||
import { getApiUrl, getSecretKey } from './config';
|
|
||||||
import { Message } from './types/message';
|
import { Message } from './types/message';
|
||||||
|
import { getSessionHistory, listSessions, SessionInfo } from './api';
|
||||||
|
import { convertApiMessageToFrontendMessage } from './components/context_management';
|
||||||
|
|
||||||
export interface SessionMetadata {
|
export interface SessionMetadata {
|
||||||
description: string;
|
description: string;
|
||||||
@@ -54,36 +55,40 @@ export function generateSessionId(): string {
|
|||||||
* Fetches all available sessions from the API
|
* Fetches all available sessions from the API
|
||||||
* @returns Promise with sessions data
|
* @returns Promise with sessions data
|
||||||
*/
|
*/
|
||||||
export async function fetchSessions(): Promise<SessionsResponse> {
|
/**
|
||||||
|
* Fetches all available sessions from the API
|
||||||
|
* @returns Promise with an array of Session objects
|
||||||
|
*/
|
||||||
|
export async function fetchSessions(): Promise<Session[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getApiUrl('/sessions'), {
|
const response = await listSessions<true>();
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
// Check if the response has the expected structure
|
||||||
throw new Error(`Failed to fetch sessions: ${response.status} ${response.statusText}`);
|
if (response && response.data && response.data.sessions) {
|
||||||
|
// Since the API returns SessionInfo, we need to convert to Session
|
||||||
|
const sessions = response.data.sessions
|
||||||
|
.filter(
|
||||||
|
(sessionInfo: SessionInfo) =>
|
||||||
|
sessionInfo.metadata && sessionInfo.metadata.description !== ''
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(sessionInfo: SessionInfo): Session => ({
|
||||||
|
id: sessionInfo.id,
|
||||||
|
path: sessionInfo.path,
|
||||||
|
modified: sessionInfo.modified,
|
||||||
|
metadata: ensureWorkingDir(sessionInfo.metadata),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// order sessions by 'modified' date descending
|
||||||
|
sessions.sort(
|
||||||
|
(a: Session, b: Session) => new Date(b.modified).getTime() - new Date(a.modified).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
return sessions;
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected response format from listSessions');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove this logic once everyone migrates to the new sessions format
|
|
||||||
// for now, filter out sessions whose description is empty (old CLI sessions)
|
|
||||||
const rawSessions = await response.json();
|
|
||||||
const sessions = rawSessions.sessions
|
|
||||||
.filter((session: Session) => session.metadata.description !== '')
|
|
||||||
.map((session: Session) => ({
|
|
||||||
...session,
|
|
||||||
metadata: ensureWorkingDir(session.metadata),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// order sessions by 'modified' date descending
|
|
||||||
sessions.sort(
|
|
||||||
(a: Session, b: Session) => new Date(b.modified).getTime() - new Date(a.modified).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
return { sessions };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching sessions:', error);
|
console.error('Error fetching sessions:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -97,22 +102,17 @@ export async function fetchSessions(): Promise<SessionsResponse> {
|
|||||||
*/
|
*/
|
||||||
export async function fetchSessionDetails(sessionId: string): Promise<SessionDetails> {
|
export async function fetchSessionDetails(sessionId: string): Promise<SessionDetails> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getApiUrl(`/sessions/${sessionId}`), {
|
const response = await getSessionHistory<true>({
|
||||||
method: 'GET',
|
path: { session_id: sessionId },
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Secret-Key': getSecretKey(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
// Convert the SessionHistoryResponse to a SessionDetails object
|
||||||
throw new Error(`Failed to fetch session details: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const details = await response.json();
|
|
||||||
return {
|
return {
|
||||||
...details,
|
session_id: response.data.sessionId,
|
||||||
metadata: ensureWorkingDir(details.metadata),
|
metadata: ensureWorkingDir(response.data.metadata),
|
||||||
|
messages: response.data.messages.map((message) =>
|
||||||
|
convertApiMessageToFrontendMessage(message, true, true)
|
||||||
|
), // slight diffs between backend and frontend Message obj
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching session details for ${sessionId}:`, error);
|
console.error(`Error fetching session details for ${sessionId}:`, error);
|
||||||
|
|||||||
Reference in New Issue
Block a user