@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.
- package/AGENTS.md +1 -0
- package/PURPOSE.md +44 -2
- package/README.md +10 -0
- package/dist/api/conversations.d.ts +5 -0
- package/dist/api/conversations.d.ts.map +1 -1
- package/dist/api/conversations.js +33 -0
- package/dist/api/conversations.js.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/execution/contract.d.ts +1 -1
- package/dist/execution/contract.d.ts.map +1 -1
- package/dist/execution/index.d.ts +2 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +136 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/types.d.ts +17 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/docs/contracts/capabilities-and-instructions.md +103 -0
- package/docs/contracts/input-and-execution.md +8 -3
- package/host/adapters/acp-capabilities.js +32 -0
- package/host/adapters/acp.js +8 -12
- package/host/adapters/claude.js +3 -2
- package/host/adapters/codex.js +19 -6
- package/host/adapters/cursor.js +9 -15
- package/host/adapters/input-capabilities.js +1 -0
- package/host/adapters/opencode.js +4 -3
- package/host/adapters/pi.js +3 -2
- package/host/adapters/prompt.js +2 -3
- package/host/auth.js +1 -1
- package/host/http.d.ts +4 -0
- package/host/http.js +43 -0
- package/host/index.d.ts +45 -3
- package/host/index.js +3 -0
- package/host/native-contract.js +26 -4
- package/host/native-runtime.js +8 -0
- package/host/platform-egress.js +46 -25
- package/host/title-generator.js +110 -0
- package/host/tooling/mcp-bundle.js +107 -56
- package/host/tooling/system-prompt.js +7 -4
- package/package.json +6 -1
- package/skills/chat/SKILL.md +300 -89
- 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
|
|
38
|
-
revision. `amalgm-tools` resolves that
|
|
39
|
-
|
|
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
|
+
}
|
package/host/adapters/acp.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
252
|
+
prompt: acpPromptParts(prompt),
|
|
257
253
|
}, 0).then(() => {
|
|
258
254
|
finished = true;
|
|
259
255
|
wakeLoop();
|
package/host/adapters/claude.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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),
|
package/host/adapters/codex.js
CHANGED
|
@@ -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 {
|
|
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) =>
|
|
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:
|
|
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
|
|
584
|
-
|
|
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')
|
package/host/adapters/cursor.js
CHANGED
|
@@ -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
|
-
//
|
|
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 {
|
|
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 =
|
|
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
|
|
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 {
|
|
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 =
|
|
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:
|
|
427
|
+
system: preparedSystemPrompt(contract) || undefined,
|
|
427
428
|
parts: openCodePromptParts(input),
|
|
428
429
|
},
|
|
429
430
|
});
|
package/host/adapters/pi.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
package/host/adapters/prompt.js
CHANGED
|
@@ -56,9 +56,8 @@ function textProjection(part) {
|
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function acpPromptParts(prompt
|
|
60
|
-
|
|
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 (
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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,
|
package/host/native-contract.js
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|