Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/roleadditional-session-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code-sdk": minor
---

Add a `roleAdditional` option to the session API. The agent profile system already renders a `{{ROLE_ADDITIONAL}}` slot, but there was no way to set it through the SDK — the render half existed with no input path. `createSession` / `resumeSession` now accept an optional `roleAdditional` string that is threaded (parallel to `additionalDirs`) into the resolved profile's system prompt for every agent in the session. When resume or reload supplies a `roleAdditional` that differs from the value that rendered an agent's persisted prompt, that agent's system prompt is re-rendered from the restored profile; an omitted value preserves the persisted standing prompt (so `Session.reloadSession()` / `/reload`, which cannot pass one, no longer clears it), while an explicit empty string clears it. Additive and optional — no behavior change when unset.
5 changes: 5 additions & 0 deletions packages/agent-core/src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export interface AgentOptions {
readonly imageLimits?: ImageLimits;
readonly replay?: ReplayBuilderOptions;
readonly additionalDirs?: readonly string[];
/** Standing prompt rendered into the profile's {{ROLE_ADDITIONAL}} slot. */
readonly roleAdditional?: string;
readonly systemPromptContextProvider?: (() => Promise<PreparedSystemPromptContext>) | undefined;
}

Expand Down Expand Up @@ -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<string>();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 });
}
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-core/src/rpc/core-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export interface CreateSessionPayload {
readonly metadata?: JsonObject | undefined;
readonly mcpServers?: Readonly<Record<string, McpServerConfig>>;
readonly additionalDirs?: readonly string[];
/** Standing prompt rendered into the profile's {{ROLE_ADDITIONAL}} slot. */
readonly roleAdditional?: string;
readonly client?: ClientTelemetryInfo | undefined;
readonly drainAgentTasksOnStop?: boolean;
}
Expand All @@ -81,6 +83,8 @@ export interface ResumeSessionPayload {
readonly sessionId: string;
readonly mcpServers?: Readonly<Record<string, McpServerConfig>>;
readonly additionalDirs?: readonly string[];
/** Standing prompt rendered into the profile's {{ROLE_ADDITIONAL}} slot, preserved across resume. */
readonly roleAdditional?: string;
/** Include persisted subagent states in the returned replay snapshot. */
readonly includeSubagents?: boolean;
/**
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core/src/rpc/core-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
pluginCommands,
appVersion: this.appVersion,
additionalDirs,
roleAdditional: options.roleAdditional,
drainAgentTasksOnStop: options.drainAgentTasksOnStop,
});
try {
Expand Down Expand Up @@ -532,6 +533,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
pluginCommands,
appVersion: this.appVersion,
additionalDirs,
roleAdditional: input.roleAdditional,
});
let warning: string | undefined;
try {
Expand Down
28 changes: 28 additions & 0 deletions packages/agent-core/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
/** Session-level standing prompt rendered into every agent's {{ROLE_ADDITIONAL}} slot. */
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
Expand All @@ -105,6 +107,8 @@ export interface AgentMeta {
readonly type: AgentType;
readonly parentAgentId?: string | null;
readonly swarmItem?: string;
/** The `roleAdditional` value used to render this agent's system prompt. */
roleAdditional?: string;
}

interface ResumedAgent {
Expand Down Expand Up @@ -183,6 +187,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<void>;
Expand Down Expand Up @@ -224,6 +229,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,
Expand Down Expand Up @@ -355,6 +361,16 @@ export class Session {
additionalDirs,
);
await this.setBaseAdditionalDirs(this.additionalDirs);
// Preserve the standing prompt persisted with the session when this
// resume/reload didn't supply a `roleAdditional`. `/reload` and plain
// `resumeSession({ id })` have no way to pass the value, so without this an
// omitted value would compare unequal to the persisted one below, clear the
// {{ROLE_ADDITIONAL}} slot, and drop the standing prompt (and newly-spawned
// agents would inherit `undefined`). An explicit value — including `""` to
// clear it — is left untouched and still overrides.
if (this.roleAdditional === undefined) {
this.roleAdditional = agents.main?.roleAdditional;
}
this.agents.clear();
// Only the main agent is needed to reopen the session; subagents replay
// lazily when an RPC or Agent(resume=...) call asks for their state.
Expand Down Expand Up @@ -641,6 +657,7 @@ export class Session {
type,
parentAgentId,
swarmItem: options.swarmItem,
roleAdditional: this.roleAdditional,
};
void this.writeMetadata();
}
Expand Down Expand Up @@ -948,6 +965,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()),
Expand Down Expand Up @@ -1035,6 +1053,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 standing instructions without
// refreshing AGENTS.md/cwd on every resume.
if (this.roleAdditional !== meta.roleAdditional) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve roleAdditional across reload

When a session that was created or resumed with roleAdditional is later reloaded, the reload path calls resumeSessionWithOverrides({ sessionId }) without a way to pass the previous value, so this.roleAdditional is undefined here while meta.roleAdditional still contains the original text. This comparison then refreshes the system prompt with the {{ROLE_ADDITIONAL}} slot cleared and records undefined, silently dropping the standing prompt on SDK Session.reloadSession()//reload; either thread the value through reload or treat an omitted value as preserving the persisted prompt.

Useful? React with 👍 / 👎.

await agent.refreshSystemPrompt();
meta.roleAdditional = this.roleAdditional;
void this.writeMetadata();
}
this.agents.set(id, agent);
return { agent, warning: parent?.warning ?? result.warning };
} catch (error) {
Expand Down
Loading