@theia/ai-copilot 1.76.0-next.4 → 1.76.0-next.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -14
- package/lib/browser/copilot-auth-dialog.d.ts.map +1 -1
- package/lib/browser/copilot-auth-dialog.js +19 -11
- package/lib/browser/copilot-auth-dialog.js.map +1 -1
- package/lib/browser/copilot-frontend-application-contribution.d.ts +21 -1
- package/lib/browser/copilot-frontend-application-contribution.d.ts.map +1 -1
- package/lib/browser/copilot-frontend-application-contribution.js +129 -26
- package/lib/browser/copilot-frontend-application-contribution.js.map +1 -1
- package/lib/common/copilot-auth-service.d.ts +27 -24
- package/lib/common/copilot-auth-service.d.ts.map +1 -1
- package/lib/common/copilot-language-models-manager.d.ts +6 -22
- package/lib/common/copilot-language-models-manager.d.ts.map +1 -1
- package/lib/common/copilot-language-models-manager.js +1 -13
- package/lib/common/copilot-language-models-manager.js.map +1 -1
- package/lib/common/copilot-preferences.d.ts +1 -0
- package/lib/common/copilot-preferences.d.ts.map +1 -1
- package/lib/common/copilot-preferences.js +24 -6
- package/lib/common/copilot-preferences.js.map +1 -1
- package/lib/common/index.d.ts +0 -1
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +0 -1
- package/lib/common/index.js.map +1 -1
- package/lib/node/copilot-auth-service-impl.d.ts +25 -16
- package/lib/node/copilot-auth-service-impl.d.ts.map +1 -1
- package/lib/node/copilot-auth-service-impl.js +67 -183
- package/lib/node/copilot-auth-service-impl.js.map +1 -1
- package/lib/node/copilot-auth-service-impl.spec.js +104 -168
- package/lib/node/copilot-auth-service-impl.spec.js.map +1 -1
- package/lib/node/copilot-backend-module.d.ts.map +1 -1
- package/lib/node/copilot-backend-module.js +10 -2
- package/lib/node/copilot-backend-module.js.map +1 -1
- package/lib/node/copilot-cli-auth-provider.d.ts +146 -0
- package/lib/node/copilot-cli-auth-provider.d.ts.map +1 -0
- package/lib/node/copilot-cli-auth-provider.js +430 -0
- package/lib/node/copilot-cli-auth-provider.js.map +1 -0
- package/lib/node/copilot-cli-auth-provider.spec.d.ts +2 -0
- package/lib/node/copilot-cli-auth-provider.spec.d.ts.map +1 -0
- package/lib/node/copilot-cli-auth-provider.spec.js +249 -0
- package/lib/node/copilot-cli-auth-provider.spec.js.map +1 -0
- package/lib/node/copilot-cli-locator.d.ts +120 -0
- package/lib/node/copilot-cli-locator.d.ts.map +1 -0
- package/lib/node/copilot-cli-locator.js +348 -0
- package/lib/node/copilot-cli-locator.js.map +1 -0
- package/lib/node/copilot-cli-locator.spec.d.ts +2 -0
- package/lib/node/copilot-cli-locator.spec.d.ts.map +1 -0
- package/lib/node/copilot-cli-locator.spec.js +327 -0
- package/lib/node/copilot-cli-locator.spec.js.map +1 -0
- package/lib/node/copilot-credential-store.d.ts +45 -0
- package/lib/node/copilot-credential-store.d.ts.map +1 -0
- package/lib/node/copilot-credential-store.js +96 -0
- package/lib/node/copilot-credential-store.js.map +1 -0
- package/lib/node/copilot-credential-store.spec.d.ts +2 -0
- package/lib/node/copilot-credential-store.spec.d.ts.map +1 -0
- package/lib/node/copilot-credential-store.spec.js +93 -0
- package/lib/node/copilot-credential-store.spec.js.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.d.ts +44 -20
- package/lib/node/copilot-language-models-manager-impl.d.ts.map +1 -1
- package/lib/node/copilot-language-models-manager-impl.js +112 -65
- package/lib/node/copilot-language-models-manager-impl.js.map +1 -1
- package/lib/node/copilot-language-models-manager-impl.spec.d.ts +2 -0
- package/lib/node/copilot-language-models-manager-impl.spec.d.ts.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.spec.js +162 -0
- package/lib/node/copilot-language-models-manager-impl.spec.js.map +1 -0
- package/lib/node/copilot-runtime-env.d.ts +20 -0
- package/lib/node/copilot-runtime-env.d.ts.map +1 -0
- package/lib/node/copilot-runtime-env.js +49 -0
- package/lib/node/copilot-runtime-env.js.map +1 -0
- package/lib/node/copilot-sdk-client-provider.d.ts +103 -0
- package/lib/node/copilot-sdk-client-provider.d.ts.map +1 -0
- package/lib/node/copilot-sdk-client-provider.js +247 -0
- package/lib/node/copilot-sdk-client-provider.js.map +1 -0
- package/lib/node/copilot-sdk-client-provider.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-client-provider.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-client-provider.spec.js +215 -0
- package/lib/node/copilot-sdk-client-provider.spec.js.map +1 -0
- package/lib/node/copilot-sdk-language-model.d.ts +85 -0
- package/lib/node/copilot-sdk-language-model.d.ts.map +1 -0
- package/lib/node/copilot-sdk-language-model.js +301 -0
- package/lib/node/copilot-sdk-language-model.js.map +1 -0
- package/lib/node/copilot-sdk-language-model.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-language-model.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-language-model.spec.js +315 -0
- package/lib/node/copilot-sdk-language-model.spec.js.map +1 -0
- package/lib/node/copilot-sdk-loader.d.ts +57 -0
- package/lib/node/copilot-sdk-loader.d.ts.map +1 -0
- package/lib/node/copilot-sdk-loader.js +133 -0
- package/lib/node/copilot-sdk-loader.js.map +1 -0
- package/lib/node/copilot-sdk-loader.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-loader.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-loader.spec.js +113 -0
- package/lib/node/copilot-sdk-loader.spec.js.map +1 -0
- package/lib/node/copilot-sdk-mappers.d.ts +54 -0
- package/lib/node/copilot-sdk-mappers.d.ts.map +1 -0
- package/lib/node/copilot-sdk-mappers.js +158 -0
- package/lib/node/copilot-sdk-mappers.js.map +1 -0
- package/lib/node/copilot-sdk-mappers.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-mappers.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-mappers.spec.js +168 -0
- package/lib/node/copilot-sdk-mappers.spec.js.map +1 -0
- package/lib/node/copilot-sdk-types.d.ts +208 -0
- package/lib/node/copilot-sdk-types.d.ts.map +1 -0
- package/lib/{common/copilot-oauth-config.js → node/copilot-sdk-types.js} +2 -13
- package/lib/node/copilot-sdk-types.js.map +1 -0
- package/lib/node/index.d.ts +9 -1
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +9 -1
- package/lib/node/index.js.map +1 -1
- package/package.json +4 -6
- package/src/browser/copilot-auth-dialog.tsx +20 -16
- package/src/browser/copilot-frontend-application-contribution.ts +144 -31
- package/src/common/copilot-auth-service.ts +28 -24
- package/src/common/copilot-language-models-manager.ts +6 -29
- package/src/common/copilot-preferences.ts +24 -5
- package/src/common/index.ts +0 -1
- package/src/node/copilot-auth-service-impl.spec.ts +122 -207
- package/src/node/copilot-auth-service-impl.ts +63 -218
- package/src/node/copilot-backend-module.ts +11 -2
- package/src/node/copilot-cli-auth-provider.spec.ts +315 -0
- package/src/node/copilot-cli-auth-provider.ts +444 -0
- package/src/node/copilot-cli-locator.spec.ts +386 -0
- package/src/node/copilot-cli-locator.ts +363 -0
- package/src/node/copilot-credential-store.spec.ts +106 -0
- package/src/node/copilot-credential-store.ts +99 -0
- package/src/node/copilot-language-models-manager-impl.spec.ts +188 -0
- package/src/node/copilot-language-models-manager-impl.ts +131 -84
- package/src/node/copilot-runtime-env.ts +46 -0
- package/src/node/copilot-sdk-client-provider.spec.ts +253 -0
- package/src/node/copilot-sdk-client-provider.ts +241 -0
- package/src/node/copilot-sdk-language-model.spec.ts +377 -0
- package/src/node/copilot-sdk-language-model.ts +330 -0
- package/src/node/copilot-sdk-loader.spec.ts +126 -0
- package/src/node/copilot-sdk-loader.ts +142 -0
- package/src/node/copilot-sdk-mappers.spec.ts +190 -0
- package/src/node/copilot-sdk-mappers.ts +172 -0
- package/src/node/copilot-sdk-types.ts +290 -0
- package/src/node/index.ts +9 -1
- package/lib/common/copilot-oauth-config.d.ts +0 -33
- package/lib/common/copilot-oauth-config.d.ts.map +0 -1
- package/lib/common/copilot-oauth-config.js.map +0 -1
- package/lib/node/copilot-language-model.d.ts +0 -34
- package/lib/node/copilot-language-model.d.ts.map +0 -1
- package/lib/node/copilot-language-model.js +0 -236
- package/lib/node/copilot-language-model.js.map +0 -1
- package/lib/node/copilot-language-model.spec.d.ts +0 -2
- package/lib/node/copilot-language-model.spec.d.ts.map +0 -1
- package/lib/node/copilot-language-model.spec.js +0 -89
- package/lib/node/copilot-language-model.spec.js.map +0 -1
- package/src/common/copilot-oauth-config.ts +0 -55
- package/src/node/copilot-language-model.spec.ts +0 -106
- package/src/node/copilot-language-model.ts +0 -273
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import { LanguageModelStreamResponsePart, ToolCallResponsePart, ToolRequest, UserRequest } from '@theia/ai-core';
|
|
19
|
+
import type { CopilotClient, PermissionRequest, Tool } from './copilot-sdk-types';
|
|
20
|
+
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
|
|
21
|
+
import { approveTheiaTools, CopilotSdkLanguageModel, CopilotStreamSink } from './copilot-sdk-language-model';
|
|
22
|
+
|
|
23
|
+
class TestableCopilotSdkLanguageModel extends CopilotSdkLanguageModel {
|
|
24
|
+
constructor() {
|
|
25
|
+
super('copilot/test', 'test-model', { status: 'ready' }, 3, async () => ({} as CopilotClient), new MockLogger());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
callCreateTools(tools: ToolRequest[], sink: CopilotStreamSink): Tool[] {
|
|
29
|
+
return this.createTools(tools, sink);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Captures the session configuration the model would create for a request. */
|
|
33
|
+
async captureSessionConfig(request: UserRequest): Promise<Record<string, unknown>> {
|
|
34
|
+
let captured: Record<string, unknown> = {};
|
|
35
|
+
const client = {
|
|
36
|
+
createSession: async (config: Record<string, unknown>) => {
|
|
37
|
+
captured = config;
|
|
38
|
+
return { on: () => () => { }, send: async () => { }, abort: async () => { }, disconnect: async () => { } };
|
|
39
|
+
}
|
|
40
|
+
} as unknown as CopilotClient;
|
|
41
|
+
const model = new TestableCopilotSdkLanguageModel();
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
(model as any).clientProvider = async () => client;
|
|
44
|
+
await model.request(request);
|
|
45
|
+
return captured;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runs a request against a session that reports itself idle as soon as it was sent to, and
|
|
50
|
+
* reports what the session was configured with, what it was sent, and what was discarded.
|
|
51
|
+
*/
|
|
52
|
+
async captureTurn(request: UserRequest): Promise<{ config: Record<string, unknown>, prompt?: string, attachments?: unknown[], deleted?: string }> {
|
|
53
|
+
let config: Record<string, unknown> = {};
|
|
54
|
+
let prompt: string | undefined;
|
|
55
|
+
let attachments: unknown[] | undefined;
|
|
56
|
+
let deleted: string | undefined;
|
|
57
|
+
const listeners = new Map<string, (event: unknown) => void>();
|
|
58
|
+
const session = {
|
|
59
|
+
sessionId: 'test-session',
|
|
60
|
+
on: (event: string, handler: (payload: unknown) => void) => {
|
|
61
|
+
listeners.set(event, handler);
|
|
62
|
+
return () => listeners.delete(event);
|
|
63
|
+
},
|
|
64
|
+
send: async (message: { prompt: string, attachments?: unknown[] }) => {
|
|
65
|
+
prompt = message.prompt;
|
|
66
|
+
attachments = message.attachments;
|
|
67
|
+
listeners.get('session.idle')?.({});
|
|
68
|
+
},
|
|
69
|
+
abort: async () => { },
|
|
70
|
+
disconnect: async () => { }
|
|
71
|
+
};
|
|
72
|
+
const client = {
|
|
73
|
+
createSession: async (sessionConfig: Record<string, unknown>) => {
|
|
74
|
+
config = sessionConfig;
|
|
75
|
+
return session;
|
|
76
|
+
},
|
|
77
|
+
deleteSession: async (sessionId: string) => { deleted = sessionId; }
|
|
78
|
+
} as unknown as CopilotClient;
|
|
79
|
+
const model = new TestableCopilotSdkLanguageModel();
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
(model as any).clientProvider = async () => client;
|
|
82
|
+
const response = await model.request(request) as { stream: AsyncIterable<LanguageModelStreamResponsePart> };
|
|
83
|
+
for await (const part of response.stream) {
|
|
84
|
+
expect(part).to.exist;
|
|
85
|
+
}
|
|
86
|
+
return { config, prompt, attachments, deleted };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toolRequest(name: string, handler: (args: string, ctx?: { toolCallId?: string }) => Promise<string>): ToolRequest {
|
|
91
|
+
return {
|
|
92
|
+
id: name,
|
|
93
|
+
name,
|
|
94
|
+
description: `the ${name} tool`,
|
|
95
|
+
parameters: { type: 'object', properties: { path: { type: 'string' } } },
|
|
96
|
+
handler
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Runs a tool invocation the way the caller does, with the stream being consumed concurrently.
|
|
102
|
+
* The handler waits for its reported call to be consumed, so nothing may invoke it in isolation.
|
|
103
|
+
*/
|
|
104
|
+
async function invokeWhileConsuming(
|
|
105
|
+
sink: CopilotStreamSink,
|
|
106
|
+
invoke: () => Promise<unknown> | unknown
|
|
107
|
+
): Promise<{ result: unknown, parts: LanguageModelStreamResponsePart[] }> {
|
|
108
|
+
const parts: LanguageModelStreamResponsePart[] = [];
|
|
109
|
+
const consumer = (async () => {
|
|
110
|
+
for await (const part of sink.drain()) {
|
|
111
|
+
parts.push(part);
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
const result = await invoke();
|
|
115
|
+
sink.finish();
|
|
116
|
+
await consumer;
|
|
117
|
+
return { result, parts };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function collect(sink: CopilotStreamSink): Promise<LanguageModelStreamResponsePart[]> {
|
|
121
|
+
const parts: LanguageModelStreamResponsePart[] = [];
|
|
122
|
+
for await (const part of sink.drain()) {
|
|
123
|
+
parts.push(part);
|
|
124
|
+
}
|
|
125
|
+
return parts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toolCalls(part: LanguageModelStreamResponsePart): ToolCallResponsePart['tool_calls'] {
|
|
129
|
+
return (part as ToolCallResponsePart).tool_calls;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
describe('CopilotSdkLanguageModel - createTools', () => {
|
|
133
|
+
|
|
134
|
+
const model = new TestableCopilotSdkLanguageModel();
|
|
135
|
+
|
|
136
|
+
it('should declare the tools of the request to the CLI', () => {
|
|
137
|
+
const tools = model.callCreateTools([toolRequest('readFile', async () => 'content')], new CopilotStreamSink());
|
|
138
|
+
expect(tools).to.have.lengthOf(1);
|
|
139
|
+
expect(tools[0].name).to.equal('readFile');
|
|
140
|
+
expect(tools[0].description).to.equal('the readFile tool');
|
|
141
|
+
expect(tools[0].parameters).to.deep.equal({ type: 'object', properties: { path: { type: 'string' } } });
|
|
142
|
+
// Theia confirms tool invocations itself, so the CLI must not prompt again.
|
|
143
|
+
expect(tools[0].skipPermission).to.be.true;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should pass the raw argument string to the handler of the request', async () => {
|
|
147
|
+
let received: string | undefined;
|
|
148
|
+
const sink = new CopilotStreamSink();
|
|
149
|
+
const tools = model.callCreateTools([toolRequest('readFile', async args => { received = args; return 'content'; })], sink);
|
|
150
|
+
|
|
151
|
+
const { result } = await invokeWhileConsuming(sink, () =>
|
|
152
|
+
tools[0].handler!({ path: 'a.ts' }, { sessionId: 's', toolCallId: 'call_1', toolName: 'readFile', arguments: {} }));
|
|
153
|
+
|
|
154
|
+
expect(received).to.equal(JSON.stringify({ path: 'a.ts' }));
|
|
155
|
+
expect(result).to.equal('content');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should report the invocation as an unfinished and then a finished tool call', async () => {
|
|
159
|
+
const sink = new CopilotStreamSink();
|
|
160
|
+
const tools = model.callCreateTools([toolRequest('readFile', async () => 'content')], sink);
|
|
161
|
+
|
|
162
|
+
const { parts } = await invokeWhileConsuming(sink, () =>
|
|
163
|
+
tools[0].handler!({ path: 'a.ts' }, { sessionId: 's', toolCallId: 'call_1', toolName: 'readFile', arguments: {} }));
|
|
164
|
+
|
|
165
|
+
expect(parts).to.have.lengthOf(2);
|
|
166
|
+
expect(toolCalls(parts[0])[0]).to.deep.include({ id: 'call_1', finished: false });
|
|
167
|
+
expect(toolCalls(parts[0])[0].function).to.deep.equal({ name: 'readFile', arguments: JSON.stringify({ path: 'a.ts' }) });
|
|
168
|
+
expect(toolCalls(parts[1])[0]).to.deep.include({ id: 'call_1', finished: true, result: 'content' });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should report a failing tool to the stream and to the agent instead of throwing', async () => {
|
|
172
|
+
const sink = new CopilotStreamSink();
|
|
173
|
+
const tools = model.callCreateTools([toolRequest('readFile', async () => { throw new Error('no such file'); })], sink);
|
|
174
|
+
|
|
175
|
+
const { result, parts } = await invokeWhileConsuming(sink, () =>
|
|
176
|
+
tools[0].handler!({ path: 'missing.ts' }, { sessionId: 's', toolCallId: 'call_1', toolName: 'readFile', arguments: {} }));
|
|
177
|
+
|
|
178
|
+
expect(result).to.deep.equal({ error: 'no such file' });
|
|
179
|
+
expect(toolCalls(parts[1])[0]).to.deep.include({ id: 'call_1', finished: true, result: 'Error: no such file' });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should return an empty tool list when the request has no tools', () => {
|
|
183
|
+
expect(model.callCreateTools([], new CopilotStreamSink())).to.be.empty;
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('CopilotSdkLanguageModel - session configuration', () => {
|
|
188
|
+
|
|
189
|
+
const model = new TestableCopilotSdkLanguageModel();
|
|
190
|
+
|
|
191
|
+
function requestWith(tools: ToolRequest[]): UserRequest {
|
|
192
|
+
return { messages: [{ actor: 'user', type: 'text', text: 'hi' }], tools } as unknown as UserRequest;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
it('should enable the declared tools and nothing else', async () => {
|
|
196
|
+
const config = await model.captureSessionConfig(requestWith([toolRequest('readFile', async () => 'content')]));
|
|
197
|
+
// An empty list would match nothing and would disable the declared tools as well.
|
|
198
|
+
expect(config.availableTools).to.deep.equal(['custom:*']);
|
|
199
|
+
expect((config.tools as Tool[]).map(tool => tool.name)).to.deep.equal(['readFile']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should declare every tool of the request', async () => {
|
|
203
|
+
const config = await model.captureSessionConfig(requestWith([
|
|
204
|
+
toolRequest('readFile', async () => 'a'),
|
|
205
|
+
toolRequest('listFiles', async () => 'b')
|
|
206
|
+
]));
|
|
207
|
+
expect((config.tools as Tool[]).map(tool => tool.name)).to.deep.equal(['readFile', 'listFiles']);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should still exclude the built-in tools when the request has none', async () => {
|
|
211
|
+
const config = await model.captureSessionConfig(requestWith([]));
|
|
212
|
+
expect(config.availableTools).to.deep.equal(['custom:*']);
|
|
213
|
+
expect(config.tools).to.be.empty;
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('approveTheiaTools', () => {
|
|
218
|
+
|
|
219
|
+
const customToolRequest = { kind: 'custom-tool', toolName: 'readFile' } as unknown as PermissionRequest;
|
|
220
|
+
|
|
221
|
+
it('should approve a request for a tool Theia declared and confirmed itself', () => {
|
|
222
|
+
expect(approveTheiaTools(customToolRequest, { sessionId: 'test-session' })).to.deep.equal({ kind: 'approve-once' });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should report that nobody can confirm a request managed policy reserves for a decision', () => {
|
|
226
|
+
const request = { ...customToolRequest, managedApprovalRequired: true } as unknown as PermissionRequest;
|
|
227
|
+
// Not `no-result`: that leaves the request pending for another client, and there is none here.
|
|
228
|
+
expect(approveTheiaTools(request, { sessionId: 'test-session' })).to.deep.equal({ kind: 'user-not-available' });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should report the same when the machine has managed settings enabled at all', () => {
|
|
232
|
+
expect(approveTheiaTools(customToolRequest, { sessionId: 'test-session', managedSettingsEnabled: true }))
|
|
233
|
+
.to.deep.equal({ kind: 'user-not-available' });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('CopilotSdkLanguageModel - turn', () => {
|
|
238
|
+
|
|
239
|
+
const model = new TestableCopilotSdkLanguageModel();
|
|
240
|
+
|
|
241
|
+
const request = {
|
|
242
|
+
messages: [
|
|
243
|
+
{ actor: 'system', type: 'text', text: 'You are a helpful assistant.' },
|
|
244
|
+
{ actor: 'user', type: 'text', text: 'hi' }
|
|
245
|
+
]
|
|
246
|
+
} as unknown as UserRequest;
|
|
247
|
+
|
|
248
|
+
it('should configure the system prompt as the system message of the session', async () => {
|
|
249
|
+
const { config } = await model.captureTurn(request);
|
|
250
|
+
expect(config.systemMessage).to.deep.include({ mode: 'customize', content: 'You are a helpful assistant.' });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should send the user turn without the system prompt in it', async () => {
|
|
254
|
+
const { prompt } = await model.captureTurn(request);
|
|
255
|
+
expect(prompt).to.equal('hi');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should delete the session it created, which the CLI would otherwise keep', async () => {
|
|
259
|
+
const { deleted } = await model.captureTurn(request);
|
|
260
|
+
expect(deleted).to.equal('test-session');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should send a base64 image of the request as a blob attachment', async () => {
|
|
264
|
+
const requestWithImage = {
|
|
265
|
+
messages: [
|
|
266
|
+
{ actor: 'user', type: 'text', text: 'what is this?' },
|
|
267
|
+
{ actor: 'user', type: 'image', image: { base64data: 'aGk=', mimeType: 'image/png' } }
|
|
268
|
+
]
|
|
269
|
+
} as unknown as UserRequest;
|
|
270
|
+
const { prompt, attachments } = await model.captureTurn(requestWithImage);
|
|
271
|
+
expect(prompt).to.equal('what is this?');
|
|
272
|
+
expect(attachments).to.deep.equal([{ type: 'blob', data: 'aGk=', mimeType: 'image/png' }]);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('CopilotStreamSink', () => {
|
|
277
|
+
|
|
278
|
+
it('should yield parts pushed before draining', async () => {
|
|
279
|
+
const sink = new CopilotStreamSink();
|
|
280
|
+
sink.push({ content: 'a' });
|
|
281
|
+
sink.push({ content: 'b' });
|
|
282
|
+
sink.finish();
|
|
283
|
+
expect(await collect(sink)).to.deep.equal([{ content: 'a' }, { content: 'b' }]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should yield parts pushed while draining', async () => {
|
|
287
|
+
const sink = new CopilotStreamSink();
|
|
288
|
+
const collected = collect(sink);
|
|
289
|
+
sink.push({ content: 'a' });
|
|
290
|
+
await Promise.resolve();
|
|
291
|
+
sink.push({ content: 'b' });
|
|
292
|
+
sink.finish();
|
|
293
|
+
expect(await collected).to.deep.equal([{ content: 'a' }, { content: 'b' }]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should raise the failure after the buffered parts have been drained', async () => {
|
|
297
|
+
const sink = new CopilotStreamSink();
|
|
298
|
+
sink.push({ content: 'partial' });
|
|
299
|
+
sink.finish(new Error('session failed'));
|
|
300
|
+
|
|
301
|
+
const parts: LanguageModelStreamResponsePart[] = [];
|
|
302
|
+
let error: unknown;
|
|
303
|
+
try {
|
|
304
|
+
for await (const part of sink.drain()) {
|
|
305
|
+
parts.push(part);
|
|
306
|
+
}
|
|
307
|
+
} catch (caught) {
|
|
308
|
+
error = caught;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
expect(parts).to.deep.equal([{ content: 'partial' }]);
|
|
312
|
+
expect((error as Error).message).to.equal('session failed');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should end without parts when finished immediately', async () => {
|
|
316
|
+
const sink = new CopilotStreamSink();
|
|
317
|
+
sink.finish();
|
|
318
|
+
expect(await collect(sink)).to.be.empty;
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('CopilotSdkLanguageModel - tool call ordering', () => {
|
|
323
|
+
|
|
324
|
+
const model = new TestableCopilotSdkLanguageModel();
|
|
325
|
+
const invocation = { sessionId: 's', toolCallId: 'call_1', toolName: 'readFile', arguments: {} };
|
|
326
|
+
|
|
327
|
+
it('should report the call before running the tool, so the caller can record it first', async () => {
|
|
328
|
+
const sink = new CopilotStreamSink();
|
|
329
|
+
const observed: string[] = [];
|
|
330
|
+
const tools = model.callCreateTools([toolRequest('readFile', async () => {
|
|
331
|
+
observed.push('tool ran');
|
|
332
|
+
return 'content';
|
|
333
|
+
})], sink);
|
|
334
|
+
|
|
335
|
+
const invoked = tools[0].handler!({ path: 'a.ts' }, invocation);
|
|
336
|
+
|
|
337
|
+
// The consumer has not taken the part yet, so the tool must not have run.
|
|
338
|
+
expect(observed).to.be.empty;
|
|
339
|
+
|
|
340
|
+
const parts: LanguageModelStreamResponsePart[] = [];
|
|
341
|
+
const consumer = (async () => {
|
|
342
|
+
for await (const part of sink.drain()) {
|
|
343
|
+
parts.push(part);
|
|
344
|
+
observed.push('part consumed');
|
|
345
|
+
}
|
|
346
|
+
})();
|
|
347
|
+
|
|
348
|
+
await invoked;
|
|
349
|
+
sink.finish();
|
|
350
|
+
await consumer;
|
|
351
|
+
|
|
352
|
+
expect(observed[0]).to.equal('part consumed');
|
|
353
|
+
expect(observed).to.contain('tool ran');
|
|
354
|
+
expect(toolCalls(parts[0])[0]).to.deep.include({ id: 'call_1', finished: false });
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should pass the call id to the handler so the caller can correlate it', async () => {
|
|
358
|
+
const sink = new CopilotStreamSink();
|
|
359
|
+
let receivedId: string | undefined;
|
|
360
|
+
const tools = model.callCreateTools([toolRequest('readFile', async (_args, ctx) => {
|
|
361
|
+
receivedId = ctx?.toolCallId;
|
|
362
|
+
return 'content';
|
|
363
|
+
})], sink);
|
|
364
|
+
|
|
365
|
+
const invoked = tools[0].handler!({ path: 'a.ts' }, invocation);
|
|
366
|
+
const consumer = (async () => {
|
|
367
|
+
for await (const part of sink.drain()) {
|
|
368
|
+
expect(part).to.exist;
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
371
|
+
await invoked;
|
|
372
|
+
sink.finish();
|
|
373
|
+
await consumer;
|
|
374
|
+
|
|
375
|
+
expect(receivedId).to.equal('call_1');
|
|
376
|
+
});
|
|
377
|
+
});
|