mirror of
https://github.com/block/buzz.git
synced 2026-11-08 18:31:56 +00:00
Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co>
272 lines
8.2 KiB
TypeScript
272 lines
8.2 KiB
TypeScript
import type { ObserverEvent, PromptSection } from "./agentSessionTypes";
|
|
import {
|
|
findBuzzToolName,
|
|
isGenericToolTitle,
|
|
normalizeToolName,
|
|
} from "./agentSessionToolCatalog";
|
|
import { asRecord, asString, titleCase } from "./agentSessionUtils";
|
|
|
|
export function extractPromptText(payload: Record<string, unknown>): string {
|
|
const params = asRecord(payload.params);
|
|
const prompt = params.prompt;
|
|
if (!Array.isArray(prompt)) return "";
|
|
return prompt.map(extractBlockText).filter(Boolean).join("\n");
|
|
}
|
|
|
|
export function parsePromptText(text: string): {
|
|
sections: PromptSection[];
|
|
userText: string;
|
|
userTitle: string;
|
|
userPubkey: string | null;
|
|
} {
|
|
const sections = parsePromptSections(text).filter(
|
|
(s) => s.body.trim().length > 0,
|
|
);
|
|
if (sections.length === 0) {
|
|
return {
|
|
sections: [],
|
|
userText: text.trim(),
|
|
userTitle: "Prompt",
|
|
userPubkey: null,
|
|
};
|
|
}
|
|
|
|
const eventSection = sections.find((section) => {
|
|
const title = section.title.toLowerCase();
|
|
return title.startsWith("buzz event") || title.startsWith("buzz event");
|
|
});
|
|
const eventContent = eventSection
|
|
? extractEventContent(eventSection.body)
|
|
: "";
|
|
const eventAuthorPubkey = eventSection
|
|
? extractEventAuthorPubkey(eventSection.body)
|
|
: null;
|
|
const eventKind = eventSection?.title.split(":").slice(1).join(":").trim();
|
|
|
|
return {
|
|
sections,
|
|
userText: eventContent,
|
|
userTitle: eventKind ? titleCase(eventKind) : "Buzz event",
|
|
userPubkey: eventAuthorPubkey,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Split the framed `session/new` `systemPrompt` into its `Base`/`System`
|
|
* sub-sections deterministically.
|
|
*
|
|
* The harness frames the value as `[Base]\n{base}\n\n[System]\n{persona}`, with
|
|
* either prompt omitted when absent: base-only is `[Base]\n{base}`, persona-only
|
|
* is `[System]\n{persona}`. We partition on the FIRST `\n[System]\n` boundary and
|
|
* read each labeled body literally. Unlike the generic `parsePromptSections`,
|
|
* embedded `[...]` lines inside a body never start a new section — so a persona
|
|
* containing a bracketed line, or a mid-string-elided header on an oversize
|
|
* prompt, can never drop a label or inflate the section count.
|
|
*/
|
|
export function parseSystemPromptSections(
|
|
systemPrompt: string,
|
|
): PromptSection[] {
|
|
const sections: PromptSection[] = [];
|
|
|
|
// Persona-only frame: no [Base], starts directly with [System].
|
|
if (systemPrompt.startsWith("[System]\n")) {
|
|
const body = systemPrompt.slice("[System]\n".length).trim();
|
|
if (body) sections.push({ title: "System", body });
|
|
return sections;
|
|
}
|
|
|
|
// Otherwise the head (up to the first [System] boundary, or the whole string)
|
|
// is the [Base] body.
|
|
const marker = "\n[System]\n";
|
|
const at = systemPrompt.indexOf(marker);
|
|
const head = at === -1 ? systemPrompt : systemPrompt.slice(0, at);
|
|
const baseBody = head.replace(/^\[Base]\n/, "").trim();
|
|
if (baseBody) sections.push({ title: "Base", body: baseBody });
|
|
|
|
if (at !== -1) {
|
|
const systemBody = systemPrompt.slice(at + marker.length).trim();
|
|
sections.push({ title: "System", body: systemBody });
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
function parsePromptSections(text: string): PromptSection[] {
|
|
const sections: PromptSection[] = [];
|
|
let current: PromptSection | null = null;
|
|
const preamble: string[] = [];
|
|
|
|
for (const line of text.split(/\r?\n/)) {
|
|
const header = line.match(/^\[([^\]]+)]\s*$/);
|
|
if (header) {
|
|
if (current) {
|
|
sections.push({
|
|
title: current.title,
|
|
body: current.body.trim(),
|
|
});
|
|
} else if (preamble.join("\n").trim()) {
|
|
sections.push({ title: "Prompt", body: preamble.join("\n").trim() });
|
|
}
|
|
current = { title: header[1], body: "" };
|
|
continue;
|
|
}
|
|
|
|
if (current) {
|
|
current.body += current.body ? `\n${line}` : line;
|
|
} else {
|
|
preamble.push(line);
|
|
}
|
|
}
|
|
|
|
if (current) {
|
|
sections.push({ title: current.title, body: current.body.trim() });
|
|
} else if (preamble.join("\n").trim()) {
|
|
sections.push({ title: "Prompt", body: preamble.join("\n").trim() });
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
function extractEventContent(body: string): string {
|
|
const contentMatch = body.match(/^Content:\s*(.*)$/m);
|
|
return contentMatch?.[1]?.trim() ?? "";
|
|
}
|
|
|
|
function extractEventAuthorPubkey(body: string): string | null {
|
|
const fromMatch = body.match(/^From:.*\bhex:\s*([0-9a-fA-F]{64})/m);
|
|
return fromMatch?.[1]?.toLowerCase() ?? null;
|
|
}
|
|
|
|
export function extractContentText(value: unknown): string {
|
|
if (typeof value === "string") return value;
|
|
if (Array.isArray(value)) return value.map(extractBlockText).join("\n");
|
|
return extractBlockText(value);
|
|
}
|
|
|
|
export function extractBlockText(value: unknown): string {
|
|
if (typeof value === "string") return value;
|
|
if (Array.isArray(value)) return value.map(extractBlockText).join("\n");
|
|
const record = asRecord(value);
|
|
const nestedContent = record.content;
|
|
const rawOutput = record.rawOutput;
|
|
const nestedText =
|
|
nestedContent && typeof nestedContent === "object"
|
|
? extractBlockText(nestedContent)
|
|
: "";
|
|
const rawOutputText =
|
|
rawOutput === undefined || rawOutput === null
|
|
? ""
|
|
: typeof rawOutput === "string"
|
|
? rawOutput
|
|
: JSON.stringify(rawOutput, null, 2);
|
|
const directText = asString(record.text) ?? asString(record.content);
|
|
return directText || nestedText || rawOutputText || "";
|
|
}
|
|
|
|
export function extractToolArgs(
|
|
update: Record<string, unknown>,
|
|
): Record<string, unknown> {
|
|
const candidates = [
|
|
update.args,
|
|
update.arguments,
|
|
update.input,
|
|
update.rawInput,
|
|
];
|
|
for (const candidate of candidates) {
|
|
if (
|
|
candidate &&
|
|
typeof candidate === "object" &&
|
|
!Array.isArray(candidate)
|
|
) {
|
|
return candidate as Record<string, unknown>;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
export function extractToolIdentity(update: Record<string, unknown>): {
|
|
title: string;
|
|
toolName: string;
|
|
buzzToolName: string | null;
|
|
} {
|
|
const candidates = collectToolNameCandidates(update);
|
|
const knownName = candidates
|
|
.map((candidate) => findBuzzToolName(candidate, true))
|
|
.find((candidate): candidate is string => Boolean(candidate));
|
|
const firstSpecific = candidates.find(
|
|
(candidate) => !isGenericToolTitle(candidate),
|
|
);
|
|
const title =
|
|
asString(update.title) ?? knownName ?? firstSpecific ?? "Tool call";
|
|
return {
|
|
title,
|
|
toolName: knownName ?? normalizeToolName(firstSpecific ?? title),
|
|
buzzToolName: knownName ?? null,
|
|
};
|
|
}
|
|
|
|
function collectToolNameCandidates(update: Record<string, unknown>): string[] {
|
|
const args = extractToolArgs(update);
|
|
const tool = asRecord(update.tool);
|
|
const input = asRecord(update.input);
|
|
const rawInput = asRecord(update.rawInput);
|
|
const candidates = [
|
|
update.toolName,
|
|
update.tool_name,
|
|
update.name,
|
|
update.title,
|
|
update.kind,
|
|
tool.name,
|
|
tool.toolName,
|
|
args.toolName,
|
|
args.tool_name,
|
|
args.name,
|
|
args.method,
|
|
input.toolName,
|
|
input.tool_name,
|
|
input.name,
|
|
rawInput.toolName,
|
|
rawInput.tool_name,
|
|
rawInput.name,
|
|
];
|
|
|
|
return candidates.flatMap((candidate) => {
|
|
const value = asString(candidate);
|
|
return value ? [value] : [];
|
|
});
|
|
}
|
|
|
|
export function extractToolResult(update: Record<string, unknown>): string {
|
|
const contentText = extractContentText(update.content);
|
|
if (contentText) return contentText;
|
|
return extractBlockText(update.rawOutput);
|
|
}
|
|
|
|
export function describeTurnStarted(payload: unknown): string {
|
|
const record = asRecord(payload);
|
|
const ids = Array.isArray(record.triggeringEventIds)
|
|
? record.triggeringEventIds.filter(
|
|
(id): id is string => typeof id === "string",
|
|
)
|
|
: [];
|
|
return ids.length > 0
|
|
? `Triggered by ${ids.length === 1 ? "1 event" : `${ids.length} events`}.`
|
|
: "";
|
|
}
|
|
|
|
export function describeSessionResolved(payload: unknown): string {
|
|
const record = asRecord(payload);
|
|
const isNewSession = record.isNewSession === true;
|
|
return isNewSession ? "New session created." : "";
|
|
}
|
|
|
|
export function describeRawEvent(event: ObserverEvent): string {
|
|
const payload = asRecord(event.payload);
|
|
const method = asString(payload.method);
|
|
if (method === "session/update") {
|
|
const update = asRecord(asRecord(payload.params).update);
|
|
return asString(update.sessionUpdate) ?? method;
|
|
}
|
|
return method ?? event.kind;
|
|
}
|