Fix input event streaming behavior semantics
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Fixed
|
### 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)).
|
- 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
|
## [0.76.0] - 2026-05-27
|
||||||
|
|||||||
@@ -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 |
|
| `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 |
|
| `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 |
|
| `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
|
### Git Integration
|
||||||
|
|
||||||
|
|||||||
@@ -984,7 +984,7 @@ export class AgentSession {
|
|||||||
currentText,
|
currentText,
|
||||||
currentImages,
|
currentImages,
|
||||||
options?.source ?? "interactive",
|
options?.source ?? "interactive",
|
||||||
options?.streamingBehavior,
|
this.isStreaming ? options?.streamingBehavior : undefined,
|
||||||
);
|
);
|
||||||
if (inputResult.action === "handled") {
|
if (inputResult.action === "handled") {
|
||||||
preflightResult?.(true);
|
preflightResult?.(true);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|||||||
import { fauxAssistantMessage, fauxToolCall, type Model } from "@earendil-works/pi-ai";
|
import { fauxAssistantMessage, fauxToolCall, type Model } from "@earendil-works/pi-ai";
|
||||||
import { Type } from "typebox";
|
import { Type } from "typebox";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
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 type { PromptTemplate } from "../../src/core/prompt-templates.ts";
|
||||||
import { createSyntheticSourceInfo } from "../../src/core/source-info.ts";
|
import { createSyntheticSourceInfo } from "../../src/core/source-info.ts";
|
||||||
import { createTestResourceLoader } from "../utilities.ts";
|
import { createTestResourceLoader } from "../utilities.ts";
|
||||||
@@ -259,6 +260,80 @@ describe("AgentSession prompt characterization", () => {
|
|||||||
expect(getMessageText(harness.session.messages[0]!)).toBe("from extension");
|
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 () => {
|
it("throws when prompted during streaming without a streamingBehavior", async () => {
|
||||||
let releaseToolExecution: (() => void) | undefined;
|
let releaseToolExecution: (() => void) | undefined;
|
||||||
const toolRelease = new Promise<void>((resolve) => {
|
const toolRelease = new Promise<void>((resolve) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user