Procházet zdrojové kódy

refactor(plugin): scope context hook to session (#37175)

Co-authored-by: Dax Raad <d@ironbay.co>
opencode-agent[bot] před 1 měsícem
rodič
revize
f92d84746b

+ 2 - 2
packages/core/src/plugin/hooks.ts

@@ -1,15 +1,15 @@
 export * as PluginHooks from "./hooks"
 
-import type { AIHooks } from "@opencode-ai/plugin/v2/effect/ai"
 import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
+import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
 import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
 import { Context, Effect, Layer, Scope } from "effect"
 import { makeLocationNode } from "../effect/app-node"
 import { State } from "../state"
 
 export interface Domains {
-  readonly ai: AIHooks
   readonly aisdk: AISDKHooks
+  readonly session: SessionHooks
   readonly tool: ToolHooks
 }
 

+ 1 - 3
packages/core/src/plugin/host.ts

@@ -80,9 +80,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
           })
         }),
     },
-    ai: {
-      hook: (name, callback) => hooks.register("ai", name, callback),
-    },
     aisdk: {
       hook: (name, callback) => {
         if (name === "sdk") {
@@ -370,6 +367,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
       },
     },
     session: {
+      hook: (name, callback) => hooks.register("session", name, callback),
       create: (input) =>
         runtime.session.create({
           id: input?.id,

+ 2 - 4
packages/core/src/plugin/promise.ts

@@ -63,10 +63,6 @@ export function fromPromise(plugin: Plugin) {
             transform: transform(host.agent),
             reload: () => run(host.agent.reload()),
           },
-          ai: {
-            hook: (name, callback) =>
-              register(host.ai.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
-          },
           aisdk: {
             hook: (name, callback) =>
               register(host.aisdk.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
@@ -166,6 +162,8 @@ export function fromPromise(plugin: Plugin) {
               register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
           },
           session: {
+            hook: (name, callback) =>
+              register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
             create: (input) =>
               run(
                 host.session.create(

+ 6 - 6
packages/core/src/session/runner/llm.ts

@@ -10,7 +10,7 @@ import {
   isContextOverflowFailure,
   type ProviderErrorEvent,
 } from "@opencode-ai/ai"
-import type { AIHooks } from "@opencode-ai/plugin/v2/effect/ai"
+import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
 import { SessionError } from "@opencode-ai/schema/session-error"
 import { Money } from "@opencode-ai/schema/money"
 import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
@@ -216,7 +216,7 @@ const layer = Layer.effect(
         toolChoice: isLastStep ? "none" : undefined,
       })
       const availableTools = new Map(request.tools.map((tool) => [tool.name, tool]))
-      const requestEvent: AIHooks["request"] = {
+      const contextEvent: SessionHooks["context"] = {
         sessionID: session.id,
         agent: agent.id,
         model: resolved.ref,
@@ -228,11 +228,11 @@ const layer = Layer.effect(
       }
       // Plugins may reshape the draft but cannot advertise tools excluded by
       // permissions, registration state, or the selected agent's step limit.
-      yield* hooks.trigger("ai", "request", requestEvent)
+      yield* hooks.trigger("session", "context", contextEvent)
       const hookedRequest = LLM.updateRequest(request, {
-        system: requestEvent.system,
-        messages: requestEvent.messages,
-        tools: Object.entries(requestEvent.tools).flatMap(([name, tool]) => {
+        system: contextEvent.system,
+        messages: contextEvent.messages,
+        tools: Object.entries(contextEvent.tools).flatMap(([name, tool]) => {
           const registered = availableTools.get(name)
           if (!registered) return []
           return [{ ...registered, description: tool.description, inputSchema: tool.input }]

+ 1 - 1
packages/core/src/tool/patch.ts

@@ -196,7 +196,7 @@ export const Plugin = {
       )
       .pipe(Effect.orDie)
 
-    yield* ctx.ai.hook("request", (event) =>
+    yield* ctx.session.hook("context", (event) =>
       Effect.sync(() => {
         const usePatch =
           event.model.providerID.toLowerCase() === "openai" || event.model.id.toLowerCase().includes("gpt")

+ 1 - 1
packages/core/src/tool/subagent.ts

@@ -216,7 +216,7 @@ export const Plugin = {
       )
       .pipe(Effect.orDie)
 
-    yield* ctx.ai.hook("request", (event) =>
+    yield* ctx.session.hook("context", (event) =>
       Effect.gen(function* () {
         const tool = event.tools[name]
         if (!tool) return

+ 1 - 1
packages/core/test/lib/tool.ts

@@ -44,7 +44,7 @@ export const registerToolPlugin = <R>(plugin: {
   Effect.gen(function* () {
     const tools = yield* Tools.Service
     const context = host({
-      ai: {
+      session: {
         hook: () => Effect.succeed({ dispose: Effect.void }),
       },
       tool: {

+ 4 - 4
packages/core/test/plugin-hooks.test.ts

@@ -12,17 +12,17 @@ const layer = PluginHooks.node.implementation as Layer.Layer<PluginHooks.Service
 const it = testEffect(layer)
 
 describe("PluginHooks", () => {
-  it.effect("registers scoped AI hooks and triggers them sequentially", () =>
+  it.effect("registers scoped session hooks and triggers them sequentially", () =>
     Effect.gen(function* () {
       const hooks = yield* PluginHooks.Service
       const seen: string[] = []
-      yield* hooks.register("ai", "request", (event) =>
+      yield* hooks.register("session", "context", (event) =>
         Effect.sync(() => {
           seen.push("first")
           event.system.push(SystemPart.make("second"))
         }),
       )
-      yield* hooks.register("ai", "request", (event) =>
+      yield* hooks.register("session", "context", (event) =>
         Effect.sync(() => {
           seen.push(event.system[1]?.text ?? "missing")
           event.messages = [Message.user("changed")]
@@ -37,7 +37,7 @@ describe("PluginHooks", () => {
         tools: {},
       }
 
-      expect(yield* hooks.trigger("ai", "request", event)).toBe(event)
+      expect(yield* hooks.trigger("session", "context", event)).toBe(event)
       expect(seen).toEqual(["first", "second"])
       expect(event.messages).toEqual([Message.user("changed")])
     }),

+ 10 - 10
packages/core/test/plugin/host.ts

@@ -8,7 +8,9 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
 import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
 import { Effect, Stream } from "effect"
 
-type Overrides = Partial<Omit<PluginContext, "options">>
+type Overrides = Partial<Omit<PluginContext, "options" | "session">> & {
+  readonly session?: Partial<PluginContext["session"]>
+}
 
 export function host(overrides: Overrides = {}): PluginContext {
   return {
@@ -18,9 +20,6 @@ export function host(overrides: Overrides = {}): PluginContext {
       transform: () => Effect.die("unused agent.transform"),
       reload: () => Effect.die("unused agent.reload"),
     },
-    ai: overrides.ai ?? {
-      hook: () => Effect.die("unused ai.hook"),
-    },
     aisdk: overrides.aisdk ?? {
       hook: () => Effect.die("unused aisdk.hook"),
     },
@@ -80,12 +79,13 @@ export function host(overrides: Overrides = {}): PluginContext {
       transform: () => Effect.die("unused tool.transform"),
       hook: () => Effect.die("unused tool.hook"),
     },
-    session: overrides.session ?? {
-      create: () => Effect.die("unused session.create"),
-      get: () => Effect.die("unused session.get"),
-      prompt: () => Effect.die("unused session.prompt"),
-      command: () => Effect.die("unused session.command"),
-      interrupt: () => Effect.die("unused session.interrupt"),
+    session: {
+      hook: overrides.session?.hook ?? (() => Effect.die("unused session.hook")),
+      create: overrides.session?.create ?? (() => Effect.die("unused session.create")),
+      get: overrides.session?.get ?? (() => Effect.die("unused session.get")),
+      prompt: overrides.session?.prompt ?? (() => Effect.die("unused session.prompt")),
+      command: overrides.session?.command ?? (() => Effect.die("unused session.command")),
+      interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")),
     },
   }
 }

+ 7 - 7
packages/core/test/plugin/promise.test.ts

@@ -10,7 +10,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
 import { SessionMessage } from "@opencode-ai/core/session/message"
 import { ToolRegistry } from "@opencode-ai/core/tool/registry"
 import { Plugin } from "@opencode-ai/plugin/v2"
-import type { AIHooks } from "@opencode-ai/plugin/v2/effect/ai"
+import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
 import { Model } from "@opencode-ai/schema/model"
 import { Provider } from "@opencode-ai/schema/provider"
 import { testEffect } from "../lib/effect"
@@ -77,24 +77,24 @@ describe("fromPromise", () => {
     }),
   )
 
-  it.effect("forwards AI request hooks", () =>
+  it.effect("forwards session context hooks", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const hooks = yield* PluginHooks.Service
       const host = yield* PluginHost.make(plugin)
       yield* PluginPromise.fromPromise(
         Plugin.define({
-          id: "promise-ai-request",
+          id: "promise-session-context",
           setup: async (ctx) => {
-            await ctx.ai.hook("request", (event) => {
+            await ctx.session.hook("context", (event) => {
               event.system.push(SystemPart.make("Promise hook"))
               delete event.tools.echo
             })
           },
         }),
       ).effect(host)
-      const event: AIHooks["request"] = {
-        sessionID: SessionV2.ID.make("ses_promise_ai_request"),
+      const event: SessionHooks["context"] = {
+        sessionID: SessionV2.ID.make("ses_promise_session_context"),
         agent: AgentV2.ID.make("build"),
         model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
         system: [SystemPart.make("Initial")],
@@ -102,7 +102,7 @@ describe("fromPromise", () => {
         tools: { echo: { description: "Echo", input: { type: "object" } } },
       }
 
-      yield* hooks.trigger("ai", "request", event)
+      yield* hooks.trigger("session", "context", event)
 
       expect(event.system.map((part) => part.text)).toEqual(["Initial", "Promise hook"])
       expect(event.tools).toEqual({})

+ 2 - 2
packages/core/test/session-runner.test.ts

@@ -777,11 +777,11 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
   })
 
 describe("SessionRunnerLLM", () => {
-  it.effect("applies AI request hooks without exposing unavailable tools", () =>
+  it.effect("applies session context hooks without exposing unavailable tools", () =>
     Effect.gen(function* () {
       const session = yield* setup
       const hooks = yield* PluginHooks.Service
-      yield* hooks.register("ai", "request", (event) =>
+      yield* hooks.register("session", "context", (event) =>
         Effect.sync(() => {
           event.system = [SystemPart.make("Hooked system")]
           event.messages = [Message.user("Hooked message")]

+ 2 - 2
packages/plugin/src/v2/effect/README.md

@@ -80,11 +80,11 @@ yield *
 
 Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks.
 
-AI request context is mutable immediately before provider dispatch:
+Session context is mutable immediately before provider dispatch:
 
 ```ts
 yield *
-  ctx.ai.hook("request", (event) =>
+  ctx.session.hook("context", (event) =>
     Effect.sync(() => {
       event.tools.read.description = "Read a file using narrow line ranges."
       delete event.tools.write

+ 0 - 23
packages/plugin/src/v2/effect/ai.ts

@@ -1,23 +0,0 @@
-import type { Message, SystemPart } from "@opencode-ai/ai"
-import type { Agent } from "@opencode-ai/schema/agent"
-import type { Model } from "@opencode-ai/schema/model"
-import type { Session } from "@opencode-ai/schema/session"
-import type { JsonSchema } from "effect"
-import type { Hooks } from "./registration.js"
-
-export interface AIRequest {
-  readonly sessionID: Session.ID
-  readonly agent: Agent.ID
-  readonly model: Model.Ref
-  system: Array<SystemPart>
-  messages: Array<Message>
-  tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
-}
-
-export interface AIHooks {
-  readonly request: AIRequest
-}
-
-export interface AIDomain {
-  readonly hook: Hooks<AIHooks>
-}

+ 0 - 2
packages/plugin/src/v2/effect/plugin.ts

@@ -2,7 +2,6 @@ import type { PluginApi } from "@opencode-ai/client/effect/api"
 import type { Effect, Scope } from "effect"
 import type { PluginOptions } from "../options.js"
 import type { AgentDomain } from "./agent.js"
-import type { AIDomain } from "./ai.js"
 import type { AISDKDomain } from "./aisdk.js"
 import type { CatalogDomain } from "./catalog.js"
 import type { CommandDomain } from "./command.js"
@@ -16,7 +15,6 @@ import type { ToolDomain } from "./tool.js"
 export interface Context {
   readonly options: PluginOptions
   readonly agent: AgentDomain
-  readonly ai: AIDomain
   readonly aisdk: AISDKDomain
   readonly catalog: CatalogDomain
   readonly command: CommandDomain

+ 22 - 1
packages/plugin/src/v2/effect/session.ts

@@ -1,3 +1,24 @@
 import type { SessionApi } from "@opencode-ai/client/effect/api"
+import type { Message, SystemPart } from "@opencode-ai/ai"
+import type { Agent } from "@opencode-ai/schema/agent"
+import type { Model } from "@opencode-ai/schema/model"
+import type { Session } from "@opencode-ai/schema/session"
+import type { JsonSchema } from "effect"
+import type { Hooks } from "./registration.js"
 
-export type SessionDomain = Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "command" | "interrupt">
+export interface SessionContext {
+  readonly sessionID: Session.ID
+  readonly agent: Agent.ID
+  readonly model: Model.Ref
+  system: Array<SystemPart>
+  messages: Array<Message>
+  tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
+}
+
+export interface SessionHooks {
+  readonly context: SessionContext
+}
+
+export type SessionDomain = Pick<SessionApi<unknown>, "create" | "get" | "prompt" | "command" | "interrupt"> & {
+  readonly hook: Hooks<SessionHooks>
+}

+ 2 - 2
packages/plugin/src/v2/promise/README.md

@@ -85,10 +85,10 @@ await ctx.aisdk.hook("language", (event) => {
 })
 ```
 
-AI request context is mutable immediately before provider dispatch:
+Session context is mutable immediately before provider dispatch:
 
 ```ts
-await ctx.ai.hook("request", (event) => {
+await ctx.session.hook("context", (event) => {
   event.tools.read.description = "Read a file using narrow line ranges."
   delete event.tools.write
 })

+ 0 - 23
packages/plugin/src/v2/promise/ai.ts

@@ -1,23 +0,0 @@
-import type { Message, SystemPart } from "@opencode-ai/ai"
-import type { Agent } from "@opencode-ai/schema/agent"
-import type { Model } from "@opencode-ai/schema/model"
-import type { Session } from "@opencode-ai/schema/session"
-import type { JsonSchema } from "effect"
-import type { Hooks } from "./registration.js"
-
-export interface AIRequest {
-  readonly sessionID: Session.ID
-  readonly agent: Agent.ID
-  readonly model: Model.Ref
-  system: Array<SystemPart>
-  messages: Array<Message>
-  tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
-}
-
-export interface AIHooks {
-  readonly request: AIRequest
-}
-
-export interface AIDomain {
-  readonly hook: Hooks<AIHooks>
-}

+ 0 - 2
packages/plugin/src/v2/promise/plugin.ts

@@ -1,7 +1,6 @@
 import type { PluginApi } from "@opencode-ai/client/promise/api"
 import type { PluginOptions } from "../options.js"
 import type { AgentDomain } from "./agent.js"
-import type { AIDomain } from "./ai.js"
 import type { AISDKDomain } from "./aisdk.js"
 import type { CatalogDomain } from "./catalog.js"
 import type { CommandDomain } from "./command.js"
@@ -15,7 +14,6 @@ import type { ToolDomain } from "./tool.js"
 export interface Context {
   readonly options: PluginOptions
   readonly agent: AgentDomain
-  readonly ai: AIDomain
   readonly aisdk: AISDKDomain
   readonly catalog: CatalogDomain
   readonly command: CommandDomain

+ 22 - 1
packages/plugin/src/v2/promise/session.ts

@@ -1,3 +1,24 @@
 import type { SessionApi } from "@opencode-ai/client/promise/api"
+import type { Message, SystemPart } from "@opencode-ai/ai"
+import type { Agent } from "@opencode-ai/schema/agent"
+import type { Model } from "@opencode-ai/schema/model"
+import type { Session } from "@opencode-ai/schema/session"
+import type { JsonSchema } from "effect"
+import type { Hooks } from "./registration.js"
 
-export type SessionDomain = Pick<SessionApi, "create" | "get" | "prompt" | "command" | "interrupt">
+export interface SessionContext {
+  readonly sessionID: Session.ID
+  readonly agent: Agent.ID
+  readonly model: Model.Ref
+  system: Array<SystemPart>
+  messages: Array<Message>
+  tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
+}
+
+export interface SessionHooks {
+  readonly context: SessionContext
+}
+
+export type SessionDomain = Pick<SessionApi, "create" | "get" | "prompt" | "command" | "interrupt"> & {
+  readonly hook: Hooks<SessionHooks>
+}