feat(plugin): add session options hook
This commit is contained in:
parent
1f2de535aa
commit
d927f6fa8d
18 changed files with 320 additions and 57 deletions
|
|
@ -218,6 +218,38 @@ describe("fromPromise", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("forwards session option 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-options",
|
||||
setup: async (ctx) => {
|
||||
await ctx.session.hook("options", (event) => {
|
||||
event.generation.temperature = 0.3
|
||||
event.providerOptions.openai ??= {}
|
||||
event.providerOptions.openai.reasoningEffort = "high"
|
||||
})
|
||||
},
|
||||
}),
|
||||
).effect(host)
|
||||
const event: SessionHooks["options"] = {
|
||||
sessionID: SessionV2.ID.make("ses_promise_session_options"),
|
||||
agent: AgentV2.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
||||
generation: { temperature: 0.7 },
|
||||
providerOptions: {},
|
||||
}
|
||||
|
||||
yield* hooks.trigger("session", "options", event)
|
||||
|
||||
expect(event.generation.temperature).toBe(0.3)
|
||||
expect(event.providerOptions).toEqual({ openai: { reasoningEffort: "high" } })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disposes a hook registration on request", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { OpenAIChat } from "@opencode-ai/ai/protocols"
|
||||
import type { LLMClientShape } from "@opencode-ai/ai/route"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
|
|
@ -40,14 +41,16 @@ const projects = Layer.succeed(
|
|||
}),
|
||||
)
|
||||
let requests: LLMRequest[] = []
|
||||
const client = Layer.mock(LLMClient.Service)({
|
||||
const clientValue: LLMClientShape = {
|
||||
prepare: () => Effect.die("unused"),
|
||||
stream: (request: LLMRequest) => {
|
||||
requests.push(request)
|
||||
return Stream.make(LLMEvent.textDelta({ id: "summary", text: "manual session summary" }))
|
||||
},
|
||||
generate: () => Effect.die("unused"),
|
||||
})
|
||||
withOptionsTransform: () => clientValue,
|
||||
}
|
||||
const client = Layer.mock(LLMClient.Service)(clientValue)
|
||||
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
|
||||
const models = SessionRunnerModel.layerWith(() => Effect.succeed(SessionRunnerModel.resolved(model)))
|
||||
const locations = Layer.effect(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { OpenAIChat } from "@opencode-ai/ai/protocols"
|
||||
import type { LLMClientShape } from "@opencode-ai/ai/route"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
|
|
@ -42,7 +43,7 @@ const cost = [
|
|||
},
|
||||
},
|
||||
]
|
||||
const client = Layer.mock(LLMClient.Service)({
|
||||
const clientValue: LLMClientShape = {
|
||||
prepare: () => Effect.die("unused"),
|
||||
stream: (request: LLMRequest) => {
|
||||
requests.push(request)
|
||||
|
|
@ -66,7 +67,9 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
)
|
||||
},
|
||||
generate: () => Effect.die("unused"),
|
||||
})
|
||||
withOptionsTransform: () => clientValue,
|
||||
}
|
||||
const client = Layer.mock(LLMClient.Service)(clientValue)
|
||||
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
|
||||
const models = Layer.mock(SessionRunnerModel.Service)({
|
||||
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model, undefined, cost)),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { expect } from "bun:test"
|
||||
import { LLMClient, LLMEvent, LLMResponse, Model, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { OpenAIChat } from "@opencode-ai/ai/protocols"
|
||||
import type { LLMClientShape } from "@opencode-ai/ai/route"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
|
|
@ -47,7 +48,7 @@ let instruction: string | Instructions.Unavailable = "Initial context"
|
|||
const sessionID = SessionSchema.ID.make("ses_generate_test")
|
||||
|
||||
const model = Model.make({ id: "generate-model", provider: "test", route: OpenAIChat.route })
|
||||
const client = Layer.mock(LLMClient.Service)({
|
||||
const clientValue: LLMClientShape = {
|
||||
prepare: () => Effect.die(new Error("unused")),
|
||||
stream: () => Stream.die(new Error("unused")),
|
||||
generate: (request) =>
|
||||
|
|
@ -64,7 +65,9 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
if (!response) throw new Error("Incomplete generate response")
|
||||
return response
|
||||
}),
|
||||
})
|
||||
withOptionsTransform: () => clientValue,
|
||||
}
|
||||
const client = Layer.mock(LLMClient.Service)(clientValue)
|
||||
const models = SessionRunnerModel.layerWith(() => Effect.succeed(SessionRunnerModel.resolved(model)))
|
||||
const builtins = Layer.mock(InstructionBuiltIns.Service, {
|
||||
load: () =>
|
||||
|
|
|
|||
57
packages/core/test/session-options-hook.test.ts
Normal file
57
packages/core/test/session-options-hook.test.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { expect } from "bun:test"
|
||||
import { LLM, LLMClient, Model as LLMModel } from "@opencode-ai/ai"
|
||||
import { OpenAIChat } from "@opencode-ai/ai/protocols"
|
||||
import type { LLMClientShape, OptionsTransform } from "@opencode-ai/ai/route"
|
||||
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 { Effect, Layer, Stream } from "effect"
|
||||
import { PluginHooks } from "../src/plugin/hooks"
|
||||
import { SessionOptionsHook } from "../src/session/options-hook"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const layer = PluginHooks.node.implementation as Layer.Layer<PluginHooks.Service>
|
||||
const it = testEffect(layer)
|
||||
|
||||
it.effect("applies session option hooks to resolved model options", () =>
|
||||
Effect.gen(function* () {
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const sessionID = Session.ID.make("ses_options")
|
||||
const agent = Agent.ID.make("build")
|
||||
const model = Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") })
|
||||
yield* hooks.register("session", "options", (event) =>
|
||||
Effect.sync(() => {
|
||||
expect(event).toMatchObject({ sessionID, agent, model })
|
||||
event.generation.temperature = 0.2
|
||||
delete event.generation.topP
|
||||
event.providerOptions.openai ??= {}
|
||||
event.providerOptions.openai.reasoningEffort = "high"
|
||||
}),
|
||||
)
|
||||
let transform: OptionsTransform | undefined
|
||||
const llm: LLMClientShape = {
|
||||
prepare: () => Effect.die("unused"),
|
||||
stream: () => Stream.die("unused"),
|
||||
generate: () => Effect.die("unused"),
|
||||
withOptionsTransform: (next) => {
|
||||
transform = next
|
||||
return llm
|
||||
},
|
||||
}
|
||||
|
||||
SessionOptionsHook.client(llm, hooks, { sessionID, agent, model })
|
||||
if (!transform) return yield* Effect.die("options transform was not installed")
|
||||
const result = yield* transform(
|
||||
LLM.request({
|
||||
model: LLMModel.make({ id: "model", provider: "test", route: OpenAIChat.route }),
|
||||
generation: { temperature: 0.7, topP: 0.9 },
|
||||
providerOptions: { openai: { promptCacheKey: "cache" } },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.generation).toMatchObject({ temperature: 0.2 })
|
||||
expect(result.generation?.topP).toBeUndefined()
|
||||
expect(result.providerOptions).toEqual({ openai: { promptCacheKey: "cache", reasoningEffort: "high" } })
|
||||
}),
|
||||
)
|
||||
|
|
@ -90,9 +90,7 @@ let toolExecutionsStarted: Deferred.Deferred<void> | undefined
|
|||
let toolExecutionsReady = 5
|
||||
let activeToolExecutions = 0
|
||||
let maxActiveToolExecutions = 0
|
||||
const client = Layer.succeed(
|
||||
LLMClient.Service,
|
||||
LLMClient.Service.of({
|
||||
const clientValue: LLMClientShape = LLMClient.Service.of({
|
||||
prepare: () => Effect.die("unused"),
|
||||
stream: ((request: LLMRequest) => {
|
||||
requests.push(request)
|
||||
|
|
@ -114,8 +112,9 @@ const client = Layer.succeed(
|
|||
)
|
||||
}) as unknown as LLMClientShape["stream"],
|
||||
generate: () => Effect.die("unused"),
|
||||
}),
|
||||
)
|
||||
withOptionsTransform: () => clientValue,
|
||||
})
|
||||
const client = Layer.succeed(LLMClient.Service, clientValue)
|
||||
const reply = {
|
||||
stop: () => [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { expect } from "bun:test"
|
||||
import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { OpenAIChat } from "@opencode-ai/ai/protocols"
|
||||
import type { LLMClientShape } from "@opencode-ai/ai/route"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
|
|
@ -40,7 +41,7 @@ const cost = [
|
|||
},
|
||||
},
|
||||
]
|
||||
const client = Layer.mock(LLMClient.Service)({
|
||||
const clientValue: LLMClientShape = {
|
||||
prepare: () => Effect.die("unused"),
|
||||
stream: (request: LLMRequest) => {
|
||||
requests.push(request)
|
||||
|
|
@ -64,7 +65,9 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
)
|
||||
},
|
||||
generate: () => Effect.die("unused"),
|
||||
})
|
||||
withOptionsTransform: () => clientValue,
|
||||
}
|
||||
const client = Layer.mock(LLMClient.Service)(clientValue)
|
||||
const models = Layer.mock(SessionRunnerModel.Service)({
|
||||
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model, undefined, cost)),
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue