refactor(gateway): share agent prompt builder

This commit is contained in:
Peter Steinberger
2026-02-14 13:34:30 +00:00
parent e707a7bd36
commit 7fc1026746
4 changed files with 103 additions and 64 deletions

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { buildHistoryContextFromEntries } from "../auto-reply/reply/history.js";
import { buildAgentMessageFromConversationEntries } from "./agent-prompt.js";
describe("gateway agent prompt", () => {
it("returns empty for no entries", () => {
expect(buildAgentMessageFromConversationEntries([])).toBe("");
});
it("returns current body when there is no history", () => {
expect(
buildAgentMessageFromConversationEntries([
{ role: "user", entry: { sender: "User", body: "hi" } },
]),
).toBe("hi");
});
it("uses history context when there is history", () => {
const entries = [
{ role: "assistant", entry: { sender: "Assistant", body: "prev" } },
{ role: "user", entry: { sender: "User", body: "next" } },
] as const;
const expected = buildHistoryContextFromEntries({
entries: entries.map((e) => e.entry),
currentMessage: "User: next",
formatEntry: (e) => `${e.sender}: ${e.body}`,
});
expect(buildAgentMessageFromConversationEntries([...entries])).toBe(expected);
});
it("prefers last tool entry over assistant for current message", () => {
const entries = [
{ role: "user", entry: { sender: "User", body: "question" } },
{ role: "tool", entry: { sender: "Tool:x", body: "tool output" } },
{ role: "assistant", entry: { sender: "Assistant", body: "assistant text" } },
] as const;
const expected = buildHistoryContextFromEntries({
entries: [entries[0].entry, entries[1].entry],
currentMessage: "Tool:x: tool output",
formatEntry: (e) => `${e.sender}: ${e.body}`,
});
expect(buildAgentMessageFromConversationEntries([...entries])).toBe(expected);
});
});

View File

@@ -0,0 +1,43 @@
import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply/reply/history.js";
export type ConversationEntry = {
role: "user" | "assistant" | "tool";
entry: HistoryEntry;
};
export function buildAgentMessageFromConversationEntries(entries: ConversationEntry[]): string {
if (entries.length === 0) {
return "";
}
// Prefer the last user/tool entry as "current message" so the agent responds to
// the latest user input or tool output, not the assistant's previous message.
let currentIndex = -1;
for (let i = entries.length - 1; i >= 0; i -= 1) {
const role = entries[i]?.role;
if (role === "user" || role === "tool") {
currentIndex = i;
break;
}
}
if (currentIndex < 0) {
currentIndex = entries.length - 1;
}
const currentEntry = entries[currentIndex]?.entry;
if (!currentEntry) {
return "";
}
const historyEntries = entries.slice(0, currentIndex).map((e) => e.entry);
if (historyEntries.length === 0) {
return currentEntry.body;
}
const formatEntry = (entry: HistoryEntry) => `${entry.sender}: ${entry.body}`;
return buildHistoryContextFromEntries({
entries: [...historyEntries, currentEntry],
currentMessage: formatEntry(currentEntry),
formatEntry,
});
}

View File

@@ -1,12 +1,15 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { randomUUID } from "node:crypto";
import type { AuthRateLimiter } from "./auth-rate-limit.js";
import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply/reply/history.js";
import { createDefaultDeps } from "../cli/deps.js";
import { agentCommand } from "../commands/agent.js";
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
import { logWarn } from "../logger.js";
import { defaultRuntime } from "../runtime.js";
import {
buildAgentMessageFromConversationEntries,
type ConversationEntry,
} from "./agent-prompt.js";
import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js";
import {
readJsonBodyOrError,
@@ -83,8 +86,7 @@ function buildAgentPrompt(messagesUnknown: unknown): {
const messages = asMessages(messagesUnknown);
const systemParts: string[] = [];
const conversationEntries: Array<{ role: "user" | "assistant" | "tool"; entry: HistoryEntry }> =
[];
const conversationEntries: ConversationEntry[] = [];
for (const msg of messages) {
if (!msg || typeof msg !== "object") {
@@ -121,34 +123,7 @@ function buildAgentPrompt(messagesUnknown: unknown): {
});
}
let message = "";
if (conversationEntries.length > 0) {
let currentIndex = -1;
for (let i = conversationEntries.length - 1; i >= 0; i -= 1) {
const entryRole = conversationEntries[i]?.role;
if (entryRole === "user" || entryRole === "tool") {
currentIndex = i;
break;
}
}
if (currentIndex < 0) {
currentIndex = conversationEntries.length - 1;
}
const currentEntry = conversationEntries[currentIndex]?.entry;
if (currentEntry) {
const historyEntries = conversationEntries.slice(0, currentIndex).map((entry) => entry.entry);
if (historyEntries.length === 0) {
message = currentEntry.body;
} else {
const formatEntry = (entry: HistoryEntry) => `${entry.sender}: ${entry.body}`;
message = buildHistoryContextFromEntries({
entries: [...historyEntries, currentEntry],
currentMessage: formatEntry(currentEntry),
formatEntry,
});
}
}
}
const message = buildAgentMessageFromConversationEntries(conversationEntries);
return {
message,

View File

@@ -12,7 +12,6 @@ import type { ClientToolDefinition } from "../agents/pi-embedded-runner/run/para
import type { ImageContent } from "../commands/agent/types.js";
import type { GatewayHttpResponsesConfig } from "../config/types.gateway.js";
import type { AuthRateLimiter } from "./auth-rate-limit.js";
import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply/reply/history.js";
import { createDefaultDeps } from "../cli/deps.js";
import { agentCommand } from "../commands/agent.js";
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
@@ -36,6 +35,10 @@ import {
type InputImageSource,
} from "../media/input-files.js";
import { defaultRuntime } from "../runtime.js";
import {
buildAgentMessageFromConversationEntries,
type ConversationEntry,
} from "./agent-prompt.js";
import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js";
import {
readJsonBodyOrError,
@@ -196,8 +199,7 @@ export function buildAgentPrompt(input: string | ItemParam[]): {
}
const systemParts: string[] = [];
const conversationEntries: Array<{ role: "user" | "assistant" | "tool"; entry: HistoryEntry }> =
[];
const conversationEntries: ConversationEntry[] = [];
for (const item of input) {
if (item.type === "message") {
@@ -227,36 +229,7 @@ export function buildAgentPrompt(input: string | ItemParam[]): {
// Skip reasoning and item_reference for prompt building (Phase 1)
}
let message = "";
if (conversationEntries.length > 0) {
// Find the last user or tool message as the current message
let currentIndex = -1;
for (let i = conversationEntries.length - 1; i >= 0; i -= 1) {
const entryRole = conversationEntries[i]?.role;
if (entryRole === "user" || entryRole === "tool") {
currentIndex = i;
break;
}
}
if (currentIndex < 0) {
currentIndex = conversationEntries.length - 1;
}
const currentEntry = conversationEntries[currentIndex]?.entry;
if (currentEntry) {
const historyEntries = conversationEntries.slice(0, currentIndex).map((entry) => entry.entry);
if (historyEntries.length === 0) {
message = currentEntry.body;
} else {
const formatEntry = (entry: HistoryEntry) => `${entry.sender}: ${entry.body}`;
message = buildHistoryContextFromEntries({
entries: [...historyEntries, currentEntry],
currentMessage: formatEntry(currentEntry),
formatEntry,
});
}
}
}
const message = buildAgentMessageFromConversationEntries(conversationEntries);
return {
message,