fix: start typing on message start
This commit is contained in:
@@ -1383,6 +1383,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
text?: string;
|
text?: string;
|
||||||
mediaUrls?: string[];
|
mediaUrls?: string[];
|
||||||
}) => void | Promise<void>;
|
}) => void | Promise<void>;
|
||||||
|
onAssistantMessageStart?: () => void | Promise<void>;
|
||||||
onBlockReply?: (payload: {
|
onBlockReply?: (payload: {
|
||||||
text?: string;
|
text?: string;
|
||||||
mediaUrls?: string[];
|
mediaUrls?: string[];
|
||||||
@@ -1774,6 +1775,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
blockReplyBreak: params.blockReplyBreak,
|
blockReplyBreak: params.blockReplyBreak,
|
||||||
blockReplyChunking: params.blockReplyChunking,
|
blockReplyChunking: params.blockReplyChunking,
|
||||||
onPartialReply: params.onPartialReply,
|
onPartialReply: params.onPartialReply,
|
||||||
|
onAssistantMessageStart: params.onAssistantMessageStart,
|
||||||
onAgentEvent: params.onAgentEvent,
|
onAgentEvent: params.onAgentEvent,
|
||||||
enforceFinalTag: params.enforceFinalTag,
|
enforceFinalTag: params.enforceFinalTag,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ function extractMessagingToolSend(
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribeEmbeddedPiSession(params: {
|
export type SubscribeEmbeddedPiSessionParams = {
|
||||||
session: AgentSession;
|
session: AgentSession;
|
||||||
runId: string;
|
runId: string;
|
||||||
verboseLevel?: "off" | "on";
|
verboseLevel?: "off" | "on";
|
||||||
@@ -173,12 +173,17 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
text?: string;
|
text?: string;
|
||||||
mediaUrls?: string[];
|
mediaUrls?: string[];
|
||||||
}) => void | Promise<void>;
|
}) => void | Promise<void>;
|
||||||
|
onAssistantMessageStart?: () => void | Promise<void>;
|
||||||
onAgentEvent?: (evt: {
|
onAgentEvent?: (evt: {
|
||||||
stream: string;
|
stream: string;
|
||||||
data: Record<string, unknown>;
|
data: Record<string, unknown>;
|
||||||
}) => void;
|
}) => void;
|
||||||
enforceFinalTag?: boolean;
|
enforceFinalTag?: boolean;
|
||||||
}) {
|
};
|
||||||
|
|
||||||
|
export function subscribeEmbeddedPiSession(
|
||||||
|
params: SubscribeEmbeddedPiSessionParams,
|
||||||
|
) {
|
||||||
const assistantTexts: string[] = [];
|
const assistantTexts: string[] = [];
|
||||||
const toolMetas: Array<{ toolName?: string; meta?: string }> = [];
|
const toolMetas: Array<{ toolName?: string; meta?: string }> = [];
|
||||||
const toolMetaById = new Map<string, string | undefined>();
|
const toolMetaById = new Map<string, string | undefined>();
|
||||||
@@ -492,6 +497,8 @@ export function subscribeEmbeddedPiSession(params: {
|
|||||||
// may deliver late text_end updates after message_end, which would
|
// may deliver late text_end updates after message_end, which would
|
||||||
// otherwise re-trigger block replies.
|
// otherwise re-trigger block replies.
|
||||||
resetAssistantMessageState(assistantTexts.length);
|
resetAssistantMessageState(assistantTexts.length);
|
||||||
|
// Use assistant message_start as the earliest "writing" signal for typing.
|
||||||
|
void params.onAssistantMessageStart?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ type EmbeddedPiAgentParams = {
|
|||||||
text?: string;
|
text?: string;
|
||||||
mediaUrls?: string[];
|
mediaUrls?: string[];
|
||||||
}) => Promise<void> | void;
|
}) => Promise<void> | void;
|
||||||
|
onAssistantMessageStart?: () => Promise<void> | void;
|
||||||
onBlockReply?: (payload: {
|
onBlockReply?: (payload: {
|
||||||
text?: string;
|
text?: string;
|
||||||
mediaUrls?: string[];
|
mediaUrls?: string[];
|
||||||
@@ -212,19 +213,21 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
|||||||
expect(typing.startTypingLoop).not.toHaveBeenCalled();
|
expect(typing.startTypingLoop).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts typing only on deltas in message mode", async () => {
|
it("starts typing on assistant message start in message mode", async () => {
|
||||||
runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({
|
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||||
payloads: [{ text: "final" }],
|
async (params: EmbeddedPiAgentParams) => {
|
||||||
meta: {},
|
await params.onAssistantMessageStart?.();
|
||||||
}));
|
return { payloads: [{ text: "final" }], meta: {} };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { run, typing } = createMinimalRun({
|
const { run, typing } = createMinimalRun({
|
||||||
typingMode: "message",
|
typingMode: "message",
|
||||||
});
|
});
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
|
expect(typing.startTypingLoop).toHaveBeenCalled();
|
||||||
expect(typing.startTypingOnText).not.toHaveBeenCalled();
|
expect(typing.startTypingOnText).not.toHaveBeenCalled();
|
||||||
expect(typing.startTypingLoop).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts typing from reasoning stream in thinking mode", async () => {
|
it("starts typing from reasoning stream in thinking mode", async () => {
|
||||||
|
|||||||
@@ -689,6 +689,9 @@ export async function runReplyAgent(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
onAssistantMessageStart: async () => {
|
||||||
|
await typingSignals.signalMessageStart();
|
||||||
|
},
|
||||||
onReasoningStream:
|
onReasoningStream:
|
||||||
typingSignals.shouldStartOnReasoning || opts?.onReasoningStream
|
typingSignals.shouldStartOnReasoning || opts?.onReasoningStream
|
||||||
? async (payload) => {
|
? async (payload) => {
|
||||||
|
|||||||
@@ -96,6 +96,20 @@ describe("createTypingSignaler", () => {
|
|||||||
expect(typing.startTypingLoop).not.toHaveBeenCalled();
|
expect(typing.startTypingLoop).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("signals on message start for message mode", async () => {
|
||||||
|
const typing = createMockTypingController();
|
||||||
|
const signaler = createTypingSignaler({
|
||||||
|
typing,
|
||||||
|
mode: "message",
|
||||||
|
isHeartbeat: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await signaler.signalMessageStart();
|
||||||
|
|
||||||
|
expect(typing.startTypingLoop).toHaveBeenCalled();
|
||||||
|
expect(typing.startTypingOnText).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("signals on reasoning for thinking mode", async () => {
|
it("signals on reasoning for thinking mode", async () => {
|
||||||
const typing = createMockTypingController();
|
const typing = createMockTypingController();
|
||||||
const signaler = createTypingSignaler({
|
const signaler = createTypingSignaler({
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ export function resolveTypingMode({
|
|||||||
export type TypingSignaler = {
|
export type TypingSignaler = {
|
||||||
mode: TypingMode;
|
mode: TypingMode;
|
||||||
shouldStartImmediately: boolean;
|
shouldStartImmediately: boolean;
|
||||||
|
shouldStartOnMessageStart: boolean;
|
||||||
shouldStartOnText: boolean;
|
shouldStartOnText: boolean;
|
||||||
shouldStartOnReasoning: boolean;
|
shouldStartOnReasoning: boolean;
|
||||||
signalRunStart: () => Promise<void>;
|
signalRunStart: () => Promise<void>;
|
||||||
|
signalMessageStart: () => Promise<void>;
|
||||||
signalTextDelta: (text?: string) => Promise<void>;
|
signalTextDelta: (text?: string) => Promise<void>;
|
||||||
signalReasoningDelta: () => Promise<void>;
|
signalReasoningDelta: () => Promise<void>;
|
||||||
signalToolStart: () => Promise<void>;
|
signalToolStart: () => Promise<void>;
|
||||||
@@ -40,6 +42,7 @@ export function createTypingSignaler(params: {
|
|||||||
}): TypingSignaler {
|
}): TypingSignaler {
|
||||||
const { typing, mode, isHeartbeat } = params;
|
const { typing, mode, isHeartbeat } = params;
|
||||||
const shouldStartImmediately = mode === "instant";
|
const shouldStartImmediately = mode === "instant";
|
||||||
|
const shouldStartOnMessageStart = mode === "message";
|
||||||
const shouldStartOnText = mode === "message" || mode === "instant";
|
const shouldStartOnText = mode === "message" || mode === "instant";
|
||||||
const shouldStartOnReasoning = mode === "thinking";
|
const shouldStartOnReasoning = mode === "thinking";
|
||||||
const disabled = isHeartbeat || mode === "never";
|
const disabled = isHeartbeat || mode === "never";
|
||||||
@@ -49,6 +52,11 @@ export function createTypingSignaler(params: {
|
|||||||
await typing.startTypingLoop();
|
await typing.startTypingLoop();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const signalMessageStart = async () => {
|
||||||
|
if (disabled || !shouldStartOnMessageStart) return;
|
||||||
|
await typing.startTypingLoop();
|
||||||
|
};
|
||||||
|
|
||||||
const signalTextDelta = async (text?: string) => {
|
const signalTextDelta = async (text?: string) => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
if (shouldStartOnText) {
|
if (shouldStartOnText) {
|
||||||
@@ -80,9 +88,11 @@ export function createTypingSignaler(params: {
|
|||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
shouldStartImmediately,
|
shouldStartImmediately,
|
||||||
|
shouldStartOnMessageStart,
|
||||||
shouldStartOnText,
|
shouldStartOnText,
|
||||||
shouldStartOnReasoning,
|
shouldStartOnReasoning,
|
||||||
signalRunStart,
|
signalRunStart,
|
||||||
|
signalMessageStart,
|
||||||
signalTextDelta,
|
signalTextDelta,
|
||||||
signalReasoningDelta,
|
signalReasoningDelta,
|
||||||
signalToolStart,
|
signalToolStart,
|
||||||
|
|||||||
Reference in New Issue
Block a user