From 518d64d684a12da4acb54cf03a000ce116076eb5 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 17 Jul 2026 00:23:48 -0500 Subject: [PATCH] feat(plugin): add session request hook --- packages/core/test/plugin-hooks.test.ts | 33 ++++++++++++++++++++++- packages/core/test/plugin/promise.test.ts | 31 +++++++++++++++++++++ packages/plugin/src/v2/effect/session.ts | 4 +++ packages/plugin/src/v2/promise/session.ts | 4 +++ packages/plugin/src/v2/session.ts | 21 +++++++++++++++ 5 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 packages/plugin/src/v2/session.ts diff --git a/packages/core/test/plugin-hooks.test.ts b/packages/core/test/plugin-hooks.test.ts index 4fdf9a9c54..690015d580 100644 --- a/packages/core/test/plugin-hooks.test.ts +++ b/packages/core/test/plugin-hooks.test.ts @@ -4,6 +4,7 @@ import { Agent } from "@opencode-ai/schema/agent" import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" import { Session } from "@opencode-ai/schema/session" +import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session" import { Effect, Layer } from "effect" import { PluginHooks } from "../src/plugin/hooks" import { testEffect } from "./lib/effect" @@ -28,7 +29,7 @@ describe("PluginHooks", () => { event.messages = [Message.user("changed")] }), ) - const event = { + const event: SessionHooks["context"] = { sessionID: Session.ID.make("ses_hooks"), agent: Agent.ID.make("build"), model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }), @@ -42,4 +43,34 @@ describe("PluginHooks", () => { expect(event.messages).toEqual([Message.user("changed")]) }), ) + + it.effect("registers session request hooks and triggers them sequentially", () => + Effect.gen(function* () { + const hooks = yield* PluginHooks.Service + yield* hooks.register("session", "request", (event) => + Effect.sync(() => { + event.headers.authorization = "Bearer changed" + delete event.body.max_output_tokens + event.body.store = false + }), + ) + yield* hooks.register("session", "request", (event) => + Effect.sync(() => { + event.headers = { ...event.headers, "x-store": String(event.body.store) } + event.body = { input: event.body.input ?? [] } + }), + ) + const event: SessionHooks["request"] = { + sessionID: Session.ID.make("ses_request_hooks"), + agent: Agent.ID.make("build"), + model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }), + headers: { authorization: "Bearer original" }, + body: { input: ["hello"], max_output_tokens: 1024 }, + } + + expect(yield* hooks.trigger("session", "request", event)).toBe(event) + expect(event.headers).toEqual({ authorization: "Bearer changed", "x-store": "false" }) + expect(event.body).toEqual({ input: ["hello"] }) + }), + ) }) diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 7922d965bb..c0f7367699 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -218,6 +218,37 @@ describe("fromPromise", () => { }), ) + it.effect("forwards session request 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-session-request", + setup: async (ctx) => { + await ctx.session.hook("request", (event) => { + event.headers["x-plugin"] = "promise" + delete event.body.max_output_tokens + }) + }, + }), + ).effect(host) + const event: SessionHooks["request"] = { + sessionID: SessionV2.ID.make("ses_promise_session_request"), + agent: AgentV2.ID.make("build"), + model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }), + headers: {}, + body: { max_output_tokens: 1024 }, + } + + yield* hooks.trigger("session", "request", event) + + expect(event.headers).toEqual({ "x-plugin": "promise" }) + expect(event.body).toEqual({}) + }), + ) + it.effect("disposes a hook registration on request", () => Effect.gen(function* () { const agents = yield* AgentV2.Service diff --git a/packages/plugin/src/v2/effect/session.ts b/packages/plugin/src/v2/effect/session.ts index 73b87fb3d1..4262705d71 100644 --- a/packages/plugin/src/v2/effect/session.ts +++ b/packages/plugin/src/v2/effect/session.ts @@ -4,8 +4,11 @@ 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 { SessionRequest } from "../session.js" import type { Hooks } from "./registration.js" +export type { RequestBody, RequestValue, SessionRequest } from "../session.js" + export interface SessionContext { readonly sessionID: Session.ID readonly agent: Agent.ID @@ -17,6 +20,7 @@ export interface SessionContext { export interface SessionHooks { readonly context: SessionContext + readonly request: SessionRequest } export type SessionDomain = Pick< diff --git a/packages/plugin/src/v2/promise/session.ts b/packages/plugin/src/v2/promise/session.ts index 99346de3ea..aef0b3778c 100644 --- a/packages/plugin/src/v2/promise/session.ts +++ b/packages/plugin/src/v2/promise/session.ts @@ -4,8 +4,11 @@ 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 { SessionRequest } from "../session.js" import type { Hooks } from "./registration.js" +export type { RequestBody, RequestValue, SessionRequest } from "../session.js" + export interface SessionContext { readonly sessionID: Session.ID readonly agent: Agent.ID @@ -17,6 +20,7 @@ export interface SessionContext { export interface SessionHooks { readonly context: SessionContext + readonly request: SessionRequest } export type SessionDomain = Pick< diff --git a/packages/plugin/src/v2/session.ts b/packages/plugin/src/v2/session.ts new file mode 100644 index 0000000000..29873e8a32 --- /dev/null +++ b/packages/plugin/src/v2/session.ts @@ -0,0 +1,21 @@ +import type { Agent } from "@opencode-ai/schema/agent" +import type { Model } from "@opencode-ai/schema/model" +import type { Session } from "@opencode-ai/schema/session" + +export type RequestValue = + | null + | boolean + | number + | string + | Array + | { [key: string]: RequestValue } + +export type RequestBody = Record + +export interface SessionRequest { + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly model: Model.Ref + headers: Record + body: RequestBody +}