import type { AgentMessage } from "@earendil-works/pi-agent-core";
import { type AgentObserveAgentSummary, createAgentObserveMessagePreview } from "./agent-observe.js";
import type { HostRequestHandler } from "./kernel/index.js";
export const AGENT_SESSIONS_HOST_PREFIX = "agent.sessions";
export type AgentSessionCapability = "send" | "recent_turns" | "stop" | "restart";
export type AgentSessionDelivery = "steer" | "follow_up";
export type AgentSessionSummary = AgentObserveAgentSummary & {
capabilities: readonly AgentSessionCapability[];
};
export interface AgentSessionRecentTurnsInput {
target: string;
count?: number;
maxChars?: number;
}
export interface AgentSessionTurnSummary {
user: string;
assistant: string;
toolCalls: number;
startedAt?: number;
endedAt?: number;
messageCount: number;
truncated: boolean;
}
export interface AgentSessionRecentTurnsResult {
agent: AgentSessionSummary;
turns: AgentSessionTurnSummary[];
count: number;
maxChars: number;
truncated: boolean;
}
export interface AgentSessionSendInput {
target: string;
message: string;
delivery: AgentSessionDelivery;
}
export interface AgentSessionSendResult {
operationId: string;
status: "accepted" | "queued";
delivery: AgentSessionDelivery;
target: {
activeSessionId: string;
sessionId: string;
sessionName?: string;
};
}
export interface AgentSessionOperationResult {
operationId: string;
status: "accepted";
}
export interface AgentSessionController {
listSessions(): AgentSessionSummary[] | Promise<AgentSessionSummary[]>;
getSession(target: string): AgentSessionSummary | Promise<AgentSessionSummary>;
recentTurns(
input: AgentSessionRecentTurnsInput,
): AgentSessionRecentTurnsResult | Promise<AgentSessionRecentTurnsResult>;
send(input: AgentSessionSendInput): AgentSessionSendResult | Promise<AgentSessionSendResult>;
stop(target: string): AgentSessionOperationResult | Promise<AgentSessionOperationResult>;
restart(target: string): AgentSessionOperationResult | Promise<AgentSessionOperationResult>;
}
export function createAgentSessionHostHandlers(controller: AgentSessionController): Record<string, HostRequestHandler> {
return {
"agent.sessions.list": async () => ({ sessions: await controller.listSessions() }),
"agent.sessions.get": async (payload: Record<string, unknown> = {}) => ({
session: await controller.getSession(requireString(payload, "target", "agent.sessions.get target")),
}),
"agent.sessions.recent_turns": async (payload: Record<string, unknown> = {}) =>
(await controller.recentTurns({
target: requireString(payload, "target", "agent.sessions.recent_turns target"),
count: normalizeOptionalInteger(payload.count, "agent.sessions.recent_turns count"),
maxChars: normalizeOptionalInteger(
payload.max_chars ?? payload.maxChars,
"agent.sessions.recent_turns max_chars",
),
})) as unknown as Record<string, unknown>,
"agent.sessions.send": async (payload: Record<string, unknown> = {}) =>
(await controller.send({
target: requireString(payload, "target", "agent.sessions.send target"),
message: requireString(payload, "message", "agent.sessions.send message"),
delivery: normalizeDelivery(payload.delivery),
})) as unknown as Record<string, unknown>,
"agent.sessions.stop": async (payload: Record<string, unknown> = {}) =>
(await controller.stop(requireString(payload, "target", "agent.sessions.stop target"))) as unknown as Record<
string,
unknown
>,
"agent.sessions.restart": async (payload: Record<string, unknown> = {}) =>
(await controller.restart(
requireString(payload, "target", "agent.sessions.restart target"),
)) as unknown as Record<string, unknown>,
};
}
export function normalizeAgentSessionTurnCount(count: number | undefined): number {
return clampInteger(count ?? 3, 1, 20, "agent.sessions.recent_turns count");
}
export function normalizeAgentSessionMaxChars(maxChars: number | undefined): number {
return clampInteger(maxChars ?? 6_000, 80, 12_000, "agent.sessions.recent_turns max_chars");
}
export function createAgentSessionTurnSummaries(
messages: AgentMessage[],
count: number,
maxChars: number,
): { turns: AgentSessionTurnSummary[]; truncated: boolean } {
const previews = messages.map((message, index) => createAgentObserveMessagePreview(message, index, maxChars));
const allTurns: MutableTurn[] = [];
let current: MutableTurn | undefined;
for (const preview of previews) {
if (preview.role === "user" && current?.hasUser) {
current = undefined;
}
if (!current) {
current = createMutableTurn();
allTurns.push(current);
}
current.messageCount += 1;
if (current.startedAt === undefined && preview.timestamp !== undefined) current.startedAt = preview.timestamp;
if (preview.timestamp !== undefined) current.endedAt = preview.timestamp;
current.truncated ||= preview.truncated;
if (preview.role === "user") {
current.hasUser = true;
const result = appendText(current.user, preview.text, maxChars);
current.user = result.text;
current.truncated ||= result.truncated;
} else if (preview.role === "assistant") {
const result = appendText(current.assistant, preview.text, maxChars);
current.assistant = result.text;
current.truncated ||= result.truncated;
current.toolCalls += preview.toolCalls?.length ?? 0;
}
}
const selected = allTurns.slice(-count).map(({ hasUser: _hasUser, ...turn }) => turn);
return {
turns: selected,
truncated: allTurns.length > selected.length || selected.some((turn) => turn.truncated),
};
}
type MutableTurn = AgentSessionTurnSummary & { hasUser: boolean };
function createMutableTurn(): MutableTurn {
return {
user: "",
assistant: "",
toolCalls: 0,
messageCount: 0,
truncated: false,
hasUser: false,
};
}
function appendText(current: string, next: string, maxChars: number): { text: string; truncated: boolean } {
if (!next) return { text: current, truncated: false };
const combined = current ? `${current}\n${next}` : next;
return combined.length <= maxChars
? { text: combined, truncated: false }
: { text: combined.slice(0, maxChars), truncated: true };
}
function requireString(payload: Record<string, unknown>, key: string, label: string): string {
const value = payload[key];
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string`);
return value.trim();
}
function normalizeOptionalInteger(value: unknown, label: string): number | undefined {
if (value === undefined) return undefined;
if (typeof value !== "number" || !Number.isInteger(value))
throw new Error(`${label} must be an integer when provided`);
return value;
}
function normalizeDelivery(value: unknown): AgentSessionDelivery {
if (value === "steer" || value === "follow_up") return value;
throw new Error("agent.sessions.send delivery must be 'steer' or 'follow_up'");
}
function clampInteger(value: number, min: number, max: number, label: string): number {
if (!Number.isInteger(value)) throw new Error(`${label} must be an integer`);
if (value < min || value > max) throw new Error(`${label} must be between ${min} and ${max}`);
return value;
}