kimi-code/packages/klient
Luyu Cheng d6021fa036
feat(kap-server): accept bundled skill activations on the prompt submission route (#2982)
* feat(kap-server): accept bundled skill activations on the prompt submission route

The bundled-submission capability was only reachable through the
in-process klient transports; the App talks to kap-server over /api/v1.
The submit-prompt route now accepts an optional non-empty skills field
and delegates to IAgentSkillService.promptWithSkills — same validation,
events, and single bundled user message as the TUI path — skipping its
own prompt-metadata update (the engine owns it there) and mapping
skill.not_found / skill.type_unsupported onto the skills route's codes.
To return the submission's queue identity, the engine's promptWithSkills
now resolves with prompt_id / user_message_id / created_at / state (plus
turn_id once launched), mirrored through the klient contract.

* refactor(agent-core-v2): slim the promptWithSkills result contract

Drop the user_message_id field (it is always the same identity as
prompt_id — the route duplicates it) and narrow state to the
running/queued/blocked vocabulary, mapped at the engine edge instead of
exposing the internal seven-state PromptState on the wire.

* fix(kap-server): harden bundled skill submissions against review findings

- Validate bundled skill names and types before any media materialization
  or control override, so a rejected bundle leaves session state untouched
  (the engine still re-validates authoritatively).
- Declare the 40415/40912 outcomes on the submit route so the generated
  API documentation includes them.
- The klient output schema no longer tolerates a missing promptWithSkills
  result (a transport-level absence now raises instead of resolving
  undefined), and a failed launch surfaces as an error rather than a
  successful running result.
- Add the changeset for the new public API field.

* fix(kap-server): preflight bundled skills before agent materialization and stabilize listed content

- Skill preflight now runs on the session's catalog before the main agent
  is resolved, so a rejected bundle cannot mutate session metadata by
  registering main (regression test on a cold session without an agent).
- The prompts list projection strips the stored skill blocks from a
  bundled prompt, so GET /prompts returns the same caller-only content as
  the submit response.

* fix(kap-server): reject bundled prompt_id combos at preflight and clean queued staging

- The skills + prompt_id incompatibility rejection now runs at the initial
  bundled preflight, before the main agent is materialized or any
  override binds (previously a yolo override could bind before the 40001).
- Queued bundles no longer skip staging cleanup forever: the discard is
  deferred to the bundle's prompt.completed / prompt.aborted lifecycle
  event, mirroring the plain path's launch-raced cleanup.

* fix(kap-server): clean queued bundle staging on the steer path too

A queued bundle steered into the active turn is consumed at steer time,
but the engine publishes prompt.completed/aborted only for the parent —
the deferred cleanup never fired and its subscription leaked. The
prompt.steered event (matching promptIds) now counts as the child's
intake-completion signal.

* fix(agent-core-v2): materialize daemon-ref media on the steer and inject paths

startNext materializes daemon file references into the session media
store before a prompt's turn, but steer() and inject() enqueued the same
references without that intake, leaving the staging upload as the only
copy — any staging cleanup at steer time would delete the media the
turn is about to consume. Both paths now run the same intake before the
SteerStepRequest is created, so prompt.steered is a truthful
intake-complete signal.

* fix(kap-server): defer staging cleanup to turn settlement, never to steer time

Prompt-intake materialization is best-effort: when it degrades, the
daemon upload is the request-time resolver's fallback source. Discarding
staging at prompt.steered could therefore delete the only readable copy
before the parent's request ran. Cleanup is now uniformly event-driven —
the bundle's own prompt.completed/aborted, or the steer parent's — so
the upload always outlives the request it feeds.

* fix(kap-server): install settlement tracking before bundled enqueue

A hook-blocked bundle completes synchronously inside the submission
call, and an exceptionally fast launch can settle just as early — a
post-call subscription misses the only settlement event and leaks both
the staging blob and the listener. The tracker now subscribes before
enqueueing, buffers lifecycle events, and settles against the returned
prompt id (or its steer parent's).

* fix(kap-server): scope settlement tracking to the owning agent and dispose on rejection

- The tracker now subscribes through the agent-scoped IEventBus instead of
  the App-scoped IEventService: prompt lifecycle events from other
  sessions never reach it, so a colliding client-chosen prompt id cannot
  trigger a foreign settlement (and the steer re-target only follows this
  agent's parent).
- A bundled submission that rejects after the tracker was installed now
  disposes it on the error path instead of leaking a permanent listener.

* fix(agent-core-v2): keep steered prompts queued until their media intake finishes

Materializing a steered prompt's daemon-ref media awaits a file copy
during which the active turn may finish. Records are now spliced out of
the queue only after that copy completes, and when the turn is gone by
enqueue time they are restored to pending so startNext can launch them
as fresh prompts — their handles always launch or settle.

* fix(agent-core-v2): revalidate the queue and active turn after steer media intake

The daemon-ref copy yields, so settle/abort can consume selected records
and the active turn can rotate meanwhile. Only records still pending are
steered, and only into the turn that was active at entry; records that
vanish from the queue are left to their own launch path, and a missing
turn restores them to pending instead of splicing an unrelated tail
prompt. The intake/queue-preservation contract is documented in the
module header.

* fix(agent-core-v2): steer only the surviving records and keep their media truthful

- The steered content is rebuilt from the records that are still pending
  after the media intake, so an aborted or concurrently consumed record's
  text is never injected (or injected twice) alongside the surviving
  handles.
- The enqueue is wrapped so an activeTurnOnly rejection restores the
  records to pending (the loop throws instead of resolving a missing
  turn, which made the previous rollback unreachable).
- The merged origin now carries the union of every record's bundled
  skillActivations, and prompt.steered publishes the caller-only content,
  so the skill instructions reach the model with their metadata intact
  while the event projection stops leaking internal skill markdown.

* fix(agent-core-v2): harden steer rollback and register bundled prompt ids

* fix(agent-core-v2): strip bundled blocks from prompt.queued and reject partial steers

* fix(kap-server): update session metadata for bundled prompts routed to subagents

* fix(agent-core-v2): restart queue after raced steer rollback and prefix skill blocks in merged steer

* fix(agent-core-v2): block queue advancement during steer admission

* chore: drop the changeset for server-only protocol plumbing
2026-08-18 14:57:13 +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 feat(kap-server): accept bundled skill activations on the prompt submission route (#2982) 2026-08-18 14:57:13 +08:00
test feat(kap-server): accept bundled skill activations on the prompt submission route (#2982) 2026-08-18 14:57:13 +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).