@amalgm/chat 0.2.8 → 0.2.9-main.34746990150
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/PURPOSE.md +94 -41
- package/README.md +17 -4
- package/dist/acp/projection.d.ts +8 -0
- package/dist/acp/projection.d.ts.map +1 -1
- package/dist/acp/projection.js +10 -3
- package/dist/acp/projection.js.map +1 -1
- package/dist/conversations/index.d.ts +1 -0
- package/dist/conversations/index.d.ts.map +1 -1
- package/dist/conversations/index.js +1 -0
- package/dist/conversations/index.js.map +1 -1
- package/dist/conversations/transcript-parts.d.ts +38 -0
- package/dist/conversations/transcript-parts.d.ts.map +1 -0
- package/dist/conversations/transcript-parts.js +2 -0
- package/dist/conversations/transcript-parts.js.map +1 -0
- package/dist/conversations/transcript-tools.d.ts +11 -0
- package/dist/conversations/transcript-tools.d.ts.map +1 -0
- package/dist/conversations/transcript-tools.js +47 -0
- package/dist/conversations/transcript-tools.js.map +1 -0
- package/dist/conversations/transcript.d.ts +5 -4
- package/dist/conversations/transcript.d.ts.map +1 -1
- package/dist/conversations/transcript.js +43 -143
- package/dist/conversations/transcript.js.map +1 -1
- package/dist/conversations/types.d.ts +19 -0
- package/dist/conversations/types.d.ts.map +1 -1
- package/dist/conversations/types.js.map +1 -1
- package/dist/events/constructors.d.ts.map +1 -1
- package/dist/events/constructors.js +2 -52
- package/dist/events/constructors.js.map +1 -1
- package/dist/events/schema.d.ts +72 -72
- package/dist/events/schema.js +16 -16
- package/dist/events/schema.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/input/prompt.js +1 -1
- package/dist/input/prompt.js.map +1 -1
- package/dist/mcp/server.js +13 -13
- package/dist/mcp/server.js.map +1 -1
- package/dist/normalizers/claude.d.ts +142 -142
- package/dist/normalizers/claude.d.ts.map +1 -1
- package/dist/normalizers/claude.js +23 -37
- package/dist/normalizers/claude.js.map +1 -1
- package/dist/normalizers/codex.d.ts +71 -71
- package/dist/normalizers/codex.d.ts.map +1 -1
- package/dist/normalizers/codex.js +12 -24
- package/dist/normalizers/codex.js.map +1 -1
- package/dist/normalizers/index.d.ts +0 -1
- package/dist/normalizers/index.d.ts.map +1 -1
- package/dist/normalizers/index.js +0 -1
- package/dist/normalizers/index.js.map +1 -1
- package/dist/normalizers/opencode.d.ts.map +1 -1
- package/dist/normalizers/opencode.js +9 -6
- package/dist/normalizers/opencode.js.map +1 -1
- package/dist/normalizers/pi.d.ts.map +1 -1
- package/dist/normalizers/pi.js +27 -14
- package/dist/normalizers/pi.js.map +1 -1
- package/dist/sessions/chat.d.ts +5 -5
- package/dist/sessions/chat.d.ts.map +1 -1
- package/dist/sessions/chat.js +51 -19
- package/dist/sessions/chat.js.map +1 -1
- package/dist/sessions/index.d.ts +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +1 -1
- package/dist/sessions/index.js.map +1 -1
- package/dist/usage/acp.d.ts +4 -0
- package/dist/usage/acp.d.ts.map +1 -0
- package/dist/usage/acp.js +20 -0
- package/dist/usage/acp.js.map +1 -0
- package/dist/usage/finalize.d.ts +1 -11
- package/dist/usage/finalize.d.ts.map +1 -1
- package/dist/usage/finalize.js +29 -47
- package/dist/usage/finalize.js.map +1 -1
- package/dist/usage/index.d.ts +2 -1
- package/dist/usage/index.d.ts.map +1 -1
- package/dist/usage/index.js +2 -1
- package/dist/usage/index.js.map +1 -1
- package/dist/usage/normalize.d.ts +8 -9
- package/dist/usage/normalize.d.ts.map +1 -1
- package/dist/usage/normalize.js +20 -20
- package/dist/usage/normalize.js.map +1 -1
- package/dist/usage/ports.d.ts +4 -4
- package/dist/usage/ports.d.ts.map +1 -1
- package/dist/usage/ports.js +5 -5
- package/dist/usage/ports.js.map +1 -1
- package/dist/usage/tokens.d.ts +13 -38
- package/dist/usage/tokens.d.ts.map +1 -1
- package/dist/usage/tokens.js +39 -72
- package/dist/usage/tokens.js.map +1 -1
- package/host/adapters/acp-prompt.js +36 -0
- package/host/adapters/acp.js +50 -71
- package/host/adapters/attachments.js +37 -0
- package/host/adapters/claude.js +9 -2
- package/host/adapters/codex.js +36 -18
- package/host/adapters/cursor.js +72 -99
- package/host/adapters/opencode.js +39 -25
- package/host/adapters/pi.js +24 -19
- package/host/adapters/prompt.js +13 -21
- package/host/adapters/provider-url.js +8 -0
- package/host/auth.js +10 -9
- package/host/http.js +3 -2
- package/host/model-resolution.js +7 -0
- package/host/native-contract.js +4 -3
- package/host/native-runtime.js +25 -11
- package/host/sqlite/index.d.ts +2 -0
- package/host/sqlite/index.js +48 -4
- package/host/tooling/runtime-home.js +1 -1
- package/host/tooling/system-instructions.js +7 -6
- package/host/tooling/system-prompt.js +12 -18
- package/package.json +26 -2
- package/dist/normalizers/cursor.d.ts +0 -33
- package/dist/normalizers/cursor.d.ts.map +0 -1
- package/dist/normalizers/cursor.js +0 -196
- package/dist/normalizers/cursor.js.map +0 -1
package/host/adapters/cursor.js
CHANGED
|
@@ -3,23 +3,21 @@
|
|
|
3
3
|
// cursor parameterization (binary resolution, model translation, permission
|
|
4
4
|
// handling, and ACP lifecycle inputs).
|
|
5
5
|
//
|
|
6
|
-
// Laws come from ../../dist: event constructors via `createEventConstructors`,
|
|
7
|
-
// the normalizer via `createCursorNormalizer`, `cursorStopReason`, and
|
|
8
|
-
// Effectful tooling and the recorder are sibling host modules.
|
|
9
|
-
|
|
10
6
|
import os from 'node:os';
|
|
7
|
+
import fs from 'node:fs';
|
|
11
8
|
import path from 'node:path';
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
12
10
|
import { spawnSync } from 'node:child_process';
|
|
13
|
-
import {
|
|
14
|
-
import { createCursorNormalizer, cursorStopReason } from '../../dist/normalizers/cursor.js';
|
|
11
|
+
import { usageFromAcpPrompt } from '../../dist/usage/index.js';
|
|
15
12
|
import { recordNativeEvent } from '../recorder.js';
|
|
16
13
|
import { toAcpMcpServers } from '../tooling/mcp-bundle.js';
|
|
17
14
|
import { executableExists, findOnPath } from '../tooling/native-binaries.js';
|
|
18
15
|
import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
|
|
16
|
+
import { preparedSystemPrompt } from '../tooling/system-prompt.js';
|
|
19
17
|
import { requestPermission } from '../permissions.js';
|
|
18
|
+
import { runAcpPrompt } from './acp-prompt.js';
|
|
20
19
|
import { AcpClient } from './acp-client.js';
|
|
21
20
|
import {
|
|
22
|
-
acpSessionParams,
|
|
23
21
|
assertAcpMcpCapabilities,
|
|
24
22
|
assertAcpPromptCapabilities,
|
|
25
23
|
} from './acp-capabilities.js';
|
|
@@ -31,12 +29,28 @@ import {
|
|
|
31
29
|
} from './input-capabilities.js';
|
|
32
30
|
import { acpPromptParts } from './prompt.js';
|
|
33
31
|
|
|
34
|
-
const events = createEventConstructors({ clockMs: () => Date.now() });
|
|
35
|
-
const { done, errorEvent, warningEvent } = events;
|
|
36
|
-
const { cursorPromptUsage, normalizeCursorUpdate } = createCursorNormalizer(events);
|
|
37
|
-
|
|
38
32
|
const PROTOCOL_VERSION = 1;
|
|
39
33
|
|
|
34
|
+
/** Cursor loads native rules and ambient MCP at process startup, while ACP's
|
|
35
|
+
* session cwd supplies the actual workspace. Keep configuration in a managed,
|
|
36
|
+
* content-addressed directory so concurrent executions cannot replace guidance. */
|
|
37
|
+
function cursorLaunchDirectory(contract) {
|
|
38
|
+
const instructions = preparedSystemPrompt(contract);
|
|
39
|
+
const revision = createHash('sha256').update(instructions).digest('hex');
|
|
40
|
+
const directory = path.join(contract.auth.runtimeHome, 'launch', revision);
|
|
41
|
+
const rules = path.join(directory, '.cursor', 'rules');
|
|
42
|
+
fs.mkdirSync(rules, { recursive: true });
|
|
43
|
+
if (instructions) {
|
|
44
|
+
const file = path.join(rules, 'amalgm.mdc');
|
|
45
|
+
const content = `---\nalwaysApply: true\n---\n${instructions}\n`;
|
|
46
|
+
try { fs.writeFileSync(file, content, { flag: 'wx', mode: 0o600 }); }
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code !== 'EEXIST' || fs.readFileSync(file, 'utf8') !== content) throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return directory;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
function resolveBinary() {
|
|
41
55
|
const binaryName = process.platform === 'win32' ? 'cursor-agent.exe' : 'cursor-agent';
|
|
42
56
|
const homes = [...new Set([process.env.AMALGM_NATIVE_HOME, os.homedir()].filter(Boolean))];
|
|
@@ -138,12 +152,14 @@ function pickRejectOption(options = []) {
|
|
|
138
152
|
|
|
139
153
|
class CursorAdapter {
|
|
140
154
|
constructor(options = {}) {
|
|
155
|
+
this.outputType = 'acp';
|
|
156
|
+
this.materializeAttachments = true;
|
|
141
157
|
this.requestPermission = options.requestPermission || null;
|
|
142
158
|
this.inputCapabilities = nativeInputCapabilities({
|
|
143
159
|
authModes: ['subscription', 'byok'],
|
|
144
160
|
modelSettings: [...COMMON_REASONING_SETTINGS, ...COMMON_SPEED_SETTINGS],
|
|
145
161
|
tools: true,
|
|
146
|
-
instructions: '
|
|
162
|
+
instructions: 'native',
|
|
147
163
|
});
|
|
148
164
|
}
|
|
149
165
|
|
|
@@ -153,7 +169,7 @@ class CursorAdapter {
|
|
|
153
169
|
const client = new AcpClient({
|
|
154
170
|
binary,
|
|
155
171
|
args: ['acp'],
|
|
156
|
-
cwd: contract
|
|
172
|
+
cwd: cursorLaunchDirectory(contract),
|
|
157
173
|
env: runtime.env,
|
|
158
174
|
label: 'Cursor',
|
|
159
175
|
});
|
|
@@ -161,40 +177,44 @@ class CursorAdapter {
|
|
|
161
177
|
sessionId: contract.sessionId,
|
|
162
178
|
providerSessionId: contract.providerSessionId || null,
|
|
163
179
|
client,
|
|
164
|
-
modelWarning: null,
|
|
165
180
|
activePrompt: null,
|
|
166
181
|
cancelRequested: false,
|
|
167
182
|
permissionMode: contract.permissionMode,
|
|
168
183
|
capabilities: {},
|
|
169
184
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
session.capabilities = initialized?.agentCapabilities || {};
|
|
177
|
-
const mcpServers = toAcpMcpServers(contract);
|
|
178
|
-
assertAcpMcpCapabilities('cursor', session.capabilities, mcpServers);
|
|
179
|
-
const sessionParams = acpSessionParams('cursor', session.capabilities, contract, mcpServers);
|
|
180
|
-
let acpSession = null;
|
|
181
|
-
if (session.providerSessionId) {
|
|
182
|
-
// session/load replays history as session/update notifications; nothing is
|
|
183
|
-
// subscribed yet, so the replay is discarded and the turn starts clean.
|
|
184
|
-
acpSession = await client.request('session/load', {
|
|
185
|
-
sessionId: session.providerSessionId,
|
|
186
|
-
...sessionParams,
|
|
187
|
-
}, 600000).catch((err) => {
|
|
188
|
-
console.warn('[Cursor] Failed to load native session; starting a new one:', err.message);
|
|
189
|
-
return null;
|
|
185
|
+
try {
|
|
186
|
+
client.onRequest((method, params) => this.handleAgentRequest(session, method, params));
|
|
187
|
+
const initialized = await client.request('initialize', {
|
|
188
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
189
|
+
clientInfo: { name: 'amalgm-chat', version: '1.0.0' },
|
|
190
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
190
191
|
});
|
|
192
|
+
session.capabilities = initialized?.agentCapabilities || {};
|
|
193
|
+
const mcpServers = toAcpMcpServers(contract);
|
|
194
|
+
assertAcpMcpCapabilities('cursor', session.capabilities, mcpServers);
|
|
195
|
+
const sessionParams = { cwd: contract.cwd, mcpServers };
|
|
196
|
+
let acpSession = null;
|
|
197
|
+
if (session.providerSessionId) {
|
|
198
|
+
// session/load replays history as session/update notifications; nothing is
|
|
199
|
+
// subscribed yet, so the replay is discarded and the turn starts clean.
|
|
200
|
+
acpSession = await client.request('session/load', {
|
|
201
|
+
sessionId: session.providerSessionId,
|
|
202
|
+
...sessionParams,
|
|
203
|
+
}, 600000).catch((err) => {
|
|
204
|
+
console.warn('[Cursor] Failed to load native session; starting a new one:', err.message);
|
|
205
|
+
return null;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (!acpSession) {
|
|
209
|
+
acpSession = await client.request('session/new', sessionParams);
|
|
210
|
+
session.providerSessionId = acpSession.sessionId;
|
|
211
|
+
}
|
|
212
|
+
await this.selectModel(session, acpSession, contract);
|
|
213
|
+
return session;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
client.close();
|
|
216
|
+
throw error;
|
|
191
217
|
}
|
|
192
|
-
if (!acpSession) {
|
|
193
|
-
acpSession = await client.request('session/new', sessionParams);
|
|
194
|
-
session.providerSessionId = acpSession.sessionId;
|
|
195
|
-
}
|
|
196
|
-
await this.selectModel(session, acpSession, contract);
|
|
197
|
-
return session;
|
|
198
218
|
}
|
|
199
219
|
|
|
200
220
|
async selectModel(session, acpSession, contract) {
|
|
@@ -268,71 +288,24 @@ class CursorAdapter {
|
|
|
268
288
|
session.client.close();
|
|
269
289
|
}
|
|
270
290
|
|
|
271
|
-
async *prompt(session, input, contract) {
|
|
291
|
+
async *prompt(session, input, contract, effects = {}) {
|
|
272
292
|
assertAcpPromptCapabilities('cursor', session.capabilities, input);
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
session.cancelRequested = false;
|
|
284
|
-
session.activePrompt = state;
|
|
285
|
-
if (session.modelWarning) {
|
|
286
|
-
push(warningEvent(session.modelWarning, { providerSessionId: session.providerSessionId }));
|
|
287
|
-
session.modelWarning = null;
|
|
288
|
-
}
|
|
289
|
-
const off = session.client.onNotification((msg) => {
|
|
290
|
-
if (msg.method !== 'session/update') return;
|
|
291
|
-
const params = msg.params || {};
|
|
292
|
-
if (params.sessionId && params.sessionId !== session.providerSessionId) return;
|
|
293
|
-
recordNativeEvent('cursor.acp.update', msg, {
|
|
294
|
-
providerSessionId: session.providerSessionId,
|
|
295
|
-
sessionId: contract.sessionId,
|
|
296
|
-
assistantMessageId: contract.assistantMessageId,
|
|
297
|
-
updateKind: params.update?.sessionUpdate,
|
|
298
|
-
});
|
|
299
|
-
for (const e of normalizeCursorUpdate(params.update || {}, state)) push(e);
|
|
293
|
+
const metadata = { providerSessionId: session.providerSessionId, sessionId: contract.sessionId,
|
|
294
|
+
assistantMessageId: contract.assistantMessageId };
|
|
295
|
+
yield* runAcpPrompt(session, acpPromptParts(input), {
|
|
296
|
+
onUpdate: message => recordNativeEvent('cursor.acp.update', message, metadata),
|
|
297
|
+
onResult: async result => {
|
|
298
|
+
recordNativeEvent('cursor.acp.prompt_result', result, metadata);
|
|
299
|
+
const usage = usageFromAcpPrompt(result?.usage);
|
|
300
|
+
if (usage) await effects.recordUsage?.({ usage,
|
|
301
|
+
raw: { source: 'cursor.acp.prompt_result', payload: result } });
|
|
302
|
+
},
|
|
300
303
|
});
|
|
301
|
-
session.client.request('session/prompt', {
|
|
302
|
-
sessionId: session.providerSessionId,
|
|
303
|
-
prompt: acpPromptParts(input),
|
|
304
|
-
}, 0).then((result) => {
|
|
305
|
-
recordNativeEvent('cursor.acp.prompt_result', result, {
|
|
306
|
-
providerSessionId: session.providerSessionId,
|
|
307
|
-
sessionId: contract.sessionId,
|
|
308
|
-
assistantMessageId: contract.assistantMessageId,
|
|
309
|
-
});
|
|
310
|
-
const usage = cursorPromptUsage(result?.usage, { providerSessionId: session.providerSessionId });
|
|
311
|
-
if (usage) push(usage);
|
|
312
|
-
const stopReason = session.cancelRequested ? 'cancelled' : cursorStopReason(result?.stopReason);
|
|
313
|
-
push(done({ providerSessionId: session.providerSessionId, stopReason }));
|
|
314
|
-
}).catch((err) => {
|
|
315
|
-
if (session.cancelRequested) {
|
|
316
|
-
push(done({ providerSessionId: session.providerSessionId, stopReason: 'cancelled' }));
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
push(errorEvent(err.message, { providerSessionId: session.providerSessionId }));
|
|
320
|
-
push(done({ providerSessionId: session.providerSessionId, stopReason: 'error' }));
|
|
321
|
-
});
|
|
322
|
-
try {
|
|
323
|
-
while (!complete || queue.length) {
|
|
324
|
-
if (!queue.length) await new Promise((resolve) => { wake = resolve; });
|
|
325
|
-
else yield queue.shift();
|
|
326
|
-
}
|
|
327
|
-
} finally {
|
|
328
|
-
off();
|
|
329
|
-
session.activePrompt = null;
|
|
330
|
-
session.cancelRequested = false;
|
|
331
|
-
}
|
|
332
304
|
}
|
|
333
305
|
}
|
|
334
306
|
|
|
335
307
|
const __private = {
|
|
308
|
+
cursorLaunchDirectory,
|
|
336
309
|
matchAcpModel,
|
|
337
310
|
pickPermissionOption,
|
|
338
311
|
resolveBinary,
|
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { createEventConstructors } from '../../dist/events/index.js';
|
|
9
9
|
import { createOpenCodeNormalizer } from '../../dist/normalizers/opencode.js';
|
|
10
10
|
import { recordNativeEvent } from '../recorder.js';
|
|
11
|
+
import { directProviderModelId } from '../model-resolution.js';
|
|
11
12
|
import { toOpenCodeMcpConfig } from '../tooling/mcp-bundle.js';
|
|
12
13
|
import { bundledOpenCodeBinary, executableExists, findOnPath } from '../tooling/native-binaries.js';
|
|
13
14
|
import { importPackage } from '../tooling/package-import.js';
|
|
@@ -16,6 +17,7 @@ import { preparedSystemPrompt } from '../tooling/system-prompt.js';
|
|
|
16
17
|
import { requestPermission } from '../permissions.js';
|
|
17
18
|
import { nativeInputCapabilities, UnsupportedExecutionInputError } from './input-capabilities.js';
|
|
18
19
|
import { openCodePromptParts } from './prompt.js';
|
|
20
|
+
import { providerBaseUrl } from './provider-url.js';
|
|
19
21
|
|
|
20
22
|
const events = createEventConstructors({ clockMs: () => Date.now() });
|
|
21
23
|
const { done, errorEvent, reasoningStarted } = events;
|
|
@@ -31,7 +33,7 @@ function splitModel(model, auth) {
|
|
|
31
33
|
if (auth?.method === 'byok' && auth?.providerKind === 'anthropic_compatible') {
|
|
32
34
|
const bare = raw.replace(/^vercel\//, '').replace(/^anthropic\//, '');
|
|
33
35
|
const modelID = bare.startsWith('anthropic/') ? bare.slice('anthropic/'.length) : bare;
|
|
34
|
-
return { providerID: 'anthropic', modelID };
|
|
36
|
+
return { providerID: 'anthropic', modelID: directProviderModelId('anthropic', modelID) };
|
|
35
37
|
}
|
|
36
38
|
if (raw.startsWith('vercel/')) {
|
|
37
39
|
return { providerID: 'vercel', modelID: raw.slice('vercel/'.length) };
|
|
@@ -99,11 +101,12 @@ function configFor(contract) {
|
|
|
99
101
|
mcp: toOpenCodeMcpConfig(contract),
|
|
100
102
|
};
|
|
101
103
|
if (contract.authMethod !== 'provider_auth') {
|
|
104
|
+
const baseUrl = providerBaseUrl(contract.auth.baseUrl, model.providerID === 'vercel' ? '/v3/ai' : '/v1');
|
|
102
105
|
config.provider = {
|
|
103
106
|
[model.providerID]: {
|
|
104
107
|
options: {
|
|
105
|
-
baseURL:
|
|
106
|
-
baseUrl
|
|
108
|
+
baseURL: baseUrl,
|
|
109
|
+
baseUrl,
|
|
107
110
|
apiKey: contract.auth.tokenRef,
|
|
108
111
|
timeout: 300000,
|
|
109
112
|
chunkTimeout: 300000,
|
|
@@ -258,31 +261,42 @@ class OpenCodeAdapter {
|
|
|
258
261
|
|
|
259
262
|
async create(contract) {
|
|
260
263
|
const instance = await this.startInstance(contract);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
try {
|
|
265
|
+
const selected = splitModel(contract.cliModel || contract.usageModelId, contract.auth);
|
|
266
|
+
const catalog = resultData(await instance.client.provider.list({ query: { directory: contract.cwd } }));
|
|
267
|
+
const model = catalog?.all?.find(provider => provider.id === selected.providerID)?.models?.[selected.modelID];
|
|
268
|
+
const contextLimit = Number(model?.limit?.context) || undefined;
|
|
269
|
+
let created = null;
|
|
270
|
+
if (contract.providerSessionId) {
|
|
271
|
+
try {
|
|
272
|
+
created = resultData(await instance.client.session.get({
|
|
273
|
+
path: { id: contract.providerSessionId },
|
|
274
|
+
query: { directory: contract.cwd },
|
|
275
|
+
}));
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.warn('[OpenCode] Failed to resume persisted session; creating a new native session:', err.message);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!created) {
|
|
281
|
+
created = resultData(await instance.client.session.create({
|
|
282
|
+
body: { title: `Amalgm ${contract.sessionId}` },
|
|
266
283
|
query: { directory: contract.cwd },
|
|
267
284
|
}));
|
|
268
|
-
} catch (err) {
|
|
269
|
-
console.warn('[OpenCode] Failed to resume persisted session; creating a new native session:', err.message);
|
|
270
285
|
}
|
|
286
|
+
if (!created?.id) throw new Error('OpenCode SDK did not return a session id');
|
|
287
|
+
return {
|
|
288
|
+
sessionId: contract.sessionId,
|
|
289
|
+
providerSessionId: created.id,
|
|
290
|
+
instance,
|
|
291
|
+
abortController: null,
|
|
292
|
+
turnCount: 0,
|
|
293
|
+
contextLimit,
|
|
294
|
+
};
|
|
295
|
+
} catch (error) {
|
|
296
|
+
instance.server.close();
|
|
297
|
+
instance.closed = true;
|
|
298
|
+
throw error;
|
|
271
299
|
}
|
|
272
|
-
if (!created) {
|
|
273
|
-
created = resultData(await instance.client.session.create({
|
|
274
|
-
body: { title: `Amalgm ${contract.sessionId}` },
|
|
275
|
-
query: { directory: contract.cwd },
|
|
276
|
-
}));
|
|
277
|
-
}
|
|
278
|
-
if (!created?.id) throw new Error('OpenCode SDK did not return a session id');
|
|
279
|
-
return {
|
|
280
|
-
sessionId: contract.sessionId,
|
|
281
|
-
providerSessionId: created.id,
|
|
282
|
-
instance,
|
|
283
|
-
abortController: null,
|
|
284
|
-
turnCount: 0,
|
|
285
|
-
};
|
|
286
300
|
}
|
|
287
301
|
|
|
288
302
|
async resume(session, contract) {
|
|
@@ -314,7 +328,7 @@ class OpenCodeAdapter {
|
|
|
314
328
|
providerSessionId: session.providerSessionId,
|
|
315
329
|
userMessageId: null,
|
|
316
330
|
turnStartedAt: Date.now(),
|
|
317
|
-
contextLimit: Number(contract.contextWindow || 0) || undefined,
|
|
331
|
+
contextLimit: session.contextLimit || Number(contract.contextWindow || 0) || undefined,
|
|
318
332
|
messageRoles: new Map(),
|
|
319
333
|
partTypes: new Map(),
|
|
320
334
|
partText: new Map(),
|
package/host/adapters/pi.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createEventConstructors } from '../../dist/events/index.js';
|
|
|
12
12
|
import { createPiNormalizer } from '../../dist/normalizers/pi.js';
|
|
13
13
|
import { allowed, requestPermission } from '../permissions.js';
|
|
14
14
|
import { recordNativeEvent } from '../recorder.js';
|
|
15
|
+
import { directProviderModelId } from '../model-resolution.js';
|
|
15
16
|
import { relayedMcpServers } from '../tooling/mcp-bundle.js';
|
|
16
17
|
import { executableExists, findOnPath } from '../tooling/native-binaries.js';
|
|
17
18
|
import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
UnsupportedExecutionInputError,
|
|
23
24
|
} from './input-capabilities.js';
|
|
24
25
|
import { piPrompt } from './prompt.js';
|
|
26
|
+
import { providerBaseUrl } from './provider-url.js';
|
|
25
27
|
|
|
26
28
|
const events = createEventConstructors({ clockMs: () => Date.now() });
|
|
27
29
|
const { done, errorEvent } = events;
|
|
@@ -190,18 +192,15 @@ function resolveBinary() {
|
|
|
190
192
|
throw new Error(`Pi CLI is not available. Checked: ${failures.join(', ') || 'no candidates'}. Install it with: npm install -g @mariozechner/pi-coding-agent`);
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
function ensureEgressExtension(runtimeHome) {
|
|
197
|
-
const file = path.join(runtimeHome, '.pi', 'amalgm-egress-extension.js');
|
|
195
|
+
// Every explicitly authenticated provider uses its prepared endpoint.
|
|
196
|
+
function ensureProviderExtension(runtimeHome, provider) {
|
|
197
|
+
const file = path.join(runtimeHome, '.pi', 'amalgm-provider-extension.js');
|
|
198
198
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
199
199
|
const source = [
|
|
200
|
-
'// Generated by @amalgm/chat:
|
|
201
|
-
'// through the per-session amalgm egress proxy for billing and audit.',
|
|
200
|
+
'// Generated by @amalgm/chat: uses the prepared provider endpoint.',
|
|
202
201
|
'export default function (pi) {',
|
|
203
|
-
' const baseUrl = process.env.
|
|
204
|
-
` if (baseUrl) pi.registerProvider(${JSON.stringify(
|
|
202
|
+
' const baseUrl = process.env.AMALGM_PI_PROVIDER_BASE_URL;',
|
|
203
|
+
` if (baseUrl) pi.registerProvider(${JSON.stringify(provider)}, { baseUrl });`,
|
|
205
204
|
'}',
|
|
206
205
|
'',
|
|
207
206
|
].join('\n');
|
|
@@ -210,7 +209,7 @@ function ensureEgressExtension(runtimeHome) {
|
|
|
210
209
|
}
|
|
211
210
|
|
|
212
211
|
function piProvider(contract) {
|
|
213
|
-
if (contract.authMethod === 'amalgm') return GATEWAY_PROVIDER;
|
|
212
|
+
if (contract.authMethod === 'amalgm' || contract.auth?.providerKind === 'ai_gateway') return GATEWAY_PROVIDER;
|
|
214
213
|
const provider = String(contract.providerId || '').trim();
|
|
215
214
|
if (provider === 'vercel' || provider === 'ai-gateway' || provider === 'ai_gateway') return GATEWAY_PROVIDER;
|
|
216
215
|
return provider;
|
|
@@ -218,11 +217,11 @@ function piProvider(contract) {
|
|
|
218
217
|
|
|
219
218
|
function piModel(contract, provider) {
|
|
220
219
|
const model = String(contract.providerModelId || contract.cliModel || '').trim();
|
|
221
|
-
if (provider === GATEWAY_PROVIDER &&
|
|
220
|
+
if (provider === GATEWAY_PROVIDER && !model.includes('/')) {
|
|
222
221
|
return `${contract.providerId}/${model}`;
|
|
223
222
|
}
|
|
224
|
-
if (provider
|
|
225
|
-
return model;
|
|
223
|
+
if (provider === GATEWAY_PROVIDER) return model;
|
|
224
|
+
return directProviderModelId(provider, model.startsWith(`${provider}/`) ? model.slice(provider.length + 1) : model);
|
|
226
225
|
}
|
|
227
226
|
|
|
228
227
|
const PI_KEY_ENV = Object.freeze({
|
|
@@ -341,13 +340,15 @@ function thinkingLevelFor(contract) {
|
|
|
341
340
|
|
|
342
341
|
function buildArgs(contract, runtimeHome) {
|
|
343
342
|
const provider = piProvider(contract);
|
|
344
|
-
const args = ['--mode', 'rpc', '--provider', provider, '--model', piModel(contract, provider)];
|
|
343
|
+
const args = ['--mode', 'rpc', '--no-extensions', '--provider', provider, '--model', piModel(contract, provider)];
|
|
345
344
|
const thinking = thinkingLevelFor(contract);
|
|
346
345
|
if (thinking) args.push('--thinking', thinking);
|
|
347
346
|
const systemPrompt = preparedSystemPrompt(contract);
|
|
348
347
|
if (systemPrompt) args.push('--system-prompt', systemPrompt);
|
|
349
348
|
if (contract.providerSessionId) args.push('--session', contract.providerSessionId);
|
|
350
|
-
if (contract.authMethod
|
|
349
|
+
if (contract.authMethod !== 'provider_auth' && contract.auth?.baseUrl) {
|
|
350
|
+
args.push('--extension', ensureProviderExtension(runtimeHome, provider));
|
|
351
|
+
}
|
|
351
352
|
const hasSelectedTools = Array.isArray(contract.mcpServers) && contract.mcpServers.length > 0;
|
|
352
353
|
if (contract.permissionMode === 'ask' || hasSelectedTools) {
|
|
353
354
|
args.push('--extension', ensureAmalgmExtension(runtimeHome, contract));
|
|
@@ -377,8 +378,9 @@ class PiAdapter {
|
|
|
377
378
|
const env = { ...runtime.env };
|
|
378
379
|
const provider = piProvider(contract);
|
|
379
380
|
applyPiByokEnvironment(env, contract, provider);
|
|
380
|
-
if (contract.authMethod
|
|
381
|
-
env.
|
|
381
|
+
if (contract.authMethod !== 'provider_auth' && contract.auth.baseUrl) {
|
|
382
|
+
env.AMALGM_PI_PROVIDER_BASE_URL = providerBaseUrl(contract.auth.baseUrl,
|
|
383
|
+
provider === 'anthropic' || provider === GATEWAY_PROVIDER ? '' : '/v1');
|
|
382
384
|
}
|
|
383
385
|
const client = new PiRpc({
|
|
384
386
|
binary,
|
|
@@ -476,7 +478,10 @@ class PiAdapter {
|
|
|
476
478
|
eventType: event?.type,
|
|
477
479
|
});
|
|
478
480
|
if (event?.type === 'agent_end') {
|
|
479
|
-
finish().catch(() =>
|
|
481
|
+
finish().catch((error) => {
|
|
482
|
+
push(errorEvent(error.message, { providerSessionId: session.providerSessionId }));
|
|
483
|
+
push(done({ providerSessionId: session.providerSessionId, stopReason: 'error' }));
|
|
484
|
+
});
|
|
480
485
|
return;
|
|
481
486
|
}
|
|
482
487
|
for (const e of normalizePiEvent(event, state)) {
|
|
@@ -510,7 +515,7 @@ const __private = {
|
|
|
510
515
|
applyPiByokEnvironment,
|
|
511
516
|
buildArgs,
|
|
512
517
|
ensureAmalgmExtension,
|
|
513
|
-
|
|
518
|
+
ensureProviderExtension,
|
|
514
519
|
piModel,
|
|
515
520
|
piProvider,
|
|
516
521
|
resolveBinary,
|
package/host/adapters/prompt.js
CHANGED
|
@@ -156,35 +156,27 @@ export function claudePrompt(prompt) {
|
|
|
156
156
|
}());
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
function openCodeMediaType(mime) {
|
|
160
|
+
return /^(image|audio)\//.test(mime) || mime === 'application/pdf';
|
|
161
|
+
}
|
|
162
|
+
|
|
159
163
|
export function openCodePromptParts(prompt) {
|
|
160
|
-
return prompt.parts.map((part) => {
|
|
164
|
+
return prompt.parts.map((part, index) => {
|
|
161
165
|
if (part.type === 'text') return { type: 'text', text: part.text };
|
|
162
166
|
if (part.type === 'resource_link') {
|
|
163
|
-
return {
|
|
164
|
-
|
|
165
|
-
mime: mimeOf(part),
|
|
166
|
-
filename: resourceName(part),
|
|
167
|
-
url: part.uri,
|
|
168
|
-
};
|
|
167
|
+
if (!openCodeMediaType(mimeOf(part))) return { type: 'text', text: linkedText(part) };
|
|
168
|
+
return { type: 'file', mime: mimeOf(part), filename: resourceName(part), url: part.uri };
|
|
169
169
|
}
|
|
170
170
|
if (part.type === 'image' || part.type === 'audio') {
|
|
171
|
-
return {
|
|
172
|
-
type: 'file',
|
|
173
|
-
mime: part.mimeType,
|
|
174
|
-
url: dataUrl(part.mimeType, part.data),
|
|
175
|
-
};
|
|
171
|
+
return { type: 'file', mime: part.mimeType, url: dataUrl(part.mimeType, part.data) };
|
|
176
172
|
}
|
|
177
173
|
const resource = part.resource;
|
|
174
|
+
if (typeof resource.text === 'string') return { type: 'text', text: embeddedText(part) };
|
|
178
175
|
const mime = mimeOf(part);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
type: 'file',
|
|
184
|
-
mime,
|
|
185
|
-
filename: resourceName(part),
|
|
186
|
-
url: dataUrl(mime, data),
|
|
187
|
-
};
|
|
176
|
+
if (!openCodeMediaType(mime)) {
|
|
177
|
+
throw new UnsupportedPromptPartError('opencode', index, part, `embedded ${mime} requires host file materialization`);
|
|
178
|
+
}
|
|
179
|
+
return { type: 'file', mime, filename: resourceName(part), url: dataUrl(mime, resource.blob) };
|
|
188
180
|
});
|
|
189
181
|
}
|
|
190
182
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Project a configured provider base onto the path consumed by its SDK. */
|
|
2
|
+
export function providerBaseUrl(baseUrl, apiPath) {
|
|
3
|
+
if (!baseUrl) return baseUrl;
|
|
4
|
+
const url = new URL(baseUrl);
|
|
5
|
+
const root = url.pathname.replace(/\/+$/, '').replace(/\/(?:v1|v3\/ai)$/, '');
|
|
6
|
+
url.pathname = root + apiPath;
|
|
7
|
+
return url.href.replace(/\/$/, '');
|
|
8
|
+
}
|
package/host/auth.js
CHANGED
|
@@ -112,12 +112,14 @@ function providerAuthIdentity(harness) {
|
|
|
112
112
|
return `${harness || 'provider'}:provider-default`;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function runtimeHome({ runtimeHomeRoot, harness, agentConfigId }) {
|
|
115
|
+
function runtimeHome({ runtimeHomeRoot, harness, agentConfigId, method, authority }) {
|
|
116
116
|
const harnessHome = path.join(runtimeHomeRoot, safePathSegment(harness));
|
|
117
117
|
return path.join(
|
|
118
118
|
harnessHome,
|
|
119
119
|
'agents',
|
|
120
120
|
safePathSegment(agentConfigId || harness),
|
|
121
|
+
'auth',
|
|
122
|
+
`${method}-${fingerprint(authority)}`,
|
|
121
123
|
'home',
|
|
122
124
|
);
|
|
123
125
|
}
|
|
@@ -139,7 +141,7 @@ export function authEnvelope({
|
|
|
139
141
|
agentConfigId,
|
|
140
142
|
}) {
|
|
141
143
|
const supported = AUTH_METHODS[harness];
|
|
142
|
-
if (supported && !supported.includes(authMethod)) {
|
|
144
|
+
if (!['amalgm', 'byok', 'provider_auth'].includes(authMethod) || (supported && !supported.includes(authMethod))) {
|
|
143
145
|
throw new Error(`${harness} does not support ${authMethod} auth`);
|
|
144
146
|
}
|
|
145
147
|
const method = authMethod;
|
|
@@ -185,7 +187,8 @@ export function authEnvelope({
|
|
|
185
187
|
} else if (method === 'provider_auth') {
|
|
186
188
|
tokenFingerprint = providerAuthFingerprint(harness);
|
|
187
189
|
}
|
|
188
|
-
const
|
|
190
|
+
const authority = method === 'amalgm' ? proxyBaseUrl : `${baseUrl || ''}:${tokenFingerprint || ''}`;
|
|
191
|
+
const runtimeHomePath = runtimeHome({ runtimeHomeRoot, harness, agentConfigId, method, authority });
|
|
189
192
|
return {
|
|
190
193
|
method,
|
|
191
194
|
baseUrl,
|
|
@@ -201,17 +204,15 @@ export function authEnvelope({
|
|
|
201
204
|
|
|
202
205
|
function safeBaseEnv(baseEnv = process.env) {
|
|
203
206
|
const env = {};
|
|
204
|
-
for (const key of ['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'TMPDIR'
|
|
207
|
+
for (const key of ['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'TMPDIR',
|
|
208
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'SSL_CERT_FILE', 'NODE_EXTRA_CA_CERTS']) {
|
|
205
209
|
if (baseEnv[key]) env[key] = baseEnv[key];
|
|
206
210
|
}
|
|
207
211
|
return env;
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
export function runtimeEnv(contract, baseEnv = process.env) {
|
|
211
|
-
const env =
|
|
212
|
-
for (const key of ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'AI_GATEWAY_API_KEY', 'AI_GATEWAY_BASE_URL', 'CURSOR_API_KEY', 'CURSOR_AUTH_TOKEN']) {
|
|
213
|
-
delete env[key];
|
|
214
|
-
}
|
|
215
|
+
const env = safeBaseEnv(baseEnv);
|
|
215
216
|
if (contract.auth.runtimeHome) {
|
|
216
217
|
fs.mkdirSync(contract.auth.runtimeHome, { recursive: true });
|
|
217
218
|
env.HOME = contract.auth.runtimeHome;
|
|
@@ -248,7 +249,7 @@ export function runtimeEnv(contract, baseEnv = process.env) {
|
|
|
248
249
|
// Pi keys the vercel-ai-gateway provider on this env var; base-URL
|
|
249
250
|
// overrides ride through the generated egress extension instead.
|
|
250
251
|
env.AI_GATEWAY_API_KEY = contract.auth.tokenRef || '';
|
|
251
|
-
} else {
|
|
252
|
+
} else if (contract.harness === 'codex') {
|
|
252
253
|
env.OPENAI_API_KEY = contract.auth.tokenRef || '';
|
|
253
254
|
if (contract.auth.baseUrl) env.OPENAI_BASE_URL = contract.auth.baseUrl;
|
|
254
255
|
}
|
package/host/http.js
CHANGED
|
@@ -96,7 +96,8 @@ async function streamTurn(response, updates, completion) {
|
|
|
96
96
|
const turn = await (typeof completion === 'function' ? completion() : completion);
|
|
97
97
|
writeSse(response, 'complete', turn);
|
|
98
98
|
} catch (error) {
|
|
99
|
-
writeSse(response, '
|
|
99
|
+
if (error?.turn) writeSse(response, 'complete', error.turn);
|
|
100
|
+
else writeSse(response, 'error', errorBody(error).error);
|
|
100
101
|
} finally {
|
|
101
102
|
if (!response.destroyed) response.end();
|
|
102
103
|
}
|
|
@@ -130,7 +131,7 @@ export function createChatHttpHandler(options) {
|
|
|
130
131
|
if (!options?.chat) throw new TypeError('createChatHttpHandler requires chat');
|
|
131
132
|
const chat = options.chat;
|
|
132
133
|
const prefix = String(options.prefix || '/chat').replace(/\/$/, '');
|
|
133
|
-
const bodyLimit = options.bodyLimitBytes ||
|
|
134
|
+
const bodyLimit = options.bodyLimitBytes || 64 * 1024 * 1024;
|
|
134
135
|
|
|
135
136
|
return async function handleChatHttp(request, response) {
|
|
136
137
|
const url = new URL(request.url || '/', 'http://localhost');
|
package/host/model-resolution.js
CHANGED
|
@@ -124,6 +124,13 @@ function selectedModelId(contract) {
|
|
|
124
124
|
return modelId.startsWith(`${providerId}/`) ? modelId : `${providerId}/${modelId}`;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/** Direct Anthropic APIs use hyphenated versions; gateways use dotted ids. */
|
|
128
|
+
export function directProviderModelId(providerId, modelId) {
|
|
129
|
+
return providerId === 'anthropic'
|
|
130
|
+
? modelId.replace(/-(\d+)\.(\d+)(?=$|-)/g, '-$1-$2')
|
|
131
|
+
: modelId;
|
|
132
|
+
}
|
|
133
|
+
|
|
127
134
|
export function gatewayModelId(raw) {
|
|
128
135
|
let bare = String(raw || '').trim()
|
|
129
136
|
.replace(/^opencode-/, '')
|
package/host/native-contract.js
CHANGED
|
@@ -5,7 +5,7 @@ import { authEnvelope } from './auth.js';
|
|
|
5
5
|
import { amalgmDir } from './lib/runtime-paths.js';
|
|
6
6
|
import { resolveNativeModel } from './model-resolution.js';
|
|
7
7
|
import { normalizeAcpMcpServers } from './tooling/mcp-bundle.js';
|
|
8
|
-
import { composeSystemPrompt } from './tooling/system-prompt.js';
|
|
8
|
+
import { composeSystemPrompt, toolSystemInstructions } from './tooling/system-prompt.js';
|
|
9
9
|
|
|
10
10
|
export { nativeModel } from './model-resolution.js';
|
|
11
11
|
|
|
@@ -58,7 +58,7 @@ export class NativeExecutionPreparer {
|
|
|
58
58
|
}));
|
|
59
59
|
this.resolveInstructions = options.resolveInstructions || (async (contract, agent) => composeSystemPrompt(
|
|
60
60
|
{ compiledAgentInstructions: agent?.compiledAgentInstructions || '' },
|
|
61
|
-
{ projectContextPromptBlock: options.projectContextPromptBlock },
|
|
61
|
+
{ amalgmDir: this.amalgmDir, projectContextPromptBlock: options.projectContextPromptBlock },
|
|
62
62
|
));
|
|
63
63
|
this.validateModel = options.validateModel || (async () => undefined);
|
|
64
64
|
this.resolveModel = options.resolveModel || resolveNativeModel;
|
|
@@ -141,7 +141,8 @@ export class NativeExecutionPreparer {
|
|
|
141
141
|
mode: contract.auth.mode,
|
|
142
142
|
bindingId: contract.auth.bindingId,
|
|
143
143
|
});
|
|
144
|
-
const instructionText = String(await this.resolveInstructions(contract, agent) || '').trim()
|
|
144
|
+
const instructionText = [String(await this.resolveInstructions(contract, agent) || '').trim(), toolSystemInstructions(tools)]
|
|
145
|
+
.filter(Boolean).join('\n\n');
|
|
145
146
|
const instructions = Object.freeze({
|
|
146
147
|
text: instructionText,
|
|
147
148
|
revisionId: instructionText
|