mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-01 03:33:18 +00:00
fix(core): shape compaction and generate requests with built-in context hooks (#48749)
This commit is contained in:
parent
c5aa7d7e34
commit
c43cfccc4e
7 changed files with 75 additions and 17 deletions
|
|
@ -2,6 +2,7 @@ export * as OpenCodeTools from "./opencode.js"
|
|||
|
||||
import { SystemPart, ToolFailure } from "@opencode/ai"
|
||||
import type { Context } from "@opencode/plugin/effect/plugin"
|
||||
import type { SessionHooks } from "@opencode/plugin/effect/session"
|
||||
import { AbsolutePath } from "@opencode/schema/schema"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
|
@ -25,15 +26,17 @@ const MoveOutput = Schema.Struct({ sessionID: Session.ID, directory: AbsolutePat
|
|||
export const Plugin = {
|
||||
id: "opencode.tools",
|
||||
effect: Effect.fn("OpenCodeTools.Plugin")(function* (ctx: Context) {
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
const hook = (event: SessionHooks["context"]) =>
|
||||
Effect.sync(() => {
|
||||
event.system.push(
|
||||
SystemPart.make(
|
||||
"When you create a worktree outside the current working directory and intend to use it as your primary working directory, consider using `execute` to call `tools.opencode.session_move` and make the worktree the session's working directory.",
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
yield* ctx.session.hook("context", hook)
|
||||
yield* ctx.session.hook("compaction", hook)
|
||||
yield* ctx.session.hook("generate", hook)
|
||||
yield* ctx.tool
|
||||
.transform((draft) => {
|
||||
draft.namespace({ name: "opencode", description: "OpenCode session and runtime tools." })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export * as PatchTool from "./patch.js"
|
||||
|
||||
import type { Context } from "@opencode/plugin/effect/plugin"
|
||||
import type { SessionHooks } from "@opencode/plugin/effect/session"
|
||||
import { ToolFailure } from "@opencode/ai"
|
||||
import { FileDiff } from "@opencode/schema/file-diff"
|
||||
import { Effect, Result, Schema } from "effect"
|
||||
|
|
@ -292,7 +293,7 @@ export const Plugin = {
|
|||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
const hook = (event: SessionHooks["context"]) =>
|
||||
Effect.sync(() => {
|
||||
const usePatch =
|
||||
event.model.id.includes("gpt-") && !event.model.id.includes("oss") && !event.model.id.includes("gpt-4")
|
||||
|
|
@ -302,8 +303,10 @@ export const Plugin = {
|
|||
return
|
||||
}
|
||||
delete event.tools.patch
|
||||
}),
|
||||
)
|
||||
})
|
||||
yield* ctx.session.hook("context", hook)
|
||||
yield* ctx.session.hook("compaction", hook)
|
||||
yield* ctx.session.hook("generate", hook)
|
||||
}),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export * as ShellTool from "./shell.js"
|
|||
|
||||
import { ToolFailure } from "@opencode/ai"
|
||||
import type { Context } from "@opencode/plugin/effect/plugin"
|
||||
import type { SessionHooks } from "@opencode/plugin/effect/session"
|
||||
import type { ShellCreateBefore } from "@opencode/plugin/effect/shell"
|
||||
import type { Tool } from "@opencode/schema/tool"
|
||||
import { Deferred, Effect, Schema, Scope } from "effect"
|
||||
|
|
@ -271,12 +272,14 @@ export const Plugin = {
|
|||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
const hook = (event: SessionHooks["context"]) =>
|
||||
Effect.gen(function* () {
|
||||
const tool = event.tools[name]
|
||||
if (!tool) return
|
||||
tool.description = description(ShellSelect.name(yield* compatibleShell))
|
||||
}),
|
||||
)
|
||||
})
|
||||
yield* ctx.session.hook("context", hook)
|
||||
yield* ctx.session.hook("compaction", hook)
|
||||
yield* ctx.session.hook("generate", hook)
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export * as SubagentTool from "./subagent.js"
|
|||
|
||||
import { ToolFailure } from "@opencode/ai"
|
||||
import type { Context } from "@opencode/plugin/effect/plugin"
|
||||
import type { SessionHooks } from "@opencode/plugin/effect/session"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Agent } from "../../agent.js"
|
||||
import { Config } from "../../config.js"
|
||||
|
|
@ -234,7 +235,7 @@ export const Plugin = {
|
|||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
const hook = (event: SessionHooks["context"]) =>
|
||||
Effect.gen(function* () {
|
||||
const tool = event.tools[name]
|
||||
if (!tool) return
|
||||
|
|
@ -258,7 +259,9 @@ export const Plugin = {
|
|||
`- ${agent.id}: ${agent.description ?? "This subagent should only be called when explicitly requested."}`,
|
||||
),
|
||||
].join("\n")
|
||||
}),
|
||||
)
|
||||
})
|
||||
yield* ctx.session.hook("context", hook)
|
||||
yield* ctx.session.hook("compaction", hook)
|
||||
yield* ctx.session.hook("generate", hook)
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export * as WebSearchTool from "./websearch.js"
|
||||
|
||||
import type { Context } from "@opencode/plugin/effect/plugin"
|
||||
import type { SessionHooks } from "@opencode/plugin/effect/session"
|
||||
import { ToolFailure } from "@opencode/ai"
|
||||
import { Effect, Schema, Semaphore } from "effect"
|
||||
import { HttpClientError } from "effect/unstable/http"
|
||||
|
|
@ -180,14 +181,16 @@ export const Plugin = {
|
|||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
const hook = (event: SessionHooks["context"]) =>
|
||||
Effect.gen(function* () {
|
||||
const disabled = yield* websearch.default().pipe(
|
||||
Effect.as(false),
|
||||
Effect.catchTag("WebSearch.Disabled", () => Effect.succeed(true)),
|
||||
)
|
||||
if (disabled) delete event.tools[name]
|
||||
}),
|
||||
)
|
||||
})
|
||||
yield* ctx.session.hook("context", hook)
|
||||
yield* ctx.session.hook("compaction", hook)
|
||||
yield* ctx.session.hook("generate", hook)
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ export const registerToolPlugin = <R>(
|
|||
const context = host({
|
||||
...overrides,
|
||||
session: {
|
||||
hook: () => Effect.succeed({ dispose: Effect.void }),
|
||||
...overrides.session,
|
||||
hook: overrides.session?.hook ?? (() => Effect.succeed({ dispose: Effect.void })),
|
||||
},
|
||||
tool: {
|
||||
transform: tools.transform,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ import { Formatter } from "@opencode/core/formatter"
|
|||
import { FileMutation } from "@opencode/core/file-mutation"
|
||||
import { Location } from "@opencode/core/location"
|
||||
import { FileAccess } from "@opencode/core/file-access"
|
||||
import { Model } from "@opencode/core/model"
|
||||
import { Permission } from "@opencode/core/permission"
|
||||
import { Provider } from "@opencode/core/provider"
|
||||
import { AbsolutePath } from "@opencode/core/schema"
|
||||
import { Session } from "@opencode/core/session"
|
||||
import { Tool } from "@opencode/core/tool"
|
||||
import { PatchTool } from "@opencode/core/tool/plugin/patch"
|
||||
import type { SessionHooks } from "@opencode/plugin/effect/session"
|
||||
import { transformEnvironmentFiles } from "./fixture/environment"
|
||||
import { location } from "./fixture/location"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
|
|
@ -22,9 +25,20 @@ import { testEffect } from "./lib/effect"
|
|||
import { permissionLayer } from "./lib/permission"
|
||||
import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool"
|
||||
|
||||
const sessionHooks = new Map<string, (event: SessionHooks["context"]) => Effect.Effect<void>>()
|
||||
const patchToolNode = makeLocationNode({
|
||||
name: "test/patch-tool-plugin",
|
||||
layer: Layer.effectDiscard(registerToolPlugin(PatchTool.Plugin)),
|
||||
layer: Layer.effectDiscard(
|
||||
registerToolPlugin(PatchTool.Plugin, {
|
||||
session: {
|
||||
hook: (name, callback) =>
|
||||
Effect.sync(() => {
|
||||
sessionHooks.set(name, callback as (event: SessionHooks["context"]) => Effect.Effect<void>)
|
||||
return { dispose: Effect.void }
|
||||
}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
deps: [
|
||||
Tool.node,
|
||||
FileAccess.node,
|
||||
|
|
@ -153,6 +167,34 @@ const withTempTool = <A, E, R>(body: (directory: string, registry: Tool.Interfac
|
|||
)
|
||||
|
||||
describe("PatchTool", () => {
|
||||
it.live("selects the same edit tools for compaction and generate requests as the agent loop", () =>
|
||||
withTempTool(() =>
|
||||
Effect.gen(function* () {
|
||||
const event = (id: string): SessionHooks["context"] => ({
|
||||
sessionID,
|
||||
agent: toolIdentity.agent,
|
||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make(id) }),
|
||||
system: [],
|
||||
messages: [],
|
||||
tools: Object.fromEntries(
|
||||
["patch", "edit", "write", "read"].map((name) => [name, { description: name, input: { type: "object" } }]),
|
||||
),
|
||||
options: {},
|
||||
})
|
||||
for (const name of ["context", "compaction", "generate"]) {
|
||||
const hook = sessionHooks.get(name)
|
||||
expect(hook).toBeDefined()
|
||||
const claude = event("claude-sonnet-4")
|
||||
yield* hook!(claude)
|
||||
expect(Object.keys(claude.tools)).toEqual(["edit", "write", "read"])
|
||||
const gpt = event("gpt-5")
|
||||
yield* hook!(gpt)
|
||||
expect(Object.keys(gpt.tools)).toEqual(["patch", "read"])
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("registers and sequentially applies add, update, and delete hunks", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue