mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 13:04:11 +00:00
feat: add configuration for model, mode, and thought level
This commit is contained in:
parent
c3a95c3611
commit
6d319a1c3e
16 changed files with 1419 additions and 67 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
AcpHttpClient,
|
||||
AcpRpcError,
|
||||
PROTOCOL_VERSION,
|
||||
type AcpEnvelopeDirection,
|
||||
type AnyMessage,
|
||||
|
|
@ -9,8 +10,12 @@ import {
|
|||
type NewSessionResponse,
|
||||
type PromptRequest,
|
||||
type PromptResponse,
|
||||
type SessionConfigOption,
|
||||
type SessionNotification,
|
||||
type SessionModeState,
|
||||
type SetSessionConfigOptionResponse,
|
||||
type SetSessionConfigOptionRequest,
|
||||
type SetSessionModeResponse,
|
||||
type SetSessionModeRequest,
|
||||
} from "acp-http-client";
|
||||
import type { SandboxAgentSpawnHandle, SandboxAgentSpawnOptions } from "./spawn.ts";
|
||||
|
|
@ -53,6 +58,9 @@ const DEFAULT_BASE_URL = "http://sandbox-agent";
|
|||
const DEFAULT_REPLAY_MAX_EVENTS = 50;
|
||||
const DEFAULT_REPLAY_MAX_CHARS = 12_000;
|
||||
const EVENT_INDEX_SCAN_EVENTS_LIMIT = 500;
|
||||
const SESSION_CANCEL_METHOD = "session/cancel";
|
||||
const MANUAL_CANCEL_ERROR =
|
||||
"Manual session/cancel calls are not allowed. Use destroySession(sessionId) instead.";
|
||||
|
||||
interface SandboxAgentConnectCommonOptions {
|
||||
headers?: HeadersInit;
|
||||
|
|
@ -85,12 +93,18 @@ export interface SessionCreateRequest {
|
|||
id?: string;
|
||||
agent: string;
|
||||
sessionInit?: Omit<NewSessionRequest, "_meta">;
|
||||
model?: string;
|
||||
mode?: string;
|
||||
thoughtLevel?: string;
|
||||
}
|
||||
|
||||
export interface SessionResumeOrCreateRequest {
|
||||
id: string;
|
||||
agent: string;
|
||||
sessionInit?: Omit<NewSessionRequest, "_meta">;
|
||||
model?: string;
|
||||
mode?: string;
|
||||
thoughtLevel?: string;
|
||||
}
|
||||
|
||||
export interface SessionSendOptions {
|
||||
|
|
@ -113,6 +127,64 @@ export class SandboxAgentError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class UnsupportedSessionCategoryError extends Error {
|
||||
readonly sessionId: string;
|
||||
readonly category: string;
|
||||
readonly availableCategories: string[];
|
||||
|
||||
constructor(sessionId: string, category: string, availableCategories: string[]) {
|
||||
super(
|
||||
`Session '${sessionId}' does not support category '${category}'. Available categories: ${availableCategories.join(", ") || "(none)"}`,
|
||||
);
|
||||
this.name = "UnsupportedSessionCategoryError";
|
||||
this.sessionId = sessionId;
|
||||
this.category = category;
|
||||
this.availableCategories = availableCategories;
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedSessionValueError extends Error {
|
||||
readonly sessionId: string;
|
||||
readonly category: string;
|
||||
readonly configId: string;
|
||||
readonly requestedValue: string;
|
||||
readonly allowedValues: string[];
|
||||
|
||||
constructor(
|
||||
sessionId: string,
|
||||
category: string,
|
||||
configId: string,
|
||||
requestedValue: string,
|
||||
allowedValues: string[],
|
||||
) {
|
||||
super(
|
||||
`Session '${sessionId}' does not support value '${requestedValue}' for category '${category}' (configId='${configId}'). Allowed values: ${allowedValues.join(", ") || "(none)"}`,
|
||||
);
|
||||
this.name = "UnsupportedSessionValueError";
|
||||
this.sessionId = sessionId;
|
||||
this.category = category;
|
||||
this.configId = configId;
|
||||
this.requestedValue = requestedValue;
|
||||
this.allowedValues = allowedValues;
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedSessionConfigOptionError extends Error {
|
||||
readonly sessionId: string;
|
||||
readonly configId: string;
|
||||
readonly availableConfigIds: string[];
|
||||
|
||||
constructor(sessionId: string, configId: string, availableConfigIds: string[]) {
|
||||
super(
|
||||
`Session '${sessionId}' does not expose config option '${configId}'. Available configIds: ${availableConfigIds.join(", ") || "(none)"}`,
|
||||
);
|
||||
this.name = "UnsupportedSessionConfigOptionError";
|
||||
this.sessionId = sessionId;
|
||||
this.configId = configId;
|
||||
this.availableConfigIds = availableConfigIds;
|
||||
}
|
||||
}
|
||||
|
||||
export class Session {
|
||||
private record: SessionRecord;
|
||||
private readonly sandbox: SandboxAgent;
|
||||
|
|
@ -166,6 +238,38 @@ export class Session {
|
|||
return response as PromptResponse;
|
||||
}
|
||||
|
||||
async setMode(modeId: string): Promise<SetSessionModeResponse | void> {
|
||||
const updated = await this.sandbox.setSessionMode(this.id, modeId);
|
||||
this.apply(updated.session.toRecord());
|
||||
return updated.response;
|
||||
}
|
||||
|
||||
async setConfigOption(configId: string, value: string): Promise<SetSessionConfigOptionResponse> {
|
||||
const updated = await this.sandbox.setSessionConfigOption(this.id, configId, value);
|
||||
this.apply(updated.session.toRecord());
|
||||
return updated.response;
|
||||
}
|
||||
|
||||
async setModel(model: string): Promise<SetSessionConfigOptionResponse> {
|
||||
const updated = await this.sandbox.setSessionModel(this.id, model);
|
||||
this.apply(updated.session.toRecord());
|
||||
return updated.response;
|
||||
}
|
||||
|
||||
async setThoughtLevel(thoughtLevel: string): Promise<SetSessionConfigOptionResponse> {
|
||||
const updated = await this.sandbox.setSessionThoughtLevel(this.id, thoughtLevel);
|
||||
this.apply(updated.session.toRecord());
|
||||
return updated.response;
|
||||
}
|
||||
|
||||
async getConfigOptions(): Promise<SessionConfigOption[]> {
|
||||
return this.sandbox.getSessionConfigOptions(this.id);
|
||||
}
|
||||
|
||||
async getModes(): Promise<SessionModeState | null> {
|
||||
return this.sandbox.getSessionModes(this.id);
|
||||
}
|
||||
|
||||
onEvent(listener: SessionEventListener): () => void {
|
||||
return this.sandbox.onSessionEvent(this.id, listener);
|
||||
}
|
||||
|
|
@ -566,12 +670,26 @@ export class SandboxAgent {
|
|||
lastConnectionId: live.connectionId,
|
||||
createdAt: nowMs(),
|
||||
sessionInit,
|
||||
configOptions: cloneConfigOptions(response.configOptions),
|
||||
modes: cloneModes(response.modes),
|
||||
};
|
||||
|
||||
await this.persist.updateSession(record);
|
||||
this.nextSessionEventIndexBySession.set(record.id, 1);
|
||||
live.bindSession(record.id, record.agentSessionId);
|
||||
return this.upsertSessionHandle(record);
|
||||
let session = this.upsertSessionHandle(record);
|
||||
|
||||
if (request.mode) {
|
||||
session = (await this.setSessionMode(session.id, request.mode)).session;
|
||||
}
|
||||
if (request.model) {
|
||||
session = (await this.setSessionModel(session.id, request.model)).session;
|
||||
}
|
||||
if (request.thoughtLevel) {
|
||||
session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
async resumeSession(id: string): Promise<Session> {
|
||||
|
|
@ -595,6 +713,8 @@ export class SandboxAgent {
|
|||
agentSessionId: recreated.sessionId,
|
||||
lastConnectionId: live.connectionId,
|
||||
destroyedAt: undefined,
|
||||
configOptions: cloneConfigOptions(recreated.configOptions),
|
||||
modes: cloneModes(recreated.modes),
|
||||
};
|
||||
|
||||
await this.persist.updateSession(updated);
|
||||
|
|
@ -607,16 +727,24 @@ export class SandboxAgent {
|
|||
async resumeOrCreateSession(request: SessionResumeOrCreateRequest): Promise<Session> {
|
||||
const existing = await this.persist.getSession(request.id);
|
||||
if (existing) {
|
||||
return this.resumeSession(existing.id);
|
||||
let session = await this.resumeSession(existing.id);
|
||||
if (request.mode) {
|
||||
session = (await this.setSessionMode(session.id, request.mode)).session;
|
||||
}
|
||||
if (request.model) {
|
||||
session = (await this.setSessionModel(session.id, request.model)).session;
|
||||
}
|
||||
if (request.thoughtLevel) {
|
||||
session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
return this.createSession(request);
|
||||
}
|
||||
|
||||
async destroySession(id: string): Promise<Session> {
|
||||
const existing = await this.persist.getSession(id);
|
||||
if (!existing) {
|
||||
throw new Error(`session '${id}' not found`);
|
||||
}
|
||||
await this.sendSessionMethodInternal(id, SESSION_CANCEL_METHOD, {}, {}, true);
|
||||
const existing = await this.requireSessionRecord(id);
|
||||
|
||||
const updated: SessionRecord = {
|
||||
...existing,
|
||||
|
|
@ -627,12 +755,175 @@ export class SandboxAgent {
|
|||
return this.upsertSessionHandle(updated);
|
||||
}
|
||||
|
||||
async setSessionMode(
|
||||
sessionId: string,
|
||||
modeId: string,
|
||||
): Promise<{ session: Session; response: SetSessionModeResponse | void }> {
|
||||
const mode = modeId.trim();
|
||||
if (!mode) {
|
||||
throw new Error("setSessionMode requires a non-empty modeId");
|
||||
}
|
||||
|
||||
const record = await this.requireSessionRecord(sessionId);
|
||||
const knownModeIds = extractKnownModeIds(record.modes);
|
||||
if (knownModeIds.length > 0 && !knownModeIds.includes(mode)) {
|
||||
throw new UnsupportedSessionValueError(sessionId, "mode", "mode", mode, knownModeIds);
|
||||
}
|
||||
|
||||
try {
|
||||
return (await this.sendSessionMethodInternal(
|
||||
sessionId,
|
||||
"session/set_mode",
|
||||
{ modeId: mode },
|
||||
{},
|
||||
false,
|
||||
)) as { session: Session; response: SetSessionModeResponse | void };
|
||||
} catch (error) {
|
||||
if (!(error instanceof AcpRpcError) || error.code !== -32601) {
|
||||
throw error;
|
||||
}
|
||||
return this.setSessionCategoryValue(sessionId, "mode", mode);
|
||||
}
|
||||
}
|
||||
|
||||
async setSessionConfigOption(
|
||||
sessionId: string,
|
||||
configId: string,
|
||||
value: string,
|
||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
const resolvedConfigId = configId.trim();
|
||||
if (!resolvedConfigId) {
|
||||
throw new Error("setSessionConfigOption requires a non-empty configId");
|
||||
}
|
||||
const resolvedValue = value.trim();
|
||||
if (!resolvedValue) {
|
||||
throw new Error("setSessionConfigOption requires a non-empty value");
|
||||
}
|
||||
|
||||
const options = await this.getSessionConfigOptions(sessionId);
|
||||
const option = findConfigOptionById(options, resolvedConfigId);
|
||||
if (!option) {
|
||||
throw new UnsupportedSessionConfigOptionError(
|
||||
sessionId,
|
||||
resolvedConfigId,
|
||||
options.map((item) => item.id),
|
||||
);
|
||||
}
|
||||
|
||||
const allowedValues = extractConfigValues(option);
|
||||
if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
|
||||
throw new UnsupportedSessionValueError(
|
||||
sessionId,
|
||||
option.category ?? "uncategorized",
|
||||
option.id,
|
||||
resolvedValue,
|
||||
allowedValues,
|
||||
);
|
||||
}
|
||||
|
||||
return (await this.sendSessionMethodInternal(
|
||||
sessionId,
|
||||
"session/set_config_option",
|
||||
{
|
||||
configId: resolvedConfigId,
|
||||
value: resolvedValue,
|
||||
},
|
||||
{},
|
||||
false,
|
||||
)) as { session: Session; response: SetSessionConfigOptionResponse };
|
||||
}
|
||||
|
||||
async setSessionModel(
|
||||
sessionId: string,
|
||||
model: string,
|
||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
return this.setSessionCategoryValue(sessionId, "model", model);
|
||||
}
|
||||
|
||||
async setSessionThoughtLevel(
|
||||
sessionId: string,
|
||||
thoughtLevel: string,
|
||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
return this.setSessionCategoryValue(sessionId, "thought_level", thoughtLevel);
|
||||
}
|
||||
|
||||
async getSessionConfigOptions(sessionId: string): Promise<SessionConfigOption[]> {
|
||||
const record = await this.requireSessionRecord(sessionId);
|
||||
const hydrated = await this.hydrateSessionConfigOptions(record);
|
||||
return cloneConfigOptions(hydrated.configOptions) ?? [];
|
||||
}
|
||||
|
||||
async getSessionModes(sessionId: string): Promise<SessionModeState | null> {
|
||||
const record = await this.requireSessionRecord(sessionId);
|
||||
return cloneModes(record.modes);
|
||||
}
|
||||
|
||||
private async setSessionCategoryValue(
|
||||
sessionId: string,
|
||||
category: string,
|
||||
value: string,
|
||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
const resolvedValue = value.trim();
|
||||
if (!resolvedValue) {
|
||||
throw new Error(`setSession${toTitleCase(category)} requires a non-empty value`);
|
||||
}
|
||||
|
||||
const options = await this.getSessionConfigOptions(sessionId);
|
||||
const option = findConfigOptionByCategory(options, category);
|
||||
if (!option) {
|
||||
const categories = uniqueCategories(options);
|
||||
throw new UnsupportedSessionCategoryError(sessionId, category, categories);
|
||||
}
|
||||
|
||||
const allowedValues = extractConfigValues(option);
|
||||
if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
|
||||
throw new UnsupportedSessionValueError(
|
||||
sessionId,
|
||||
category,
|
||||
option.id,
|
||||
resolvedValue,
|
||||
allowedValues,
|
||||
);
|
||||
}
|
||||
|
||||
return this.setSessionConfigOption(sessionId, option.id, resolvedValue);
|
||||
}
|
||||
|
||||
private async hydrateSessionConfigOptions(record: SessionRecord): Promise<SessionRecord> {
|
||||
if (record.configOptions !== undefined) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const info = await this.getAgent(record.agent, { config: true });
|
||||
const configOptions = normalizeSessionConfigOptions(info.configOptions) ?? [];
|
||||
const updated: SessionRecord = {
|
||||
...record,
|
||||
configOptions,
|
||||
};
|
||||
await this.persist.updateSession(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async sendSessionMethod(
|
||||
sessionId: string,
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
options: SessionSendOptions = {},
|
||||
): Promise<{ session: Session; response: unknown }> {
|
||||
return this.sendSessionMethodInternal(sessionId, method, params, options, false);
|
||||
}
|
||||
|
||||
private async sendSessionMethodInternal(
|
||||
sessionId: string,
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
options: SessionSendOptions,
|
||||
allowManagedCancel: boolean,
|
||||
): Promise<{ session: Session; response: unknown }> {
|
||||
if (method === SESSION_CANCEL_METHOD && !allowManagedCancel) {
|
||||
throw new Error(MANUAL_CANCEL_ERROR);
|
||||
}
|
||||
|
||||
const record = await this.persist.getSession(sessionId);
|
||||
if (!record) {
|
||||
throw new Error(`session '${sessionId}' not found`);
|
||||
|
|
@ -642,10 +933,11 @@ export class SandboxAgent {
|
|||
if (!live.hasBoundSession(record.id, record.agentSessionId)) {
|
||||
// The persisted session points at a stale connection; restore lazily.
|
||||
const restored = await this.resumeSession(record.id);
|
||||
return this.sendSessionMethod(restored.id, method, params, options);
|
||||
return this.sendSessionMethodInternal(restored.id, method, params, options, allowManagedCancel);
|
||||
}
|
||||
|
||||
const response = await live.sendSessionMethod(record.id, method, params, options);
|
||||
await this.persistSessionStateFromMethod(record, method, params, response);
|
||||
const refreshed = await this.requireSessionRecord(record.id);
|
||||
return {
|
||||
session: this.upsertSessionHandle(refreshed),
|
||||
|
|
@ -653,6 +945,54 @@ export class SandboxAgent {
|
|||
};
|
||||
}
|
||||
|
||||
private async persistSessionStateFromMethod(
|
||||
record: SessionRecord,
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
response: unknown,
|
||||
): Promise<void> {
|
||||
if (method === "session/set_config_option") {
|
||||
const configOptions = extractConfigOptionsFromSetResponse(response);
|
||||
if (configOptions) {
|
||||
await this.persist.updateSession({
|
||||
...record,
|
||||
configOptions: cloneConfigOptions(configOptions),
|
||||
});
|
||||
} else if (record.configOptions) {
|
||||
// Server didn't return configOptions — optimistically update the
|
||||
// cached currentValue so subsequent getConfigOptions() reflects the
|
||||
// change without a round-trip.
|
||||
const configId = typeof params.configId === "string" ? params.configId : null;
|
||||
const value = typeof params.value === "string" ? params.value : null;
|
||||
if (configId && value) {
|
||||
const updated = applyConfigOptionValue(record.configOptions, configId, value);
|
||||
if (updated) {
|
||||
await this.persist.updateSession({
|
||||
...record,
|
||||
configOptions: updated,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "session/set_mode") {
|
||||
const modeId = typeof params.modeId === "string" ? params.modeId : null;
|
||||
if (!modeId) {
|
||||
return;
|
||||
}
|
||||
const nextModes = applyCurrentMode(record.modes, modeId);
|
||||
if (!nextModes) {
|
||||
return;
|
||||
}
|
||||
await this.persist.updateSession({
|
||||
...record,
|
||||
modes: nextModes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSessionEvent(sessionId: string, listener: SessionEventListener): () => void {
|
||||
const listeners = this.eventListeners.get(sessionId) ?? new Set<SessionEventListener>();
|
||||
listeners.add(listener);
|
||||
|
|
@ -837,6 +1177,7 @@ export class SandboxAgent {
|
|||
};
|
||||
|
||||
await this.persist.insertEvent(event);
|
||||
await this.persistSessionStateFromEvent(localSessionId, envelope, direction);
|
||||
|
||||
const listeners = this.eventListeners.get(localSessionId);
|
||||
if (!listeners || listeners.size === 0) {
|
||||
|
|
@ -848,6 +1189,56 @@ export class SandboxAgent {
|
|||
}
|
||||
}
|
||||
|
||||
private async persistSessionStateFromEvent(
|
||||
sessionId: string,
|
||||
envelope: AnyMessage,
|
||||
direction: AcpEnvelopeDirection,
|
||||
): Promise<void> {
|
||||
if (direction !== "inbound") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (envelopeMethod(envelope) !== "session/update") {
|
||||
return;
|
||||
}
|
||||
|
||||
const update = envelopeSessionUpdate(envelope);
|
||||
if (!update || typeof update.sessionUpdate !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = await this.persist.getSession(sessionId);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.sessionUpdate === "config_option_update") {
|
||||
const configOptions = normalizeSessionConfigOptions(update.configOptions);
|
||||
if (configOptions) {
|
||||
await this.persist.updateSession({
|
||||
...record,
|
||||
configOptions,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.sessionUpdate === "current_mode_update") {
|
||||
const modeId = typeof update.currentModeId === "string" ? update.currentModeId : null;
|
||||
if (!modeId) {
|
||||
return;
|
||||
}
|
||||
const nextModes = applyCurrentMode(record.modes, modeId);
|
||||
if (!nextModes) {
|
||||
return;
|
||||
}
|
||||
await this.persist.updateSession({
|
||||
...record,
|
||||
modes: nextModes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async allocateSessionEventIndex(sessionId: string): Promise<number> {
|
||||
await this.ensureSessionEventIndexSeeded(sessionId);
|
||||
const nextIndex = this.nextSessionEventIndexBySession.get(sessionId) ?? 1;
|
||||
|
|
@ -1230,3 +1621,142 @@ async function readProblem(response: Response): Promise<ProblemDetails | undefin
|
|||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSessionConfigOptions(value: unknown): SessionConfigOption[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.filter(isSessionConfigOption) as SessionConfigOption[];
|
||||
return cloneConfigOptions(normalized) ?? [];
|
||||
}
|
||||
|
||||
function extractConfigOptionsFromSetResponse(response: unknown): SessionConfigOption[] | undefined {
|
||||
if (!isRecord(response)) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeSessionConfigOptions(response.configOptions);
|
||||
}
|
||||
|
||||
function findConfigOptionByCategory(
|
||||
options: SessionConfigOption[],
|
||||
category: string,
|
||||
): SessionConfigOption | undefined {
|
||||
return options.find((option) => option.category === category);
|
||||
}
|
||||
|
||||
function findConfigOptionById(
|
||||
options: SessionConfigOption[],
|
||||
configId: string,
|
||||
): SessionConfigOption | undefined {
|
||||
return options.find((option) => option.id === configId);
|
||||
}
|
||||
|
||||
function uniqueCategories(options: SessionConfigOption[]): string[] {
|
||||
return [...new Set(options.map((option) => option.category).filter((value): value is string => !!value))].sort();
|
||||
}
|
||||
|
||||
function extractConfigValues(option: SessionConfigOption): string[] {
|
||||
if (!isRecord(option) || option.type !== "select" || !Array.isArray(option.options)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const values: string[] = [];
|
||||
for (const entry of option.options as unknown[]) {
|
||||
if (isRecord(entry) && typeof entry.value === "string") {
|
||||
values.push(entry.value);
|
||||
continue;
|
||||
}
|
||||
if (isRecord(entry) && Array.isArray(entry.options)) {
|
||||
for (const nested of entry.options) {
|
||||
if (isRecord(nested) && typeof nested.value === "string") {
|
||||
values.push(nested.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function extractKnownModeIds(modes: SessionModeState | null | undefined): string[] {
|
||||
if (!modes || !Array.isArray(modes.availableModes)) {
|
||||
return [];
|
||||
}
|
||||
return modes.availableModes
|
||||
.map((mode) => (typeof mode.id === "string" ? mode.id : null))
|
||||
.filter((value): value is string => !!value);
|
||||
}
|
||||
|
||||
function applyCurrentMode(
|
||||
modes: SessionModeState | null | undefined,
|
||||
currentModeId: string,
|
||||
): SessionModeState | null {
|
||||
if (modes && Array.isArray(modes.availableModes)) {
|
||||
return {
|
||||
...modes,
|
||||
currentModeId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
currentModeId,
|
||||
availableModes: [],
|
||||
};
|
||||
}
|
||||
|
||||
function applyConfigOptionValue(
|
||||
configOptions: SessionConfigOption[],
|
||||
configId: string,
|
||||
value: string,
|
||||
): SessionConfigOption[] | null {
|
||||
const idx = configOptions.findIndex((o) => o.id === configId);
|
||||
if (idx === -1) {
|
||||
return null;
|
||||
}
|
||||
const updated = cloneConfigOptions(configOptions) ?? [];
|
||||
updated[idx] = { ...updated[idx]!, currentValue: value };
|
||||
return updated;
|
||||
}
|
||||
|
||||
function envelopeSessionUpdate(message: AnyMessage): Record<string, unknown> | null {
|
||||
if (!isRecord(message) || !("params" in message) || !isRecord(message.params)) {
|
||||
return null;
|
||||
}
|
||||
if (!("update" in message.params) || !isRecord(message.params.update)) {
|
||||
return null;
|
||||
}
|
||||
return message.params.update;
|
||||
}
|
||||
|
||||
function cloneConfigOptions(value: SessionConfigOption[] | null | undefined): SessionConfigOption[] | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value)) as SessionConfigOption[];
|
||||
}
|
||||
|
||||
function cloneModes(value: SessionModeState | null | undefined): SessionModeState | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value)) as SessionModeState;
|
||||
}
|
||||
|
||||
function isSessionConfigOption(value: unknown): value is SessionConfigOption {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.id === "string" &&
|
||||
typeof value.name === "string" &&
|
||||
typeof value.type === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function toTitleCase(input: string): string {
|
||||
if (!input) {
|
||||
return "";
|
||||
}
|
||||
return input
|
||||
.split(/[_\s-]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => part[0]!.toUpperCase() + part.slice(1))
|
||||
.join("");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ export {
|
|||
SandboxAgent,
|
||||
SandboxAgentError,
|
||||
Session,
|
||||
UnsupportedSessionCategoryError,
|
||||
UnsupportedSessionConfigOptionError,
|
||||
UnsupportedSessionValueError,
|
||||
} from "./client.ts";
|
||||
|
||||
export { AcpRpcError } from "acp-http-client";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import type { AnyMessage, NewSessionRequest } from "acp-http-client";
|
||||
import type {
|
||||
AnyMessage,
|
||||
NewSessionRequest,
|
||||
SessionConfigOption,
|
||||
SessionModeState,
|
||||
} from "acp-http-client";
|
||||
import type { components, operations } from "./generated/openapi.ts";
|
||||
|
||||
export type ProblemDetails = components["schemas"]["ProblemDetails"];
|
||||
|
|
@ -39,6 +44,8 @@ export interface SessionRecord {
|
|||
createdAt: number;
|
||||
destroyedAt?: number;
|
||||
sessionInit?: Omit<NewSessionRequest, "_meta">;
|
||||
configOptions?: SessionConfigOption[];
|
||||
modes?: SessionModeState | null;
|
||||
}
|
||||
|
||||
export type SessionEventSender = "client" | "agent";
|
||||
|
|
@ -178,6 +185,12 @@ function cloneSessionRecord(session: SessionRecord): SessionRecord {
|
|||
sessionInit: session.sessionInit
|
||||
? (JSON.parse(JSON.stringify(session.sessionInit)) as SessionRecord["sessionInit"])
|
||||
: undefined,
|
||||
configOptions: session.configOptions
|
||||
? (JSON.parse(JSON.stringify(session.configOptions)) as SessionRecord["configOptions"])
|
||||
: undefined,
|
||||
modes: session.modes
|
||||
? (JSON.parse(JSON.stringify(session.modes)) as SessionRecord["modes"])
|
||||
: session.modes,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue