upgrade acp sdk to 1.3.0 (#11218)
This commit is contained in:
@@ -1,24 +1,36 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { GOOSE_SERVE_EXITED_USER_MESSAGE } from '../../gooseServeLeaseRegistry';
|
||||
|
||||
const sdk = vi.hoisted(() => {
|
||||
const mockClientFactory = vi.hoisted(() => {
|
||||
const initialize = vi.fn();
|
||||
const instances: MockGooseClient[] = [];
|
||||
type MockStream = object;
|
||||
type MockClient = {
|
||||
connection: {
|
||||
agent: { request: typeof initialize };
|
||||
closed: Promise<void>;
|
||||
close: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
goose: Record<string, never>;
|
||||
};
|
||||
const instances: Array<{ client: MockClient; resolveClosed: () => void }> = [];
|
||||
const connectGooseAcpClient = vi.fn((_stream: MockStream): MockClient => {
|
||||
let resolveClosed: () => void = () => undefined;
|
||||
const closed = new Promise<void>((resolve) => {
|
||||
resolveClosed = resolve;
|
||||
});
|
||||
const client: MockClient = {
|
||||
connection: {
|
||||
agent: { request: initialize },
|
||||
closed,
|
||||
close: vi.fn(),
|
||||
},
|
||||
goose: {},
|
||||
};
|
||||
instances.push({ client, resolveClosed });
|
||||
return client;
|
||||
});
|
||||
|
||||
class MockGooseClient {
|
||||
readonly initialize = initialize;
|
||||
readonly closed: Promise<void>;
|
||||
resolveClosed: () => void = () => undefined;
|
||||
|
||||
constructor() {
|
||||
this.closed = new Promise<void>((resolve) => {
|
||||
this.resolveClosed = resolve;
|
||||
});
|
||||
instances.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
return { GooseClient: MockGooseClient, initialize, instances };
|
||||
return { connectGooseAcpClient, initialize, instances };
|
||||
});
|
||||
|
||||
const transport = vi.hoisted(() => ({
|
||||
@@ -27,10 +39,13 @@ const transport = vi.hoisted(() => ({
|
||||
|
||||
vi.mock('@aaif/goose-sdk', () => ({
|
||||
DEFAULT_GOOSE_MCP_HOST_CAPABILITIES: {},
|
||||
GooseClient: sdk.GooseClient,
|
||||
}));
|
||||
|
||||
vi.mock('../createWebSocketStream', () => ({
|
||||
vi.mock('../gooseAcpClient', () => ({
|
||||
connectGooseAcpClient: mockClientFactory.connectGooseAcpClient,
|
||||
}));
|
||||
|
||||
vi.mock('@agentclientprotocol/sdk/experimental/ws-client', () => ({
|
||||
createWebSocketStream: transport.createWebSocketStream,
|
||||
}));
|
||||
|
||||
@@ -39,12 +54,11 @@ describe('ACP connection ownership', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.resetModules();
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
||||
sdk.initialize.mockReset().mockResolvedValue({});
|
||||
sdk.instances.length = 0;
|
||||
mockClientFactory.initialize.mockReset().mockResolvedValue({});
|
||||
mockClientFactory.instances.length = 0;
|
||||
transport.createWebSocketStream.mockReset().mockImplementation(() => ({
|
||||
readable: {},
|
||||
writable: {},
|
||||
close: vi.fn(),
|
||||
}));
|
||||
window.electron.getAcpUrl = vi.fn().mockResolvedValue('ws://localhost/acp');
|
||||
});
|
||||
@@ -60,56 +74,56 @@ describe('ACP connection ownership', () => {
|
||||
const [first, second] = await Promise.all([getAcpClient(), getAcpClient()]);
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(sdk.instances).toHaveLength(1);
|
||||
expect(sdk.initialize).toHaveBeenCalledTimes(1);
|
||||
expect(transport.createWebSocketStream).toHaveBeenCalledTimes(1);
|
||||
expect(mockClientFactory.instances).toHaveLength(1);
|
||||
expect(mockClientFactory.initialize).toHaveBeenCalledTimes(1);
|
||||
expect(transport.createWebSocketStream).toHaveBeenCalledWith('ws://localhost/acp', {
|
||||
protocols: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('automatically reconnects after close and shares the result between callers', async () => {
|
||||
const { getAcpClient } = await import('../acpConnection');
|
||||
const firstClient = await getAcpClient();
|
||||
const firstStream = transport.createWebSocketStream.mock.results[0].value;
|
||||
|
||||
sdk.instances[0].resolveClosed();
|
||||
mockClientFactory.instances[0].resolveClosed();
|
||||
await Promise.resolve();
|
||||
const firstCaller = getAcpClient();
|
||||
const secondCaller = getAcpClient();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(249);
|
||||
expect(sdk.instances).toHaveLength(1);
|
||||
expect(mockClientFactory.instances).toHaveLength(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
const [firstResult, secondResult] = await Promise.all([firstCaller, secondCaller]);
|
||||
|
||||
expect(firstStream.close).toHaveBeenCalledOnce();
|
||||
expect(mockClientFactory.instances[0].client.connection.close).toHaveBeenCalledOnce();
|
||||
expect(firstResult).toBe(secondResult);
|
||||
expect(firstResult).not.toBe(firstClient);
|
||||
expect(sdk.instances).toHaveLength(2);
|
||||
expect(mockClientFactory.instances).toHaveLength(2);
|
||||
expect(transport.createWebSocketStream).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('increases the backoff after a failed reconnect attempt', async () => {
|
||||
sdk.initialize
|
||||
mockClientFactory.initialize
|
||||
.mockResolvedValueOnce({})
|
||||
.mockRejectedValueOnce(new Error('server unavailable'))
|
||||
.mockResolvedValueOnce({});
|
||||
const { getAcpClient } = await import('../acpConnection');
|
||||
await getAcpClient();
|
||||
|
||||
sdk.instances[0].resolveClosed();
|
||||
mockClientFactory.instances[0].resolveClosed();
|
||||
await Promise.resolve();
|
||||
const reconnected = getAcpClient();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(sdk.instances).toHaveLength(2);
|
||||
expect(mockClientFactory.instances).toHaveLength(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(499);
|
||||
expect(sdk.instances).toHaveLength(2);
|
||||
expect(mockClientFactory.instances).toHaveLength(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await reconnected;
|
||||
|
||||
expect(sdk.instances).toHaveLength(3);
|
||||
expect(mockClientFactory.instances).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('stops reconnecting when the Goose backend has exited', async () => {
|
||||
@@ -124,7 +138,7 @@ describe('ACP connection ownership', () => {
|
||||
new Error(`Error invoking remote method 'get-acp-url': ${GOOSE_SERVE_EXITED_USER_MESSAGE}`)
|
||||
);
|
||||
window.electron.getAcpUrl = getAcpUrl;
|
||||
sdk.instances[0].resolveClosed();
|
||||
mockClientFactory.instances[0].resolveClosed();
|
||||
await Promise.resolve();
|
||||
|
||||
const connection = expect(getAcpClient()).rejects.toThrow(GOOSE_SERVE_EXITED_USER_MESSAGE);
|
||||
@@ -146,7 +160,7 @@ describe('ACP connection ownership', () => {
|
||||
new Error(`Error invoking remote method 'get-acp-url': ${GOOSE_SERVE_EXITED_USER_MESSAGE}`)
|
||||
);
|
||||
window.electron.getAcpUrl = getAcpUrl;
|
||||
sdk.instances[0].resolveClosed();
|
||||
mockClientFactory.instances[0].resolveClosed();
|
||||
await Promise.resolve();
|
||||
|
||||
const failedRecovery = expect(getAcpClient()).rejects.toThrow(GOOSE_SERVE_EXITED_USER_MESSAGE);
|
||||
@@ -156,20 +170,18 @@ describe('ACP connection ownership', () => {
|
||||
await expect(getAcpClient()).rejects.toThrow(GOOSE_SERVE_EXITED_USER_MESSAGE);
|
||||
|
||||
expect(getAcpUrl).toHaveBeenCalledTimes(2);
|
||||
expect(sdk.instances).toHaveLength(1);
|
||||
expect(mockClientFactory.instances).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('reconnects immediately after system resume', async () => {
|
||||
const { getAcpClient, reconnectAcpAfterSystemResume } = await import('../acpConnection');
|
||||
await getAcpClient();
|
||||
const firstStream = transport.createWebSocketStream.mock.results[0].value;
|
||||
|
||||
reconnectAcpAfterSystemResume();
|
||||
const reconnected = getAcpClient();
|
||||
await reconnected;
|
||||
|
||||
expect(firstStream.close).toHaveBeenCalledOnce();
|
||||
expect(sdk.instances).toHaveLength(2);
|
||||
expect(mockClientFactory.instances[0].client.connection.close).toHaveBeenCalledOnce();
|
||||
expect(mockClientFactory.instances).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('does nothing on system resume before ACP has been used', async () => {
|
||||
@@ -178,12 +190,12 @@ describe('ACP connection ownership', () => {
|
||||
reconnectAcpAfterSystemResume();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(sdk.instances).toHaveLength(0);
|
||||
expect(mockClientFactory.instances).toHaveLength(0);
|
||||
expect(transport.createWebSocketStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses normal backoff when the immediate resume attempt fails', async () => {
|
||||
sdk.initialize
|
||||
mockClientFactory.initialize
|
||||
.mockResolvedValueOnce({})
|
||||
.mockRejectedValueOnce(new Error('network is not ready'))
|
||||
.mockResolvedValueOnce({});
|
||||
@@ -193,30 +205,30 @@ describe('ACP connection ownership', () => {
|
||||
reconnectAcpAfterSystemResume();
|
||||
const reconnected = getAcpClient();
|
||||
await Promise.resolve();
|
||||
expect(sdk.instances).toHaveLength(2);
|
||||
expect(mockClientFactory.instances).toHaveLength(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(249);
|
||||
expect(sdk.instances).toHaveLength(2);
|
||||
expect(mockClientFactory.instances).toHaveLength(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await reconnected;
|
||||
|
||||
expect(sdk.instances).toHaveLength(3);
|
||||
expect(mockClientFactory.instances).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('supersedes an older retry loop after system resume', async () => {
|
||||
const { getAcpClient, reconnectAcpAfterSystemResume } = await import('../acpConnection');
|
||||
await getAcpClient();
|
||||
|
||||
sdk.instances[0].resolveClosed();
|
||||
mockClientFactory.instances[0].resolveClosed();
|
||||
await Promise.resolve();
|
||||
reconnectAcpAfterSystemResume();
|
||||
await getAcpClient();
|
||||
expect(sdk.instances).toHaveLength(2);
|
||||
expect(mockClientFactory.instances).toHaveLength(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
|
||||
expect(sdk.instances).toHaveLength(2);
|
||||
expect(mockClientFactory.instances).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('notifies subscribers while reconnecting and after recovery', async () => {
|
||||
@@ -225,7 +237,7 @@ describe('ACP connection ownership', () => {
|
||||
subscribeToAcpRecovery(listener);
|
||||
await getAcpClient();
|
||||
|
||||
sdk.instances[0].resolveClosed();
|
||||
mockClientFactory.instances[0].resolveClosed();
|
||||
await Promise.resolve();
|
||||
expect(listener).toHaveBeenCalledWith(true);
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
CreateElicitationRequest,
|
||||
RequestPermissionRequest,
|
||||
SessionNotification,
|
||||
} from '@agentclientprotocol/sdk';
|
||||
import type { RequestPermissionRequest, SessionNotification } from '@agentclientprotocol/sdk';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import type { Message } from '../../types/message';
|
||||
@@ -15,6 +11,7 @@ import {
|
||||
acpChatSessionStore,
|
||||
useAcpChatSessionSnapshot,
|
||||
} from '../chatSessionStore';
|
||||
import type { AcpElicitationRequest } from '../elicitationRequests';
|
||||
|
||||
function message(id: string, text: string): Message {
|
||||
return {
|
||||
@@ -67,14 +64,7 @@ function permissionRequest(sessionId: string, toolCallId = 'tool-1'): RequestPer
|
||||
};
|
||||
}
|
||||
|
||||
function elicitationRequest(sessionId: string): {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
request: CreateElicitationRequest & {
|
||||
mode: 'form';
|
||||
sessionId: string;
|
||||
};
|
||||
} {
|
||||
function elicitationRequest(sessionId: string): AcpElicitationRequest {
|
||||
return {
|
||||
id: 'acp_elicitation_1',
|
||||
sessionId,
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createWebSocketStream } from '../createWebSocketStream';
|
||||
|
||||
class FakeWebSocket extends window.EventTarget {
|
||||
static readonly CONNECTING = 0;
|
||||
static readonly OPEN = 1;
|
||||
static readonly CLOSING = 2;
|
||||
static readonly CLOSED = 3;
|
||||
|
||||
readonly sent: string[] = [];
|
||||
readyState = FakeWebSocket.CONNECTING;
|
||||
|
||||
constructor(readonly url: string) {
|
||||
super();
|
||||
fakeWebSockets.push(this);
|
||||
}
|
||||
|
||||
open(): void {
|
||||
this.readyState = FakeWebSocket.OPEN;
|
||||
this.dispatchEvent(new Event('open'));
|
||||
}
|
||||
|
||||
send(message: string): void {
|
||||
this.sent.push(message);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.readyState === FakeWebSocket.CLOSED) {
|
||||
return;
|
||||
}
|
||||
this.readyState = FakeWebSocket.CLOSED;
|
||||
this.dispatchEvent(new Event('close'));
|
||||
}
|
||||
|
||||
fail(): void {
|
||||
this.dispatchEvent(new Event('error'));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
const fakeWebSockets: FakeWebSocket[] = [];
|
||||
|
||||
function latestWebSocket(): FakeWebSocket {
|
||||
const ws = fakeWebSockets[fakeWebSockets.length - 1];
|
||||
if (!ws) {
|
||||
throw new Error('Expected a WebSocket to be created');
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
|
||||
function testRequest() {
|
||||
return {
|
||||
jsonrpc: '2.0' as const,
|
||||
id: 1,
|
||||
method: 'test',
|
||||
};
|
||||
}
|
||||
|
||||
describe('createWebSocketStream', () => {
|
||||
beforeEach(() => {
|
||||
fakeWebSockets.length = 0;
|
||||
vi.stubGlobal('WebSocket', FakeWebSocket);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('waits for the socket to open before sending JSON', async () => {
|
||||
const stream = createWebSocketStream('ws://localhost/acp');
|
||||
const writer = stream.writable.getWriter();
|
||||
const write = writer.write(testRequest());
|
||||
const ws = latestWebSocket();
|
||||
|
||||
expect(ws.sent).toEqual([]);
|
||||
|
||||
ws.open();
|
||||
await write;
|
||||
|
||||
expect(ws.sent).toEqual(['{"jsonrpc":"2.0","id":1,"method":"test"}']);
|
||||
});
|
||||
|
||||
it('closes the readable stream when the socket closes', async () => {
|
||||
const stream = createWebSocketStream('ws://localhost/acp');
|
||||
const reader = stream.readable.getReader();
|
||||
const ws = latestWebSocket();
|
||||
|
||||
ws.open();
|
||||
ws.close();
|
||||
|
||||
await expect(reader.read()).resolves.toEqual({ done: true, value: undefined });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
event: 'closes',
|
||||
trigger: (ws: FakeWebSocket) => ws.close(),
|
||||
error: 'ACP WebSocket closed before connection opened',
|
||||
},
|
||||
{
|
||||
event: 'errors',
|
||||
trigger: (ws: FakeWebSocket) => ws.fail(),
|
||||
error: 'ACP WebSocket connection failed',
|
||||
},
|
||||
])(
|
||||
'rejects a pending write when the socket $event before opening',
|
||||
async ({ trigger, error }) => {
|
||||
const stream = createWebSocketStream('ws://localhost/acp');
|
||||
const writer = stream.writable.getWriter();
|
||||
const write = writer.write(testRequest());
|
||||
|
||||
trigger(latestWebSocket());
|
||||
|
||||
await expect(write).rejects.toThrow(error);
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects a write when the socket has closed', async () => {
|
||||
const stream = createWebSocketStream('ws://localhost/acp');
|
||||
const writer = stream.writable.getWriter();
|
||||
const ws = latestWebSocket();
|
||||
|
||||
ws.open();
|
||||
ws.close();
|
||||
|
||||
await expect(writer.write(testRequest())).rejects.toThrow('ACP WebSocket connection lost');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { AnyMessage, Stream } from '@agentclientprotocol/sdk';
|
||||
import { connectGooseAcpClient, type GooseAcpCallbacks } from '../gooseAcpClient';
|
||||
|
||||
function createTestStream(): Stream & {
|
||||
push(message: AnyMessage): void;
|
||||
writes: AnyMessage[];
|
||||
} {
|
||||
let controller: ReadableStreamDefaultController<AnyMessage> | undefined;
|
||||
const writes: AnyMessage[] = [];
|
||||
|
||||
return {
|
||||
readable: new ReadableStream<AnyMessage>({
|
||||
start(nextController) {
|
||||
controller = nextController;
|
||||
},
|
||||
}),
|
||||
writable: new WritableStream<AnyMessage>({
|
||||
write(message) {
|
||||
writes.push(message);
|
||||
},
|
||||
}),
|
||||
push(message) {
|
||||
controller?.enqueue(message);
|
||||
},
|
||||
writes,
|
||||
};
|
||||
}
|
||||
|
||||
function callbacks(): GooseAcpCallbacks {
|
||||
return {
|
||||
requestPermission: vi.fn().mockResolvedValue({
|
||||
outcome: { outcome: 'selected', optionId: 'allow' },
|
||||
}),
|
||||
sessionUpdate: vi.fn(),
|
||||
unstable_createElicitation: vi.fn(),
|
||||
unstable_sessionRecipeRequestParams: vi.fn().mockResolvedValue({
|
||||
action: 'submit',
|
||||
values: { name: 'Ada' },
|
||||
}),
|
||||
unstable_sessionUpdate: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForWrites(stream: { writes: AnyMessage[] }, count: number): Promise<void> {
|
||||
await vi.waitFor(() => {
|
||||
expect(stream.writes).toHaveLength(count);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Goose ACP client composition', () => {
|
||||
it('registers standard and Goose-specific handlers on a live ACP connection', async () => {
|
||||
const stream = createTestStream();
|
||||
const handlers = callbacks();
|
||||
const client = connectGooseAcpClient(stream, handlers);
|
||||
|
||||
stream.push({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'session/request_permission',
|
||||
params: {
|
||||
sessionId: 'session-1',
|
||||
toolCall: { toolCallId: 'tool-1' },
|
||||
options: [{ optionId: 'allow', name: 'Allow', kind: 'allow_once' }],
|
||||
},
|
||||
});
|
||||
await waitForWrites(stream, 1);
|
||||
|
||||
expect(handlers.requestPermission).toHaveBeenCalledOnce();
|
||||
expect(stream.writes[0]).toMatchObject({
|
||||
id: 1,
|
||||
result: { outcome: { outcome: 'selected', optionId: 'allow' } },
|
||||
});
|
||||
|
||||
stream.push({
|
||||
jsonrpc: '2.0',
|
||||
method: '_goose/unstable/session/update',
|
||||
params: {
|
||||
sessionId: 'session-1',
|
||||
update: {
|
||||
sessionUpdate: 'status_message',
|
||||
status: { type: 'notice', message: 'ready' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(handlers.unstable_sessionUpdate).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
stream.push({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: '_goose/unstable/session/recipe/request-params',
|
||||
params: {
|
||||
sessionId: 'session-1',
|
||||
parameters: [
|
||||
{
|
||||
key: 'name',
|
||||
input_type: 'string',
|
||||
requirement: 'user_prompt',
|
||||
description: 'Name',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await waitForWrites(stream, 2);
|
||||
expect(handlers.unstable_sessionRecipeRequestParams).toHaveBeenCalledOnce();
|
||||
expect(stream.writes[1]).toMatchObject({
|
||||
id: 2,
|
||||
result: { action: 'submit', values: { name: 'Ada' } },
|
||||
});
|
||||
|
||||
const toolsRequest = client.goose.toolsList_unstable({ sessionId: 'session-1' });
|
||||
await waitForWrites(stream, 3);
|
||||
const outboundRequest = stream.writes[2] as { id: number; method: string };
|
||||
expect(outboundRequest.method).toBe('_goose/unstable/tools/list');
|
||||
stream.push({
|
||||
jsonrpc: '2.0',
|
||||
id: outboundRequest.id,
|
||||
result: { tools: [] },
|
||||
});
|
||||
await expect(toolsRequest).resolves.toEqual({ tools: [] });
|
||||
|
||||
client.connection.close();
|
||||
await client.connection.closed;
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { methods } from '@agentclientprotocol/sdk';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getAcpClient } from '../acpConnection';
|
||||
import { acpSetSessionProviderModel } from '../providers';
|
||||
@@ -23,27 +24,31 @@ describe('ACP providers', () => {
|
||||
|
||||
it('sets thinking effort after provider and model, then returns the final config response', async () => {
|
||||
const client = {
|
||||
setSessionConfigOption: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
configOptions: [
|
||||
selectConfigOption('provider', 'anthropic'),
|
||||
selectConfigOption('model', 'provider-default-model'),
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
configOptions: [
|
||||
selectConfigOption('provider', 'anthropic'),
|
||||
selectConfigOption('model', 'claude-sonnet-4-5'),
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
configOptions: [
|
||||
selectConfigOption('provider', 'anthropic'),
|
||||
selectConfigOption('model', 'claude-sonnet-4-5'),
|
||||
selectConfigOption('thinking_effort', 'high'),
|
||||
],
|
||||
}),
|
||||
connection: {
|
||||
agent: {
|
||||
request: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
configOptions: [
|
||||
selectConfigOption('provider', 'anthropic'),
|
||||
selectConfigOption('model', 'provider-default-model'),
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
configOptions: [
|
||||
selectConfigOption('provider', 'anthropic'),
|
||||
selectConfigOption('model', 'claude-sonnet-4-5'),
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
configOptions: [
|
||||
selectConfigOption('provider', 'anthropic'),
|
||||
selectConfigOption('model', 'claude-sonnet-4-5'),
|
||||
selectConfigOption('thinking_effort', 'high'),
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
vi.mocked(getAcpClient).mockResolvedValue(
|
||||
client as unknown as Awaited<ReturnType<typeof getAcpClient>>
|
||||
@@ -56,22 +61,34 @@ describe('ACP providers', () => {
|
||||
'high'
|
||||
);
|
||||
|
||||
expect(client.setSessionConfigOption).toHaveBeenCalledTimes(3);
|
||||
expect(client.setSessionConfigOption).toHaveBeenNthCalledWith(1, {
|
||||
sessionId: 'session-1',
|
||||
configId: 'provider',
|
||||
value: 'anthropic',
|
||||
});
|
||||
expect(client.setSessionConfigOption).toHaveBeenNthCalledWith(2, {
|
||||
sessionId: 'session-1',
|
||||
configId: 'model',
|
||||
value: 'claude-sonnet-4-5',
|
||||
});
|
||||
expect(client.setSessionConfigOption).toHaveBeenNthCalledWith(3, {
|
||||
sessionId: 'session-1',
|
||||
configId: 'thinking_effort',
|
||||
value: 'high',
|
||||
});
|
||||
expect(client.connection.agent.request).toHaveBeenCalledTimes(3);
|
||||
expect(client.connection.agent.request).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
methods.agent.session.setConfigOption,
|
||||
{
|
||||
sessionId: 'session-1',
|
||||
configId: 'provider',
|
||||
value: 'anthropic',
|
||||
}
|
||||
);
|
||||
expect(client.connection.agent.request).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
methods.agent.session.setConfigOption,
|
||||
{
|
||||
sessionId: 'session-1',
|
||||
configId: 'model',
|
||||
value: 'claude-sonnet-4-5',
|
||||
}
|
||||
);
|
||||
expect(client.connection.agent.request).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
methods.agent.session.setConfigOption,
|
||||
{
|
||||
sessionId: 'session-1',
|
||||
configId: 'thinking_effort',
|
||||
value: 'high',
|
||||
}
|
||||
);
|
||||
expect(applied).toEqual({
|
||||
providerId: 'anthropic',
|
||||
modelId: 'claude-sonnet-4-5',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SessionInfo } from '@agentclientprotocol/sdk';
|
||||
import { methods, type SessionInfo } from '@agentclientprotocol/sdk';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getAcpClient } from '../acpConnection';
|
||||
import {
|
||||
@@ -54,13 +54,17 @@ describe('ACP sessions', () => {
|
||||
},
|
||||
});
|
||||
const client = {
|
||||
connection: {
|
||||
agent: {
|
||||
request: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
},
|
||||
goose: {
|
||||
sessionInfo_unstable: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ session: sessionInfo() })
|
||||
.mockResolvedValueOnce({ session: loadedSessionInfo }),
|
||||
},
|
||||
loadSession: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
vi.mocked(getAcpClient).mockResolvedValue(
|
||||
client as unknown as Awaited<ReturnType<typeof getAcpClient>>
|
||||
@@ -68,7 +72,7 @@ describe('ACP sessions', () => {
|
||||
|
||||
const result = await acpLoadSession('session-1');
|
||||
|
||||
expect(client.loadSession).toHaveBeenCalledWith({
|
||||
expect(client.connection.agent.request).toHaveBeenCalledWith(methods.agent.session.load, {
|
||||
sessionId: 'session-1',
|
||||
cwd: '/tmp',
|
||||
mcpServers: [],
|
||||
@@ -84,10 +88,14 @@ describe('ACP sessions', () => {
|
||||
it('carries the recipe parameter scope id in new-session metadata', async () => {
|
||||
const createdSessionInfo = sessionInfo();
|
||||
const client = {
|
||||
connection: {
|
||||
agent: {
|
||||
request: vi.fn().mockResolvedValue({ sessionId: 'session-1' }),
|
||||
},
|
||||
},
|
||||
goose: {
|
||||
sessionInfo_unstable: vi.fn().mockResolvedValue({ session: createdSessionInfo }),
|
||||
},
|
||||
newSession: vi.fn().mockResolvedValue({ sessionId: 'session-1' }),
|
||||
};
|
||||
vi.mocked(getAcpClient).mockResolvedValue(
|
||||
client as unknown as Awaited<ReturnType<typeof getAcpClient>>
|
||||
@@ -98,7 +106,7 @@ describe('ACP sessions', () => {
|
||||
recipeParameterScopeId: 'scope-1',
|
||||
});
|
||||
|
||||
expect(client.newSession).toHaveBeenCalledWith({
|
||||
expect(client.connection.agent.request).toHaveBeenCalledWith(methods.agent.session.new, {
|
||||
cwd: '/tmp',
|
||||
mcpServers: [],
|
||||
_meta: {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import {
|
||||
DEFAULT_GOOSE_MCP_HOST_CAPABILITIES,
|
||||
GooseClient,
|
||||
type GooseClientCallbacks,
|
||||
} from '@aaif/goose-sdk';
|
||||
import { PROTOCOL_VERSION, type InitializeResponse } from '@agentclientprotocol/sdk';
|
||||
import { DEFAULT_GOOSE_MCP_HOST_CAPABILITIES } from '@aaif/goose-sdk';
|
||||
import { methods, PROTOCOL_VERSION, type InitializeResponse } from '@agentclientprotocol/sdk';
|
||||
import { createWebSocketStream } from '@agentclientprotocol/sdk/experimental/ws-client';
|
||||
import packageJson from '../../package.json';
|
||||
import { GOOSE_SERVE_EXITED_USER_MESSAGE } from '../gooseServeLeaseRegistry';
|
||||
import {
|
||||
handleAcpGooseSessionNotification,
|
||||
handleAcpSessionNotification,
|
||||
} from './chatNotifications';
|
||||
import { createWebSocketStream } from './createWebSocketStream';
|
||||
import { requestAcpElicitation } from './elicitationRequests';
|
||||
import {
|
||||
connectGooseAcpClient,
|
||||
type GooseAcpCallbacks,
|
||||
type GooseAcpClient,
|
||||
} from './gooseAcpClient';
|
||||
import { requestAcpPermission } from './permissionRequests';
|
||||
import { requestAcpRecipeParams } from './recipeParamRequests';
|
||||
|
||||
type AcpConnection = {
|
||||
client: GooseClient;
|
||||
stream: ReturnType<typeof createWebSocketStream>;
|
||||
client: GooseAcpClient;
|
||||
initializeResponse: InitializeResponse;
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ let connectionGeneration = 0;
|
||||
let recovering = false;
|
||||
const recoveryListeners = new Set<AcpRecoveryListener>();
|
||||
|
||||
export async function getAcpClient(): Promise<GooseClient> {
|
||||
export async function getAcpClient(): Promise<GooseAcpClient> {
|
||||
return (await getConnection()).client;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ function recoverConnection(immediate: boolean): void {
|
||||
connectionGeneration += 1;
|
||||
currentConnection = null;
|
||||
pendingConnection = null;
|
||||
previousConnection?.stream.close();
|
||||
previousConnection?.client.connection.close();
|
||||
|
||||
const generation = connectionGeneration;
|
||||
const recoveryAttempt = immediate
|
||||
@@ -132,12 +132,13 @@ async function openConnection(generation: number): Promise<AcpConnection> {
|
||||
throw new Error('ACP URL is not available');
|
||||
}
|
||||
|
||||
const stream = createWebSocketStream(wsUrl);
|
||||
const client = new GooseClient(createClientCallbacks(), stream);
|
||||
// Electron treats an explicitly passed undefined protocol as a subprotocol.
|
||||
const stream = createWebSocketStream(wsUrl, { protocols: [] });
|
||||
const client = connectGooseAcpClient(stream, createClientCallbacks());
|
||||
|
||||
try {
|
||||
const initializeResponse = await withTimeout(
|
||||
client.initialize({
|
||||
client.connection.agent.request(methods.agent.initialize, {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
_meta: {
|
||||
'goose/useLoginShellPath': true,
|
||||
@@ -165,17 +166,17 @@ async function openConnection(generation: number): Promise<AcpConnection> {
|
||||
throw new Error('ACP connection attempt is no longer current');
|
||||
}
|
||||
|
||||
const connection = { client, stream, initializeResponse };
|
||||
const connection = { client, initializeResponse };
|
||||
currentConnection = connection;
|
||||
const handleClose = () => {
|
||||
if (currentConnection === connection) {
|
||||
recoverConnection(false);
|
||||
}
|
||||
};
|
||||
connection.client.closed.then(handleClose, handleClose);
|
||||
connection.client.connection.closed.then(handleClose, handleClose);
|
||||
return connection;
|
||||
} catch (error) {
|
||||
stream.close();
|
||||
client.connection.close(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -212,14 +213,14 @@ function delay(delayMs: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
|
||||
function createClientCallbacks(): () => GooseClientCallbacks {
|
||||
return () => ({
|
||||
function createClientCallbacks(): GooseAcpCallbacks {
|
||||
return {
|
||||
requestPermission: requestAcpPermission,
|
||||
unstable_createElicitation: requestAcpElicitation,
|
||||
unstable_sessionRecipeRequestParams: requestAcpRecipeParams,
|
||||
sessionUpdate: handleAcpSessionNotification,
|
||||
unstable_sessionUpdate: handleAcpGooseSessionNotification,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { Stream } from '@aaif/goose-sdk';
|
||||
|
||||
export type ClosableAcpStream = Stream & {
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
export function createWebSocketStream(wsUrl: string): ClosableAcpStream {
|
||||
const ws = new window.WebSocket(wsUrl);
|
||||
|
||||
const incoming: unknown[] = [];
|
||||
const waiters: Array<() => void> = [];
|
||||
let closed = false;
|
||||
|
||||
function pushMessage(message: unknown): void {
|
||||
incoming.push(message);
|
||||
waiters.shift()?.();
|
||||
}
|
||||
|
||||
function waitForMessage(): Promise<void> {
|
||||
if (incoming.length > 0 || closed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => waiters.push(resolve));
|
||||
}
|
||||
|
||||
const openPromise = new Promise<void>((resolve, reject) => {
|
||||
ws.addEventListener('open', () => resolve(), { once: true });
|
||||
ws.addEventListener('error', () => reject(new Error('ACP WebSocket connection failed')), {
|
||||
once: true,
|
||||
});
|
||||
ws.addEventListener(
|
||||
'close',
|
||||
() => reject(new Error('ACP WebSocket closed before connection opened')),
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
|
||||
ws.addEventListener('message', (event) => {
|
||||
if (typeof event.data !== 'string') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
pushMessage(JSON.parse(event.data));
|
||||
} catch {
|
||||
// Ignore malformed messages from the transport.
|
||||
}
|
||||
});
|
||||
|
||||
const closeWaiters = () => {
|
||||
closed = true;
|
||||
for (const waiter of waiters) {
|
||||
waiter();
|
||||
}
|
||||
waiters.length = 0;
|
||||
};
|
||||
|
||||
ws.addEventListener('close', closeWaiters);
|
||||
ws.addEventListener('error', closeWaiters);
|
||||
|
||||
const readable = new window.ReadableStream({
|
||||
async pull(controller) {
|
||||
await waitForMessage();
|
||||
while (incoming.length > 0) {
|
||||
controller.enqueue(incoming.shift());
|
||||
}
|
||||
if (closed && incoming.length === 0) {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const writable = new window.WritableStream({
|
||||
async write(message) {
|
||||
await openPromise;
|
||||
if (closed || ws.readyState !== window.WebSocket.OPEN) {
|
||||
throw new Error('ACP WebSocket connection lost');
|
||||
}
|
||||
ws.send(JSON.stringify(message));
|
||||
},
|
||||
close() {
|
||||
ws.close();
|
||||
},
|
||||
abort() {
|
||||
ws.close();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
readable,
|
||||
writable,
|
||||
close: () => ws.close(),
|
||||
} as ClosableAcpStream;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
client,
|
||||
methods,
|
||||
type Client,
|
||||
type ClientConnection,
|
||||
type Stream,
|
||||
} from '@agentclientprotocol/sdk';
|
||||
import {
|
||||
GOOSE_EXT_AGENT_REQUESTS,
|
||||
GOOSE_EXT_NOTIFICATIONS,
|
||||
GooseExtClient,
|
||||
type GooseSessionNotification_unstable,
|
||||
type RecipeParamsResponse_unstable,
|
||||
type RequestRecipeParams_unstable,
|
||||
zGooseSessionNotification_unstable,
|
||||
zRequestRecipeParams_unstable,
|
||||
} from '@aaif/goose-sdk';
|
||||
|
||||
const [gooseSessionUpdate] = GOOSE_EXT_NOTIFICATIONS;
|
||||
const [gooseRecipeParamsRequest] = GOOSE_EXT_AGENT_REQUESTS;
|
||||
|
||||
export type GooseAcpCallbacks = Required<
|
||||
Pick<Client, 'requestPermission' | 'sessionUpdate' | 'unstable_createElicitation'>
|
||||
> & {
|
||||
unstable_sessionRecipeRequestParams: (
|
||||
request: RequestRecipeParams_unstable
|
||||
) => Promise<RecipeParamsResponse_unstable>;
|
||||
unstable_sessionUpdate: (notification: GooseSessionNotification_unstable) => Promise<void>;
|
||||
};
|
||||
|
||||
export type GooseAcpClient = {
|
||||
connection: ClientConnection;
|
||||
goose: GooseExtClient;
|
||||
};
|
||||
|
||||
export function connectGooseAcpClient(
|
||||
stream: Stream,
|
||||
callbacks: GooseAcpCallbacks
|
||||
): GooseAcpClient {
|
||||
const app = client({ name: 'goose' })
|
||||
.onRequest(methods.client.session.requestPermission, (context) =>
|
||||
callbacks.requestPermission(context.params)
|
||||
)
|
||||
.onNotification(methods.client.session.update, (context) =>
|
||||
callbacks.sessionUpdate(context.params)
|
||||
)
|
||||
.onRequest(methods.client.elicitation.create, (context) =>
|
||||
callbacks.unstable_createElicitation(context.params)
|
||||
)
|
||||
.onRequest(gooseRecipeParamsRequest.method, zRequestRecipeParams_unstable, (context) =>
|
||||
callbacks.unstable_sessionRecipeRequestParams(context.params)
|
||||
)
|
||||
.onNotification(gooseSessionUpdate.method, zGooseSessionNotification_unstable, (context) =>
|
||||
callbacks.unstable_sessionUpdate(context.params)
|
||||
);
|
||||
|
||||
const connection = app.connect(stream);
|
||||
const goose = new GooseExtClient(connection.agent);
|
||||
|
||||
return { connection, goose };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ContentBlock, PromptResponse } from '@agentclientprotocol/sdk';
|
||||
import { methods, type ContentBlock, type PromptResponse } from '@agentclientprotocol/sdk';
|
||||
import type { SteerSessionRequest_unstable, SteerSessionResponse_unstable } from '@aaif/goose-sdk';
|
||||
import type { Message } from '../types/message';
|
||||
import { getAcpClient } from './acpConnection';
|
||||
@@ -8,7 +8,7 @@ export async function acpPromptSession(
|
||||
message: Message
|
||||
): Promise<PromptResponse> {
|
||||
const client = await getAcpClient();
|
||||
return client.prompt({
|
||||
return client.connection.agent.request(methods.agent.session.prompt, {
|
||||
sessionId,
|
||||
prompt: messageToAcpPromptContent(message),
|
||||
});
|
||||
@@ -16,7 +16,7 @@ export async function acpPromptSession(
|
||||
|
||||
export async function acpCancelPrompt(sessionId: string): Promise<void> {
|
||||
const client = await getAcpClient();
|
||||
await client.cancel({ sessionId });
|
||||
await client.connection.agent.notify(methods.agent.session.cancel, { sessionId });
|
||||
}
|
||||
|
||||
export async function acpSteerSession(
|
||||
|
||||
@@ -6,7 +6,12 @@ import type {
|
||||
ProviderTemplateCatalogEntryDto,
|
||||
ProviderTemplateDto,
|
||||
} from '@aaif/goose-sdk';
|
||||
import type { ProviderDetails, ThinkingEffort, UpdateCustomProviderRequest } from '../types/providers';
|
||||
import { methods } from '@agentclientprotocol/sdk';
|
||||
import type {
|
||||
ProviderDetails,
|
||||
ThinkingEffort,
|
||||
UpdateCustomProviderRequest,
|
||||
} from '../types/providers';
|
||||
import { getAcpClient } from './acpConnection';
|
||||
|
||||
export type { CanonicalModelInfoDto, ProviderSecretDto };
|
||||
@@ -260,20 +265,20 @@ export async function acpSetSessionProviderModel(
|
||||
thinkingEffort?: ThinkingEffort | null
|
||||
): Promise<AppliedSessionProviderModel> {
|
||||
const client = await getAcpClient();
|
||||
let response = await client.setSessionConfigOption({
|
||||
let response = await client.connection.agent.request(methods.agent.session.setConfigOption, {
|
||||
sessionId,
|
||||
configId: 'provider',
|
||||
value: providerId,
|
||||
});
|
||||
if (modelId) {
|
||||
response = await client.setSessionConfigOption({
|
||||
response = await client.connection.agent.request(methods.agent.session.setConfigOption, {
|
||||
sessionId,
|
||||
configId: 'model',
|
||||
value: modelId,
|
||||
});
|
||||
}
|
||||
if (thinkingEffort != null) {
|
||||
response = await client.setSessionConfigOption({
|
||||
response = await client.connection.agent.request(methods.agent.session.setConfigOption, {
|
||||
sessionId,
|
||||
configId: 'thinking_effort',
|
||||
value: thinkingEffort,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type {
|
||||
ForkSessionRequest,
|
||||
ListSessionsRequest,
|
||||
LoadSessionResponse,
|
||||
NewSessionRequest,
|
||||
SessionInfo,
|
||||
import {
|
||||
methods,
|
||||
type ForkSessionRequest,
|
||||
type ListSessionsRequest,
|
||||
type LoadSessionResponse,
|
||||
type NewSessionRequest,
|
||||
type SessionInfo,
|
||||
} from '@agentclientprotocol/sdk';
|
||||
import type { GooseExtension, SessionExportFormat, SessionImportSource } from '@aaif/goose-sdk';
|
||||
import { getAcpClient } from './acpConnection';
|
||||
@@ -151,7 +152,7 @@ export async function acpListSessions(
|
||||
meta.query = keyword;
|
||||
}
|
||||
request._meta = meta;
|
||||
const response = await client.listSessions(request);
|
||||
const response = await client.connection.agent.request(methods.agent.session.list, request);
|
||||
return {
|
||||
sessions: response.sessions.map(sessionInfoToListItem),
|
||||
nextCursor: response.nextCursor ?? null,
|
||||
@@ -164,7 +165,9 @@ export async function acpListRecentSessions(maxSessions: number): Promise<Sessio
|
||||
}
|
||||
|
||||
const client = await getAcpClient();
|
||||
const response = await client.listSessions({ _meta: { types: SESSION_LIST_TYPES } });
|
||||
const response = await client.connection.agent.request(methods.agent.session.list, {
|
||||
_meta: { types: SESSION_LIST_TYPES },
|
||||
});
|
||||
return response.sessions.slice(0, maxSessions).map(sessionInfoToListItem);
|
||||
}
|
||||
|
||||
@@ -199,7 +202,7 @@ async function loadAcpSession(sessionId: string): Promise<AcpLoadSessionResult>
|
||||
const client = await getAcpClient();
|
||||
const initialSessionInfoResponse = await client.goose.sessionInfo_unstable({ sessionId });
|
||||
const initialSessionInfo = initialSessionInfoResponse.session;
|
||||
const response = await client.loadSession({
|
||||
const response = await client.connection.agent.request(methods.agent.session.load, {
|
||||
sessionId,
|
||||
cwd: initialSessionInfo.cwd,
|
||||
mcpServers: [],
|
||||
@@ -245,7 +248,7 @@ export async function acpNewSession(
|
||||
meta.recipeParameterScopeId = recipe.recipeParameterScopeId;
|
||||
}
|
||||
const request: NewSessionRequest = { cwd, mcpServers: [], _meta: meta };
|
||||
const response = await client.newSession(request);
|
||||
const response = await client.connection.agent.request(methods.agent.session.new, request);
|
||||
const sessionId = String(response.sessionId);
|
||||
const sessionInfoResponse = await client.goose.sessionInfo_unstable({ sessionId });
|
||||
|
||||
@@ -258,12 +261,12 @@ export async function acpNewSession(
|
||||
|
||||
export async function acpDeleteSession(sessionId: string): Promise<void> {
|
||||
const client = await getAcpClient();
|
||||
await client.goose.sessionDelete({ sessionId });
|
||||
await client.connection.agent.request(methods.agent.session.delete, { sessionId });
|
||||
}
|
||||
|
||||
export async function acpCloseSession(sessionId: string): Promise<void> {
|
||||
const client = await getAcpClient();
|
||||
await client.unstable_closeSession({ sessionId });
|
||||
await client.connection.agent.request(methods.agent.session.close, { sessionId });
|
||||
}
|
||||
|
||||
export async function acpRenameSession(sessionId: string, title: string): Promise<void> {
|
||||
@@ -295,7 +298,7 @@ export async function acpForkSession(
|
||||
if (conversationBefore !== undefined) {
|
||||
request._meta = { conversationBefore };
|
||||
}
|
||||
const response = await client.unstable_forkSession(request);
|
||||
const response = await client.connection.agent.request(methods.agent.session.fork, request);
|
||||
return String(response.sessionId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user