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:
@@ -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;
|
||||
}
|
||||
@@ -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 ?? [])] };
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./types.js";
|
||||
export * from "./builders.js";
|
||||
export * from "./aggregate.js";
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user