refactor: split acp mappers
This commit is contained in:
parent
41fbcc405f
commit
3bd7615c4f
34
src/acp/event-mapper.test.ts
Normal file
34
src/acp/event-mapper.test.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js";
|
||||||
|
|
||||||
|
describe("acp event mapper", () => {
|
||||||
|
it("extracts text and resource blocks into prompt text", () => {
|
||||||
|
const text = extractTextFromPrompt([
|
||||||
|
{ type: "text", text: "Hello" },
|
||||||
|
{ type: "resource", resource: { text: "File contents" } },
|
||||||
|
{ type: "resource_link", uri: "https://example.com", title: "Spec" },
|
||||||
|
{ type: "image", data: "abc", mimeType: "image/png" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(text).toBe(
|
||||||
|
"Hello\nFile contents\n[Resource link (Spec)] https://example.com",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts image blocks into gateway attachments", () => {
|
||||||
|
const attachments = extractAttachmentsFromPrompt([
|
||||||
|
{ type: "image", data: "abc", mimeType: "image/png" },
|
||||||
|
{ type: "image", data: "", mimeType: "image/png" },
|
||||||
|
{ type: "text", text: "ignored" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(attachments).toEqual([
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
mimeType: "image/png",
|
||||||
|
content: "abc",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
73
src/acp/event-mapper.ts
Normal file
73
src/acp/event-mapper.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import type { ContentBlock, ImageContent, ToolKind } from "@agentclientprotocol/sdk";
|
||||||
|
|
||||||
|
export type GatewayAttachment = {
|
||||||
|
type: string;
|
||||||
|
mimeType: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function extractTextFromPrompt(prompt: ContentBlock[]): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const block of prompt) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
parts.push(block.text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.type === "resource") {
|
||||||
|
const resource = block.resource as { text?: string } | undefined;
|
||||||
|
if (resource?.text) parts.push(resource.text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.type === "resource_link") {
|
||||||
|
const title = block.title ? ` (${block.title})` : "";
|
||||||
|
const uri = block.uri ?? "";
|
||||||
|
const line = uri ? `[Resource link${title}] ${uri}` : `[Resource link${title}]`;
|
||||||
|
parts.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractAttachmentsFromPrompt(prompt: ContentBlock[]): GatewayAttachment[] {
|
||||||
|
const attachments: GatewayAttachment[] = [];
|
||||||
|
for (const block of prompt) {
|
||||||
|
if (block.type !== "image") continue;
|
||||||
|
const image = block as ImageContent;
|
||||||
|
if (!image.data || !image.mimeType) continue;
|
||||||
|
attachments.push({
|
||||||
|
type: "image",
|
||||||
|
mimeType: image.mimeType,
|
||||||
|
content: image.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatToolTitle(
|
||||||
|
name: string | undefined,
|
||||||
|
args: Record<string, unknown> | undefined,
|
||||||
|
): string {
|
||||||
|
const base = name ?? "tool";
|
||||||
|
if (!args || Object.keys(args).length === 0) return base;
|
||||||
|
const parts = Object.entries(args).map(([key, value]) => {
|
||||||
|
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
||||||
|
const safe = raw.length > 100 ? `${raw.slice(0, 100)}...` : raw;
|
||||||
|
return `${key}: ${safe}`;
|
||||||
|
});
|
||||||
|
return `${base}: ${parts.join(", ")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inferToolKind(name?: string): ToolKind | undefined {
|
||||||
|
if (!name) return "other";
|
||||||
|
const normalized = name.toLowerCase();
|
||||||
|
if (normalized.includes("read")) return "read";
|
||||||
|
if (normalized.includes("write") || normalized.includes("edit")) return "edit";
|
||||||
|
if (normalized.includes("delete") || normalized.includes("remove")) return "delete";
|
||||||
|
if (normalized.includes("move") || normalized.includes("rename")) return "move";
|
||||||
|
if (normalized.includes("search") || normalized.includes("find")) return "search";
|
||||||
|
if (normalized.includes("exec") || normalized.includes("run") || normalized.includes("bash")) {
|
||||||
|
return "execute";
|
||||||
|
}
|
||||||
|
if (normalized.includes("fetch") || normalized.includes("http")) return "fetch";
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
@ -1,2 +1,4 @@
|
|||||||
export { serveAcpGateway } from "./server.js";
|
export { serveAcpGateway } from "./server.js";
|
||||||
|
export { createInMemorySessionStore } from "./session.js";
|
||||||
|
export type { AcpSessionStore } from "./session.js";
|
||||||
export type { AcpServerOptions } from "./types.js";
|
export type { AcpServerOptions } from "./types.js";
|
||||||
|
|||||||
35
src/acp/meta.ts
Normal file
35
src/acp/meta.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export function readString(
|
||||||
|
meta: Record<string, unknown> | null | undefined,
|
||||||
|
keys: string[],
|
||||||
|
): string | undefined {
|
||||||
|
if (!meta) return undefined;
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = meta[key];
|
||||||
|
if (typeof value === "string" && value.trim()) return value.trim();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readBool(
|
||||||
|
meta: Record<string, unknown> | null | undefined,
|
||||||
|
keys: string[],
|
||||||
|
): boolean | undefined {
|
||||||
|
if (!meta) return undefined;
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = meta[key];
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readNumber(
|
||||||
|
meta: Record<string, unknown> | null | undefined,
|
||||||
|
keys: string[],
|
||||||
|
): number | undefined {
|
||||||
|
if (!meta) return undefined;
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = meta[key];
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
57
src/acp/session-mapper.test.ts
Normal file
57
src/acp/session-mapper.test.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { GatewayClient } from "../gateway/client.js";
|
||||||
|
import { parseSessionMeta, resolveSessionKey } from "./session-mapper.js";
|
||||||
|
|
||||||
|
function createGateway(resolveLabelKey = "agent:main:label"): {
|
||||||
|
gateway: GatewayClient;
|
||||||
|
request: ReturnType<typeof vi.fn>;
|
||||||
|
} {
|
||||||
|
const request = vi.fn(async (method: string, params: Record<string, unknown>) => {
|
||||||
|
if (method === "sessions.resolve" && "label" in params) {
|
||||||
|
return { ok: true, key: resolveLabelKey };
|
||||||
|
}
|
||||||
|
if (method === "sessions.resolve" && "key" in params) {
|
||||||
|
return { ok: true, key: params.key as string };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
gateway: { request } as unknown as GatewayClient,
|
||||||
|
request,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("acp session mapper", () => {
|
||||||
|
it("prefers explicit sessionLabel over sessionKey", async () => {
|
||||||
|
const { gateway, request } = createGateway();
|
||||||
|
const meta = parseSessionMeta({ sessionLabel: "support", sessionKey: "agent:main:main" });
|
||||||
|
|
||||||
|
const key = await resolveSessionKey({
|
||||||
|
meta,
|
||||||
|
fallbackKey: "acp:fallback",
|
||||||
|
gateway,
|
||||||
|
opts: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(key).toBe("agent:main:label");
|
||||||
|
expect(request).toHaveBeenCalledTimes(1);
|
||||||
|
expect(request).toHaveBeenCalledWith("sessions.resolve", { label: "support" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets meta sessionKey override default label", async () => {
|
||||||
|
const { gateway, request } = createGateway();
|
||||||
|
const meta = parseSessionMeta({ sessionKey: "agent:main:override" });
|
||||||
|
|
||||||
|
const key = await resolveSessionKey({
|
||||||
|
meta,
|
||||||
|
fallbackKey: "acp:fallback",
|
||||||
|
gateway,
|
||||||
|
opts: { defaultSessionLabel: "default-label" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(key).toBe("agent:main:override");
|
||||||
|
expect(request).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
95
src/acp/session-mapper.ts
Normal file
95
src/acp/session-mapper.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import type { GatewayClient } from "../gateway/client.js";
|
||||||
|
|
||||||
|
import type { AcpServerOptions } from "./types.js";
|
||||||
|
import { readBool, readString } from "./meta.js";
|
||||||
|
|
||||||
|
export type AcpSessionMeta = {
|
||||||
|
sessionKey?: string;
|
||||||
|
sessionLabel?: string;
|
||||||
|
resetSession?: boolean;
|
||||||
|
requireExisting?: boolean;
|
||||||
|
prefixCwd?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseSessionMeta(meta: unknown): AcpSessionMeta {
|
||||||
|
if (!meta || typeof meta !== "object") return {};
|
||||||
|
const record = meta as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
sessionKey: readString(record, ["sessionKey", "session", "key"]),
|
||||||
|
sessionLabel: readString(record, ["sessionLabel", "label"]),
|
||||||
|
resetSession: readBool(record, ["resetSession", "reset"]),
|
||||||
|
requireExisting: readBool(record, ["requireExistingSession", "requireExisting"]),
|
||||||
|
prefixCwd: readBool(record, ["prefixCwd"]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveSessionKey(params: {
|
||||||
|
meta: AcpSessionMeta;
|
||||||
|
fallbackKey: string;
|
||||||
|
gateway: GatewayClient;
|
||||||
|
opts: AcpServerOptions;
|
||||||
|
}): Promise<string> {
|
||||||
|
const requestedLabel = params.meta.sessionLabel ?? params.opts.defaultSessionLabel;
|
||||||
|
const requestedKey = params.meta.sessionKey ?? params.opts.defaultSessionKey;
|
||||||
|
const requireExisting =
|
||||||
|
params.meta.requireExisting ?? params.opts.requireExistingSession ?? false;
|
||||||
|
|
||||||
|
if (params.meta.sessionLabel) {
|
||||||
|
const resolved = await params.gateway.request<{ ok: true; key: string }>(
|
||||||
|
"sessions.resolve",
|
||||||
|
{ label: params.meta.sessionLabel },
|
||||||
|
);
|
||||||
|
if (!resolved?.key) {
|
||||||
|
throw new Error(`Unable to resolve session label: ${params.meta.sessionLabel}`);
|
||||||
|
}
|
||||||
|
return resolved.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.meta.sessionKey) {
|
||||||
|
if (!requireExisting) return params.meta.sessionKey;
|
||||||
|
const resolved = await params.gateway.request<{ ok: true; key: string }>(
|
||||||
|
"sessions.resolve",
|
||||||
|
{ key: params.meta.sessionKey },
|
||||||
|
);
|
||||||
|
if (!resolved?.key) {
|
||||||
|
throw new Error(`Session key not found: ${params.meta.sessionKey}`);
|
||||||
|
}
|
||||||
|
return resolved.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedLabel) {
|
||||||
|
const resolved = await params.gateway.request<{ ok: true; key: string }>(
|
||||||
|
"sessions.resolve",
|
||||||
|
{ label: requestedLabel },
|
||||||
|
);
|
||||||
|
if (!resolved?.key) {
|
||||||
|
throw new Error(`Unable to resolve session label: ${requestedLabel}`);
|
||||||
|
}
|
||||||
|
return resolved.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedKey) {
|
||||||
|
if (!requireExisting) return requestedKey;
|
||||||
|
const resolved = await params.gateway.request<{ ok: true; key: string }>(
|
||||||
|
"sessions.resolve",
|
||||||
|
{ key: requestedKey },
|
||||||
|
);
|
||||||
|
if (!resolved?.key) {
|
||||||
|
throw new Error(`Session key not found: ${requestedKey}`);
|
||||||
|
}
|
||||||
|
return resolved.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.fallbackKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetSessionIfNeeded(params: {
|
||||||
|
meta: AcpSessionMeta;
|
||||||
|
sessionKey: string;
|
||||||
|
gateway: GatewayClient;
|
||||||
|
opts: AcpServerOptions;
|
||||||
|
}): Promise<void> {
|
||||||
|
const resetSession = params.meta.resetSession ?? params.opts.resetSession ?? false;
|
||||||
|
if (!resetSession) return;
|
||||||
|
await params.gateway.request("sessions.reset", { key: params.sessionKey });
|
||||||
|
}
|
||||||
@ -1,30 +1,26 @@
|
|||||||
import { describe, expect, it, afterEach } from "vitest";
|
import { describe, expect, it, afterEach } from "vitest";
|
||||||
|
|
||||||
import {
|
import { createInMemorySessionStore } from "./session.js";
|
||||||
cancelActiveRun,
|
|
||||||
clearAllSessionsForTest,
|
|
||||||
createSession,
|
|
||||||
getSessionByRunId,
|
|
||||||
setActiveRun,
|
|
||||||
} from "./session.js";
|
|
||||||
|
|
||||||
describe("acp session manager", () => {
|
describe("acp session manager", () => {
|
||||||
|
const store = createInMemorySessionStore();
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
clearAllSessionsForTest();
|
store.clearAllSessionsForTest();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tracks active runs and clears on cancel", () => {
|
it("tracks active runs and clears on cancel", () => {
|
||||||
const session = createSession({
|
const session = store.createSession({
|
||||||
sessionKey: "acp:test",
|
sessionKey: "acp:test",
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
});
|
});
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
setActiveRun(session.sessionId, "run-1", controller);
|
store.setActiveRun(session.sessionId, "run-1", controller);
|
||||||
|
|
||||||
expect(getSessionByRunId("run-1")?.sessionId).toBe(session.sessionId);
|
expect(store.getSessionByRunId("run-1")?.sessionId).toBe(session.sessionId);
|
||||||
|
|
||||||
const cancelled = cancelActiveRun(session.sessionId);
|
const cancelled = store.cancelActiveRun(session.sessionId);
|
||||||
expect(cancelled).toBe(true);
|
expect(cancelled).toBe(true);
|
||||||
expect(getSessionByRunId("run-1")).toBeUndefined();
|
expect(store.getSessionByRunId("run-1")).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,70 +2,92 @@ import { randomUUID } from "node:crypto";
|
|||||||
|
|
||||||
import type { AcpSession } from "./types.js";
|
import type { AcpSession } from "./types.js";
|
||||||
|
|
||||||
const sessions = new Map<string, AcpSession>();
|
export type AcpSessionStore = {
|
||||||
const runIdToSessionId = new Map<string, string>();
|
createSession: (params: {
|
||||||
|
sessionKey: string;
|
||||||
|
cwd: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}) => AcpSession;
|
||||||
|
getSession: (sessionId: string) => AcpSession | undefined;
|
||||||
|
getSessionByRunId: (runId: string) => AcpSession | undefined;
|
||||||
|
setActiveRun: (sessionId: string, runId: string, abortController: AbortController) => void;
|
||||||
|
clearActiveRun: (sessionId: string) => void;
|
||||||
|
cancelActiveRun: (sessionId: string) => boolean;
|
||||||
|
clearAllSessionsForTest: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export function createSession(params: {
|
export function createInMemorySessionStore(): AcpSessionStore {
|
||||||
sessionKey: string;
|
const sessions = new Map<string, AcpSession>();
|
||||||
cwd: string;
|
const runIdToSessionId = new Map<string, string>();
|
||||||
sessionId?: string;
|
|
||||||
}): AcpSession {
|
const createSession: AcpSessionStore["createSession"] = (params) => {
|
||||||
const sessionId = params.sessionId ?? randomUUID();
|
const sessionId = params.sessionId ?? randomUUID();
|
||||||
const session: AcpSession = {
|
const session: AcpSession = {
|
||||||
sessionId,
|
sessionId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
abortController: null,
|
abortController: null,
|
||||||
activeRunId: null,
|
activeRunId: null,
|
||||||
|
};
|
||||||
|
sessions.set(sessionId, session);
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSession: AcpSessionStore["getSession"] = (sessionId) => sessions.get(sessionId);
|
||||||
|
|
||||||
|
const getSessionByRunId: AcpSessionStore["getSessionByRunId"] = (runId) => {
|
||||||
|
const sessionId = runIdToSessionId.get(runId);
|
||||||
|
return sessionId ? sessions.get(sessionId) : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setActiveRun: AcpSessionStore["setActiveRun"] = (
|
||||||
|
sessionId,
|
||||||
|
runId,
|
||||||
|
abortController,
|
||||||
|
) => {
|
||||||
|
const session = sessions.get(sessionId);
|
||||||
|
if (!session) return;
|
||||||
|
session.activeRunId = runId;
|
||||||
|
session.abortController = abortController;
|
||||||
|
runIdToSessionId.set(runId, sessionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearActiveRun: AcpSessionStore["clearActiveRun"] = (sessionId) => {
|
||||||
|
const session = sessions.get(sessionId);
|
||||||
|
if (!session) return;
|
||||||
|
if (session.activeRunId) runIdToSessionId.delete(session.activeRunId);
|
||||||
|
session.activeRunId = null;
|
||||||
|
session.abortController = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelActiveRun: AcpSessionStore["cancelActiveRun"] = (sessionId) => {
|
||||||
|
const session = sessions.get(sessionId);
|
||||||
|
if (!session?.abortController) return false;
|
||||||
|
session.abortController.abort();
|
||||||
|
if (session.activeRunId) runIdToSessionId.delete(session.activeRunId);
|
||||||
|
session.abortController = null;
|
||||||
|
session.activeRunId = null;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAllSessionsForTest: AcpSessionStore["clearAllSessionsForTest"] = () => {
|
||||||
|
for (const session of sessions.values()) {
|
||||||
|
session.abortController?.abort();
|
||||||
|
}
|
||||||
|
sessions.clear();
|
||||||
|
runIdToSessionId.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSession,
|
||||||
|
getSession,
|
||||||
|
getSessionByRunId,
|
||||||
|
setActiveRun,
|
||||||
|
clearActiveRun,
|
||||||
|
cancelActiveRun,
|
||||||
|
clearAllSessionsForTest,
|
||||||
};
|
};
|
||||||
sessions.set(sessionId, session);
|
|
||||||
return session;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSession(sessionId: string): AcpSession | undefined {
|
export const defaultAcpSessionStore = createInMemorySessionStore();
|
||||||
return sessions.get(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSessionByRunId(runId: string): AcpSession | undefined {
|
|
||||||
const sessionId = runIdToSessionId.get(runId);
|
|
||||||
return sessionId ? sessions.get(sessionId) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setActiveRun(
|
|
||||||
sessionId: string,
|
|
||||||
runId: string,
|
|
||||||
abortController: AbortController,
|
|
||||||
): void {
|
|
||||||
const session = sessions.get(sessionId);
|
|
||||||
if (!session) return;
|
|
||||||
session.activeRunId = runId;
|
|
||||||
session.abortController = abortController;
|
|
||||||
runIdToSessionId.set(runId, sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearActiveRun(sessionId: string): void {
|
|
||||||
const session = sessions.get(sessionId);
|
|
||||||
if (!session) return;
|
|
||||||
if (session.activeRunId) runIdToSessionId.delete(session.activeRunId);
|
|
||||||
session.activeRunId = null;
|
|
||||||
session.abortController = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cancelActiveRun(sessionId: string): boolean {
|
|
||||||
const session = sessions.get(sessionId);
|
|
||||||
if (!session?.abortController) return false;
|
|
||||||
session.abortController.abort();
|
|
||||||
if (session.activeRunId) runIdToSessionId.delete(session.activeRunId);
|
|
||||||
session.abortController = null;
|
|
||||||
session.activeRunId = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearAllSessionsForTest(): void {
|
|
||||||
for (const session of sessions.values()) {
|
|
||||||
session.abortController?.abort();
|
|
||||||
}
|
|
||||||
sessions.clear();
|
|
||||||
runIdToSessionId.clear();
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import type {
|
|||||||
AuthenticateRequest,
|
AuthenticateRequest,
|
||||||
AuthenticateResponse,
|
AuthenticateResponse,
|
||||||
CancelNotification,
|
CancelNotification,
|
||||||
ContentBlock,
|
|
||||||
ImageContent,
|
|
||||||
InitializeRequest,
|
InitializeRequest,
|
||||||
InitializeResponse,
|
InitializeResponse,
|
||||||
ListSessionsRequest,
|
ListSessionsRequest,
|
||||||
@ -21,21 +19,22 @@ import type {
|
|||||||
SetSessionModeRequest,
|
SetSessionModeRequest,
|
||||||
SetSessionModeResponse,
|
SetSessionModeResponse,
|
||||||
StopReason,
|
StopReason,
|
||||||
ToolKind,
|
|
||||||
} from "@agentclientprotocol/sdk";
|
} from "@agentclientprotocol/sdk";
|
||||||
import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
|
import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
|
||||||
|
|
||||||
import type { GatewayClient } from "../gateway/client.js";
|
import type { GatewayClient } from "../gateway/client.js";
|
||||||
import type { EventFrame } from "../gateway/protocol/index.js";
|
import type { EventFrame } from "../gateway/protocol/index.js";
|
||||||
import type { SessionsListResult } from "../gateway/session-utils.js";
|
import type { SessionsListResult } from "../gateway/session-utils.js";
|
||||||
import { ACP_AGENT_INFO, type AcpServerOptions } from "./types.js";
|
import { readBool, readNumber, readString } from "./meta.js";
|
||||||
import {
|
import {
|
||||||
cancelActiveRun,
|
extractAttachmentsFromPrompt,
|
||||||
clearActiveRun,
|
extractTextFromPrompt,
|
||||||
createSession,
|
formatToolTitle,
|
||||||
getSession,
|
inferToolKind,
|
||||||
setActiveRun,
|
} from "./event-mapper.js";
|
||||||
} from "./session.js";
|
import { parseSessionMeta, resetSessionIfNeeded, resolveSessionKey } from "./session-mapper.js";
|
||||||
|
import { ACP_AGENT_INFO, type AcpServerOptions } from "./types.js";
|
||||||
|
import { defaultAcpSessionStore, type AcpSessionStore } from "./session.js";
|
||||||
|
|
||||||
type PendingPrompt = {
|
type PendingPrompt = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@ -48,25 +47,22 @@ type PendingPrompt = {
|
|||||||
toolCalls?: Set<string>;
|
toolCalls?: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SessionMeta = {
|
type AcpGatewayAgentOptions = AcpServerOptions & {
|
||||||
sessionKey?: string;
|
sessionStore?: AcpSessionStore;
|
||||||
sessionLabel?: string;
|
|
||||||
resetSession?: boolean;
|
|
||||||
requireExisting?: boolean;
|
|
||||||
prefixCwd?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AcpGatewayAgent implements Agent {
|
export class AcpGatewayAgent implements Agent {
|
||||||
private connection: AgentSideConnection;
|
private connection: AgentSideConnection;
|
||||||
private gateway: GatewayClient;
|
private gateway: GatewayClient;
|
||||||
private opts: AcpServerOptions;
|
private opts: AcpGatewayAgentOptions;
|
||||||
private log: (msg: string) => void;
|
private log: (msg: string) => void;
|
||||||
|
private sessionStore: AcpSessionStore;
|
||||||
private pendingPrompts = new Map<string, PendingPrompt>();
|
private pendingPrompts = new Map<string, PendingPrompt>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
connection: AgentSideConnection,
|
connection: AgentSideConnection,
|
||||||
gateway: GatewayClient,
|
gateway: GatewayClient,
|
||||||
opts: AcpServerOptions = {},
|
opts: AcpGatewayAgentOptions = {},
|
||||||
) {
|
) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.gateway = gateway;
|
this.gateway = gateway;
|
||||||
@ -74,6 +70,7 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
this.log = opts.verbose
|
this.log = opts.verbose
|
||||||
? (msg: string) => process.stderr.write(`[acp] ${msg}\n`)
|
? (msg: string) => process.stderr.write(`[acp] ${msg}\n`)
|
||||||
: () => {};
|
: () => {};
|
||||||
|
this.sessionStore = opts.sessionStore ?? defaultAcpSessionStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
@ -88,7 +85,7 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
this.log(`gateway disconnected: ${reason}`);
|
this.log(`gateway disconnected: ${reason}`);
|
||||||
for (const pending of this.pendingPrompts.values()) {
|
for (const pending of this.pendingPrompts.values()) {
|
||||||
pending.reject(new Error(`Gateway disconnected: ${reason}`));
|
pending.reject(new Error(`Gateway disconnected: ${reason}`));
|
||||||
clearActiveRun(pending.sessionId);
|
this.sessionStore.clearActiveRun(pending.sessionId);
|
||||||
}
|
}
|
||||||
this.pendingPrompts.clear();
|
this.pendingPrompts.clear();
|
||||||
}
|
}
|
||||||
@ -132,11 +129,21 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = randomUUID();
|
const sessionId = randomUUID();
|
||||||
const meta = this.parseSessionMeta(params._meta);
|
const meta = parseSessionMeta(params._meta);
|
||||||
const sessionKey = await this.resolveSessionKey(meta, `acp:${sessionId}`);
|
const sessionKey = await resolveSessionKey({
|
||||||
await this.resetSessionIfNeeded(meta, sessionKey);
|
meta,
|
||||||
|
fallbackKey: `acp:${sessionId}`,
|
||||||
|
gateway: this.gateway,
|
||||||
|
opts: this.opts,
|
||||||
|
});
|
||||||
|
await resetSessionIfNeeded({
|
||||||
|
meta,
|
||||||
|
sessionKey,
|
||||||
|
gateway: this.gateway,
|
||||||
|
opts: this.opts,
|
||||||
|
});
|
||||||
|
|
||||||
const session = createSession({
|
const session = this.sessionStore.createSession({
|
||||||
sessionId,
|
sessionId,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
@ -150,11 +157,21 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
this.log(`ignoring ${params.mcpServers.length} MCP servers`);
|
this.log(`ignoring ${params.mcpServers.length} MCP servers`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = this.parseSessionMeta(params._meta);
|
const meta = parseSessionMeta(params._meta);
|
||||||
const sessionKey = await this.resolveSessionKey(meta, params.sessionId);
|
const sessionKey = await resolveSessionKey({
|
||||||
await this.resetSessionIfNeeded(meta, sessionKey);
|
meta,
|
||||||
|
fallbackKey: params.sessionId,
|
||||||
|
gateway: this.gateway,
|
||||||
|
opts: this.opts,
|
||||||
|
});
|
||||||
|
await resetSessionIfNeeded({
|
||||||
|
meta,
|
||||||
|
sessionKey,
|
||||||
|
gateway: this.gateway,
|
||||||
|
opts: this.opts,
|
||||||
|
});
|
||||||
|
|
||||||
const session = createSession({
|
const session = this.sessionStore.createSession({
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
@ -190,7 +207,7 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
async setSessionMode(
|
async setSessionMode(
|
||||||
params: SetSessionModeRequest,
|
params: SetSessionModeRequest,
|
||||||
): Promise<SetSessionModeResponse | void> {
|
): Promise<SetSessionModeResponse | void> {
|
||||||
const session = getSession(params.sessionId);
|
const session = this.sessionStore.getSession(params.sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(`Session ${params.sessionId} not found`);
|
throw new Error(`Session ${params.sessionId} not found`);
|
||||||
}
|
}
|
||||||
@ -208,22 +225,22 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
||||||
const session = getSession(params.sessionId);
|
const session = this.sessionStore.getSession(params.sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(`Session ${params.sessionId} not found`);
|
throw new Error(`Session ${params.sessionId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.abortController) {
|
if (session.abortController) {
|
||||||
cancelActiveRun(params.sessionId);
|
this.sessionStore.cancelActiveRun(params.sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const runId = randomUUID();
|
const runId = randomUUID();
|
||||||
setActiveRun(params.sessionId, runId, abortController);
|
this.sessionStore.setActiveRun(params.sessionId, runId, abortController);
|
||||||
|
|
||||||
const meta = this.parseSessionMeta(params._meta);
|
const meta = parseSessionMeta(params._meta);
|
||||||
const userText = this.extractTextFromPrompt(params.prompt);
|
const userText = extractTextFromPrompt(params.prompt);
|
||||||
const attachments = this.extractAttachmentsFromPrompt(params.prompt);
|
const attachments = extractAttachmentsFromPrompt(params.prompt);
|
||||||
const prefixCwd = meta.prefixCwd ?? this.opts.prefixCwd ?? true;
|
const prefixCwd = meta.prefixCwd ?? this.opts.prefixCwd ?? true;
|
||||||
const message = prefixCwd ? `[Working directory: ${session.cwd}]\n\n${userText}` : userText;
|
const message = prefixCwd ? `[Working directory: ${session.cwd}]\n\n${userText}` : userText;
|
||||||
|
|
||||||
@ -252,17 +269,17 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.pendingPrompts.delete(params.sessionId);
|
this.pendingPrompts.delete(params.sessionId);
|
||||||
clearActiveRun(params.sessionId);
|
this.sessionStore.clearActiveRun(params.sessionId);
|
||||||
reject(err instanceof Error ? err : new Error(String(err)));
|
reject(err instanceof Error ? err : new Error(String(err)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancel(params: CancelNotification): Promise<void> {
|
async cancel(params: CancelNotification): Promise<void> {
|
||||||
const session = getSession(params.sessionId);
|
const session = this.sessionStore.getSession(params.sessionId);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|
||||||
cancelActiveRun(params.sessionId);
|
this.sessionStore.cancelActiveRun(params.sessionId);
|
||||||
try {
|
try {
|
||||||
await this.gateway.request("chat.abort", { sessionKey: session.sessionKey });
|
await this.gateway.request("chat.abort", { sessionKey: session.sessionKey });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -389,7 +406,7 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
stopReason: StopReason,
|
stopReason: StopReason,
|
||||||
): void {
|
): void {
|
||||||
this.pendingPrompts.delete(sessionId);
|
this.pendingPrompts.delete(sessionId);
|
||||||
clearActiveRun(sessionId);
|
this.sessionStore.clearActiveRun(sessionId);
|
||||||
pending.resolve({ stopReason });
|
pending.resolve({ stopReason });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,157 +417,4 @@ export class AcpGatewayAgent implements Agent {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractTextFromPrompt(prompt: ContentBlock[]): string {
|
|
||||||
const parts: string[] = [];
|
|
||||||
for (const block of prompt) {
|
|
||||||
if (block.type === "text") {
|
|
||||||
parts.push(block.text);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (block.type === "resource") {
|
|
||||||
const resource = block.resource as { text?: string } | undefined;
|
|
||||||
if (resource?.text) parts.push(resource.text);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (block.type === "resource_link") {
|
|
||||||
const title = block.title ? ` (${block.title})` : "";
|
|
||||||
const uri = block.uri ?? "";
|
|
||||||
const line = uri ? `[Resource link${title}] ${uri}` : `[Resource link${title}]`;
|
|
||||||
parts.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parts.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractAttachmentsFromPrompt(
|
|
||||||
prompt: ContentBlock[],
|
|
||||||
): Array<{ type: string; mimeType: string; content: string }> {
|
|
||||||
const attachments: Array<{ type: string; mimeType: string; content: string }> = [];
|
|
||||||
for (const block of prompt) {
|
|
||||||
if (block.type !== "image") continue;
|
|
||||||
const image = block as ImageContent;
|
|
||||||
if (!image.data || !image.mimeType) continue;
|
|
||||||
attachments.push({
|
|
||||||
type: "image",
|
|
||||||
mimeType: image.mimeType,
|
|
||||||
content: image.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return attachments;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseSessionMeta(meta: unknown): SessionMeta {
|
|
||||||
if (!meta || typeof meta !== "object") return {};
|
|
||||||
const record = meta as Record<string, unknown>;
|
|
||||||
return {
|
|
||||||
sessionKey: readString(record, ["sessionKey", "session", "key"]),
|
|
||||||
sessionLabel: readString(record, ["sessionLabel", "label"]),
|
|
||||||
resetSession: readBool(record, ["resetSession", "reset"]),
|
|
||||||
requireExisting: readBool(record, ["requireExistingSession", "requireExisting"]),
|
|
||||||
prefixCwd: readBool(record, ["prefixCwd"]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resolveSessionKey(meta: SessionMeta, fallbackKey: string): Promise<string> {
|
|
||||||
const requestedKey = meta.sessionKey ?? this.opts.defaultSessionKey;
|
|
||||||
const requestedLabel = meta.sessionLabel ?? this.opts.defaultSessionLabel;
|
|
||||||
const requireExisting =
|
|
||||||
meta.requireExisting ?? this.opts.requireExistingSession ?? false;
|
|
||||||
|
|
||||||
if (requestedLabel) {
|
|
||||||
const resolved = await this.gateway.request<{ ok: true; key: string }>(
|
|
||||||
"sessions.resolve",
|
|
||||||
{ label: requestedLabel },
|
|
||||||
);
|
|
||||||
if (!resolved?.key) {
|
|
||||||
throw new Error(`Unable to resolve session label: ${requestedLabel}`);
|
|
||||||
}
|
|
||||||
return resolved.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestedKey) {
|
|
||||||
if (!requireExisting) return requestedKey;
|
|
||||||
const resolved = await this.gateway.request<{ ok: true; key: string }>(
|
|
||||||
"sessions.resolve",
|
|
||||||
{ key: requestedKey },
|
|
||||||
);
|
|
||||||
if (!resolved?.key) {
|
|
||||||
throw new Error(`Session key not found: ${requestedKey}`);
|
|
||||||
}
|
|
||||||
return resolved.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resetSessionIfNeeded(meta: SessionMeta, sessionKey: string): Promise<void> {
|
|
||||||
const resetSession = meta.resetSession ?? this.opts.resetSession ?? false;
|
|
||||||
if (!resetSession) return;
|
|
||||||
await this.gateway.request("sessions.reset", { key: sessionKey });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readString(
|
|
||||||
meta: Record<string, unknown> | null | undefined,
|
|
||||||
keys: string[],
|
|
||||||
): string | undefined {
|
|
||||||
if (!meta) return undefined;
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = meta[key];
|
|
||||||
if (typeof value === "string" && value.trim()) return value.trim();
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readBool(
|
|
||||||
meta: Record<string, unknown> | null | undefined,
|
|
||||||
keys: string[],
|
|
||||||
): boolean | undefined {
|
|
||||||
if (!meta) return undefined;
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = meta[key];
|
|
||||||
if (typeof value === "boolean") return value;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readNumber(
|
|
||||||
meta: Record<string, unknown> | null | undefined,
|
|
||||||
keys: string[],
|
|
||||||
): number | undefined {
|
|
||||||
if (!meta) return undefined;
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = meta[key];
|
|
||||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatToolTitle(
|
|
||||||
name: string | undefined,
|
|
||||||
args: Record<string, unknown> | undefined,
|
|
||||||
): string {
|
|
||||||
const base = name ?? "tool";
|
|
||||||
if (!args || Object.keys(args).length === 0) return base;
|
|
||||||
const parts = Object.entries(args).map(([key, value]) => {
|
|
||||||
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
||||||
const safe = raw.length > 100 ? `${raw.slice(0, 100)}...` : raw;
|
|
||||||
return `${key}: ${safe}`;
|
|
||||||
});
|
|
||||||
return `${base}: ${parts.join(", ")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferToolKind(name?: string): ToolKind | undefined {
|
|
||||||
if (!name) return "other";
|
|
||||||
const normalized = name.toLowerCase();
|
|
||||||
if (normalized.includes("read")) return "read";
|
|
||||||
if (normalized.includes("write") || normalized.includes("edit")) return "edit";
|
|
||||||
if (normalized.includes("delete") || normalized.includes("remove")) return "delete";
|
|
||||||
if (normalized.includes("move") || normalized.includes("rename")) return "move";
|
|
||||||
if (normalized.includes("search") || normalized.includes("find")) return "search";
|
|
||||||
if (normalized.includes("exec") || normalized.includes("run") || normalized.includes("bash")) {
|
|
||||||
return "execute";
|
|
||||||
}
|
|
||||||
if (normalized.includes("fetch") || normalized.includes("http")) return "fetch";
|
|
||||||
return "other";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,15 @@
|
|||||||
|
import {
|
||||||
|
parseAgentSessionKey,
|
||||||
|
type ParsedAgentSessionKey,
|
||||||
|
} from "../sessions/session-key-utils.js";
|
||||||
|
|
||||||
|
export {
|
||||||
|
isAcpSessionKey,
|
||||||
|
isSubagentSessionKey,
|
||||||
|
parseAgentSessionKey,
|
||||||
|
type ParsedAgentSessionKey,
|
||||||
|
} from "../sessions/session-key-utils.js";
|
||||||
|
|
||||||
export const DEFAULT_AGENT_ID = "main";
|
export const DEFAULT_AGENT_ID = "main";
|
||||||
export const DEFAULT_MAIN_KEY = "main";
|
export const DEFAULT_MAIN_KEY = "main";
|
||||||
export const DEFAULT_ACCOUNT_ID = "default";
|
export const DEFAULT_ACCOUNT_ID = "default";
|
||||||
@ -11,11 +23,6 @@ export function normalizeMainKey(value: string | undefined | null): string {
|
|||||||
return trimmed ? trimmed : DEFAULT_MAIN_KEY;
|
return trimmed ? trimmed : DEFAULT_MAIN_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ParsedAgentSessionKey = {
|
|
||||||
agentId: string;
|
|
||||||
rest: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function toAgentRequestSessionKey(storeKey: string | undefined | null): string | undefined {
|
export function toAgentRequestSessionKey(storeKey: string | undefined | null): string | undefined {
|
||||||
const raw = (storeKey ?? "").trim();
|
const raw = (storeKey ?? "").trim();
|
||||||
if (!raw) return undefined;
|
if (!raw) return undefined;
|
||||||
@ -70,37 +77,6 @@ export function normalizeAccountId(value: string | undefined | null): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseAgentSessionKey(
|
|
||||||
sessionKey: string | undefined | null,
|
|
||||||
): ParsedAgentSessionKey | null {
|
|
||||||
const raw = (sessionKey ?? "").trim();
|
|
||||||
if (!raw) return null;
|
|
||||||
const parts = raw.split(":").filter(Boolean);
|
|
||||||
if (parts.length < 3) return null;
|
|
||||||
if (parts[0] !== "agent") return null;
|
|
||||||
const agentId = parts[1]?.trim();
|
|
||||||
const rest = parts.slice(2).join(":");
|
|
||||||
if (!agentId || !rest) return null;
|
|
||||||
return { agentId, rest };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSubagentSessionKey(sessionKey: string | undefined | null): boolean {
|
|
||||||
const raw = (sessionKey ?? "").trim();
|
|
||||||
if (!raw) return false;
|
|
||||||
if (raw.toLowerCase().startsWith("subagent:")) return true;
|
|
||||||
const parsed = parseAgentSessionKey(raw);
|
|
||||||
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("subagent:"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isAcpSessionKey(sessionKey: string | undefined | null): boolean {
|
|
||||||
const raw = (sessionKey ?? "").trim();
|
|
||||||
if (!raw) return false;
|
|
||||||
const normalized = raw.toLowerCase();
|
|
||||||
if (normalized.startsWith("acp:")) return true;
|
|
||||||
const parsed = parseAgentSessionKey(raw);
|
|
||||||
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("acp:"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildAgentMainSessionKey(params: {
|
export function buildAgentMainSessionKey(params: {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
mainKey?: string | undefined;
|
mainKey?: string | undefined;
|
||||||
|
|||||||
35
src/sessions/session-key-utils.ts
Normal file
35
src/sessions/session-key-utils.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export type ParsedAgentSessionKey = {
|
||||||
|
agentId: string;
|
||||||
|
rest: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseAgentSessionKey(
|
||||||
|
sessionKey: string | undefined | null,
|
||||||
|
): ParsedAgentSessionKey | null {
|
||||||
|
const raw = (sessionKey ?? "").trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
const parts = raw.split(":").filter(Boolean);
|
||||||
|
if (parts.length < 3) return null;
|
||||||
|
if (parts[0] !== "agent") return null;
|
||||||
|
const agentId = parts[1]?.trim();
|
||||||
|
const rest = parts.slice(2).join(":");
|
||||||
|
if (!agentId || !rest) return null;
|
||||||
|
return { agentId, rest };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSubagentSessionKey(sessionKey: string | undefined | null): boolean {
|
||||||
|
const raw = (sessionKey ?? "").trim();
|
||||||
|
if (!raw) return false;
|
||||||
|
if (raw.toLowerCase().startsWith("subagent:")) return true;
|
||||||
|
const parsed = parseAgentSessionKey(raw);
|
||||||
|
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("subagent:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAcpSessionKey(sessionKey: string | undefined | null): boolean {
|
||||||
|
const raw = (sessionKey ?? "").trim();
|
||||||
|
if (!raw) return false;
|
||||||
|
const normalized = raw.toLowerCase();
|
||||||
|
if (normalized.startsWith("acp:")) return true;
|
||||||
|
const parsed = parseAgentSessionKey(raw);
|
||||||
|
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("acp:"));
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user