feat(agent): expose expandPromptTemplates in sendUserMessage (#7857)

This commit is contained in:
Duncan Ogilvie 2026-08-11 11:14:28 +02:00 • committed by GitHub
parent 4a879dd759
commit b987ead358
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 75 additions and 8 deletions

View file

@ -1426,12 +1426,16 @@ pi.sendUserMessage([
// During streaming - must specify delivery mode // During streaming - must specify delivery mode
pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" }); pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
pi.sendUserMessage("And then summarize", { deliverAs: "followUp" }); pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });
// Opt in to extension command dispatch and skill/prompt template expansion
pi.sendUserMessage("/review src/index.ts", { expandPromptTemplates: true });
``` ```
**Options:** **Options:**
- `deliverAs` - Required when agent is streaming: - `deliverAs` - Required when agent is streaming:
- `"steer"` - Queues the message for delivery after the current assistant turn finishes executing its tool calls - `"steer"` - Queues the message for delivery after the current assistant turn finishes executing its tool calls
- `"followUp"` - Waits for agent to finish all tools - `"followUp"` - Waits for agent to finish all tools
- `expandPromptTemplates` - Dispatch extension commands and expand skill commands and prompt templates. Defaults to `false`.
When not streaming, the message is sent immediately and triggers a new turn. When streaming without `deliverAs`, throws an error. When not streaming, the message is sent immediately and triggers a new turn. When streaming without `deliverAs`, throws an error.

View file

@ -238,7 +238,7 @@ export interface ExtensionBindings {
/** Options for AgentSession.prompt() */ /** Options for AgentSession.prompt() */
export interface PromptOptions { export interface PromptOptions {
/** Whether to expand file-based prompt templates (default: true) */ /** Whether to dispatch extension commands and expand skill commands and prompt templates (default: true) */
expandPromptTemplates?: boolean; expandPromptTemplates?: boolean;
/** Image attachments */ /** Image attachments */
images?: ImageContent[]; images?: ImageContent[];
@ -1476,10 +1476,11 @@ export class AgentSession {
* *
* @param content User message content (string or content array) * @param content User message content (string or content array)
* @param options.deliverAs Delivery mode when streaming: "steer" or "followUp" * @param options.deliverAs Delivery mode when streaming: "steer" or "followUp"
* @param options.expandPromptTemplates Whether to dispatch extension commands and expand skill commands and prompt templates. Default: false.
*/ */
async sendUserMessage( async sendUserMessage(
content: string | (TextContent | ImageContent)[], content: string | (TextContent | ImageContent)[],
options?: { deliverAs?: "steer" | "followUp" }, options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
): Promise<void> { ): Promise<void> {
// Normalize content to text string + optional images // Normalize content to text string + optional images
let text: string; let text: string;
@ -1501,9 +1502,8 @@ export class AgentSession {
if (images.length === 0) images = undefined; if (images.length === 0) images = undefined;
} }
// Use prompt() with expandPromptTemplates: false to skip command handling and template expansion
await this.prompt(text, { await this.prompt(text, {
expandPromptTemplates: false, expandPromptTemplates: options?.expandPromptTemplates ?? false,
streamingBehavior: options?.deliverAs, streamingBehavior: options?.deliverAs,
images, images,
source: "extension", source: "extension",

View file

@ -399,7 +399,7 @@ export interface ReplacedSessionContext extends ExtensionCommandContext {
sendUserMessage( sendUserMessage(
content: string | (TextContent | ImageContent)[], content: string | (TextContent | ImageContent)[],
options?: { deliverAs?: "steer" | "followUp" }, options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
): Promise<void>; ): Promise<void>;
} }
@ -1307,10 +1307,11 @@ export interface ExtensionAPI {
/** /**
* Send a user message to the agent. Always triggers a turn. * Send a user message to the agent. Always triggers a turn.
* When the agent is streaming, use deliverAs to specify how to queue the message. * When the agent is streaming, use deliverAs to specify how to queue the message.
* Set expandPromptTemplates to dispatch extension commands and expand skill commands and prompt templates.
*/ */
sendUserMessage( sendUserMessage(
content: string | (TextContent | ImageContent)[], content: string | (TextContent | ImageContent)[],
options?: { deliverAs?: "steer" | "followUp" }, options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
): void; ): void;
/** Append a custom entry to the session for state persistence (not sent to LLM). */ /** Append a custom entry to the session for state persistence (not sent to LLM). */
@ -1560,7 +1561,7 @@ export type SendMessageHandler = <T = unknown>(
export type SendUserMessageHandler = ( export type SendUserMessageHandler = (
content: string | (TextContent | ImageContent)[], content: string | (TextContent | ImageContent)[],
options?: { deliverAs?: "steer" | "followUp" }, options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
) => void; ) => void;
export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void; export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;

View file

@ -5,7 +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 { ExtensionAPI, 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";
@ -224,6 +224,39 @@ describe("AgentSession prompt characterization", () => {
expect(expandedPrompt).toBe("Review this code: src/index.ts"); expect(expandedPrompt).toBe("Review this code: src/index.ts");
}); });
it("sendUserMessage can opt into prompt template expansion", async () => {
const template: PromptTemplate = {
name: "review",
description: "Review template",
content: "Review this code: $1",
filePath: "/virtual/review.md",
sourceInfo: createSyntheticSourceInfo("/virtual/review.md", {
source: "local",
scope: "temporary",
origin: "top-level",
}),
};
const resourceLoader = {
...createTestResourceLoader(),
getPrompts: () => ({ prompts: [template], diagnostics: [] }),
};
const harness = await createHarness({ resourceLoader });
harnesses.push(harness);
let expandedPrompt = "";
harness.setResponses([
(context) => {
const user = context.messages.find((message) => message.role === "user");
expandedPrompt = user ? getMessageText(user) : "";
return fauxAssistantMessage("ok");
},
]);
await harness.session.sendUserMessage("/review src/index.ts", { expandPromptTemplates: true });
expect(expandedPrompt).toBe("Review this code: src/index.ts");
});
it("dispatches extension commands without consuming a provider response", async () => { it("dispatches extension commands without consuming a provider response", async () => {
const commandRuns: string[] = []; const commandRuns: string[] = [];
const harness = await createHarness({ const harness = await createHarness({
@ -248,6 +281,35 @@ describe("AgentSession prompt characterization", () => {
expect(harness.getPendingResponseCount()).toBe(1); expect(harness.getPendingResponseCount()).toBe(1);
}); });
it("extension sendUserMessage can opt into extension command dispatch", async () => {
let extensionApi: ExtensionAPI | undefined;
let resolveCommandRun: (args: string) => void = () => {};
const commandRun = new Promise<string>((resolve) => {
resolveCommandRun = resolve;
});
const harness = await createHarness({
extensionFactories: [
(pi) => {
extensionApi = pi;
pi.registerCommand("testcmd", {
description: "Test command",
handler: async (args) => {
resolveCommandRun(args);
},
});
},
],
});
harnesses.push(harness);
expect(extensionApi).toBeDefined();
extensionApi?.sendUserMessage("/testcmd hello world", { expandPromptTemplates: true });
await expect(commandRun).resolves.toBe("hello world");
expect(harness.session.messages).toEqual([]);
expect(harness.getPendingResponseCount()).toBe(0);
});
it("sendUserMessage while idle triggers a turn", async () => { it("sendUserMessage while idle triggers a turn", async () => {
const harness = await createHarness(); const harness = await createHarness();
harnesses.push(harness); harnesses.push(harness);