From ab96cf368446cfe9457985425b7fc07c01064832 Mon Sep 17 00:00:00 2001 From: Kai Date: Sun, 12 Jul 2026 13:46:09 +0800 Subject: [PATCH 1/4] feat(botiverse): thread roleAdditional standing-prompt through 0.23.5 session API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.23.5 has the ROLE_ADDITIONAL render half (profile/resolve.ts + system.md {{ROLE_ADDITIONAL}}) but not input-threading. Re-wire our standing-prompt plumbing following the additionalDirs sibling: node-sdk CreateSessionOptions/ ResumeSessionInput → CreateSessionPayload/ResumeSessionPayload → core-impl new Session → SessionOptions → instantiateAgent → AgentOptions → updateSystemPromptFromProfile. Verified: renders end-to-end + orphan-fix (#768) intact + LocalKaos. Signed-off-by: Kai --- packages/agent-core/src/agent/index.ts | 5 +++++ packages/agent-core/src/rpc/core-api.ts | 4 ++++ packages/agent-core/src/rpc/core-impl.ts | 2 ++ packages/agent-core/src/session/index.ts | 5 +++++ packages/node-sdk/src/types.ts | 8 ++++++++ 5 files changed, 24 insertions(+) diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts index cc57dc146..49a3dec2a 100644 --- a/packages/agent-core/src/agent/index.ts +++ b/packages/agent-core/src/agent/index.ts @@ -101,6 +101,8 @@ export interface AgentOptions { readonly imageLimits?: ImageLimits; readonly replay?: ReplayBuilderOptions; readonly additionalDirs?: readonly string[]; + /** Botiverse mirror: standing-prompt (Raft role/context) → ROLE_ADDITIONAL. */ + readonly roleAdditional?: string; readonly systemPromptContextProvider?: (() => Promise) | undefined; } @@ -160,6 +162,7 @@ export class Agent { printDrainAgentTasksOnStop = false; private additionalDirs: readonly string[]; + private roleAdditional?: string; private activeProfile?: ResolvedAgentProfile; private brandHome?: string; private readonly emittedThinkingEffortWarnings = new Set(); @@ -193,6 +196,7 @@ export class Agent { this.experimentalFlags = options.experimentalFlags ?? new FlagResolver(); this.imageLimits = options.imageLimits ?? new ImageLimits(); this.additionalDirs = normalizeAdditionalDirs(options.additionalDirs ?? []); + this.roleAdditional = options.roleAdditional; this.systemPromptContextProvider = options.systemPromptContextProvider; this.llmRequestLogger = new LlmRequestLogger(this.log); @@ -475,6 +479,7 @@ export class Agent { cwdListing: context?.cwdListing, agentsMd: context?.agentsMd, additionalDirsInfo: context?.additionalDirsInfo, + roleAdditional: this.roleAdditional, }); this.config.update({ profileName: profile.name, systemPrompt }); } diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts index ec6d825f5..62ea4ea1e 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -61,6 +61,8 @@ export interface CreateSessionPayload { readonly metadata?: JsonObject | undefined; readonly mcpServers?: Readonly>; readonly additionalDirs?: readonly string[]; + /** Botiverse mirror: standing-prompt (Raft role/context) → ROLE_ADDITIONAL. */ + readonly roleAdditional?: string; readonly client?: ClientTelemetryInfo | undefined; readonly drainAgentTasksOnStop?: boolean; } @@ -83,6 +85,8 @@ export interface ResumeSessionPayload { readonly additionalDirs?: readonly string[]; /** Include persisted subagent states in the returned replay snapshot. */ readonly includeSubagents?: boolean; + /** Botiverse mirror: standing-prompt preserved across resume. */ + readonly roleAdditional?: string; } export interface ReloadSessionPayload { diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index 09cf2965c..4ff2affb5 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -379,6 +379,7 @@ export class KimiCore implements PromisableMethods { pluginCommands, appVersion: this.appVersion, additionalDirs, + roleAdditional: options.roleAdditional, drainAgentTasksOnStop: options.drainAgentTasksOnStop, }); try { @@ -525,6 +526,7 @@ export class KimiCore implements PromisableMethods { pluginCommands, appVersion: this.appVersion, additionalDirs, + roleAdditional: input.roleAdditional, }); let warning: string | undefined; try { diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 0f7ec8d23..66b098012 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -81,6 +81,8 @@ export interface SessionOptions { /** Owner-scoped [image] limits, threaded from the owning core into every agent. */ readonly imageLimits?: ImageLimits; readonly additionalDirs?: readonly string[]; + /** Botiverse mirror: standing-prompt (Raft role/context), session-level → every agent's ROLE_ADDITIONAL. */ + readonly roleAdditional?: string; /** * Print-mode (`kimi -p`) only: hold the main turn open while background * subagents (`kind === 'agent'`) are still running, idle-waiting until they @@ -183,6 +185,7 @@ export class Session { private persistenceKaos: Kaos; private additionalDirs: readonly string[]; private sessionAdditionalDirs: readonly string[] = []; + private roleAdditional?: string; private readonly pluginCommands: readonly PluginCommandDef[]; private agentIdCounter = 0; private readonly skillsReady: Promise; @@ -224,6 +227,7 @@ export class Session { this.toolKaos = options.kaos; this.persistenceKaos = options.persistenceKaos ?? options.kaos; this.additionalDirs = normalizeAdditionalDirs(options.additionalDirs ?? []); + this.roleAdditional = options.roleAdditional; this.pluginCommands = options.pluginCommands ?? []; this.skills = new SessionSkillRegistry({ sessionId: options.id, @@ -948,6 +952,7 @@ export class Session { experimentalFlags: this.experimentalFlags, imageLimits: this.imageLimits, additionalDirs: parentAgent?.getAdditionalDirs() ?? this.additionalDirs, + roleAdditional: this.roleAdditional, systemPromptContextProvider: () => prepareSystemPromptContext( this.systemContextKaos(agent.kaos.getcwd()), diff --git a/packages/node-sdk/src/types.ts b/packages/node-sdk/src/types.ts index c72419e3f..f63373b13 100644 --- a/packages/node-sdk/src/types.ts +++ b/packages/node-sdk/src/types.ts @@ -107,6 +107,12 @@ export interface CreateSessionOptions { readonly kaos?: Kaos | undefined; readonly persistenceKaos?: Kaos | undefined; readonly additionalDirs?: readonly string[]; + /** + * Botiverse mirror: extra standing-prompt text (Raft agent role/context) + * threaded into the resolved profile's ROLE_ADDITIONAL slot. Flows via the + * createSession spread → session config → Agent → updateSystemPromptFromProfile. + */ + readonly roleAdditional?: string; readonly sessionStartedProperties?: TelemetryProperties; /** * Print-mode (`kimi -p`) only: when the main agent ends a turn while @@ -130,6 +136,8 @@ export interface ResumeSessionInput { readonly additionalDirs?: readonly string[]; /** Include persisted subagent states in the returned replay snapshot. */ readonly includeSubagents?: boolean; + /** Botiverse mirror: standing-prompt text preserved across resume. */ + readonly roleAdditional?: string; readonly sessionStartedProperties?: TelemetryProperties; } From e554a0141329664e62a188494c352031428ba527 Mon Sep 17 00:00:00 2001 From: XX Date: Fri, 17 Jul 2026 01:43:46 +0800 Subject: [PATCH 2/4] fix(node-sdk): bundle protocol declarations Signed-off-by: XX --- packages/node-sdk/api-extractor.json | 3 ++- packages/node-sdk/scripts/build-dts.mjs | 22 ++++++++++++++++++++-- packages/node-sdk/tsconfig.dts.json | 3 ++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/node-sdk/api-extractor.json b/packages/node-sdk/api-extractor.json index fe6249071..7e9a0b9e5 100644 --- a/packages/node-sdk/api-extractor.json +++ b/packages/node-sdk/api-extractor.json @@ -5,7 +5,8 @@ "@moonshot-ai/agent-core", "@moonshot-ai/kaos", "@moonshot-ai/kimi-code-oauth", - "@moonshot-ai/kosong" + "@moonshot-ai/kosong", + "@moonshot-ai/protocol" ], "compiler": { "tsconfigFilePath": "/tsconfig.api-extractor.json", diff --git a/packages/node-sdk/scripts/build-dts.mjs b/packages/node-sdk/scripts/build-dts.mjs index b25ac114b..ca5beac2a 100644 --- a/packages/node-sdk/scripts/build-dts.mjs +++ b/packages/node-sdk/scripts/build-dts.mjs @@ -12,12 +12,13 @@ const providerClientShimPath = path.join(dtsRoot, 'provider-clients.d.ts'); const tscBinPath = packageBinPath('typescript', 'bin/tsc'); const apiExtractorBinPath = packageBinPath('@microsoft/api-extractor', 'bin/api-extractor'); -const packageDirs = new Set(['agent-core', 'kaos', 'kosong', 'node-sdk', 'oauth']); +const packageDirs = new Set(['agent-core', 'kaos', 'kosong', 'node-sdk', 'oauth', 'protocol']); const workspacePackages = new Map([ ['@moonshot-ai/agent-core', 'agent-core'], ['@moonshot-ai/kaos', 'kaos'], ['@moonshot-ai/kimi-code-oauth', 'oauth'], ['@moonshot-ai/kosong', 'kosong'], + ['@moonshot-ai/protocol', 'protocol'], ]); try { @@ -105,7 +106,7 @@ async function rewriteWorkspaceSpecifiers() { `import { GoogleGenAI as GenAIClient } from '${providerClientSpecifier}';`, ); const updated = providerClientText.replaceAll( - /(["'])(#\/[^"']+|@moonshot-ai\/(?:agent-core|kaos|kimi-code-oauth|kosong)(?:\/[^"']+)?)\1/g, + /(["'])(#\/[^"']+|@moonshot-ai\/(?:agent-core|kaos|kimi-code-oauth|kosong|protocol)(?:\/[^"']+)?)\1/g, (_match, quote, specifier) => { const resolved = resolveSpecifier({ currentFile: file, @@ -123,6 +124,23 @@ async function rewriteWorkspaceSpecifiers() { } }), ); + + await assertNoWorkspaceSpecifiers(files); +} + +async function assertNoWorkspaceSpecifiers(files) { + const unresolved = []; + + for (const file of files) { + const text = await readFile(file, 'utf8'); + if (/from ["']@moonshot-ai\//.test(text)) { + unresolved.push(path.relative(dtsRoot, file)); + } + } + + if (unresolved.length > 0) { + throw new Error(`Unresolved workspace specifiers in emitted declarations: ${unresolved.join(', ')}`); + } } async function findDtsFiles(dir) { diff --git a/packages/node-sdk/tsconfig.dts.json b/packages/node-sdk/tsconfig.dts.json index c1210f3de..7b0dd6a68 100644 --- a/packages/node-sdk/tsconfig.dts.json +++ b/packages/node-sdk/tsconfig.dts.json @@ -15,7 +15,8 @@ "../agent-core/src/prompt-modules.d.ts", "../kaos/src/**/*.ts", "../kosong/src/**/*.ts", - "../oauth/src/**/*.ts" + "../oauth/src/**/*.ts", + "../protocol/src/**/*.ts" ], "exclude": ["dist", ".tmp-api-extractor", "examples", "test", "../*/dist", "../*/test"] } From d4066da87338929ef71c3caa076a7fd6bd5ec517 Mon Sep 17 00:00:00 2001 From: Xinran Date: Mon, 20 Jul 2026 17:02:49 +0800 Subject: [PATCH 3/4] fix(agent-core): refresh system prompt on resume when roleAdditional changes Signed-off-by: Xinran --- packages/agent-core/src/session/index.ts | 13 ++ .../session/resume-roleadditional.test.ts | 219 ++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 packages/agent-core/test/session/resume-roleadditional.test.ts diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 66b098012..e248a7ce2 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 000000000..6822b8fcc --- /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(); + } + }); +}); From 70c1c21833a3df8283b5f4180bf33c661448b436 Mon Sep 17 00:00:00 2001 From: Xinran Date: Tue, 21 Jul 2026 22:33:11 +0800 Subject: [PATCH 4/4] chore: add changeset for Botiverse 0.28.1 patch stack Signed-off-by: Xinran --- .changeset/botiverse-0-28-1-patch-stack.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/botiverse-0-28-1-patch-stack.md diff --git a/.changeset/botiverse-0-28-1-patch-stack.md b/.changeset/botiverse-0-28-1-patch-stack.md new file mode 100644 index 000000000..a4c75c64e --- /dev/null +++ b/.changeset/botiverse-0-28-1-patch-stack.md @@ -0,0 +1,7 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +"@moonshot-ai/kimi-code-sdk": patch +--- + +Thread standing prompts through the session API, bundle protocol declarations, and refresh system prompts on resume.