Initialize repository with harness code and assets

Initial import of all source code, config, and README assets: the
packages workspace (cli, core, server, web, docs, landing, skills),
build scripts, tooling config, and CI workflows.

Includes the data-layout revision made on this branch: the local data
root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the
installer keeps its binaries in ~/.penguin), and every Agent lives
under <project>/agents/<agent>/ — path helpers, the three
agent-enumeration scans, the system prompt, built-in Skills, tests
and docs all follow the new layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
Yaowei Zheng
2026-07-19 14:06:53 +08:00
committed by GitHub
parent 056bed7aeb
commit 45bfae6e94
543 changed files with 92949 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
/**
* Aggregates streaming partial_* messages into a complete model_msg.
*
* When recording a Trace, streaming `partial_*` messages must first be joined into a complete
* `model_msg` before writing. This module provides:
* - `PartialAggregator`: a stateful aggregator, pushed one message at a time, producing a
* complete message when a fragment ends with `stop`;
* - `aggregateAll`: a one-shot pass that collapses `partial_*` messages in an array into
* complete messages.
*
* Complete / event / session_meta messages pass through unchanged, preserving their original
* order.
* Docs: /docs/omni-message § "The streaming discipline".
*/
import { assistantText, thinkingMessage, toolCall, toolCallOutput } from "./builders.js";
import type { OmniMessage, PartialModelPayload, StopReason } from "./types.js";
import { isPartialPayload } from "./types.js";
type PartialKind = PartialModelPayload["type"];
interface OpenFragment {
kind: PartialKind;
/** Accumulation buffer for text / thinking / tool_call arguments / tool_call_output. */
buffer: string;
name?: string;
toolCallId?: string;
/** Images carried by tool_call_output (images aren't incremental — a single delta carries the whole set; a later one overwrites). */
images?: string[];
lastStopReason: StopReason;
}
/** Merge key for partial fragments: same type + same tool_call_id counts as the same fragment. */
function fragmentKey(p: PartialModelPayload): string {
const id = "tool_call_id" in p ? p.tool_call_id : "";
return `${p.type}::${id}`;
}
function finalize(frag: OpenFragment): OmniMessage {
switch (frag.kind) {
case "partial_text":
return assistantText(frag.buffer, frag.lastStopReason);
case "partial_thinking":
return thinkingMessage(frag.buffer, frag.lastStopReason);
case "partial_tool_call":
return toolCall({
name: frag.name ?? "",
arguments: frag.buffer,
toolCallId: frag.toolCallId ?? "",
stopReason: frag.lastStopReason,
});
case "partial_tool_call_output":
return toolCallOutput({
output: frag.buffer,
toolCallId: frag.toolCallId ?? "",
stopReason: frag.lastStopReason,
...(frag.images !== undefined ? { images: frag.images } : {}),
});
}
}
function appendDelta(frag: OpenFragment, p: PartialModelPayload): void {
switch (p.type) {
case "partial_text":
frag.buffer += p.text;
break;
case "partial_thinking":
frag.buffer += p.thinking;
break;
case "partial_tool_call":
frag.buffer += p.arguments;
if (p.name) frag.name = p.name;
frag.toolCallId = p.tool_call_id;
break;
case "partial_tool_call_output":
frag.buffer += p.output;
if (p.images && p.images.length > 0) frag.images = p.images;
frag.toolCallId = p.tool_call_id;
break;
}
if (p.stop_reason !== undefined) frag.lastStopReason = p.stop_reason;
}
function newFragment(p: PartialModelPayload): OpenFragment {
const frag: OpenFragment = {
kind: p.type,
buffer: "",
lastStopReason: "completed",
};
if (p.type === "partial_tool_call") {
frag.name = p.name;
frag.toolCallId = p.tool_call_id;
} else if (p.type === "partial_tool_call_output") {
frag.toolCallId = p.tool_call_id;
}
return frag;
}
/**
* Stateful aggregator. Pushed one message at a time via `push`:
* - complete / event / session_meta messages are returned unchanged;
* - `partial_*` messages accumulate into an internal fragment, producing a complete message
* when `event_type === "stop"`;
* - `flush` forcibly emits any fragments that haven't yet received a stop.
*/
export class PartialAggregator {
private open = new Map<string, OpenFragment>();
push(msg: OmniMessage): OmniMessage[] {
if (!isPartialPayload(msg.payload)) {
return [msg];
}
const p = msg.payload;
const key = fragmentKey(p);
let frag = this.open.get(key);
if (p.event_type === "start") {
// start reopens a fragment; if a fragment with the same key already exists (out-of-order), finalize it first.
const out: OmniMessage[] = [];
if (frag) out.push(finalize(frag));
frag = newFragment(p);
appendDelta(frag, p);
this.open.set(key, frag);
return out;
}
if (!frag) {
// delta/stop without a preceding start: handle leniently, creating a new fragment as needed.
frag = newFragment(p);
this.open.set(key, frag);
}
appendDelta(frag, p);
if (p.event_type === "stop") {
this.open.delete(key);
return [finalize(frag)];
}
return [];
}
/** Finalizes: emits all still-open fragments (in the order they were opened). */
flush(): OmniMessage[] {
const out = [...this.open.values()].map(finalize);
this.open.clear();
return out;
}
}
/** One-shot aggregation: keeps non-partial messages in their original order, collapsing partial ones into complete messages. */
export function aggregateAll(messages: OmniMessage[]): OmniMessage[] {
const agg = new PartialAggregator();
const out: OmniMessage[] = [];
for (const msg of messages) out.push(...agg.push(msg));
out.push(...agg.flush());
return out;
}
+342
View File
@@ -0,0 +1,342 @@
/**
* OmniMessage builders. All modules create messages exclusively through these builders, avoiding
* ad hoc protocol structures scattered across the codebase.
* Every builder writes an ISO 8601 UTC timestamp.
* Docs: /docs/omni-message § "Builders and guards".
*/
import type {
AbortPayload,
ApprovalDecision,
ApprovalDecisionPayload,
CompactionBeginPayload,
CompactionEndPayload,
CompactionMode,
CompactionReason,
EventMessage,
ImageUrlPayload,
InlineDataPayload,
InlineThinkingPayload,
MessageOrigin,
ModelMessage,
OmniMessage,
PartialTextPayload,
PartialThinkingPayload,
PartialToolCallOutputPayload,
PartialToolCallPayload,
RequestBeginPayload,
RequestEndPayload,
Role,
SessionMetaMessage,
SessionMetaPayload,
StopReason,
StreamEventType,
SubagentPayload,
TextPayload,
ThinkingPayload,
TokenCounts,
TokenUsagePayload,
ToolCallOutputPayload,
ToolCallPayload,
} from "./types.js";
/** The current moment's ISO 8601 UTC timestamp. */
function nowIso(): string {
return new Date().toISOString();
}
function model<P extends ModelMessage["payload"]>(payload: P): OmniMessage<P> {
return { timestamp: nowIso(), type: "model_msg", payload };
}
function event<P extends EventMessage["payload"]>(payload: P): OmniMessage<P> {
return { timestamp: nowIso(), type: "event_msg", payload };
}
// session_meta ---------------------------------------------------------------
export function sessionMeta(payload: SessionMetaPayload): SessionMetaMessage {
return { timestamp: nowIso(), type: "session_meta", payload };
}
// Complete model_msg -----------------------------------------------------------
/**
* Provider fidelity fields: kept as-is and restored verbatim on replay.
* Builder convention: positional-argument-style builders carry these in a trailing `fidelity`
* object (narrowed via Pick per payload type — e.g. thinking only has signature); object-argument-
* style builders (toolCall) flatten `fidelity` fields into the parameter object alongside
* `stopReason`, mirroring the payload structure directly.
*/
export interface FidelityFields {
phase?: string | null;
signature?: string;
}
export function textMessage(
role: Role,
text: string,
stopReason: StopReason = "completed",
fidelity?: FidelityFields,
): OmniMessage<TextPayload> {
return model({
type: "text",
role,
text,
stop_reason: stopReason,
...(fidelity?.phase != null ? { phase: fidelity.phase } : {}),
...(fidelity?.signature !== undefined ? { signature: fidelity.signature } : {}),
});
}
export const userText = (text: string): OmniMessage<TextPayload> => textMessage("user", text);
export const assistantText = (
text: string,
stopReason: StopReason = "completed",
fidelity?: FidelityFields,
): OmniMessage<TextPayload> => textMessage("assistant", text, stopReason, fidelity);
export function imageUrlMessage(imageUrl: string): OmniMessage<ImageUrlPayload> {
return model({
type: "image_url",
role: "user",
image_url: imageUrl,
stop_reason: "completed",
});
}
export function inlineData(
role: Role,
data: string,
mimeType: string,
fidelity?: Pick<FidelityFields, "signature">,
): OmniMessage<InlineDataPayload> {
return model({
type: "inline_data",
role,
data,
mime_type: mimeType,
stop_reason: "completed",
...(fidelity?.signature !== undefined ? { signature: fidelity.signature } : {}),
});
}
export function thinkingMessage(
thinking: string,
stopReason: StopReason = "completed",
fidelity?: Pick<FidelityFields, "signature">,
): OmniMessage<ThinkingPayload> {
return model({
type: "thinking",
role: "assistant",
thinking,
stop_reason: stopReason,
...(fidelity?.signature !== undefined ? { signature: fidelity.signature } : {}),
});
}
export function inlineThinking(
data: string,
mimeType: string,
fidelity?: Pick<FidelityFields, "signature">,
): OmniMessage<InlineThinkingPayload> {
return model({
type: "inline_thinking",
role: "assistant",
data,
mime_type: mimeType,
stop_reason: "completed",
...(fidelity?.signature !== undefined ? { signature: fidelity.signature } : {}),
});
}
export function toolCall(args: {
name: string;
arguments: string;
toolCallId: string;
stopReason?: StopReason;
signature?: string;
}): OmniMessage<ToolCallPayload> {
return model({
type: "tool_call",
role: "assistant",
name: args.name,
arguments: args.arguments,
tool_call_id: args.toolCallId,
stop_reason: args.stopReason ?? "completed",
...(args.signature !== undefined ? { signature: args.signature } : {}),
});
}
export function toolCallOutput(args: {
output: string;
toolCallId: string;
stopReason?: StopReason;
/** Images carried by the tool output (array of data URLs); images aren't incremental — a single delta carries the whole set in the streaming path, and the complete message carries them too. */
images?: string[];
}): OmniMessage<ToolCallOutputPayload> {
return model({
type: "tool_call_output",
role: "user",
output: args.output,
...(args.images !== undefined && args.images.length > 0 ? { images: args.images } : {}),
tool_call_id: args.toolCallId,
stop_reason: args.stopReason ?? "completed",
});
}
// Streaming partial_* model_msg -------------------------------------------------
export function partialText(
eventType: StreamEventType,
text = "",
stopReason: StopReason = "completed",
): OmniMessage<PartialTextPayload> {
return model({
type: "partial_text",
role: "assistant",
event_type: eventType,
text,
stop_reason: stopReason,
});
}
export function partialThinking(
eventType: StreamEventType,
thinking = "",
stopReason: StopReason = "completed",
): OmniMessage<PartialThinkingPayload> {
return model({
type: "partial_thinking",
role: "assistant",
event_type: eventType,
thinking,
stop_reason: stopReason,
});
}
export function partialToolCall(args: {
eventType: StreamEventType;
name: string;
arguments?: string;
toolCallId: string;
stopReason?: StopReason;
}): OmniMessage<PartialToolCallPayload> {
return model({
type: "partial_tool_call",
role: "assistant",
event_type: args.eventType,
name: args.name,
arguments: args.arguments ?? "",
tool_call_id: args.toolCallId,
stop_reason: args.stopReason ?? "completed",
});
}
export function partialToolCallOutput(args: {
eventType: StreamEventType;
output?: string;
toolCallId: string;
stopReason?: StopReason;
/** Images carried by the tool output (array of data URLs); images aren't incremental — a single delta carries the whole set. */
images?: string[];
}): OmniMessage<PartialToolCallOutputPayload> {
return model({
type: "partial_tool_call_output",
role: "user",
event_type: args.eventType,
output: args.output ?? "",
...(args.images !== undefined && args.images.length > 0 ? { images: args.images } : {}),
tool_call_id: args.toolCallId,
stop_reason: args.stopReason ?? "completed",
});
}
// event_msg -------------------------------------------------------------------
export function approvalDecision(
decision: ApprovalDecision,
toolCallId: string,
): OmniMessage<ApprovalDecisionPayload> {
return event({ type: "approval_decision", decision, tool_call_id: toolCallId });
}
export function abortEvent(reason: string | null = null): OmniMessage<AbortPayload> {
return event({ type: "abort", reason });
}
/** request begin event: marks the start of one LLM Request. */
export function requestBegin(): OmniMessage<RequestBeginPayload> {
return event({ type: "request_begin" });
}
/** request end event: carries the terminal state (`completed` means this turn was already committed to AgentHub). */
export function requestEnd(status: StopReason): OmniMessage<RequestEndPayload> {
return event({ type: "request_end", status });
}
/** compaction begin event: carries the trigger reason, mode, current context usage, and cumulative Session turn count. */
export function compactionBegin(args: {
reason: CompactionReason;
mode: CompactionMode;
context: number;
turns: number;
}): OmniMessage<CompactionBeginPayload> {
return event({
type: "compaction_begin",
reason: args.reason,
mode: args.mode,
context: args.context,
turns: args.turns,
});
}
/** compaction end event: carries the compaction result (non-`completed` means compaction was abandoned and the original context is kept). */
export function compactionEnd(args: {
reason: CompactionReason;
mode: CompactionMode;
status: StopReason;
}): OmniMessage<CompactionEndPayload> {
return event({
type: "compaction_end",
reason: args.reason,
mode: args.mode,
status: args.status,
});
}
/** subagent derivation pointer event: records only the direct child session's Session id (written to the parent Trace by context_engine). */
export function subagentEvent(sessionId: string): OmniMessage<SubagentPayload> {
return event({ type: "subagent", session_id: sessionId });
}
export function emptyTokenCounts(): TokenCounts {
return { cache_read: 0, cache_write: 0, output: 0, total: 0 };
}
export function tokenUsage(
session: TokenCounts,
request: TokenCounts,
): OmniMessage<TokenUsagePayload> {
return event({ type: "token_usage", session, request });
}
/** Adds two sets of Token counts together, used to maintain cumulative Session usage. */
export function addTokenCounts(a: TokenCounts, b: TokenCounts): TokenCounts {
return {
cache_read: a.cache_read + b.cache_read,
cache_write: a.cache_write + b.cache_write,
output: a.output + b.output,
total: a.total + b.total,
};
}
/**
* Marks a message with a nested-origin tag: prepends one hop (a child Session id) to the front
* of `origin`, outer-to-inner.
* Used by host tools (e.g. run_subagent) when forwarding child-session messages; an absent
* `origin` means the message comes from the main Session.
*/
export function withOrigin<M extends OmniMessage>(msg: M, sessionId: MessageOrigin): M {
return { ...msg, origin: [sessionId, ...(msg.origin ?? [])] };
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./types.js";
export * from "./builders.js";
export * from "./aggregate.js";
+400
View File
@@ -0,0 +1,400 @@
/**
* OmniMessage — PenguinHarness's primary message protocol.
*
* All messages share one envelope: `timestamp` (ISO 8601 UTC), `type`, and `payload`.
* The outer `type` falls into three categories:
* - `session_meta`: Session metadata;
* - `model_msg`: model input/output messages (both complete messages and streaming
* `partial_*` messages);
* - `event_msg`: control/statistics events during execution.
*
* Trace records only: `session_meta`, complete `model_msg`, and all `event_msg`;
* the Human interface communicates using: complete `model_msg`, streaming `partial_*`, and all
* `event_msg`.
*
* Docs: packages/docs/content/omni-message.{zh,en}.md (site path /docs/omni-message) documents
* this protocol payload-for-payload — keep the page in sync when changing types here.
*/
/** The outer message category. */
export type OmniMessageType = "session_meta" | "model_msg" | "event_msg";
/** The message's originating role. */
export type Role = "user" | "assistant";
/**
* The reason a model response or message generation ended. Only five protocol values are
* allowed:
* - `completed`: finished normally, including completed text, thinking, tool requests, or
* tool output;
* - `failed`: a non-retryable error or tool execution failure;
* - `aborted`: user-initiated interruption or cancellation;
* - `timeout`: LLM request timed out;
* - `malformed`: the LLM response was malformed (e.g. AgentHub JSON parsing exception).
* Only LLM timeout / malformed trigger a context_engine reconnect.
* Docs: /docs/omni-message § "stop_reason".
*/
export type StopReason = "completed" | "failed" | "aborted" | "timeout" | "malformed";
/** The event phase of a streaming fragment. `stop` marks the end of a fragment and usually carries no incremental content. */
export type StreamEventType = "start" | "delta" | "stop";
/**
* Nested-origin marker: a child Session id. The message envelope's `origin` is a chain of child
* Session ids ordered **outer-to-inner**, identifying that the message comes from a nested child
* session (e.g. a child Session derived by `run_subagent`); each layer of host-tool forwarding
* prepends one more hop at the front. **An absent `origin` (the message carries no `origin`)
* means the message comes from the main Session itself** (an empty array is never produced
* either). Only session_id is recorded: the corresponding tool_call / agent info can be obtained
* from the `run_subagent` tool_call in the parent session's stream and the child Session's own
* Trace (session_meta).
* Docs: /docs/omni-message § "origin: the Subagent chain".
*/
export type MessageOrigin = string;
/** The approval decision for a tool call. */
export type ApprovalDecision = "allow" | "deny";
/** Token counts (input/output/cache/total). */
export interface TokenCounts {
cache_read: number;
cache_write: number;
output: number;
total: number;
}
// ---------------------------------------------------------------------------
// session_meta
// ---------------------------------------------------------------------------
// Docs: /docs/omni-message § "session_meta"
/** Tool definition passed to the LLM (OpenAI/JSON Schema style). */
export interface ToolDefinition {
name: string;
description: string;
parameters?: Record<string, unknown>;
}
export interface SessionMetaPayload {
session_id: string;
/** The session model's provider group (paired with `model_id` to form a model reference). */
provider: string;
/** The session model's upstream model_id (the request id sent to AgentHub; paired with `provider`). */
model_id: string;
model_context_window: number | string;
/** The system prompt actually used by this Session (the assembled result with environment placeholders already substituted). */
system_prompt: string;
/** The list of tool definitions this Session exposes to the model (full schema, matching what's sent to the LLM). */
tools: ToolDefinition[];
/** The model's thinking level (from system_config.model.thinking_level; "default" when unconfigured). */
thinking_level: string;
/** Absolute path to the Agent State. */
agent_state: string;
/** Absolute path to the Workspace. */
workspace: string;
}
// ---------------------------------------------------------------------------
// model_msg — complete messages
// ---------------------------------------------------------------------------
// Docs: /docs/omni-message § "model_msg: complete payloads"
export interface TextPayload {
type: "text";
role: Role;
text: string;
stop_reason?: StopReason;
/** Provider fidelity field: text phase marker (e.g. GPT-5 segments by phase), kept as-is and restored verbatim. */
phase?: string | null;
/** Provider fidelity field: signature, kept as-is and restored verbatim. */
signature?: string;
}
export interface ImageUrlPayload {
type: "image_url";
role: "user";
/** A web URL or a base64 data URL. */
image_url: string;
stop_reason?: StopReason;
}
export interface InlineDataPayload {
type: "inline_data";
role: Role;
/** Base64-encoded bytes. */
data: string;
mime_type: string;
stop_reason?: StopReason;
/** Provider fidelity field: signature, kept as-is and restored verbatim. */
signature?: string;
}
export interface ThinkingPayload {
type: "thinking";
role: "assistant";
thinking: string;
stop_reason?: StopReason;
/**
* Provider fidelity field: thinking-block signature (Claude thinking blocks / redacted
* thinking, GPT-5 encrypted reasoning, etc. — **required** when some models replay history),
* kept as-is and restored verbatim — losing it breaks Session resumption.
*/
signature?: string;
}
export interface InlineThinkingPayload {
type: "inline_thinking";
role: "assistant";
/** Base64-encoded bytes. */
data: string;
mime_type: string;
stop_reason?: StopReason;
/** Provider fidelity field: signature, kept as-is and restored verbatim. */
signature?: string;
}
export interface ToolCallPayload {
type: "tool_call";
role: "assistant";
name: string;
/** Tool arguments as a JSON string. */
arguments: string;
tool_call_id: string;
stop_reason?: StopReason;
/** Provider fidelity field: signature, kept as-is and restored verbatim. */
signature?: string;
}
export interface ToolCallOutputPayload {
type: "tool_call_output";
role: "user";
output: string;
/**
* Images carried by the tool output (optional): each is a `data:<mime>;base64,...` data URL,
* fed back to the model alongside the text (e.g. images read by read_image). Images aren't
* incremental: the streaming path carries the whole set once via a single delta (see
* `PartialToolCallOutputPayload.images`), and the complete message carries them again — the
* streamed-and-joined result equals the complete message.
*/
images?: string[];
tool_call_id: string;
stop_reason?: StopReason;
}
// ---------------------------------------------------------------------------
// model_msg — streaming partial_* messages
// ---------------------------------------------------------------------------
// Docs: /docs/omni-message § "model_msg: streaming partials"
export interface PartialTextPayload {
type: "partial_text";
role: "assistant";
event_type: StreamEventType;
text: string;
stop_reason?: StopReason;
}
export interface PartialThinkingPayload {
type: "partial_thinking";
role: "assistant";
event_type: StreamEventType;
thinking: string;
stop_reason?: StopReason;
}
export interface PartialToolCallPayload {
type: "partial_tool_call";
role: "assistant";
event_type: StreamEventType;
name: string;
/** Incremental fragment of the arguments JSON. */
arguments: string;
tool_call_id: string;
stop_reason?: StopReason;
}
export interface PartialToolCallOutputPayload {
type: "partial_tool_call_output";
role: "user";
event_type: StreamEventType;
output: string;
/** Images carried by the tool output (optional): images aren't incremental, carried as a whole by a single delta (consistent with the complete message). */
images?: string[];
tool_call_id: string;
stop_reason?: StopReason;
}
// ---------------------------------------------------------------------------
// event_msg
// ---------------------------------------------------------------------------
// Docs: /docs/omni-message § "event_msg"
export interface ApprovalDecisionPayload {
type: "approval_decision";
decision: ApprovalDecision;
tool_call_id: string;
}
export interface AbortPayload {
type: "abort";
reason?: string | null;
}
export interface TokenUsagePayload {
type: "token_usage";
/** Current Session cumulative token usage. */
session: TokenCounts;
/** Token usage for the most recent Request. */
request: TokenCounts;
}
/**
* Request boundary event: the boundary of one LLM Request, produced **in pairs** by
* `context_engine` and written to Trace. `request_end`
* with `status` of `completed` means the turn has been committed by AgentHub — this is the
* mechanical criterion Trace replay (Session resumption) uses to determine whether a turn was
* committed, and it also gives performance analysis a basis for Request latency and turn counts.
* A compaction request produces this same event pair too (written to Trace only, not streamed).
*/
export interface RequestBeginPayload {
type: "request_begin";
}
export interface RequestEndPayload {
type: "request_end";
/** Terminal state of this Request (reuses the five StopReason values, sharing its source with this turn's complete message's stop_reason / LLMOutcome). */
status: StopReason;
}
/** Compaction trigger reason: context threshold / turn-count threshold / user-initiated request. */
export type CompactionReason = "context" | "turns" | "manual";
/** Context compaction mode: summary relay / direct discard. */
export type CompactionMode = "summarize" | "discard";
/**
* Compaction boundary event: the compaction process exposes
* only this event pair to Human, produced **in pairs** by `context_engine`. Both `reason` and
* `mode` are carried on both events, for stateless frontend rendering; `status` reuses the
* five-value `StopReason` protocol (compaction converges to a terminal state, taking
* `completed` / `failed` / `aborted` in practice — `timeout` / `malformed` are handled internally
* by the compaction request's existing retry mechanism, collapsing to `failed` once retries are
* exhausted).
*/
export interface CompactionBeginPayload {
type: "compaction_begin";
reason: CompactionReason;
mode: CompactionMode;
/** Current context token usage (the most recent token_usage's request.total). */
context: number;
/** Session cumulative turn count. */
turns: number;
}
export interface CompactionEndPayload {
type: "compaction_end";
reason: CompactionReason;
mode: CompactionMode;
/** Compaction result; non-`completed` means compaction was abandoned and the original context was kept. */
status: StopReason;
}
/**
* Subagent pointer event: when the parent Session spawns a
* **direct** child session, `context_engine` writes this to the parent Trace (not streamed),
* recording only the child session's Session id — the child session's other details live in its
* own Trace's `session_meta`. When the session is reopened, the server uses this to recursively
* expand the child Trace and reconstruct the `origin` chain; a grandchild session's pointer is
* recorded by the child Trace itself.
*/
export interface SubagentPayload {
type: "subagent";
/** The direct child session's Session id. */
session_id: string;
}
// ---------------------------------------------------------------------------
// Union types and the message envelope
// ---------------------------------------------------------------------------
/** Complete model_msg payload (written to Trace and exposed externally). */
export type CompleteModelPayload =
| TextPayload
| ImageUrlPayload
| InlineDataPayload
| ThinkingPayload
| InlineThinkingPayload
| ToolCallPayload
| ToolCallOutputPayload;
/** Streaming model_msg payload. */
export type PartialModelPayload =
| PartialTextPayload
| PartialThinkingPayload
| PartialToolCallPayload
| PartialToolCallOutputPayload;
export type ModelPayload = CompleteModelPayload | PartialModelPayload;
export type EventPayload =
| ApprovalDecisionPayload
| AbortPayload
| RequestBeginPayload
| RequestEndPayload
| TokenUsagePayload
| CompactionBeginPayload
| CompactionEndPayload
| SubagentPayload;
export type OmniPayload = SessionMetaPayload | ModelPayload | EventPayload;
/** The unified message envelope. */
export interface OmniMessage<P extends OmniPayload = OmniPayload> {
/** ISO 8601 UTC timestamp. */
timestamp: string;
type: OmniMessageType;
payload: P;
/** Nested-origin marker: the chain of child Session ids ordered outer-to-inner; absent = from the main Session (see MessageOrigin). */
origin?: MessageOrigin[];
}
// Convenience aliases for concrete message types --------------------------------
export type SessionMetaMessage = OmniMessage<SessionMetaPayload>;
export type ModelMessage = OmniMessage<ModelPayload>;
export type EventMessage = OmniMessage<EventPayload>;
export type CompleteModelMessage = OmniMessage<CompleteModelPayload>;
export type PartialModelMessage = OmniMessage<PartialModelPayload>;
// ---------------------------------------------------------------------------
// Runtime discrimination helpers
// ---------------------------------------------------------------------------
/** The set of type values for streaming partial_* payloads. */
const PARTIAL_PAYLOAD_TYPES = [
"partial_text",
"partial_thinking",
"partial_tool_call",
"partial_tool_call_output",
] as const;
export function isPartialPayload(p: OmniPayload): p is PartialModelPayload {
return (PARTIAL_PAYLOAD_TYPES as readonly string[]).includes((p as { type?: string }).type ?? "");
}
export function isModelMessage(msg: OmniMessage): msg is ModelMessage {
return msg.type === "model_msg";
}
export function isEventMessage(msg: OmniMessage): msg is EventMessage {
return msg.type === "event_msg";
}
export function isSessionMeta(msg: OmniMessage): msg is SessionMetaMessage {
return msg.type === "session_meta";
}
/** A complete model_msg (not partial_*), i.e. a message that can be written to Trace. */
export function isCompleteModelMessage(msg: OmniMessage): msg is CompleteModelMessage {
return msg.type === "model_msg" && !isPartialPayload(msg.payload);
}