mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
Merge pull request #15 from getcompanion-ai/hari/migrate-openclaw-to-pi-mono
feat: extend GatewayRuntime with session management and API endpoints
This commit is contained in:
commit
cb7e083f9e
2 changed files with 414 additions and 19 deletions
|
|
@ -2913,6 +2913,24 @@ export const MODELS = {
|
||||||
contextWindow: 400000,
|
contextWindow: 400000,
|
||||||
maxTokens: 128000,
|
maxTokens: 128000,
|
||||||
} satisfies Model<"openai-responses">,
|
} satisfies Model<"openai-responses">,
|
||||||
|
"gpt-5.4": {
|
||||||
|
id: "gpt-5.4",
|
||||||
|
name: "GPT-5.4",
|
||||||
|
api: "openai-responses",
|
||||||
|
provider: "github-copilot",
|
||||||
|
baseUrl: "https://api.individual.githubcopilot.com",
|
||||||
|
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 400000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
} satisfies Model<"openai-responses">,
|
||||||
"grok-code-fast-1": {
|
"grok-code-fast-1": {
|
||||||
id: "grok-code-fast-1",
|
id: "grok-code-fast-1",
|
||||||
name: "Grok Code Fast 1",
|
name: "Grok Code Fast 1",
|
||||||
|
|
@ -7598,7 +7616,7 @@ export const MODELS = {
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.27,
|
input: 0.27,
|
||||||
output: 0.95,
|
output: 0.95,
|
||||||
cacheRead: 0.0299999997,
|
cacheRead: 0.0290000007,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 196608,
|
contextWindow: 196608,
|
||||||
|
|
@ -9398,13 +9416,13 @@ export const MODELS = {
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0,
|
input: 0.11,
|
||||||
output: 0,
|
output: 0.6,
|
||||||
cacheRead: 0,
|
cacheRead: 0.055,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 131072,
|
contextWindow: 262144,
|
||||||
maxTokens: 4096,
|
maxTokens: 262144,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"qwen/qwen3-30b-a3b": {
|
"qwen/qwen3-30b-a3b": {
|
||||||
id: "qwen/qwen3-30b-a3b",
|
id: "qwen/qwen3-30b-a3b",
|
||||||
|
|
@ -10401,9 +10419,9 @@ export const MODELS = {
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.3,
|
input: 0.38,
|
||||||
output: 1.4,
|
output: 1.9800000000000002,
|
||||||
cacheRead: 0.15,
|
cacheRead: 0.19,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 202752,
|
contextWindow: 202752,
|
||||||
|
|
@ -11448,6 +11466,23 @@ export const MODELS = {
|
||||||
contextWindow: 204800,
|
contextWindow: 204800,
|
||||||
maxTokens: 131000,
|
maxTokens: 131000,
|
||||||
} satisfies Model<"anthropic-messages">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
|
"minimax/minimax-m2.5-highspeed": {
|
||||||
|
id: "minimax/minimax-m2.5-highspeed",
|
||||||
|
name: "MiniMax M2.5 High Speed",
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "vercel-ai-gateway",
|
||||||
|
baseUrl: "https://ai-gateway.vercel.sh",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.6,
|
||||||
|
output: 2.4,
|
||||||
|
cacheRead: 0.03,
|
||||||
|
cacheWrite: 0.375,
|
||||||
|
},
|
||||||
|
contextWindow: 4096,
|
||||||
|
maxTokens: 4096,
|
||||||
|
} satisfies Model<"anthropic-messages">,
|
||||||
"mistral/codestral": {
|
"mistral/codestral": {
|
||||||
id: "mistral/codestral",
|
id: "mistral/codestral",
|
||||||
name: "Mistral Codestral",
|
name: "Mistral Codestral",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { URL } from "node:url";
|
import { URL } from "node:url";
|
||||||
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { ImageContent } from "@mariozechner/pi-ai";
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
import type { AgentSession, AgentSessionEvent } from "./agent-session.js";
|
import type { AgentSession, AgentSessionEvent } from "./agent-session.js";
|
||||||
import { SessionManager } from "./session-manager.js";
|
import { SessionManager } from "./session-manager.js";
|
||||||
|
import type { Settings } from "./settings-manager.js";
|
||||||
import {
|
import {
|
||||||
createVercelStreamListener,
|
createVercelStreamListener,
|
||||||
errorVercelStream,
|
errorVercelStream,
|
||||||
|
|
@ -51,6 +53,35 @@ export interface GatewaySessionSnapshot {
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
lastActiveAt: number;
|
lastActiveAt: number;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
name?: string;
|
||||||
|
lastMessagePreview?: string;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelInfo {
|
||||||
|
provider: string;
|
||||||
|
modelId: string;
|
||||||
|
displayName: string;
|
||||||
|
capabilities?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HistoryMessage {
|
||||||
|
id: string;
|
||||||
|
role: "user" | "assistant" | "toolResult";
|
||||||
|
parts: HistoryPart[];
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HistoryPart =
|
||||||
|
| { type: "text"; text: string }
|
||||||
|
| { type: "reasoning"; text: string }
|
||||||
|
| { type: "tool-invocation"; toolCallId: string; toolName: string; args: unknown; state: string; result?: unknown };
|
||||||
|
|
||||||
|
export interface ChannelStatus {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
connected: boolean;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GatewayRuntimeOptions {
|
export interface GatewayRuntimeOptions {
|
||||||
|
|
@ -101,6 +132,15 @@ interface ManagedGatewaySession {
|
||||||
unsubscribe: () => void;
|
unsubscribe: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HttpError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly statusCode: number,
|
||||||
|
message: string,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let activeGatewayRuntime: GatewayRuntime | null = null;
|
let activeGatewayRuntime: GatewayRuntime | null = null;
|
||||||
|
|
||||||
export function setActiveGatewayRuntime(runtime: GatewayRuntime | null): void {
|
export function setActiveGatewayRuntime(runtime: GatewayRuntime | null): void {
|
||||||
|
|
@ -122,13 +162,22 @@ export class GatewayRuntime {
|
||||||
private server: Server | null = null;
|
private server: Server | null = null;
|
||||||
private idleSweepTimer: NodeJS.Timeout | null = null;
|
private idleSweepTimer: NodeJS.Timeout | null = null;
|
||||||
private ready = false;
|
private ready = false;
|
||||||
|
private logBuffer: string[] = [];
|
||||||
|
private readonly maxLogBuffer = 1000;
|
||||||
|
|
||||||
constructor(options: GatewayRuntimeOptions) {
|
constructor(options: GatewayRuntimeOptions) {
|
||||||
this.config = options.config;
|
this.config = options.config;
|
||||||
this.primarySessionKey = options.primarySessionKey;
|
this.primarySessionKey = options.primarySessionKey;
|
||||||
this.primarySession = options.primarySession;
|
this.primarySession = options.primarySession;
|
||||||
this.createSession = options.createSession;
|
this.createSession = options.createSession;
|
||||||
this.log = options.log ?? (() => {});
|
const originalLog = options.log;
|
||||||
|
this.log = (msg: string) => {
|
||||||
|
this.logBuffer.push(msg);
|
||||||
|
if (this.logBuffer.length > this.maxLogBuffer) {
|
||||||
|
this.logBuffer = this.logBuffer.slice(-this.maxLogBuffer);
|
||||||
|
}
|
||||||
|
originalLog?.(msg);
|
||||||
|
};
|
||||||
this.sessionDirRoot = join(options.primarySession.sessionManager.getSessionDir(), "..", "gateway-sessions");
|
this.sessionDirRoot = join(options.primarySession.sessionManager.getSessionDir(), "..", "gateway-sessions");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,7 +188,10 @@ export class GatewayRuntime {
|
||||||
this.server = createServer((request, response) => {
|
this.server = createServer((request, response) => {
|
||||||
void this.handleHttpRequest(request, response).catch((error) => {
|
void this.handleHttpRequest(request, response).catch((error) => {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
this.writeJson(response, 500, { error: message });
|
const statusCode = error instanceof HttpError ? error.statusCode : 500;
|
||||||
|
if (!response.writableEnded) {
|
||||||
|
this.writeJson(response, statusCode, { error: message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -253,18 +305,20 @@ export class GatewayRuntime {
|
||||||
const managedSession = this.sessions.get(sessionKey);
|
const managedSession = this.sessions.get(sessionKey);
|
||||||
if (!managedSession) return;
|
if (!managedSession) return;
|
||||||
|
|
||||||
|
if (managedSession.processing) {
|
||||||
|
await managedSession.session.abort();
|
||||||
|
}
|
||||||
|
|
||||||
if (sessionKey === this.primarySessionKey) {
|
if (sessionKey === this.primarySessionKey) {
|
||||||
|
this.rejectQueuedMessages(managedSession, "Session reset");
|
||||||
await managedSession.session.newSession();
|
await managedSession.session.newSession();
|
||||||
managedSession.queue.length = 0;
|
|
||||||
managedSession.processing = false;
|
managedSession.processing = false;
|
||||||
managedSession.lastActiveAt = Date.now();
|
managedSession.lastActiveAt = Date.now();
|
||||||
this.emitState(managedSession);
|
this.emitState(managedSession);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (managedSession.processing) {
|
this.rejectQueuedMessages(managedSession, "Session reset");
|
||||||
await managedSession.session.abort();
|
|
||||||
}
|
|
||||||
managedSession.unsubscribe();
|
managedSession.unsubscribe();
|
||||||
managedSession.session.dispose();
|
managedSession.session.dispose();
|
||||||
this.sessions.delete(sessionKey);
|
this.sessions.delete(sessionKey);
|
||||||
|
|
@ -353,6 +407,26 @@ export class GatewayRuntime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getManagedSessionOrThrow(sessionKey: string): ManagedGatewaySession {
|
||||||
|
const managedSession = this.sessions.get(sessionKey);
|
||||||
|
if (!managedSession) {
|
||||||
|
throw new HttpError(404, `Session not found: ${sessionKey}`);
|
||||||
|
}
|
||||||
|
return managedSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
private rejectQueuedMessages(managedSession: ManagedGatewaySession, error: string): void {
|
||||||
|
const queuedMessages = managedSession.queue.splice(0);
|
||||||
|
for (const queuedMessage of queuedMessages) {
|
||||||
|
queuedMessage.resolve({
|
||||||
|
ok: false,
|
||||||
|
response: "",
|
||||||
|
error,
|
||||||
|
sessionKey: managedSession.sessionKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleSessionEvent(managedSession: ManagedGatewaySession, event: AgentSessionEvent): void {
|
private handleSessionEvent(managedSession: ManagedGatewaySession, event: AgentSessionEvent): void {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "turn_start":
|
case "turn_start":
|
||||||
|
|
@ -443,14 +517,40 @@ export class GatewayRuntime {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createSnapshot(managedSession: ManagedGatewaySession): GatewaySessionSnapshot {
|
private createSnapshot(managedSession: ManagedGatewaySession): GatewaySessionSnapshot {
|
||||||
|
const messages = managedSession.session.messages;
|
||||||
|
let lastMessagePreview: string | undefined;
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const msg = messages[i];
|
||||||
|
if (msg.role === "user" || msg.role === "assistant") {
|
||||||
|
const content = (msg as { content: unknown }).content;
|
||||||
|
if (typeof content === "string" && content.length > 0) {
|
||||||
|
lastMessagePreview = content.slice(0, 120);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
for (const part of content) {
|
||||||
|
if (typeof part === "object" && part !== null && (part as { type: string }).type === "text") {
|
||||||
|
const text = (part as { text: string }).text;
|
||||||
|
if (text.length > 0) {
|
||||||
|
lastMessagePreview = text.slice(0, 120);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastMessagePreview) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
sessionKey: managedSession.sessionKey,
|
sessionKey: managedSession.sessionKey,
|
||||||
sessionId: managedSession.session.sessionId,
|
sessionId: managedSession.session.sessionId,
|
||||||
messageCount: managedSession.session.messages.length,
|
messageCount: messages.length,
|
||||||
queueDepth: managedSession.queue.length,
|
queueDepth: managedSession.queue.length,
|
||||||
processing: managedSession.processing,
|
processing: managedSession.processing,
|
||||||
lastActiveAt: managedSession.lastActiveAt,
|
lastActiveAt: managedSession.lastActiveAt,
|
||||||
createdAt: managedSession.createdAt,
|
createdAt: managedSession.createdAt,
|
||||||
|
updatedAt: managedSession.lastActiveAt,
|
||||||
|
lastMessagePreview,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -509,7 +609,40 @@ export class GatewayRuntime {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionMatch = path.match(/^\/sessions\/([^/]+)(?:\/(events|messages|abort|reset|chat))?$/);
|
if (method === "GET" && path === "/models") {
|
||||||
|
const models = await this.handleGetModels();
|
||||||
|
this.writeJson(response, 200, models);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "GET" && path === "/config") {
|
||||||
|
const config = this.getPublicConfig();
|
||||||
|
this.writeJson(response, 200, config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "PATCH" && path === "/config") {
|
||||||
|
const body = await this.readJsonBody(request);
|
||||||
|
await this.handlePatchConfig(body);
|
||||||
|
this.writeJson(response, 200, { ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "GET" && path === "/channels/status") {
|
||||||
|
const status = this.handleGetChannelsStatus();
|
||||||
|
this.writeJson(response, 200, { channels: status });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "GET" && path === "/logs") {
|
||||||
|
const logs = this.handleGetLogs();
|
||||||
|
this.writeJson(response, 200, { logs });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionMatch = path.match(
|
||||||
|
/^\/sessions\/([^/]+)(?:\/(events|messages|abort|reset|chat|history|model|reload))?$/,
|
||||||
|
);
|
||||||
if (!sessionMatch) {
|
if (!sessionMatch) {
|
||||||
this.writeJson(response, 404, { error: "Not found" });
|
this.writeJson(response, 404, { error: "Not found" });
|
||||||
return;
|
return;
|
||||||
|
|
@ -519,11 +652,24 @@ export class GatewayRuntime {
|
||||||
const action = sessionMatch[2];
|
const action = sessionMatch[2];
|
||||||
|
|
||||||
if (!action && method === "GET") {
|
if (!action && method === "GET") {
|
||||||
const session = await this.ensureSession(sessionKey);
|
const session = this.getManagedSessionOrThrow(sessionKey);
|
||||||
this.writeJson(response, 200, { session: this.createSnapshot(session) });
|
this.writeJson(response, 200, { session: this.createSnapshot(session) });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!action && method === "PATCH") {
|
||||||
|
const body = await this.readJsonBody(request);
|
||||||
|
await this.handlePatchSession(sessionKey, body as { name?: string });
|
||||||
|
this.writeJson(response, 200, { ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!action && method === "DELETE") {
|
||||||
|
await this.handleDeleteSession(sessionKey);
|
||||||
|
this.writeJson(response, 200, { ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (action === "events" && method === "GET") {
|
if (action === "events" && method === "GET") {
|
||||||
await this.handleSse(sessionKey, request, response);
|
await this.handleSse(sessionKey, request, response);
|
||||||
return;
|
return;
|
||||||
|
|
@ -551,16 +697,40 @@ export class GatewayRuntime {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "abort" && method === "POST") {
|
if (action === "abort" && method === "POST") {
|
||||||
|
this.getManagedSessionOrThrow(sessionKey);
|
||||||
this.writeJson(response, 200, { ok: this.abortSession(sessionKey) });
|
this.writeJson(response, 200, { ok: this.abortSession(sessionKey) });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "reset" && method === "POST") {
|
if (action === "reset" && method === "POST") {
|
||||||
|
this.getManagedSessionOrThrow(sessionKey);
|
||||||
await this.resetSession(sessionKey);
|
await this.resetSession(sessionKey);
|
||||||
this.writeJson(response, 200, { ok: true });
|
this.writeJson(response, 200, { ok: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === "history" && method === "GET") {
|
||||||
|
const limitParam = url.searchParams.get("limit");
|
||||||
|
const messages = this.handleGetHistory(sessionKey, limitParam ? parseInt(limitParam, 10) : undefined);
|
||||||
|
this.writeJson(response, 200, { messages });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "model" && method === "POST") {
|
||||||
|
const body = await this.readJsonBody(request);
|
||||||
|
const provider = typeof body.provider === "string" ? body.provider : "";
|
||||||
|
const modelId = typeof body.modelId === "string" ? body.modelId : "";
|
||||||
|
const result = await this.handleSetModel(sessionKey, provider, modelId);
|
||||||
|
this.writeJson(response, 200, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "reload" && method === "POST") {
|
||||||
|
await this.handleReloadSession(sessionKey);
|
||||||
|
this.writeJson(response, 200, { ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.writeJson(response, 405, { error: "Method not allowed" });
|
this.writeJson(response, 405, { error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -705,7 +875,11 @@ export class GatewayRuntime {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const body = Buffer.concat(chunks).toString("utf8");
|
const body = Buffer.concat(chunks).toString("utf8");
|
||||||
return JSON.parse(body) as Record<string, unknown>;
|
try {
|
||||||
|
return JSON.parse(body) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
throw new HttpError(400, "Invalid JSON body");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private writeJson(response: ServerResponse, statusCode: number, payload: unknown): void {
|
private writeJson(response: ServerResponse, statusCode: number, payload: unknown): void {
|
||||||
|
|
@ -714,6 +888,192 @@ export class GatewayRuntime {
|
||||||
response.end(JSON.stringify(payload));
|
response.end(JSON.stringify(payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// New handler methods added for companion-cloud web app integration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private async handleGetModels(): Promise<{
|
||||||
|
models: ModelInfo[];
|
||||||
|
current: { provider: string; modelId: string } | null;
|
||||||
|
}> {
|
||||||
|
const available = this.primarySession.modelRegistry.getAvailable();
|
||||||
|
const models: ModelInfo[] = available.map((m) => ({
|
||||||
|
provider: m.provider,
|
||||||
|
modelId: m.id,
|
||||||
|
displayName: m.name,
|
||||||
|
capabilities: [...(m.reasoning ? ["reasoning"] : []), ...(m.input.includes("image") ? ["vision"] : [])],
|
||||||
|
}));
|
||||||
|
const currentModel = this.primarySession.model;
|
||||||
|
const current = currentModel ? { provider: currentModel.provider, modelId: currentModel.id } : null;
|
||||||
|
return { models, current };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSetModel(
|
||||||
|
sessionKey: string,
|
||||||
|
provider: string,
|
||||||
|
modelId: string,
|
||||||
|
): Promise<{ ok: true; model: { provider: string; modelId: string } }> {
|
||||||
|
const managed = this.getManagedSessionOrThrow(sessionKey);
|
||||||
|
const found = managed.session.modelRegistry.find(provider, modelId);
|
||||||
|
if (!found) {
|
||||||
|
throw new HttpError(404, `Model not found: ${provider}/${modelId}`);
|
||||||
|
}
|
||||||
|
await managed.session.setModel(found);
|
||||||
|
return { ok: true, model: { provider, modelId } };
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetHistory(sessionKey: string, limit?: number): HistoryMessage[] {
|
||||||
|
if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
|
||||||
|
throw new HttpError(400, "History limit must be a positive integer");
|
||||||
|
}
|
||||||
|
const managed = this.getManagedSessionOrThrow(sessionKey);
|
||||||
|
const rawMessages = managed.session.messages;
|
||||||
|
const messages: HistoryMessage[] = [];
|
||||||
|
for (const msg of rawMessages) {
|
||||||
|
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "toolResult") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
messages.push({
|
||||||
|
id: `${msg.timestamp}-${msg.role}`,
|
||||||
|
role: msg.role,
|
||||||
|
parts: this.messageContentToParts(msg),
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return limit ? messages.slice(-limit) : messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePatchSession(sessionKey: string, patch: { name?: string }): Promise<void> {
|
||||||
|
const managed = this.getManagedSessionOrThrow(sessionKey);
|
||||||
|
if (patch.name !== undefined) {
|
||||||
|
// Labels in pi-mono are per-entry; we label the current leaf entry
|
||||||
|
const leafId = managed.session.sessionManager.getLeafId();
|
||||||
|
if (!leafId) {
|
||||||
|
throw new HttpError(409, `Cannot rename session without an active leaf entry: ${sessionKey}`);
|
||||||
|
}
|
||||||
|
managed.session.sessionManager.appendLabelChange(leafId, patch.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleDeleteSession(sessionKey: string): Promise<void> {
|
||||||
|
if (sessionKey === this.primarySessionKey) {
|
||||||
|
throw new HttpError(400, "Cannot delete primary session");
|
||||||
|
}
|
||||||
|
const managed = this.getManagedSessionOrThrow(sessionKey);
|
||||||
|
if (managed.processing) {
|
||||||
|
await managed.session.abort();
|
||||||
|
}
|
||||||
|
this.rejectQueuedMessages(managed, `Session deleted: ${sessionKey}`);
|
||||||
|
managed.unsubscribe();
|
||||||
|
managed.session.dispose();
|
||||||
|
this.sessions.delete(sessionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPublicConfig(): Record<string, unknown> {
|
||||||
|
const settings = this.primarySession.settingsManager.getGlobalSettings();
|
||||||
|
const { gateway, ...rest } = settings as Record<string, unknown> & { gateway?: Record<string, unknown> };
|
||||||
|
const { bearerToken: _bearerToken, webhook, ...safeGatewayRest } = gateway ?? {};
|
||||||
|
const { secret: _secret, ...safeWebhook } =
|
||||||
|
webhook && typeof webhook === "object" ? (webhook as Record<string, unknown>) : {};
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
gateway: {
|
||||||
|
...safeGatewayRest,
|
||||||
|
...(webhook && typeof webhook === "object" ? { webhook: safeWebhook } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePatchConfig(patch: Record<string, unknown>): Promise<void> {
|
||||||
|
// Apply overrides on top of current settings (in-memory only for daemon use)
|
||||||
|
this.primarySession.settingsManager.applyOverrides(patch as Settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetChannelsStatus(): ChannelStatus[] {
|
||||||
|
// Extension channel status is not currently exposed as a public API on AgentSession.
|
||||||
|
// Return empty array as a safe default.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetLogs(): string[] {
|
||||||
|
return this.logBuffer.slice(-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleReloadSession(sessionKey: string): Promise<void> {
|
||||||
|
const managed = this.getManagedSessionOrThrow(sessionKey);
|
||||||
|
// Reloading config by calling settingsManager.reload() on the session
|
||||||
|
managed.session.settingsManager.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private messageContentToParts(msg: AgentMessage): HistoryPart[] {
|
||||||
|
if (msg.role === "user") {
|
||||||
|
const content = msg.content;
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return [{ type: "text", text: content }];
|
||||||
|
}
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
return content
|
||||||
|
.filter(
|
||||||
|
(c): c is { type: "text"; text: string } => typeof c === "object" && c !== null && c.type === "text",
|
||||||
|
)
|
||||||
|
.map((c) => ({ type: "text" as const, text: c.text }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.role === "assistant") {
|
||||||
|
const content = msg.content;
|
||||||
|
if (!Array.isArray(content)) return [];
|
||||||
|
const parts: HistoryPart[] = [];
|
||||||
|
for (const c of content) {
|
||||||
|
if (typeof c !== "object" || c === null) continue;
|
||||||
|
if (c.type === "text") {
|
||||||
|
parts.push({ type: "text", text: (c as { type: "text"; text: string }).text });
|
||||||
|
} else if (c.type === "thinking") {
|
||||||
|
parts.push({ type: "reasoning", text: (c as { type: "thinking"; thinking: string }).thinking });
|
||||||
|
} else if (c.type === "toolCall") {
|
||||||
|
const tc = c as { type: "toolCall"; id: string; name: string; arguments: unknown };
|
||||||
|
parts.push({
|
||||||
|
type: "tool-invocation",
|
||||||
|
toolCallId: tc.id,
|
||||||
|
toolName: tc.name,
|
||||||
|
args: tc.arguments,
|
||||||
|
state: "call",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.role === "toolResult") {
|
||||||
|
const tr = msg as {
|
||||||
|
role: "toolResult";
|
||||||
|
toolCallId: string;
|
||||||
|
toolName: string;
|
||||||
|
content: unknown;
|
||||||
|
isError: boolean;
|
||||||
|
};
|
||||||
|
const textParts = Array.isArray(tr.content)
|
||||||
|
? (tr.content as { type: string; text?: string }[])
|
||||||
|
.filter((c) => c.type === "text" && typeof c.text === "string")
|
||||||
|
.map((c) => c.text as string)
|
||||||
|
.join("")
|
||||||
|
: "";
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "tool-invocation",
|
||||||
|
toolCallId: tr.toolCallId,
|
||||||
|
toolName: tr.toolName,
|
||||||
|
args: undefined,
|
||||||
|
state: tr.isError ? "error" : "result",
|
||||||
|
result: textParts,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
getGatewaySessionDir(sessionKey: string): string {
|
getGatewaySessionDir(sessionKey: string): string {
|
||||||
return join(this.sessionDirRoot, sanitizeSessionKey(sessionKey));
|
return join(this.sessionDirRoot, sanitizeSessionKey(sessionKey));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue