mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-21 16:01:09 +00:00
feat: persist chat threads and recover gateway sessions
Add a Convex-backed durable chat layer for companion threads, recover gateway sessions from persisted Pi session files after eviction, and start splitting gateway runtime internals into focused modules. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
8ecd55a522
commit
753cb935f1
5 changed files with 265 additions and 212 deletions
29
packages/coding-agent/src/core/gateway-runtime-helpers.ts
Normal file
29
packages/coding-agent/src/core/gateway-runtime-helpers.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { AgentSession } from "./agent-session.js";
|
||||||
|
|
||||||
|
export function extractMessageText(message: { content: unknown }): string {
|
||||||
|
if (!Array.isArray(message.content)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return message.content
|
||||||
|
.filter((part): part is { type: "text"; text: string } => {
|
||||||
|
return (
|
||||||
|
typeof part === "object" &&
|
||||||
|
part !== null &&
|
||||||
|
"type" in part &&
|
||||||
|
"text" in part &&
|
||||||
|
part.type === "text"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((part) => part.text)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLastAssistantText(session: AgentSession): string {
|
||||||
|
for (let index = session.messages.length - 1; index >= 0; index--) {
|
||||||
|
const message = session.messages[index];
|
||||||
|
if (message.role === "assistant") {
|
||||||
|
return extractMessageText(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import type { AgentSession } from "./agent-session.js";
|
||||||
|
import type {
|
||||||
|
GatewayMessageRequest,
|
||||||
|
GatewayMessageResult,
|
||||||
|
GatewaySessionSnapshot,
|
||||||
|
} from "./gateway-runtime-types.js";
|
||||||
|
|
||||||
|
export interface GatewayQueuedMessage {
|
||||||
|
request: GatewayMessageRequest;
|
||||||
|
resolve: (result: GatewayMessageResult) => void;
|
||||||
|
onStart?: () => void;
|
||||||
|
onFinish?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GatewayEvent =
|
||||||
|
| { type: "hello"; sessionKey: string; snapshot: GatewaySessionSnapshot }
|
||||||
|
| {
|
||||||
|
type: "session_state";
|
||||||
|
sessionKey: string;
|
||||||
|
snapshot: GatewaySessionSnapshot;
|
||||||
|
}
|
||||||
|
| { type: "turn_start"; sessionKey: string }
|
||||||
|
| { type: "turn_end"; sessionKey: string }
|
||||||
|
| { type: "message_start"; sessionKey: string; role?: string }
|
||||||
|
| { type: "token"; sessionKey: string; delta: string; contentIndex: number }
|
||||||
|
| {
|
||||||
|
type: "thinking";
|
||||||
|
sessionKey: string;
|
||||||
|
delta: string;
|
||||||
|
contentIndex: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "tool_start";
|
||||||
|
sessionKey: string;
|
||||||
|
toolCallId: string;
|
||||||
|
toolName: string;
|
||||||
|
args: unknown;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "tool_update";
|
||||||
|
sessionKey: string;
|
||||||
|
toolCallId: string;
|
||||||
|
toolName: string;
|
||||||
|
partialResult: unknown;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "tool_complete";
|
||||||
|
sessionKey: string;
|
||||||
|
toolCallId: string;
|
||||||
|
toolName: string;
|
||||||
|
result: unknown;
|
||||||
|
isError: boolean;
|
||||||
|
}
|
||||||
|
| { type: "message_complete"; sessionKey: string; text: string }
|
||||||
|
| { type: "error"; sessionKey: string; error: string }
|
||||||
|
| { type: "aborted"; sessionKey: string };
|
||||||
|
|
||||||
|
export interface ManagedGatewaySession {
|
||||||
|
sessionKey: string;
|
||||||
|
session: AgentSession;
|
||||||
|
queue: GatewayQueuedMessage[];
|
||||||
|
processing: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
lastActiveAt: number;
|
||||||
|
listeners: Set<(event: GatewayEvent) => void>;
|
||||||
|
unsubscribe: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly statusCode: number,
|
||||||
|
message: string,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
packages/coding-agent/src/core/gateway-runtime-types.ts
Normal file
90
packages/coding-agent/src/core/gateway-runtime-types.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import type { AgentSession } from "./agent-session.js";
|
||||||
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
|
export interface GatewayConfig {
|
||||||
|
bind: string;
|
||||||
|
port: number;
|
||||||
|
bearerToken?: string;
|
||||||
|
session: {
|
||||||
|
idleMinutes: number;
|
||||||
|
maxQueuePerSession: number;
|
||||||
|
};
|
||||||
|
webhook: {
|
||||||
|
enabled: boolean;
|
||||||
|
basePath: string;
|
||||||
|
secret?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GatewaySessionFactory = (
|
||||||
|
sessionKey: string,
|
||||||
|
) => Promise<AgentSession>;
|
||||||
|
|
||||||
|
export interface GatewayMessageRequest {
|
||||||
|
sessionKey: string;
|
||||||
|
text: string;
|
||||||
|
source?: "interactive" | "rpc" | "extension";
|
||||||
|
images?: ImageContent[];
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GatewayMessageResult {
|
||||||
|
ok: boolean;
|
||||||
|
response: string;
|
||||||
|
error?: string;
|
||||||
|
sessionKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GatewaySessionSnapshot {
|
||||||
|
sessionKey: string;
|
||||||
|
sessionId: string;
|
||||||
|
messageCount: number;
|
||||||
|
queueDepth: number;
|
||||||
|
processing: boolean;
|
||||||
|
lastActiveAt: 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 {
|
||||||
|
config: GatewayConfig;
|
||||||
|
primarySessionKey: string;
|
||||||
|
primarySession: AgentSession;
|
||||||
|
createSession: GatewaySessionFactory;
|
||||||
|
log?: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
@ -4,12 +4,34 @@ import {
|
||||||
type Server,
|
type Server,
|
||||||
type ServerResponse,
|
type ServerResponse,
|
||||||
} from "node:http";
|
} from "node:http";
|
||||||
|
import { rm } from "node:fs/promises";
|
||||||
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 { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
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 {
|
||||||
|
extractMessageText,
|
||||||
|
getLastAssistantText,
|
||||||
|
} from "./gateway-runtime-helpers.js";
|
||||||
|
import {
|
||||||
|
type GatewayEvent,
|
||||||
|
type GatewayQueuedMessage,
|
||||||
|
HttpError,
|
||||||
|
type ManagedGatewaySession,
|
||||||
|
} from "./gateway-runtime-internal-types.js";
|
||||||
|
import { sanitizeSessionKey } from "./gateway-session-manager.js";
|
||||||
|
import type {
|
||||||
|
ChannelStatus,
|
||||||
|
GatewayConfig,
|
||||||
|
GatewayMessageRequest,
|
||||||
|
GatewayMessageResult,
|
||||||
|
GatewayRuntimeOptions,
|
||||||
|
GatewaySessionFactory,
|
||||||
|
GatewaySessionSnapshot,
|
||||||
|
HistoryMessage,
|
||||||
|
HistoryPart,
|
||||||
|
ModelInfo,
|
||||||
|
} from "./gateway-runtime-types.js";
|
||||||
import type { Settings } from "./settings-manager.js";
|
import type { Settings } from "./settings-manager.js";
|
||||||
import {
|
import {
|
||||||
createVercelStreamListener,
|
createVercelStreamListener,
|
||||||
|
|
@ -17,164 +39,22 @@ import {
|
||||||
extractUserText,
|
extractUserText,
|
||||||
finishVercelStream,
|
finishVercelStream,
|
||||||
} from "./vercel-ai-stream.js";
|
} from "./vercel-ai-stream.js";
|
||||||
|
export {
|
||||||
export interface GatewayConfig {
|
createGatewaySessionManager,
|
||||||
bind: string;
|
sanitizeSessionKey,
|
||||||
port: number;
|
} from "./gateway-session-manager.js";
|
||||||
bearerToken?: string;
|
export type {
|
||||||
session: {
|
ChannelStatus,
|
||||||
idleMinutes: number;
|
GatewayConfig,
|
||||||
maxQueuePerSession: number;
|
GatewayMessageRequest,
|
||||||
};
|
GatewayMessageResult,
|
||||||
webhook: {
|
GatewayRuntimeOptions,
|
||||||
enabled: boolean;
|
GatewaySessionFactory,
|
||||||
basePath: string;
|
GatewaySessionSnapshot,
|
||||||
secret?: string;
|
HistoryMessage,
|
||||||
};
|
HistoryPart,
|
||||||
}
|
ModelInfo,
|
||||||
|
} from "./gateway-runtime-types.js";
|
||||||
export type GatewaySessionFactory = (
|
|
||||||
sessionKey: string,
|
|
||||||
) => Promise<AgentSession>;
|
|
||||||
|
|
||||||
export interface GatewayMessageRequest {
|
|
||||||
sessionKey: string;
|
|
||||||
text: string;
|
|
||||||
source?: "interactive" | "rpc" | "extension";
|
|
||||||
images?: ImageContent[];
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GatewayMessageResult {
|
|
||||||
ok: boolean;
|
|
||||||
response: string;
|
|
||||||
error?: string;
|
|
||||||
sessionKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GatewaySessionSnapshot {
|
|
||||||
sessionKey: string;
|
|
||||||
sessionId: string;
|
|
||||||
messageCount: number;
|
|
||||||
queueDepth: number;
|
|
||||||
processing: boolean;
|
|
||||||
lastActiveAt: 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 {
|
|
||||||
config: GatewayConfig;
|
|
||||||
primarySessionKey: string;
|
|
||||||
primarySession: AgentSession;
|
|
||||||
createSession: GatewaySessionFactory;
|
|
||||||
log?: (message: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GatewayQueuedMessage {
|
|
||||||
request: GatewayMessageRequest;
|
|
||||||
resolve: (result: GatewayMessageResult) => void;
|
|
||||||
onStart?: () => void;
|
|
||||||
onFinish?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type GatewayEvent =
|
|
||||||
| { type: "hello"; sessionKey: string; snapshot: GatewaySessionSnapshot }
|
|
||||||
| {
|
|
||||||
type: "session_state";
|
|
||||||
sessionKey: string;
|
|
||||||
snapshot: GatewaySessionSnapshot;
|
|
||||||
}
|
|
||||||
| { type: "turn_start"; sessionKey: string }
|
|
||||||
| { type: "turn_end"; sessionKey: string }
|
|
||||||
| { type: "message_start"; sessionKey: string; role?: string }
|
|
||||||
| { type: "token"; sessionKey: string; delta: string; contentIndex: number }
|
|
||||||
| {
|
|
||||||
type: "thinking";
|
|
||||||
sessionKey: string;
|
|
||||||
delta: string;
|
|
||||||
contentIndex: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "tool_start";
|
|
||||||
sessionKey: string;
|
|
||||||
toolCallId: string;
|
|
||||||
toolName: string;
|
|
||||||
args: unknown;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "tool_update";
|
|
||||||
sessionKey: string;
|
|
||||||
toolCallId: string;
|
|
||||||
toolName: string;
|
|
||||||
partialResult: unknown;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "tool_complete";
|
|
||||||
sessionKey: string;
|
|
||||||
toolCallId: string;
|
|
||||||
toolName: string;
|
|
||||||
result: unknown;
|
|
||||||
isError: boolean;
|
|
||||||
}
|
|
||||||
| { type: "message_complete"; sessionKey: string; text: string }
|
|
||||||
| { type: "error"; sessionKey: string; error: string }
|
|
||||||
| { type: "aborted"; sessionKey: string };
|
|
||||||
|
|
||||||
interface ManagedGatewaySession {
|
|
||||||
sessionKey: string;
|
|
||||||
session: AgentSession;
|
|
||||||
queue: GatewayQueuedMessage[];
|
|
||||||
processing: boolean;
|
|
||||||
createdAt: number;
|
|
||||||
lastActiveAt: number;
|
|
||||||
listeners: Set<(event: GatewayEvent) => 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;
|
||||||
|
|
||||||
|
|
@ -759,7 +639,7 @@ export class GatewayRuntime {
|
||||||
const action = sessionMatch[2];
|
const action = sessionMatch[2];
|
||||||
|
|
||||||
if (!action && method === "GET") {
|
if (!action && method === "GET") {
|
||||||
const session = this.getManagedSessionOrThrow(sessionKey);
|
const session = await this.ensureSession(sessionKey);
|
||||||
this.writeJson(response, 200, { session: this.createSnapshot(session) });
|
this.writeJson(response, 200, { session: this.createSnapshot(session) });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -818,7 +698,7 @@ export class GatewayRuntime {
|
||||||
|
|
||||||
if (action === "history" && method === "GET") {
|
if (action === "history" && method === "GET") {
|
||||||
const limitParam = url.searchParams.get("limit");
|
const limitParam = url.searchParams.get("limit");
|
||||||
const messages = this.handleGetHistory(
|
const messages = await this.handleGetHistory(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
limitParam ? parseInt(limitParam, 10) : undefined,
|
limitParam ? parseInt(limitParam, 10) : undefined,
|
||||||
);
|
);
|
||||||
|
|
@ -1067,7 +947,7 @@ export class GatewayRuntime {
|
||||||
provider: string,
|
provider: string,
|
||||||
modelId: string,
|
modelId: string,
|
||||||
): Promise<{ ok: true; model: { provider: string; modelId: string } }> {
|
): Promise<{ ok: true; model: { provider: string; modelId: string } }> {
|
||||||
const managed = this.getManagedSessionOrThrow(sessionKey);
|
const managed = await this.ensureSession(sessionKey);
|
||||||
const found = managed.session.modelRegistry.find(provider, modelId);
|
const found = managed.session.modelRegistry.find(provider, modelId);
|
||||||
if (!found) {
|
if (!found) {
|
||||||
throw new HttpError(404, `Model not found: ${provider}/${modelId}`);
|
throw new HttpError(404, `Model not found: ${provider}/${modelId}`);
|
||||||
|
|
@ -1076,14 +956,14 @@ export class GatewayRuntime {
|
||||||
return { ok: true, model: { provider, modelId } };
|
return { ok: true, model: { provider, modelId } };
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleGetHistory(
|
private async handleGetHistory(
|
||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
): HistoryMessage[] {
|
): Promise<HistoryMessage[]> {
|
||||||
if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
|
if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
|
||||||
throw new HttpError(400, "History limit must be a positive integer");
|
throw new HttpError(400, "History limit must be a positive integer");
|
||||||
}
|
}
|
||||||
const managed = this.getManagedSessionOrThrow(sessionKey);
|
const managed = await this.ensureSession(sessionKey);
|
||||||
const rawMessages = managed.session.messages;
|
const rawMessages = managed.session.messages;
|
||||||
const messages: HistoryMessage[] = [];
|
const messages: HistoryMessage[] = [];
|
||||||
for (const msg of rawMessages) {
|
for (const msg of rawMessages) {
|
||||||
|
|
@ -1108,7 +988,7 @@ export class GatewayRuntime {
|
||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
patch: { name?: string },
|
patch: { name?: string },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const managed = this.getManagedSessionOrThrow(sessionKey);
|
const managed = await this.ensureSession(sessionKey);
|
||||||
if (patch.name !== undefined) {
|
if (patch.name !== undefined) {
|
||||||
// Labels in pi-mono are per-entry; we label the current leaf entry
|
// Labels in pi-mono are per-entry; we label the current leaf entry
|
||||||
const leafId = managed.session.sessionManager.getLeafId();
|
const leafId = managed.session.sessionManager.getLeafId();
|
||||||
|
|
@ -1126,7 +1006,7 @@ export class GatewayRuntime {
|
||||||
if (sessionKey === this.primarySessionKey) {
|
if (sessionKey === this.primarySessionKey) {
|
||||||
throw new HttpError(400, "Cannot delete primary session");
|
throw new HttpError(400, "Cannot delete primary session");
|
||||||
}
|
}
|
||||||
const managed = this.getManagedSessionOrThrow(sessionKey);
|
const managed = await this.ensureSession(sessionKey);
|
||||||
if (managed.processing) {
|
if (managed.processing) {
|
||||||
await managed.session.abort();
|
await managed.session.abort();
|
||||||
}
|
}
|
||||||
|
|
@ -1134,6 +1014,10 @@ export class GatewayRuntime {
|
||||||
managed.unsubscribe();
|
managed.unsubscribe();
|
||||||
managed.session.dispose();
|
managed.session.dispose();
|
||||||
this.sessions.delete(sessionKey);
|
this.sessions.delete(sessionKey);
|
||||||
|
await rm(this.getGatewaySessionDir(sessionKey), {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
}).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPublicConfig(): Record<string, unknown> {
|
private getPublicConfig(): Record<string, unknown> {
|
||||||
|
|
@ -1179,7 +1063,7 @@ export class GatewayRuntime {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleReloadSession(sessionKey: string): Promise<void> {
|
private async handleReloadSession(sessionKey: string): Promise<void> {
|
||||||
const managed = this.getManagedSessionOrThrow(sessionKey);
|
const managed = await this.ensureSession(sessionKey);
|
||||||
// Reloading config by calling settingsManager.reload() on the session
|
// Reloading config by calling settingsManager.reload() on the session
|
||||||
managed.session.settingsManager.reload();
|
managed.session.settingsManager.reload();
|
||||||
}
|
}
|
||||||
|
|
@ -1269,46 +1153,3 @@ export class GatewayRuntime {
|
||||||
return join(this.sessionDirRoot, sanitizeSessionKey(sessionKey));
|
return join(this.sessionDirRoot, sanitizeSessionKey(sessionKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractMessageText(message: { content: unknown }): string {
|
|
||||||
if (!Array.isArray(message.content)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return message.content
|
|
||||||
.filter((part): part is { type: "text"; text: string } => {
|
|
||||||
return (
|
|
||||||
typeof part === "object" &&
|
|
||||||
part !== null &&
|
|
||||||
"type" in part &&
|
|
||||||
"text" in part &&
|
|
||||||
part.type === "text"
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((part) => part.text)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastAssistantText(session: AgentSession): string {
|
|
||||||
for (let index = session.messages.length - 1; index >= 0; index--) {
|
|
||||||
const message = session.messages[index];
|
|
||||||
if (message.role === "assistant") {
|
|
||||||
return extractMessageText(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sanitizeSessionKey(sessionKey: string): string {
|
|
||||||
return sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createGatewaySessionManager(
|
|
||||||
cwd: string,
|
|
||||||
sessionKey: string,
|
|
||||||
sessionDirRoot: string,
|
|
||||||
): SessionManager {
|
|
||||||
return SessionManager.create(
|
|
||||||
cwd,
|
|
||||||
join(sessionDirRoot, sanitizeSessionKey(sessionKey)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
17
packages/coding-agent/src/core/gateway-session-manager.ts
Normal file
17
packages/coding-agent/src/core/gateway-session-manager.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { SessionManager } from "./session-manager.js";
|
||||||
|
|
||||||
|
export function sanitizeSessionKey(sessionKey: string): string {
|
||||||
|
return sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGatewaySessionManager(
|
||||||
|
cwd: string,
|
||||||
|
sessionKey: string,
|
||||||
|
sessionDirRoot: string,
|
||||||
|
): SessionManager {
|
||||||
|
return SessionManager.continueRecent(
|
||||||
|
cwd,
|
||||||
|
join(sessionDirRoot, sanitizeSessionKey(sessionKey)),
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue