* 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
|
||
|---|---|---|
| .. | ||
| examples | ||
| scripts | ||
| src | ||
| test | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| Dockerfile | ||
| package.json | ||
| README.md | ||
| tsconfig.examples.json | ||
| tsconfig.json | ||
| tsdown.config.ts | ||
| vitest.config.ts | ||
@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: falseto disable). Validation is sub-µs for typical payloads — cheaper than the JSON serialization the wire already pays. - Events —
klient.events.on(...)for the global bus (config.changed,kosong.models.changed,session.archived, …),session(id).events.on('metadata.changed' | 'interactions.changed' | 'interactions.resolved'), andagent(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 toevents.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/v1live suites and their client harness (skip unlessKIMI_SERVER_URLis 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).