Files
hapi/cli/src/agent/messageConverter.ts
T
2026-08-03 17:00:14 +08:00

104 lines
3.7 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import type { AgentMessage, PlanItem } from './types';
export type CodexMessage =
| { type: 'message'; message: string }
| { type: 'reasoning'; message: string; id: string }
| {
type: 'token_count';
model: string | null;
info: {
total: {
inputTokens: number;
outputTokens: number;
totalTokens?: number;
thoughtTokens?: number;
cachedInputTokens?: number;
};
contextTokens?: number;
modelContextWindow?: number;
};
}
| {
type: 'tool-call';
name: string;
callId: string;
input: unknown;
status?: 'pending' | 'in_progress' | 'completed' | 'failed';
nativeTitle?: string;
nativeKind?: string;
}
| {
type: 'tool-call-result';
callId: string;
output: unknown;
is_error?: boolean;
}
| { type: 'plan'; entries: PlanItem[] }
| { type: 'error'; message: string };
export function convertAgentMessage(message: AgentMessage, model?: string | null): CodexMessage | null {
switch (message.type) {
case 'text':
return { type: 'message', message: message.text };
case 'reasoning':
// AgentMessage uses `text` (consistent with the `text` variant);
// the wire-level CodexMessage uses `message` to match the
// existing reasoning format emitted by the Codex path.
return { type: 'reasoning', message: message.text, id: message.id ?? randomUUID() };
case 'usage':
return {
type: 'token_count',
model: typeof model === 'string' && model.trim() ? model.trim() : null,
info: {
total: {
inputTokens: message.inputTokens,
outputTokens: message.outputTokens,
totalTokens: message.totalTokens,
thoughtTokens: message.thoughtTokens,
cachedInputTokens: message.cacheReadTokens
},
contextTokens: message.contextTokens,
modelContextWindow: message.contextWindow
}
};
case 'tool_call':
return {
type: 'tool-call',
name: message.name,
callId: message.id,
input: message.input,
status: message.status,
...(message.title ? { nativeTitle: message.title } : {}),
...(message.kind ? { nativeKind: message.kind } : {})
};
case 'tool_result':
return {
type: 'tool-call-result',
callId: message.id,
output: message.output,
is_error: message.status === 'failed'
};
case 'plan':
return {
type: 'plan',
entries: message.items
};
case 'error':
return { type: 'error', message: message.message };
case 'turn_complete':
return null;
default: {
// Unreachable while every AgentMessage variant is handled above —
// the `never` binding is what enforces that at compile time. The
// runtime return is deliberately `null` rather than the message
// itself: callers forward a non-null result straight into the chat
// stream, so echoing an unrecognized shape here would put a raw
// object on screen instead of failing closed.
const _exhaustive: never = message;
void _exhaustive;
return null;
}
}
}