kimi-code/packages/klient
7Sageer f6736d7c0d
feat(agent-core-v2): add a fork parameter to the Agent tool (#3007)
* feat(agent-core-v2): add a fork parameter to the Agent tool

Spawning with fork: true starts the subagent from a one-time snapshot of
the calling agent's completed conversation history — same profile, tool
set, and model — instead of zero context. The seed trims the trailing
open tool exchange (the in-flight Agent call itself) before appending
into the child's context memory, and the first prompt carries an
inheritance notice framing the seeded history as reference material.

Fork rejects resume, a different subagent_type, or a model override as
tool errors, and skips the subagents allowlist since a self-inheritance
is not a delegation.

* fix(agent-core-v2): bind the stale-todo reminder only into the main agent

Subagents share the session todo list but no longer receive the
stale-todo nudge — the reminder injector now registers only on the main
agent, so delegated and forked agents are not prompted to maintain a
list they do not own.

* fix(agent-core-v2): inherit the caller's live binding and label fork launches correctly

Review follow-ups for the Agent tool fork mode:

- overlay the caller's live profile.data() via applyBindingSnapshot after
  the catalog re-bind, so ephemeral addActiveTool deltas, the rendered
  system prompt, and runtime model/subagents updates survive the fork;
  skip the profile prompt prefix since the caller's prefixed first
  prompt is already part of the seeded history
- resolve the fork activity label and approval-rule subject from the
  caller's own profile instead of falling back to the default subagent
  type, so an Agent(<other profile>) rule cannot approve a fork

* fix(agent-core-v2): close inherited in-flight tool calls instead of trimming them

Fork seeding now answers the source's trailing open tool calls with a
synthetic in-flight result instead of cutting the whole trailing
exchange: the seeded history stays protocol-valid, keeps the source's
final step visible as reference, and no longer confuses side-question
(btw) agents forked while the main agent is mid-turn. The close helper
is shared by the Agent tool fork and IAgentLifecycleService.fork.

Fork launches also stop requiring the caller's profile to still exist
in the session catalog: the child is created unbound and overlaid with
the caller's live binding snapshot, matching the lifecycle fork path,
and now records forkedFrom provenance.

* refactor(agent-core-v2): route Agent tool forks through agentLifecycle.fork

* feat(agent-core-v2): add a fork parameter to the AgentSwarm tool

* fix(agent-core-v2): seal partial assistant forks

* fix(agent-core-v2): align fork parameter descriptions

* fix(agent-core-v2): drop the main-only registration gate from goal tools

* fix(agent-core-v2): disclose dates via reminders to keep the system prompt byte-stable

* docs: condense the fork changesets to single sentences

* docs(agent-core-v2): frame the tool-contribution when gate as a fork parity trade-off

* test(agent-core-v2): plug fork coverage gaps and decouple swarm tests from spawn internals

* docs(agent-core-v2): keep the when-gate guidance in the contribution JSDoc only

* fix(agent-core-v2): contribute cron tools to every agent for fork prefix-cache parity

CronCreate/CronList/CronDelete were registered directly into the main
agent's tool registry by SessionCronServiceImpl, bypassing the
AgentToolContribution seam and keying on per-agent identity — so a forked
agent rebuilt a tool surface three tools shorter than its caller and the
inherited prompt prefix missed the cache.

Register the three tools through registerAgentToolService like the goal
tools do (no when gate, identical surface for every agent) and enforce
the main-agent restriction at execution time instead. Also fall back to
DEFAULT_CRON_CONFIG when the config section is absent, since the service
can now be constructed after the main agent exists.

* feat(agent-core-v2): track the fork parameter in the subagent_created event

* fix(agent-core-v2): gate tower orchestration tools at execution time

TowerInit/TowerPlan/TowerSpawn/TowerMerge/TowerTeardown were contributed
with a when predicate keyed on agentId === 'main', so a forked agent
rebuilt a tool surface missing TowerInit (always present for the default
profile) plus the rest of the tower set once it was enabled — breaking
prompt prefix-cache parity with the caller.

Contribute the tools with no when gate (profile policy still controls
visibility) and reject non-main callers at execution time instead.

* test(agent-core-v2): expect the fork field in the subagent_created mirror assertion

* test(agent-core-v2): cover fork subagent first-request prefix parity

* refactor(agent-core-v2): share the main-agent-only tool refusal across cron and goal tools

Goal tools rejected subagent callers by throwing GOAL_UNSUPPORTED_AGENT
from the service, which the executor wrapped as a resolution failure;
cron tools returned a clean refusal but each tool open-coded the same
identity check. Centralize the check and both messages in
agent/tools/mainAgentOnly.ts and use it from all seven tools, keeping
AgentGoalService.assertSupportedAgent as the coded boundary for RPC and
SDK callers.

* refactor(agent-core-v2): keep the goal main-agent gate at the tool layer only

* fix(agent-core-v2): preserve the fork tool surface when inheriting user tools

* Revert "refactor(agent-core-v2): keep the goal main-agent gate at the tool layer only"

This reverts commit fc09a8fa32.

* test(agent-core-v2): complete fork lifecycle stub

* Delete .changeset/btw-inflight-tool-calls.md

Signed-off-by: 7Sageer <sag77r@hotmail.com>

* Delete .changeset/todo-reminder-main-only.md

Signed-off-by: 7Sageer <sag77r@hotmail.com>

* Delete .changeset/swarm-fork-context.md

Signed-off-by: 7Sageer <sag77r@hotmail.com>

* Add optional 'fork' parameter to subagent tools

Signed-off-by: 7Sageer <sag77r@hotmail.com>

* docs(agent-core-v2): drop the fork JSDoc comments

* feat(agent-core-v2): add prompt_cache_probe telemetry for forked agents

* feat(agent-core-v2): gate the subagent fork parameter behind an experimental flag

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-08-20 21:50:44 +08:00
..
examples feat: unify the host identity across OAuth, telemetry, and kap-server (#2382) 2026-07-30 13:45:41 +08:00
scripts feat(klient): contract-driven facade with http/ipc/memory transports (#1768) 2026-07-16 16:43:09 +08:00
src refactor(agent-core-v2): migrate agent domains to model-as-container architecture (#3103) 2026-08-20 16:06:59 +08:00
test feat(agent-core-v2): add a fork parameter to the Agent tool (#3007) 2026-08-20 21:50:44 +08:00
AGENTS.md feat: engine-native image references via kimi-file:// media resolver (#2593) 2026-08-17 13:11:28 +08:00
CHANGELOG.md ci: release packages (#2881) 2026-08-14 20:51:17 +08:00
Dockerfile feat(klient): contract-driven facade with http/ipc/memory transports (#1768) 2026-07-16 16:43:09 +08:00
package.json ci: release packages (#2881) 2026-08-14 20:51:17 +08:00
README.md feat: agent-core-v2 permission/workspace refactors and transcript durability (#2021) 2026-07-22 19:21:56 +08:00
tsconfig.examples.json test(klient): add real-server smoke coverage (#1713) 2026-07-15 15:24:36 +08:00
tsconfig.json feat(klient): contract-driven facade with http/ipc/memory transports (#1768) 2026-07-16 16:43:09 +08:00
tsdown.config.ts feat(transcript): add unified transcript layer, drop the /api/v2 RPC surface (#1888) 2026-07-20 15:33:05 +08:00
vitest.config.ts feat(klient): contract-driven facade with http/ipc/memory transports (#1768) 2026-07-16 16:43:09 +08:00

@moonshot-ai/klient

Contract-driven client SDK for the agent-core-v2 engine. One facade, two transports — you pick the transport once at creation; everything after that is byte-identical:

import { bootstrap, logSeed, resolveLoggingConfig } from '@moonshot-ai/agent-core-v2';
import { createKlient } from '@moonshot-ai/klient/memory';   // or '/ipc'

const { app } = bootstrap({ homeDir }, [
  ...logSeed(resolveLoggingConfig({ homeDir, env: process.env })),
]);
const klient = createKlient({ scope: app });

const env = await klient.global.env();
const sessions = await klient.global.sessions.list({ limit: 20 });

const session = await klient.global.sessions.create({ workDir: process.cwd() });
const agent = klient.session(session.id).agent('main');
agent.events.on('assistant.delta', (e) => process.stdout.write(e.delta));
agent.events.on('prompt.completed', () => console.log('\ndone'));
await agent.prompt({ input: [{ type: 'text', text: 'Say OK.' }] });

await klient.close();

Architecture

facade (klient.global.*, klient.session(id).*, session.agent(id).*, *.events.*)
   ↓ single-object params, zod-validated
contract (procedure schemas, shared by all transports)
   ↓
KlientChannel { call, listen }   ← the only transport SPI
   ↓
ipc │ memory
  • Facade — aggregated methods, no engine service tokens, no onDid*/onWill* event names. There is no escape hatch to raw services: the facade is the public contract.
    • klient.global.*sessions.* (incl. create), workspaces.*, config.*, providers.*, models.*, catalog.*, auth.*, flags.*, plugins.*, hostFs.*, env().
    • klient.session(id).*get/setTitle/update/status/close/archive/ restore/fork/createChild, approvals.*, questions.*, interactions.*, agents().
    • session.agent(id).*prompt/steer/cancel/runShellCommand/ cancelShellCommand/getModel/setModel/setPermission/getUsage/getContext/ getPlan*/getTasks*/stopTask/getTaskOutput.
  • Contract — every method has a zod input tuple + output schema, validated on the client before send / after receive (default on; validate: false to disable). Validation is sub-µs for typical payloads — cheaper than the JSON serialization the wire already pays.
  • Eventsklient.events.on(...) for the global bus (config.changed, kosong.models.changed, session.archived, …), session(id).events.on('metadata.changed' | 'interactions.changed' | 'interactions.resolved'), and agent(id).events.on('turn.started' | 'assistant.delta' | 'tool.call.started' | 'prompt.completed' | …). Underlying subscriptions are shared and ref-counted; payloads are validated; bad payloads drop to events.onError.

Transports

entry options events
@moonshot-ai/klient/ipc { socketPath, token? } same socket
@moonshot-ai/klient/memory { scope } (a bootstrapped engine app scope) direct emitter/bus subscription

ipc and memory share one in-process dispatcher, so they behave identically by construction; memory additionally JSON round-trips every value so results cross the same JSON boundary a socket transport would impose. The IPC host ships with the transport: serveKlientIpc({ scope, socketPath }).

The same conformance suite runs against both transports in this package's tests (test/helpers/conformance.ts — one test file per transport).

This package also hosts the e2e suites (the retired server-e2e package was folded in here):

  • test/e2e/legacy/ + test/e2e/harness/ — the legacy /api/v1 live suites and their client harness (skip unless KIMI_SERVER_URL is set; the v1 surface has no in-memory equivalent, so these stay live-server-only).

The docker e2e runner (pnpm docker:e2e) runs this whole vitest suite inside a container against a container-local server. See AGENTS.md for the testing rules.

Scope

The facade covers the global (app), session, and agent surfaces shown above. What it deliberately leaves out (for now): onWill/hook-style interception (engine hooks are in-process OrderedHookSlots and not wire-exposable), file upload (v1 multipart REST only), and the terminal surface (v1 REST + WS only).

Smoke check

pnpm -C packages/klient smoke

examples/smoke.ts boots an in-process engine (memory transport) and asserts the global facade end-to-end — no server needed. examples/basic.ts is a shorter narrated tour; examples/context-usage.ts traces context-size readings through a real prompt (requires KIMI_EXAMPLE_MODEL + KIMI_EXAMPLE_API_KEY).