refactor(plugin): group HTTP under session hooks

This commit is contained in:
Aiden Cline 2026-08-03 23:11:59 -05:00
parent 43a1c74dd0
commit 4b19e9ce27
10 changed files with 66 additions and 25 deletions

View file

@ -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,

View file

@ -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(

View file

@ -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")

View file

@ -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")),

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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<void>]
| [name: "http", middleware: SessionHttpMiddleware]
export interface SessionHook {
(name: "context", callback: (event: SessionContext) => Effect.Effect<void>): Effect.Effect<
Registration,
never,
Scope.Scope
>
(name: "http", middleware: SessionHttpMiddleware): Effect.Effect<Registration, never, Scope.Scope>
}
export type SessionDomain = Pick<
SessionApi<unknown>,
"create" | "get" | "prompt" | "generate" | "command" | "synthetic" | "interrupt" | "rename" | "wait"
> & {
readonly hook: Hooks<SessionHooks>
readonly http: (middleware: SessionHttpMiddleware) => Effect.Effect<Registration, never, Scope.Scope>
readonly hook: SessionHook
}

View file

@ -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> | void]
| [name: "http", middleware: SessionHttpMiddleware]
export interface SessionHook {
(name: "context", callback: (event: SessionContext) => Promise<void> | void): Promise<Registration>
(name: "http", middleware: SessionHttpMiddleware): Promise<Registration>
}
export type SessionDomain = Pick<
SessionApi,
"create" | "get" | "prompt" | "generate" | "command" | "synthetic" | "interrupt"
> & {
readonly hook: Hooks<SessionHooks>
readonly http: (middleware: SessionHttpMiddleware) => Promise<Registration>
readonly hook: SessionHook
}

View file

@ -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