@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.
Files changed (114) hide show
  1. package/PURPOSE.md +94 -41
  2. package/README.md +17 -4
  3. package/dist/acp/projection.d.ts +8 -0
  4. package/dist/acp/projection.d.ts.map +1 -1
  5. package/dist/acp/projection.js +10 -3
  6. package/dist/acp/projection.js.map +1 -1
  7. package/dist/conversations/index.d.ts +1 -0
  8. package/dist/conversations/index.d.ts.map +1 -1
  9. package/dist/conversations/index.js +1 -0
  10. package/dist/conversations/index.js.map +1 -1
  11. package/dist/conversations/transcript-parts.d.ts +38 -0
  12. package/dist/conversations/transcript-parts.d.ts.map +1 -0
  13. package/dist/conversations/transcript-parts.js +2 -0
  14. package/dist/conversations/transcript-parts.js.map +1 -0
  15. package/dist/conversations/transcript-tools.d.ts +11 -0
  16. package/dist/conversations/transcript-tools.d.ts.map +1 -0
  17. package/dist/conversations/transcript-tools.js +47 -0
  18. package/dist/conversations/transcript-tools.js.map +1 -0
  19. package/dist/conversations/transcript.d.ts +5 -4
  20. package/dist/conversations/transcript.d.ts.map +1 -1
  21. package/dist/conversations/transcript.js +43 -143
  22. package/dist/conversations/transcript.js.map +1 -1
  23. package/dist/conversations/types.d.ts +19 -0
  24. package/dist/conversations/types.d.ts.map +1 -1
  25. package/dist/conversations/types.js.map +1 -1
  26. package/dist/events/constructors.d.ts.map +1 -1
  27. package/dist/events/constructors.js +2 -52
  28. package/dist/events/constructors.js.map +1 -1
  29. package/dist/events/schema.d.ts +72 -72
  30. package/dist/events/schema.js +16 -16
  31. package/dist/events/schema.js.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/input/prompt.js +1 -1
  37. package/dist/input/prompt.js.map +1 -1
  38. package/dist/mcp/server.js +13 -13
  39. package/dist/mcp/server.js.map +1 -1
  40. package/dist/normalizers/claude.d.ts +142 -142
  41. package/dist/normalizers/claude.d.ts.map +1 -1
  42. package/dist/normalizers/claude.js +23 -37
  43. package/dist/normalizers/claude.js.map +1 -1
  44. package/dist/normalizers/codex.d.ts +71 -71
  45. package/dist/normalizers/codex.d.ts.map +1 -1
  46. package/dist/normalizers/codex.js +12 -24
  47. package/dist/normalizers/codex.js.map +1 -1
  48. package/dist/normalizers/index.d.ts +0 -1
  49. package/dist/normalizers/index.d.ts.map +1 -1
  50. package/dist/normalizers/index.js +0 -1
  51. package/dist/normalizers/index.js.map +1 -1
  52. package/dist/normalizers/opencode.d.ts.map +1 -1
  53. package/dist/normalizers/opencode.js +9 -6
  54. package/dist/normalizers/opencode.js.map +1 -1
  55. package/dist/normalizers/pi.d.ts.map +1 -1
  56. package/dist/normalizers/pi.js +27 -14
  57. package/dist/normalizers/pi.js.map +1 -1
  58. package/dist/sessions/chat.d.ts +5 -5
  59. package/dist/sessions/chat.d.ts.map +1 -1
  60. package/dist/sessions/chat.js +51 -19
  61. package/dist/sessions/chat.js.map +1 -1
  62. package/dist/sessions/index.d.ts +1 -1
  63. package/dist/sessions/index.d.ts.map +1 -1
  64. package/dist/sessions/index.js +1 -1
  65. package/dist/sessions/index.js.map +1 -1
  66. package/dist/usage/acp.d.ts +4 -0
  67. package/dist/usage/acp.d.ts.map +1 -0
  68. package/dist/usage/acp.js +20 -0
  69. package/dist/usage/acp.js.map +1 -0
  70. package/dist/usage/finalize.d.ts +1 -11
  71. package/dist/usage/finalize.d.ts.map +1 -1
  72. package/dist/usage/finalize.js +29 -47
  73. package/dist/usage/finalize.js.map +1 -1
  74. package/dist/usage/index.d.ts +2 -1
  75. package/dist/usage/index.d.ts.map +1 -1
  76. package/dist/usage/index.js +2 -1
  77. package/dist/usage/index.js.map +1 -1
  78. package/dist/usage/normalize.d.ts +8 -9
  79. package/dist/usage/normalize.d.ts.map +1 -1
  80. package/dist/usage/normalize.js +20 -20
  81. package/dist/usage/normalize.js.map +1 -1
  82. package/dist/usage/ports.d.ts +4 -4
  83. package/dist/usage/ports.d.ts.map +1 -1
  84. package/dist/usage/ports.js +5 -5
  85. package/dist/usage/ports.js.map +1 -1
  86. package/dist/usage/tokens.d.ts +13 -38
  87. package/dist/usage/tokens.d.ts.map +1 -1
  88. package/dist/usage/tokens.js +39 -72
  89. package/dist/usage/tokens.js.map +1 -1
  90. package/host/adapters/acp-prompt.js +36 -0
  91. package/host/adapters/acp.js +50 -71
  92. package/host/adapters/attachments.js +37 -0
  93. package/host/adapters/claude.js +9 -2
  94. package/host/adapters/codex.js +36 -18
  95. package/host/adapters/cursor.js +72 -99
  96. package/host/adapters/opencode.js +39 -25
  97. package/host/adapters/pi.js +24 -19
  98. package/host/adapters/prompt.js +13 -21
  99. package/host/adapters/provider-url.js +8 -0
  100. package/host/auth.js +10 -9
  101. package/host/http.js +3 -2
  102. package/host/model-resolution.js +7 -0
  103. package/host/native-contract.js +4 -3
  104. package/host/native-runtime.js +25 -11
  105. package/host/sqlite/index.d.ts +2 -0
  106. package/host/sqlite/index.js +48 -4
  107. package/host/tooling/runtime-home.js +1 -1
  108. package/host/tooling/system-instructions.js +7 -6
  109. package/host/tooling/system-prompt.js +12 -18
  110. package/package.json +26 -2
  111. package/dist/normalizers/cursor.d.ts +0 -33
  112. package/dist/normalizers/cursor.d.ts.map +0 -1
  113. package/dist/normalizers/cursor.js +0 -196
  114. package/dist/normalizers/cursor.js.map +0 -1
@@ -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 { createEventConstructors } from '../../dist/events/index.js';
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: 'acp_extension',
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.cwd,
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
- client.onRequest((method, params) => this.handleAgentRequest(session, method, params));
171
- const initialized = await client.request('initialize', {
172
- protocolVersion: PROTOCOL_VERSION,
173
- clientInfo: { name: 'amalgm-chat', version: '1.0.0' },
174
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
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 queue = [];
274
- let complete = false;
275
- let wake = null;
276
- const wakeLoop = () => { if (wake) { wake(); wake = null; } };
277
- const push = (e) => {
278
- if (e.type === 'done' || e.type === 'error') complete = true;
279
- queue.push(e);
280
- wakeLoop();
281
- };
282
- const state = { providerSessionId: session.providerSessionId, tools: new Map() };
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: contract.auth.baseUrl,
106
- baseUrl: contract.auth.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
- let created = null;
262
- if (contract.providerSessionId) {
263
- try {
264
- created = resultData(await instance.client.session.get({
265
- path: { id: contract.providerSessionId },
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(),
@@ -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
- // Under amalgm auth every gateway request must flow through the local egress
194
- // proxy. Pi's provider baseUrl is overridden by a tiny extension; the URL rides
195
- // in on an env var so the extension file itself is static.
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: routes the Vercel AI Gateway provider',
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.AMALGM_PI_GATEWAY_BASE_URL;',
204
- ` if (baseUrl) pi.registerProvider(${JSON.stringify(GATEWAY_PROVIDER)}, { baseUrl });`,
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 && contract.authMethod === 'amalgm' && !model.includes('/')) {
220
+ if (provider === GATEWAY_PROVIDER && !model.includes('/')) {
222
221
  return `${contract.providerId}/${model}`;
223
222
  }
224
- if (provider !== GATEWAY_PROVIDER && model.startsWith(`${provider}/`)) return model.slice(provider.length + 1);
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 === 'amalgm') args.push('--extension', ensureEgressExtension(runtimeHome));
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 === 'amalgm' && contract.auth.baseUrl) {
381
- env.AMALGM_PI_GATEWAY_BASE_URL = contract.auth.baseUrl;
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(() => push(done({ providerSessionId: session.providerSessionId, stopReason: 'end_turn' })));
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
- ensureEgressExtension,
518
+ ensureProviderExtension,
514
519
  piModel,
515
520
  piProvider,
516
521
  resolveBinary,
@@ -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
- type: 'file',
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
- const data = typeof resource.text === 'string'
180
- ? Buffer.from(resource.text, 'utf8').toString('base64')
181
- : resource.blob;
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 runtimeHomePath = runtimeHome({ runtimeHomeRoot, harness, agentConfigId });
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 = contract.authMethod === 'provider_auth' ? { ...baseEnv } : safeBaseEnv(baseEnv);
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, 'error', errorBody(error).error);
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 || 2_000_000;
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');
@@ -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-/, '')
@@ -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