refactor(opencode): separate deferred MCP prompt
This commit is contained in:
parent
421e33e531
commit
d3e2382f53
4 changed files with 53 additions and 48 deletions
|
|
@ -43,16 +43,19 @@ export const DEFERRED_TOOL_SYSTEM_PROMPT = `Deferred tools are separate from dir
|
||||||
- Use \`call_deferred_tool\` only with a \`tool_id\` copied from \`search_deferred_tools\` results.
|
- Use \`call_deferred_tool\` only with a \`tool_id\` copied from \`search_deferred_tools\` results.
|
||||||
- Never use deferred tools for direct tools already listed in the current tool list, including shell, file, search, edit, web, task, LSP, question, or apply_patch tools.`
|
- Never use deferred tools for direct tools already listed in the current tool list, including shell, file, search, edit, web, task, LSP, question, or apply_patch tools.`
|
||||||
|
|
||||||
interface Input {
|
interface Input extends DeferredInput {
|
||||||
agent: Agent.Info
|
|
||||||
model: Provider.Model
|
model: Provider.Model
|
||||||
session: Session.Info
|
|
||||||
processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
|
processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
|
||||||
bypassAgentCheck: boolean
|
bypassAgentCheck: boolean
|
||||||
messages: SessionV1.WithParts[]
|
|
||||||
promptOps: TaskPromptOps
|
promptOps: TaskPromptOps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DeferredInput {
|
||||||
|
agent: Agent.Info
|
||||||
|
session: Session.Info
|
||||||
|
messages: SessionV1.WithParts[]
|
||||||
|
}
|
||||||
|
|
||||||
interface DeferredToolDescriptor {
|
interface DeferredToolDescriptor {
|
||||||
id: string
|
id: string
|
||||||
description: string
|
description: string
|
||||||
|
|
@ -60,11 +63,6 @@ interface DeferredToolDescriptor {
|
||||||
searchText: string
|
searchText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Output {
|
|
||||||
tools: Record<string, Tool>
|
|
||||||
deferredSystemPrompt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchDeferredToolsParameters = Schema.Struct({
|
const SearchDeferredToolsParameters = Schema.Struct({
|
||||||
query: Schema.String.annotate({
|
query: Schema.String.annotate({
|
||||||
description:
|
description:
|
||||||
|
|
@ -104,7 +102,6 @@ export const resolve = Effect.fn("SessionMcpTools.resolve")(function* (input: In
|
||||||
const permission = yield* Permission.Service
|
const permission = yield* Permission.Service
|
||||||
const mcp = yield* MCP.Service
|
const mcp = yield* MCP.Service
|
||||||
const truncate = yield* Truncate.Service
|
const truncate = yield* Truncate.Service
|
||||||
const flags = yield* RuntimeFlags.Service
|
|
||||||
|
|
||||||
const context = (args: Record<string, unknown>, options: ToolExecutionOptions): Context => ({
|
const context = (args: Record<string, unknown>, options: ToolExecutionOptions): Context => ({
|
||||||
sessionID: input.session.id,
|
sessionID: input.session.id,
|
||||||
|
|
@ -238,36 +235,20 @@ export const resolve = Effect.fn("SessionMcpTools.resolve")(function* (input: In
|
||||||
if (hasMcpResourceServer) addResourceTools(tools, { input, run, mcp, plugin, truncate, context })
|
if (hasMcpResourceServer) addResourceTools(tools, { input, run, mcp, plugin, truncate, context })
|
||||||
|
|
||||||
const mcpTools = yield* mcp.tools()
|
const mcpTools = yield* mcp.tools()
|
||||||
const mcpDisabled = Permission.disabled(
|
const deferred = yield* deferredState(input, mcpTools)
|
||||||
Object.keys(mcpTools),
|
|
||||||
Permission.merge(input.agent.permission, input.session.permission ?? []),
|
|
||||||
)
|
|
||||||
const userTools = currentUserToolOverrides(input.messages)
|
|
||||||
const allowedMcpTools = Object.fromEntries(
|
|
||||||
Object.entries(mcpTools).filter(([key]) => userTools?.[key] !== false && !mcpDisabled.has(key)),
|
|
||||||
)
|
|
||||||
const deferredDescriptors =
|
|
||||||
flags.experimentalToolSearch && Object.keys(allowedMcpTools).length > 0
|
|
||||||
? yield* deferredToolDescriptors(allowedMcpTools)
|
|
||||||
: []
|
|
||||||
const deferMcpTools =
|
|
||||||
deferredDescriptors.length > 0 && deferredToolSchemaTokens(deferredDescriptors) >= MIN_DEFERRED_MCP_SCHEMA_TOKENS
|
|
||||||
|
|
||||||
if (deferMcpTools) {
|
if (deferred) {
|
||||||
addDeferredTools(tools, {
|
addDeferredTools(tools, {
|
||||||
input,
|
input,
|
||||||
run,
|
run,
|
||||||
plugin,
|
plugin,
|
||||||
truncate,
|
truncate,
|
||||||
context,
|
context,
|
||||||
deferredDescriptors,
|
deferredDescriptors: deferred.descriptors,
|
||||||
allowedMcpTools,
|
allowedMcpTools: deferred.allowedMcpTools,
|
||||||
executeMcpTool,
|
executeMcpTool,
|
||||||
})
|
})
|
||||||
return {
|
return tools
|
||||||
tools,
|
|
||||||
deferredSystemPrompt: deferredSystemPrompt(allowedMcpTools, clients),
|
|
||||||
} satisfies Output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, item] of Object.entries(mcpTools)) {
|
for (const [key, item] of Object.entries(mcpTools)) {
|
||||||
|
|
@ -281,9 +262,37 @@ export const resolve = Effect.fn("SessionMcpTools.resolve")(function* (input: In
|
||||||
tools[key] = item
|
tools[key] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
return { tools } satisfies Output
|
return tools
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const systemPrompt = Effect.fn("SessionMcpTools.systemPrompt")(function* (input: DeferredInput) {
|
||||||
|
const mcp = yield* MCP.Service
|
||||||
|
const deferred = yield* deferredState(input, yield* mcp.tools())
|
||||||
|
if (!deferred) return undefined
|
||||||
|
return deferredSystemPrompt(deferred.allowedMcpTools, yield* mcp.clients())
|
||||||
|
})
|
||||||
|
|
||||||
|
function deferredState(input: DeferredInput, mcpTools: Record<string, Tool>) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const flags = yield* RuntimeFlags.Service
|
||||||
|
if (!flags.experimentalToolSearch) return undefined
|
||||||
|
|
||||||
|
const mcpDisabled = Permission.disabled(
|
||||||
|
Object.keys(mcpTools),
|
||||||
|
Permission.merge(input.agent.permission, input.session.permission ?? []),
|
||||||
|
)
|
||||||
|
const userTools = currentUserToolOverrides(input.messages)
|
||||||
|
const allowedMcpTools = Object.fromEntries(
|
||||||
|
Object.entries(mcpTools).filter(([key]) => userTools?.[key] !== false && !mcpDisabled.has(key)),
|
||||||
|
)
|
||||||
|
if (Object.keys(allowedMcpTools).length === 0) return undefined
|
||||||
|
|
||||||
|
const descriptors = yield* deferredToolDescriptors(allowedMcpTools)
|
||||||
|
if (deferredToolSchemaTokens(descriptors) < MIN_DEFERRED_MCP_SCHEMA_TOKENS) return undefined
|
||||||
|
return { allowedMcpTools, descriptors }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function deferredSystemPrompt(allowedMcpTools: Record<string, Tool>, clients: Record<string, unknown>) {
|
function deferredSystemPrompt(allowedMcpTools: Record<string, Tool>, clients: Record<string, unknown>) {
|
||||||
const servers = deferredServerSummaries(Object.keys(allowedMcpTools), Object.keys(clients))
|
const servers = deferredServerSummaries(Object.keys(allowedMcpTools), Object.keys(clients))
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ import { eq } from "drizzle-orm"
|
||||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||||
import { SessionReminders } from "./reminders"
|
import { SessionReminders } from "./reminders"
|
||||||
import { SessionTools } from "./tools"
|
import { SessionTools } from "./tools"
|
||||||
|
import { SessionMcpTools } from "./mcp-tools"
|
||||||
import { LLMEvent } from "@opencode-ai/llm"
|
import { LLMEvent } from "@opencode-ai/llm"
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -1223,7 +1224,7 @@ export const layer = Layer.effect(
|
||||||
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
|
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
|
||||||
const promptOps = yield* ops()
|
const promptOps = yield* ops()
|
||||||
|
|
||||||
const resolvedTools = yield* SessionTools.resolve({
|
const tools = yield* SessionTools.resolve({
|
||||||
agent,
|
agent,
|
||||||
session,
|
session,
|
||||||
model,
|
model,
|
||||||
|
|
@ -1239,7 +1240,6 @@ export const layer = Layer.effect(
|
||||||
Effect.provideService(Truncate.Service, truncate),
|
Effect.provideService(Truncate.Service, truncate),
|
||||||
Effect.provideService(RuntimeFlags.Service, flags),
|
Effect.provideService(RuntimeFlags.Service, flags),
|
||||||
)
|
)
|
||||||
const tools = resolvedTools.tools
|
|
||||||
|
|
||||||
if (lastUser.format?.type === "json_schema") {
|
if (lastUser.format?.type === "json_schema") {
|
||||||
tools["StructuredOutput"] = createStructuredOutputTool({
|
tools["StructuredOutput"] = createStructuredOutputTool({
|
||||||
|
|
@ -1260,7 +1260,10 @@ export const layer = Layer.effect(
|
||||||
sys.environment(model),
|
sys.environment(model),
|
||||||
instruction.system().pipe(Effect.orDie),
|
instruction.system().pipe(Effect.orDie),
|
||||||
sys.mcp(agent, session.permission),
|
sys.mcp(agent, session.permission),
|
||||||
Effect.succeed(resolvedTools.deferredSystemPrompt),
|
SessionMcpTools.systemPrompt({ agent, session, messages: msgs }).pipe(
|
||||||
|
Effect.provideService(MCP.Service, mcp),
|
||||||
|
Effect.provideService(RuntimeFlags.Service, flags),
|
||||||
|
),
|
||||||
MessageV2.toModelMessagesEffect(msgs, model),
|
MessageV2.toModelMessagesEffect(msgs, model),
|
||||||
])
|
])
|
||||||
const system = [
|
const system = [
|
||||||
|
|
|
||||||
|
|
@ -108,12 +108,8 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const mcp = yield* SessionMcpTools.resolve(input)
|
Object.assign(tools, yield* SessionMcpTools.resolve(input))
|
||||||
Object.assign(tools, mcp.tools)
|
return tools
|
||||||
return {
|
|
||||||
tools,
|
|
||||||
deferredSystemPrompt: mcp.deferredSystemPrompt,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export * as SessionTools from "./tools"
|
export * as SessionTools from "./tools"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { MCP } from "@/mcp"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { SessionTools } from "@/session/tools"
|
import { SessionTools } from "@/session/tools"
|
||||||
|
import { SessionMcpTools } from "@/session/mcp-tools"
|
||||||
import { MessageID, SessionID } from "@/session/schema"
|
import { MessageID, SessionID } from "@/session/schema"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
|
|
@ -132,7 +133,7 @@ const belowThresholdIt = makeIt({
|
||||||
queryDescription: "Natural language analytics query",
|
queryDescription: "Natural language analytics query",
|
||||||
})
|
})
|
||||||
|
|
||||||
function resolveToolResult(input: { messages?: SessionV1.WithParts[] } = {}) {
|
function resolveTools(input: { messages?: SessionV1.WithParts[] } = {}) {
|
||||||
return SessionTools.resolve({
|
return SessionTools.resolve({
|
||||||
agent,
|
agent,
|
||||||
model,
|
model,
|
||||||
|
|
@ -144,10 +145,6 @@ function resolveToolResult(input: { messages?: SessionV1.WithParts[] } = {}) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTools(input: { messages?: SessionV1.WithParts[] } = {}) {
|
|
||||||
return resolveToolResult(input).pipe(Effect.map((result) => result.tools))
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("session.tools", () => {
|
describe("session.tools", () => {
|
||||||
deferredIt.instance("defers MCP tools behind fixed search and call tools", () =>
|
deferredIt.instance("defers MCP tools behind fixed search and call tools", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -184,7 +181,7 @@ describe("session.tools", () => {
|
||||||
|
|
||||||
deferredIt.instance("lists deferred MCP servers in the system prompt", () =>
|
deferredIt.instance("lists deferred MCP servers in the system prompt", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const prompt = (yield* resolveToolResult()).deferredSystemPrompt
|
const prompt = yield* SessionMcpTools.systemPrompt({ agent, session, messages: [] })
|
||||||
|
|
||||||
expect(prompt).toContain("Deferred MCP servers available through `search_deferred_tools`:")
|
expect(prompt).toContain("Deferred MCP servers available through `search_deferred_tools`:")
|
||||||
expect(prompt).toContain("- posthog: 2 tools")
|
expect(prompt).toContain("- posthog: 2 tools")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue