UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

116 lines (114 loc) • 4.54 kB
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 }; }