254 lines
8.0 KiB
TypeScript
254 lines
8.0 KiB
TypeScript
/**
|
|
* Domain model for subagents.
|
|
*
|
|
* Everything downstream of a backend (manager, tools, UI) speaks only these
|
|
* types. Backends translate their native streams (pi session events, Claude
|
|
* Agent SDK messages, Codex app-server JSON-RPC notifications) into the
|
|
* normalized `SubagentEvent` union.
|
|
*/
|
|
|
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
import { Data } from "effect";
|
|
|
|
export const BACKEND_NAMES = ["pi", "claude", "codex"] as const;
|
|
export type BackendName = (typeof BACKEND_NAMES)[number];
|
|
|
|
/** Who initiated the session. User asides stay out of model-facing tooling. */
|
|
export type SubagentOrigin = "model" | "btw";
|
|
|
|
/**
|
|
* Shared reasoning-effort scale (pi's thinking levels). Each backend maps a
|
|
* value to its nearest native equivalent: pi uses it directly, codex
|
|
* translates to its reasoning-effort slugs, claude translates to thinking
|
|
* budgets. Omitted = backend default (pi inherits the parent level).
|
|
*/
|
|
export const REASONING_EFFORTS = [
|
|
"off",
|
|
"minimal",
|
|
"low",
|
|
"medium",
|
|
"high",
|
|
"xhigh",
|
|
"max",
|
|
] as const;
|
|
export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
|
|
|
|
export type SubagentStatus = "running" | "done" | "error";
|
|
|
|
/** Parent-session context resolved by the tool layer and passed opaquely. */
|
|
export interface ParentContext {
|
|
readonly parentCwd: string;
|
|
readonly projectTrusted: boolean;
|
|
/** Parent pi model, for the pi backend's "inherit" default. */
|
|
readonly inheritedModel?: { readonly provider: string; readonly id: string };
|
|
readonly inheritedThinkingLevel?: string;
|
|
/** Parent model registry; required by the pi backend to resolve models. */
|
|
readonly modelRegistry?: ModelRegistry;
|
|
}
|
|
|
|
export interface SpawnTask {
|
|
/** Omitted for normal tool-driven spawns. */
|
|
readonly origin?: SubagentOrigin;
|
|
readonly prompt: string;
|
|
readonly title: string;
|
|
readonly cwd: string;
|
|
/**
|
|
* Generic model hint, interpreted per backend:
|
|
* pi: "provider/model-id" or bare model id; claude: model alias;
|
|
* codex: model slug. Omitted = backend default / inherit.
|
|
*/
|
|
readonly model?: string;
|
|
/** Shared effort scale; each backend maps it to its native equivalent. */
|
|
readonly reasoningEffort?: ReasoningEffort;
|
|
readonly parent: ParentContext;
|
|
}
|
|
|
|
export interface SubagentMeta {
|
|
readonly backend: BackendName;
|
|
/** Display label, e.g. "anthropic/claude-opus-4-5" or "gpt-5-codex". */
|
|
readonly modelLabel?: string;
|
|
/** Context window capacity for utilization display, when known. */
|
|
readonly contextWindow?: number;
|
|
/** pi session file / Claude projects JSONL / Codex rollout path. */
|
|
readonly sessionFilePath?: string;
|
|
/** Claude session id / Codex conversation id. */
|
|
readonly nativeSessionId?: string;
|
|
}
|
|
|
|
// --- Transcript ------------------------------------------------------------
|
|
|
|
export type TranscriptPart =
|
|
| { readonly type: "text"; readonly text: string }
|
|
| {
|
|
readonly type: "thinking";
|
|
readonly text: string;
|
|
readonly redacted?: boolean;
|
|
}
|
|
| {
|
|
readonly type: "toolCall";
|
|
readonly toolId: string;
|
|
readonly name: string;
|
|
readonly argsPreview?: string;
|
|
};
|
|
|
|
export type TranscriptItem =
|
|
| { readonly kind: "user"; readonly text: string }
|
|
| {
|
|
readonly kind: "assistant";
|
|
readonly parts: ReadonlyArray<TranscriptPart>;
|
|
}
|
|
| {
|
|
readonly kind: "toolResult";
|
|
readonly toolId: string;
|
|
readonly name: string;
|
|
readonly isError: boolean;
|
|
readonly outputPreview?: string;
|
|
};
|
|
|
|
export interface LiveToolState {
|
|
readonly toolId: string;
|
|
readonly name: string;
|
|
readonly argsPreview?: string;
|
|
readonly outputPreview?: string;
|
|
readonly done?: boolean;
|
|
readonly isError?: boolean;
|
|
}
|
|
|
|
export interface QueuedMessage {
|
|
readonly text: string;
|
|
readonly kind: "steer" | "follow-up";
|
|
}
|
|
|
|
// --- Events ------------------------------------------------------------------
|
|
|
|
export type RunOutcome =
|
|
| { readonly _tag: "Completed"; readonly finalText: string }
|
|
| {
|
|
readonly _tag: "Failed";
|
|
readonly errorText: string;
|
|
readonly partialText?: string;
|
|
}
|
|
| { readonly _tag: "Interrupted"; readonly partialText?: string };
|
|
|
|
/**
|
|
* Normalized activity stream. Previews (`argsPreview`, `outputPreview`) are
|
|
* pre-flattened single-line strings because the UI only ever renders one
|
|
* sanitized line, which keeps three different native tool-result shapes out
|
|
* of the interface.
|
|
*/
|
|
export type SubagentEvent =
|
|
// lifecycle (a session can run multiple turns via send())
|
|
| { readonly _tag: "RunStarted" }
|
|
| { readonly _tag: "RunSettled"; readonly outcome: RunOutcome }
|
|
// transcript building blocks
|
|
| { readonly _tag: "UserMessage"; readonly text: string }
|
|
| {
|
|
readonly _tag: "AssistantDelta";
|
|
readonly kind: "text" | "thinking";
|
|
readonly delta: string;
|
|
}
|
|
| {
|
|
readonly _tag: "AssistantMessage";
|
|
readonly parts: ReadonlyArray<TranscriptPart>;
|
|
}
|
|
| {
|
|
readonly _tag: "ToolStart";
|
|
readonly toolId: string;
|
|
readonly name: string;
|
|
readonly argsPreview?: string;
|
|
}
|
|
| {
|
|
readonly _tag: "ToolUpdate";
|
|
readonly toolId: string;
|
|
readonly outputPreview?: string;
|
|
}
|
|
| {
|
|
readonly _tag: "ToolEnd";
|
|
readonly toolId: string;
|
|
readonly name: string;
|
|
readonly isError: boolean;
|
|
readonly outputPreview?: string;
|
|
}
|
|
// bookkeeping
|
|
| {
|
|
readonly _tag: "QueueChanged";
|
|
readonly queued: ReadonlyArray<QueuedMessage>;
|
|
}
|
|
| {
|
|
readonly _tag: "UsageChanged";
|
|
readonly tokens?: number;
|
|
readonly contextWindow?: number;
|
|
}
|
|
| { readonly _tag: "MetaChanged"; readonly meta: Partial<SubagentMeta> }
|
|
/** Non-fatal diagnostics. Fatal failures arrive as a RunSettled outcome. */
|
|
| { readonly _tag: "BackendError"; readonly message: string };
|
|
|
|
// --- Snapshot ---------------------------------------------------------------
|
|
|
|
/**
|
|
* The manager folds `SubagentEvent`s into one snapshot per subagent. This is
|
|
* everything the tools, footer status, and both TUI views read.
|
|
*/
|
|
export interface SubagentSnapshot {
|
|
readonly id: string;
|
|
readonly origin: SubagentOrigin;
|
|
readonly backend: BackendName;
|
|
readonly title: string;
|
|
readonly prompt: string;
|
|
readonly cwd: string;
|
|
readonly status: SubagentStatus;
|
|
readonly createdAt: number;
|
|
readonly settledAt?: number;
|
|
readonly errorText?: string;
|
|
readonly meta: SubagentMeta;
|
|
readonly usage: { readonly tokens?: number; readonly contextWindow?: number };
|
|
readonly transcript: ReadonlyArray<TranscriptItem>;
|
|
/** Streaming assistant buffers, cleared when the finalized message lands. */
|
|
readonly liveAssistant?: { readonly text: string; readonly thinking: string };
|
|
readonly liveTools: ReadonlyArray<LiveToolState>;
|
|
readonly queued: ReadonlyArray<QueuedMessage>;
|
|
/** Final text of the most recent completed run (v1 `finalOutput`). */
|
|
readonly finalText: string;
|
|
/** Count of finalized assistant messages (for subagent_check). */
|
|
readonly turns: number;
|
|
}
|
|
|
|
/** Final text, or the live streaming buffer while a run is active (v1 `latestOutput`). */
|
|
export function latestText(snap: SubagentSnapshot) {
|
|
const live = snap.liveAssistant?.text.trim();
|
|
if (live) return live;
|
|
return snap.finalText;
|
|
}
|
|
|
|
export function formatElapsed(snap: SubagentSnapshot) {
|
|
const end = snap.settledAt ?? Date.now();
|
|
const totalSeconds = Math.max(0, Math.round((end - snap.createdAt) / 1000));
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|
const seconds = totalSeconds % 60;
|
|
return minutes > 0
|
|
? `${minutes}m${seconds.toString().padStart(2, "0")}s`
|
|
: `${seconds}s`;
|
|
}
|
|
|
|
// --- Errors -------------------------------------------------------------------
|
|
|
|
export class SpawnError extends Data.TaggedError("SpawnError")<{
|
|
readonly message: string;
|
|
}> {}
|
|
|
|
export class BackendUnavailableError extends Data.TaggedError(
|
|
"BackendUnavailableError",
|
|
)<{
|
|
readonly message: string;
|
|
}> {}
|
|
|
|
export class ConcurrencyLimitError extends Data.TaggedError(
|
|
"ConcurrencyLimitError",
|
|
)<{
|
|
readonly message: string;
|
|
}> {}
|
|
|
|
export class SendError extends Data.TaggedError("SendError")<{
|
|
readonly message: string;
|
|
}> {}
|