pull/5462/head
Dax Raad 2025-12-10 00:39:24 -05:00
parent 915559b532
commit 5a382b31d8
7 changed files with 142 additions and 112 deletions

View File

@ -10,17 +10,5 @@
"options": {},
},
},
"mcp": {
"exa": {
"type": "remote",
"url": "https://mcp.exa.ai/mcp",
},
"morph": {
"type": "local",
"command": ["bunx", "@morphllm/morphmcp"],
"environment": {
"ENABLED_TOOLS": "warp_grep",
},
},
},
"mcp": {},
}

View File

@ -157,6 +157,51 @@ export namespace Agent {
mode: "primary",
builtIn: true,
},
summary: {
name: "summary",
mode: "subagent",
options: {},
builtIn: true,
permission: agentPermission,
prompt: `You are a title generator. You output ONLY a thread title. Nothing else.
<task>
Generate a brief title that would help the user find this conversation later.
Follow all rules in <rules>
Use the <examples> so you know what a good title looks like.
Your output must be:
- A single line
- 50 characters
- No explanations
</task>
<rules>
- Focus on the main topic or question the user needs to retrieve
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
- Keep exact: technical terms, numbers, filenames, HTTP codes
- Remove: the, this, my, a, an
- Never assume tech stack
- Never use tools
- NEVER respond to questions, just generate a title for the conversation
- The title should NEVER include "summarizing" or "generating" when generating a title
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
- Always output something meaningful, even if the input is minimal.
- If the user message is short or conversational (e.g. hello, lol, whats up, hey):
create a title that reflects the users tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
</rules>
<examples>
"hey" -> Greeting
"debug 500 errors in production" Debugging production 500 errors
"refactor user service" Refactoring user service
"why is app.js failing" Analyzing app.js failure
"implement rate limiting" Implementing rate limiting
"how do I connect postgres to my API" Connecting Postgres to API
"best practices for React hooks" React hooks best practices
</examples>`,
tools: {},
},
plan: {
name: "plan",
options: {},

View File

@ -129,7 +129,7 @@ export namespace SessionCompaction {
})
const agent = await Agent.get(input.agent)
const result = await processor.process({
requestID: input.parentID,
user: input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User,
agent,
abort: input.abort,
sessionID: input.sessionID,

View File

@ -8,6 +8,7 @@ import { Instance } from "@/project/instance"
import type { Agent } from "@/agent/agent"
import type { MessageV2 } from "./message-v2"
import { Plugin } from "@/plugin"
import { SystemPrompt } from "./system"
export namespace LLM {
const log = Log.create({ service: "llm" })
@ -22,6 +23,7 @@ export namespace LLM {
system: string[]
abort: AbortSignal
messages: ModelMessage[]
small?: boolean
tools: Record<string, Tool>
retries?: number
}
@ -29,9 +31,19 @@ export namespace LLM {
export type StreamOutput = StreamTextResult<ToolSet, unknown>
export async function stream(input: StreamInput) {
const l = log
.clone()
.tag("providerID", input.model.providerID)
.tag("modelID", input.model.id)
.tag("sessionID", input.sessionID)
.tag("small", (input.small ?? false).toString())
l.info("stream", {
modelID: input.model.id,
providerID: input.model.providerID,
})
const [language, cfg] = await Promise.all([Provider.getLanguage(input.model), Config.get()])
const [first, ...rest] = input.system
const [first, ...rest] = [...SystemPrompt.header(input.model.providerID), ...input.system]
const system = [first, rest.join("\n")]
const params = await Plugin.trigger(
@ -49,13 +61,18 @@ export namespace LLM {
: undefined,
topP: input.agent.topP ?? ProviderTransform.topP(input.model),
options: pipe(
ProviderTransform.options(input.model, input.sessionID),
mergeDeep(ProviderTransform.options(input.model, input.sessionID)),
input.small ? mergeDeep(ProviderTransform.smallOptions(input.model)) : mergeDeep({}),
mergeDeep(input.model.options),
mergeDeep(input.agent.options),
),
},
)
l.info("params", {
params,
})
const maxOutputTokens = ProviderTransform.maxOutputTokens(
input.model.api.npm,
params.options,
@ -65,14 +82,14 @@ export namespace LLM {
return streamText({
onError(error) {
log.error("stream error", {
l.error("stream error", {
error,
})
},
async experimental_repairToolCall(failed) {
const lower = failed.toolCall.toolName.toLowerCase()
if (lower !== failed.toolCall.toolName && input.tools[lower]) {
log.info("repairing tool call", {
l.info("repairing tool call", {
tool: failed.toolCall.toolName,
repaired: lower,
})
@ -94,6 +111,7 @@ export namespace LLM {
topP: params.topP,
providerOptions: ProviderTransform.providerOptions(input.model, params.options, input.messages),
activeTools: Object.keys(input.tools).filter((x) => x !== "invalid"),
tools: input.tools,
maxOutputTokens,
abortSignal: input.abort,
headers: {

View File

@ -41,6 +41,8 @@ import { fn } from "@/util/fn"
import { SessionProcessor } from "./processor"
import { TaskTool } from "@/tool/task"
import { SessionStatus } from "./status"
import { LLM } from "./llm"
import { iife } from "@/util/iife"
// @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false
@ -281,7 +283,6 @@ export namespace SessionPrompt {
})
const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
const language = await Provider.getLanguage(model)
const task = tasks.pop()
// pending subtask
@ -427,7 +428,6 @@ export namespace SessionPrompt {
}
// normal processing
const cfg = await Config.get()
const agent = await Agent.get(lastUser.agent)
const maxSteps = agent.maxSteps ?? Infinity
const isLastStep = step >= maxSteps
@ -435,6 +435,7 @@ export namespace SessionPrompt {
messages: msgs,
agent,
})
const processor = SessionProcessor.create({
assistantMessage: (await Session.updateMessage({
id: Identifier.ascending("message"),
@ -467,7 +468,6 @@ export namespace SessionPrompt {
model,
agent,
system: lastUser.system,
isLastStep,
})
const tools = await resolveTools({
agent,
@ -526,13 +526,9 @@ export namespace SessionPrompt {
return Provider.defaultModel()
}
async function resolveSystemPrompt(input: {
system?: string
agent: Agent.Info
model: Provider.Model
isLastStep?: boolean
}) {
let system = SystemPrompt.header(input.model.providerID)
async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) {
using _ = log.time("system")
let system = []
system.push(
...(() => {
if (input.system) return [input.system]
@ -542,14 +538,6 @@ export namespace SessionPrompt {
)
system.push(...(await SystemPrompt.environment()))
system.push(...(await SystemPrompt.custom()))
if (input.isLastStep) {
system.push(MAX_STEPS)
}
// max 2 system prompt messages for caching purposes
const [first, ...rest] = system
system = [first, rest.join("\n")]
return system
}
@ -560,6 +548,7 @@ export namespace SessionPrompt {
tools?: Record<string, boolean>
processor: SessionProcessor.Info
}) {
using _ = log.time("resolveTools")
const tools: Record<string, AITool> = {}
const enabledTools = pipe(
input.agent.tools,
@ -1319,28 +1308,24 @@ export namespace SessionPrompt {
input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
.length === 1
if (!isFirst) return
const cfg = await Config.get()
const small =
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
const language = await Provider.getLanguage(small)
const provider = await Provider.getProvider(small.providerID)
const options = pipe(
{},
mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)),
mergeDeep(ProviderTransform.smallOptions(small)),
mergeDeep(small.options),
)
await generateText({
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
providerOptions: ProviderTransform.providerOptions(small, options, []),
const agent = await Agent.get("summary")
if (!agent) return
const result = await LLM.stream({
agent,
user: input.message.info as MessageV2.User,
system: [agent.prompt!],
small: true,
tools: {},
model: await iife(async () => {
if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID)
return (
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
)
}),
abort: new AbortController().signal,
sessionID: input.session.id,
retries: 2,
messages: [
...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({
role: "system",
content: x,
}),
),
{
role: "user",
content: "Generate a title for this conversation:\n",
@ -1364,32 +1349,19 @@ export namespace SessionPrompt {
},
]),
],
headers: small.headers,
model: language,
experimental_telemetry: {
isEnabled: cfg.experimental?.openTelemetry,
metadata: {
userId: cfg.username ?? "unknown",
sessionId: input.session.id,
},
},
})
.then((result) => {
if (result.text)
return Session.update(input.session.id, (draft) => {
const cleaned = result.text
.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
.split("\n")
.map((line) => line.trim())
.find((line) => line.length > 0)
if (!cleaned) return
const text = await result.text.catch((err) => log.error("failed to generate title", { error: err }))
if (text)
return Session.update(input.session.id, (draft) => {
const cleaned = text
.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
.split("\n")
.map((line) => line.trim())
.find((line) => line.length > 0)
if (!cleaned) return
const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
draft.title = title
})
})
.catch((error) => {
log.error("failed to generate title", { error, model: small.id })
const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
draft.title = title
})
}
}

View File

@ -50,35 +50,36 @@ const parser = lazy(async () => {
return p
})
const getShell = lazy(() => {
const s = process.env.SHELL
if (s) {
const basename = path.basename(s)
if (!new Set(["fish", "nu"]).has(basename)) {
return s
}
}
if (process.platform === "darwin") {
return "/bin/zsh"
}
if (process.platform === "win32") {
// Let Bun / Node pick COMSPEC (usually cmd.exe)
// or explicitly:
return process.env.COMSPEC || true
}
const bash = Bun.which("bash")
if (bash) {
return bash
}
return true
})
// TODO: we may wanna rename this tool so it works better on other shells
export const BashTool = Tool.define("bash", async () => {
const shell = iife(() => {
const s = process.env.SHELL
if (s) {
const basename = path.basename(s)
if (!new Set(["fish", "nu"]).has(basename)) {
return s
}
}
if (process.platform === "darwin") {
return "/bin/zsh"
}
if (process.platform === "win32") {
// Let Bun / Node pick COMSPEC (usually cmd.exe)
// or explicitly:
return process.env.COMSPEC || true
}
const bash = Bun.which("bash")
if (bash) {
return bash
}
return true
})
const shell = getShell()
log.info("bash tool using shell", { shell })
return {

View File

@ -21,8 +21,11 @@ import { Plugin } from "../plugin"
import { WebSearchTool } from "./websearch"
import { CodeSearchTool } from "./codesearch"
import { Flag } from "@/flag/flag"
import { Log } from "@/util/log"
export namespace ToolRegistry {
const log = Log.create({ service: "tool.registry" })
export const state = Instance.state(async () => {
const custom = [] as Tool.Info[]
const glob = new Bun.Glob("tool/*.{js,ts}")
@ -119,10 +122,13 @@ export namespace ToolRegistry {
}
return true
})
.map(async (t) => ({
id: t.id,
...(await t.init()),
})),
.map(async (t) => {
using _ = log.time(t.id)
return {
id: t.id,
...(await t.init()),
}
}),
)
return result
}