@sema-agent/core
Version:
Stateless, task-oriented AI agent core
116 lines (114 loc) • 4.54 kB
JavaScript
import { brainToRuntime } from "../runtime.js";
import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
import { cutEnginePrefix, cutEngineSegments, flattenableUserText } from "../untrusted-text.js";
const SUGGESTION_MAX_CHARS = 200;
const SUGGEST_SYSTEM_PROMPT = `You suggest plausible NEXT prompts a user might send to continue this conversation.
Rules:
- Ground every suggestion ONLY in the conversation transcript above. Do NOT invent tasks, files, or facts not present.
- Each suggestion is a short, concrete prompt the USER would type next (an imperative or a question), not a description of what you did.
- If the conversation gives no meaningful next step (e.g. it is trivial, fully resolved with nothing to follow up, or off-topic), respond with an empty array.
- Respond with ONLY a JSON array of strings. No prose, no markdown, no code fences. Example: ["Add tests for the new parser","Why did the build fail?"]`;
function renderTranscript(messages) {
const lines = [];
for (const m of messages) {
const role = m.role;
if (role === "user") {
const um = m;
if (um.engineMinted === true || um.provenance === "engine-note")
continue;
const content = um.content;
const flat = flattenableUserText(content);
let text;
if (flat === undefined) {
text = typeof content === "string" ? content : extractText(content);
}
else {
const bySegments = cutEngineSegments(flat, um.engineSegments);
text = bySegments.malformed ? flat : bySegments.cut ? bySegments.text : cutEnginePrefix(flat, um.enginePrefixChars).text;
}
if (text.trim())
lines.push(`USER: ${text.trim()}`);
}
else if (role === "assistant") {
const text = extractText(m.content);
if (text.trim())
lines.push(`ASSISTANT: ${text.trim()}`);
}
}
return lines.join("\n\n");
}
function extractText(content) {
if (typeof content === "string")
return content;
if (!Array.isArray(content))
return "";
return content
.filter((p) => p?.type === "text" && typeof p.text === "string")
.map((p) => p.text)
.join("");
}
export function parseSuggestions(text, count) {
const arr = tryParseStringArray(text);
if (!arr)
return [];
const out = [];
for (const s of arr) {
if (typeof s !== "string")
continue;
const t = s.replace(/\s+/g, " ").trim();
if (!t)
continue;
out.push(t.length > SUGGESTION_MAX_CHARS ? t.slice(0, SUGGESTION_MAX_CHARS) : t);
if (out.length >= count)
break;
}
return out;
}
function tryParseStringArray(text) {
const direct = safeJsonArray(text.trim());
if (direct)
return direct;
const start = text.indexOf("[");
const end = text.lastIndexOf("]");
if (start >= 0 && end > start) {
const sliced = safeJsonArray(text.slice(start, end + 1));
if (sliced)
return sliced;
}
return null;
}
function safeJsonArray(s) {
try {
const v = JSON.parse(s);
return Array.isArray(v) ? v : null;
}
catch {
return null;
}
}
export async function generatePromptSuggestions(llm, transcript, count) {
const empty = { suggestions: [], tokens: 0, costMicroUsd: 0 };
if (count <= 0)
return empty;
const rendered = renderTranscript(transcript);
if (!rendered.trim())
return empty;
const runtime = brainToRuntime(llm.brain);
const auth = await llm.getApiKeyAndHeaders?.(llm.model);
const message = await runtime.completeSimple(llm.model, {
systemPrompt: SUGGEST_SYSTEM_PROMPT,
messages: [{ role: "user", content: `Conversation transcript:\n\n${rendered}\n\nSuggest up to ${count} next prompts.`, timestamp: Date.now() }],
}, {
reasoning: llm.thinking && llm.thinking !== "off" ? llm.thinking : undefined,
signal: llm.signal,
apiKey: auth?.apiKey,
headers: auth?.headers,
});
const text = message.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("");
const u = message.usage;
const { costMicroUsd } = usageCostMicroUsd(cacheFamilyOf(llm.model), u, llm.pricing);
return { suggestions: parseSuggestions(text, count), tokens: u.totalTokens || 0, costMicroUsd };
}