mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-20 05:04:49 +00:00
Fix SDK mode hydration
This commit is contained in:
parent
97d9356932
commit
24e99ac5e7
3 changed files with 88 additions and 290 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentStyle": "space"
|
"indentStyle": "space",
|
||||||
|
"lineWidth": 160
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,7 @@ const DEFAULT_REPLAY_MAX_EVENTS = 50;
|
||||||
const DEFAULT_REPLAY_MAX_CHARS = 12_000;
|
const DEFAULT_REPLAY_MAX_CHARS = 12_000;
|
||||||
const EVENT_INDEX_SCAN_EVENTS_LIMIT = 500;
|
const EVENT_INDEX_SCAN_EVENTS_LIMIT = 500;
|
||||||
const SESSION_CANCEL_METHOD = "session/cancel";
|
const SESSION_CANCEL_METHOD = "session/cancel";
|
||||||
const MANUAL_CANCEL_ERROR =
|
const MANUAL_CANCEL_ERROR = "Manual session/cancel calls are not allowed. Use destroySession(sessionId) instead.";
|
||||||
"Manual session/cancel calls are not allowed. Use destroySession(sessionId) instead.";
|
|
||||||
const HEALTH_WAIT_MIN_DELAY_MS = 500;
|
const HEALTH_WAIT_MIN_DELAY_MS = 500;
|
||||||
const HEALTH_WAIT_MAX_DELAY_MS = 15_000;
|
const HEALTH_WAIT_MAX_DELAY_MS = 15_000;
|
||||||
const HEALTH_WAIT_LOG_AFTER_MS = 5_000;
|
const HEALTH_WAIT_LOG_AFTER_MS = 5_000;
|
||||||
|
|
@ -209,9 +208,7 @@ export class UnsupportedSessionCategoryError extends Error {
|
||||||
readonly availableCategories: string[];
|
readonly availableCategories: string[];
|
||||||
|
|
||||||
constructor(sessionId: string, category: string, availableCategories: string[]) {
|
constructor(sessionId: string, category: string, availableCategories: string[]) {
|
||||||
super(
|
super(`Session '${sessionId}' does not support category '${category}'. Available categories: ${availableCategories.join(", ") || "(none)"}`);
|
||||||
`Session '${sessionId}' does not support category '${category}'. Available categories: ${availableCategories.join(", ") || "(none)"}`,
|
|
||||||
);
|
|
||||||
this.name = "UnsupportedSessionCategoryError";
|
this.name = "UnsupportedSessionCategoryError";
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.category = category;
|
this.category = category;
|
||||||
|
|
@ -226,13 +223,7 @@ export class UnsupportedSessionValueError extends Error {
|
||||||
readonly requestedValue: string;
|
readonly requestedValue: string;
|
||||||
readonly allowedValues: string[];
|
readonly allowedValues: string[];
|
||||||
|
|
||||||
constructor(
|
constructor(sessionId: string, category: string, configId: string, requestedValue: string, allowedValues: string[]) {
|
||||||
sessionId: string,
|
|
||||||
category: string,
|
|
||||||
configId: string,
|
|
||||||
requestedValue: string,
|
|
||||||
allowedValues: string[],
|
|
||||||
) {
|
|
||||||
super(
|
super(
|
||||||
`Session '${sessionId}' does not support value '${requestedValue}' for category '${category}' (configId='${configId}'). Allowed values: ${allowedValues.join(", ") || "(none)"}`,
|
`Session '${sessionId}' does not support value '${requestedValue}' for category '${category}' (configId='${configId}'). Allowed values: ${allowedValues.join(", ") || "(none)"}`,
|
||||||
);
|
);
|
||||||
|
|
@ -251,9 +242,7 @@ export class UnsupportedSessionConfigOptionError extends Error {
|
||||||
readonly availableConfigIds: string[];
|
readonly availableConfigIds: string[];
|
||||||
|
|
||||||
constructor(sessionId: string, configId: string, availableConfigIds: string[]) {
|
constructor(sessionId: string, configId: string, availableConfigIds: string[]) {
|
||||||
super(
|
super(`Session '${sessionId}' does not expose config option '${configId}'. Available configIds: ${availableConfigIds.join(", ") || "(none)"}`);
|
||||||
`Session '${sessionId}' does not expose config option '${configId}'. Available configIds: ${availableConfigIds.join(", ") || "(none)"}`,
|
|
||||||
);
|
|
||||||
this.name = "UnsupportedSessionConfigOptionError";
|
this.name = "UnsupportedSessionConfigOptionError";
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.configId = configId;
|
this.configId = configId;
|
||||||
|
|
@ -267,9 +256,7 @@ export class UnsupportedPermissionReplyError extends Error {
|
||||||
readonly availableReplies: PermissionReply[];
|
readonly availableReplies: PermissionReply[];
|
||||||
|
|
||||||
constructor(permissionId: string, requestedReply: PermissionReply, availableReplies: PermissionReply[]) {
|
constructor(permissionId: string, requestedReply: PermissionReply, availableReplies: PermissionReply[]) {
|
||||||
super(
|
super(`Permission '${permissionId}' does not support reply '${requestedReply}'. Available replies: ${availableReplies.join(", ") || "(none)"}`);
|
||||||
`Permission '${permissionId}' does not support reply '${requestedReply}'. Available replies: ${availableReplies.join(", ") || "(none)"}`,
|
|
||||||
);
|
|
||||||
this.name = "UnsupportedPermissionReplyError";
|
this.name = "UnsupportedPermissionReplyError";
|
||||||
this.permissionId = permissionId;
|
this.permissionId = permissionId;
|
||||||
this.requestedReply = requestedReply;
|
this.requestedReply = requestedReply;
|
||||||
|
|
@ -417,12 +404,7 @@ export class LiveAcpConnection {
|
||||||
agent: string,
|
agent: string,
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
acp: AcpHttpClient,
|
acp: AcpHttpClient,
|
||||||
onObservedEnvelope: (
|
onObservedEnvelope: (connection: LiveAcpConnection, envelope: AnyMessage, direction: AcpEnvelopeDirection, localSessionId: string | null) => void,
|
||||||
connection: LiveAcpConnection,
|
|
||||||
envelope: AnyMessage,
|
|
||||||
direction: AcpEnvelopeDirection,
|
|
||||||
localSessionId: string | null,
|
|
||||||
) => void,
|
|
||||||
onPermissionRequest: (
|
onPermissionRequest: (
|
||||||
connection: LiveAcpConnection,
|
connection: LiveAcpConnection,
|
||||||
localSessionId: string,
|
localSessionId: string,
|
||||||
|
|
@ -444,12 +426,7 @@ export class LiveAcpConnection {
|
||||||
headers?: HeadersInit;
|
headers?: HeadersInit;
|
||||||
agent: string;
|
agent: string;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
onObservedEnvelope: (
|
onObservedEnvelope: (connection: LiveAcpConnection, envelope: AnyMessage, direction: AcpEnvelopeDirection, localSessionId: string | null) => void;
|
||||||
connection: LiveAcpConnection,
|
|
||||||
envelope: AnyMessage,
|
|
||||||
direction: AcpEnvelopeDirection,
|
|
||||||
localSessionId: string | null,
|
|
||||||
) => void;
|
|
||||||
onPermissionRequest: (
|
onPermissionRequest: (
|
||||||
connection: LiveAcpConnection,
|
connection: LiveAcpConnection,
|
||||||
localSessionId: string,
|
localSessionId: string,
|
||||||
|
|
@ -492,13 +469,7 @@ export class LiveAcpConnection {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
live = new LiveAcpConnection(
|
live = new LiveAcpConnection(options.agent, connectionId, acp, options.onObservedEnvelope, options.onPermissionRequest);
|
||||||
options.agent,
|
|
||||||
connectionId,
|
|
||||||
acp,
|
|
||||||
options.onObservedEnvelope,
|
|
||||||
options.onPermissionRequest,
|
|
||||||
);
|
|
||||||
|
|
||||||
const initResult = await acp.initialize({
|
const initResult = await acp.initialize({
|
||||||
protocolVersion: PROTOCOL_VERSION,
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
|
@ -541,10 +512,7 @@ export class LiveAcpConnection {
|
||||||
this.pendingReplayByLocalSessionId.set(localSessionId, replayText);
|
this.pendingReplayByLocalSessionId.set(localSessionId, replayText);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createRemoteSession(
|
async createRemoteSession(localSessionId: string, sessionInit: Omit<NewSessionRequest, "_meta">): Promise<NewSessionResponse> {
|
||||||
localSessionId: string,
|
|
||||||
sessionInit: Omit<NewSessionRequest, "_meta">,
|
|
||||||
): Promise<NewSessionResponse> {
|
|
||||||
const createStartedAt = Date.now();
|
const createStartedAt = Date.now();
|
||||||
this.pendingNewSessionLocals.push(localSessionId);
|
this.pendingNewSessionLocals.push(localSessionId);
|
||||||
|
|
||||||
|
|
@ -566,12 +534,7 @@ export class LiveAcpConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendSessionMethod(
|
async sendSessionMethod(localSessionId: string, method: string, params: Record<string, unknown>, options: SessionSendOptions): Promise<unknown> {
|
||||||
localSessionId: string,
|
|
||||||
method: string,
|
|
||||||
params: Record<string, unknown>,
|
|
||||||
options: SessionSendOptions,
|
|
||||||
): Promise<unknown> {
|
|
||||||
const agentSessionId = this.sessionByLocalId.get(localSessionId);
|
const agentSessionId = this.sessionByLocalId.get(localSessionId);
|
||||||
if (!agentSessionId) {
|
if (!agentSessionId) {
|
||||||
throw new Error(`session '${localSessionId}' is not bound to live ACP connection '${this.connectionId}'`);
|
throw new Error(`session '${localSessionId}' is not bound to live ACP connection '${this.connectionId}'`);
|
||||||
|
|
@ -632,21 +595,14 @@ export class LiveAcpConnection {
|
||||||
this.lastAdapterExitAt = Date.now();
|
this.lastAdapterExitAt = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handlePermissionRequest(
|
private async handlePermissionRequest(request: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
||||||
request: RequestPermissionRequest,
|
|
||||||
): Promise<RequestPermissionResponse> {
|
|
||||||
const agentSessionId = request.sessionId;
|
const agentSessionId = request.sessionId;
|
||||||
const localSessionId = this.localByAgentSessionId.get(agentSessionId);
|
const localSessionId = this.localByAgentSessionId.get(agentSessionId);
|
||||||
if (!localSessionId) {
|
if (!localSessionId) {
|
||||||
return cancelledPermissionResponse();
|
return cancelledPermissionResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.onPermissionRequest(
|
return this.onPermissionRequest(this, localSessionId, agentSessionId, clonePermissionRequest(request));
|
||||||
this,
|
|
||||||
localSessionId,
|
|
||||||
agentSessionId,
|
|
||||||
clonePermissionRequest(request),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveSessionId(envelope: AnyMessage, direction: AcpEnvelopeDirection): string | null {
|
private resolveSessionId(envelope: AnyMessage, direction: AcpEnvelopeDirection): string | null {
|
||||||
|
|
@ -1108,10 +1064,7 @@ export class SandboxAgent {
|
||||||
return this.upsertSessionHandle(updated);
|
return this.upsertSessionHandle(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSessionMode(
|
async setSessionMode(sessionId: string, modeId: string): Promise<{ session: Session; response: SetSessionModeResponse | void }> {
|
||||||
sessionId: string,
|
|
||||||
modeId: string,
|
|
||||||
): Promise<{ session: Session; response: SetSessionModeResponse | void }> {
|
|
||||||
const mode = modeId.trim();
|
const mode = modeId.trim();
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
throw new Error("setSessionMode requires a non-empty modeId");
|
throw new Error("setSessionMode requires a non-empty modeId");
|
||||||
|
|
@ -1124,13 +1077,10 @@ export class SandboxAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return (await this.sendSessionMethodInternal(
|
return (await this.sendSessionMethodInternal(sessionId, "session/set_mode", { modeId: mode }, {}, false)) as {
|
||||||
sessionId,
|
session: Session;
|
||||||
"session/set_mode",
|
response: SetSessionModeResponse | void;
|
||||||
{ modeId: mode },
|
};
|
||||||
{},
|
|
||||||
false,
|
|
||||||
)) as { session: Session; response: SetSessionModeResponse | void };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof AcpRpcError) || error.code !== -32601) {
|
if (!(error instanceof AcpRpcError) || error.code !== -32601) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -1139,11 +1089,7 @@ export class SandboxAgent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSessionConfigOption(
|
async setSessionConfigOption(sessionId: string, configId: string, value: string): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||||
sessionId: string,
|
|
||||||
configId: string,
|
|
||||||
value: string,
|
|
||||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
|
||||||
const resolvedConfigId = configId.trim();
|
const resolvedConfigId = configId.trim();
|
||||||
if (!resolvedConfigId) {
|
if (!resolvedConfigId) {
|
||||||
throw new Error("setSessionConfigOption requires a non-empty configId");
|
throw new Error("setSessionConfigOption requires a non-empty configId");
|
||||||
|
|
@ -1165,13 +1111,7 @@ export class SandboxAgent {
|
||||||
|
|
||||||
const allowedValues = extractConfigValues(option);
|
const allowedValues = extractConfigValues(option);
|
||||||
if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
|
if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
|
||||||
throw new UnsupportedSessionValueError(
|
throw new UnsupportedSessionValueError(sessionId, option.category ?? "uncategorized", option.id, resolvedValue, allowedValues);
|
||||||
sessionId,
|
|
||||||
option.category ?? "uncategorized",
|
|
||||||
option.id,
|
|
||||||
resolvedValue,
|
|
||||||
allowedValues,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await this.sendSessionMethodInternal(
|
return (await this.sendSessionMethodInternal(
|
||||||
|
|
@ -1186,17 +1126,11 @@ export class SandboxAgent {
|
||||||
)) as { session: Session; response: SetSessionConfigOptionResponse };
|
)) as { session: Session; response: SetSessionConfigOptionResponse };
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSessionModel(
|
async setSessionModel(sessionId: string, model: string): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||||
sessionId: string,
|
|
||||||
model: string,
|
|
||||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
|
||||||
return this.setSessionCategoryValue(sessionId, "model", model);
|
return this.setSessionCategoryValue(sessionId, "model", model);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSessionThoughtLevel(
|
async setSessionThoughtLevel(sessionId: string, thoughtLevel: string): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||||
sessionId: string,
|
|
||||||
thoughtLevel: string,
|
|
||||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
|
||||||
return this.setSessionCategoryValue(sessionId, "thought_level", thoughtLevel);
|
return this.setSessionCategoryValue(sessionId, "thought_level", thoughtLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1249,13 +1183,7 @@ export class SandboxAgent {
|
||||||
|
|
||||||
const allowedValues = extractConfigValues(option);
|
const allowedValues = extractConfigValues(option);
|
||||||
if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
|
if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
|
||||||
throw new UnsupportedSessionValueError(
|
throw new UnsupportedSessionValueError(sessionId, category, option.id, resolvedValue, allowedValues);
|
||||||
sessionId,
|
|
||||||
category,
|
|
||||||
option.id,
|
|
||||||
resolvedValue,
|
|
||||||
allowedValues,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.setSessionConfigOption(sessionId, option.id, resolvedValue);
|
return this.setSessionConfigOption(sessionId, option.id, resolvedValue);
|
||||||
|
|
@ -1267,16 +1195,26 @@ export class SandboxAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = await this.getAgent(snapshot.agent, { config: true });
|
const info = await this.getAgent(snapshot.agent, { config: true });
|
||||||
const configOptions = normalizeSessionConfigOptions(info.configOptions) ?? [];
|
let configOptions = normalizeSessionConfigOptions(info.configOptions) ?? [];
|
||||||
// Re-read the record from persistence so we merge against the latest
|
// Re-read the record from persistence so we merge against the latest
|
||||||
// state, not a stale snapshot captured before the network await.
|
// state, not a stale snapshot captured before the network await.
|
||||||
const record = await this.persist.getSession(sessionId);
|
const record = await this.persist.getSession(sessionId);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return { ...snapshot, configOptions };
|
return { ...snapshot, configOptions };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentModeId = record.modes?.currentModeId;
|
||||||
|
if (currentModeId) {
|
||||||
|
const modeOption = findConfigOptionByCategory(configOptions, "mode");
|
||||||
|
if (modeOption) {
|
||||||
|
configOptions = applyConfigOptionValue(configOptions, modeOption.id, currentModeId) ?? configOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updated: SessionRecord = {
|
const updated: SessionRecord = {
|
||||||
...record,
|
...record,
|
||||||
configOptions,
|
configOptions,
|
||||||
|
modes: deriveModesFromConfigOptions(configOptions) ?? record.modes,
|
||||||
};
|
};
|
||||||
await this.persist.updateSession(updated);
|
await this.persist.updateSession(updated);
|
||||||
return updated;
|
return updated;
|
||||||
|
|
@ -1323,12 +1261,7 @@ export class SandboxAgent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async persistSessionStateFromMethod(
|
private async persistSessionStateFromMethod(sessionId: string, method: string, params: Record<string, unknown>, response: unknown): Promise<void> {
|
||||||
sessionId: string,
|
|
||||||
method: string,
|
|
||||||
params: Record<string, unknown>,
|
|
||||||
response: unknown,
|
|
||||||
): Promise<void> {
|
|
||||||
// Re-read the record from persistence so we merge against the latest
|
// Re-read the record from persistence so we merge against the latest
|
||||||
// state, not a stale snapshot captured before the RPC await.
|
// state, not a stale snapshot captured before the RPC await.
|
||||||
const record = await this.persist.getSession(sessionId);
|
const record = await this.persist.getSession(sessionId);
|
||||||
|
|
@ -1624,21 +1557,13 @@ export class SandboxAgent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async followProcessLogs(
|
async followProcessLogs(id: string, listener: ProcessLogListener, query: ProcessLogFollowQuery = {}): Promise<ProcessLogSubscription> {
|
||||||
id: string,
|
|
||||||
listener: ProcessLogListener,
|
|
||||||
query: ProcessLogFollowQuery = {},
|
|
||||||
): Promise<ProcessLogSubscription> {
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const response = await this.requestRaw(
|
const response = await this.requestRaw("GET", `${API_PREFIX}/processes/${encodeURIComponent(id)}/logs`, {
|
||||||
"GET",
|
query: { ...query, follow: true },
|
||||||
`${API_PREFIX}/processes/${encodeURIComponent(id)}/logs`,
|
accept: "text/event-stream",
|
||||||
{
|
signal: abortController.signal,
|
||||||
query: { ...query, follow: true },
|
});
|
||||||
accept: "text/event-stream",
|
|
||||||
signal: abortController.signal,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.body) {
|
if (!response.body) {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
|
|
@ -1659,23 +1584,13 @@ export class SandboxAgent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async resizeProcessTerminal(
|
async resizeProcessTerminal(id: string, request: ProcessTerminalResizeRequest): Promise<ProcessTerminalResizeResponse> {
|
||||||
id: string,
|
return this.requestJson("POST", `${API_PREFIX}/processes/${encodeURIComponent(id)}/terminal/resize`, {
|
||||||
request: ProcessTerminalResizeRequest,
|
body: request,
|
||||||
): Promise<ProcessTerminalResizeResponse> {
|
});
|
||||||
return this.requestJson(
|
|
||||||
"POST",
|
|
||||||
`${API_PREFIX}/processes/${encodeURIComponent(id)}/terminal/resize`,
|
|
||||||
{
|
|
||||||
body: request,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildProcessTerminalWebSocketUrl(
|
buildProcessTerminalWebSocketUrl(id: string, options: ProcessTerminalWebSocketUrlOptions = {}): string {
|
||||||
id: string,
|
|
||||||
options: ProcessTerminalWebSocketUrlOptions = {},
|
|
||||||
): string {
|
|
||||||
return toWebSocketUrl(
|
return toWebSocketUrl(
|
||||||
this.buildUrl(`${API_PREFIX}/processes/${encodeURIComponent(id)}/terminal/ws`, {
|
this.buildUrl(`${API_PREFIX}/processes/${encodeURIComponent(id)}/terminal/ws`, {
|
||||||
access_token: options.accessToken ?? this.token,
|
access_token: options.accessToken ?? this.token,
|
||||||
|
|
@ -1683,10 +1598,7 @@ export class SandboxAgent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectProcessTerminalWebSocket(
|
connectProcessTerminalWebSocket(id: string, options: ProcessTerminalConnectOptions = {}): WebSocket {
|
||||||
id: string,
|
|
||||||
options: ProcessTerminalConnectOptions = {},
|
|
||||||
): WebSocket {
|
|
||||||
const WebSocketCtor = options.WebSocket ?? globalThis.WebSocket;
|
const WebSocketCtor = options.WebSocket ?? globalThis.WebSocket;
|
||||||
if (!WebSocketCtor) {
|
if (!WebSocketCtor) {
|
||||||
throw new Error("WebSocket API is not available; provide a WebSocket implementation.");
|
throw new Error("WebSocket API is not available; provide a WebSocket implementation.");
|
||||||
|
|
@ -1700,10 +1612,7 @@ export class SandboxAgent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectProcessTerminal(
|
connectProcessTerminal(id: string, options: ProcessTerminalSessionOptions = {}): ProcessTerminalSession {
|
||||||
id: string,
|
|
||||||
options: ProcessTerminalSessionOptions = {},
|
|
||||||
): ProcessTerminalSession {
|
|
||||||
return new ProcessTerminalSession(this.connectProcessTerminalWebSocket(id, options));
|
return new ProcessTerminalSession(this.connectProcessTerminalWebSocket(id, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1789,11 +1698,7 @@ export class SandboxAgent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async persistSessionStateFromEvent(
|
private async persistSessionStateFromEvent(sessionId: string, envelope: AnyMessage, direction: AcpEnvelopeDirection): Promise<void> {
|
||||||
sessionId: string,
|
|
||||||
envelope: AnyMessage,
|
|
||||||
direction: AcpEnvelopeDirection,
|
|
||||||
): Promise<void> {
|
|
||||||
if (direction !== "inbound") {
|
if (direction !== "inbound") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2081,12 +1986,9 @@ export class SandboxAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runHealthWait(): Promise<void> {
|
private async runHealthWait(): Promise<void> {
|
||||||
const signal = this.healthWait.enabled
|
const signal = this.healthWait.enabled ? anyAbortSignal([this.healthWait.signal, this.healthWaitAbortController.signal]) : undefined;
|
||||||
? anyAbortSignal([this.healthWait.signal, this.healthWaitAbortController.signal])
|
|
||||||
: undefined;
|
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const deadline =
|
const deadline = typeof this.healthWait.timeoutMs === "number" ? startedAt + this.healthWait.timeoutMs : undefined;
|
||||||
typeof this.healthWait.timeoutMs === "number" ? startedAt + this.healthWait.timeoutMs : undefined;
|
|
||||||
|
|
||||||
let delayMs = HEALTH_WAIT_MIN_DELAY_MS;
|
let delayMs = HEALTH_WAIT_MIN_DELAY_MS;
|
||||||
let nextLogAt = startedAt + HEALTH_WAIT_LOG_AFTER_MS;
|
let nextLogAt = startedAt + HEALTH_WAIT_LOG_AFTER_MS;
|
||||||
|
|
@ -2111,9 +2013,7 @@ export class SandboxAgent {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now >= nextLogAt) {
|
if (now >= nextLogAt) {
|
||||||
const details = formatHealthWaitError(lastError);
|
const details = formatHealthWaitError(lastError);
|
||||||
console.warn(
|
console.warn(`sandbox-agent at ${this.baseUrl} is not healthy after ${now - startedAt}ms; still waiting (${details})`);
|
||||||
`sandbox-agent at ${this.baseUrl} is not healthy after ${now - startedAt}ms; still waiting (${details})`,
|
|
||||||
);
|
|
||||||
nextLogAt = now + HEALTH_WAIT_LOG_EVERY_MS;
|
nextLogAt = now + HEALTH_WAIT_LOG_EVERY_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2125,9 +2025,7 @@ export class SandboxAgent {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(`Timed out waiting for sandbox-agent health after ${this.healthWait.timeoutMs}ms (${formatHealthWaitError(lastError)})`);
|
||||||
`Timed out waiting for sandbox-agent health after ${this.healthWait.timeoutMs}ms (${formatHealthWaitError(lastError)})`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildHeaders(extra?: HeadersInit): Headers {
|
private buildHeaders(extra?: HeadersInit): Headers {
|
||||||
|
|
@ -2189,9 +2087,7 @@ type RequestOptions = {
|
||||||
skipReadyWait?: boolean;
|
skipReadyWait?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NormalizedHealthWaitOptions =
|
type NormalizedHealthWaitOptions = { enabled: false; timeoutMs?: undefined; signal?: undefined } | { enabled: true; timeoutMs?: number; signal?: AbortSignal };
|
||||||
| { enabled: false; timeoutMs?: undefined; signal?: undefined }
|
|
||||||
| { enabled: true; timeoutMs?: number; signal?: AbortSignal };
|
|
||||||
|
|
||||||
function parseProcessTerminalServerFrame(payload: string): ProcessTerminalServerFrame | null {
|
function parseProcessTerminalServerFrame(payload: string): ProcessTerminalServerFrame | null {
|
||||||
try {
|
try {
|
||||||
|
|
@ -2204,12 +2100,7 @@ function parseProcessTerminalServerFrame(payload: string): ProcessTerminalServer
|
||||||
return parsed as ProcessTerminalServerFrame;
|
return parsed as ProcessTerminalServerFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (parsed.type === "exit" && (parsed.exitCode === undefined || parsed.exitCode === null || typeof parsed.exitCode === "number")) {
|
||||||
parsed.type === "exit" &&
|
|
||||||
(parsed.exitCode === undefined ||
|
|
||||||
parsed.exitCode === null ||
|
|
||||||
typeof parsed.exitCode === "number")
|
|
||||||
) {
|
|
||||||
return parsed as ProcessTerminalServerFrame;
|
return parsed as ProcessTerminalServerFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2223,9 +2114,7 @@ function parseProcessTerminalServerFrame(payload: string): ProcessTerminalServer
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeTerminalInput(
|
function encodeTerminalInput(data: string | ArrayBuffer | ArrayBufferView): { data: string; encoding?: "base64" } {
|
||||||
data: string | ArrayBuffer | ArrayBufferView,
|
|
||||||
): { data: string; encoding?: "base64" } {
|
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
return { data };
|
return { data };
|
||||||
}
|
}
|
||||||
|
|
@ -2286,12 +2175,7 @@ async function autoAuthenticate(acp: AcpHttpClient, methods: AuthMethod[]): Prom
|
||||||
// Only attempt env-var-based methods that the server process can satisfy
|
// Only attempt env-var-based methods that the server process can satisfy
|
||||||
// automatically. Interactive methods (e.g. "claude-login") cannot be
|
// automatically. Interactive methods (e.g. "claude-login") cannot be
|
||||||
// fulfilled programmatically and must be skipped.
|
// fulfilled programmatically and must be skipped.
|
||||||
const envBased = methods.find(
|
const envBased = methods.find((m) => m.id === "codex-api-key" || m.id === "openai-api-key" || m.id === "anthropic-api-key");
|
||||||
(m) =>
|
|
||||||
m.id === "codex-api-key" ||
|
|
||||||
m.id === "openai-api-key" ||
|
|
||||||
m.id === "anthropic-api-key",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!envBased) {
|
if (!envBased) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -2316,9 +2200,7 @@ function toAgentQuery(options: AgentQueryOptions | undefined): Record<string, Qu
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSessionInit(
|
function normalizeSessionInit(value: Omit<NewSessionRequest, "_meta"> | undefined): Omit<NewSessionRequest, "_meta"> {
|
||||||
value: Omit<NewSessionRequest, "_meta"> | undefined,
|
|
||||||
): Omit<NewSessionRequest, "_meta"> {
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return {
|
return {
|
||||||
cwd: defaultCwd(),
|
cwd: defaultCwd(),
|
||||||
|
|
@ -2354,8 +2236,7 @@ function buildReplayText(events: SessionEvent[], maxChars: number): string | nul
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix =
|
const prefix = "Previous session history is replayed below as JSON-RPC envelopes. Use it as context before responding to the latest user prompt.\n";
|
||||||
"Previous session history is replayed below as JSON-RPC envelopes. Use it as context before responding to the latest user prompt.\n";
|
|
||||||
let text = prefix;
|
let text = prefix;
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
|
|
@ -2469,10 +2350,7 @@ function normalizePositiveInt(value: number | undefined, fallback: number): numb
|
||||||
return Math.floor(value as number);
|
return Math.floor(value as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeHealthWaitOptions(
|
function normalizeHealthWaitOptions(value: boolean | SandboxAgentHealthWaitOptions | undefined, signal: AbortSignal | undefined): NormalizedHealthWaitOptions {
|
||||||
value: boolean | SandboxAgentHealthWaitOptions | undefined,
|
|
||||||
signal: AbortSignal | undefined,
|
|
||||||
): NormalizedHealthWaitOptions {
|
|
||||||
if (value === false) {
|
if (value === false) {
|
||||||
return { enabled: false };
|
return { enabled: false };
|
||||||
}
|
}
|
||||||
|
|
@ -2481,10 +2359,7 @@ function normalizeHealthWaitOptions(
|
||||||
return { enabled: true, signal };
|
return { enabled: true, signal };
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutMs =
|
const timeoutMs = typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs) && value.timeoutMs > 0 ? Math.floor(value.timeoutMs) : undefined;
|
||||||
typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs) && value.timeoutMs > 0
|
|
||||||
? Math.floor(value.timeoutMs)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -2538,17 +2413,11 @@ function extractConfigOptionsFromSetResponse(response: unknown): SessionConfigOp
|
||||||
return normalizeSessionConfigOptions(response.configOptions);
|
return normalizeSessionConfigOptions(response.configOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findConfigOptionByCategory(
|
function findConfigOptionByCategory(options: SessionConfigOption[], category: string): SessionConfigOption | undefined {
|
||||||
options: SessionConfigOption[],
|
|
||||||
category: string,
|
|
||||||
): SessionConfigOption | undefined {
|
|
||||||
return options.find((option) => option.category === category);
|
return options.find((option) => option.category === category);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findConfigOptionById(
|
function findConfigOptionById(options: SessionConfigOption[], configId: string): SessionConfigOption | undefined {
|
||||||
options: SessionConfigOption[],
|
|
||||||
configId: string,
|
|
||||||
): SessionConfigOption | undefined {
|
|
||||||
return options.find((option) => option.id === configId);
|
return options.find((option) => option.id === configId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2583,14 +2452,10 @@ function extractKnownModeIds(modes: SessionModeState | null | undefined): string
|
||||||
if (!modes || !Array.isArray(modes.availableModes)) {
|
if (!modes || !Array.isArray(modes.availableModes)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return modes.availableModes
|
return modes.availableModes.map((mode) => (typeof mode.id === "string" ? mode.id : null)).filter((value): value is string => !!value);
|
||||||
.map((mode) => (typeof mode.id === "string" ? mode.id : null))
|
|
||||||
.filter((value): value is string => !!value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deriveModesFromConfigOptions(
|
function deriveModesFromConfigOptions(configOptions: SessionConfigOption[] | undefined): SessionModeState | null {
|
||||||
configOptions: SessionConfigOption[] | undefined,
|
|
||||||
): SessionModeState | null {
|
|
||||||
if (!configOptions || configOptions.length === 0) {
|
if (!configOptions || configOptions.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -2609,18 +2474,12 @@ function deriveModesFromConfigOptions(
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentModeId:
|
currentModeId: typeof modeOption.currentValue === "string" && modeOption.currentValue.length > 0 ? modeOption.currentValue : (availableModes[0]?.id ?? ""),
|
||||||
typeof modeOption.currentValue === "string" && modeOption.currentValue.length > 0
|
|
||||||
? modeOption.currentValue
|
|
||||||
: availableModes[0]?.id ?? "",
|
|
||||||
availableModes,
|
availableModes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCurrentMode(
|
function applyCurrentMode(modes: SessionModeState | null | undefined, currentModeId: string): SessionModeState | null {
|
||||||
modes: SessionModeState | null | undefined,
|
|
||||||
currentModeId: string,
|
|
||||||
): SessionModeState | null {
|
|
||||||
if (modes && Array.isArray(modes.availableModes)) {
|
if (modes && Array.isArray(modes.availableModes)) {
|
||||||
return {
|
return {
|
||||||
...modes,
|
...modes,
|
||||||
|
|
@ -2633,11 +2492,7 @@ function applyCurrentMode(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyConfigOptionValue(
|
function applyConfigOptionValue(configOptions: SessionConfigOption[], configId: string, value: string): SessionConfigOption[] | null {
|
||||||
configOptions: SessionConfigOption[],
|
|
||||||
configId: string,
|
|
||||||
value: string,
|
|
||||||
): SessionConfigOption[] | null {
|
|
||||||
const idx = configOptions.findIndex((o) => o.id === configId);
|
const idx = configOptions.findIndex((o) => o.id === configId);
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -2704,28 +2559,16 @@ function availablePermissionReplies(options: PermissionOption[]): PermissionRepl
|
||||||
return [...replies];
|
return [...replies];
|
||||||
}
|
}
|
||||||
|
|
||||||
function permissionReplyToResponse(
|
function permissionReplyToResponse(permissionId: string, request: RequestPermissionRequest, reply: PermissionReply): RequestPermissionResponse {
|
||||||
permissionId: string,
|
|
||||||
request: RequestPermissionRequest,
|
|
||||||
reply: PermissionReply,
|
|
||||||
): RequestPermissionResponse {
|
|
||||||
const preferredKinds: PermissionOptionKind[] =
|
const preferredKinds: PermissionOptionKind[] =
|
||||||
reply === "once"
|
reply === "once" ? ["allow_once"] : reply === "always" ? ["allow_always", "allow_once"] : ["reject_once", "reject_always"];
|
||||||
? ["allow_once"]
|
|
||||||
: reply === "always"
|
|
||||||
? ["allow_always", "allow_once"]
|
|
||||||
: ["reject_once", "reject_always"];
|
|
||||||
|
|
||||||
const selected = preferredKinds
|
const selected = preferredKinds
|
||||||
.map((kind) => request.options.find((option) => option.kind === kind))
|
.map((kind) => request.options.find((option) => option.kind === kind))
|
||||||
.find((option): option is PermissionOption => Boolean(option));
|
.find((option): option is PermissionOption => Boolean(option));
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
throw new UnsupportedPermissionReplyError(
|
throw new UnsupportedPermissionReplyError(permissionId, reply, availablePermissionReplies(request.options));
|
||||||
permissionId,
|
|
||||||
reply,
|
|
||||||
availablePermissionReplies(request.options),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -2745,12 +2588,7 @@ function cancelledPermissionResponse(): RequestPermissionResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSessionConfigOption(value: unknown): value is SessionConfigOption {
|
function isSessionConfigOption(value: unknown): value is SessionConfigOption {
|
||||||
return (
|
return isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && typeof value.type === "string";
|
||||||
isRecord(value) &&
|
|
||||||
typeof value.id === "string" &&
|
|
||||||
typeof value.name === "string" &&
|
|
||||||
typeof value.type === "string"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toTitleCase(input: string): string {
|
function toTitleCase(input: string): string {
|
||||||
|
|
@ -2850,11 +2688,7 @@ async function waitForAbortable<T>(promise: Promise<T>, signal: AbortSignal | un
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function consumeProcessLogSse(
|
async function consumeProcessLogSse(body: ReadableStream<Uint8Array>, listener: ProcessLogListener, signal: AbortSignal): Promise<void> {
|
||||||
body: ReadableStream<Uint8Array>,
|
|
||||||
listener: ProcessLogListener,
|
|
||||||
signal: AbortSignal,
|
|
||||||
): Promise<void> {
|
|
||||||
const reader = body.getReader();
|
const reader = body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,7 @@ import { dirname, resolve } from "node:path";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import {
|
import { InMemorySessionPersistDriver, SandboxAgent, type SessionEvent } from "../src/index.ts";
|
||||||
InMemorySessionPersistDriver,
|
|
||||||
SandboxAgent,
|
|
||||||
type SessionEvent,
|
|
||||||
} from "../src/index.ts";
|
|
||||||
import { spawnSandboxAgent, isNodeRuntime, type SandboxAgentSpawnHandle } from "../src/spawn.ts";
|
import { spawnSandboxAgent, isNodeRuntime, type SandboxAgentSpawnHandle } from "../src/spawn.ts";
|
||||||
import { prepareMockAgentDataHome } from "./helpers/mock-agent.ts";
|
import { prepareMockAgentDataHome } from "./helpers/mock-agent.ts";
|
||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
|
|
@ -21,10 +17,7 @@ function findBinary(): string | null {
|
||||||
return process.env.SANDBOX_AGENT_BIN;
|
return process.env.SANDBOX_AGENT_BIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cargoPaths = [
|
const cargoPaths = [resolve(__dirname, "../../../target/debug/sandbox-agent"), resolve(__dirname, "../../../target/release/sandbox-agent")];
|
||||||
resolve(__dirname, "../../../target/debug/sandbox-agent"),
|
|
||||||
resolve(__dirname, "../../../target/release/sandbox-agent"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const p of cargoPaths) {
|
for (const p of cargoPaths) {
|
||||||
if (existsSync(p)) {
|
if (existsSync(p)) {
|
||||||
|
|
@ -37,9 +30,7 @@ function findBinary(): string | null {
|
||||||
|
|
||||||
const BINARY_PATH = findBinary();
|
const BINARY_PATH = findBinary();
|
||||||
if (!BINARY_PATH) {
|
if (!BINARY_PATH) {
|
||||||
throw new Error(
|
throw new Error("sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.");
|
||||||
"sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!process.env.SANDBOX_AGENT_BIN) {
|
if (!process.env.SANDBOX_AGENT_BIN) {
|
||||||
process.env.SANDBOX_AGENT_BIN = BINARY_PATH;
|
process.env.SANDBOX_AGENT_BIN = BINARY_PATH;
|
||||||
|
|
@ -49,11 +40,7 @@ function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitFor<T>(
|
async function waitFor<T>(fn: () => T | undefined | null, timeoutMs = 6000, stepMs = 30): Promise<T> {
|
||||||
fn: () => T | undefined | null,
|
|
||||||
timeoutMs = 6000,
|
|
||||||
stepMs = 30,
|
|
||||||
): Promise<T> {
|
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
while (Date.now() - started < timeoutMs) {
|
while (Date.now() - started < timeoutMs) {
|
||||||
const value = fn();
|
const value = fn();
|
||||||
|
|
@ -65,11 +52,7 @@ async function waitFor<T>(
|
||||||
throw new Error("timed out waiting for condition");
|
throw new Error("timed out waiting for condition");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForAsync<T>(
|
async function waitForAsync<T>(fn: () => Promise<T | undefined | null>, timeoutMs = 6000, stepMs = 30): Promise<T> {
|
||||||
fn: () => Promise<T | undefined | null>,
|
|
||||||
timeoutMs = 6000,
|
|
||||||
stepMs = 30,
|
|
||||||
): Promise<T> {
|
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
while (Date.now() - started < timeoutMs) {
|
while (Date.now() - started < timeoutMs) {
|
||||||
const value = await fn();
|
const value = await fn();
|
||||||
|
|
@ -265,10 +248,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
||||||
});
|
});
|
||||||
expect(moved.to).toBe(movedPath);
|
expect(moved.to).toBe(movedPath);
|
||||||
|
|
||||||
const uploadResult = await sdk.uploadFsBatch(
|
const uploadResult = await sdk.uploadFsBatch(buildTarArchive([{ name: "batch.txt", content: "batch upload works" }]), { path: uploadDir });
|
||||||
buildTarArchive([{ name: "batch.txt", content: "batch upload works" }]),
|
|
||||||
{ path: uploadDir },
|
|
||||||
);
|
|
||||||
expect(uploadResult.paths.some((path) => path.endsWith("batch.txt"))).toBe(true);
|
expect(uploadResult.paths.some((path) => path.endsWith("batch.txt"))).toBe(true);
|
||||||
|
|
||||||
const uploaded = await sdk.readFsFile({ path: join(uploadDir, "batch.txt") });
|
const uploaded = await sdk.readFsFile({ path: join(uploadDir, "batch.txt") });
|
||||||
|
|
@ -316,9 +296,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
it("requires baseUrl when fetch is not provided", async () => {
|
it("requires baseUrl when fetch is not provided", async () => {
|
||||||
await expect(SandboxAgent.connect({ token } as any)).rejects.toThrow(
|
await expect(SandboxAgent.connect({ token } as any)).rejects.toThrow("baseUrl is required unless fetch is provided.");
|
||||||
"baseUrl is required unless fetch is provided.",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("waits for health before non-ACP HTTP helpers", async () => {
|
it("waits for health before non-ACP HTTP helpers", async () => {
|
||||||
|
|
@ -357,11 +335,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
||||||
|
|
||||||
const firstAgentsRequest = seenPaths.indexOf("/v1/agents");
|
const firstAgentsRequest = seenPaths.indexOf("/v1/agents");
|
||||||
expect(firstAgentsRequest).toBeGreaterThanOrEqual(0);
|
expect(firstAgentsRequest).toBeGreaterThanOrEqual(0);
|
||||||
expect(seenPaths.slice(0, firstAgentsRequest)).toEqual([
|
expect(seenPaths.slice(0, firstAgentsRequest)).toEqual(["/v1/health", "/v1/health", "/v1/health"]);
|
||||||
"/v1/health",
|
|
||||||
"/v1/health",
|
|
||||||
"/v1/health",
|
|
||||||
]);
|
|
||||||
|
|
||||||
await sdk.dispose();
|
await sdk.dispose();
|
||||||
});
|
});
|
||||||
|
|
@ -469,11 +443,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
||||||
const params = payload.params as Record<string, unknown> | undefined;
|
const params = payload.params as Record<string, unknown> | undefined;
|
||||||
const prompt = Array.isArray(params?.prompt) ? params?.prompt : [];
|
const prompt = Array.isArray(params?.prompt) ? params?.prompt : [];
|
||||||
const firstBlock = prompt[0] as Record<string, unknown> | undefined;
|
const firstBlock = prompt[0] as Record<string, unknown> | undefined;
|
||||||
return (
|
return method === "session/prompt" && typeof firstBlock?.text === "string" && firstBlock.text.includes("Previous session history is replayed below");
|
||||||
method === "session/prompt" &&
|
|
||||||
typeof firstBlock?.text === "string" &&
|
|
||||||
firstBlock.text.includes("Previous session history is replayed below")
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(replayInjected).toBeTruthy();
|
expect(replayInjected).toBeTruthy();
|
||||||
|
|
@ -512,12 +482,8 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
||||||
|
|
||||||
const session = await sdk.createSession({ agent: "mock" });
|
const session = await sdk.createSession({ agent: "mock" });
|
||||||
|
|
||||||
await expect(session.rawSend("session/cancel")).rejects.toThrow(
|
await expect(session.rawSend("session/cancel")).rejects.toThrow("Use destroySession(sessionId) instead.");
|
||||||
"Use destroySession(sessionId) instead.",
|
await expect(sdk.rawSendSessionMethod(session.id, "session/cancel", {})).rejects.toThrow("Use destroySession(sessionId) instead.");
|
||||||
);
|
|
||||||
await expect(sdk.rawSendSessionMethod(session.id, "session/cancel", {})).rejects.toThrow(
|
|
||||||
"Use destroySession(sessionId) instead.",
|
|
||||||
);
|
|
||||||
|
|
||||||
const destroyed = await sdk.destroySession(session.id);
|
const destroyed = await sdk.destroySession(session.id);
|
||||||
expect(destroyed.destroyedAt).toBeDefined();
|
expect(destroyed.destroyedAt).toBeDefined();
|
||||||
|
|
@ -574,6 +540,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
||||||
|
|
||||||
const modes = await session.getModes();
|
const modes = await session.getModes();
|
||||||
expect(modes?.currentModeId).toBe("plan");
|
expect(modes?.currentModeId).toBe("plan");
|
||||||
|
expect((await session.getConfigOptions()).find((o) => o.category === "mode")?.currentValue).toBe("plan");
|
||||||
|
|
||||||
await sdk.dispose();
|
await sdk.dispose();
|
||||||
});
|
});
|
||||||
|
|
@ -775,13 +742,9 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
||||||
|
|
||||||
const initialLogs = await waitForAsync(async () => {
|
const initialLogs = await waitForAsync(async () => {
|
||||||
const logs = await sdk.getProcessLogs(interactiveProcess.id, { tail: 10 });
|
const logs = await sdk.getProcessLogs(interactiveProcess.id, { tail: 10 });
|
||||||
return logs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready"))
|
return logs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready")) ? logs : undefined;
|
||||||
? logs
|
|
||||||
: undefined;
|
|
||||||
});
|
});
|
||||||
expect(
|
expect(initialLogs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready"))).toBe(true);
|
||||||
initialLogs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready")),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
const followedLogs: string[] = [];
|
const followedLogs: string[] = [];
|
||||||
const subscription = await sdk.followProcessLogs(
|
const subscription = await sdk.followProcessLogs(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue