amalgm 0.1.80 → 0.1.81
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/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/agent-config/compiler/instructions.js +16 -3
- package/runtime/scripts/amalgm-mcp/agents/rest.js +2 -0
- package/runtime/scripts/chat-core/chat-payload.js +26 -3
- package/runtime/scripts/chat-core/contract.js +51 -1
- package/runtime/scripts/chat-core/engine.js +17 -4
- package/runtime/scripts/chat-core/tests/chat-payload.test.js +143 -0
- package/runtime/scripts/chat-core/tests/engine.test.js +73 -0
- package/runtime/scripts/chat-core/tests/system-prompt.test.js +67 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -1
package/package.json
CHANGED
|
@@ -21,7 +21,10 @@ function compileSkillsSection(skills) {
|
|
|
21
21
|
const enabled = (Array.isArray(skills) ? skills : []).filter((skill) => skill.enabled !== false);
|
|
22
22
|
if (enabled.length === 0) return '';
|
|
23
23
|
|
|
24
|
-
const lines = [
|
|
24
|
+
const lines = [
|
|
25
|
+
'## Skills to use',
|
|
26
|
+
'Use these skills when relevant to complete tasks.',
|
|
27
|
+
];
|
|
25
28
|
for (const skill of enabled) {
|
|
26
29
|
const title = skill.description
|
|
27
30
|
? `${skill.name || skill.id}: ${skill.description}`
|
|
@@ -41,7 +44,10 @@ function compileSubagentsSection(subagents) {
|
|
|
41
44
|
const enabled = (Array.isArray(subagents) ? subagents : []).filter((subagent) => subagent.enabled !== false);
|
|
42
45
|
if (enabled.length === 0) return '';
|
|
43
46
|
|
|
44
|
-
const lines = [
|
|
47
|
+
const lines = [
|
|
48
|
+
'## Subagents',
|
|
49
|
+
'The user wants you to use these subagents when relevant.',
|
|
50
|
+
];
|
|
45
51
|
for (const subagent of enabled) {
|
|
46
52
|
const name = subagent.name || subagent.agentId;
|
|
47
53
|
const title = subagent.description
|
|
@@ -54,8 +60,15 @@ function compileSubagentsSection(subagents) {
|
|
|
54
60
|
|
|
55
61
|
function compileAgentInstructions(config, options = {}) {
|
|
56
62
|
const normalized = normalizeAgentConfig(config);
|
|
63
|
+
const userInstructions = normalized.instructions
|
|
64
|
+
? [
|
|
65
|
+
'## Instructions',
|
|
66
|
+
"Here are instructions from the user on how they'd like you to operate.",
|
|
67
|
+
normalized.instructions,
|
|
68
|
+
].join('\n')
|
|
69
|
+
: '';
|
|
57
70
|
const chunks = [
|
|
58
|
-
|
|
71
|
+
userInstructions,
|
|
59
72
|
compileSkillsSection(normalized.skills),
|
|
60
73
|
compileSubagentsSection(normalized.subagents),
|
|
61
74
|
options.extraMarkdown,
|
|
@@ -72,6 +72,7 @@ async function handleGet(body, sendJson) {
|
|
|
72
72
|
|
|
73
73
|
async function handleCreate(body, sendJson) {
|
|
74
74
|
const {
|
|
75
|
+
id,
|
|
75
76
|
name,
|
|
76
77
|
description,
|
|
77
78
|
baseHarnessId,
|
|
@@ -99,6 +100,7 @@ async function handleCreate(body, sendJson) {
|
|
|
99
100
|
loadout: agentLoadout,
|
|
100
101
|
};
|
|
101
102
|
agent = createAgent({
|
|
103
|
+
...(typeof id === 'string' && id.trim() ? { id: id.trim() } : {}),
|
|
102
104
|
name: name.trim(),
|
|
103
105
|
description: description || '',
|
|
104
106
|
baseHarnessId,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { chatInputToLegacyFields, normalizeChatInput } = require('../../lib/chatInput');
|
|
4
4
|
const { getChatPayload } = require('../amalgm-mcp/lib/chat-payloads');
|
|
5
5
|
const { buildLocalMcpServerConfigs } = require('../amalgm-mcp/lib/mcp-resolver');
|
|
6
|
+
const { getAgentConfig } = require('../amalgm-mcp/agent-config/store');
|
|
6
7
|
const { normalizeLoadout } = require('../amalgm-mcp/agent-config/loadout');
|
|
7
8
|
|
|
8
9
|
function isObject(value) {
|
|
@@ -33,6 +34,10 @@ function applyPayloadRecord(body, record) {
|
|
|
33
34
|
const rawAgent = isObject(rawChatInput.agent) ? rawChatInput.agent : {};
|
|
34
35
|
const rawTools = isObject(rawChatInput.tools) ? rawChatInput.tools : {};
|
|
35
36
|
const rawExecution = isObject(rawChatInput.execution) ? rawChatInput.execution : {};
|
|
37
|
+
const agentConfigSnapshot = isObject(payload.agentConfigSnapshot) ? payload.agentConfigSnapshot : null;
|
|
38
|
+
const snapshotAgentConfig = isObject(agentConfigSnapshot?.config) ? agentConfigSnapshot.config : null;
|
|
39
|
+
const snapshotAgentId = nullableString(agentConfigSnapshot?.agentId);
|
|
40
|
+
const snapshotCompiledAgentInstructions = nullableString(agentConfigSnapshot?.compiledInstructions);
|
|
36
41
|
const modelSettings = isObject(payload.modelSettings)
|
|
37
42
|
? payload.modelSettings
|
|
38
43
|
: isObject(body.modelSettings)
|
|
@@ -55,11 +60,27 @@ function applyPayloadRecord(body, record) {
|
|
|
55
60
|
|| nullableString(body.authMethod);
|
|
56
61
|
const customAgentId =
|
|
57
62
|
nullableString(payload.customAgentId)
|
|
58
|
-
|| nullableString(rawAgent.customAgentId)
|
|
59
|
-
|
|
63
|
+
|| nullableString(rawAgent.customAgentId)
|
|
64
|
+
|| snapshotAgentId;
|
|
65
|
+
const agentConfigId =
|
|
66
|
+
snapshotAgentId
|
|
67
|
+
|| nullableString(payload.agentConfigId)
|
|
68
|
+
|| nullableString(body.agentConfigId)
|
|
69
|
+
|| customAgentId;
|
|
70
|
+
const agentConfig =
|
|
71
|
+
snapshotAgentConfig
|
|
72
|
+
|| (isObject(payload.agentConfig) && payload.agentConfig)
|
|
73
|
+
|| (isObject(body.agentConfig) && body.agentConfig)
|
|
74
|
+
|| (agentConfigId ? getAgentConfig(agentConfigId) : null);
|
|
75
|
+
const compiledAgentInstructions =
|
|
76
|
+
snapshotCompiledAgentInstructions
|
|
77
|
+
|| nullableString(payload.compiledAgentInstructions)
|
|
78
|
+
|| nullableString(body.compiledAgentInstructions);
|
|
79
|
+
const legacySystemPrompt =
|
|
60
80
|
nullableString(payload.systemPrompt)
|
|
61
81
|
|| nullableString(rawAgent.systemPrompt)
|
|
62
82
|
|| nullableString(body.systemPrompt);
|
|
83
|
+
const systemPrompt = agentConfig || compiledAgentInstructions ? null : legacySystemPrompt;
|
|
63
84
|
const cwd =
|
|
64
85
|
nullableString(body.cwd)
|
|
65
86
|
|| nullableString(rawExecution.cwd)
|
|
@@ -122,7 +143,9 @@ function applyPayloadRecord(body, record) {
|
|
|
122
143
|
mcpAppIds,
|
|
123
144
|
modelSettings,
|
|
124
145
|
agentId: customAgentId || harnessToAgent(harness),
|
|
125
|
-
...(
|
|
146
|
+
...(agentConfigId ? { agentConfigId } : {}),
|
|
147
|
+
...(agentConfig ? { agentConfig } : {}),
|
|
148
|
+
...(compiledAgentInstructions ? { compiledAgentInstructions } : {}),
|
|
126
149
|
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
127
150
|
...(modelSettings.fastMode === true ? { fastMode: true } : {}),
|
|
128
151
|
};
|
|
@@ -202,11 +202,22 @@ function frozenRuntimeFields(input) {
|
|
|
202
202
|
.sort((a, b) => `${a.name}\0${a.type}\0${a.url}`.localeCompare(`${b.name}\0${b.type}\0${b.url}`));
|
|
203
203
|
return {
|
|
204
204
|
sessionId: input.sessionId,
|
|
205
|
+
userId: input.userId,
|
|
205
206
|
computerId: input.computerId,
|
|
206
207
|
harness: input.harness,
|
|
207
208
|
authMethod: input.authMethod,
|
|
208
209
|
auth: authIdentity,
|
|
210
|
+
modelId: input.modelId,
|
|
211
|
+
usageModelId: input.usageModelId,
|
|
212
|
+
cliModel: input.cliModel,
|
|
213
|
+
reasoningEffort: input.reasoningEffort || null,
|
|
214
|
+
fastMode: input.fastMode === true,
|
|
215
|
+
contextWindow: input.contextWindow ?? null,
|
|
216
|
+
maxTokens: input.maxTokens ?? null,
|
|
217
|
+
cwd: input.cwd,
|
|
218
|
+
runtimeHomeLayout: input.runtimeHomeLayout,
|
|
209
219
|
systemPrompt: fingerprint(input.systemPrompt || ''),
|
|
220
|
+
compiledAgentInstructions: fingerprint(input.compiledAgentInstructions || ''),
|
|
210
221
|
agentConfig: fingerprint(JSON.stringify(input.agentConfig || null)),
|
|
211
222
|
origin: fingerprint(JSON.stringify(input.origin || null)),
|
|
212
223
|
mcpServers: fingerprint(JSON.stringify(mcpServers)),
|
|
@@ -233,13 +244,50 @@ function frozenContractChangedError(current, next) {
|
|
|
233
244
|
const changed = changedRuntimeFields(current, next);
|
|
234
245
|
const label = changed.length ? ` (${changed.join(', ')})` : '';
|
|
235
246
|
const err = new Error(
|
|
236
|
-
`Frozen session contract changed${label}; start a new session for auth/harness/computer/
|
|
247
|
+
`Frozen session contract changed${label}; start a new session for auth/harness/computer/instruction/MCP changes`,
|
|
237
248
|
);
|
|
238
249
|
err.code = 'FROZEN_SESSION_CONTRACT_CHANGED';
|
|
239
250
|
err.changedFields = changed;
|
|
240
251
|
return err;
|
|
241
252
|
}
|
|
242
253
|
|
|
254
|
+
function continueFrozenRuntimeContract(current, next) {
|
|
255
|
+
if (!current) return next;
|
|
256
|
+
const changed = changedRuntimeFields(current, next);
|
|
257
|
+
if (changed.length === 0) return next;
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
...next,
|
|
261
|
+
userId: current.userId,
|
|
262
|
+
computerId: current.computerId,
|
|
263
|
+
agentId: current.agentId,
|
|
264
|
+
agentConfigId: current.agentConfigId,
|
|
265
|
+
agentConfig: current.agentConfig,
|
|
266
|
+
runtimeHomeLayout: current.runtimeHomeLayout,
|
|
267
|
+
harness: current.harness,
|
|
268
|
+
authMethod: current.authMethod,
|
|
269
|
+
modelId: current.modelId,
|
|
270
|
+
usageModelId: current.usageModelId,
|
|
271
|
+
cliModel: current.cliModel,
|
|
272
|
+
reasoningEffort: current.reasoningEffort,
|
|
273
|
+
fastMode: current.fastMode,
|
|
274
|
+
contextWindow: current.contextWindow,
|
|
275
|
+
maxTokens: current.maxTokens,
|
|
276
|
+
cwd: current.cwd,
|
|
277
|
+
origin: current.origin,
|
|
278
|
+
constructs: current.constructs,
|
|
279
|
+
localBaseUrl: current.localBaseUrl,
|
|
280
|
+
mcpServers: current.mcpServers,
|
|
281
|
+
systemPrompt: current.systemPrompt,
|
|
282
|
+
compiledAgentInstructions: current.compiledAgentInstructions,
|
|
283
|
+
usageOwner: current.usageOwner,
|
|
284
|
+
auth: current.auth,
|
|
285
|
+
runtimeFields: current.runtimeFields,
|
|
286
|
+
runtimeKey: current.runtimeKey,
|
|
287
|
+
continuedFromChangedContract: changed,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
243
291
|
function expandHome(value) {
|
|
244
292
|
const raw = String(value || '').trim();
|
|
245
293
|
if (!raw) return '';
|
|
@@ -403,6 +451,7 @@ function createContract(payload, options = {}) {
|
|
|
403
451
|
localBaseUrl,
|
|
404
452
|
mcpServers: Array.isArray(payload.mcpServers) ? payload.mcpServers : [],
|
|
405
453
|
systemPrompt: payload.systemPrompt || '',
|
|
454
|
+
compiledAgentInstructions: payload.compiledAgentInstructions || '',
|
|
406
455
|
usageOwner: auth.method === 'amalgm' ? 'platform_proxy' : 'local_user',
|
|
407
456
|
auth,
|
|
408
457
|
};
|
|
@@ -424,6 +473,7 @@ module.exports = {
|
|
|
424
473
|
canonicalModel,
|
|
425
474
|
changedRuntimeFields,
|
|
426
475
|
cliModelFor,
|
|
476
|
+
continueFrozenRuntimeContract,
|
|
427
477
|
createContract,
|
|
428
478
|
frozenRuntimeFields,
|
|
429
479
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { PartAccumulator } = require('./parts');
|
|
4
|
-
const {
|
|
4
|
+
const { changedRuntimeFields, continueFrozenRuntimeContract, createContract } = require('./contract');
|
|
5
5
|
const { done, errorEvent } = require('./events');
|
|
6
6
|
const { normalizeInput } = require('./input');
|
|
7
7
|
const { frameFor, titleFrame, withSseId } = require('./sse');
|
|
@@ -80,9 +80,22 @@ class ChatCore {
|
|
|
80
80
|
contractFor(payload) {
|
|
81
81
|
const next = createContract(payload, this.options);
|
|
82
82
|
const existing = this.contracts.get(next.sessionId);
|
|
83
|
-
if (existing)
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
if (!existing) {
|
|
84
|
+
this.contracts.set(next.sessionId, next);
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const changed = changedRuntimeFields(existing, next);
|
|
89
|
+
const contract = changed.length > 0
|
|
90
|
+
? continueFrozenRuntimeContract(existing, next)
|
|
91
|
+
: next;
|
|
92
|
+
if (changed.length > 0) {
|
|
93
|
+
console.warn(
|
|
94
|
+
`[ChatCore] Ignoring changed frozen session contract fields for ${next.sessionId}: ${changed.join(', ')}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
this.contracts.set(contract.sessionId, contract);
|
|
98
|
+
return contract;
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
active(sessionId) {
|
|
@@ -12,6 +12,8 @@ process.env.AMALGM_COMPUTER_ID = 'machine-local';
|
|
|
12
12
|
|
|
13
13
|
const { closeLocalDb } = require('../../amalgm-mcp/state/db');
|
|
14
14
|
const { writeChatPayload } = require('../../amalgm-mcp/lib/chat-payloads');
|
|
15
|
+
const { createAgent } = require('../../amalgm-mcp/agents/store');
|
|
16
|
+
const { upsertAgentConfig } = require('../../amalgm-mcp/agent-config/store');
|
|
15
17
|
const { resolveMachineChatPayload } = require('../chat-payload');
|
|
16
18
|
|
|
17
19
|
test.after(() => {
|
|
@@ -63,6 +65,147 @@ test('chat-core resolves launch payload from the local machine cache', async ()
|
|
|
63
65
|
assert.equal(resolved.cliModel, undefined);
|
|
64
66
|
});
|
|
65
67
|
|
|
68
|
+
test('chat-core hydrates latest agent config when resolving local machine payloads', async () => {
|
|
69
|
+
createAgent({
|
|
70
|
+
id: 'custom-agent-test',
|
|
71
|
+
name: 'Custom Agent Test',
|
|
72
|
+
baseHarnessId: 'codex',
|
|
73
|
+
baseModelId: 'openai/gpt-5.5',
|
|
74
|
+
loadout: { toolIds: ['tool.search'] },
|
|
75
|
+
});
|
|
76
|
+
upsertAgentConfig('custom-agent-test', {
|
|
77
|
+
instructions: 'test code = 1234',
|
|
78
|
+
skills: [{
|
|
79
|
+
id: 'skill-test',
|
|
80
|
+
name: 'Test skill',
|
|
81
|
+
content: 'Use the configured skill.',
|
|
82
|
+
enabled: true,
|
|
83
|
+
}],
|
|
84
|
+
subagents: [{
|
|
85
|
+
id: 'subagent-reviewer',
|
|
86
|
+
agentId: 'reviewer',
|
|
87
|
+
name: 'Reviewer',
|
|
88
|
+
description: 'Use for review tasks.',
|
|
89
|
+
enabled: true,
|
|
90
|
+
}],
|
|
91
|
+
loadout: { toolIds: ['tool.search'] },
|
|
92
|
+
hooks: [],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
writeChatPayload({
|
|
96
|
+
id: 'chat-view:agent-config',
|
|
97
|
+
revision: 'rev-agent-config',
|
|
98
|
+
payload: {
|
|
99
|
+
machineId: 'machine-local',
|
|
100
|
+
harness: 'codex',
|
|
101
|
+
resolvedHarnessId: 'codex',
|
|
102
|
+
modelId: 'openai/gpt-5.5',
|
|
103
|
+
modelSettings: {},
|
|
104
|
+
authMethod: 'amalgm',
|
|
105
|
+
cwd: '/tmp/amalgm-project',
|
|
106
|
+
customAgentId: 'custom-agent-test',
|
|
107
|
+
systemPrompt: 'stale legacy prompt',
|
|
108
|
+
mcpAppIds: [],
|
|
109
|
+
loadout: { toolIds: ['tool.search'] },
|
|
110
|
+
pendingWorktree: false,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const resolved = await resolveMachineChatPayload({
|
|
115
|
+
chatPayloadId: 'chat-view:agent-config',
|
|
116
|
+
chatPayloadRevision: 'rev-agent-config',
|
|
117
|
+
prompt: 'hello',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
assert.equal(resolved.agentConfigId, 'custom-agent-test');
|
|
121
|
+
assert.equal(resolved.agentConfig.instructions, 'test code = 1234');
|
|
122
|
+
assert.equal(resolved.agentConfig.skills[0].content, 'Use the configured skill.');
|
|
123
|
+
assert.equal(resolved.agentConfig.subagents[0].agentId, 'reviewer');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('chat-core resolves agent config snapshot from the machine payload before live config', async () => {
|
|
127
|
+
createAgent({
|
|
128
|
+
id: 'custom-agent-snapshot',
|
|
129
|
+
name: 'Custom Agent Snapshot',
|
|
130
|
+
baseHarnessId: 'codex',
|
|
131
|
+
baseModelId: 'openai/gpt-5.5',
|
|
132
|
+
loadout: { toolIds: ['tool.live'] },
|
|
133
|
+
});
|
|
134
|
+
upsertAgentConfig('custom-agent-snapshot', {
|
|
135
|
+
instructions: 'live config should not win',
|
|
136
|
+
skills: [],
|
|
137
|
+
subagents: [],
|
|
138
|
+
loadout: { toolIds: ['tool.live'] },
|
|
139
|
+
hooks: [],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const snapshotConfig = {
|
|
143
|
+
instructions: 'payload snapshot wins',
|
|
144
|
+
skills: [{
|
|
145
|
+
id: 'skill-snapshot',
|
|
146
|
+
name: 'Snapshot skill',
|
|
147
|
+
content: 'Use the payload snapshot skill.',
|
|
148
|
+
enabled: true,
|
|
149
|
+
}],
|
|
150
|
+
subagents: [{
|
|
151
|
+
id: 'subagent-snapshot-reviewer',
|
|
152
|
+
agentId: 'snapshot-reviewer',
|
|
153
|
+
name: 'Snapshot Reviewer',
|
|
154
|
+
description: 'Review from the payload snapshot.',
|
|
155
|
+
enabled: true,
|
|
156
|
+
}],
|
|
157
|
+
loadout: { toolIds: ['tool.snapshot'] },
|
|
158
|
+
hooks: [],
|
|
159
|
+
};
|
|
160
|
+
const compiledInstructions = [
|
|
161
|
+
'## Instructions',
|
|
162
|
+
"Here are instructions from the user on how they'd like you to operate.",
|
|
163
|
+
'payload snapshot wins',
|
|
164
|
+
'',
|
|
165
|
+
'## Skills to use',
|
|
166
|
+
'Use these skills when relevant to complete tasks.',
|
|
167
|
+
'- Snapshot skill',
|
|
168
|
+
' Use the payload snapshot skill.',
|
|
169
|
+
].join('\n');
|
|
170
|
+
|
|
171
|
+
writeChatPayload({
|
|
172
|
+
id: 'chat-view:agent-config-snapshot',
|
|
173
|
+
revision: 'rev-agent-config-snapshot',
|
|
174
|
+
payload: {
|
|
175
|
+
machineId: 'machine-local',
|
|
176
|
+
harness: 'codex',
|
|
177
|
+
resolvedHarnessId: 'codex',
|
|
178
|
+
modelId: 'openai/gpt-5.5',
|
|
179
|
+
modelSettings: {},
|
|
180
|
+
authMethod: 'amalgm',
|
|
181
|
+
cwd: '/tmp/amalgm-project',
|
|
182
|
+
customAgentId: 'custom-agent-snapshot',
|
|
183
|
+
systemPrompt: 'stale legacy prompt',
|
|
184
|
+
agentConfigSnapshot: {
|
|
185
|
+
agentId: 'custom-agent-snapshot',
|
|
186
|
+
revision: 'rev-snapshot',
|
|
187
|
+
config: snapshotConfig,
|
|
188
|
+
compiledInstructions,
|
|
189
|
+
},
|
|
190
|
+
mcpAppIds: [],
|
|
191
|
+
loadout: { toolIds: ['tool.snapshot'] },
|
|
192
|
+
pendingWorktree: false,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const resolved = await resolveMachineChatPayload({
|
|
197
|
+
chatPayloadId: 'chat-view:agent-config-snapshot',
|
|
198
|
+
chatPayloadRevision: 'rev-agent-config-snapshot',
|
|
199
|
+
prompt: 'hello',
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
assert.equal(resolved.agentConfigId, 'custom-agent-snapshot');
|
|
203
|
+
assert.equal(resolved.agentConfig.instructions, 'payload snapshot wins');
|
|
204
|
+
assert.equal(resolved.compiledAgentInstructions, compiledInstructions);
|
|
205
|
+
assert.equal(resolved.systemPrompt, null);
|
|
206
|
+
assert.deepEqual(resolved.chatInput.tools.toolIds, ['tool.snapshot']);
|
|
207
|
+
});
|
|
208
|
+
|
|
66
209
|
test('chat-core rejects stale chat payload revisions', async () => {
|
|
67
210
|
await assert.rejects(
|
|
68
211
|
() => resolveMachineChatPayload({
|
|
@@ -53,6 +53,79 @@ function runTurnWithFrames(core, turnPayload) {
|
|
|
53
53
|
return { run, frames };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function agentConfig(instructions) {
|
|
57
|
+
return {
|
|
58
|
+
version: 1,
|
|
59
|
+
agentId: 'codex',
|
|
60
|
+
runtimeHomeLayout: 'legacy',
|
|
61
|
+
instructions,
|
|
62
|
+
skills: [],
|
|
63
|
+
loadout: { toolIds: [] },
|
|
64
|
+
hooks: [],
|
|
65
|
+
subagents: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test('existing sessions keep their original frozen contract when payload config changes', async () => {
|
|
70
|
+
const contracts = [];
|
|
71
|
+
const db = {
|
|
72
|
+
saveUserMessage: async () => true,
|
|
73
|
+
ensureAssistantMessage: async () => true,
|
|
74
|
+
saveAssistantMessage: async () => true,
|
|
75
|
+
mergeSessionMetadata: async () => {},
|
|
76
|
+
logUsage: async () => {},
|
|
77
|
+
};
|
|
78
|
+
const fakeRuntime = {
|
|
79
|
+
get: () => ({ warm: true }),
|
|
80
|
+
stop: async () => {},
|
|
81
|
+
destroy: async () => true,
|
|
82
|
+
async *prompt(contract) {
|
|
83
|
+
contracts.push(contract);
|
|
84
|
+
yield textDelta('hi');
|
|
85
|
+
yield done({ providerSessionId: 'provider-test' });
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const core = new ChatCore({
|
|
89
|
+
db,
|
|
90
|
+
runtime: fakeRuntime,
|
|
91
|
+
turns: new TurnStore(),
|
|
92
|
+
options: {
|
|
93
|
+
beginTurn: async () => {},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await core.runTurn(payload({
|
|
98
|
+
agentId: 'codex',
|
|
99
|
+
modelId: 'openai/gpt-5.5',
|
|
100
|
+
assistantMessageId: 'assistant-1',
|
|
101
|
+
userMessageId: 'user-1',
|
|
102
|
+
agentConfig: agentConfig('old instructions'),
|
|
103
|
+
compiledAgentInstructions: 'compiled old instructions',
|
|
104
|
+
mcpServers: [],
|
|
105
|
+
}));
|
|
106
|
+
await core.runTurn(payload({
|
|
107
|
+
agentId: 'codex',
|
|
108
|
+
modelId: 'openai/gpt-4.1',
|
|
109
|
+
assistantMessageId: 'assistant-2',
|
|
110
|
+
userMessageId: 'user-2',
|
|
111
|
+
agentConfig: agentConfig('new instructions'),
|
|
112
|
+
compiledAgentInstructions: 'compiled new instructions',
|
|
113
|
+
mcpServers: [{ name: 'new-tools', type: 'http', url: 'https://example.test/mcp' }],
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
assert.equal(contracts.length, 2);
|
|
117
|
+
assert.equal(contracts[1].assistantMessageId, 'assistant-2');
|
|
118
|
+
assert.equal(contracts[1].userMessageId, 'user-2');
|
|
119
|
+
assert.equal(contracts[1].modelId, 'openai/gpt-5.5');
|
|
120
|
+
assert.equal(contracts[1].agentConfig.instructions, 'old instructions');
|
|
121
|
+
assert.equal(contracts[1].compiledAgentInstructions, 'compiled old instructions');
|
|
122
|
+
assert.deepEqual(contracts[1].mcpServers, []);
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
contracts[1].continuedFromChangedContract.sort(),
|
|
125
|
+
['agentConfig', 'cliModel', 'compiledAgentInstructions', 'mcpServers', 'modelId', 'usageModelId'].sort(),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
56
129
|
test('title generation waits for user save and does not block a fast turn', async () => {
|
|
57
130
|
let resolveUserSave;
|
|
58
131
|
let resolveTitle;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
7
|
+
|
|
8
|
+
test('composeSystemPrompt renders agent config instructions over legacy systemPrompt', () => {
|
|
9
|
+
const prompt = composeSystemPrompt({
|
|
10
|
+
systemPrompt: 'legacy stale prompt',
|
|
11
|
+
agentConfig: {
|
|
12
|
+
agentId: 'agent-codex',
|
|
13
|
+
instructions: 'test code = 1234',
|
|
14
|
+
skills: [{
|
|
15
|
+
id: 'skill-debug',
|
|
16
|
+
name: 'Debug skill',
|
|
17
|
+
content: 'Use the debug checklist.',
|
|
18
|
+
enabled: true,
|
|
19
|
+
}],
|
|
20
|
+
subagents: [{
|
|
21
|
+
id: 'agent-reviewer',
|
|
22
|
+
agentId: 'reviewer',
|
|
23
|
+
name: 'Reviewer',
|
|
24
|
+
description: 'Review risky changes.',
|
|
25
|
+
enabled: true,
|
|
26
|
+
}],
|
|
27
|
+
loadout: { toolIds: [] },
|
|
28
|
+
hooks: [],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.match(prompt, /test code = 1234/);
|
|
33
|
+
assert.match(prompt, /## Instructions/);
|
|
34
|
+
assert.match(prompt, /Here are instructions from the user/);
|
|
35
|
+
assert.match(prompt, /## Skills to use/);
|
|
36
|
+
assert.match(prompt, /Use the debug checklist/);
|
|
37
|
+
assert.match(prompt, /## Subagents/);
|
|
38
|
+
assert.match(prompt, /Reviewer: Review risky changes\./);
|
|
39
|
+
assert.doesNotMatch(prompt, /legacy stale prompt/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('composeSystemPrompt prefers compiled payload instructions over agent config recompute', () => {
|
|
43
|
+
const prompt = composeSystemPrompt({
|
|
44
|
+
systemPrompt: 'legacy stale prompt',
|
|
45
|
+
compiledAgentInstructions: 'compiled payload text wins',
|
|
46
|
+
agentConfig: {
|
|
47
|
+
agentId: 'agent-codex',
|
|
48
|
+
instructions: 'live agent config should not win',
|
|
49
|
+
skills: [],
|
|
50
|
+
subagents: [],
|
|
51
|
+
loadout: { toolIds: [] },
|
|
52
|
+
hooks: [],
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.match(prompt, /compiled payload text wins/);
|
|
57
|
+
assert.doesNotMatch(prompt, /live agent config should not win/);
|
|
58
|
+
assert.doesNotMatch(prompt, /legacy stale prompt/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('composeSystemPrompt falls back to legacy systemPrompt without agent config instructions', () => {
|
|
62
|
+
const prompt = composeSystemPrompt({
|
|
63
|
+
systemPrompt: 'legacy fallback prompt',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.match(prompt, /legacy fallback prompt/);
|
|
67
|
+
});
|
|
@@ -12,8 +12,10 @@ function trimBlock(value) {
|
|
|
12
12
|
function composeSystemPrompt(contract) {
|
|
13
13
|
const parts = [];
|
|
14
14
|
const platform = trimBlock(PLATFORM_CONTEXT);
|
|
15
|
-
const
|
|
15
|
+
const compiledAgentInstructions =
|
|
16
|
+
trimBlock(contract?.compiledAgentInstructions)
|
|
16
17
|
|| trimBlock(renderAgentInstructions(contract?.agentConfig, { includeAgentToAgentPrompt: false }));
|
|
18
|
+
const custom = compiledAgentInstructions || trimBlock(contract?.systemPrompt);
|
|
17
19
|
const instructions = contract?.providerSessionId ? '' : trimBlock(instructionsContextBlock(contract));
|
|
18
20
|
const active = contract?.providerSessionId ? '' : trimBlock(activeMemoryContextBlock(contract));
|
|
19
21
|
const passive = contract?.providerSessionId ? '' : trimBlock(passiveMemoryContextBlock());
|