Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/server-align.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ Where the route mirrors v1, the test is the regression guard for the schema-fide
- `pnpm -C packages/kap-server test` — server routes green (incl. any wire-schema guards).
- `pnpm -C packages/agent-core-v2 test` — native + Legacy Service tests green.
- `pnpm -C packages/agent-core-v2 run lint:domain` — a LegacyService is still inside the domain layers (edge adapter, L7); it must not pull business code into the edge or invert scope direction.
- `pnpm -C packages/server-e2e ...` when a v1 parity scenario exists.
- `pnpm -C packages/klient test` (optionally with `KIMI_SERVER_URL` for the live legacy suites) when a v1 parity scenario exists.

## Worked example — porting v1 `/sessions/:sid/prompts`

Expand Down
1 change: 0 additions & 1 deletion .changeset/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ All other workspace packages are private internal packages, are not published to
- `@moonshot-ai/kosong`
- `@moonshot-ai/migration-legacy`
- `@moonshot-ai/protocol`
- `@moonshot-ai/server-e2e`
- `@moonshot-ai/vis`
- `@moonshot-ai/vis-server`
- `@moonshot-ai/vis-web`
Expand Down
1 change: 0 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"@moonshot-ai/server-e2e",
"@moonshot-ai/vis",
"@moonshot-ai/vis-server",
"@moonshot-ai/vis-web"
Expand Down
6 changes: 6 additions & 0 deletions .changeset/kap-server-v2-model-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/kap-server": minor
"@moonshot-ai/kimi-code": patch
---

Expose the model registry and model catalog services on the server's v2 RPC surface so remote clients can manage model configuration and read the provider/model catalog.
5 changes: 5 additions & 0 deletions .changeset/klient-global-facade-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/klient": minor
---

Add `models`, `catalog`, and `hostFs` sections to the global facade for model configuration, the provider/model catalog, and host folder browsing, plus a `models.changed` event, `flags.enabledIds()`, and caching of the `env()` snapshot. `auth.refreshProviderModels()` is deprecated in favor of `catalog.refresh({ scope: 'oauth' })`.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- `packages/oauth`: Kimi OAuth and managed auth utilities.
- `packages/telemetry`: shared client-side telemetry infrastructure.
- `packages/kap-server`: the Kimi Code server, backed by the DI × Scope agent engine (`@moonshot-ai/agent-core-v2`). Exposes sessions over REST + WebSocket (`/api/v1` and the native `/api/v2` RPC surface); bootstrapped from `src/start.ts` and consumed by `apps/kimi-code`.
- `packages/server-e2e`: live e2e tests and scenarios against a running server (`KIMI_SERVER_URL`, default `http://127.0.0.1:58627`). See `packages/server-e2e/AGENTS.md`.
- `packages/klient`: the client SDK — a contract-driven facade over agent-core-v2 with aggregated `global.*` / `session(id).*` / `agent(id).*` methods, zod validation on every call, and klient-level typed event forwarding. Transport is chosen once at creation via subpath entry (`@moonshot-ai/klient/http|ipc|memory`); all three return the same `Klient`. The package also hosts the e2e suites: dual-backend session/agent suites (`test/e2e/dual/`, in-memory + in-process server), `/api/v2` wire tests (`test/e2e/v2/`), the legacy `/api/v1` live suites (`test/e2e/legacy/`), and the docker e2e runner (`pnpm --filter @moonshot-ai/klient docker:e2e`). See `packages/klient/AGENTS.md`.

## Environment Requirements

Expand Down
2 changes: 0 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
./packages/agent-core
./packages/agent-core-v2
./packages/kap-server
./packages/server-e2e
./packages/kaos
./packages/klient
./packages/kosong
Expand All @@ -91,7 +90,6 @@
"@moonshot-ai/agent-core"
"@moonshot-ai/agent-core-v2"
"@moonshot-ai/kap-server"
"@moonshot-ai/server-e2e"
"@moonshot-ai/kaos"
"@moonshot-ai/kosong"
"@moonshot-ai/migration-legacy"
Expand Down
4 changes: 4 additions & 0 deletions packages/kap-server/src/transport/channelRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
IFaultInjectionService,
IFlagService,
IHostFolderBrowser,
IModelCatalogService,
IModelService,
IOAuthService,
IPluginService,
IProviderService,
Expand Down Expand Up @@ -204,6 +206,8 @@ const EXPOSED_SERVICES: readonly ServiceIdentifier<unknown>[] = [
IWorkspaceRegistry,
IConfigService,
IProviderService,
IModelService,
IModelCatalogService,
IOAuthService,
IAuthSummaryService,
IFlagService,
Expand Down
83 changes: 83 additions & 0 deletions packages/klient/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# klient Agent Guide

Package-local rules for `packages/klient`.

## Architecture

The package is layered; keep the layers strict when changing code:

- **Facade** (`src/core/facade/`, `src/core/klient.ts`) — the only public API:
aggregated `global.*` / `session(id).*` / `session(id).agent(id).*` methods
and their `events.*` hubs. No engine service tokens, no `onDid*`/`onWill*`
names, and **no escape hatch to raw services** — do not reintroduce a
service locator (`core()`/`service()`/`makeProxy`).
- **Contract** (`src/contract/`) — zod input/output schemas for every wire
method plus event payload schemas. Schemas are hand-mirrored from
agent-core-v2 types and pinned by the compile-time parity assertions in
`test/contract-parity.ts`; when the engine types change, tsc fails here
first. `maybe()`/`noResult()` in `src/contract/helpers.ts` encode the HTTP
wire's `null`-vs-`undefined` semantics — use them for every
`X | undefined` / `void` result.
- **Transports** (`src/transports/{http,ipc,memory}`) — each implements the
`KlientChannel` SPI (`src/core/channel.ts`) and nothing else. http carries
events over a lazily opened WS; ipc reuses the WS frame shapes over a unix
socket and shares the in-process dispatcher with memory; memory JSON
round-trips every value so all three transports return byte-identical data.

The facade only covers services kap-server exposes over `/api/v2` **and** that
behave identically on all three transports (the in-process dispatcher mirrors
the server's scope resolution, including `main`-agent materialization via
`ensureMainAgent`). onWill/hook-style interception is not wire-exposable
(engine hooks are in-process `OrderedHookSlot`s); file upload and the
terminal surface are v1-only and live in the legacy suites.

## Testing

- One shared conformance suite (`test/helpers/conformance.ts`) runs unchanged
against every transport — one test file per transport under `test/`; the
http leg boots an in-process kap-server. Add new **global** facade coverage
there, not per-transport.
- Session/agent coverage lives in `test/e2e/dual/` (`test/helpers/dual.ts`):
every suite runs the exact same body against an in-memory engine AND an
in-process kap-server. Model-requiring suites declare
`{ requiresModel: true }` and skip unless `KIMI_E2E_MODEL` +
`KIMI_E2E_API_KEY` (optional `KIMI_E2E_BASE_URL` / `KIMI_E2E_PROTOCOL`)
are set; the model is seeded into each backend's temp home via
`klient.global.models.set` and agents bind it with
`agent.setModel(DUAL_MODEL_ID)`.
- `test/e2e/v2/` — `/api/v2` wire tests booting kap-server in-process.
- `test/e2e/legacy/` + `test/e2e/harness/` — the legacy `/api/v1` live
suites (moved from server-e2e). They skip unless `KIMI_SERVER_URL` points
at a running server and **must keep running unchanged**; the v1 surface
has no in-memory equivalent, so these stay http-only — do not try to
dual-run them.
- The retired `scenarios/` scripts were rewritten as suites: prompt /
approval / workspace / catalog / children / pending flows live in
`test/e2e/dual/`; image-upload and terminal (v1-only surfaces) live in
`test/e2e/legacy/`; refresh-replay was dropped as redundant with the
legacy test of the same name.

## Observability (inherited from server-e2e)

- Keep observability inside each e2e case; every live case prints structured,
case-scoped details (requests, envelopes, WS handshakes, terminal frames,
error envelopes) through the shared logger in `test/e2e/legacy/log.ts`,
not ad hoc `console.log`.
- Logs must stay visible for passing Vitest cases — write through stdout.
- When adding or changing an e2e case, update its observability at the same
time; do not add a scenario solely to print data an existing case should
already expose.

## Command reference

- `pnpm --filter @moonshot-ai/klient test` — all Vitest suites (unit +
conformance + e2e; live and model cases skip without their env).
- `KIMI_SERVER_URL=http://127.0.0.1:58627 pnpm --filter @moonshot-ai/klient test`
— include the live legacy/v2 cases against a running server.
- `KIMI_E2E_MODEL=... KIMI_E2E_API_KEY=... [KIMI_E2E_BASE_URL=...] pnpm --filter @moonshot-ai/klient exec vitest run test/e2e/dual`
— run the model-requiring dual suites against both backends.
- `pnpm --filter @moonshot-ai/klient docker:e2e` — docker e2e; the run
derives its runner name/namespace from the current workspace to avoid
cross-workspace conflicts.
- `pnpm --filter @moonshot-ai/klient typecheck` / `pnpm smoke` (real-server
smoke; see `examples/smoke.ts`).
4 changes: 2 additions & 2 deletions packages/server-e2e/Dockerfile → packages/klient/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.7
#
# server-e2e docker-run image.
# klient e2e docker-run image.
#
# This image layers server-e2e defaults on top of the repository server dev
# image. Source code and node_modules are still provided by bind mounts from
Expand All @@ -22,6 +22,6 @@ ENV KIMI_CODE_HOME=/data/docker-e2e/kimi-code-home \
npm_config_store_dir=/workspace/kimi-code/node_modules/.pnpm-store \
npm_config_package_import_method=copy

WORKDIR /workspace/kimi-code/packages/server-e2e
WORKDIR /workspace/kimi-code/packages/klient

CMD ["bash"]
163 changes: 93 additions & 70 deletions packages/klient/README.md
Original file line number Diff line number Diff line change
@@ -1,89 +1,112 @@
# @moonshot-ai/klient

Client SDK that reuses `agent-core-v2` service interfaces and fulfills them over
the `/api/v2` HTTP channel. It follows the VS Code model: a channel is bound to
**one Service** (the URL carries the scope + the Service's decorator id) and
method calls are forwarded **verbatim** to the server's reflection dispatcher —
no per-method allowlist, no `resource:action`, no renaming. The shared interface
is the whole contract.
Contract-driven client SDK for the agent-core-v2 engine. One facade, three
transports — you pick the transport **once** at creation; everything after
that is byte-identical:

```ts
import { Klient, SessionIndexClient, HttpChannel } from '@moonshot-ai/klient';
import { ISessionIndex } from '@moonshot-ai/agent-core-v2/app/sessionIndex/sessionIndex';

const client = new Klient({ url: 'http://127.0.0.1:58627' });

// Generic typed proxy: the v2 service token carries both the type and the
// channel name (`String(ISessionIndex)` === 'sessionIndex').
const sessions = await client.core(ISessionIndex).list({});
const meta = await client.session('s1').service(ISessionMetadata).read();

// Explicit, fully-typed implementation of a single interface. The channel is
// bound to the Service's scope URL.
const index: ISessionIndex = new SessionIndexClient(
new HttpChannel({ baseUrl: 'http://127.0.0.1:58627/api/v2/sessionIndex' }),
);
const page = await index.list({ workspaceId: 'w1' });
```
import { createKlient } from '@moonshot-ai/klient/http'; // or '/ipc', '/memory'

Service interfaces and tokens are imported directly from `agent-core-v2` leaf
subpaths; the channel and proxy live in this package.
const klient = createKlient({ url: 'http://127.0.0.1:58627', token });

## WebSocket transport (calls + events)
const env = await klient.global.env();
const sessions = await klient.global.sessions.list({ limit: 20 });

`Klient#ws()` returns a lazily-created `WsKlient` over the persistent
`/api/v2/ws` socket: the same scope entries and typed proxies (one socket
multiplexes every `call`), plus `listen(event, handler)` on each scope for the
server's event streams — core `events`, session `interactions` /
`interactions:resolved`, agent `events`:
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.' }] });

```ts
const ws = client.ws();
const sub = ws.session('s1').agent('main').listen('events', (event) => {
console.log('agent event', event);
});
const pending = await ws.session('s1').service(ISessionApprovalService).listPending();
sub.dispose();
ws.close();
await klient.close();
```

The socket answers heartbeats, applies per-call timeouts, and reconnects
automatically after an unexpected close (active `listen`s are re-subscribed;
in-flight calls reject). The bearer token rides the
`kimi-code.bearer.<token>` subprotocol, so the transport works unchanged in
browsers.

## Real-server smoke checks
## Architecture

Run the transport smoke against a real server (the model phase is opt-in). It
creates and archives a fixture session, and therefore touches the selected
workspace's persisted metadata:

```sh
KIMI_SERVER_URL=http://127.0.0.1:58627 \
KIMI_SERVER_TOKEN=YOUR_SERVER_TOKEN \
pnpm smoke

KIMI_SMOKE_MODEL=YOUR_MODEL pnpm smoke
```
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
http │ ipc │ memory
```

The history smoke checks persisted sessions before warming one, including the
cold-session regression where an indexed session is unavailable through the v2
session scope. It sends no explicit mutation request. When `KIMI_SMOKE_MARKER`
is set, the v1 message read resumes the session and may persist server-side
legacy metadata migrations:
- **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.
- **Events** — `klient.events.on(...)` for the global bus
(`config.changed`, `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/http` | `{ url, token?, fetch?, WebSocketImpl? }` | lazily opened WS, transparent |
| `@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
match the networked transports byte-for-byte. The IPC host ships with the
transport: `serveKlientIpc({ scope, socketPath })`.

The same conformance suite runs against all three transports in this
package's tests (`test/helpers/conformance.ts` — one test file per transport;
the http leg boots an in-process kap-server).

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

- `test/e2e/dual/` — session/agent suites that run the **exact same body**
against an in-memory engine and an in-process kap-server
(`test/helpers/dual.ts`). Model-requiring suites skip unless
`KIMI_E2E_MODEL` + `KIMI_E2E_API_KEY` (optional `KIMI_E2E_BASE_URL`,
`KIMI_E2E_PROTOCOL`) are set; the model is seeded into each backend's temp
home through the facade itself.
- `test/e2e/v2/` — `/api/v2` wire tests booting kap-server in-process.
- `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 http-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 `OrderedHookSlot`s and not wire-exposable), file
upload (v1 multipart REST only), and the terminal surface (v1 REST + WS
only).

## Real-server smoke check

```sh
KIMI_SERVER_URL=http://127.0.0.1:58627 \
KIMI_SERVER_TOKEN=YOUR_SERVER_TOKEN \
KIMI_SMOKE_EXPECT_SESSION_ID=YOUR_SESSION_ID \
KIMI_SMOKE_MARKER=YOUR_MARKER \
KIMI_SMOKE_REQUIRE_HISTORY=1 \
pnpm smoke:history
pnpm -C packages/klient smoke
```

`KIMI_SMOKE_EXPECT_CWD` can select a session by working directory instead of
`KIMI_SMOKE_EXPECT_SESSION_ID`. The transport smoke creates its fixture in the
first registered workspace; set `KIMI_SMOKE_CWD` when a different server-local
folder is required. Omit `KIMI_SERVER_TOKEN` only for a server started with
authentication bypassed.
Omit `KIMI_SERVER_TOKEN` only for a server started with authentication
bypassed. `examples/basic.ts` is a shorter narrated tour.
Loading
Loading