Compare commits

...

2 Commits

Author SHA1 Message Date
Aiden Cline 2e69258e3d refactor(core): own the execute tool description 2026-07-20 23:48:47 -05:00
Aiden Cline d98e51def5 feat(core): deliver CodeMode catalog through instructions 2026-07-20 22:40:44 -05:00
10 changed files with 267 additions and 38 deletions
+80
View File
@@ -0,0 +1,80 @@
export * as CodeMode from "./codemode"
import { Context, Effect, Layer, Scope } from "effect"
import { makeLocationNode } from "./effect/app-node"
import { PermissionV2 } from "./permission"
import { ExecuteTool } from "./tool/execute"
import { permission, registrationEntries, type AnyTool } from "./tool/tool"
import { Tools } from "./tool/tools"
import { Wildcard } from "./util/wildcard"
export interface Materialization {
readonly tool?: AnyTool
readonly instructions?: string
}
export interface Interface {
readonly register: (
tools: Readonly<Record<string, AnyTool>>,
options?: Tools.RegisterOptions,
) => Effect.Effect<void, never, Scope.Scope>
readonly materialize: (permissions?: PermissionV2.Ruleset) => Effect.Effect<Materialization>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/CodeMode") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const local = new Map<
string,
Array<{ readonly token: object; readonly registration: ExecuteTool.Registration }>
>()
return Service.of({
register: Effect.fn("CodeMode.register")(function* (tools, options) {
const entries = registrationEntries(tools, options?.namespace)
if (entries.length === 0) return
yield* Effect.uninterruptible(
Effect.gen(function* () {
const token = {}
for (const entry of entries)
local.set(entry.key, [
...(local.get(entry.key) ?? []),
{ token, registration: { tool: entry.tool, name: entry.name, namespace: entry.namespace } },
])
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
for (const entry of entries) {
const registrations = local.get(entry.key)?.filter((item) => item.token !== token) ?? []
if (registrations.length > 0) local.set(entry.key, registrations)
else local.delete(entry.key)
}
}),
)
}),
)
}),
materialize: Effect.fn("CodeMode.materialize")(function* (permissions) {
const registrations = new Map<string, ExecuteTool.Registration>()
const rules = permissions ?? []
for (const [name, entries] of local) {
const registration = entries.at(-1)?.registration
if (!registration) continue
const rule = rules.findLast((rule) => Wildcard.match(permission(registration.tool, name), rule.action))
if (rule?.resource === "*" && rule.effect === "deny") continue
registrations.set(name, registration)
}
if (registrations.size === 0) return {}
const executeRule = rules.findLast((rule) => Wildcard.match("execute", rule.action))
if (executeRule?.resource === "*" && executeRule.effect === "deny") return {}
return {
tool: ExecuteTool.create(registrations),
instructions: ExecuteTool.instructions(registrations),
}
}),
})
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [] })
@@ -0,0 +1,45 @@
export * as CodeModeInstructions from "./instructions"
import { Context, Effect, Layer, Schema } from "effect"
import { AgentV2 } from "../agent"
import { CodeMode } from "../codemode"
import { makeLocationNode } from "../effect/app-node"
import { Instructions } from "../instructions/index"
export interface Interface {
readonly load: (agent: AgentV2.Selection) => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/CodeModeInstructions") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const codeMode = yield* CodeMode.Service
return Service.of({
load: Effect.fn("CodeModeInstructions.load")(function* (selection) {
const instructions = selection.info
? (yield* codeMode.materialize(selection.info.permissions)).instructions
: undefined
return Instructions.make({
key: Instructions.Key.make("core/codemode"),
codec: Schema.toCodecJson(Schema.String),
read: Effect.succeed(instructions ?? Instructions.removed),
render: {
initial: (current) => current,
changed: (_previous, current) =>
[
"The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.",
current,
].join("\n\n"),
removed: () =>
"Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.",
},
})
}),
})
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [CodeMode.node] })
+4
View File
@@ -2,6 +2,8 @@ import { Effect, Layer, LayerMap } from "effect"
import { AgentV2 } from "./agent"
import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { CodeMode } from "./codemode"
import { CodeModeInstructions } from "./codemode/instructions"
import { CommandV2 } from "./command"
import { Config } from "./config"
import { LayerNode } from "./effect/layer-node"
@@ -66,6 +68,7 @@ const locationServiceNodes = [
Pty.node,
Shell.node,
SkillV2.node,
CodeMode.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
LocationMutation.node,
@@ -77,6 +80,7 @@ const locationServiceNodes = [
ToolRegistry.toolsNode,
Image.node,
SkillInstructions.node,
CodeModeInstructions.node,
ReferenceInstructions.node,
InstructionEntry.node,
Form.node,
+4
View File
@@ -12,6 +12,7 @@ import { McpInstructions } from "../mcp/instructions"
import { PluginSupervisor } from "../plugin/supervisor"
import { ReferenceInstructions } from "../reference/instructions"
import { SkillInstructions } from "../skill/instructions"
import { CodeModeInstructions } from "../codemode/instructions"
import { AgentNotFoundError } from "./error"
import { SessionHistory } from "./history"
import { InstructionEntry } from "./instruction-entry"
@@ -54,6 +55,7 @@ const layer = Layer.effect(
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const builtins = yield* InstructionBuiltIns.Service
const codeModeInstructions = yield* CodeModeInstructions.Service
const db = (yield* Database.Service).db
const discovery = yield* InstructionDiscovery.Service
const entries = yield* InstructionEntry.Service
@@ -77,6 +79,7 @@ const layer = Layer.effect(
const instructions = yield* Effect.all(
[
builtins.load(sessionID),
codeModeInstructions.load(agent),
discovery.load(),
skillInstructions.load(agent),
referenceInstructions.load(),
@@ -109,6 +112,7 @@ export const node = makeLocationNode({
layer,
deps: [
AgentV2.node,
CodeModeInstructions.node,
Database.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
+34 -21
View File
@@ -36,34 +36,23 @@ type CollectedFiles = {
readonly files: Array<typeof ExecuteFile.Type>
}
interface Registration {
export interface Registration {
readonly tool: AnyTool
readonly name: string
readonly namespace?: string
}
// Invariant model-facing guidance; the changing tool catalog is delivered through Instructions.
const description = [
"Run JavaScript in a confined Code Mode runtime through { code }.",
"Call Code Mode tools through `tools` using the exact paths and signatures from the instructions.",
"Use `search({ query })` to discover exact signatures when needed.",
"Await important calls and use `Promise.all` for independent calls.",
].join("\n")
export const create = (registrations: ReadonlyMap<string, Registration>) => {
const runtime = (
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
hooks?: CodeMode.ToolCallHooks,
) => {
const tools: Record<string, Tool.Definition<never>> = {}
for (const [name, registration] of registrations) {
const child = definition(name, registration.tool)
const value = Tool.make({
description: child.description,
input: child.inputSchema,
output: child.outputSchema,
run: (input) => invoke(name, registration, input),
})
const path = registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}`
tools[path] = value
}
return CodeMode.make<typeof tools>({ tools, ...hooks })
}
const discovery = runtime(() => Effect.fail(toolError("Execute context is unavailable")))
return make({
description: discovery.instructions(),
description,
input: CodeMode.Input,
output: ExecuteOutput,
structured: ExecuteMetadata,
@@ -92,6 +81,7 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
),
)
const result = yield* runtime(
registrations,
(name, registration, input) =>
Effect.gen(function* () {
const index = yield* Ref.getAndUpdate(callIndex, (index) => index + 1)
@@ -141,6 +131,29 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
})
}
export const instructions = (registrations: ReadonlyMap<string, Registration>) => {
return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable"))).instructions()
}
function runtime(
registrations: ReadonlyMap<string, Registration>,
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
hooks?: CodeMode.ToolCallHooks,
) {
const tools: Record<string, Tool.Definition<never>> = {}
for (const [name, registration] of registrations) {
const child = definition(name, registration.tool)
const path = registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}`
tools[path] = Tool.make({
description: child.description,
input: child.inputSchema,
output: child.outputSchema,
run: (input) => invoke(name, registration, input),
})
}
return CodeMode.make<typeof tools>({ tools, ...hooks })
}
function displayInput(input: unknown): Record<string, unknown> | undefined {
if (input === null || input === undefined) return
if (typeof input !== "object" || Array.isArray(input)) return { input }
+18 -15
View File
@@ -9,7 +9,7 @@ import { SessionMessage } from "../session/message"
import { SessionSchema } from "../session/schema"
import { ToolOutputStore } from "../tool-output-store"
import { Wildcard } from "../util/wildcard"
import { ExecuteTool } from "./execute"
import { CodeMode } from "../codemode"
import {
definition,
permission,
@@ -74,6 +74,7 @@ const registryLayer = Layer.effect(
const resources = yield* ToolOutputStore.Service
const toolHooks = yield* ToolHooks.Service
const image = yield* Image.Service
const codeMode = yield* CodeMode.Service
type NormalizedItem = ToolOutput["content"][number] | "decode" | "size"
const normalizeImages = Effect.fn("ToolRegistry.normalizeImages")(function* (content: ToolOutput["content"]) {
@@ -111,7 +112,6 @@ const registryLayer = Layer.effect(
readonly tool: AnyTool
readonly name: string
readonly namespace?: string
readonly codemode: boolean
}
const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>()
const registrationLock = Semaphore.makeUnsafe(1)
@@ -212,15 +212,22 @@ const registryLayer = Layer.effect(
return yield* Effect.fail(
new RegistrationError({ name: reserved.key, message: 'Tool name "execute" is reserved for CodeMode' }),
)
return { entries, codemode }
return { tools, options, entries, codemode }
}),
)
if (planned.every((plan) => plan.entries.length === 0)) return
// CodeMode registrations live in the CodeMode service; the registry keeps only direct tools.
yield* Effect.forEach(
planned.filter((plan) => plan.codemode && plan.entries.length > 0),
(plan) => codeMode.register(plan.tools, plan.options),
{ discard: true },
)
const direct = planned.filter((plan) => !plan.codemode)
if (direct.every((plan) => plan.entries.length === 0)) return
yield* Effect.uninterruptible(
registrationLock.withPermit(
Effect.gen(function* () {
const token = {}
for (const { entries, codemode } of planned)
for (const { entries } of direct)
for (const entry of entries)
local.set(entry.key, [
...(local.get(entry.key) ?? []),
@@ -230,14 +237,13 @@ const registryLayer = Layer.effect(
tool: entry.tool,
name: entry.name,
namespace: entry.namespace,
codemode,
},
},
])
yield* Effect.addFinalizer(() =>
registrationLock.withPermit(
Effect.sync(() => {
for (const { entries } of planned)
for (const { entries } of direct)
for (const entry of entries) {
const registrations =
local.get(entry.key)?.filter((registration) => registration.token !== token) ?? []
@@ -265,19 +271,16 @@ const registryLayer = Layer.effect(
registerBatch,
materialize: Effect.fn("ToolRegistry.materialize")((permissions) =>
registrationLock.withPermit(
Effect.sync(() => {
Effect.gen(function* () {
const direct = new Map<string, Registration>()
const codemode = new Map<string, Registration>()
const rules = permissions ?? []
for (const [name, entries] of local) {
const registration = entries.at(-1)?.registration
if (!registration) continue
if (whollyDisabled(permission(registration.tool, name), rules)) continue
if (registration.codemode) codemode.set(name, registration)
else direct.set(name, registration)
direct.set(name, registration)
}
const execute =
codemode.size > 0 && !whollyDisabled("execute", rules) ? ExecuteTool.create(codemode) : undefined
const execute = (yield* codeMode.materialize(permissions)).tool
return {
definitions: [
...Array.from(direct, ([name, registration]) => definition(name, registration.tool)),
@@ -315,11 +318,11 @@ function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
export const node = makeLocationNode({
service: Service,
layer,
deps: [ToolOutputStore.node, ToolHooks.node, Image.node],
deps: [CodeMode.node, ToolOutputStore.node, ToolHooks.node, Image.node],
})
export const toolsNode = makeLocationNode({
service: Tools.Service,
layer,
deps: [ToolOutputStore.node, ToolHooks.node, Image.node],
deps: [CodeMode.node, ToolOutputStore.node, ToolHooks.node, Image.node],
})
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect } from "bun:test"
import { CodeMode } from "@opencode-ai/core/codemode"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Tool } from "@opencode-ai/core/tool/tool"
import { Effect, Schema } from "effect"
import { it } from "./lib/effect"
describe("CodeMode", () => {
it.effect("owns registrations, execute, and catalog materialization", () =>
Effect.gen(function* () {
const codeMode = yield* CodeMode.Service
yield* codeMode.register({
echo: Tool.make({
description: "Echo text",
input: Schema.Struct({ text: Schema.String }),
output: Schema.String,
execute: ({ text }) => Effect.succeed(text),
}),
})
const materialized = yield* codeMode.materialize()
expect(materialized.tool).toBeDefined()
expect(materialized.instructions).toContain("Echo text")
expect(materialized.instructions).toContain("tools.echo(input:")
}).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(CodeMode.node))),
)
})
@@ -0,0 +1,49 @@
import { describe, expect } from "bun:test"
import { AgentV2 } from "@opencode-ai/core/agent"
import { CodeMode } from "@opencode-ai/core/codemode"
import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Effect, Layer } from "effect"
import { it } from "../lib/effect"
import { readInitial, readUpdate } from "../lib/instructions"
const agent = AgentV2.Info.make(AgentV2.Info.empty(AgentV2.ID.make("build")))
describe("CodeModeInstructions", () => {
it.effect("renders catalog changes and removal", () => {
let catalog: string | undefined = "Initial Code Mode catalog"
const layer = AppNodeBuilder.build(CodeModeInstructions.node, [
[
CodeMode.node,
Layer.mock(CodeMode.Service, {
materialize: () => Effect.succeed({ ...(catalog === undefined ? {} : { instructions: catalog }) }),
register: () => Effect.void,
}),
],
])
return Effect.gen(function* () {
const instructions = yield* CodeModeInstructions.Service
const initialized = yield* instructions.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(readInitial))
expect(initialized.text).toBe("Initial Code Mode catalog")
catalog = "Updated Code Mode catalog"
expect(
yield* instructions
.load({ id: agent.id, info: agent })
.pipe(Effect.flatMap((context) => readUpdate(context, initialized))),
).toMatchObject({
text: "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.\n\nUpdated Code Mode catalog",
})
catalog = undefined
expect(
yield* instructions
.load({ id: agent.id, info: agent })
.pipe(Effect.flatMap((context) => readUpdate(context, initialized))),
).toMatchObject({
text: "Code Mode tools are no longer available. Do not use any previously listed Code Mode tools.",
})
}).pipe(Effect.provide(layer))
})
})
+3 -2
View File
@@ -767,9 +767,10 @@ it.effect("advertises MCP output schemas to Code Mode", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
yield* waitForTool(registry, "execute")
const execute = (yield* toolDefinitions(registry)).find((tool) => tool.name === "execute")
const materialized = yield* registry.materialize()
const execute = materialized.definitions.find((tool) => tool.name === "execute")
expect(execute?.description).toContain("tools.demo.search(input: {}): Promise<{\n ok: boolean,\n}>")
expect(execute?.description).not.toContain("tools.demo.search")
}),
)
@@ -481,6 +481,9 @@ describe("ToolRegistry", () => {
})
.pipe(Scope.provide(scope))
const materialized = yield* service.materialize()
const execute = materialized.definitions.find((tool) => tool.name === "execute")
expect(execute?.description).toContain("confined Code Mode runtime")
expect(execute?.description).not.toContain("Echo text")
yield* Scope.close(scope, Exit.void)
yield* service.register({
echo: Tool.make({