@amalgm/chat 0.2.2 → 0.2.3

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 (56) hide show
  1. package/AGENTS.md +1 -0
  2. package/PURPOSE.md +44 -2
  3. package/README.md +10 -0
  4. package/dist/api/conversations.d.ts +5 -0
  5. package/dist/api/conversations.d.ts.map +1 -1
  6. package/dist/api/conversations.js +33 -0
  7. package/dist/api/conversations.js.map +1 -1
  8. package/dist/api/index.d.ts +1 -1
  9. package/dist/api/index.d.ts.map +1 -1
  10. package/dist/api/index.js.map +1 -1
  11. package/dist/execution/contract.d.ts +1 -1
  12. package/dist/execution/contract.d.ts.map +1 -1
  13. package/dist/execution/index.d.ts +2 -0
  14. package/dist/execution/index.d.ts.map +1 -1
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/index.d.ts +3 -0
  21. package/dist/mcp/index.d.ts.map +1 -0
  22. package/dist/mcp/index.js +2 -0
  23. package/dist/mcp/index.js.map +1 -0
  24. package/dist/mcp/server.d.ts +7 -0
  25. package/dist/mcp/server.d.ts.map +1 -0
  26. package/dist/mcp/server.js +136 -0
  27. package/dist/mcp/server.js.map +1 -0
  28. package/dist/mcp/types.d.ts +17 -0
  29. package/dist/mcp/types.d.ts.map +1 -0
  30. package/dist/mcp/types.js +2 -0
  31. package/dist/mcp/types.js.map +1 -0
  32. package/docs/contracts/capabilities-and-instructions.md +103 -0
  33. package/docs/contracts/input-and-execution.md +8 -3
  34. package/host/adapters/acp-capabilities.js +32 -0
  35. package/host/adapters/acp.js +8 -12
  36. package/host/adapters/claude.js +3 -2
  37. package/host/adapters/codex.js +19 -6
  38. package/host/adapters/cursor.js +9 -15
  39. package/host/adapters/input-capabilities.js +1 -0
  40. package/host/adapters/opencode.js +4 -3
  41. package/host/adapters/pi.js +3 -2
  42. package/host/adapters/prompt.js +2 -3
  43. package/host/auth.js +1 -1
  44. package/host/http.d.ts +4 -0
  45. package/host/http.js +43 -0
  46. package/host/index.d.ts +45 -3
  47. package/host/index.js +3 -0
  48. package/host/native-contract.js +26 -4
  49. package/host/native-runtime.js +8 -0
  50. package/host/platform-egress.js +46 -25
  51. package/host/title-generator.js +110 -0
  52. package/host/tooling/mcp-bundle.js +107 -56
  53. package/host/tooling/system-prompt.js +7 -4
  54. package/package.json +6 -1
  55. package/skills/chat/SKILL.md +300 -89
  56. package/skills/chat/references/contracts.md +73 -47
@@ -34,9 +34,10 @@ discovery and resolution; start and send consume its result.
34
34
  defaults or fallback auth modes.
35
35
  4. A model is identified by `(agent revision, provider id, model id)`. Model
36
36
  settings are ACP session-config selections scoped inside that model choice.
37
- 5. Ordinary tools and MCP tools are selected through one immutable toolset
38
- revision. `amalgm-tools` resolves that revision to the complete official ACP
39
- MCP server configuration during preparation; Chat adds no ambient server.
37
+ 5. Ordinary tools and MCP tools are selected through one immutable deployment-
38
+ head revision, never a local SQLite sequence. `amalgm-tools` resolves that
39
+ exact revision to the complete official ACP `McpServer[]` configuration
40
+ during preparation; Chat adds no ambient server or latest-version fallback.
40
41
  6. Secrets are not execution-contract values. Auth names an opaque binding; the
41
42
  authorized host resolves secrets into the opaque prepared binding.
42
43
  7. A cwd is an absolute, already-materialized path on the selected computer. A
@@ -57,6 +58,10 @@ discovery and resolution; start and send consume its result.
57
58
  before prompting. No adapter may silently ignore an attachment, auth mode,
58
59
  tool, permission mode, model setting, provider/model choice, cwd, agent
59
60
  revision, or computer placement.
61
+ 13. Machine, project, and exact Agent-revision instructions compose once during
62
+ preparation. They are not prompt parts. Native adapters use a real
63
+ system/developer channel; ACP agents must advertise the `amalgm.dev`
64
+ session-instructions extension.
60
65
 
61
66
  ## Public path
62
67
 
@@ -1,5 +1,7 @@
1
1
  import { UnsupportedExecutionInputError } from './input-capabilities.js';
2
2
 
3
+ export const AMALGM_ACP_META_KEY = 'amalgm.dev';
4
+
3
5
  /** Validate optional ACP inputs against the capabilities negotiated at initialize. */
4
6
  export function assertAcpMcpCapabilities(adapterId, capabilities, servers) {
5
7
  const mcp = capabilities?.mcpCapabilities || {};
@@ -28,3 +30,33 @@ export function assertAcpPromptCapabilities(adapterId, capabilities, prompt) {
28
30
  }
29
31
  }
30
32
  }
33
+
34
+ /**
35
+ * ACP v1 has no system-prompt field. Apply instructions only through the
36
+ * advertised Amalgm lifecycle extension; never turn them into prompt content.
37
+ */
38
+ export function acpSessionParams(adapterId, capabilities, contract, mcpServers) {
39
+ const instructions = contract?.instructions || { text: '', revisionId: null };
40
+ const text = String(instructions.text || '').trim();
41
+ if (!text) return { cwd: contract.cwd, mcpServers };
42
+ const extension = capabilities?._meta?.[AMALGM_ACP_META_KEY];
43
+ if (extension?.sessionInstructions !== true) {
44
+ throw new UnsupportedExecutionInputError(
45
+ adapterId,
46
+ 'instructions',
47
+ `agent does not advertise the ACP ${AMALGM_ACP_META_KEY} sessionInstructions extension`,
48
+ );
49
+ }
50
+ return {
51
+ cwd: contract.cwd,
52
+ mcpServers,
53
+ _meta: {
54
+ [AMALGM_ACP_META_KEY]: {
55
+ sessionInstructions: {
56
+ revisionId: instructions.revisionId,
57
+ text,
58
+ },
59
+ },
60
+ },
61
+ };
62
+ }
@@ -3,10 +3,13 @@
3
3
  import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
4
4
  import { toAcpMcpServers } from '../tooling/mcp-bundle.js';
5
5
  import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
6
- import { composeSystemPrompt } from '../tooling/system-prompt.js';
7
6
  import { requestPermission } from '../permissions.js';
8
7
  import { AcpClient } from './acp-client.js';
9
- import { assertAcpMcpCapabilities, assertAcpPromptCapabilities } from './acp-capabilities.js';
8
+ import {
9
+ acpSessionParams,
10
+ assertAcpMcpCapabilities,
11
+ assertAcpPromptCapabilities,
12
+ } from './acp-capabilities.js';
10
13
  import { nativeInputCapabilities, UnsupportedExecutionInputError } from './input-capabilities.js';
11
14
  import { acpPromptParts } from './prompt.js';
12
15
 
@@ -125,6 +128,7 @@ export class AcpAdapter {
125
128
  authModes: ['platform', 'subscription', 'byok'],
126
129
  modelSettings: 'negotiated',
127
130
  tools: true,
131
+ instructions: 'acp_extension',
128
132
  });
129
133
  }
130
134
 
@@ -149,7 +153,6 @@ export class AcpAdapter {
149
153
  client,
150
154
  capabilities: {},
151
155
  permissionMode: contract.permissionMode,
152
- needsSystemPrompt: false,
153
156
  activePrompt: false,
154
157
  };
155
158
  client.onRequest((method, params) => this.handleRequest(session, method, params));
@@ -170,7 +173,7 @@ export class AcpAdapter {
170
173
 
171
174
  const mcpServers = toAcpMcpServers(contract);
172
175
  assertAcpMcpCapabilities('acp', session.capabilities, mcpServers);
173
- const sessionParams = { cwd: contract.cwd, mcpServers };
176
+ const sessionParams = acpSessionParams('acp', session.capabilities, contract, mcpServers);
174
177
  let response;
175
178
  if (session.providerSessionId) {
176
179
  if (session.capabilities.loadSession === true) {
@@ -189,7 +192,6 @@ export class AcpAdapter {
189
192
  } else {
190
193
  response = await client.request('session/new', sessionParams, 600000);
191
194
  session.providerSessionId = response?.sessionId || null;
192
- session.needsSystemPrompt = true;
193
195
  }
194
196
  if (!session.providerSessionId) throw new Error('ACP agent did not return a sessionId');
195
197
  await applyConfiguration(client, session.providerSessionId, response, contract, launch);
@@ -244,16 +246,10 @@ export class AcpAdapter {
244
246
  if (message.params?.update) queue.push(message.params.update);
245
247
  wakeLoop();
246
248
  });
247
- let preamble = '';
248
- if (session.needsSystemPrompt) {
249
- const systemPrompt = composeSystemPrompt(contract);
250
- if (systemPrompt) preamble = `<system-instructions>\n${systemPrompt}\n</system-instructions>`;
251
- session.needsSystemPrompt = false;
252
- }
253
249
  session.activePrompt = true;
254
250
  session.client.request('session/prompt', {
255
251
  sessionId: session.providerSessionId,
256
- prompt: acpPromptParts(prompt, preamble),
252
+ prompt: acpPromptParts(prompt),
257
253
  }, 0).then(() => {
258
254
  finished = true;
259
255
  wakeLoop();
@@ -21,7 +21,7 @@ import { toClaudeMcpServers } from '../tooling/mcp-bundle.js';
21
21
  import { bundledClaudeBinary } from '../tooling/native-binaries.js';
22
22
  import { importPackage } from '../tooling/package-import.js';
23
23
  import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
24
- import { composeSystemPrompt } from '../tooling/system-prompt.js';
24
+ import { preparedSystemPrompt } from '../tooling/system-prompt.js';
25
25
  import { allowed, requestPermission } from '../permissions.js';
26
26
  import {
27
27
  COMMON_REASONING_SETTINGS,
@@ -55,6 +55,7 @@ class ClaudeAdapter {
55
55
  authModes: ['platform', 'subscription', 'byok'],
56
56
  modelSettings: [...COMMON_REASONING_SETTINGS, ...COMMON_SPEED_SETTINGS],
57
57
  tools: true,
58
+ instructions: 'native',
58
59
  });
59
60
  }
60
61
 
@@ -70,7 +71,7 @@ class ClaudeAdapter {
70
71
 
71
72
  options(contract, extra = {}) {
72
73
  const runtime = prepareHarnessRuntime(contract);
73
- const systemPrompt = composeSystemPrompt(contract);
74
+ const systemPrompt = preparedSystemPrompt(contract);
74
75
  const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
75
76
  const settings = {
76
77
  ...this.renderers.toClaudeSettings(contract.agentConfig),
@@ -17,11 +17,11 @@ import path from 'node:path';
17
17
  import { createEventConstructors } from '../../dist/events/index.js';
18
18
  import { codexErrorMessage, createCodexNormalizer } from '../../dist/normalizers/codex.js';
19
19
  import { recordNativeEvent } from '../recorder.js';
20
- import { relayedMcpServers, toCodexMcpToml } from '../tooling/mcp-bundle.js';
20
+ import { codexMcpSectionName, relayedMcpServers, toCodexMcpToml } from '../tooling/mcp-bundle.js';
21
21
  import { bundledCodexBinary, bundledCodexPathDirs, executableExists, findOnPath } from '../tooling/native-binaries.js';
22
22
  import { syncCodexProviderAuth } from '../tooling/native-config.js';
23
23
  import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
24
- import { composeSystemPrompt } from '../tooling/system-prompt.js';
24
+ import { preparedSystemPrompt } from '../tooling/system-prompt.js';
25
25
  import { requestPermission } from '../permissions.js';
26
26
  import {
27
27
  COMMON_REASONING_SETTINGS,
@@ -447,7 +447,7 @@ function generatedCodexHookTrust(contract, renderers = defaultRenderers) {
447
447
  }
448
448
 
449
449
  function generatedMcpSectionNames(contract) {
450
- return relayedMcpServers(contract).map((server) => `mcp_servers.${server.name}`);
450
+ return relayedMcpServers(contract).map((server) => codexMcpSectionName(server.name));
451
451
  }
452
452
 
453
453
  function buildCodexConfig(contract, existingConfig, syncInfo, renderers = defaultRenderers) {
@@ -524,6 +524,7 @@ class CodexAdapter {
524
524
  authModes: ['platform', 'subscription', 'byok'],
525
525
  modelSettings: [...COMMON_REASONING_SETTINGS, ...COMMON_SPEED_SETTINGS],
526
526
  tools: true,
527
+ instructions: 'native',
527
528
  });
528
529
  }
529
530
 
@@ -541,7 +542,7 @@ class CodexAdapter {
541
542
  sandbox: fullAccess ? 'danger-full-access' : 'workspace-write',
542
543
  modelProvider: contract.authMethod === 'amalgm' ? 'amalgm' : 'openai',
543
544
  serviceTier: serviceTierForContract(contract),
544
- developerInstructions: composeSystemPrompt(contract) || null,
545
+ developerInstructions: preparedSystemPrompt(contract) || null,
545
546
  persistExtendedHistory: true,
546
547
  };
547
548
  const session = {
@@ -571,6 +572,7 @@ class CodexAdapter {
571
572
  'item/commandExecution/requestApproval',
572
573
  'item/fileChange/requestApproval',
573
574
  'item/permissions/requestApproval',
575
+ 'mcpServer/elicitation/request',
574
576
  'execCommandApproval',
575
577
  'applyPatchApproval',
576
578
  ]);
@@ -580,8 +582,12 @@ class CodexAdapter {
580
582
  : await requestPermission(this.permissionPort, {
581
583
  adapterId: 'codex',
582
584
  providerSessionId: session.providerSessionId,
583
- kind: method.includes('fileChange') || method === 'applyPatchApproval' ? 'file_change' : 'command',
584
- title: params.reason || params.command || 'Codex requests permission',
585
+ kind: method === 'mcpServer/elicitation/request'
586
+ ? 'mcp_tool'
587
+ : method.includes('fileChange') || method === 'applyPatchApproval'
588
+ ? 'file_change'
589
+ : 'command',
590
+ title: params.message || params.reason || params.command || 'Codex requests permission',
585
591
  input: params,
586
592
  });
587
593
  const allow = decision.outcome === 'allow_once' || decision.outcome === 'allow_always';
@@ -603,6 +609,13 @@ class CodexAdapter {
603
609
  scope: always ? 'session' : 'turn',
604
610
  };
605
611
  }
612
+ if (method === 'mcpServer/elicitation/request') {
613
+ return {
614
+ action: allow ? 'accept' : decision.outcome === 'cancel' ? 'cancel' : 'decline',
615
+ content: allow ? decision.updatedInput || null : null,
616
+ _meta: null,
617
+ };
618
+ }
606
619
  return {
607
620
  decision: allow
608
621
  ? (always ? 'approved_for_session' : 'approved')
@@ -1,7 +1,7 @@
1
1
  // Cursor Agent Client Protocol adapter. The generic ACP transport lives in
2
2
  // ./acp-client.js; this file is the
3
3
  // cursor parameterization (binary resolution, model translation, permission
4
- // auto-grant, first-prompt system preamble).
4
+ // handling, and ACP lifecycle inputs).
5
5
  //
6
6
  // Laws come from ../../dist: event constructors via `createEventConstructors`,
7
7
  // the normalizer via `createCursorNormalizer`, `cursorStopReason`, and
@@ -16,10 +16,13 @@ import { recordNativeEvent } from '../recorder.js';
16
16
  import { toAcpMcpServers } from '../tooling/mcp-bundle.js';
17
17
  import { executableExists, findOnPath } from '../tooling/native-binaries.js';
18
18
  import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
19
- import { composeSystemPrompt } from '../tooling/system-prompt.js';
20
19
  import { requestPermission } from '../permissions.js';
21
20
  import { AcpClient } from './acp-client.js';
22
- import { assertAcpMcpCapabilities, assertAcpPromptCapabilities } from './acp-capabilities.js';
21
+ import {
22
+ acpSessionParams,
23
+ assertAcpMcpCapabilities,
24
+ assertAcpPromptCapabilities,
25
+ } from './acp-capabilities.js';
23
26
  import {
24
27
  COMMON_REASONING_SETTINGS,
25
28
  COMMON_SPEED_SETTINGS,
@@ -140,6 +143,7 @@ class CursorAdapter {
140
143
  authModes: ['subscription', 'byok'],
141
144
  modelSettings: [...COMMON_REASONING_SETTINGS, ...COMMON_SPEED_SETTINGS],
142
145
  tools: true,
146
+ instructions: 'acp_extension',
143
147
  });
144
148
  }
145
149
 
@@ -157,7 +161,6 @@ class CursorAdapter {
157
161
  sessionId: contract.sessionId,
158
162
  providerSessionId: contract.providerSessionId || null,
159
163
  client,
160
- needsSystemPrompt: false,
161
164
  modelWarning: null,
162
165
  activePrompt: null,
163
166
  cancelRequested: false,
@@ -173,7 +176,7 @@ class CursorAdapter {
173
176
  session.capabilities = initialized?.agentCapabilities || {};
174
177
  const mcpServers = toAcpMcpServers(contract);
175
178
  assertAcpMcpCapabilities('cursor', session.capabilities, mcpServers);
176
- const sessionParams = { cwd: contract.cwd, mcpServers };
179
+ const sessionParams = acpSessionParams('cursor', session.capabilities, contract, mcpServers);
177
180
  let acpSession = null;
178
181
  if (session.providerSessionId) {
179
182
  // session/load replays history as session/update notifications; nothing is
@@ -189,7 +192,6 @@ class CursorAdapter {
189
192
  if (!acpSession) {
190
193
  acpSession = await client.request('session/new', sessionParams);
191
194
  session.providerSessionId = acpSession.sessionId;
192
- session.needsSystemPrompt = true;
193
195
  }
194
196
  await this.selectModel(session, acpSession, contract);
195
197
  return session;
@@ -296,17 +298,9 @@ class CursorAdapter {
296
298
  });
297
299
  for (const e of normalizeCursorUpdate(params.update || {}, state)) push(e);
298
300
  });
299
- let preamble = '';
300
- if (session.needsSystemPrompt) {
301
- // ACP has no system-prompt slot; a fresh native session gets the composed
302
- // instructions as a preamble on its first prompt only.
303
- const systemPrompt = composeSystemPrompt(contract);
304
- if (systemPrompt) preamble = `<system-instructions>\n${systemPrompt}\n</system-instructions>`;
305
- session.needsSystemPrompt = false;
306
- }
307
301
  session.client.request('session/prompt', {
308
302
  sessionId: session.providerSessionId,
309
- prompt: acpPromptParts(input, preamble),
303
+ prompt: acpPromptParts(input),
310
304
  }, 0).then((result) => {
311
305
  recordNativeEvent('cursor.acp.prompt_result', result, {
312
306
  providerSessionId: session.providerSessionId,
@@ -29,6 +29,7 @@ export function nativeInputCapabilities(input = {}) {
29
29
  ? 'negotiated'
30
30
  : Object.freeze([...(input.modelSettings || [])]),
31
31
  tools: input.tools === true,
32
+ instructions: input.instructions || 'unsupported',
32
33
  permissionModes: Object.freeze([...(input.permissionModes || ['ask', 'full_access'])]),
33
34
  });
34
35
  }
@@ -12,7 +12,7 @@ import { toOpenCodeMcpConfig } from '../tooling/mcp-bundle.js';
12
12
  import { bundledOpenCodeBinary, executableExists, findOnPath } from '../tooling/native-binaries.js';
13
13
  import { importPackage } from '../tooling/package-import.js';
14
14
  import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
15
- import { composeSystemPrompt } from '../tooling/system-prompt.js';
15
+ import { preparedSystemPrompt } from '../tooling/system-prompt.js';
16
16
  import { requestPermission } from '../permissions.js';
17
17
  import { nativeInputCapabilities, UnsupportedExecutionInputError } from './input-capabilities.js';
18
18
  import { openCodePromptParts } from './prompt.js';
@@ -58,7 +58,7 @@ function selectedVariant(contract) {
58
58
  function configFor(contract) {
59
59
  const model = splitModel(contract.cliModel || contract.usageModelId, contract.auth);
60
60
  const variant = selectedVariant(contract);
61
- const systemPrompt = composeSystemPrompt(contract);
61
+ const systemPrompt = preparedSystemPrompt(contract);
62
62
  const permission = contract.permissionMode === 'full_access' ? 'allow' : 'ask';
63
63
  const config = {
64
64
  logLevel: process.env.OPENCODE_LOG_LEVEL || 'ERROR',
@@ -241,6 +241,7 @@ class OpenCodeAdapter {
241
241
  authModes: ['platform', 'subscription', 'byok'],
242
242
  modelSettings: ['variant'],
243
243
  tools: true,
244
+ instructions: 'native',
244
245
  });
245
246
  }
246
247
 
@@ -423,7 +424,7 @@ class OpenCodeAdapter {
423
424
  model,
424
425
  ...(variant ? { variant } : {}),
425
426
  agent: process.env.AMALGM_CHAT_OPENCODE_AGENT || 'build',
426
- system: composeSystemPrompt(contract) || undefined,
427
+ system: preparedSystemPrompt(contract) || undefined,
427
428
  parts: openCodePromptParts(input),
428
429
  },
429
430
  });
@@ -15,7 +15,7 @@ import { recordNativeEvent } from '../recorder.js';
15
15
  import { relayedMcpServers } from '../tooling/mcp-bundle.js';
16
16
  import { executableExists, findOnPath } from '../tooling/native-binaries.js';
17
17
  import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
18
- import { composeSystemPrompt } from '../tooling/system-prompt.js';
18
+ import { preparedSystemPrompt } from '../tooling/system-prompt.js';
19
19
  import {
20
20
  COMMON_REASONING_SETTINGS,
21
21
  nativeInputCapabilities,
@@ -344,7 +344,7 @@ function buildArgs(contract, runtimeHome) {
344
344
  const args = ['--mode', 'rpc', '--provider', provider, '--model', piModel(contract, provider)];
345
345
  const thinking = thinkingLevelFor(contract);
346
346
  if (thinking) args.push('--thinking', thinking);
347
- const systemPrompt = composeSystemPrompt(contract);
347
+ const systemPrompt = preparedSystemPrompt(contract);
348
348
  if (systemPrompt) args.push('--system-prompt', systemPrompt);
349
349
  if (contract.providerSessionId) args.push('--session', contract.providerSessionId);
350
350
  if (contract.authMethod === 'amalgm') args.push('--extension', ensureEgressExtension(runtimeHome));
@@ -362,6 +362,7 @@ class PiAdapter {
362
362
  authModes: ['platform', 'subscription', 'byok'],
363
363
  modelSettings: COMMON_REASONING_SETTINGS,
364
364
  tools: true,
365
+ instructions: 'native',
365
366
  });
366
367
  }
367
368
 
@@ -56,9 +56,8 @@ function textProjection(part) {
56
56
  return null;
57
57
  }
58
58
 
59
- export function acpPromptParts(prompt, preamble = '') {
60
- const parts = prompt.parts.map((part) => structuredClone(part));
61
- return preamble ? [{ type: 'text', text: preamble }, ...parts] : parts;
59
+ export function acpPromptParts(prompt) {
60
+ return prompt.parts.map((part) => structuredClone(part));
62
61
  }
63
62
 
64
63
  export function codexPromptInput(prompt) {
package/host/auth.js CHANGED
@@ -230,7 +230,7 @@ export function runtimeEnv(contract, baseEnv = process.env) {
230
230
  }
231
231
  }
232
232
  env.IS_SANDBOX = '1';
233
- if (baseEnv.AMALGM_RUNTIME_TOKEN) env.AMALGM_RUNTIME_TOKEN = baseEnv.AMALGM_RUNTIME_TOKEN;
233
+ if (contract.runtimeToken) env.AMALGM_RUNTIME_TOKEN = contract.runtimeToken;
234
234
  if (contract.authMethod === 'amalgm' || contract.authMethod === 'byok') {
235
235
  if (contract.harness === 'claude_code') {
236
236
  if (contract.authMethod === 'amalgm') {
package/host/http.d.ts CHANGED
@@ -14,6 +14,10 @@ export interface ChatHttpHandlerOptions<Binding = unknown, RuntimeSession = unkn
14
14
  readonly serverName: string;
15
15
  readonly search: string;
16
16
  }>;
17
+ readonly generateTitle?: (input: Readonly<{
18
+ readonly conversationId: string;
19
+ readonly message: string;
20
+ }>) => AsyncIterable<string>;
17
21
  }
18
22
 
19
23
  export type ChatAuxiliaryHandler<Input> = (
package/host/http.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { normalizeGeneratedTitle, titleMessageExcerpt } from './title-generator.js';
2
+
1
3
  const JSON_HEADERS = Object.freeze({
2
4
  'content-type': 'application/json; charset=utf-8',
3
5
  'cache-control': 'no-store',
@@ -100,6 +102,29 @@ async function streamTurn(response, updates, completion) {
100
102
  }
101
103
  }
102
104
 
105
+ async function streamGeneratedTitle(response, chunks, persist) {
106
+ response.writeHead(200, {
107
+ 'content-type': 'text/plain; charset=utf-8',
108
+ 'cache-control': 'no-store',
109
+ });
110
+ response.flushHeaders?.();
111
+ let generated = '';
112
+ let streamed = '';
113
+ for await (const chunk of chunks) {
114
+ if (typeof chunk !== 'string' || !chunk) continue;
115
+ generated += chunk;
116
+ const title = normalizeGeneratedTitle(generated);
117
+ if (title.startsWith(streamed)) {
118
+ const addition = title.slice(streamed.length);
119
+ if (addition && !response.destroyed) response.write(addition);
120
+ streamed = title;
121
+ }
122
+ }
123
+ const title = normalizeGeneratedTitle(generated);
124
+ if (title) await persist(title);
125
+ if (!response.destroyed) response.end();
126
+ }
127
+
103
128
  /** Node HTTP adapter over the public Chat capability. */
104
129
  export function createChatHttpHandler(options) {
105
130
  if (!options?.chat) throw new TypeError('createChatHttpHandler requires chat');
@@ -189,6 +214,24 @@ export function createChatHttpHandler(options) {
189
214
  await streamTurn(response, stream, stream.completion);
190
215
  return true;
191
216
  }
217
+ if (method === 'POST' && path[2] === 'title' && path.length === 3) {
218
+ if (typeof options.generateTitle !== 'function') {
219
+ sendJson(response, 501, errorBody(Object.assign(new Error('Title generation is not configured'), {
220
+ code: 'TITLE_GENERATION_NOT_CONFIGURED',
221
+ })));
222
+ return true;
223
+ }
224
+ const body = await readJson(request, bodyLimit);
225
+ const message = titleMessageExcerpt(body.message);
226
+ if (!message.trim()) throw Object.assign(new Error('message is required'), { code: 'INVALID_REQUEST' });
227
+ await chat.inspectSession(conversationId);
228
+ await streamGeneratedTitle(
229
+ response,
230
+ options.generateTitle({ conversationId, message }),
231
+ (title) => chat.updateTitle(conversationId, title),
232
+ );
233
+ return true;
234
+ }
192
235
  if (method === 'GET' && path[2] === 'turns' && path[3] && path[4] === 'events') {
193
236
  const after = Number(url.searchParams.get('afterSequence') || request.headers['last-event-id'] || 0);
194
237
  const updates = chat.reconnectTurn(conversationId, path[3], after);
package/host/index.d.ts CHANGED
@@ -7,6 +7,47 @@ import type {
7
7
  } from '../dist/index.js';
8
8
  import type { ConversationStorePort, ConversationTurn } from '../dist/conversations/index.js';
9
9
  import type { ChatHttpHandler } from './http.js';
10
+ import type { ExecutionContract, ToolSelection } from '../dist/execution/index.js';
11
+ import type { McpServer } from '@agentclientprotocol/sdk';
12
+
13
+ export interface ResolvedAgentExecution {
14
+ readonly agentConfig?: Readonly<Record<string, unknown>>;
15
+ readonly compiledAgentInstructions?: string;
16
+ readonly acpLaunch?: Readonly<Record<string, unknown>> | null;
17
+ }
18
+
19
+ export interface PreparedInstructionBundle {
20
+ readonly text: string;
21
+ readonly revisionId: string | null;
22
+ }
23
+
24
+ export interface NativeExecutionPreparerOptions {
25
+ readonly currentComputerId?: string | null;
26
+ readonly localBaseUrl?: string | (() => string | null) | null;
27
+ readonly runtimeToken?: string | (() => string | null) | null;
28
+ readonly amalgmDir?: string;
29
+ readonly runtimeHomeRoot?: string;
30
+ readonly proxyBaseUrl?: string;
31
+ readonly resolveTools?: (
32
+ selection: ToolSelection,
33
+ contract: ExecutionContract,
34
+ ) => readonly McpServer[] | Promise<readonly McpServer[]>;
35
+ readonly resolveAgent?: (
36
+ selection: ExecutionContract['agent'],
37
+ contract: ExecutionContract,
38
+ ) => ResolvedAgentExecution | Promise<ResolvedAgentExecution>;
39
+ readonly resolveInstructions?: (
40
+ contract: ExecutionContract,
41
+ agent: ResolvedAgentExecution,
42
+ ) => string | Promise<string>;
43
+ readonly projectContextPromptBlock?: (contract: ExecutionContract) => string;
44
+ readonly [key: string]: unknown;
45
+ }
46
+
47
+ export interface NativeChatRuntimeOptions extends NativeExecutionPreparerOptions {
48
+ readonly adapters?: Readonly<Record<string, (options: Readonly<Record<string, unknown>>) => unknown>>;
49
+ readonly requestPermission?: ((request: unknown) => Promise<unknown>) | null;
50
+ }
10
51
 
11
52
  export interface PlatformCredentialBroker {
12
53
  authorize(request: Readonly<{
@@ -27,7 +68,7 @@ export interface CreateChatHostOptions<Binding = unknown, RuntimeSession = unkno
27
68
  readonly databasePath?: string;
28
69
  readonly conversations?: ConversationStorePort;
29
70
  readonly runtime?: ChatRuntimePort<Binding, RuntimeSession>;
30
- readonly native?: Readonly<Record<string, unknown>> & {
71
+ readonly native?: NativeChatRuntimeOptions & {
31
72
  readonly credentialBroker?: PlatformCredentialBroker;
32
73
  };
33
74
  readonly uuid?: RandomUuidPort;
@@ -58,7 +99,7 @@ export { createChatHttpHandler } from './http.js';
58
99
  export type { ChatHttpHandler, ChatHttpHandlerOptions } from './http.js';
59
100
 
60
101
  export class NativeChatRuntime implements ChatRuntimePort<unknown, unknown> {
61
- constructor(options?: Readonly<Record<string, unknown>>);
102
+ constructor(options?: NativeChatRuntimeOptions);
62
103
  prepareExecution(contract: import('../dist/execution/index.js').ExecutionContract): Promise<unknown>;
63
104
  startSession(request: import('../dist/sessions/index.js').RuntimeStartRequest<unknown>): Promise<unknown>;
64
105
  resumeSession(request: import('../dist/sessions/index.js').RuntimeResumeRequest<unknown>): Promise<unknown>;
@@ -66,10 +107,11 @@ export class NativeChatRuntime implements ChatRuntimePort<unknown, unknown> {
66
107
  interruptTurn(request: import('../dist/sessions/index.js').RuntimeInterruptRequest<unknown, unknown>): Promise<void>;
67
108
  checkpointSession(request: { readonly session: unknown }): unknown;
68
109
  closeSession(sessionId: string, session: unknown): Promise<void>;
110
+ generateTitle(input: Readonly<{ conversationId: string; message: string }>): AsyncIterable<string>;
69
111
  }
70
112
 
71
113
  export class NativeExecutionPreparer {
72
- constructor(options?: Readonly<Record<string, unknown>>);
114
+ constructor(options?: NativeExecutionPreparerOptions);
73
115
  prepare(contract: import('../dist/execution/index.js').ExecutionContract): Promise<unknown>;
74
116
  }
75
117
 
package/host/index.js CHANGED
@@ -38,6 +38,9 @@ export async function createChatHost(options) {
38
38
  ...(typeof runtime.forwardMcp === 'function'
39
39
  ? { forwardMcp: runtime.forwardMcp.bind(runtime) }
40
40
  : {}),
41
+ ...(typeof runtime.generateTitle === 'function'
42
+ ? { generateTitle: runtime.generateTitle.bind(runtime) }
43
+ : {}),
41
44
  });
42
45
  return Object.freeze({
43
46
  chat,
@@ -1,9 +1,11 @@
1
- import { createHmac, randomBytes } from 'node:crypto';
1
+ import { createHash, createHmac, randomBytes } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { authEnvelope } from './auth.js';
5
5
  import { amalgmDir } from './lib/runtime-paths.js';
6
6
  import { resolveNativeModel } from './model-resolution.js';
7
+ import { normalizeAcpMcpServers } from './tooling/mcp-bundle.js';
8
+ import { composeSystemPrompt } from './tooling/system-prompt.js';
7
9
 
8
10
  export { nativeModel } from './model-resolution.js';
9
11
 
@@ -54,6 +56,10 @@ export class NativeExecutionPreparer {
54
56
  agentConfig: {},
55
57
  compiledAgentInstructions: '',
56
58
  }));
59
+ this.resolveInstructions = options.resolveInstructions || (async (contract, agent) => composeSystemPrompt(
60
+ { compiledAgentInstructions: agent?.compiledAgentInstructions || '' },
61
+ { projectContextPromptBlock: options.projectContextPromptBlock },
62
+ ));
57
63
  this.validateModel = options.validateModel || (async () => undefined);
58
64
  this.resolveModel = options.resolveModel || resolveNativeModel;
59
65
  this.resolveAuth = options.resolveAuth || ((selection, contract, context) => authEnvelope({
@@ -71,6 +77,7 @@ export class NativeExecutionPreparer {
71
77
  agentConfigId: contract.agent.installationId,
72
78
  }));
73
79
  this.localBaseUrl = options.localBaseUrl || null;
80
+ this.runtimeToken = options.runtimeToken || null;
74
81
  this.amalgmDir = options.amalgmDir || amalgmDir();
75
82
  this.runtimeHomeRoot = path.resolve(
76
83
  options.runtimeHomeRoot || path.join(this.amalgmDir, 'cli-homes'),
@@ -87,6 +94,13 @@ export class NativeExecutionPreparer {
87
94
  return typeof value === 'string' && value.trim() ? value.replace(/\/$/, '') : null;
88
95
  }
89
96
 
97
+ resolveRuntimeToken() {
98
+ const value = typeof this.runtimeToken === 'function'
99
+ ? this.runtimeToken()
100
+ : this.runtimeToken;
101
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
102
+ }
103
+
90
104
  async resolvePreparedAuth(selection, contract, context) {
91
105
  try {
92
106
  return await this.resolveAuth(selection, contract, context);
@@ -106,13 +120,13 @@ export class NativeExecutionPreparer {
106
120
 
107
121
  const authMethod = AUTH_METHOD[contract.auth.mode];
108
122
  const model = await this.resolveModel(contract);
109
- const [tools, agent, , auth] = await Promise.all([
123
+ const [resolvedTools, agent, , auth] = await Promise.all([
110
124
  this.resolveTools(contract.tools, contract),
111
125
  this.resolveAgent(contract.agent, contract),
112
126
  this.validateModel(contract.model, contract.agent),
113
127
  this.resolvePreparedAuth(contract.auth, contract, { authMethod, modelId: model.usageModelId }),
114
128
  ]);
115
- if (!Array.isArray(tools)) throw new Error('resolveTools must return ACP/native MCP server configs');
129
+ const tools = normalizeAcpMcpServers(resolvedTools);
116
130
  if (!auth || typeof auth !== 'object') {
117
131
  throw new InvalidPreparedAuthError('resolveAuth must return a prepared auth binding');
118
132
  }
@@ -127,6 +141,13 @@ export class NativeExecutionPreparer {
127
141
  mode: contract.auth.mode,
128
142
  bindingId: contract.auth.bindingId,
129
143
  });
144
+ const instructionText = String(await this.resolveInstructions(contract, agent) || '').trim();
145
+ const instructions = Object.freeze({
146
+ text: instructionText,
147
+ revisionId: instructionText
148
+ ? createHash('sha256').update(instructionText).digest('hex')
149
+ : null,
150
+ });
130
151
 
131
152
  return Object.freeze({
132
153
  adapterId: contract.agent.adapterId,
@@ -156,7 +177,7 @@ export class NativeExecutionPreparer {
156
177
  localBaseUrl: this.resolveLocalBaseUrl(),
157
178
  mcpServers: tools,
158
179
  toolSelection: contract.tools,
159
- compiledAgentInstructions: agent?.compiledAgentInstructions || '',
180
+ instructions,
160
181
  permissionMode: contract.permissionMode,
161
182
  executionComputerId: contract.computerId,
162
183
  usageOwner: contract.auth.mode === 'platform' ? 'platform_proxy' : 'local_user',
@@ -179,6 +200,7 @@ export class NativeExecutionPreparer {
179
200
  assistantMessageId: turnId,
180
201
  userMessageId: null,
181
202
  providerSessionId,
203
+ runtimeToken: this.resolveRuntimeToken(),
182
204
  auth: sessionAuth(binding.base.auth, sessionId, this.localEgressSecret),
183
205
  };
184
206
  }