diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 17eda7b4645..407695defe1 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -2,6 +2,7 @@ export * as PluginHost from "./host" import { Plugin } from "@opencode-ai/plugin/effect" import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import type { SessionHookRegistration } from "@opencode-ai/plugin/effect/session" import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types" import { EventManifest } from "@opencode-ai/schema/event-manifest" import { App } from "../app" @@ -337,14 +338,16 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../p }), }, session: { - hook: (name, callback) => hooks.register("session", name, callback), - http: (middleware) => - hooks.register("session", "http", (event) => + hook: (...registration: SessionHookRegistration) => { + if (registration[0] === "context") return hooks.register("session", "context", registration[1]) + const middleware = registration[1] + return hooks.register("session", "http", (event) => Effect.sync(() => { const next = event.request event.request = (request) => middleware(event, request, next) }), - ), + ) + }, create: (input) => runtime.session.create({ id: input?.id, diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index 97a8d871135..6bbe4f1805d 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -2,6 +2,7 @@ export * as PluginPromise from "./promise" import { define } from "@opencode-ai/plugin/effect/plugin" import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin" +import type { SessionHookRegistration } from "@opencode-ai/plugin/promise/session" import type { Info } from "@opencode-ai/plugin/promise/tool" import { Agent } from "@opencode-ai/schema/agent" import { Integration } from "@opencode-ai/schema/integration" @@ -265,11 +266,16 @@ export function fromPromise(plugin: Plugin) { ), }, session: { - hook: (name, callback) => - register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))), - http: (middleware) => - register( - host.session.http((event, input, next) => + hook: (...registration: SessionHookRegistration) => { + if (registration[0] === "context") + return register( + host.session.hook("context", (event) => + Effect.promise(() => Promise.resolve(registration[1](event))), + ), + ) + const middleware = registration[1] + return register( + host.session.hook("http", (event, input, next) => Effect.tryPromise({ try: (signal) => Promise.resolve( @@ -280,7 +286,8 @@ export function fromPromise(plugin: Plugin) { catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))), }), ), - ), + ) + }, create: (input) => run( host.session.create( diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index 50f21581021..9b0c0392783 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -225,7 +225,7 @@ export const OpenAIPlugin = define({ }) } }) - yield* ctx.session.http((evt, request, next) => { + yield* ctx.session.hook("http", (evt, request, next) => { if (!chatgpt || evt.model.providerID !== Provider.ID.openai) return next(request) const url = new URL(request.url) request.headers.set("originator", "opencode") diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 1b1b1b82a1f..f45364ebdfe 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -101,7 +101,6 @@ export function host(overrides: Overrides = {}): Plugin.Context { }, session: { hook: overrides.session?.hook ?? (() => Effect.die("unused session.hook")), - http: overrides.session?.http ?? (() => Effect.die("unused session.http")), 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")), diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 2e1e6a27783..e1120f2854d 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -232,12 +232,12 @@ describe("fromPromise", () => { define({ id: "promise-session-http", setup: async (ctx) => { - await ctx.session.http(async (_event, request, next) => { + await ctx.session.hook("http", async (_event, request, next) => { request.headers.set("x-hook", "promise") const response = await next(request) return new Response(`${await response.text()}-response`) }) - await ctx.session.http(async (_event, request, next) => { + await ctx.session.hook("http", async (_event, request, next) => { const response = await next(request) return new Response(`${await response.text()}-outer`) }) @@ -267,7 +267,7 @@ describe("fromPromise", () => { define({ id: "promise-session-http-interrupt", setup: async (ctx) => { - await ctx.session.http((_event, request, next) => next(request)) + await ctx.session.hook("http", (_event, request, next) => next(request)) }, }), ).effect(host) diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index 6fd9ef67dad..ad735569ccb 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -1,4 +1,5 @@ import { HttpRecorder } from "@opencode-ai/http-recorder" +import type { SessionHookRegistration } from "@opencode-ai/plugin/effect/session" import * as OpenAIChat from "@opencode-ai/ai/protocols/openai-chat" import { Auth, LLMClient, RequestExecutor } from "@opencode-ai/ai/route" import { Catalog } from "@opencode-ai/core/catalog" @@ -189,7 +190,12 @@ describe("SessionRunnerLLM recorded", () => { const pluginHost = host({ agent: agentHost(agents), catalog: catalogHost(catalog), - session: { hook: (name, callback) => hooks.register("session", name, callback) }, + session: { + hook: (...registration: SessionHookRegistration) => { + if (registration[0] === "http") return Effect.die("unused session HTTP hook") + return hooks.register("session", "context", registration[1]) + }, + }, }) yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), { discard: true }) const { db } = yield* Database.Service diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index f7bca091004..76f820517ec 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -14,6 +14,7 @@ import { } from "@opencode-ai/ai" import * as OpenAIChat from "@opencode-ai/ai/protocols/openai-chat" import { TestLLM } from "@opencode-ai/ai/testing" +import type { SessionHookRegistration } from "@opencode-ai/plugin/effect/session" import { Catalog } from "@opencode-ai/core/catalog" import { Database } from "@opencode-ai/core/database/database" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -474,7 +475,12 @@ const setup = Effect.gen(function* () { const pluginHost = host({ agent: agentHost(agents), catalog: catalogHost(catalog), - session: { hook: (name, callback) => hooks.register("session", name, callback) }, + session: { + hook: (...registration: SessionHookRegistration) => { + if (registration[0] === "http") return Effect.die("unused session HTTP hook") + return hooks.register("session", "context", registration[1]) + }, + }, }) yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), { discard: true, diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index 407ee33e5e9..ad25d27cc5f 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -4,7 +4,7 @@ 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 { Effect, JsonSchema, Scope } from "effect" -import type { Hooks, Registration } from "./registration.js" +import type { Registration } from "./registration.js" export interface SessionContext { readonly sessionID: Session.ID @@ -31,10 +31,22 @@ export interface SessionHooks { readonly context: SessionContext } +export type SessionHookRegistration = + | [name: "context", callback: (event: SessionContext) => Effect.Effect] + | [name: "http", middleware: SessionHttpMiddleware] + +export interface SessionHook { + (name: "context", callback: (event: SessionContext) => Effect.Effect): Effect.Effect< + Registration, + never, + Scope.Scope + > + (name: "http", middleware: SessionHttpMiddleware): Effect.Effect +} + export type SessionDomain = Pick< SessionApi, "create" | "get" | "prompt" | "generate" | "command" | "synthetic" | "interrupt" | "rename" | "wait" > & { - readonly hook: Hooks - readonly http: (middleware: SessionHttpMiddleware) => Effect.Effect + readonly hook: SessionHook } diff --git a/packages/plugin/src/promise/session.ts b/packages/plugin/src/promise/session.ts index d5587274129..5f074106d66 100644 --- a/packages/plugin/src/promise/session.ts +++ b/packages/plugin/src/promise/session.ts @@ -4,7 +4,7 @@ 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, Registration } from "./registration.js" +import type { Registration } from "./registration.js" export interface SessionContext { readonly sessionID: Session.ID @@ -31,10 +31,18 @@ export interface SessionHooks { readonly context: SessionContext } +export type SessionHookRegistration = + | [name: "context", callback: (event: SessionContext) => Promise | void] + | [name: "http", middleware: SessionHttpMiddleware] + +export interface SessionHook { + (name: "context", callback: (event: SessionContext) => Promise | void): Promise + (name: "http", middleware: SessionHttpMiddleware): Promise +} + export type SessionDomain = Pick< SessionApi, "create" | "get" | "prompt" | "generate" | "command" | "synthetic" | "interrupt" > & { - readonly hook: Hooks - readonly http: (middleware: SessionHttpMiddleware) => Promise + readonly hook: SessionHook } diff --git a/packages/www/content/docs/build/plugins.mdx b/packages/www/content/docs/build/plugins.mdx index 5e6f2430e74..9ad2c24f2a9 100644 --- a/packages/www/content/docs/build/plugins.mdx +++ b/packages/www/content/docs/build/plugins.mdx @@ -246,7 +246,7 @@ Runtime hooks intercept live operations: | `ctx.aisdk.hook("sdk", callback)` | `sdk`, after inspecting `model`, `package`, and `options` | | `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` | | `ctx.session.hook("context", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch | -| `ctx.session.http(middleware)` | The model's HTTP request and response | +| `ctx.session.hook("http", middleware)` | The model's HTTP request and response | | `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes | | `ctx.tool.hook("execute.after", callback)` | Terminal `result` on success or `error` on failure | @@ -254,7 +254,7 @@ HTTP middleware can modify requests, inspect responses, retry, or return a response without calling the provider: ```ts -await ctx.session.http(async (event, request, next) => { +await ctx.session.hook("http", async (event, request, next) => { request.headers.set("x-session-id", event.sessionID) const response = await next(request) return response