Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f51209a
feat(agent-core-v2): add a fork parameter to the Agent tool
7Sageer Aug 17, 2026
c93fede
fix(agent-core-v2): bind the stale-todo reminder only into the main a…
7Sageer Aug 17, 2026
2844e13
fix(agent-core-v2): inherit the caller's live binding and label fork …
7Sageer Aug 18, 2026
d32a871
fix(agent-core-v2): close inherited in-flight tool calls instead of t…
7Sageer Aug 18, 2026
51b5403
refactor(agent-core-v2): route Agent tool forks through agentLifecycl…
7Sageer Aug 18, 2026
6cfc57d
Merge remote-tracking branch 'origin/main' into fork-subagent
7Sageer Aug 18, 2026
4b8095d
feat(agent-core-v2): add a fork parameter to the AgentSwarm tool
7Sageer Aug 18, 2026
f7fdc19
Merge remote-tracking branch 'origin/main' into fork-subagent
7Sageer Aug 18, 2026
9c68567
fix(agent-core-v2): seal partial assistant forks
7Sageer Aug 18, 2026
3fa0d0a
fix(agent-core-v2): align fork parameter descriptions
7Sageer Aug 18, 2026
65af0ca
fix(agent-core-v2): drop the main-only registration gate from goal tools
7Sageer Aug 19, 2026
0e68451
Merge remote-tracking branch 'origin/main' into fork-subagent
7Sageer Aug 19, 2026
ecbaf05
fix(agent-core-v2): disclose dates via reminders to keep the system p…
7Sageer Aug 19, 2026
fe9bfdf
docs: condense the fork changesets to single sentences
7Sageer Aug 19, 2026
56c1329
docs(agent-core-v2): frame the tool-contribution when gate as a fork …
7Sageer Aug 19, 2026
957b3a6
test(agent-core-v2): plug fork coverage gaps and decouple swarm tests…
7Sageer Aug 19, 2026
1de050e
Merge remote-tracking branch 'origin/fork-subagent' into fork-subagent
7Sageer Aug 19, 2026
7aad1fc
docs(agent-core-v2): keep the when-gate guidance in the contribution …
7Sageer Aug 19, 2026
261e60f
fix(agent-core-v2): contribute cron tools to every agent for fork pre…
7Sageer Aug 19, 2026
4777fd5
feat(agent-core-v2): track the fork parameter in the subagent_created…
7Sageer Aug 19, 2026
014c367
fix(agent-core-v2): gate tower orchestration tools at execution time
7Sageer Aug 19, 2026
66f104d
test(agent-core-v2): expect the fork field in the subagent_created mi…
7Sageer Aug 19, 2026
c99ec1f
test(agent-core-v2): cover fork subagent first-request prefix parity
7Sageer Aug 19, 2026
b4e4a94
refactor(agent-core-v2): share the main-agent-only tool refusal acros…
7Sageer Aug 19, 2026
35f6a13
Merge remote-tracking branch 'origin/main' into fork-subagent
7Sageer Aug 19, 2026
fc09a8f
refactor(agent-core-v2): keep the goal main-agent gate at the tool la…
7Sageer Aug 19, 2026
a77d8a5
fix(agent-core-v2): preserve the fork tool surface when inheriting us…
7Sageer Aug 19, 2026
eeafd0f
Revert "refactor(agent-core-v2): keep the goal main-agent gate at the…
7Sageer Aug 19, 2026
7eaed14
test(agent-core-v2): complete fork lifecycle stub
7Sageer Aug 19, 2026
a3a58d6
Delete .changeset/btw-inflight-tool-calls.md
7Sageer Aug 19, 2026
3234a05
Delete .changeset/todo-reminder-main-only.md
7Sageer Aug 19, 2026
3227e09
Delete .changeset/swarm-fork-context.md
7Sageer Aug 19, 2026
4176ff8
Add optional 'fork' parameter to subagent tools
7Sageer Aug 19, 2026
c390800
docs(agent-core-v2): drop the fork JSDoc comments
7Sageer Aug 19, 2026
f70b856
Merge remote-tracking branch 'origin/main' into fork-subagent
7Sageer Aug 20, 2026
0c93629
feat(agent-core-v2): add prompt_cache_probe telemetry for forked agents
7Sageer Aug 20, 2026
2efffa0
Merge remote-tracking branch 'origin/main' into fork-subagent
7Sageer Aug 20, 2026
8b30c28
feat(agent-core-v2): gate the subagent fork parameter behind an exper…
7Sageer Aug 20, 2026
9c74db8
Merge remote-tracking branch 'origin/main' into fork-subagent
7Sageer Aug 20, 2026
63a178e
Merge remote-tracking branch 'origin/main' into fork-subagent
7Sageer Aug 20, 2026
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/btw-inflight-tool-calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix side-question conversations started while the main agent is mid-task: tool calls still running on the main agent are now marked as unfinished in the side conversation instead of dangling, so the side agent no longer reports their results as lost.
5 changes: 5 additions & 0 deletions .changeset/subagent-fork-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Subagents can now fork the current context: the Agent tool accepts an optional `fork` parameter that starts the subagent with a snapshot of the calling agent's conversation history — same agent type, tool set, and model — instead of zero context. Tool calls still in flight at fork time are carried over explicitly marked as unfinished. The model opts in per spawn with `fork: true` when a task builds on the current conversation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Rewrite the fork changesets as single sentences

This entry uses three sentences and mixes the fork feature with in-flight-call handling and usage guidance; .changeset/swarm-fork-context.md likewise uses multiple sentences. The repository requires every changeset to contain one short user-facing sentence stating only what changed, so condense each entry and leave the separately changesetted in-flight fix out of this one.

AGENTS.md reference: AGENTS.md:L85-L87

Useful? React with 👍 / 👎.

5 changes: 5 additions & 0 deletions .changeset/swarm-fork-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Agent swarms can now fork the current context: the AgentSwarm tool accepts an optional `fork` parameter that starts every item-spawned subagent with a snapshot of the calling agent's conversation history — same agent type, tool set, and model — instead of zero context. Pass `fork: true` when every item builds on the current conversation.
5 changes: 5 additions & 0 deletions .changeset/todo-reminder-main-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Only the main agent now receives the stale-todo-list reminder; subagents keep sharing the session todo list but are no longer nudged about it.
2 changes: 1 addition & 1 deletion packages/agent-core-v2/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The DI kernel (`src/_base/di/`) owns the unit layer on top of the scoped registr
- `instantiation.ts` — the `@ref(IX)` decorator factory (`LiveRef<T>`: `current` live read + `onDidChange` availability event; observation creates no binding and no graph edge) and `ScopeActivation`.
- `src/app/feature/` — `IFeatureManager` (App scope): runtime unit assembly (`provideUnit` / `unprovideUnit` / `updateUnit`) and introspection (`units()` / `onDidChangeUnits`); managed units hang on the manager's own book. External package management stays with `IPluginService`. The `features` assembly (`src/features/featureAssemblyService.ts`) drains the module-level feature table through it.

The four contribution seams (token → fold): config sections — `ConfigSectionContribution` → `ConfigRegistry` fold (`src/app/config/`; module-level `registerConfigSection` stays the static built-in channel drained at construction, a withdrawn runtime record unregisters the section while user TOML values survive); agent tools — `AgentToolContribution` → `AgentToolActivationService` fold (built-in records provided once at App scope by `builtinToolAssemblyService`; `registerAgentToolService` stays the static channel: Agent-scope DI `OnDemand` registration + module table); agent profiles — `AgentProfileContribution` → `IAgentProfileRegistry` fold (see Scopes); event/state vocabulary — `EventStateContribution` → `IEventDispatcher` fold (a record bundles `events`; `registerEvent2Class` stays the static channel drained at fold time, while `defineState(...).replayable(...)` keys are explicit owner-service contributions: each replayable key's owning service contributes it via `contributeState` at construction — Agent-scope owners are eager, the session-domain todo/cron/interaction keys bridge through `agentLifecycle.onDidCreate` before `restore()` — and replaying a withdrawn domain's history lands on the unknown-type skip-and-count path). A fifth seam: executable commands — `CommandContribution` → `IAgentCommandService` fold (`src/agent/command/`; a contributed command runs engine-side — `run(ctx)` gets `ctx.get` resolving through the agent container, valid only during the synchronous part of `run`; name-level dedup, last record wins; surfaced over RPC as `agentCommandService.list` / `run`).
The four contribution seams (token → fold): config sections — `ConfigSectionContribution` → `ConfigRegistry` fold (`src/app/config/`; module-level `registerConfigSection` stays the static built-in channel drained at construction, a withdrawn runtime record unregisters the section while user TOML values survive); agent tools — `AgentToolContribution` → `AgentToolActivationService` fold (built-in records provided once at App scope by `builtinToolAssemblyService`; `registerAgentToolService` stays the static channel: Agent-scope DI `OnDemand` registration + module table; a contribution's `when` must be a pure function of facts every agent in the session recomputes identically — never per-agent identity such as `agentId === 'main'`, because forked agents inherit the caller's context and must rebuild an identical tool surface for prefix-cache parity; capability differences are enforced at execution time, see `AgentGoalService.assertSupportedAgent`); agent profiles — `AgentProfileContribution` → `IAgentProfileRegistry` fold (see Scopes); event/state vocabulary — `EventStateContribution` → `IEventDispatcher` fold (a record bundles `events`; `registerEvent2Class` stays the static channel drained at fold time, while `defineState(...).replayable(...)` keys are explicit owner-service contributions: each replayable key's owning service contributes it via `contributeState` at construction — Agent-scope owners are eager, the session-domain todo/cron/interaction keys bridge through `agentLifecycle.onDidCreate` before `restore()` — and replaying a withdrawn domain's history lands on the unknown-type skip-and-count path). A fifth seam: executable commands — `CommandContribution` → `IAgentCommandService` fold (`src/agent/command/`; a contributed command runs engine-side — `run(ctx)` gets `ctx.get` resolving through the agent container, valid only during the synchronous part of `run`; name-level dedup, last record wins; surfaced over RPC as `agentCommandService.list` / `run`).

`src/features/` — built-in capabilities authored as self-contained Feature units (`plan` was the first, extracted from `agent/plan` + `agent/tools/plan`; `swarm` followed, extracted from `agent/swarm` + `session/swarm` + `agent/tools/agent-swarm` into a scope-organized `agent/` + `session/` + `tools/` layout; `tower` lives here as `features/tower/` — protocol store, rate limit, tower-mode service, eleven `Tower*` tools, the `tower-worker` profile, and the `/tower` skill body). A `Feature` (`src/features/feature.ts`) is an App-scope unit recipe with a `static override readonly name` and `contribute*` helpers composing the seams: `contributeService(scope, id, ctor)` / `contributeAgentService` (per-scope materialization via `ScopeUnits` — provider death retracts everywhere, 连坐), `contributeTool` (per-agent `OnDemand` registration + the `AgentToolContribution` record), `contributeProfiles`, `contributeConfig`, `contributeCommand`, plus `onDispose`. Feature modules self-register at import (`registerFeature`, `src/features/featureRegistry.ts`); the App-scope `IFeatureAssemblyService` drains the table through `IFeatureManager.provideUnit`, so every feature is a named, introspectable, retractable managed unit. Built-in features keep user-facing static contracts — config sections, agent profiles, wire vocabulary — on the static import=register channels (the config/state manifest generators read static tables / call sites; wire records must stay replayable); the Feature unit carries the runtime capabilities (services, tools, commands). The string form of the unit `on(...)` capability (`this.on('turn.ended', …)`) is backed by the production `FiberEventResolver` registered in `src/app/event/fiberEventResolver.ts`, resolving against the scope's `IEventBus`.

Expand Down
48 changes: 48 additions & 0 deletions packages/agent-core-v2/src/agent/contextMemory/openToolExchange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createToolMessage } from '#/kosong/contract/message';

import type { ContextMessage } from './types';

export const INHERITED_IN_FLIGHT_TOOL_OUTPUT =
'This tool call was still executing when this conversation snapshot was inherited from the source agent, so its result is not part of this context. The outcome is unknown — do not assume it succeeded or failed, and do not wait for it.';

/**
* Closes a trailing open tool exchange in a history about to seed another
* agent's context memory. The source's in-flight tool calls can never receive
* their real results in the inheriting agent, so each unclosed call is
* answered with a synthetic {@link INHERITED_IN_FLIGHT_TOOL_OUTPUT} tool
* message rather than trimmed away — the seeded conversation stays
* well-formed and the source's final step remains visible as reference.
*/
export function closeTrailingOpenToolExchange(
history: readonly ContextMessage[],
): ContextMessage[] {
let lastNonToolIndex = history.length - 1;
while (lastNonToolIndex >= 0 && history[lastNonToolIndex]?.role === 'tool') {
lastNonToolIndex -= 1;
}

const assistant = history[lastNonToolIndex];
if (assistant === undefined) return [];
if (assistant.role !== 'assistant' || assistant.toolCalls.length === 0) return [...history];

const answeredToolCallIds = new Set(
history
.slice(lastNonToolIndex + 1)
.map((message) => message.toolCallId)
.filter((toolCallId): toolCallId is string => typeof toolCallId === 'string'),
);
const openCalls = assistant.toolCalls.filter(
(toolCall) => !answeredToolCallIds.has(toolCall.id),
);
if (openCalls.length === 0) return [...history];
const settledAssistant =
assistant.partial === true ? { ...assistant, partial: undefined } : assistant;
return [
...history.slice(0, lastNonToolIndex),
settledAssistant,
...history.slice(lastNonToolIndex + 1),
...openCalls.map((toolCall) =>
createToolMessage(toolCall.id, INHERITED_IN_FLIGHT_TOOL_OUTPUT),
),
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ export interface AgentToolContributionOptions {
readonly name: string;
readonly source?: ToolSource;
readonly disclosure?: ToolDisclosure;
/**
* Extra activation predicate, re-evaluated at every activation wave. Must be a
* pure function of facts every agent in the session recomputes identically
* (runtime capabilities, provider availability, config). Never gate on
* per-agent identity (e.g. `agentId === 'main'`): forked agents inherit the
* caller's conversation and must rebuild an identical tool surface for prompt
* prefix-cache parity. Restrict what a tool may DO at execution time instead
* (a service-level authority check that rejects the call).
*/
readonly when?: (accessor: ServicesAccessor) => boolean;
readonly requiredRuntimeCapabilities?: readonly RuntimeCapability[];
readonly domain?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/agent/tools/agent/agent.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Launch a subagent to handle a task. The subagent runs as a same-process loop instance with its own context and wire file. Delegating also keeps the bulk of intermediate file contents out of your own context — you get a conclusion back instead of a pile of dumps.

Writing the prompt:
- The subagent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.
- By default, the subagent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics. When the task builds on this conversation, pass `fork: true` instead: the subagent then starts with a snapshot of your completed history (inheriting your own agent type, tool set, and model), so the prompt only needs the task itself.
- Lookups (read this file, run that test): put the exact path or command in the prompt. The subagent should not have to search for things you already know.
- Investigations (figure out X, find why Y): give the question, not prescribed steps — fixed steps become dead weight when the premise is wrong.
- Do not delegate understanding. If the task hinges on a file path or line number, find it yourself first and write it into the prompt.
Expand Down
12 changes: 10 additions & 2 deletions packages/agent-core-v2/src/agent/tools/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { z } from 'zod';

import { createDecorator } from '#/_base/di/instantiation';
import { type AgentTool } from '#/tool/toolContract';
import { DEFAULT_PROFILE_NAME } from '#/session/subagent/spawn';

export const DEFAULT_PROFILE_NAME = 'coder';
export { DEFAULT_PROFILE_NAME };
export const RESUMED_LABEL = 'subagent';

export const SubagentToolInputSchema = z.preprocess(
Expand All @@ -17,7 +18,8 @@ export const SubagentToolInputSchema = z.preprocess(
typeof normalized['resume'] === 'string' && normalized['resume'].trim().length > 0;
const hasSubagentType =
typeof normalized['subagent_type'] === 'string' && normalized['subagent_type'].length > 0;
if (!hasSubagentType && !hasResumeId) {
const hasFork = normalized['fork'] === true;
if (!hasSubagentType && !hasResumeId && !hasFork) {
normalized['subagent_type'] = DEFAULT_PROFILE_NAME;
} else if (!hasSubagentType) {
delete normalized['subagent_type'];
Expand Down Expand Up @@ -45,6 +47,12 @@ export const SubagentToolInputSchema = z.preprocess(
.describe(
'If true, return immediately without waiting for completion. Prefer false unless the task can run independently and there is a clear benefit to not waiting.',
),
fork: z
.boolean()
.optional()
.describe(
'Fork the current context: the subagent starts with a snapshot of this agent\'s completed conversation history instead of zero context, inheriting this agent\'s agent type, tool set, and model. A non-empty resume is rejected. If subagent_type is provided, it must match this agent\'s type; if model is provided, it must be this agent\'s model or "primary". Different types and model overrides are rejected.',
),
model: z
.string()
.optional()
Expand Down
Loading