kimi-code/AGENTS.md
Haozhe 5ae60fa673
feat(transcript): add unified transcript layer, drop the /api/v2 RPC surface (#1888)
* feat(transcript): add unified transcript layer and v1 surface

- add packages/transcript: agent-granular L1 store, idempotent L2 ops,
  off/turn/block/delta L3 granularity, L4 view registry, and turn-cursor
  pagination; sole owner of all transcript wire types
- kap-server: engine-event-driven TranscriptService with history backfill,
  GET /sessions/{id}/transcript, and transcript.ops WS deltas with
  per-connection granularity control
- kimi-inspect: render ChatView from the transcript surface (REST pages +
  delta-only WS) instead of context memory
- sync flake.nix workspace lists and document packages/transcript and
  packages/server-e2e in AGENTS.md

* fix(transcript): project live prompts and anchor backfilled items

- agent-core-v2: carry the extracted prompt text on turn.started so the
  transcript projector can render the user input at turn open (the context
  append with the same text is not a bus event and lands later)
- kap-server: keep the prompt through turn.ended; anchor backfilled
  markers/taskrefs to their following snapshot turn so replaying history
  after live turns arrived keeps the historical order
- transcript: add an optional beforeTurn placement anchor to
  marker.upsert / taskref.upsert; anchored inserts land before the first
  turn at or past the anchor instead of appending blindly

Addresses review feedback on #1888.

* fix(transcript): adopt backfilled stream frames and group goal turns

- kap-server: on mid-stream attach, adopt the backfill-seeded stream frame
  (id + offset) instead of opening an empty one whose upsert clobbered the
  seeded text and whose offset-0 appends could not land
- transcript: group a turn-opening system_trigger (goal continuation) into
  its own turn so cold rebuilds keep the turn boundary and stay
  ordinal-aligned with the engine's live turn numbering

Addresses review feedback on #1888.

* fix(transcript): drop stale live stores on close and heal after turns

- drop a session's live transcript store when the session closes or archives
  (lifecycle events plus a re-check on the cached-entry path) so reads fall
  back to the cold rebuild instead of serving a stale store
- re-read an ended turn from the persisted wire records (debounced per
  agent) and merge it back live-first: headers keep live state/timestamps
  while origin/prompt recover from disk, and truncated text/thinking frames
  from a mid-turn attach are restored only when the persisted text is
  longer — a fresh live frame or a lagging flush is never reverted

Addresses review feedback on #1888.

* fix(transcript): adopt seeded tool frames and route subagent questions

- kap-server: adopt the backfill-seeded tool frame when a tool.result
  arrives for a call that started before the projector attached, so the
  output lands instead of being dropped (the producer-store lookups now
  ride one projector options object)
- agent-core-v2: record the owning agent on question interactions
  (ISessionQuestionService.request gains an agentId option, passed by
  AskUserQuestionTool from its agent scope) so a subagent's questions
  route to its own transcript and WS events instead of 'main'

Addresses review feedback on #1888.

* fix(transcript): adopt parent frames, namespace live markers, redact resets

- kap-server: fall back to store adoption when subagent.spawned links a
  parent tool call that started before the projector attached, so the
  agentRefs link is not lost on mid-bind attaches
- kap-server: id live markers in their own namespace (live-mN) — the cold
  rebuild numbers its markers m1... too, and a colliding id made the
  store's upsert replace the historical marker instead of appending
- transcript/kap-server: redact transcript.reset snapshots to the
  subscriber's grade (below 'block' the step/frame detail is stripped), so
  a 'turn'-grade subscription no longer receives full content on resets

Addresses review feedback on #1888.

* fix(transcript): fold blocked turns into failed, drop inline v2 comment

- kap-server: map turn.ended reason 'blocked' to the 'failed' transcript
  state, matching the engine's TurnEndReason wire contract and the v1
  mapTurnReason folding (it was presented as a user cancellation)
- agent-core-v2: move the question agentId rationale into the ask-user
  file header — package rules keep comments in the top-of-file block only

Addresses review feedback on #1888.

* fix(transcript): keep roster descriptors and resolved-event agents

- kap-server: keep the metadata-seeded roster descriptor (parentAgentId /
  label) when an on-demand history backfill lands its roster entry, instead
  of downgrading it to { agentId, type }
- kap-server: remember each interaction's owning agent and stamp it on the
  resolved question/approval v1 events — they were hard-coded to 'main', so
  an agent-filtered subscriber saw a subagent's question open but never
  close

Addresses review feedback on #1888.

* fix(transcript): defer pending seeding, skip ghost roster entries

- kap-server: announce interactions pending at bind time only after the
  initial backfill (new TranscriptBinding.seedPendingInteractions), and
  adopt the seeded tool frame on the request/resolve paths, so a
  pre-existing approval lands next to its backfilled tool call and keeps
  the approvalId back-link
- kap-server: skip the roster entry when a probed agent id has neither a
  roster presence nor persisted content (agent_id=nope no longer conjures
  a ghost subagent)
- agent-core-v2: fold the turnEvents import rationale into the loop file
  header (package rule: comments live in the top-of-file block only)

Addresses review feedback on #1888.

* fix(transcript): reject hostile agent ids, honor the agent allowlist

- transcript/kap-server: validate agent ids as plain names (no separators,
  no dot segments) at the REST query layer and again before the id is
  joined into the wire-records path — an authenticated client could
  otherwise read a wire.jsonl outside the agents directory
- kap-server: compose the legacy v1 agent allowlist with transcript
  grades on every fan-out path (initial resets, per-ops fan-out,
  roster-driven resets), so a filtered connection no longer receives
  other agents' transcript frames

Addresses review feedback on #1888.

* fix(transcript): settle foreground shell tasks on shell.completed

- agent-core-v2: emit a transient shell.completed event when a foreground
  `!` command settles (detached runs keep reporting through the task
  lifecycle) — the generic task.terminated never fires for foreground
  tasks, so their transcript cards were stuck at 'running'
- kap-server: map shell.completed to the terminal transcript task state
  (completed/failed) and classify the event as volatile on both v1
  durability gates, like its shell.* siblings

Addresses review feedback on #1888.

* fix(transcript): replay early resolves, reset on a widened filter

- kap-server: register bind-time pending interactions without frames so a
  resolve arriving before the post-backfill seed still routes, then replay
  it at seed time — request and resolve land together with the approvalId
  back-link, instead of the interaction vanishing entirely
- kap-server: treat an agent newly admitted by a broadened legacy agent
  filter as owed a transcript.reset even when its grade transition is a
  no-op (delta → delta) — its ops were suppressed so far, so it has no
  baseline otherwise

Addresses review feedback on #1888.

* fix(transcript): keep materialized transcripts on agent disposal

- kap-server: only the projector dies with the agent scope — the
  materialized transcript and roster entry now survive disposal (the
  roster mirrors session metadata, which keeps completed agents).
  Dropping them lost already-served history for good: the backfill cache
  dedupes per agent, so the next read rebuilt an empty shell instead of
  replaying the persisted records

Addresses review feedback on #1888.

* fix(transcript): anchor refreshes by oldest turn, group slash turns

- kimi-inspect: re-cover the previously loaded window after a refresh by
  paging until the previous OLDEST turn is loaded again (extracted as
  recoverLoadedWindow) — a count-based stop silently dropped the window's
  head once new turns shifted the server window
- transcript: group user-slash skill/plugin activations into their own
  turn (marker included), mirroring the engine's isRealUserPrompt — their
  assistant output no longer folds into the previous turn on cold rebuilds;
  other triggers stay marker-only

Addresses review feedback on #1888.

* fix(transcript): compare all tool fields, overlay in-flight backfills

- transcript: include toolCallId/name/view/input in the tool frame
  equality check — an upsert correcting only those was dropped as a no-op,
  leaving stale tool metadata on clients
- kap-server: overlay the loop's active turn as 'running' after a backfill
  (cold grouping marks every rebuilt turn completed, so a live turn showed
  as finished until it ended); snapshot data supplies origin/prompt, and a
  projector-owned running header is never downgraded

Addresses review feedback on #1888.

* fix(transcript): gate ops before the seed, re-assert running headers

- kap-server: gate the transcript ops fan-out (and roster-driven resets) on
  a per-connection seeded flag set only after the baseline reset has
  landed — a subscriber joining mid-stream no longer receives deltas
  against an empty baseline
- kap-server: always re-assert the loop's active turn as 'running' after
  the snapshot ops in a backfill (skipping the overlay when a live running
  header existed let the snapshot's cold 'completed' header downgrade it);
  live header fields win over the snapshot's

Addresses review feedback on #1888.

* docs(agent-core-v2): fold turnEvents notes into the file header

Move the turn.started prompt rationale from field/function TSDoc into the
module header — package rules keep comments in the top-of-file block only.

Addresses review feedback on #1888.

* fix(transcript): emit shell failure output, dispose per-agent listeners

- agent-core-v2: emit the synthesized failure text as a final shell.output
  chunk before shell.completed (it was never streamed, so failed
  foreground commands showed empty output in transcript tasks until a
  full rebuild)
- kap-server: track each agent's bus subscription per agent and dispose it
  in onDidDispose — the listener captures the projector, so a disposed
  agent no longer keeps projecting late events into the store

Addresses review feedback on #1888.

* fix(transcript): route shell events by task id, tidy question docs

- agent-core-v2: keep the commandId → foreground-task-id mapping and carry
  taskId on shell.output / shell.completed, so consumers attaching
  mid-command (having missed shell.started) can still route output and the
  terminal state
- kap-server: fall back to the event's taskId in the shell output/completed
  projectors and seed the shell task before the first chunk, so output is
  preserved and the terminal upsert cannot clobber it with an empty tail
- agent-core-v2: fold the question agentId note into the question.ts file
  header (package rule: comments live in the top-of-file block only)

Addresses review feedback on #1888.

* fix(transcript): tighten agent id validation, match kinds in heals

- transcript: constrain agent ids to a filename-safe shape
  ([A-Za-z0-9._-], <=128 chars) — NUL-containing or overlong ids made the
  wire-records read throw unhandled errors (500) instead of failing
  validation
- kap-server: require the live frame's kind to match before the post-turn
  heal's length shortcut — a kind-mismatched frame (the projector guessed
  the stream kind wrong mid-turn) is now replaced by the persisted one
  instead of being skipped

Addresses review feedback on #1888.

* fix(transcript): heal missed tool results, seed pendings per agent

- kap-server: re-emit tool frames in the post-turn heal when the live step
  lacks the frame or the live frame lacks the outcome the persisted one
  carries (a tool.result dropped in the attach race is otherwise
  unrecoverable); live-only extras (display / agentRefs / approvalId) are
  preserved, and frames with a live outcome stay untouched
- kap-server: scope seedPendingInteractions by agent — the initial seed
  after backfillMain covers main-owned pendings, and each subagent's
  pendings seed after its own on-demand backfill, so placement and the
  approvalId back-link find the persisted tool frames

Addresses review feedback on #1888.

* fix(protocol): register shell.completed on the v1 event surface

- packages/protocol: add ShellCompletedEvent (plus optional taskId on
  shell.output / shell.completed and prompt on turn.started) to the event
  interfaces, zod schemas, the agent event union, and the volatile list —
  schema-validating consumers previously rejected the forwarded
  shell.completed frames outright
- kap-server: mirror the same fields in the v1 events-zod module

Addresses review feedback on #1888.

* test(node-sdk): cover shell.completed in the exhaustive event switch

The SDK's session-event type test asserts exhaustiveness with assertNever;
register the new event there (CI typecheck caught it).

Addresses review feedback on #1888.

* fix(transcript): source cold-session rosters from session metadata

- kap-server: add TranscriptService.readColdRoster (persisted state.json →
  descriptors, mapped like the live seeding) and use it for the cold
  transcript path — the requested agent id is only appended when it has
  content (or is main), so an empty probe (agent_id=nope) no longer
  fabricates a ghost roster entry, matching the live path

Addresses review feedback on #1888.

* fix(transcript): emit taskrefs when seeding missed shell commands

- kap-server: the mid-command-attach seeding in onShellOutput now emits
  the matching taskref.upsert (exactly like onShellStarted), and
  onShellCompleted emits one when the whole command was missed — the task
  no longer exists only in the global map with no timeline item to render

Addresses review feedback on #1888.

* fix(transcript): defer unseeded live pendings, keep cold tools running

- kap-server: pendings created before their owning agent's seed has run
  now defer into the same unseeded queue as bind-time ones (tracked per
  agent) — announcing them during the backfill window misplaced them into
  a synthetic step with no later repair
- transcript: cold grouping initializes tool frames as 'running' and lets
  the tool-message branch transition them to done/error — an approval-
  gated or still-executing tool no longer shows as completed on rebuilds

Addresses review feedback on #1888.

* fix(transcript): seed live-created agents, merge backfills live-first

- kap-server: agents created after binding are marked seeded immediately —
  their projector covers every event from creation on, so their pendings
  announce without waiting for an explicit history read (which previously
  left live subagent approvals/questions stuck in the unseeded queue)
- kap-server: the initial backfill merges turns live-first via
  healTurnOps (snapshotToOps gains a turn-mapper parameter) — live frame
  fields landed during the disk read (display/approvalId, longer text)
  are no longer replaced by the staler persisted version

Addresses review feedback on #1888.

* fix(transcript): count pages in turn segments, not head units

- transcript: the leading non-turn unit no longer consumes a turn slot —
  pages are counted in turn segments and the head unit rides only with the
  page reaching the first turn. A timeline with a head marker and exactly
  pageSize turns used to drop the marker from the newest page and
  hallucinate an older marker-only page (has_more: true with no older
  turns)

Addresses review feedback on #1888.

* fix(transcript): send baseline resets after cursor replay

- kap-server: broadcaster.subscribe gains deferTranscriptReset (recording
  prev grades/filter per target) plus flushTranscriptSeed; the v1
  connection defers the transcript baseline on cursor-carrying
  (re)subscribes and flushes it after replay — a reconnecting client no
  longer sees the reset's current seq ahead of the replayed lower-seq
  backlog

Addresses review feedback on #1888.

* fix(transcript): gate the ops fan-out only when a reset is coming

- kap-server: willSendTranscriptReset decides upfront whether any reset
  will be sent (grade upgrade or widened legacy filter); a same-grades
  resubscribe no longer un-seeds the target, so ops emitted mid-resubscribe
  keep flowing instead of being silently dropped by the fan-out gate

Addresses review feedback on #1888.

* fix(transcript): seed subscribers even when no reset is owed

- kap-server: a no-reset subscription (e.g. a client subscribing to a
  fresh session with an empty roster) now still marks the target seeded
  after subscribeTranscript completes — roster resets and ops would
  otherwise stay gated forever once agents appear

Addresses review feedback on #1888.

* fix(transcript): guard mismatched appends, expose prompt via klient

- transcript: appendAtOffset now treats an overlapping chunk whose head
  does not match the local tail as a gap (diverged stream) instead of
  silently rewriting from the offset and dropping local content
- klient: add the optional prompt field to the turn.started event schema
  so SDK listeners receive it instead of zod stripping it

Addresses review feedback on #1888.

* fix(transcript): open turns for subagent run prompts in cold grouping

- transcript: add the subagent system trigger to the turn-opening set —
  a subagent's run prompt (persisted as system_trigger/'subagent') always
  launches a new engine turn, so resumed subagent histories no longer fold
  the response into the previous turn or lose the prompt

Addresses review feedback on #1888.

* fix(transcript): guard bus subscriptions independently of projectors

- kap-server: subscribeAgent now guards on a dedicated subscribedAgents
  set instead of projector existence — a projector seeded before its
  agent's handle exists (e.g. during an on-demand backfill) no longer
  blocks the bus subscription, so the agent's live events keep flowing

Addresses review feedback on #1888.

* fix(kimi-inspect): reconcile the transcript on every socket open

- apps/kimi-inspect: TranscriptWs now reports onReconnected on the FIRST
  successful open too, not only on re-established ones — ops emitted
  between the REST page load and the subscription (a delayed or failed
  first connection) were previously lost onto a stale store; the consumer's
  refresh guard drops the no-op call while the initial load is in flight

Addresses review feedback on #1888.

* fix(transcript): derive the active step, dedupe tool error rendering

- kap-server: the projector gains a stepOrdinal lookup backed by the
  engine's activity view (resolved lazily through the agent lifecycle), so
  deltas after a late attach at step >= 2 land in the real active step
  instead of a synthesized t<N>.1
- apps/kimi-inspect: render a tool frame's error only when it differs from
  its output — onToolResult sets both to the same string for failed tools,
  which drew the failure twice in red

Addresses review feedback on #1888.

* fix(kimi-inspect): reconcile the transcript on the subscribe ack

- apps/kimi-inspect: TranscriptWs now fires onReconnected when the
  subscribe ack for its client_hello arrives instead of at socket open —
  the server attaches the transcript stream only after processing
  client_hello, so a refresh fired at open could finish before the
  subscription was active and still miss the ops in between

Addresses review feedback on #1888.

* fix(kimi-inspect): coalesce concurrent transcript refreshes instead of dropping

A subscribe ack landing while the initial REST load was still in flight
hit the `if (refreshing) return` guard, so ops emitted between the REST
page snapshot and the WS subscribe were neither in the page nor
delivered over the socket. Replace the drop guard with a coalesced
runner: at most one refresh in flight, and triggers during a run are
collapsed into exactly one follow-up run after it settles.

* fix(kap-server): force the transcript baseline after cursor-based replay

A cursor re-subscribe at unchanged grades deferred its baseline and then
compared against the previous grades on flush, so no reset was sent —
while volatile ops fanned out during the deferral had been dropped,
leaving the client with a permanent gap. flushTranscriptSeed now always
seeds a full baseline (previous grades no longer tracked in the deferred
record), and a regression test covers the same-grade cursor resubscribe.

Also drop the inline comments added to shellCommandService.ts — the
agent-core-v2 convention keeps commentary in the top-of-file block; the
context moved there.

* fix(kap-server): harden transcript seeding against stale and wildcard subs

Two subscribeTranscript gaps found in review:

- Re-read the target's subscription after the history awaits: subscribe
  work runs asynchronously, so an overlapping downgrade/unsubscribe used
  to be answered with resets computed from the stale spec. The reset
  loop now uses the latest grades/filter from state.targets and bails
  when the target is gone or no longer graded.
- Backfill roster agents admitted via the wildcard grade, not just
  explicitly named ones: a historical subagent seeded into the roster
  from session metadata had no materialized AgentTranscript, so
  wildcard subscribers silently never received its baseline reset.

Adds regression tests for the wildcard backfill, the mid-seed
downgrade, and the mid-seed unsubscribe (all three fail without the
fix); makeCore now accepts persisted agent metadata for roster seeding.

* fix(transcript): let meta.merge clear mode badges on mode exit

`agent.status.updated` with `planMode: false` / `swarmMode: false` was
dropped by the transcript projector because `meta.merge` could only set
mode badges, never clear them — clients kept rendering an exited mode
until the next full reset. The merge wire shape now accepts `null` per
mode key (set = object, clear = null, absent = keep): the reducer
deletes the key and normalizes an empty `modes` away, the zod schema
validates the nullable form, and the projector emits the clearing op
for exit events.

* fix(agent-core-v2): keep system-turn steering text out of turn prompts

`turn.started.prompt` was populated from the turn input for every
origin, so system-triggered turns (goal continuation, subagent run,
cron) exposed their internal steering text to live transcript
consumers; the cold rebuild mirrored the same leak when grouping
persisted history. The loop now populates the prompt only for
displayable user origins (user input, or a user-slash skill/plugin
activation) via the new isDisplayablePromptOrigin gate, and the cold
grouping opens hidden-origin turns promptless. Turns still open
normally — only the prompt text is withheld.

* fix(kap-server): reattach the transcript fan-out after a session reload

When the engine session closed or archived, TranscriptService dropped
the live store together with its ops listener set, but the
broadcaster's SessionState kept its transcriptStream — so
ensureTranscriptStream returned early for a later subscribe on the
resumed session, delivering a fresh reset but never the live
transcript.ops. The stream is now pinned to its TranscriptStore
instance and the fan-out re-registers whenever a rebuilt store shows
up. Adds a regression test that drops the service entry mid-stream and
asserts ops keep flowing after resubscribe (fails without the fix).

* fix(transcript): map legacy background_task origins in cold rebuilds

Legacy/v1 sessions persist background-task notifications with
origin.kind === 'background_task' (the live mapper already handles that
spelling), but the cold grouping only mapped 'task' — after a restart
those turns fell through to { kind: 'other' } and lost their taskId, so
the transcript could no longer associate the notification turn with its
background task. Both spellings now share the task-origin branch.

* fix(kap-server): project no-taskId shell failures into the transcript

A foreground `!` command that failed before onForegroundTaskStart ran
(Bash validation/spawn/registerTask errors) published shell.output /
shell.completed with taskId undefined, and the projector's guard
dropped them — the live transcript lost the stderr and the terminal
state of a command that did run. Shell events now resolve their task as
the id learned at shell.started, else the event's own taskId, else a
synthetic per-command id (shell-<commandId>), so early failures land
like any other shell task.

* refactor: drop the /api/v2 RPC surface and the klient http transport

- kap-server: remove the /api/v2 REST routes and /api/v2/ws socket (registerRpcRoutes renamed to serviceDispatcherRoutes; transport/ws/{eventMap,registerWs,wsClient,wsConnection,wsProtocol} deleted). /api/v1/debug/* is now the only RPC surface — a reflection dispatcher over the entire scoped DI registry with no whitelist — and /api/v1/ws the only WS endpoint
- klient: drop the http transport (transports/http/*, transports/ws/wsSocket.ts) and the kap-server devDependency; transports reduce to the ipc|memory subpath entries, and the dual/v2 e2e suites go with them
- kimi-inspect: target /api/v1/debug only with no fallback, replace the Service-event push channel (wsChannel/wsSocket) with on-demand fetch plus 15 s polling, and show a blocking "Debug surface unavailable" screen on connection failure
- transcript: add global attachment/interaction/todo entities (model, ops, wire schema) and project them from engine events in kap-server's coreEventMap

* fix(kimi-inspect): drop the unused TranscriptTodo import
2026-07-20 15:33:05 +08:00

12 KiB
Raw Permalink Blame History

Repository-level Agent Guide

Reply in the same language as the user.

This is a TypeScript monorepo built for agent-assisted development. Keep the root AGENTS.md limited to hot-path rules: the project map, hard constraints, and workflow requirements — things every task needs to know.

Working Principles

  • Think from first principles. Start from real requirements, code facts, and verification results; if the goal is unclear, discuss it with the user first.
  • Treat code, not documentation, as the source of truth. Unless the user explicitly says otherwise, do not read ordinary Markdown just to understand the implementation.
  • Before making code changes, read the relevant code and the most recent constraints, and follow the nearest AGENTS.md in the directory tree.
  • Keep changes focused. Do not slip in unrelated refactors along the way.
  • When committing, do not add any co-author attribution, and do not reveal the identity of the agent in commit messages, PR descriptions, or any explanatory text.

Project Map

  • apps/kimi-code: the CLI / TUI application. It consumes core capabilities through @moonshot-ai/kimi-code-sdk and must not depend directly on @moonshot-ai/agent-core. When writing or modifying its terminal UI, use the write-tui skill (.agents/skills/write-tui/SKILL.md).
  • apps/kimi-web: the browser web UI, a peer to the TUI. Vue 3 + Vite + vue-i18n; talks to the server over REST + WebSocket under /api/v1. It must not depend on @moonshot-ai/agent-core (wire types are re-implemented locally). Debug against the two engines via the root pnpm dev:v1 / pnpm dev:v2 backend scripts — the dev Sidebar shows the active backend and switches it at runtime. See apps/kimi-web/AGENTS.md.
  • apps/vis, apps/vis/server, apps/vis/web: visual debugging tools for sessions and replays.
  • apps/kimi-inspect: web inspector for the kap-server /api/v1/debug RPC surface — workspace/session browser, per-session chat, and Service panels (data + trigger buttons) for the Session and Agent scopes. Built on its own old-klient-style channel layer (src/channel/: the VS Code ProxyChannel model — service-bound IChannel, HTTP ProxyChannel for calls routed to /api/v1/debug), typed by agent-core-v2 Service interfaces; GET /api/v1/debug/channels loads the whole wire protocol 1:1 (every scoped Service, no whitelist). There is no Service-event push channel: panels fetch/refresh on demand (Sidebar polls react-query on a 15 s interval), and a connection failure shows a blocking "Debug surface unavailable" screen instead of falling back anywhere. The Vite dev server proxies /api to a running kap-server (KIMI_SERVER_URL, default http://127.0.0.1:58627) and exposes GET /__inspect/servers (vite/serverDiscovery.ts), which scans the local kap-server instance registry (~/.kimi-code/server/instances + legacy lock) and the home token so the app can zero-config auto-connect and switch servers from the header dropdown at runtime. The per-session chat (src/components/ChatView.tsx) renders turn-granularly from the transcript surface instead of context memory: full state is read from GET /api/v1/sessions/{id}/transcript (initial load = newest page, refreshes re-read from the tail backwards, "load earlier" pages with before_turn), while /api/v1/ws is a delta-only channel (transcript.ops, grade delta; transcript.reset is ignored). Convergence reuses @moonshot-ai/transcript's L2 reducer (src/transcript/: REST/WS clients + store; the data model and reducer come from the package, nothing is re-implemented locally).
  • packages/agent-core: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, the in-process DI service layer (src/services/), and other core capabilities.
  • packages/node-sdk: the public TypeScript SDK and harness.
  • packages/kosong: the LLM / provider abstraction layer.
  • packages/kaos: the execution environment and file/process abstractions.
  • packages/oauth: Kimi OAuth and managed auth utilities.
  • packages/telemetry: shared client-side telemetry infrastructure.
  • packages/transcript: the isomorphic transcript rendering data layer — agent-granular L1 store, idempotent L2 operations, off/turn/block/delta L3 subscription granularity, framework-free L4 view registry, and turn-cursor pagination. Pure TypeScript (browser-safe, no engine imports) and the sole owner of all transcript wire types; consumed by packages/kap-server (engine events → transcript, REST + WS surface; live stores backfill history from the persisted per-agent wire records — main on first attach, any agent on demand, cold sessions rebuild any agent — with 0-based turn ordinals matching the engine's).
  • 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 + /api/v1/ws); bootstrapped from src/start.ts and consumed by apps/kimi-code. The RPC surface is /api/v1/debug/* — a reflection dispatcher over the ENTIRE scoped DI registry (every Service callable, no whitelist; src/transport/registerDebugRoutes.ts + serviceDispatcherRoutes.ts), mounted only with --debug-endpoints on a loopback bind and gated by the global bearer auth; repo dev scripts pass the flag.
  • 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/ipc|memory); both return the same Klient. The package also hosts the e2e suites: 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.
  • 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.

Environment Requirements

  • Node.js: >=24.15.0 (from the root package.json engines; .nvmrc is 24.15.0, used by nvm / fnm / mise to pick the minimum recommended version).
  • pnpm: 10.33.0 (from the root package.json packageManager).
  • pnpm install will fail when the Node version is not satisfied, because .npmrc sets engine-strict=true.

Monorepo Workspace Maintenance

  • pnpm-workspace.yaml is the source of truth for workspace membership, but flake.nix also contains hardcoded workspacePaths and workspaceNames lists.
  • Whenever you add or remove a workspace package, you MUST update both pnpm-workspace.yaml and flake.nix — for every package, including leaf / test / e2e packages that nothing depends on.
    • pnpm-workspace.yaml uses globs (packages/*, apps/*), so most packages land there automatically; flake.nix is fully manual and is where omissions happen.
    • Missing a path in flake.nix's workspacePaths will silently drop files from the Nix build's src fileset.
    • Missing a name in flake.nix's workspaceNames will break pnpmConfigHook because dependencies for that workspace will not be fetched.
  • The automated "Check flake.nix workspace sync" (scripts/check-nix-workspace.mjs) only validates the transitive dependency closure of @moonshot-ai/kimi-code. A leaf package outside that closure (e.g. an e2e package nobody imports) slips through even when it is missing from flake.nix. A green check is therefore NOT proof that flake.nix is fully in sync — keep it updated by hand on every add/remove, do not rely on the check to catch omissions.

General Coding Rules

  • For optional object properties, pass undefined directly instead of using conditional spread.
    • YES: { user }
    • NO: { ...(user ? { user } : undefined) }
  • Optional object properties do not need to additionally allow undefined in the type.
    • YES: interface Options { user?: User }
    • NO: interface Options { user?: User | undefined }
  • Internal methods with only a single parameter should not be turned into options objects just for stylistic uniformity.
  • Except for a package's index.ts, other index.ts files should prefer export * from './module';.
  • The Agent class in packages/agent-core/src/agent must be usable on its own. The constructor must not force the caller to create a Session instance, nor require an agentId or session. It may accept an optional sessionId as a request-config hint — for example mapped to the provider's prompt_cache_key — but the instance must not hold sessionId, and must not depend on the Session lifecycle, metadata, or parent/child relationship logic.
  • Do not add too many new test files. Prefer adding tests to the existing test file of the corresponding component or module.
  • When a test fails because of a user modification, default to fixing the test first; do not change the implementation to satisfy an old test unless the implementation truly has a bug.
  • Do not sacrifice code quality for external compatibility unless the user explicitly asks for it. Breaking changes go through changesets and a major bump, gated by the rule below.

Experimental Features

  • Gate a not-yet-public feature behind an experimental flag. Add the flag to the registry at packages/agent-core/src/flags/registry.ts, then check it with flags.enabled('my-feature'). Flags are env-driven and default off: KIMI_CODE_EXPERIMENTAL_<NAME> toggles one, KIMI_CODE_EXPERIMENTAL_FLAG enables all. Release by flipping the entry's default to true.

Where to Update Instructions

  • Hard rules that affect almost every task: update the root AGENTS.md.
  • Rules that only affect a specific directory: update the nearest sub-directory AGENTS.md.
  • Keep instruction updates focused and supported by code facts.

Workflow Requirements

  • Prefer rg / rg --files when reading code.
  • When designing changes, follow existing boundaries and local patterns first.
  • In public text and test data, replace real internal identifiers with neutral placeholders such as example.com, example.test, and YOUR_API_KEY. Before opening a PR, ask a read-only agent to audit the diff for context-specific internal identifiers.
  • When creating a PR, the PR title must follow Conventional Commit style, e.g. chore: remove legacy format commands.
  • When an AI agent opens or updates a PR, fill in .github/pull_request_template.md — link the related issue or explain the problem, then describe what changed. Do not leave placeholder text or submit a generic summary of the diff.
  • Do not submit vague AI-generated PR text. The human author must understand the change well enough to explain the code, edge cases, and why the approach fits this repository.
  • After finishing a task and before submitting a PR, you must run the gen-changesets skill (see .agents/skills/gen-changesets/SKILL.md) and generate a changeset under .changeset/ according to its rules.
  • When generating a changeset, never decide on a major bump on your own. When you judge a change to meet the major criteria (breaking changes, incompatible user configuration, renamed or removed commands/arguments, changed behavior semantics, etc.), you must stop and explain it to the user and ask for confirmation. Only write major after the user has explicitly agreed. Otherwise default to minor (and fall back to patch if minor is unclear). See the "Hard rule: confirm with the user before writing major" section in .agents/skills/gen-changesets/SKILL.md for details.
  • Prefer importing via import ... from '#/...', which serves the same purpose as import ... from '@/...'.
  • Do not commit throwaway scratch or exploratory files. Never stage:
    • Agent working notes or handoff/summary documents (e.g. HANDOVER-*.md, HANDOFF-*.md, handoff.md).
    • Throwaway UI/UX prototypes or design mockups (e.g. *-designs.html, *-mockup.html, *-demo(s).html) at the repo root or under a design/ folder. The only tracked .html files should be Vite index.html entrypoints. Before committing or opening a PR, run git status and git diff --staged --stat and remove anything matching these patterns. Put scratch work under .tmp/ (gitignored) instead of the repo root or the source tree.