Skip to content

Commit da55cfe

Browse files
committed
feat(tui): inspect historical agent graph runs
Generated-by: Codex
1 parent ad7de3c commit da55cfe

4 files changed

Lines changed: 193 additions & 1 deletion

File tree

‎packages/cli/src/__tests__/pi-tui-runner.test.ts‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { type SessionSummary, type StoredMessage } from '@maka/core/session';
2020
import { type ThinkingLevel } from '@maka/core/model-thinking';
2121
import { type UserQuestionResponse } from '@maka/core/user-question';
2222
import type { SkillInvocationResult } from '@maka/core/skill-invocation';
23+
import type { AgentGraphClientSnapshot } from '@maka/runtime-host/protocol';
2324
import { SessionActivityRegistry } from '@maka/runtime/goal-turn-lifecycle';
2425
import { type ContextDiagnostics } from '@maka/runtime/context-diagnostics';
2526
import type {
@@ -116,6 +117,40 @@ function deferred<T>() {
116117
return { promise, resolve };
117118
}
118119

120+
function historicalGraphSnapshot(graphId: string): AgentGraphClientSnapshot {
121+
return {
122+
schemaVersion: 1,
123+
rootSessionId: 'session-1',
124+
graphId,
125+
orchestrationMode: 'graph',
126+
snapshotVersion: `sha256:${'1'.repeat(64)}`,
127+
status: 'completed',
128+
scheduleRevision: 2,
129+
topologyFingerprint: `sha256:${'2'.repeat(64)}`,
130+
closed: true,
131+
operators: [],
132+
edges: [],
133+
work: [],
134+
reconciliationFailures: [],
135+
stoppedTargets: [],
136+
finish: { resultIds: ['result-1'], reason: 'done', revision: 2, committedAt: 2 },
137+
claims: [],
138+
recentControlDecisions: [],
139+
recentActivity: [],
140+
terminalHistory: { records: [] },
141+
omitted: {
142+
operators: 0,
143+
edges: 0,
144+
work: 0,
145+
reconciliationFailures: 0,
146+
stoppedTargets: 0,
147+
claims: 0,
148+
controlDecisions: 0,
149+
recentActivity: 0,
150+
},
151+
};
152+
}
153+
119154
/** Catalog API-key providers as wizard entries with no existing connection —
120155
* the default `/setup` provider list for tests that don't need 已设置 state. */
121156
function defaultOnboardingProviders(): OnboardingProviderEntry[] {
@@ -2436,6 +2471,47 @@ describe('Maka Pi TUI runner', () => {
24362471
await run;
24372472
});
24382473

2474+
test('inspects a historical Agent Graph run without starting a turn', async () => {
2475+
const terminal = new FakeTerminal();
2476+
const driver = new SlashCommandDriver();
2477+
const requestedGraphIds: string[] = [];
2478+
const run = runMakaPiTui({
2479+
title: 'Maka',
2480+
driver,
2481+
cwd: '/repo',
2482+
model: 'deepseek-v4-flash',
2483+
connectionSlug: 'deepseek',
2484+
permissionMode: 'ask',
2485+
terminal,
2486+
agentGraphHistory: {
2487+
listEpochs: async () => [
2488+
{ epoch: 2, graphId: 'graph-2', createdAt: 2, current: true },
2489+
{ epoch: 1, graphId: 'graph-1', createdAt: 1, current: false },
2490+
],
2491+
getSnapshot: async (_rootSessionId, graphId) => {
2492+
requestedGraphIds.push(graphId);
2493+
return historicalGraphSnapshot(graphId);
2494+
},
2495+
},
2496+
});
2497+
2498+
terminal.input('/graph history');
2499+
terminal.input('\r');
2500+
await waitFor(() =>
2501+
plainTerminalOutput(terminal.screenOutput()).includes('Agent Graph History'),
2502+
);
2503+
terminal.input('\x1b[B');
2504+
terminal.input('\r');
2505+
await waitFor(() =>
2506+
plainTerminalOutput(terminal.output()).includes('Agent Graph run #1 · History (read-only)'),
2507+
);
2508+
2509+
assert.deepEqual(requestedGraphIds, ['graph-1']);
2510+
assert.deepEqual(driver.prompts, []);
2511+
exitMaka(terminal);
2512+
await run;
2513+
});
2514+
24392515
test('rejects unsupported /thinking levels with usage instead of sending an update', async () => {
24402516
const terminal = new FakeTerminal();
24412517
const driver = new SlashCommandDriver();

‎packages/cli/src/pi-tui-runner.ts‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import type {
5353
} from './pi-tui-contracts.js';
5454
import { AUTO_RECAP_DISPLAY_LIMIT_BYTES, shouldAutoRecap } from './session-recap.js';
5555
import type { InvocableSkillEntry } from '@maka/runtime/skill-invocation';
56+
import type { AgentGraphClientSnapshot, AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
5657
import { MakaSkillHighlightEditor } from './skill-highlight-editor.js';
5758
import { parseGraphCommand, type ParsedGraphCommand } from '@maka/core/graph-command';
5859
import { parseSwarmCommand, type ParsedSwarmCommand } from '@maka/core/swarm-command';
@@ -166,6 +167,11 @@ export interface MakaPiTuiInput {
166167
listShellRunUpdates?: (sessionId: string) => Promise<ShellRunUpdate[]>;
167168
/** Host-owned invocable Skill catalog used for picker, completion, and token highlighting. */
168169
listSkills?: (cwd: string) => Promise<readonly InvocableSkillEntry[]>;
170+
/** Read-only Runtime Host projection for inspecting current and historical Graph epochs. */
171+
agentGraphHistory?: {
172+
listEpochs(rootSessionId: string): Promise<readonly AgentGraphEpochSummary[]>;
173+
getSnapshot(rootSessionId: string, graphId: string): Promise<AgentGraphClientSnapshot>;
174+
};
169175
/** Serializes TUI turn and control activity for the attached Session. */
170176
turnActivity: MakaPiTuiTurnActivitySurface;
171177
/** API-key onboarding surface (#1098). When present, /setup runs the wizard,
@@ -2264,6 +2270,56 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
22642270
requestRender();
22652271
};
22662272

2273+
const showGraphHistory = async (): Promise<void> => {
2274+
const rootSessionId = input.driver.getSessionId();
2275+
if (!rootSessionId || !input.agentGraphHistory) {
2276+
throw new Error('Agent Graph history is unavailable on this session driver.');
2277+
}
2278+
const epochs = await input.agentGraphHistory.listEpochs(rootSessionId);
2279+
const items: SelectItem[] = epochs.map((entry) => ({
2280+
value: entry.graphId,
2281+
label: `Run #${entry.epoch}${entry.current ? ' · Current' : ''}`,
2282+
description: entry.current ? 'Current graph' : 'History · read-only',
2283+
}));
2284+
if (items.length === 0) {
2285+
state.entries.push({
2286+
kind: 'notice',
2287+
level: 'info',
2288+
text: 'This session has no Agent Graph runs.',
2289+
});
2290+
requestRender();
2291+
return;
2292+
}
2293+
const epochsByGraphId = new Map(epochs.map((entry) => [entry.graphId, entry]));
2294+
showSelectPicker(
2295+
'Agent Graph History',
2296+
`${items.length} run${items.length === 1 ? '' : 's'}`,
2297+
items,
2298+
(item) => {
2299+
const epoch = epochsByGraphId.get(item.value);
2300+
if (!epoch) return;
2301+
void runControl(async () => {
2302+
const graph = await input.agentGraphHistory!.getSnapshot(rootSessionId, epoch.graphId);
2303+
state.entries.push({
2304+
kind: 'notice',
2305+
level: 'info',
2306+
text: formatAgentGraphHistory(graph, epoch),
2307+
});
2308+
requestRender();
2309+
});
2310+
},
2311+
{
2312+
minPrimaryColumnWidth: 16,
2313+
maxPrimaryColumnWidth: 28,
2314+
selectedIndex: Math.max(
2315+
0,
2316+
epochs.findIndex((entry) => entry.current),
2317+
),
2318+
hint: '↑↓ move · Enter inspect · Esc close',
2319+
},
2320+
);
2321+
};
2322+
22672323
const setGraphMode = async (mode: OrchestrationMode) => {
22682324
if (!input.driver.setOrchestrationMode) {
22692325
throw new Error('Graph Mode is unavailable on this session driver.');
@@ -2283,6 +2339,10 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
22832339
showGraphStatus();
22842340
return;
22852341
}
2342+
if (command.kind === 'history') {
2343+
void runControl(showGraphHistory);
2344+
return;
2345+
}
22862346
if (command.kind === 'set_mode') {
22872347
void runControl(() => setGraphMode(command.mode));
22882348
return;
@@ -3025,6 +3085,41 @@ function formatContextCount(value: number): string {
30253085
return value.toLocaleString('en-US');
30263086
}
30273087

3088+
function formatAgentGraphHistory(
3089+
graph: AgentGraphClientSnapshot,
3090+
epoch: AgentGraphEpochSummary,
3091+
): string {
3092+
const settled = graph.operators.filter((operator) =>
3093+
['completed', 'failed', 'aborted', 'cancelled'].includes(operator.status),
3094+
).length;
3095+
const lines = [
3096+
`Agent Graph run #${epoch.epoch}${epoch.current ? ' · Current' : ' · History (read-only)'}`,
3097+
` ${formatAgentGraphStatus(graph.status)} · ${settled}/${graph.operators.length} operators settled`,
3098+
];
3099+
for (const operator of graph.operators) {
3100+
lines.push(` ${operator.agentId}: ${operator.status.replaceAll('_', ' ')}`);
3101+
}
3102+
if (graph.finish) {
3103+
lines.push(` Selected results: ${graph.finish.resultIds.join(', ') || 'none'}`);
3104+
}
3105+
if (graph.omitted.operators > 0) {
3106+
lines.push(` ${graph.omitted.operators} more operators omitted`);
3107+
}
3108+
return lines.join('\n');
3109+
}
3110+
3111+
function formatAgentGraphStatus(status: AgentGraphClientSnapshot['status']): string {
3112+
return {
3113+
empty: 'Awaiting schedule',
3114+
active: 'Running',
3115+
closing: 'Finishing',
3116+
waiting: 'Waiting',
3117+
stopped: 'Stopped',
3118+
failed: 'Failed',
3119+
completed: 'Completed',
3120+
}[status];
3121+
}
3122+
30283123
function flattenLinkedSessionTree(
30293124
roots: readonly SessionSummary[],
30303125
childrenByParentId: ReadonlyMap<string, readonly SessionSummary[]>,

‎packages/cli/src/runtime-host-tui-command.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export async function runRuntimeHostTui(input: RunRuntimeHostTuiInput): Promise<
6464
permissionMode: 'ask',
6565
turnActivity: context.turnActivity,
6666
listSkills: context.listSkills,
67+
agentGraphHistory: context.agentGraphHistory,
6768
onboarding: context.onboarding,
6869
recap: context.recap,
6970
...(context.profile.kind === 'local'

‎packages/cli/src/runtime-host-tui-context.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ import type { ConnectionCatalogEntry, ConnectionCatalogSnapshot } from '@maka/co
55
import { SessionActivityRegistry } from '@maka/runtime/goal-turn-lifecycle';
66
import { type InvocableSkillEntry } from '@maka/runtime/skill-invocation';
77
import {
8+
readRuntimeHostAgentGraphEpochs,
89
readRuntimeHostInvocableSkills,
910
readRuntimeHostProjects,
1011
type RuntimeHostConnection,
1112
type RuntimeHostProfile,
1213
} from '@maka/runtime-host/client';
13-
import type { WorkspaceTarget } from '@maka/runtime-host/protocol';
14+
import type {
15+
AgentGraphClientSnapshot,
16+
AgentGraphEpochSummary,
17+
WorkspaceTarget,
18+
} from '@maka/runtime-host/protocol';
1419
import { connectRuntimeHostCli, resolveRuntimeHostCliTarget } from './runtime-host-cli-context.js';
1520
import type {
1621
MakaPiTuiTurnActivitySurface,
@@ -38,6 +43,10 @@ export interface RuntimeHostTuiContext {
3843
readonly modelChoices: readonly ModelChoice[];
3944
readonly turnActivity: MakaPiTuiTurnActivitySurface;
4045
readonly listSkills: (cwd: string) => Promise<readonly InvocableSkillEntry[]>;
46+
readonly agentGraphHistory: {
47+
listEpochs(rootSessionId: string): Promise<readonly AgentGraphEpochSummary[]>;
48+
getSnapshot(rootSessionId: string, graphId: string): Promise<AgentGraphClientSnapshot>;
49+
};
4150
readonly recap: SessionRecapGenerator;
4251
readonly onboarding: ReturnType<typeof createRuntimeHostOnboardingSurface>;
4352
readonly profile: RuntimeHostProfile;
@@ -101,6 +110,7 @@ export async function createRuntimeHostTuiContext(
101110
(connected.profile.kind === 'local' ? { kind: 'host_path', path: cwd } : undefined),
102111
driver.getPermissionMode?.() ?? 'ask',
103112
),
113+
agentGraphHistory: createRuntimeHostAgentGraphHistory(connection),
104114
recap: createRuntimeHostRecapGenerator(connection),
105115
onboarding: createRuntimeHostOnboardingSurface(connection),
106116
profile: connected.profile,
@@ -112,6 +122,16 @@ export async function createRuntimeHostTuiContext(
112122
}
113123
}
114124

125+
function createRuntimeHostAgentGraphHistory(
126+
connection: RuntimeHostConnection,
127+
): RuntimeHostTuiContext['agentGraphHistory'] {
128+
return {
129+
listEpochs: (rootSessionId) => readRuntimeHostAgentGraphEpochs(connection, rootSessionId),
130+
getSnapshot: (rootSessionId, graphId) =>
131+
connection.request('agent.graph.query', { rootSessionId, graphId }),
132+
};
133+
}
134+
115135
function createRuntimeHostRecapGenerator(connection: RuntimeHostConnection): SessionRecapGenerator {
116136
return {
117137
generate: async (sessionId, reason) => {

0 commit comments

Comments
 (0)