2026-02-08 16:25:25 +09:00
|
|
|
import type { Message, Part } from "@opencode-ai/sdk"
|
|
|
|
|
|
2026-04-28 14:51:38 -04:00
|
|
|
import { log } from "../shared/logger"
|
2026-02-08 16:25:25 +09:00
|
|
|
import type { CreatedHooks } from "../create-hooks"
|
|
|
|
|
|
2026-05-10 15:54:29 +09:00
|
|
|
const ASSISTANT_PREFILL_RECOVERY_TEXT = "[internal] Continue from the previous assistant state."
|
|
|
|
|
|
2026-02-08 16:25:25 +09:00
|
|
|
type MessageWithParts = {
|
|
|
|
|
info: Message
|
|
|
|
|
parts: Part[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MessagesTransformOutput = { messages: MessageWithParts[] }
|
2026-05-10 15:54:29 +09:00
|
|
|
type UserMessageInfo = Extract<Message, { role: "user" }>
|
|
|
|
|
|
|
|
|
|
function getSessionID(message: MessageWithParts): string | undefined {
|
|
|
|
|
return message.info.sessionID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findLastUserMessage(messages: MessageWithParts[]): UserMessageInfo | undefined {
|
|
|
|
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
const message = messages[index]
|
|
|
|
|
if (message?.info.role === "user") {
|
|
|
|
|
return message.info
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createAssistantPrefillRecoveryMessage(
|
|
|
|
|
lastAssistantMessage: MessageWithParts,
|
|
|
|
|
messages: MessageWithParts[],
|
|
|
|
|
): MessageWithParts {
|
|
|
|
|
const lastUserMessage = findLastUserMessage(messages)
|
|
|
|
|
const sessionID = getSessionID(lastAssistantMessage) ?? lastUserMessage?.sessionID ?? ""
|
|
|
|
|
const messageID = `${lastAssistantMessage.info.id}_prefill_recovery`
|
|
|
|
|
const model = lastUserMessage?.model ?? {
|
|
|
|
|
providerID: "internal",
|
|
|
|
|
modelID: "assistant-prefill-guard",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
info: {
|
|
|
|
|
id: messageID,
|
|
|
|
|
sessionID,
|
|
|
|
|
role: "user",
|
|
|
|
|
time: { created: Date.now() },
|
|
|
|
|
agent: lastUserMessage?.agent ?? "internal",
|
|
|
|
|
model,
|
|
|
|
|
...(lastUserMessage?.system ? { system: lastUserMessage.system } : {}),
|
|
|
|
|
...(lastUserMessage?.tools ? { tools: lastUserMessage.tools } : {}),
|
|
|
|
|
},
|
|
|
|
|
parts: [
|
|
|
|
|
{
|
|
|
|
|
id: `${messageID}_text`,
|
|
|
|
|
sessionID,
|
|
|
|
|
messageID,
|
|
|
|
|
type: "text",
|
|
|
|
|
text: ASSISTANT_PREFILL_RECOVERY_TEXT,
|
|
|
|
|
synthetic: true,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensureUserTurnAfterAssistantTail(output: MessagesTransformOutput): void {
|
|
|
|
|
const lastMessage = output.messages.at(-1)
|
|
|
|
|
if (!lastMessage || lastMessage.info.role !== "assistant") {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
output.messages.push(createAssistantPrefillRecoveryMessage(lastMessage, output.messages))
|
|
|
|
|
}
|
2026-02-08 16:25:25 +09:00
|
|
|
|
2026-04-28 14:51:38 -04:00
|
|
|
async function runMessagesTransformHookSafely<I, O>(
|
|
|
|
|
hookName: string,
|
|
|
|
|
handler: ((input: I, output: O) => unknown | Promise<unknown>) | null | undefined,
|
|
|
|
|
input: I,
|
|
|
|
|
output: O,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
if (!handler) return
|
|
|
|
|
try {
|
|
|
|
|
await Promise.resolve(handler(input, output))
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Isolate per-handler failures so later handlers (notably toolPairValidator)
|
|
|
|
|
// always run. A throw here used to leave orphaned tool_use blocks in the
|
|
|
|
|
// post-compaction payload, producing API 400s like
|
|
|
|
|
// "tool_use ids were found without tool_result blocks immediately after".
|
|
|
|
|
log("[messages-transform] hook execution failed", {
|
|
|
|
|
hook: hookName,
|
|
|
|
|
error,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 16:25:25 +09:00
|
|
|
export function createMessagesTransformHandler(args: {
|
|
|
|
|
hooks: CreatedHooks
|
|
|
|
|
}): (input: Record<string, never>, output: MessagesTransformOutput) => Promise<void> {
|
|
|
|
|
return async (input, output): Promise<void> => {
|
2026-04-28 14:51:38 -04:00
|
|
|
await runMessagesTransformHookSafely(
|
|
|
|
|
"contextInjectorMessagesTransform",
|
|
|
|
|
args.hooks.contextInjectorMessagesTransform?.[
|
|
|
|
|
"experimental.chat.messages.transform"
|
|
|
|
|
],
|
|
|
|
|
input,
|
|
|
|
|
output,
|
|
|
|
|
)
|
2026-02-08 16:25:25 +09:00
|
|
|
|
2026-04-28 10:47:38 +09:00
|
|
|
await runMessagesTransformHookSafely(
|
|
|
|
|
"teamModeStatusInjector",
|
|
|
|
|
args.hooks.teamModeStatusInjector?.[
|
|
|
|
|
"experimental.chat.messages.transform"
|
|
|
|
|
],
|
|
|
|
|
input,
|
|
|
|
|
output,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await runMessagesTransformHookSafely(
|
|
|
|
|
"teamMailboxInjector",
|
|
|
|
|
args.hooks.teamMailboxInjector?.[
|
|
|
|
|
"experimental.chat.messages.transform"
|
|
|
|
|
],
|
|
|
|
|
input,
|
|
|
|
|
output,
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-28 14:51:38 -04:00
|
|
|
await runMessagesTransformHookSafely(
|
|
|
|
|
"thinkingBlockValidator",
|
|
|
|
|
args.hooks.thinkingBlockValidator?.[
|
|
|
|
|
"experimental.chat.messages.transform"
|
|
|
|
|
],
|
|
|
|
|
input,
|
|
|
|
|
output,
|
|
|
|
|
)
|
2026-04-02 13:31:48 +09:00
|
|
|
|
2026-04-28 14:51:38 -04:00
|
|
|
await runMessagesTransformHookSafely(
|
|
|
|
|
"toolPairValidator",
|
|
|
|
|
args.hooks.toolPairValidator?.[
|
|
|
|
|
"experimental.chat.messages.transform"
|
|
|
|
|
],
|
|
|
|
|
input,
|
|
|
|
|
output,
|
|
|
|
|
)
|
2026-05-10 15:54:29 +09:00
|
|
|
|
|
|
|
|
ensureUserTurnAfterAssistantTail(output)
|
2026-02-08 16:25:25 +09:00
|
|
|
}
|
|
|
|
|
}
|