Files
hapi/cli/src/agent/messageConverter.ts
T
Junmo KimandGitHub 96d766d7f1 feat(acp): forward agent_thought_chunk as reasoning message (#520)
* refactor(agent): extend AgentMessage and CodexMessage unions with reasoning variant

Add a reasoning variant to the shared AgentMessage union that flows
out of the ACP backend, and pass it through to CodexMessage so the
existing web reducer (which already renders { type: 'reasoning' }
parts as collapsible blocks) can consume ACP-sourced thoughts
identically to Codex.

No behavior change yet: the ACP handler still drops thought chunks,
and the remote launchers receive the new variant as a no-op. The
behavior is wired up in the following commit.

* feat(acp): forward agent_thought_chunk as reasoning message

Route ACP thought chunks to the session as reasoning AgentMessages so
OpenCode and Gemini thinking output reaches the web UI's Reasoning
block, matching the existing Codex behavior.

Thought chunks are emitted inline without flushing the pending text
buffer — text and thought live on independent interleave lanes, so
splitting a live text segment on every thought arrival would be
wrong. The inline-emit ordering is documented alongside the test
that depends on it.

extractTextContent is not reused for thought content: its
assistant-audience filter is correct for regular message chunks but
would silently drop thoughts annotated with a non-assistant audience,
which have no meaningful audience to filter against. A direct text
block shape check handles the narrower need.

In the remote launchers, reasoning is surfaced to the local terminal
buffer as a truncated system-role hint prefixed with [Thinking],
matching how the Codex flavor already displays reasoning chunks
in-terminal without mixing them into the assistant reply stream.
2026-04-23 20:45:20 +08:00

62 lines
2.0 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: 'tool-call';
name: string;
callId: string;
input: unknown;
status?: 'pending' | 'in_progress' | 'completed' | 'failed';
}
| {
type: 'tool-call-result';
callId: string;
output: unknown;
is_error?: boolean;
}
| { type: 'plan'; entries: PlanItem[] }
| { type: 'error'; message: string };
export function convertAgentMessage(message: AgentMessage): 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: randomUUID() };
case 'tool_call':
return {
type: 'tool-call',
name: message.name,
callId: message.id,
input: message.input,
status: message.status
};
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: {
const _exhaustive: never = message;
return _exhaustive;
}
}
}