fix: avoid echoing prompts when rpc returns empty
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
- Tau RPC timeout is now inactivity-based (5m without events) and error messages show seconds only.
|
- Tau RPC timeout is now inactivity-based (5m without events) and error messages show seconds only.
|
||||||
- Directive triggers (`/think`, `/verbose`, `/stop` et al.) now reply immediately using normalized bodies (timestamps/group prefixes stripped) without waiting for the agent.
|
- Directive triggers (`/think`, `/verbose`, `/stop` et al.) now reply immediately using normalized bodies (timestamps/group prefixes stripped) without waiting for the agent.
|
||||||
- Batched history blocks no longer trip directive parsing; `/think` in prior messages won't emit stray acknowledgements.
|
- Batched history blocks no longer trip directive parsing; `/think` in prior messages won't emit stray acknowledgements.
|
||||||
|
- RPC fallbacks no longer echo the user's prompt (e.g., pasting a link) when the agent returns no assistant text.
|
||||||
|
|
||||||
## 1.4.1 — 2025-12-04
|
## 1.4.1 — 2025-12-04
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,43 @@ describe("runCommandReply (pi)", () => {
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not echo the user's prompt when the agent returns no assistant text", async () => {
|
||||||
|
const rpcMock = mockPiRpc({
|
||||||
|
stdout: [
|
||||||
|
'{"type":"agent_start"}',
|
||||||
|
'{"type":"turn_start"}',
|
||||||
|
'{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}',
|
||||||
|
'{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}',
|
||||||
|
// assistant emits nothing useful
|
||||||
|
'{"type":"agent_end"}',
|
||||||
|
].join("\n"),
|
||||||
|
stderr: "",
|
||||||
|
code: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { payloads } = await runCommandReply({
|
||||||
|
reply: {
|
||||||
|
mode: "command",
|
||||||
|
command: ["pi", "{{Body}}"],
|
||||||
|
agent: { kind: "pi" },
|
||||||
|
},
|
||||||
|
templatingCtx: { ...noopTemplateCtx, Body: "hello", BodyStripped: "hello" },
|
||||||
|
sendSystemOnce: false,
|
||||||
|
isNewSession: true,
|
||||||
|
isFirstTurnInSession: true,
|
||||||
|
systemSent: false,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
timeoutSeconds: 1,
|
||||||
|
commandRunner: vi.fn(),
|
||||||
|
enqueue: enqueueImmediate,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rpcMock).toHaveBeenCalledOnce();
|
||||||
|
expect(payloads?.length).toBe(1);
|
||||||
|
expect(payloads?.[0]?.text).toMatch(/no output/i);
|
||||||
|
expect(payloads?.[0]?.text).not.toContain("hello");
|
||||||
|
});
|
||||||
|
|
||||||
it("adds session args and --continue when resuming", async () => {
|
it("adds session args and --continue when resuming", async () => {
|
||||||
const rpcMock = mockPiRpc({
|
const rpcMock = mockPiRpc({
|
||||||
stdout:
|
stdout:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ function stripRpcNoise(raw: string): string {
|
|||||||
const type = evt?.type;
|
const type = evt?.type;
|
||||||
const msg = evt?.message ?? evt?.assistantMessageEvent;
|
const msg = evt?.message ?? evt?.assistantMessageEvent;
|
||||||
const msgType = msg?.type;
|
const msgType = msg?.type;
|
||||||
|
const role = msg?.role;
|
||||||
|
|
||||||
// RPC streaming emits one message_update per delta; skip them to avoid flooding fallbacks.
|
// RPC streaming emits one message_update per delta; skip them to avoid flooding fallbacks.
|
||||||
if (type === "message_update") continue;
|
if (type === "message_update") continue;
|
||||||
@@ -40,6 +41,11 @@ function stripRpcNoise(raw: string): string {
|
|||||||
if (type === "message_update" && msgType === "toolcall_delta") continue;
|
if (type === "message_update" && msgType === "toolcall_delta") continue;
|
||||||
if (type === "input_audio_buffer.append") continue;
|
if (type === "input_audio_buffer.append") continue;
|
||||||
|
|
||||||
|
// Keep only assistant/tool messages; drop agent_start/turn_start/user/etc.
|
||||||
|
const isAssistant = role === "assistant";
|
||||||
|
const isToolRole = typeof role === "string" && role.toLowerCase().includes("tool");
|
||||||
|
if (!isAssistant && !isToolRole) continue;
|
||||||
|
|
||||||
// Ignore assistant messages that have no text content (pure toolcall scaffolding).
|
// Ignore assistant messages that have no text content (pure toolcall scaffolding).
|
||||||
if (msg?.role === "assistant" && Array.isArray(msg?.content)) {
|
if (msg?.role === "assistant" && Array.isArray(msg?.content)) {
|
||||||
const hasText = msg.content.some(
|
const hasText = msg.content.some(
|
||||||
@@ -770,9 +776,15 @@ export async function runCommandReply(
|
|||||||
extractRpcAssistantText(trimmed) ??
|
extractRpcAssistantText(trimmed) ??
|
||||||
extractAssistantTextLoosely(trimmed) ??
|
extractAssistantTextLoosely(trimmed) ??
|
||||||
trimmed;
|
trimmed;
|
||||||
if (replyItems.length === 0 && fallbackText && !hasParsedContent) {
|
const promptEcho =
|
||||||
|
fallbackText &&
|
||||||
|
(fallbackText === (templatingCtx.Body ?? "") ||
|
||||||
|
fallbackText === (templatingCtx.BodyStripped ?? ""));
|
||||||
|
const safeFallbackText = promptEcho ? undefined : fallbackText;
|
||||||
|
|
||||||
|
if (replyItems.length === 0 && safeFallbackText && !hasParsedContent) {
|
||||||
const { text: cleanedText, mediaUrls: mediaFound } =
|
const { text: cleanedText, mediaUrls: mediaFound } =
|
||||||
splitMediaFromOutput(fallbackText);
|
splitMediaFromOutput(safeFallbackText);
|
||||||
if (cleanedText || mediaFound?.length) {
|
if (cleanedText || mediaFound?.length) {
|
||||||
replyItems.push({
|
replyItems.push({
|
||||||
text: cleanedText,
|
text: cleanedText,
|
||||||
|
|||||||
Reference in New Issue
Block a user