Fix input event streaming behavior semantics

This commit is contained in:
Mario Zechner
2026-05-28 10:13:06 +02:00
parent 1fc57a5d5b
commit cbe8625528
4 changed files with 78 additions and 1 deletions

View File

@@ -4,6 +4,7 @@
### Fixed
- Fixed extension input events to report `streamingBehavior` only for prompts actually queued during streaming ([#5107](https://github.com/earendil-works/pi-mono/pull/5107)).
- Fixed fenced `diff` code blocks and other highlight.js scopes to keep theme-aware syntax colors after the `cli-highlight` replacement ([#5092](https://github.com/earendil-works/pi/issues/5092)).
## [0.76.0] - 2026-05-27

View File

@@ -75,6 +75,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
| `reload-runtime.ts` | Adds `/reload-runtime` and `reload_runtime` tool showing safe reload flow |
| `interactive-shell.ts` | Run interactive commands (vim, htop) with full terminal via `user_bash` hook |
| `inline-bash.ts` | Expands `!{command}` patterns in prompts via `input` event transformation |
| `input-transform-streaming.ts` | Skips expensive input preprocessing for mid-stream steering via `streamingBehavior` |
### Git Integration

View File

@@ -984,7 +984,7 @@ export class AgentSession {
currentText,
currentImages,
options?.source ?? "interactive",
options?.streamingBehavior,
this.isStreaming ? options?.streamingBehavior : undefined,
);
if (inputResult.action === "handled") {
preflightResult?.(true);

View File

@@ -5,6 +5,7 @@ import type { AgentTool } from "@earendil-works/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall, type Model } from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import type { InputEvent } from "../../src/core/extensions/index.ts";
import type { PromptTemplate } from "../../src/core/prompt-templates.ts";
import { createSyntheticSourceInfo } from "../../src/core/source-info.ts";
import { createTestResourceLoader } from "../utilities.ts";
@@ -259,6 +260,80 @@ describe("AgentSession prompt characterization", () => {
expect(getMessageText(harness.session.messages[0]!)).toBe("from extension");
});
it("does not report streamingBehavior to input handlers while idle", async () => {
const inputEvents: InputEvent[] = [];
const harness = await createHarness({
extensionFactories: [
(pi) => {
pi.on("input", (event) => {
inputEvents.push(event);
});
},
],
});
harnesses.push(harness);
harness.setResponses([fauxAssistantMessage("ok")]);
await harness.session.prompt("idle", { streamingBehavior: "followUp" });
expect(inputEvents).toHaveLength(1);
expect(inputEvents[0]?.streamingBehavior).toBeUndefined();
});
it("reports streamingBehavior to input handlers while streaming", async () => {
let releaseToolExecution: (() => void) | undefined;
const toolRelease = new Promise<void>((resolve) => {
releaseToolExecution = resolve;
});
const inputEvents: InputEvent[] = [];
const waitTool: AgentTool = {
name: "wait",
label: "Wait",
description: "Wait for release",
parameters: Type.Object({}),
execute: async () => {
await toolRelease;
return {
content: [{ type: "text", text: "released" }],
details: {},
};
},
};
const harness = await createHarness({
tools: [waitTool],
extensionFactories: [
(pi) => {
pi.on("input", (event) => {
inputEvents.push(event);
});
},
],
});
harnesses.push(harness);
harness.setResponses([
fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" }),
fauxAssistantMessage("done"),
]);
const sawToolStart = new Promise<void>((resolve) => {
const unsubscribe = harness.session.subscribe((event) => {
if (event.type === "tool_execution_start") {
unsubscribe();
resolve();
}
});
});
const promptPromise = harness.session.prompt("start");
await sawToolStart;
await harness.session.prompt("queued", { streamingBehavior: "followUp" });
expect(inputEvents.map((event) => event.streamingBehavior)).toEqual([undefined, "followUp"]);
releaseToolExecution?.();
await promptPromise;
});
it("throws when prompted during streaming without a streamingBehavior", async () => {
let releaseToolExecution: (() => void) | undefined;
const toolRelease = new Promise<void>((resolve) => {