Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e2adc96
feat(agent-core): re-export wire record types for in-monorepo consumers
RealKai42 May 25, 2026
be38639
chore(vis): purge legacy wire protocol code
RealKai42 May 25, 2026
0550de8
feat(vis): introduce single-source agent-record types
RealKai42 May 25, 2026
da6d1cf
test(vis): add fixture session and builder helper
RealKai42 May 25, 2026
a801bec
feat(vis): implement new session store reader
RealKai42 May 25, 2026
767ccfe
feat(vis): wire new session list/detail routes
RealKai42 May 25, 2026
497b271
refactor(vis): drop legacy path config
RealKai42 May 25, 2026
f50b056
feat(vis): adapt session list page to new DTO
RealKai42 May 25, 2026
8b44f26
feat(vis): implement per-agent wire reader
RealKai42 May 25, 2026
632d3ff
feat(vis): rewrite wire route for new protocol
RealKai42 May 25, 2026
a12f94f
feat(vis): rewrite wire type metadata for new protocol
RealKai42 May 25, 2026
bdbbd39
feat(vis): rewrite wire row + headline for new record union
RealKai42 May 25, 2026
66fbf54
feat(vis): wire tab detail panel + multi-agent selector
RealKai42 May 25, 2026
66e9f07
feat(vis): rebuild wire issues detection for new protocol
RealKai42 May 25, 2026
f71e655
feat(vis): implement context projector
RealKai42 May 25, 2026
9154f3b
feat(vis): rewrite context route on projector
RealKai42 May 25, 2026
32ea6e1
feat(vis): rebuild context tab for new ContextMessage shape
RealKai42 May 25, 2026
d499f4b
feat(vis): implement agent tree builder
RealKai42 May 25, 2026
0a687ed
feat(vis): rewrite agents route
RealKai42 May 25, 2026
0841f4f
feat(vis): rebuild subagents tab around state.json.agents
RealKai42 May 25, 2026
07feae3
feat(vis): rebuild state tab on raw state.json
RealKai42 May 25, 2026
0598ad3
chore(vis): purge residual legacy field references
RealKai42 May 25, 2026
b59595d
vis: rewrite complete on new agent-core protocol
RealKai42 May 25, 2026
5735aa4
fix(vis): adapt to wire protocol 1.1 with flattened tool calls
RealKai42 May 26, 2026
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
7 changes: 4 additions & 3 deletions apps/vis/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { contextRoute } from './routes/context';
import { sessionDetailRoute } from './routes/session-detail';
import { sessionsRoute } from './routes/sessions';
import { subagentsRoute } from './routes/subagents';
import { toolResultsRoute } from './routes/tool-results';
import { wireRoute } from './routes/wire';

/** Resolve the SPA bundle directory next to the compiled server.mjs, if it
Expand Down Expand Up @@ -87,9 +86,11 @@ export async function createApp(options: CreateAppOptions = {}): Promise<Hono> {
api.route('/sessions', sessionsRoute());
api.route('/sessions', sessionDetailRoute());
api.route('/sessions', wireRoute());
api.route('/sessions', contextRoute());
api.route('/sessions', subagentsRoute());
api.route('/sessions', toolResultsRoute());
// Mount contextRoute last because it currently uses a catch-all stub
// (Phase C scope) that would otherwise shadow more specific routes
// registered below it.
api.route('/sessions', contextRoute());

app.route('/api', api);

Expand Down
102 changes: 0 additions & 102 deletions apps/vis/server/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

Expand Down Expand Up @@ -53,104 +52,3 @@ export function resolveVisAuthToken(host: string = resolveHost()): string | unde
}

export const KIMI_CODE_HOME: string = resolveKimiCodeHome();

export class VisPathConfig {
readonly sessionsDir: string;

constructor(readonly home: string) {
this.sessionsDir = join(home, 'sessions');
}

sessionDir(sessionId: string): string {
return (
this.findIndexedSessionDir(sessionId) ??
this.findNestedSessionDir(sessionId) ??
join(this.sessionsDir, sessionId)
);
}

statePath(sessionId: string): string {
return join(this.sessionDir(sessionId), 'state.json');
}

mainAgentDir(sessionId: string): string {
const sessionDir = this.sessionDir(sessionId);
const mainDir = join(sessionDir, 'agents', 'main');
return isDirectory(mainDir) ? mainDir : sessionDir;
}

wirePath(sessionId: string): string {
return join(this.mainAgentDir(sessionId), 'wire.jsonl');
}

subagentDir(sessionId: string, agentId: string): string {
const sessionDir = this.sessionDir(sessionId);
const agentDir = join(sessionDir, 'agents', agentId);
return isDirectory(agentDir) ? agentDir : join(sessionDir, 'subagents', agentId);
}

toolResultArchivePath(sessionId: string, toolCallId: string): string {
const sessionDir = this.sessionDir(sessionId);
const mainPath = join(sessionDir, 'agents', 'main', 'tool-results', `${toolCallId}.txt`);
return existsSync(mainPath) ? mainPath : join(sessionDir, 'tool-results', `${toolCallId}.txt`);
}

private findIndexedSessionDir(sessionId: string): string | null {
const indexPath = join(this.home, 'session_index.jsonl');
let raw: string;
try {
raw = readFileSync(indexPath, 'utf8');
} catch {
return null;
}
for (const line of raw.split(/\r?\n/)) {
if (line.trim().length === 0) continue;
try {
const parsed = JSON.parse(line) as {
sessionId?: unknown;
session_id?: unknown;
sessionDir?: unknown;
session_dir?: unknown;
};
const id = stringValue(parsed.sessionId) ?? stringValue(parsed.session_id);
if (id !== sessionId) continue;
const dir = stringValue(parsed.sessionDir) ?? stringValue(parsed.session_dir);
if (dir !== null && isDirectory(dir)) return dir;
} catch {
// Ignore malformed index lines.
}
}
return null;
}

private findNestedSessionDir(sessionId: string): string | null {
const direct = join(this.sessionsDir, sessionId);
if (isDirectory(direct)) return direct;
let entries: string[];
try {
entries = readdirSync(this.sessionsDir);
} catch {
return null;
}
for (const entry of entries) {
const candidate = join(this.sessionsDir, entry, sessionId);
if (isDirectory(candidate)) return candidate;
}
return null;
}
}

function stringValue(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null;
}

function isDirectory(path: string): boolean {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}

/** Singleton path config pinned to the resolved KIMI_CODE_HOME. */
export const pathConfig = new VisPathConfig(KIMI_CODE_HOME);
99 changes: 99 additions & 0 deletions apps/vis/server/src/lib/agent-record-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// apps/vis/server/src/lib/agent-record-types.ts
// Single source of truth: everything below comes from agent-core directly.
// Do NOT add local interfaces that duplicate upstream shapes.

export type {
AgentRecord,
AgentRecordEvents,
AgentRecordOf,
AgentConfigUpdateData,
CompactionBeginData,
CompactionResult,
PermissionApprovalResultRecord,
PermissionMode,
UsageRecordScope,
ToolStoreUpdate,
LoopRecordedEvent,
ContextMessage,
PromptOrigin,
} from '@moonshot-ai/agent-core';
export { AGENT_WIRE_PROTOCOL_VERSION } from '@moonshot-ai/agent-core';
export type { Message, ContentPart, ToolCall, TokenUsage } from '@moonshot-ai/kosong';

// ── vis-only DTOs ──────────────────────────────────────────────────────────

export interface ApiError {
error: string;
code:
| 'NOT_FOUND'
| 'BAD_REQUEST'
| 'UNAUTHORIZED'
| 'READ_ERROR'
| 'PARSE_ERROR'
| 'DELETE_ERROR'
| 'UNSUPPORTED_PROTOCOL';
}

export type SessionHealth =
| 'ok'
| 'broken_state'
| 'missing_main_wire'
| 'unsupported_protocol';

export interface SessionSummary {
sessionId: string;
sessionDir: string;
workDir: string;
title: string | null;
lastPrompt: string | null;
isCustomTitle: boolean;
createdAt: number;
updatedAt: number;
agentCount: number;
mainAgentExists: boolean;
mainWireRecordCount: number;
wireProtocolVersion: string | null;
health: SessionHealth;
}

export interface AgentInfo {
agentId: string;
type: 'main' | 'sub' | 'independent';
parentAgentId: string | null;
homedir: string;
wireExists: boolean;
wireRecordCount: number;
wireProtocolVersion: string | null;
}

export interface SessionDetail {
sessionId: string;
workDir: string;
state: unknown; // 原样透传,前端按 state.json 真实形状渲染
agents: AgentInfo[];
}

export type WireLine = { _lineNo: number } & {
// structural unification with AgentRecord; preserves discriminant
[K in keyof import('@moonshot-ai/agent-core').AgentRecordEvents]: import('@moonshot-ai/agent-core').AgentRecordOf<K> & {
_lineNo: number;
};
}[keyof import('@moonshot-ai/agent-core').AgentRecordEvents];

export interface WireResponse {
sessionId: string;
agentId: string;
protocolVersion: string;
metadata: { protocolVersion: string; createdAt: number };
records: readonly WireLine[];
warnings: string[];
}

export interface AgentNode extends AgentInfo {
children: AgentNode[];
}

export interface AgentTreeResponse {
sessionId: string;
tree: AgentNode[];
}
33 changes: 33 additions & 0 deletions apps/vis/server/src/lib/agent-tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { AgentInfo } from './agent-record-types';

export interface AgentNode extends AgentInfo {
children: AgentNode[];
}

/**
* Build a parent/child tree from the flat agent inventory found on
* `state.json.agents`. Roots are agents with no `parentAgentId`, plus any
* agent whose `parentAgentId` does not resolve in the inventory (orphans).
* The returned roots are sorted so that the `main` agent always appears
* first; remaining roots fall back to a stable lexicographic order.
*/
export function buildAgentTree(agents: ReadonlyArray<AgentInfo>): AgentNode[] {
const byId = new Map<string, AgentNode>();
for (const a of agents) byId.set(a.agentId, { ...a, children: [] });

const roots: AgentNode[] = [];
for (const node of byId.values()) {
if (node.parentAgentId !== null && byId.has(node.parentAgentId)) {
byId.get(node.parentAgentId)!.children.push(node);
} else {
roots.push(node);
}
}
return roots.sort(sortAgents);

Check warning on line 26 in apps/vis/server/src/lib/agent-tree.ts

View workflow job for this annotation

GitHub Actions / lint

eslint-plugin-unicorn(no-array-sort)

Use `Array#toSorted()` instead of `Array#sort()`.
}

function sortAgents(a: AgentNode, b: AgentNode): number {
if (a.agentId === 'main') return -1;
if (b.agentId === 'main') return 1;
return a.agentId.localeCompare(b.agentId);
}
Loading
Loading