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
5 changes: 5 additions & 0 deletions .changeset/multi-environment-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add experimental per-call environment routing for file and shell tools, which lists a connected environment's `AGENTS.md` files and adds its project skills as `<skill>@<environment>`. Enable it with `multi_environment = true` under `[experimental]` or `KIMI_CODE_EXPERIMENTAL_MULTI_ENVIRONMENT`.
18 changes: 12 additions & 6 deletions apps/kimi-code/src/tui/reverse-rpc/approval/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ export function createApprovalRequestHandler(
try {
const data = adaptApprovalRequest(event);
// A badge lookup failure must never cancel the approval itself — it
// degrades to no badge.
let environment: string | undefined;
try {
environment = resolveEnvironment?.();
} catch {
environment = undefined;
// degrades to no badge. A routed call's own environment takes precedence
// over the session's primary environment.
let environment =
event.display.kind === 'command' || event.display.kind === 'file_io'
? event.display.environment
: undefined;
if (environment === undefined) {
try {
environment = resolveEnvironment?.();
} catch {
environment = undefined;
}
}
const response = await controller.show(
environment === undefined ? data : { ...data, environment },
Expand Down
16 changes: 16 additions & 0 deletions apps/kimi-code/test/tui/reverse-rpc/approval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,22 @@ describe('approval reverse-rpc', () => {
);
});

it('labels a routed call with its own environment instead of the session environment', async () => {
const controller = new ApprovalController();
const show = vi.spyOn(controller, 'show').mockResolvedValue({ decision: 'approved' });
const handler = createApprovalRequestHandler(controller, undefined, () => 'ssh:dev');

await handler(approvalEvent({
display: { kind: 'command', command: 'pwd', cwd: '/work', environment: 'gpu' },
}));
await handler(approvalEvent({
display: { kind: 'file_io', operation: 'write', path: '/tmp/a.txt', environment: 'local' },
}));
await handler(approvalEvent({ display: { kind: 'command', command: 'pwd' } }));

expect(show.mock.calls.map(([data]) => data.environment)).toEqual(['gpu', 'local', 'ssh:dev']);
});

it('tracks the requesting agent until approval settles or fails', async () => {
const controller = new ApprovalController();
const show = vi.spyOn(controller, 'show').mockResolvedValue({ decision: 'approved' });
Expand Down
17 changes: 17 additions & 0 deletions docs/en/guides/remote-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ Resuming reads the persisted environment id and working directory and reconnects

The footer shows the active environment's identity before the working directory; `local` renders nothing. A disconnected environment is shown with its failure reason, and the local git status slot is hidden for remote sessions. Approval prompts display the target environment next to the command or path, and `@` file completion queries the target filesystem.

## Multiple environments in one session

The experimental multi-environment feature lets `Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, and `ReadMediaFile` choose a configured environment for each call. It is disabled by default. Enable it in `config.toml` alongside your [environment declarations](#declaring-environments):

```toml
[experimental]
multi_environment = true
```

You can instead set `KIMI_CODE_EXPERIMENTAL_MULTI_ENVIRONMENT=true` for one process. When at least two environments are available, these tools gain an optional `environment` argument. Omitting it uses the session's primary environment; an unknown id fails the call rather than running it elsewhere. This enables the model to select environments — it does not add a manual environment selector to the TUI.

All configured environments are candidates. The first call to another environment connects it lazily and uses its `defaultCwd`, or the working directory reported by the target when `defaultCwd` is unset. The primary keeps its existing working directory and additional directories. For example, a local session can inspect a remote checkout with `Read` using `environment = "dev-box"`, then run its tests with `Bash` in the same environment.

Approval prompts show the environment for each call, and "allow for this session" approvals are remembered separately per environment. Tools outside the list above keep using the primary environment or the local host as described elsewhere on this page.

The system prompt still describes only the primary environment. Once another environment connects, the session reads that environment's project context once: the `AGENTS.md` files found from its working directory are listed for the model, which is asked to read them before modifying files there, and its project skills join the session's skill list as `<skill>@<environment>` (for example `deploy@dev-box`). Environments that have not connected yet contribute nothing until their first use. When the primary environment is remote, user-level skills from the local host are marked as belonging to `local`, so the model reads their supporting files there.

## Disconnects and reconnecting

Each environment id keeps one connection for the whole process. Workspaces bound to that id share it; a different id, including another id on the same machine, keeps its own. Two layers decide when the connection is dead: the SSH launcher's transport keepalive (`ServerAliveInterval`), and a protocol-level liveness probe — after about 30 seconds of idle time the client sends a `ping` to the executor, and no response within 10 seconds means the connection is dead. Any inbound traffic resets the idle clock so active transfers are never probed, and executors too old to know `ping` disable the probe automatically. A closed local pipe is also a disconnect signal. When the connection drops — network loss, a stopped container, or the executor exiting — every workspace using that id goes `disconnected` at once, and the processes it started on the target end with the connection.
Expand Down
17 changes: 17 additions & 0 deletions docs/zh/guides/remote-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ kimi --environment dev-box --environment-cwd /home/me/projects -p "Run the test

底部状态栏会在工作目录前显示当前环境标识;`local` 不渲染。环境断开时显示失败原因,远程会话中本机 git 状态槽会隐藏。审批面板会在命令或路径旁显示目标环境标识,`@` 文件补全查询目标文件系统。

## 在一个会话中使用多个环境

实验性的多环境功能允许 `Bash`、`Read`、`Write`、`Edit`、`Glob`、`Grep` 和 `ReadMediaFile` 在每次调用时选择一个已配置的环境。该功能默认关闭,在 `config.toml` 中与[环境声明](#声明环境)一起启用:

```toml
[experimental]
multi_environment = true
```

也可以仅为当前进程设置 `KIMI_CODE_EXPERIMENTAL_MULTI_ENVIRONMENT=true`。至少有两个可用环境时,这些工具会增加可选的 `environment` 参数;省略时使用会话的主环境,传入未知 id 会让调用失败,不会改到别处执行。模型可以通过这个参数选择环境;TUI 目前没有提供手动选择环境的界面。

所有已配置的环境都是候选项。首次使用另一个环境时才建立连接,工作目录使用它的 `defaultCwd`;未设置时使用目标环境报告的工作目录。主环境保留原有的工作目录和附加目录。例如,本地会话可以让 `Read` 通过 `environment = "dev-box"` 查看远端代码,再让 `Bash` 在同一环境运行测试。

审批面板会显示每次调用所在的环境,「本会话内允许」的审批按环境分别记忆。上述列表之外的工具仍按本页其他章节的规则使用主环境或本机。

系统提示仍只描述主环境。另一个环境连接后,会话会读取一次它的项目上下文:从其工作目录向上找到的 `AGENTS.md` 会列给模型,并要求模型在修改该环境的文件前先读取;它的项目级 skills 以 `<skill>@<environment>` 的名字加入会话的 skill 列表(例如 `deploy@dev-box`)。尚未连接的环境在首次使用前不贡献任何内容。主环境是远程环境时,来自本机的用户级 skills 会标注为属于 `local`,模型会到本机读取它们的附属文件。

## 断线与重连

每个环境 id 在整个进程里只有一条连接。绑到这个 id 的工作区共用它;另一个 id,哪怕在同一台机器上,仍是各自的连接。连接是否已死由两层机制判断:SSH 启动器带 `ServerAliveInterval` 的传输层保活,以及协议层的存活探测——连接空闲约 30 秒时本机向执行器发送 `ping`,10 秒内未收到响应即判定断线;任何入站流量都会重置空闲计时,活跃传输不会被误判,不认识 `ping` 的旧版本执行器会自动停用该探测。本地管道关闭也是断线信号。连接断开时——网络中断、容器停止、执行器退出——使用这个 id 的所有工作区同时进入 `disconnected`,它在目标上启动的进程随连接结束。
Expand Down
26 changes: 26 additions & 0 deletions packages/agent-core-v2/docs/state-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export interface WorkspaceStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -190,6 +191,7 @@ export interface WorkspaceStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -218,6 +220,7 @@ export interface WorkspaceStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -254,6 +257,7 @@ export interface WorkspaceStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -282,6 +286,7 @@ export interface WorkspaceStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -310,6 +315,7 @@ export interface WorkspaceStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -340,6 +346,7 @@ export interface WorkspaceStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -368,6 +375,7 @@ export interface WorkspaceStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -397,6 +405,7 @@ export interface WorkspaceStateSnapshot {
readonly agentsMd: string | undefined;
readonly agentsMdWarning: string | undefined;
readonly agentsMdPaths: readonly string[] | undefined;
readonly agentsMdProjectPaths: readonly string[] | undefined;
};
// src/workspace/workspaceTrust/workspaceTrustService.ts
'workspaceTrust.trusted': boolean;
Expand Down Expand Up @@ -427,6 +436,7 @@ export interface SessionStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -465,6 +475,7 @@ export interface SessionStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -493,6 +504,7 @@ export interface SessionStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -529,6 +541,7 @@ export interface SessionStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -557,6 +570,7 @@ export interface SessionStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -585,6 +599,7 @@ export interface SessionStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -615,6 +630,7 @@ export interface SessionStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -643,6 +659,7 @@ export interface SessionStateSnapshot {
[key: string]: unknown;
};
readonly source: /* SkillSource — packages/agent-core-v2/src/features/skill/catalog/types.ts */ 'project' | 'user' | 'extra' | 'builtin';
readonly environmentId?: string;
readonly plugin?: /* SkillPluginContext — packages/agent-core-v2/src/features/skill/catalog/types.ts */ {
readonly id: string;
readonly instructions?: string;
Expand Down Expand Up @@ -879,6 +896,7 @@ export interface AgentStateSnapshot {
cwd?: string;
description?: string;
language?: 'bash';
environment?: string;
} | {
kind: 'file_io';
operation: 'read' | 'write' | 'edit' | 'glob' | 'grep';
Expand All @@ -887,6 +905,7 @@ export interface AgentStateSnapshot {
content?: string;
before?: string;
after?: string;
environment?: string;
} | {
kind: 'diff';
path: string;
Expand Down Expand Up @@ -1092,6 +1111,7 @@ export interface AgentStateSnapshot {
cwd?: string;
description?: string;
language?: 'bash';
environment?: string;
} | {
kind: 'file_io';
operation: 'read' | 'write' | 'edit' | 'glob' | 'grep';
Expand All @@ -1100,6 +1120,7 @@ export interface AgentStateSnapshot {
content?: string;
before?: string;
after?: string;
environment?: string;
} | {
kind: 'diff';
path: string;
Expand Down Expand Up @@ -1314,6 +1335,7 @@ export interface AgentStateSnapshot {
cwd?: string;
description?: string;
language?: 'bash';
environment?: string;
} | {
kind: 'file_io';
operation: 'read' | 'write' | 'edit' | 'glob' | 'grep';
Expand All @@ -1322,6 +1344,7 @@ export interface AgentStateSnapshot {
content?: string;
before?: string;
after?: string;
environment?: string;
} | {
kind: 'diff';
path: string;
Expand Down Expand Up @@ -1528,6 +1551,7 @@ export interface AgentStateSnapshot {
cwd?: string;
description?: string;
language?: 'bash';
environment?: string;
} | {
kind: 'file_io';
operation: 'read' | 'write' | 'edit' | 'glob' | 'grep';
Expand All @@ -1536,6 +1560,7 @@ export interface AgentStateSnapshot {
content?: string;
before?: string;
after?: string;
environment?: string;
} | {
kind: 'diff';
path: string;
Expand Down Expand Up @@ -1790,6 +1815,7 @@ export interface AgentStateSnapshot {
readonly reason?: string;
}[];
readonly sessionApprovalRulePatterns: readonly string[];
readonly environmentSessionApprovalRulePatterns?: Readonly<Record<string, readonly string[]>>;
};
// src/agent/plugin/agentPluginOps.ts
// replayable · durable — folds: PluginSessionStartEvent
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/docs/wire-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ interface PermissionRecordApprovalResultPayload {
toolName: string;
action: string;
sessionApprovalRule?: string;
environmentId?: string;
result: PermissionApprovalResultRecord['result'];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class AgentAgentsMdReminderService
}),
);
const handler = async (ctx: ToolDidExecuteContext, next: () => Promise<void>): Promise<void> => {
await this.probeAndRemind(ctx);
if (ctx.environment?.primary !== false) await this.probeAndRemind(ctx);
await next();
};
this._register(toolExecutor.hooks.onDidExecuteTool.register('agentsMdReminder', handler));
Expand Down
Loading
Loading