chore: archive legacy workflow extensions
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Shared workflow run model + formatting helpers, used by the tool renderers
|
||||
* (index.ts) and the /workflows dashboard (dashboard.ts).
|
||||
*/
|
||||
|
||||
import * as os from "node:os";
|
||||
import {
|
||||
truncateHead,
|
||||
type ExtensionContext,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { formatContextUtilization } from "../shared/context-utilization.ts";
|
||||
import { safeStringify } from "./serialization.ts";
|
||||
|
||||
export type Theme = ExtensionContext["ui"]["theme"];
|
||||
|
||||
export const RESULT_JSON_MAX_BYTES = 24 * 1024;
|
||||
export const RESULT_JSON_MAX_LINES = 600;
|
||||
|
||||
export interface AgentUsage {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
cost: number;
|
||||
/** Latest compaction-aware conversation occupancy, not cumulative billing. */
|
||||
contextTokens?: number;
|
||||
turns: number;
|
||||
}
|
||||
|
||||
export function emptyUsage(): AgentUsage {
|
||||
return {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
cost: 0,
|
||||
turns: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export type AgentState = "running" | "done" | "error";
|
||||
export type WorkflowStatus = "running" | "completed" | "failed" | "aborted";
|
||||
|
||||
export type TranscriptRole =
|
||||
"user" | "assistant" | "thinking" | "tool" | "toolResult";
|
||||
|
||||
export interface TranscriptEntry {
|
||||
role: TranscriptRole;
|
||||
text: string;
|
||||
/** Tool name for tool calls/results. */
|
||||
name?: string;
|
||||
/** Stable tool-call identifier used to pair calls, results, and timings. */
|
||||
toolCallId?: string;
|
||||
isError?: boolean;
|
||||
/** Original message timestamp, when provided by the model/session. */
|
||||
timestamp?: number;
|
||||
/** Tool execution lifecycle timestamps, measured by the child session. */
|
||||
startedAt?: number;
|
||||
finishedAt?: number;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
export interface AgentRecord {
|
||||
index: number;
|
||||
label: string;
|
||||
phase?: string;
|
||||
state: AgentState;
|
||||
model?: string;
|
||||
/** Context capacity of the active model used for this agent. */
|
||||
contextWindow?: number;
|
||||
startedAt: number;
|
||||
finishedAt?: number;
|
||||
error?: string;
|
||||
preview: string;
|
||||
usage: AgentUsage;
|
||||
/** Normalized, serializable subagent conversation shown by /workflows. */
|
||||
transcript: TranscriptEntry[];
|
||||
}
|
||||
|
||||
export interface WorkflowDetails {
|
||||
runId: string;
|
||||
/** Pi session that launched this run. */
|
||||
sessionId?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
background: boolean;
|
||||
status: WorkflowStatus;
|
||||
startedAt: number;
|
||||
finishedAt?: number;
|
||||
phases: { title: string; detail?: string }[];
|
||||
currentPhase?: string;
|
||||
agents: AgentRecord[];
|
||||
result?: unknown;
|
||||
resultArtifact?: string;
|
||||
transcriptArtifact?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Colored square state indicator (no emojis/glyphs). */
|
||||
export const SQUARE = "■";
|
||||
|
||||
export function stateSquare(state: AgentState, theme: Theme): string {
|
||||
if (state === "done") return theme.fg("success", SQUARE);
|
||||
if (state === "error") return theme.fg("error", SQUARE);
|
||||
return theme.fg("warning", SQUARE);
|
||||
}
|
||||
|
||||
export function statusSquare(status: WorkflowStatus, theme: Theme): string {
|
||||
if (status === "completed") return theme.fg("success", SQUARE);
|
||||
if (status === "running") return theme.fg("warning", SQUARE);
|
||||
return theme.fg("error", SQUARE);
|
||||
}
|
||||
|
||||
export function statusWord(status: WorkflowStatus): string {
|
||||
return status === "completed" ? "done" : status;
|
||||
}
|
||||
|
||||
export function statusColor(
|
||||
status: WorkflowStatus,
|
||||
): "success" | "warning" | "error" {
|
||||
if (status === "completed") return "success";
|
||||
if (status === "running") return "warning";
|
||||
return "error";
|
||||
}
|
||||
|
||||
export function shortenHome(p: string): string {
|
||||
const home = os.homedir();
|
||||
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
||||
}
|
||||
|
||||
export function formatTokens(count: number): string {
|
||||
if (count < 1000) return count.toString();
|
||||
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
||||
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
||||
return `${(count / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
|
||||
export function formatUsage(usage: AgentUsage, model?: string): string {
|
||||
const parts: string[] = [];
|
||||
if (usage.turns)
|
||||
parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
||||
if (usage.input) parts.push(`${formatTokens(usage.input)} in`);
|
||||
if (usage.output) parts.push(`${formatTokens(usage.output)} out`);
|
||||
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
||||
if (model) parts.push(model);
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
/** Current per-agent context-window utilization, e.g. "7%/372k". */
|
||||
export function agentContext(agent: AgentRecord): string {
|
||||
return formatContextUtilization({
|
||||
tokens: agent.usage.contextTokens,
|
||||
contextWindow: agent.contextWindow,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatElapsed(startedAt: number, finishedAt?: number): string {
|
||||
const totalSeconds = Math.max(
|
||||
0,
|
||||
Math.round(((finishedAt ?? Date.now()) - startedAt) / 1000),
|
||||
);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return minutes > 0
|
||||
? `${minutes}m${seconds.toString().padStart(2, "0")}s`
|
||||
: `${seconds}s`;
|
||||
}
|
||||
|
||||
export function aggregateUsage(agents: AgentRecord[]): AgentUsage {
|
||||
const total = emptyUsage();
|
||||
for (const agent of agents) {
|
||||
total.input += agent.usage.input;
|
||||
total.output += agent.usage.output;
|
||||
total.cacheRead += agent.usage.cacheRead;
|
||||
total.cacheWrite += agent.usage.cacheWrite;
|
||||
total.cost += agent.usage.cost;
|
||||
total.turns += agent.usage.turns;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
export function countStates(details: WorkflowDetails) {
|
||||
let done = 0;
|
||||
let failed = 0;
|
||||
let running = 0;
|
||||
for (const agent of details.agents) {
|
||||
if (agent.state === "done") done++;
|
||||
else if (agent.state === "error") failed++;
|
||||
else running++;
|
||||
}
|
||||
return { done, failed, running };
|
||||
}
|
||||
|
||||
export interface PhaseGroup {
|
||||
title: string;
|
||||
agents: AgentRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Group agents by phase in declared phase order. With `includeEmpty`, phases
|
||||
* that have no agents yet are still listed (used by the dashboard sidebar).
|
||||
*/
|
||||
export function phaseGroups(
|
||||
details: WorkflowDetails,
|
||||
includeEmpty = false,
|
||||
): PhaseGroup[] {
|
||||
const byPhase = new Map<string, AgentRecord[]>();
|
||||
for (const agent of details.agents) {
|
||||
const key = agent.phase ?? "(unphased)";
|
||||
const list = byPhase.get(key) ?? [];
|
||||
list.push(agent);
|
||||
byPhase.set(key, list);
|
||||
}
|
||||
const groups: PhaseGroup[] = [];
|
||||
for (const phase of details.phases) {
|
||||
const agents = byPhase.get(phase.title);
|
||||
if (agents || includeEmpty)
|
||||
groups.push({ title: phase.title, agents: agents ?? [] });
|
||||
byPhase.delete(phase.title);
|
||||
}
|
||||
for (const [title, agents] of byPhase) groups.push({ title, agents });
|
||||
return groups;
|
||||
}
|
||||
|
||||
export function resultJson(value: unknown): string {
|
||||
const text =
|
||||
typeof value === "string"
|
||||
? value
|
||||
: safeStringify(value, {
|
||||
maxBytes: RESULT_JSON_MAX_BYTES * 2,
|
||||
maxDepth: 16,
|
||||
maxNodes: 10_000,
|
||||
});
|
||||
const truncation = truncateHead(text ?? "", {
|
||||
maxLines: RESULT_JSON_MAX_LINES,
|
||||
maxBytes: RESULT_JSON_MAX_BYTES,
|
||||
});
|
||||
return truncation.truncated
|
||||
? `${truncation.content}\n…[result truncated; bounded result artifact in result.json]`
|
||||
: truncation.content;
|
||||
}
|
||||
Reference in New Issue
Block a user