fix: align pending questions by native occurrence
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import type { AssistantMessage, Message, ToolResultMessage, UserMessage } from "@earendil-works/pi-ai";
|
||||
import { QUESTION_TOOLS } from "./pending.ts";
|
||||
import { QUESTION_TOOLS, type PendingQuestion } from "./pending.ts";
|
||||
import { CLAUDE_CODE_PROVIDER } from "./contracts/claude-code.ts";
|
||||
import {
|
||||
CONCLUDE_ACK_TEXT,
|
||||
@@ -673,16 +673,48 @@ export function toLlmMessages(messages: readonly AgentMessage[]): Message[] {
|
||||
export function danglingQuestionOf(
|
||||
messages: readonly LhMessage[],
|
||||
): { toolCallId: string; tool: string; input: unknown } | null {
|
||||
const question = questionOccurrenceOf(messages);
|
||||
return question ? { toolCallId: question.toolCallId, tool: question.tool, input: question.input } : null;
|
||||
}
|
||||
|
||||
/** The same authority, retaining native boundaries for the client dock.
|
||||
* Unique call ids are stable even when a live message's local id is replaced
|
||||
* by its persisted stamp. Only colliding ids need an occurrence-qualified
|
||||
* draft/reset key; this never changes stored ids or the answer wire. */
|
||||
export function pendingQuestionOf(messages: readonly LhMessage[]): PendingQuestion | null {
|
||||
const question = questionOccurrenceOf(messages);
|
||||
if (!question) return null;
|
||||
let occurrences = 0;
|
||||
for (const message of messages) {
|
||||
// A streamed c_ assistant can temporarily coexist with its saved
|
||||
// copy during append-only adoption. New live calls are already
|
||||
// uniquified; only settled history establishes reuse.
|
||||
if (message.role !== "assistant" || message.id.startsWith("c_")) continue;
|
||||
for (const call of message.content) {
|
||||
if (call.type === "toolCall" && call.id === question.toolCallId) occurrences++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...question,
|
||||
key: occurrences > 1 ? JSON.stringify([question.messageId, question.contentIndex, question.toolCallId]) : question.toolCallId,
|
||||
};
|
||||
}
|
||||
|
||||
/** Shared native authority: newest assistant only, siblings in call order. */
|
||||
function questionOccurrenceOf(messages: readonly LhMessage[]): Omit<PendingQuestion, "key"> | null {
|
||||
for (let mi = messages.length - 1; mi >= 0; mi--) {
|
||||
const m = messages[mi]!;
|
||||
if ((m as { role?: string }).role !== "assistant") continue;
|
||||
const assistant = m as AssistantMessage & { id: string };
|
||||
const answered = resultIdsFrom(messages.slice(mi + 1));
|
||||
for (const block of assistant.content) {
|
||||
for (const [contentIndex, block] of assistant.content.entries()) {
|
||||
if (block.type !== "toolCall") continue;
|
||||
if (!(QUESTION_TOOLS as readonly string[]).includes(block.name)) continue;
|
||||
if (answered.has(block.id)) continue;
|
||||
return { toolCallId: block.id, tool: block.name, input: block.arguments };
|
||||
return {
|
||||
toolCallId: block.id, tool: block.name, input: block.arguments,
|
||||
messageId: assistant.id, contentIndex,
|
||||
};
|
||||
}
|
||||
return null; // the newest assistant message's questions are all answered
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user