* 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 |
||
|---|---|---|
| .. | ||
| 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).