149 lines
5.1 KiB
TypeScript
149 lines
5.1 KiB
TypeScript
export interface DelegateRun {
|
|
delegateRunId: string;
|
|
parentRunId: string | null;
|
|
description: string;
|
|
depth: number;
|
|
status: 'success' | 'aborted' | 'needs_user_input' | 'running';
|
|
startTs: string;
|
|
endTs: string | null;
|
|
eventCount: number;
|
|
toolCalls: number;
|
|
resultPreview?: string | null;
|
|
totalTokens?: number;
|
|
toolSummary?: Array<{ tool: string; count: number }>;
|
|
filesChanged?: string[];
|
|
lastErrorTool?: string | null;
|
|
}
|
|
|
|
export interface DelegateRunNode extends DelegateRun {
|
|
children: DelegateRunNode[];
|
|
}
|
|
|
|
/** サブタスクジョブに紐づく delegate run のグループ。Task 7 でレンダリングされる。 */
|
|
export interface SubtaskDelegateGroup {
|
|
jobId: string;
|
|
issueNumber: number;
|
|
depth: number;
|
|
status: string;
|
|
runs: DelegateRun[];
|
|
}
|
|
|
|
/** GET /:id/delegate-runs のレスポンス型。 */
|
|
export interface DelegateRunsResult {
|
|
runs: DelegateRun[];
|
|
subtasks: SubtaskDelegateGroup[];
|
|
}
|
|
|
|
/** delegate run の status に対応する i18n ラベルキーと Tailwind 色クラス。 */
|
|
export function delegateStatusBadge(
|
|
status: DelegateRun['status'],
|
|
): { labelKey: string; cls: string } {
|
|
switch (status) {
|
|
case 'success': return { labelKey: 'subtasks.delegateStatus.success', cls: 'bg-emerald-100 text-emerald-800' };
|
|
case 'aborted': return { labelKey: 'subtasks.delegateStatus.aborted', cls: 'bg-red-100 text-red-800' };
|
|
case 'needs_user_input': return { labelKey: 'subtasks.delegateStatus.needs_user_input', cls: 'bg-amber-100 text-amber-800' };
|
|
default: return { labelKey: 'subtasks.delegateStatus.running', cls: 'bg-blue-100 text-blue-800' };
|
|
}
|
|
}
|
|
|
|
/** endTs があればそこまで、無ければ now までの経過。clock skew で負にならないようガード。 */
|
|
export function formatElapsed(startTs: string, endTs: string | null, now: number): string {
|
|
const end = endTs ? new Date(endTs).getTime() : now;
|
|
const ms = Math.max(0, end - new Date(startTs).getTime());
|
|
if (ms < 1000) return `${ms}ms`;
|
|
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
return `${Math.floor(ms / 60_000)}m${Math.floor((ms % 60_000) / 1000)}s`;
|
|
}
|
|
|
|
/** トークン数を人間が読みやすい形式にフォーマット。例: 999 → "999 tok", 1000 → "1.0k tok"。 */
|
|
export function formatTokens(n: number): string {
|
|
if (n < 1000) return `${n} tok`;
|
|
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k tok`;
|
|
return `${(n / 1_000_000).toFixed(1)}M tok`;
|
|
}
|
|
|
|
/**
|
|
* Build a tree from a flat list of DelegateRun records.
|
|
* Runs whose parentRunId is null, or whose parent is not in the set,
|
|
* are treated as roots (orphan fallback — no data loss).
|
|
* Sorts roots and children by startTs (ascending), with delegateRunId as tiebreaker.
|
|
*/
|
|
export function buildDelegateRunTree(runs: DelegateRun[]): DelegateRunNode[] {
|
|
const nodes = new Map<string, DelegateRunNode>();
|
|
for (const r of runs) {
|
|
nodes.set(r.delegateRunId, { ...r, children: [] });
|
|
}
|
|
const roots: DelegateRunNode[] = [];
|
|
for (const node of nodes.values()) {
|
|
const parent = node.parentRunId ? nodes.get(node.parentRunId) : undefined;
|
|
if (parent) {
|
|
parent.children.push(node);
|
|
} else {
|
|
roots.push(node);
|
|
}
|
|
}
|
|
// Sort roots and children by startTs (ascending), tiebreak by delegateRunId
|
|
const sortFn = (a: DelegateRunNode, b: DelegateRunNode) => {
|
|
const cmp = a.startTs.localeCompare(b.startTs);
|
|
return cmp !== 0 ? cmp : a.delegateRunId.localeCompare(b.delegateRunId);
|
|
};
|
|
roots.sort(sortFn);
|
|
for (const node of nodes.values()) {
|
|
node.children.sort(sortFn);
|
|
}
|
|
return roots;
|
|
}
|
|
|
|
/**
|
|
* Determine the currently running tool from a sequence of trace events.
|
|
* Scans events in order: tool_call marks a tool as pending,
|
|
* tool_result for the same tool clears the pending state.
|
|
* Returns the name of the tool still pending (not yet completed),
|
|
* or null if no tool is currently running.
|
|
*/
|
|
export function currentRunningTool(
|
|
events: Array<{ kind: string; payload: unknown }>
|
|
): string | null {
|
|
let pending: string | null = null;
|
|
for (const event of events) {
|
|
if (event.kind === 'tool_call') {
|
|
const payload = event.payload as Record<string, unknown> | null;
|
|
const tool = payload?.tool;
|
|
if (typeof tool === 'string') {
|
|
pending = tool;
|
|
}
|
|
} else if (event.kind === 'tool_result') {
|
|
const payload = event.payload as Record<string, unknown> | null;
|
|
const tool = payload?.tool;
|
|
if (typeof tool === 'string' && tool === pending) {
|
|
pending = null;
|
|
}
|
|
}
|
|
}
|
|
return pending;
|
|
}
|
|
|
|
/**
|
|
* Summarize descendants of a node (children recursively, excluding self).
|
|
* Returns { total, failed, running } counts.
|
|
* - failed = runs with status === 'aborted'
|
|
* - running = runs with status === 'running'
|
|
*/
|
|
export function summarizeDescendants(node: DelegateRunNode): { total: number; failed: number; running: number } {
|
|
let total = 0;
|
|
let failed = 0;
|
|
let running = 0;
|
|
|
|
const traverse = (n: DelegateRunNode) => {
|
|
for (const child of n.children) {
|
|
total++;
|
|
if (child.status === 'aborted') failed++;
|
|
if (child.status === 'running') running++;
|
|
traverse(child);
|
|
}
|
|
};
|
|
|
|
traverse(node);
|
|
return { total, failed, running };
|
|
}
|