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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.80",
3
+ "version": "0.1.81",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -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 = ['## Skills you should use'];
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 = ['## Agents you should use'];
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
- normalized.instructions,
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
- const systemPrompt =
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
- ...(customAgentId ? { agentConfigId: customAgentId } : {}),
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/system-prompt/MCP changes`,
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 { assertSameContract, createContract } = require('./contract');
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) assertSameContract(existing, next);
84
- this.contracts.set(next.sessionId, next);
85
- return next;
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 custom = trimBlock(contract?.systemPrompt)
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());