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
14 changes: 7 additions & 7 deletions AGENTS.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions apps/kimi-code/src/tui/controllers/subagent-event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,10 @@ export class SubAgentEventHandler {
parentToolCallId: event.parentToolCallId,
agentName: event.subagentName,
description: typeof description === 'string' ? description : undefined,
model:
event.model === undefined
? undefined
: modelDisplayName(event.model, this.host.state.appState.availableModels[event.model]),
};
}

Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/src/tui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export interface BackgroundAgentMetadata {
readonly parentToolCallId: string;
readonly agentName?: string;
readonly description?: string;
/** Display name of the model bound to the subagent (v2 spawns only). */
readonly model?: string;
}

export type BackgroundAgentStatusPhase = 'started' | 'completed' | 'failed';
Expand Down
8 changes: 5 additions & 3 deletions apps/kimi-code/src/tui/utils/background-agent-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ export function formatBackgroundAgentTranscript(
): BackgroundAgentStatusData {
const normalizedAgentName = normalizeBackgroundField(meta.agentName);
const subject = normalizedAgentName !== undefined ? `${normalizedAgentName} agent` : 'agent';
const model = normalizeBackgroundField(meta.model);
const subjectWithModel = model === undefined ? subject : `${subject} (${model})`;
const headline =
phase === 'started'
? `${subject} started in background`
? `${subjectWithModel} started in background`
: phase === 'completed'
? `${subject} completed in background`
: `${subject} failed in background`;
? `${subjectWithModel} completed in background`
: `${subjectWithModel} failed in background`;
const tail = phase === 'failed' ? normalizeBackgroundField(extras?.error) : undefined;
const detailParts = [normalizeBackgroundField(meta.description), tail].filter(
(part): part is string => part !== undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,37 @@ import { describe, expect, it } from 'vitest';

import { BackgroundAgentStatusComponent } from '#/tui/components/messages/background-agent-status';
import { STATUS_BULLET } from '#/tui/constant/symbols';
import { formatBackgroundAgentTranscript } from '#/tui/utils/background-agent-status';

function strip(text: string): string {
return text.replaceAll(/\u001B\[[0-9;]*m/g, '');
}

describe('formatBackgroundAgentTranscript', () => {
it('includes the bound model in the headline when known', () => {
const status = formatBackgroundAgentTranscript('started', {
agentId: 'agent-1',
parentToolCallId: 'call-1',
agentName: 'coder',
description: 'Implement the fix',
model: 'GLM 5.2',
});

expect(status.headline).toBe('coder agent (GLM 5.2) started in background');
expect(status.detail).toBe('Implement the fix');
});

it('falls back to the plain headline when the model is unknown', () => {
const status = formatBackgroundAgentTranscript('started', {
agentId: 'agent-1',
parentToolCallId: 'call-1',
agentName: 'coder',
});

expect(status.headline).toBe('coder agent started in background');
});
});

describe('BackgroundAgentStatusComponent', () => {
it('renders started/completed with the shared bullet and failed with a red x marker', () => {
const started = new BackgroundAgentStatusComponent({
Expand Down
46 changes: 46 additions & 0 deletions apps/kimi-inspect/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# kimi-inspect Agent Guide

Package-local rules for `apps/kimi-inspect` (`@moonshot-ai/kimi-inspect`).

## What it is

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.

## Top-level views (`src/components/NavRail.tsx`)

A left icon rail switches top-level views:

- **Chat workspace** — the per-session chat (see "ChatView transcript rendering" below).
- **Global message search** (`src/components/SearchView.tsx`) — cross-session full-text search over `POST /api/v1/search`, cursor-paged via a manual Load more; an exact-match checkbox maps to the API's `mode: 'literal'` substring search, which ignores sort and orders newest-first; a `live`/`index` badge on the results shows which server route served them (in-memory session transcript vs the persisted index).
- **Model Catalog** (`src/components/ModelCatalogView.tsx`) — every Provider with its Models and the default marker, via `IModelCatalog` / `IModelService` channel proxies. Expanding a Model opens the model inspector inside that view: provider/model config layers plus the resolved runtime view with per-value provenance (config / override / builtin / env / synthesized), served on demand by `IModelCatalog.inspect` — the same resolution pass the runtime's `get` serves, traced via `ResolutionTraceCollector` and assembled by `kosong/model/inspection.ts`.
- **App Services** (`src/components/AppServicesView.tsx`) — the app-scope Service reflection, full width.

## Scope panels

- The **Agent scope** stays in the Chat view's right dock (`src/components/RightPanel.tsx`) across two tabs: the `Agent` tab (`Inspector`: agent switcher + a Plan lookup card — `PlanCard` in `src/components/Inspector.tsx` — querying `GET /sessions/{id}/transcript/plan` (one tool_call_id, or every plan of the agent) via `src/transcript/api.ts`'s `fetchTranscriptPlan` — plus the agent Service panels) and the `State` tab (every key an Agent Service registered into the agent-state container, polled live via `IAgentStateService.snapshot()` — the same live diff-tree view as the session State tab, sharing `StateCard` from `src/components/StateCard.tsx`).
- The **Session scope** has its own column right next to the session-list sidebar (`src/components/SessionPane.tsx`) with two tabs: Services (the pending-interactions card — `src/components/InteractionsCard.tsx` — plus the session Service panels) and State (every key a Session Service registered into the session-state container, read on demand via `ISessionStateService.snapshot()`).

## Channel layer

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.

## Session activity (`src/activity/`)

Session-level coarse status is the one exception to no-push: `src/activity/` holds a second `/api/v1/ws` client (`GlobalEventsWs`) that subscribes to nothing and consumes the server-pushed global facts — `event.session.work_changed` updates a per-session activity map (`SessionActivityHub` + subscribe/version store, seeded on connect/reconnect from `GET /api/v1/sessions`), while `event.session.created` / `session.meta.updated` invalidate the `['sessions']` query; the `Sidebar` session rows render `running` / `approval` / `question` / `failed` badges from it via `useSessionActivities`.

## Dev server and server discovery

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.

## ChatView transcript rendering

The per-session chat (`src/components/ChatView.tsx`) renders turn-granularly from the **transcript** surface instead of context memory:

- It carries an in-chat search bar (`src/components/ChatSearchBar.tsx`): it searches the current session via `POST /api/v1/search` with `container: { session_id }` (usually served by the live route, since selecting a session resumes it), and a result click funnels through the app shell's `openSearchHit` — the same agent-switch + `ChatJump` (page-back, scroll, flash) path the global search view uses.
- Full state is read from `GET /api/v1/sessions/{id}/transcript` (initial load = newest page, refreshes re-read from the tail backwards); older history auto-pages with `before_turn` via an IntersectionObserver sentinel at the top of the scroll view, and each timeline item is wrapped in `content-visibility: auto` + `contain-intrinsic-size` so the browser virtualizes off-screen rendering natively (no windowing library).
- `/api/v1/ws` is an incremental channel (`transcript.ops`, grade `block` — the cheapest grade that still carries whole-state frame upserts, dropping per-token `append` frames; `transcript.reset` is ignored by the store, surfaced only to the audit recorder via the optional `onReset` handler). The channel tracks the op-batch watermark: a dedicated `subscribe_v2` control frame carries the per-agent grades and the `transcript_since` cursor, a seq gap / reconnect / `resync_required` / append gap triggers a point-to-point catch-up (`fetchTranscriptOps` → `GET .../transcript/ops?since_seq=`), and any legacy/incomplete answer falls back to the full REST refresh.
- 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).

## Transcript audit panel

The Transcript audit panel (`src/components/audit/`, the `Audit` tab of the chat view's right dock — `src/components/RightPanel.tsx`, fed the trail by `ChatView`'s `onTrailChange`) replays how the visible store was built: an `AuditTrail` (`src/audit/`) records every step — each REST page (request + replace/prepend), every WS frame (`transcript.ops` live/buffered/flushed/catchup, `transcript.reset`), loss signals, and prompt/cancel actions — with the resulting immutable `AgentState` per entry; the panel offers a draggable timeline plus a Diff tab (structural diff vs the previous entry: added/modified/removed colored, long strings tail-truncated, all fields kept), a full State view, and the raw Event payload.
4 changes: 3 additions & 1 deletion packages/agent-core/src/agent/records/migration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { migrateV1_0ToV1_1 } from './v1.1';
import { migrateV1_1ToV1_2 } from './v1.2';
import { migrateV1_2ToV1_3 } from './v1.3';
import { migrateV1_3ToV1_4 } from './v1.4';
import { migrateV1_4ToV1_5 } from './v1.5';

// Wire protocol versions currently support only the `number.number` format.
// Bump this only for changes that require migration of existing records or
// change how existing records must be interpreted. Do not bump it only because
// a new feature adds a new wire record type: older versions do not implement
// that feature and do not need to understand the new record type.
export const AGENT_WIRE_PROTOCOL_VERSION = '1.4';
export const AGENT_WIRE_PROTOCOL_VERSION = '1.5';

export interface WireMigrationRecord {
readonly type: string;
Expand All @@ -26,6 +27,7 @@ const MIGRATIONS: readonly WireMigration[] = [
migrateV1_1ToV1_2,
migrateV1_2ToV1_3,
migrateV1_3ToV1_4,
migrateV1_4ToV1_5,
];

export function isNewerWireVersion(readVersion: string): boolean {
Expand Down
32 changes: 32 additions & 0 deletions packages/agent-core/src/agent/records/migration/v1.5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Wire protocol 1.5 persists an epoch-ms anchor at every goal create/resume
* boundary and wall-clock checkpoint. Version 1.4 records already carry an
* epoch-ms `time`, so the migration can recover that boundary without
* inventing a crash timestamp or adding periodic checkpoint writes. Existing
* anchors are authoritative.
*
* Ported from agent-core-v2 (`wire/migration/v1.5.ts`) so the v1 engine can
* resume v2-written 1.5 sessions natively instead of replaying them without
* migration.
*/
import type { WireMigration, WireMigrationRecord } from './index';

export const migrateV1_4ToV1_5: WireMigration = {
sourceVersion: '1.4',
targetVersion: '1.5',
migrateRecord(record: WireMigrationRecord): WireMigrationRecord {
if (!advancesActiveInterval(record)) return record;
if (record['wallClockResumedAt'] !== undefined) return record;
if (typeof record['time'] !== 'number') return record;
return { ...record, wallClockResumedAt: record['time'] };
},
};

function advancesActiveInterval(record: WireMigrationRecord): boolean {
return (
record.type === 'goal.create' ||
(record.type === 'goal.update' &&
(record['status'] === 'active' ||
(record['status'] === undefined && typeof record['wallClockMs'] === 'number')))
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('1.3 to 1.4', () => {
},
]),
).toMatchInlineSnapshot(`
[wire] metadata { "protocol_version": "<protocol-version>", "created_at": "<time>" }
[wire] metadata { "protocol_version": "1.4", "created_at": "<time>" }
[wire] goal.create { "goalId": "goal-1", "objective": "ship the feature", "completionCriterion": "tests pass", "time": "<time>" }
[wire] goal.update { "tokensUsed": 5, "wallClockMs": 0, "time": "<time>" }
[wire] goal.update { "turnsUsed": 1, "time": "<time>" }
Expand Down
69 changes: 69 additions & 0 deletions packages/agent-core/test/agent/records/migration/v1.5.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';

import { migrateV1_4ToV1_5 } from '../../../../src/agent/records/migration/v1.5';
import { runMigration } from './utils';

describe('1.4 to 1.5 active wall-clock anchor migration', () => {
it('backfills missing anchors from create and resume record timestamps', () => {
expect(
runMigration(migrateV1_4ToV1_5, [
{
type: 'metadata',
protocol_version: '1.4',
created_at: 1,
},
{
type: 'goal.create',
goalId: 'goal-1',
objective: 'ship the feature',
time: 10,
},
{
type: 'goal.update',
status: 'paused',
wallClockMs: 20,
time: 30,
},
{
type: 'goal.update',
status: 'active',
time: 40,
},
]),
).toMatchInlineSnapshot(`
[wire] metadata { "protocol_version": "<protocol-version>", "created_at": "<time>" }
[wire] goal.create { "goalId": "goal-1", "objective": "ship the feature", "time": "<time>", "wallClockResumedAt": 10 }
[wire] goal.update { "status": "paused", "wallClockMs": 20, "time": "<time>" }
[wire] goal.update { "status": "active", "time": "<time>", "wallClockResumedAt": 40 }
`);
});

it('preserves an existing active wall-clock anchor', () => {
expect(
runMigration(migrateV1_4ToV1_5, [
{
type: 'goal.update',
status: 'active',
wallClockResumedAt: 35,
time: 40,
},
]),
).toMatchInlineSnapshot(
`[wire] goal.update { "status": "active", "wallClockResumedAt": 35, "time": "<time>" }`,
);
});

it('advances a missing anchor from a wall-clock checkpoint timestamp', () => {
expect(
runMigration(migrateV1_4ToV1_5, [
{
type: 'goal.update',
wallClockMs: 3_000,
time: 4_000,
},
]),
).toMatchInlineSnapshot(
`[wire] goal.update { "wallClockMs": 3000, "time": "<time>", "wallClockResumedAt": 4000 }`,
);
});
});
Loading
Loading