mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 06:37:29 +00:00
refactor(gateway): share agent prompt builder
This commit is contained in:
48
src/gateway/agent-prompt.e2e.test.ts
Normal file
48
src/gateway/agent-prompt.e2e.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
43
src/gateway/agent-prompt.ts
Normal file
43
src/gateway/agent-prompt.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user