Files

345 lines
10 KiB
JavaScript

import { createMockId } from "../../state.mjs";
import {
collectMessagesByRole,
detectModelFamily,
latestRoleMessage,
} from "./shared.mjs";
function compactWhitespace(text) {
return String(text || "")
.replace(/\s+/g, " ")
.trim();
}
function sentenceParts(text) {
return compactWhitespace(text)
.split(/(?<=[.!?])\s+/)
.map((part) => part.trim())
.filter(Boolean);
}
function truncate(text, limit = 160) {
const compact = compactWhitespace(text);
if (compact.length <= limit) return compact;
return `${compact.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
}
function inferLanguage(prompt, thread) {
const lower = String(prompt || "").toLowerCase();
if (/typescript|tsx|ts\b/.test(lower)) return "ts";
if (/javascript|node|react|js\b/.test(lower)) return "js";
if (/python|pytest/.test(lower)) return "python";
if (/rust|cargo/.test(lower)) return "rust";
if (/bash|shell|zsh|sh\b/.test(lower)) return "bash";
if (/json/.test(lower)) return "json";
return thread?.lastCodeLanguage || "ts";
}
function makeCodeSnippet(language, prompt, thread) {
const lower = String(prompt || "").toLowerCase();
if (language === "python") {
if (/async/.test(lower)) {
return [
"import asyncio",
"",
"async def run_task(name: str) -> str:",
" await asyncio.sleep(0)",
' return f\"done:{name}\"',
].join("\n");
}
if (/test/.test(lower)) {
return [
"def test_run_task_returns_done():",
' assert run_task_sync(\"build\") == \"done:build\"',
].join("\n");
}
return [
"def run_task_sync(name: str) -> str:",
' return f\"done:{name}\"',
].join("\n");
}
if (language === "rust") {
if (/async/.test(lower)) {
return [
"pub async fn run_task(name: &str) -> String {",
' format!("done:{name}")',
"}",
].join("\n");
}
return [
"pub fn run_task(name: &str) -> String {",
' format!("done:{name}")',
"}",
].join("\n");
}
if (language === "bash") {
return [
"#!/usr/bin/env bash",
"set -euo pipefail",
'name=\"${1:-build}\"',
'printf \"done:%s\\n\" \"$name\"',
].join("\n");
}
if (language === "json") {
return JSON.stringify(
{
task: "mock",
prompt: truncate(prompt, 48),
mode: /async/.test(lower) ? "async" : "sync",
},
null,
2,
);
}
const asyncKeyword = language === "ts" ? "async " : "";
const typeSuffix = language === "ts" ? ": Promise<string>" : "";
const awaitLine = /async/.test(lower) ? " await Promise.resolve();\n" : "";
const testBlock = /test/.test(lower)
? `\nexport function runTaskTest() {\n return runTask("build");\n}\n`
: "\n";
return [
`${asyncKeyword}function runTask(name${language === "ts" ? ": string" : ""})${typeSuffix} {`,
awaitLine ? awaitLine.trimEnd() : " return `done:${name}`;",
awaitLine ? " return `done:${name}`;" : "",
"}",
testBlock.trimEnd(),
]
.filter(Boolean)
.join("\n");
}
function summarizeText(source, style = "balanced") {
const sentences = sentenceParts(source);
if (sentences.length === 0) return "Nothing substantial to summarize yet.";
if (style === "brief") return truncate(sentences.slice(0, 1).join(" "), 120);
if (style === "bullets") {
return sentences
.slice(0, 3)
.map((sentence) => `- ${truncate(sentence, 90)}`)
.join("\n");
}
return truncate(sentences.slice(0, 3).join(" "), 240);
}
function buildReasoningSummary(prompt, toolText, thread) {
const basis = toolText || prompt || thread?.lastUserMessage || "the request";
return [
`Recommendation: focus on ${truncate(basis, 80)}.`,
thread?.turnCount > 0
? "This follows the previous turn and keeps the same working context."
: "This is the first pass, so the answer stays broad and reversible.",
"If you want, I can tighten this into an implementation plan or a direct patch.",
].join(" ");
}
// Orchestration/delegate tools (analyze_image, delegate_*) require a `prompt`
// argument (and carry `citation_requirement`). The dynamic fallback can only
// synthesize simple worker-tool args (q/path/cmd/url), so a fabricated call to a
// delegate tool fails schema validation with a confusing "arguments.prompt is
// required" bubble (tinyhumansai/openhuman#4517). Treat only tools that do NOT
// require `prompt` as fabricatable; otherwise fall through to benign text.
function isSimpleWorkerTool(toolDef) {
const params = toolDef?.function?.parameters || toolDef?.parameters || {};
const required = Array.isArray(params.required) ? params.required : [];
return !required.includes("prompt");
}
function inferToolCalls(prompt, toolDefs = []) {
const lower = String(prompt || "").toLowerCase();
const declared = (Array.isArray(toolDefs) ? toolDefs : []).filter(
isSimpleWorkerTool,
);
// Only delegate/orchestration tools are available — the fallback can't build a
// valid call, so return benign text rather than a schema-error tool call.
if (declared.length === 0) return [];
const picked = (name, args) => ({
id: createMockId("tool"),
name,
arguments: args,
});
if (/search|look up|find|research/.test(lower)) {
return [
picked(declared[0]?.function?.name || declared[0]?.name || "web_search", {
q: truncate(prompt, 120),
}),
];
}
if (/read file|open file|inspect file/.test(lower)) {
return [
picked(declared[0]?.function?.name || declared[0]?.name || "fs_read", {
path: "src/example.ts",
}),
];
}
if (/run|shell|command|git/.test(lower)) {
return [
picked(declared[0]?.function?.name || declared[0]?.name || "shell_exec", {
cmd: "echo mock-agentic-run",
}),
];
}
if (/fetch|download|http|url/.test(lower)) {
return [
picked(declared[0]?.function?.name || declared[0]?.name || "http_fetch", {
url: "https://example.com/mock",
}),
];
}
if (declared.length > 0) {
return [
picked(declared[0]?.function?.name || declared[0]?.name || "mock_tool", {
input: truncate(prompt, 120),
}),
];
}
return [];
}
function latestToolResult(parsedBody, thread) {
const toolMessage = latestRoleMessage(parsedBody, "tool");
return toolMessage?.normalizedContent || thread?.lastToolResult || "";
}
function followUpMode(prompt) {
const lower = String(prompt || "").toLowerCase();
return {
shorter: /shorter|brief|condense|tldr|tl;dr/.test(lower),
continue: /continue|go on|next/.test(lower),
expand: /expand|more detail|elaborate/.test(lower),
async: /async/.test(lower),
tests: /test|spec/.test(lower),
};
}
export function buildDynamicCompletion({ model, parsedBody, thread }) {
const family = detectModelFamily({ model, parsedBody });
const latestUser = latestRoleMessage(parsedBody, "user");
const prompt = latestUser?.normalizedContent || "";
const toolText = latestToolResult(parsedBody, thread);
const mode = followUpMode(prompt);
if (family === "summarization") {
const sourceCandidates = [
toolText,
thread?.lastAssistantContent,
...collectMessagesByRole(parsedBody, "assistant").map(
(item) => item.normalizedContent,
),
...collectMessagesByRole(parsedBody, "user")
.slice(0, -1)
.map((item) => item.normalizedContent),
].filter(Boolean);
const source = sourceCandidates.join(" ");
const content = summarizeText(
source ||
prompt ||
thread?.lastUserMessage ||
"No prior material was provided.",
mode.shorter
? "brief"
: /bullet|list/.test(prompt.toLowerCase())
? "bullets"
: "balanced",
);
return {
family,
content,
streamScript: [{ text: content }, { finish: "stop" }],
codeLanguage: null,
};
}
if (family === "coding") {
const language = inferLanguage(prompt, thread);
const code = makeCodeSnippet(language, prompt, thread);
const intro =
thread?.turnCount > 0
? `Updated ${language.toUpperCase()} version based on the previous turn:`
: `Generated ${language.toUpperCase()} starter:`;
const content = `${intro}\n\n\`\`\`${language}\n${code}\n\`\`\``;
return {
family,
content,
streamScript: [
{ text: intro },
{ text: `\n\n\`\`\`${language}\n${code}\n\`\`\`` },
{ finish: "stop" },
],
codeLanguage: language,
};
}
if (family === "agentic") {
const toolCalls = toolText ? [] : inferToolCalls(prompt, parsedBody?.tools);
const content = toolText
? `Tool result received. The relevant outcome is: ${truncate(
toolText,
mode.shorter ? 100 : 220,
)}${mode.expand ? " I can turn this into a fuller answer if needed." : ""}`
: "Planning tool work now.";
return {
family,
content,
toolCalls,
streamScript:
toolCalls.length > 0
? [
{ text: "Planning tool work now." },
...toolCalls.map((toolCall) => ({ toolCall })),
{ finish: "tool_calls" },
]
: [{ text: content }, { finish: "stop" }],
codeLanguage: null,
};
}
if (family === "reasoning") {
const content = buildReasoningSummary(prompt, toolText, thread);
return {
family,
content,
streamScript: [
{
thinking: `Assessing the request about ${truncate(prompt || toolText || "the current task", 72)}.`,
},
{
thinking:
thread?.turnCount > 0
? "Incorporating prior turn context."
: "Choosing a safe first-pass approach.",
},
{ text: content },
{
usage: { prompt_tokens: 18, completion_tokens: 24, total_tokens: 42 },
},
{ finish: "stop" },
],
codeLanguage: null,
};
}
const content =
mode.shorter && thread?.lastAssistantContent
? truncate(thread.lastAssistantContent, 120)
: thread?.turnCount > 0 && (mode.continue || mode.expand)
? `Continuing from the previous turn: ${truncate(
thread.lastAssistantContent || toolText || prompt || "ready",
mode.expand ? 220 : 140,
)}`
: "Hello from e2e mock agent";
return {
family,
content,
streamScript: [{ text: content }, { finish: "stop" }],
codeLanguage: null,
};
}