@lowire/loop 0.0.2 → 0.0.4

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.
@@ -15,47 +15,208 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.Github = exports.kEditorHeaders = void 0;
19
- const openaiCompletions_1 = require("./openaiCompletions");
20
- exports.kEditorHeaders = {
21
- 'Editor-Version': 'vscode/1.96.0',
22
- 'Editor-Plugin-Version': 'copilot-chat/0.24.0',
23
- 'User-Agent': 'GitHubCopilotChat/0.24.0',
24
- 'Accept': 'application/json',
25
- 'Content-Type': 'application/json'
26
- };
27
- // Copilot endpoint does not reply with content+tool_call, it instead
28
- // replies with the content and expects continuation. I.e. instead of navigating
29
- // to a page it will reply with "Navigating to <url>" w/o tool call. Mitigate it
30
- // via injecting a tool call intent and then converting it into the assistant
31
- // message content.
32
- class Github extends openaiCompletions_1.OpenAICompletions {
33
- name = 'copilot';
34
- async connect() {
18
+ exports.kEditorHeaders = exports.Github = void 0;
19
+ class Github {
20
+ name = 'github';
21
+ _apiKey;
22
+ async _bearer() {
23
+ if (!this._apiKey)
24
+ this._apiKey = await getCopilotToken();
25
+ return this._apiKey;
26
+ }
27
+ async complete(conversation, options) {
28
+ // Convert generic messages to OpenAI format
29
+ const systemMessage = {
30
+ role: 'system',
31
+ content: systemPrompt(conversation.systemPrompt)
32
+ };
33
+ const openaiMessages = [systemMessage, ...conversation.messages.map(toCopilotMessages).flat()];
34
+ const openaiTools = conversation.tools.map(t => toCopilotTool(t));
35
+ const bearer = await this._bearer();
36
+ let response;
37
+ // Github provider is unreliable, retry up to 3 times.
38
+ for (let i = 0; i < 3; ++i) {
39
+ response = await create({
40
+ model: options.model,
41
+ max_tokens: options.maxTokens,
42
+ temperature: options.temperature,
43
+ messages: openaiMessages,
44
+ tools: openaiTools,
45
+ tool_choice: conversation.tools.length > 0 ? 'auto' : undefined,
46
+ reasoning_effort: options.reasoning ? 'medium' : undefined,
47
+ parallel_tool_calls: false,
48
+ }, bearer, options);
49
+ if (response.choices.length)
50
+ break;
51
+ }
52
+ if (!response || !response.choices.length)
53
+ throw new Error('Failed to get response from GitHub Copilot');
54
+ const result = { role: 'assistant', content: [] };
55
+ const message = response.choices[0].message;
56
+ if (message.content)
57
+ result.content.push({ type: 'text', text: message.content });
58
+ for (const entry of message.tool_calls || []) {
59
+ if (entry.type !== 'function')
60
+ continue;
61
+ const { toolCall, intent } = toToolCall(entry);
62
+ if (intent)
63
+ result.content.push({ type: 'text', text: intent, copilotToolCallId: toolCall.id });
64
+ result.content.push(toolCall);
65
+ }
66
+ const usage = {
67
+ input: response.usage?.prompt_tokens ?? 0,
68
+ output: response.usage?.completion_tokens ?? 0,
69
+ };
70
+ return { result, usage };
71
+ }
72
+ }
73
+ exports.Github = Github;
74
+ async function create(createParams, bearer, options) {
75
+ const headers = {
76
+ 'Authorization': `Bearer ${bearer}`,
77
+ ...exports.kEditorHeaders,
78
+ };
79
+ const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
80
+ options.debug?.('lowire:github')('Request:', JSON.stringify(debugBody, null, 2));
81
+ const response = await fetch(`https://api.githubcopilot.com/chat/completions`, {
82
+ method: 'POST',
83
+ headers,
84
+ body: JSON.stringify(createParams),
85
+ });
86
+ if (!response.ok) {
87
+ options.debug?.('lowire:github')('Response:', response.status);
88
+ throw new Error(`API error: ${response.status} ${response.statusText} ${await response.text()}`);
89
+ }
90
+ const responseBody = await response.json();
91
+ options.debug?.('lowire:github')('Response:', JSON.stringify(responseBody, null, 2));
92
+ return responseBody;
93
+ }
94
+ function toCopilotResultContentPart(part) {
95
+ if (part.type === 'text') {
35
96
  return {
36
- baseUrl: 'https://api.githubcopilot.com',
37
- apiKey: await getCopilotToken(),
38
- headers: exports.kEditorHeaders
97
+ type: 'text',
98
+ text: part.text,
39
99
  };
40
100
  }
41
- async complete(conversation, options) {
42
- const message = await super.complete(conversation, { ...options, injectIntent: true });
43
- const textPart = message.result.content.find(part => part.type === 'text');
44
- if (!textPart) {
45
- const content = [];
46
- const toolCalls = message.result.content.filter(part => part.type === 'tool_call');
47
- for (const toolCall of toolCalls) {
48
- content.push(toolCall.arguments?._intent ?? '');
49
- delete toolCall.arguments._intent;
101
+ if (part.type === 'image') {
102
+ return {
103
+ type: 'image_url',
104
+ image_url: {
105
+ url: `data:${part.mimeType};base64,${part.data}`,
106
+ },
107
+ };
108
+ }
109
+ throw new Error(`Cannot convert content part of type ${part.type} to text content part`);
110
+ }
111
+ function toCopilotMessages(message) {
112
+ if (message.role === 'user') {
113
+ return [{
114
+ role: 'user',
115
+ content: message.content
116
+ }];
117
+ }
118
+ if (message.role === 'assistant') {
119
+ const assistantMessage = {
120
+ role: 'assistant'
121
+ };
122
+ const toolIntents = new Map();
123
+ for (const part of message.content) {
124
+ if (part.type === 'text' && part.copilotToolCallId)
125
+ toolIntents.set(part.copilotToolCallId, part.text);
126
+ }
127
+ const textParts = message.content.filter(part => part.type === 'text' && !part.copilotToolCallId);
128
+ const toolCallParts = message.content.filter(part => part.type === 'tool_call');
129
+ if (textParts.length === 1)
130
+ assistantMessage.content = textParts[0].text;
131
+ else
132
+ assistantMessage.content = textParts;
133
+ const toolCalls = [];
134
+ const toolResultMessages = [];
135
+ for (const toolCall of toolCallParts) {
136
+ const args = { ...toolCall.arguments };
137
+ if (toolIntents.has(toolCall.id))
138
+ args['_intent'] = toolIntents.get(toolCall.id);
139
+ toolCalls.push({
140
+ id: toolCall.id,
141
+ type: 'function',
142
+ function: {
143
+ name: toolCall.name,
144
+ arguments: JSON.stringify(args)
145
+ }
146
+ });
147
+ if (toolCall.result) {
148
+ toolResultMessages.push({
149
+ role: 'tool',
150
+ tool_call_id: toolCall.id,
151
+ content: toolCall.result.content.map(toCopilotResultContentPart),
152
+ });
50
153
  }
51
- const text = content.join(' ').trim();
52
- if (text.trim())
53
- message.result.content.unshift({ type: 'text', text: content.join(' ') });
54
154
  }
55
- return message;
155
+ if (toolCalls.length > 0)
156
+ assistantMessage.tool_calls = toolCalls;
157
+ if (message.toolError) {
158
+ toolResultMessages.push({
159
+ role: 'user',
160
+ content: [{
161
+ type: 'text',
162
+ text: message.toolError,
163
+ }]
164
+ });
165
+ }
166
+ return [assistantMessage, ...toolResultMessages];
56
167
  }
168
+ throw new Error(`Unsupported message role: ${message.role}`);
57
169
  }
58
- exports.Github = Github;
170
+ // Copilot endpoint does not reply with content+tool_call, it instead
171
+ // replies with the content and expects continuation. I.e. instead of navigating
172
+ // to a page it will reply with "Navigating to <url>" w/o tool call. Mitigate it
173
+ // via injecting a tool call intent and then converting it into the assistant
174
+ // message content.
175
+ function toCopilotTool(tool) {
176
+ const parameters = { ...tool.inputSchema };
177
+ parameters.properties = {
178
+ _intent: { type: 'string', description: 'Describe the intent of this tool call' },
179
+ ...parameters.properties || {},
180
+ };
181
+ return {
182
+ type: 'function',
183
+ function: {
184
+ name: tool.name,
185
+ description: tool.description,
186
+ parameters,
187
+ },
188
+ };
189
+ }
190
+ function toToolCall(entry) {
191
+ const toolCall = {
192
+ type: 'tool_call',
193
+ name: entry.type === 'function' ? entry.function.name : entry.custom.name,
194
+ arguments: JSON.parse(entry.type === 'function' ? entry.function.arguments : entry.custom.input),
195
+ id: entry.id,
196
+ };
197
+ const intent = toolCall.arguments['_intent'];
198
+ delete toolCall.arguments['_intent'];
199
+ return { toolCall, intent };
200
+ }
201
+ const systemPrompt = (prompt) => `
202
+ ### System instructions
203
+
204
+ ${prompt}
205
+
206
+ ### Tool calling instructions
207
+ - Your reply MUST be a tool call and nothing but the tool call.
208
+ - NEVER respond with text content, only tool calls.
209
+ - Do NOT describe your plan, do NOT explain what you are doing, do NOT describe what you see, call tools.
210
+ - Provide thoughts in the '_intent' property of the tool calls instead.
211
+ `;
212
+ exports.kEditorHeaders = {
213
+ 'Editor-Version': 'vscode/1.96.0',
214
+ 'Editor-Plugin-Version': 'copilot-chat/0.24.0',
215
+ 'User-Agent': 'GitHubCopilotChat/0.24.0',
216
+ 'Accept': 'application/json',
217
+ 'Content-Type': 'application/json',
218
+ 'Copilot-Vision-Request': 'true',
219
+ };
59
220
  async function getCopilotToken() {
60
221
  const response = await fetch('https://api.github.com/copilot_internal/v2/token', {
61
222
  method: 'GET',
@@ -30,7 +30,7 @@ class Google {
30
30
  contents,
31
31
  tools: conversation.tools.length > 0 ? [{ functionDeclarations: conversation.tools.map(toGeminiTool) }] : undefined,
32
32
  generationConfig: { temperature: options.temperature },
33
- });
33
+ }, options);
34
34
  const [candidate] = response.candidates ?? [];
35
35
  if (!candidate)
36
36
  throw new Error('No candidates in response');
@@ -43,21 +43,27 @@ class Google {
43
43
  }
44
44
  }
45
45
  exports.Google = Google;
46
- async function create(model, body) {
46
+ async function create(model, createParams, options) {
47
47
  const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
48
48
  if (!apiKey)
49
49
  throw new Error('GEMINI_API_KEY or GOOGLE_API_KEY environment variable is required');
50
+ const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
51
+ options.debug?.('lowire:google')('Request:', JSON.stringify(debugBody, null, 2));
50
52
  const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, {
51
53
  method: 'POST',
52
54
  headers: {
53
55
  'Content-Type': 'application/json',
54
56
  'x-goog-api-key': apiKey,
55
57
  },
56
- body: JSON.stringify(body)
58
+ body: JSON.stringify(createParams)
57
59
  });
58
- if (!response.ok)
60
+ if (!response.ok) {
61
+ options.debug?.('lowire:google')('Response:', response.status);
59
62
  throw new Error(`API error: ${response.status} ${response.statusText} ${await response.text()}`);
60
- return await response.json();
63
+ }
64
+ const responseBody = await response.json();
65
+ options.debug?.('lowire:google')('Response:', JSON.stringify(responseBody, null, 2));
66
+ return responseBody;
61
67
  }
62
68
  function toGeminiTool(tool) {
63
69
  return {
@@ -71,6 +77,7 @@ function stripUnsupportedSchemaFields(schema) {
71
77
  return schema;
72
78
  const cleaned = Array.isArray(schema) ? [...schema] : { ...schema };
73
79
  delete cleaned.additionalProperties;
80
+ delete cleaned.$schema;
74
81
  for (const key in cleaned) {
75
82
  if (cleaned[key] && typeof cleaned[key] === 'object')
76
83
  cleaned[key] = stripUnsupportedSchemaFields(cleaned[key]);
@@ -111,6 +118,7 @@ function toGeminiContent(message) {
111
118
  }
112
119
  if (message.role === 'assistant') {
113
120
  const parts = [];
121
+ const toolResults = [];
114
122
  for (const part of message.content) {
115
123
  if (part.type === 'text') {
116
124
  parts.push({
@@ -127,51 +135,61 @@ function toGeminiContent(message) {
127
135
  },
128
136
  thoughtSignature: part.googleThoughtSignature,
129
137
  });
138
+ if (part.result)
139
+ toolResults.push(...toGeminiToolResult(part, part.result));
130
140
  }
131
141
  }
142
+ if (message.toolError) {
143
+ toolResults.push({
144
+ role: 'user',
145
+ parts: [{
146
+ text: message.toolError,
147
+ }]
148
+ });
149
+ }
132
150
  return [{
133
151
  role: 'model',
134
152
  parts
135
- }];
153
+ }, ...toolResults];
136
154
  }
137
- if (message.role === 'tool_result') {
138
- const responseContent = {};
139
- const textParts = [];
140
- const inlineDatas = [];
141
- for (const part of message.result.content) {
142
- if (part.type === 'text') {
143
- textParts.push(part.text);
144
- }
145
- else if (part.type === 'image') {
146
- // Store image data for inclusion in response
147
- inlineDatas.push({
148
- inline_data: {
149
- mime_type: part.mimeType,
150
- data: part.data
151
- }
152
- });
153
- }
155
+ throw new Error(`Unsupported message role: ${message.role}`);
156
+ }
157
+ function toGeminiToolResult(call, toolResult) {
158
+ const responseContent = {};
159
+ const textParts = [];
160
+ const inlineDatas = [];
161
+ for (const part of toolResult.content) {
162
+ if (part.type === 'text') {
163
+ textParts.push(part.text);
154
164
  }
155
- if (textParts.length > 0)
156
- responseContent.result = textParts.join('\n');
157
- const result = [{
158
- role: 'function',
159
- parts: [{
160
- functionResponse: {
161
- name: message.toolName,
162
- response: responseContent
163
- }
164
- }]
165
- }];
166
- if (inlineDatas.length > 0) {
167
- result.push({
168
- role: 'user',
169
- parts: inlineDatas
165
+ else if (part.type === 'image') {
166
+ // Store image data for inclusion in response
167
+ inlineDatas.push({
168
+ inline_data: {
169
+ mime_type: part.mimeType,
170
+ data: part.data
171
+ }
170
172
  });
171
173
  }
172
- return result;
173
174
  }
174
- throw new Error(`Unsupported message role: ${message.role}`);
175
+ if (textParts.length > 0)
176
+ responseContent.result = textParts.join('\n');
177
+ const result = [{
178
+ role: 'function',
179
+ parts: [{
180
+ functionResponse: {
181
+ name: call.name,
182
+ response: responseContent
183
+ }
184
+ }]
185
+ }];
186
+ if (inlineDatas.length > 0) {
187
+ result.push({
188
+ role: 'user',
189
+ parts: inlineDatas
190
+ });
191
+ }
192
+ return result;
175
193
  }
176
194
  const systemPrompt = (prompt) => `
177
195
  ### System instructions
@@ -29,7 +29,7 @@ class OpenAI {
29
29
  tools: tools.length > 0 ? tools : undefined,
30
30
  tool_choice: conversation.tools.length > 0 ? 'auto' : undefined,
31
31
  parallel_tool_calls: false,
32
- });
32
+ }, options);
33
33
  // Parse response output items
34
34
  const result = { role: 'assistant', content: [] };
35
35
  for (const item of response.output) {
@@ -58,21 +58,27 @@ class OpenAI {
58
58
  }
59
59
  }
60
60
  exports.OpenAI = OpenAI;
61
- async function create(body) {
61
+ async function create(createParams, options) {
62
62
  const apiKey = process.env.OPENAI_API_KEY;
63
63
  const headers = {
64
64
  'Content-Type': 'application/json',
65
65
  'Authorization': `Bearer ${apiKey}`,
66
66
  'Copilot-Vision-Request': 'true',
67
67
  };
68
+ const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
69
+ options.debug?.('lowire:openai-responses')('Request:', JSON.stringify(debugBody, null, 2));
68
70
  const response = await fetch(`https://api.openai.com/v1/responses`, {
69
71
  method: 'POST',
70
72
  headers,
71
- body: JSON.stringify(body)
73
+ body: JSON.stringify(createParams)
72
74
  });
73
- if (!response.ok)
75
+ if (!response.ok) {
76
+ options.debug?.('lowire:openai-responses')('Response:', response.status);
74
77
  throw new Error(`API error: ${response.status} ${response.statusText} ${await response.text()}`);
75
- return await response.json();
78
+ }
79
+ const responseBody = await response.json();
80
+ options.debug?.('lowire:openai-responses')('Response:', JSON.stringify(responseBody, null, 2));
81
+ return responseBody;
76
82
  }
77
83
  function toResultContentPart(part) {
78
84
  if (part.type === 'text') {
@@ -118,16 +124,16 @@ function toResponseInputItems(message) {
118
124
  };
119
125
  items.push(outputMessage);
120
126
  }
121
- items.push(...toolCallParts.map(toFunctionToolCall));
127
+ if (message.toolError) {
128
+ items.push({
129
+ type: 'message',
130
+ role: 'user',
131
+ content: message.toolError
132
+ });
133
+ }
134
+ items.push(...toolCallParts.map(toFunctionToolCall).flat());
122
135
  return items;
123
136
  }
124
- if (message.role === 'tool_result') {
125
- return [{
126
- type: 'function_call_output',
127
- call_id: message.toolCallId,
128
- output: message.result.content.map(toResultContentPart),
129
- }];
130
- }
131
137
  throw new Error(`Unsupported message role: ${message.role}`);
132
138
  }
133
139
  function toOpenAIFunctionTool(tool) {
@@ -140,14 +146,22 @@ function toOpenAIFunctionTool(tool) {
140
146
  };
141
147
  }
142
148
  function toFunctionToolCall(toolCall) {
143
- return {
144
- type: 'function_call',
145
- call_id: toolCall.id,
146
- name: toolCall.name,
147
- arguments: JSON.stringify(toolCall.arguments),
148
- id: toolCall.openaiId,
149
- status: toolCall.openaiStatus,
150
- };
149
+ const result = [{
150
+ type: 'function_call',
151
+ call_id: toolCall.id,
152
+ name: toolCall.name,
153
+ arguments: JSON.stringify(toolCall.arguments),
154
+ id: toolCall.openaiId,
155
+ status: toolCall.openaiStatus,
156
+ }];
157
+ if (toolCall.result) {
158
+ result.push({
159
+ type: 'function_call_output',
160
+ call_id: toolCall.id,
161
+ output: toolCall.result.content.map(toResultContentPart),
162
+ });
163
+ }
164
+ return result;
151
165
  }
152
166
  function toToolCall(functionCall) {
153
167
  return {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import type * as types from './types';
17
+ export declare function summarizeConversation(task: string, conversation: types.Conversation, options: Pick<types.CompletionOptions, 'debug'>): {
18
+ summary: string;
19
+ lastMessage: types.AssistantMessage;
20
+ };
package/lib/summary.js ADDED
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.summarizeConversation = summarizeConversation;
4
+ const jsx_runtime_1 = require("./jsx/jsx-runtime");
5
+ /**
6
+ * Copyright (c) Microsoft Corporation.
7
+ *
8
+ * Licensed under the Apache License, Version 2.0 (the "License");
9
+ * you may not use this file except in compliance with the License.
10
+ * You may obtain a copy of the License at
11
+ *
12
+ * http://www.apache.org/licenses/LICENSE-2.0
13
+ *
14
+ * Unless required by applicable law or agreed to in writing, software
15
+ * distributed under the License is distributed on an "AS IS" BASIS,
16
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ * See the License for the specific language governing permissions and
18
+ * limitations under the License.
19
+ */
20
+ const jsx_runtime_2 = require("./jsx/jsx-runtime");
21
+ function summarizeConversation(task, conversation, options) {
22
+ const summary = ['## Task', task];
23
+ const combinedState = {};
24
+ const assistantMessages = conversation.messages.filter(message => message.role === 'assistant');
25
+ for (let turn = 0; turn < assistantMessages.length - 1; ++turn) {
26
+ if (turn === 0) {
27
+ summary.push('');
28
+ summary.push('## History');
29
+ }
30
+ summary.push(``);
31
+ const text = assistantMessages[turn].content.filter(part => part.type === 'text').map(part => part.text).join('\n');
32
+ const toolCalls = assistantMessages[turn].content.filter(part => part.type === 'tool_call');
33
+ for (const toolCall of toolCalls) {
34
+ if (toolCall.result) {
35
+ for (const [name, state] of Object.entries(toolCall.result._meta?.['dev.lowire/state'] || {}))
36
+ combinedState[name] = state;
37
+ }
38
+ }
39
+ const message = assistantMessages[turn];
40
+ summary.push((0, jsx_runtime_1.jsxs)("step", { turn: turn + 1, children: [(0, jsx_runtime_1.jsx)("title", { children: text }), toolCalls.map(toolCall => (0, jsx_runtime_1.jsxs)("tool-call", { children: [(0, jsx_runtime_1.jsx)("name", { children: toolCall.name }), Object.keys(toolCall.arguments).length > 0 && (0, jsx_runtime_1.jsx)("arguments", { children: Object.entries(toolCall.arguments).map(([key, value]) => (0, jsx_runtime_2.jsx)(key, { children: [JSON.stringify(value)] })) })] })), toolCalls.map(toolCall => toolCall.result?._meta?.['dev.lowire/history'] || []).flat().map(h => (0, jsx_runtime_2.jsx)(h.category, { children: [h.content] })), message.toolError && (0, jsx_runtime_1.jsx)("error", { children: message.toolError })] }));
41
+ }
42
+ const lastMessage = assistantMessages[assistantMessages.length - 1];
43
+ if (lastMessage) { // Remove state from combined state as it'll be a part of the last assistant message.
44
+ for (const part of lastMessage.content.filter(part => part.type === 'tool_call')) {
45
+ for (const name of Object.keys(part.result?._meta?.['dev.lowire/state'] || {}))
46
+ delete combinedState[name];
47
+ }
48
+ }
49
+ for (const [name, state] of Object.entries(combinedState)) {
50
+ summary.push('');
51
+ summary.push((0, jsx_runtime_1.jsx)("state", { name: name, children: state }));
52
+ }
53
+ options.debug?.('lowire:summary')(summary.join('\n'));
54
+ options.debug?.('lowire:summary')(JSON.stringify(lastMessage, null, 2));
55
+ return { summary: summary.join('\n'), lastMessage };
56
+ }