diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 66b09801..e248a7ce 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -107,6 +107,8 @@ export interface AgentMeta { readonly type: AgentType; readonly parentAgentId?: string | null; readonly swarmItem?: string; + /** The `roleAdditional` value that was used to render this agent's system prompt. */ + roleAdditional?: string; } interface ResumedAgent { @@ -645,6 +647,7 @@ export class Session { type, parentAgentId, swarmItem: options.swarmItem, + roleAdditional: this.roleAdditional, }; void this.writeMetadata(); } @@ -1040,6 +1043,16 @@ export class Session { ); const result = await agent.resume(); this.restoreAgentProfileHandle(agent, meta, parent?.agent); + // If the session was resumed with a different `roleAdditional` than the + // one used to render this agent's persisted system prompt, re-render the + // prompt using the restored profile and the fresh context. This keeps the + // persisted profile in sync with the new wrapper/standing instructions + // without refreshing AGENTS.md/cwd on every resume. + if (this.roleAdditional !== meta.roleAdditional) { + await agent.refreshSystemPrompt(); + meta.roleAdditional = this.roleAdditional; + void this.writeMetadata(); + } this.agents.set(id, agent); return { agent, warning: parent?.warning ?? result.warning }; } catch (error) { diff --git a/packages/agent-core/test/session/resume-roleadditional.test.ts b/packages/agent-core/test/session/resume-roleadditional.test.ts new file mode 100644 index 00000000..6822b8fc --- /dev/null +++ b/packages/agent-core/test/session/resume-roleadditional.test.ts @@ -0,0 +1,219 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'pathe'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { DEFAULT_AGENT_PROFILES } from '../../src/profile'; +import type { ResolvedAgentProfile } from '../../src/profile'; +import type { SDKSessionRPC } from '../../src/rpc'; +import { Session } from '../../src/session'; +import { ProviderManager } from '../../src/session/provider-manager'; +import { testKaos } from '../fixtures/test-kaos'; + +const MOCK_PROVIDER = { + type: 'kimi', + apiKey: 'test-key', + model: 'mock-model', +} as const; + +const tempDirs: string[] = []; + +afterEach(async () => { + for (const dir of tempDirs.splice(0)) { + await rm(dir, { recursive: true, force: true }); + } +}); + +async function makeTempDir(): Promise { + const dir = await mkdtemp(join(tmpdir(), 'kimi-core-resume-roleadditional-')); + tempDirs.push(dir); + return dir; +} + +function testProviderManager(): ProviderManager { + return new ProviderManager({ + config: { + providers: { + test: { + type: MOCK_PROVIDER.type, + apiKey: MOCK_PROVIDER.apiKey, + }, + }, + models: { + [MOCK_PROVIDER.model]: { + provider: 'test', + model: MOCK_PROVIDER.model, + maxContextSize: 1_000_000, + }, + }, + }, + }); +} + +function createSessionRpc(): SDKSessionRPC { + return { + emitEvent: vi.fn(async () => {}), + requestApproval: vi.fn(async () => ({ decision: 'cancelled' })), + requestQuestion: vi.fn(async () => null), + toolCall: vi.fn(async () => ({ + output: 'custom tools are not supported in this test', + isError: true, + })), + } as SDKSessionRPC; +} + +function customRoleAdditionalProfile(roleAdditionalMarker: string): ResolvedAgentProfile { + return { + name: 'role-additional-test', + systemPrompt: ({ roleAdditional }) => + `system prompt marker: ${roleAdditionalMarker}; roleAdditional: ${roleAdditional ?? ''}`, + tools: [], + }; +} + +describe('Session.resume with roleAdditional', () => { + it('re-renders the system prompt with the fresh roleAdditional after replay', async () => { + const sessionDir = await makeTempDir(); + const workDir = await makeTempDir(); + + // Launch 1: create and persist a session with roleAdditional "W1". + const session1 = new Session({ + id: 'test-resume-roleadditional', + kaos: testKaos.withCwd(workDir), + homedir: sessionDir, + rpc: createSessionRpc(), + providerManager: testProviderManager(), + roleAdditional: 'role-additional-marker-W1', + }); + + const { agent: mainAgent1 } = await session1.createAgent( + { type: 'main' }, + { profile: DEFAULT_AGENT_PROFILES['agent'] }, + ); + + expect(mainAgent1.config.systemPrompt).toContain('role-additional-marker-W1'); + expect(mainAgent1.config.systemPrompt).not.toContain('role-additional-marker-W2'); + + await session1.close(); + + // Launch 2: resume the same session with roleAdditional "W2". + const session2 = new Session({ + id: 'test-resume-roleadditional', + kaos: testKaos.withCwd(workDir), + homedir: sessionDir, + rpc: createSessionRpc(), + providerManager: testProviderManager(), + roleAdditional: 'role-additional-marker-W2', + }); + + try { + await session2.resume(); + const mainAgent2 = await session2.ensureAgentResumed('main'); + + // The resumed agent must use the fresh W2 roleAdditional, not the stale W1. + expect(mainAgent2.config.systemPrompt).toContain('role-additional-marker-W2'); + expect(mainAgent2.config.systemPrompt).not.toContain('role-additional-marker-W1'); + } finally { + await session2.close(); + } + }); + + it('does not re-render the system prompt when roleAdditional is unchanged', async () => { + const sessionDir = await makeTempDir(); + const workDir = await makeTempDir(); + await mkdir(join(workDir, '.git')); + await writeFile(join(workDir, 'AGENTS.md'), 'initial resume instructions', 'utf-8'); + + const session1 = new Session({ + id: 'test-resume-roleadditional-unchanged', + kaos: testKaos.withCwd(workDir), + persistenceKaos: testKaos.withCwd(workDir), + homedir: sessionDir, + rpc: createSessionRpc(), + providerManager: testProviderManager(), + roleAdditional: 'role-additional-marker-W1', + }); + + const { agent: mainAgent1 } = await session1.createAgent( + { type: 'main' }, + { profile: DEFAULT_AGENT_PROFILES['agent'] }, + ); + + expect(mainAgent1.config.systemPrompt).toContain('initial resume instructions'); + await session1.closeForReload(); + + // Mutate AGENTS.md while the session is closed. + await writeFile(join(workDir, 'AGENTS.md'), 'updated resume instructions', 'utf-8'); + + // Resume with the same roleAdditional: the persisted prompt must stay intact. + const session2 = new Session({ + id: 'test-resume-roleadditional-unchanged', + kaos: testKaos.withCwd(workDir), + persistenceKaos: testKaos.withCwd(workDir), + homedir: sessionDir, + rpc: createSessionRpc(), + providerManager: testProviderManager(), + roleAdditional: 'role-additional-marker-W1', + }); + + try { + await session2.resume(); + const mainAgent2 = await session2.ensureAgentResumed('main'); + + expect(mainAgent2.config.systemPrompt).toContain('initial resume instructions'); + expect(mainAgent2.config.systemPrompt).not.toContain('updated resume instructions'); + + // An explicit refresh still picks up the new AGENTS.md. + await mainAgent2.refreshSystemPrompt(); + expect(mainAgent2.config.systemPrompt).toContain('updated resume instructions'); + expect(mainAgent2.config.systemPrompt).not.toContain('initial resume instructions'); + } finally { + await session2.close(); + } + }); + + it('leaves an unresolved custom profile unchanged when roleAdditional changes', async () => { + const sessionDir = await makeTempDir(); + const workDir = await makeTempDir(); + + const session1 = new Session({ + id: 'test-resume-roleadditional-custom', + kaos: testKaos.withCwd(workDir), + homedir: sessionDir, + rpc: createSessionRpc(), + providerManager: testProviderManager(), + roleAdditional: 'role-additional-marker-W1', + }); + + const { agent: mainAgent1 } = await session1.createAgent( + { type: 'main' }, + { profile: customRoleAdditionalProfile('W1') }, + ); + + expect(mainAgent1.config.systemPrompt).toContain('roleAdditional: role-additional-marker-W1'); + await session1.close(); + + // Resume with a different roleAdditional but a custom profile that cannot be + // resolved from DEFAULT_AGENT_PROFILES. The persisted prompt must remain + // unchanged and the resume must not throw. + const session2 = new Session({ + id: 'test-resume-roleadditional-custom', + kaos: testKaos.withCwd(workDir), + homedir: sessionDir, + rpc: createSessionRpc(), + providerManager: testProviderManager(), + roleAdditional: 'role-additional-marker-W2', + }); + + try { + await session2.resume(); + const mainAgent2 = await session2.ensureAgentResumed('main'); + + expect(mainAgent2.config.systemPrompt).toContain('roleAdditional: role-additional-marker-W1'); + expect(mainAgent2.config.systemPrompt).not.toContain('roleAdditional: role-additional-marker-W2'); + } finally { + await session2.close(); + } + }); +});