mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 09:10:47 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbdc9075b5 | |||
| ccba1c0df9 |
@@ -2,7 +2,6 @@ export * as PluginPromise from "./promise"
|
|||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||||
import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin"
|
import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin"
|
||||||
import type { SessionHooks, SessionHttp, SessionHttpMiddleware } from "@opencode-ai/plugin/promise/session"
|
|
||||||
import type { Info } from "@opencode-ai/plugin/promise/tool"
|
import type { Info } from "@opencode-ai/plugin/promise/tool"
|
||||||
import { Agent } from "@opencode-ai/schema/agent"
|
import { Agent } from "@opencode-ai/schema/agent"
|
||||||
import { Integration } from "@opencode-ai/schema/integration"
|
import { Integration } from "@opencode-ai/schema/integration"
|
||||||
@@ -58,62 +57,6 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
function sessionHook<Name extends keyof SessionHooks>(
|
|
||||||
name: Name,
|
|
||||||
callback: (event: SessionHooks[Name]) => Promise<void> | void,
|
|
||||||
): Promise<Registration>
|
|
||||||
function sessionHook(
|
|
||||||
...registration: {
|
|
||||||
[Name in keyof SessionHooks]: [
|
|
||||||
name: Name,
|
|
||||||
callback: (event: SessionHooks[Name]) => Promise<void> | void,
|
|
||||||
]
|
|
||||||
}[keyof SessionHooks]
|
|
||||||
) {
|
|
||||||
if (registration[0] !== "http")
|
|
||||||
return register(
|
|
||||||
host.session.hook(registration[0], (event) =>
|
|
||||||
Effect.promise(() => Promise.resolve(registration[1](event))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return register(
|
|
||||||
host.session.hook("http", (event) => {
|
|
||||||
const middlewares: SessionHttpMiddleware[] = []
|
|
||||||
const output: SessionHttp = {
|
|
||||||
...event,
|
|
||||||
use: (item) => {
|
|
||||||
middlewares.push(item)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return Effect.promise(() => Promise.resolve(registration[1](output))).pipe(
|
|
||||||
Effect.flatMap(() =>
|
|
||||||
Effect.forEach(
|
|
||||||
middlewares,
|
|
||||||
(item) =>
|
|
||||||
event.use((input, next) =>
|
|
||||||
Effect.tryPromise({
|
|
||||||
try: (signal) => {
|
|
||||||
const inputSignal = AbortSignal.any([signal, input.signal])
|
|
||||||
return Promise.resolve(
|
|
||||||
item(new Request(input, { signal: inputSignal }), (request) => {
|
|
||||||
const requestSignal = AbortSignal.any([signal, request.signal])
|
|
||||||
return Effect.runPromiseWith(
|
|
||||||
context,
|
|
||||||
)(next(new Request(request, { signal: requestSignal })), { signal: requestSignal })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
{ discard: true },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const context2: Context = {
|
const context2: Context = {
|
||||||
app: host.app,
|
app: host.app,
|
||||||
options: host.options,
|
options: host.options,
|
||||||
@@ -322,7 +265,8 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
hook: sessionHook,
|
hook: (name, callback) =>
|
||||||
|
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||||
create: (input) =>
|
create: (input) =>
|
||||||
run(
|
run(
|
||||||
host.session.create(
|
host.session.create(
|
||||||
|
|||||||
@@ -225,14 +225,14 @@ export const OpenAIPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
yield* ctx.session.hook("http", (evt) =>
|
yield* ctx.session.hook("http.request", (evt) =>
|
||||||
evt.use((request, next) => {
|
Effect.sync(() => {
|
||||||
if (!chatgpt || evt.model.providerID !== Provider.ID.openai) return next(request)
|
if (!chatgpt || evt.model.providerID !== Provider.ID.openai) return
|
||||||
const url = new URL(request.url)
|
const url = new URL(evt.request.url)
|
||||||
request.headers.set("originator", "opencode")
|
evt.request.headers.set("originator", "opencode")
|
||||||
request.headers.set("session-id", evt.sessionID)
|
evt.request.headers.set("session-id", evt.sessionID)
|
||||||
if (url.origin !== "https://api.openai.com") return next(request)
|
if (url.origin !== "https://api.openai.com") return
|
||||||
return next(new Request(`${codexBaseURL}${url.pathname.replace(/^\/v1/, "")}${url.search}`, request))
|
evt.request = new Request(`${codexBaseURL}${url.pathname.replace(/^\/v1/, "")}${url.search}`, evt.request)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ export * as SessionModelRequest from "./model-request"
|
|||||||
|
|
||||||
import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
||||||
import type { StreamOptions } from "@opencode-ai/ai/route"
|
import type { StreamOptions } from "@opencode-ai/ai/route"
|
||||||
import type { SessionHttpHandler, SessionHttpMiddleware } from "@opencode-ai/plugin/effect/session"
|
|
||||||
import type { Content } from "@opencode-ai/schema/tool"
|
import type { Content } from "@opencode-ai/schema/tool"
|
||||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||||
import { Cause, Config, Context, Effect, Layer, Result, Stream } from "effect"
|
import { Cause, Config, Context, Effect, Layer, Result, Stream } from "effect"
|
||||||
@@ -230,44 +229,31 @@ export const layer = Layer.effect(
|
|||||||
const options: StreamOptions = {
|
const options: StreamOptions = {
|
||||||
http: (request, handler) =>
|
http: (request, handler) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
let latest = request
|
const before = yield* hooks.trigger("session", "http.request", {
|
||||||
const origins = new WeakMap<Response, HttpClientRequest.HttpClientRequest>()
|
|
||||||
const middlewares: SessionHttpMiddleware[] = []
|
|
||||||
const web = yield* HttpClientRequest.toWeb(request)
|
|
||||||
yield* hooks.trigger("session", "http", {
|
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
agent: agent.id,
|
agent: agent.id,
|
||||||
model: resolved.ref,
|
model: resolved.ref,
|
||||||
use: (item) =>
|
request: yield* HttpClientRequest.toWeb(request),
|
||||||
Effect.sync(() => {
|
|
||||||
middlewares.push(item)
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
const send = (input: Request) =>
|
let sent = HttpClientRequest.fromWeb(before.request)
|
||||||
Effect.gen(function* () {
|
if (before.request.body)
|
||||||
let sent = HttpClientRequest.fromWeb(input)
|
sent = HttpClientRequest.bodyUint8Array(
|
||||||
if (input.body)
|
sent,
|
||||||
sent = HttpClientRequest.bodyUint8Array(
|
new Uint8Array(yield* Effect.promise(() => before.request.clone().arrayBuffer())),
|
||||||
sent,
|
before.request.headers.get("content-type") ?? undefined,
|
||||||
new Uint8Array(yield* Effect.promise(() => input.clone().arrayBuffer())),
|
)
|
||||||
input.headers.get("content-type") ?? undefined,
|
const response = yield* handler(sent)
|
||||||
)
|
const after = yield* hooks.trigger("session", "http.response", {
|
||||||
latest = sent
|
sessionID: session.id,
|
||||||
const response = yield* handler(sent)
|
agent: agent.id,
|
||||||
const body = [204, 205, 304].includes(response.status)
|
model: resolved.ref,
|
||||||
? null
|
request: before.request,
|
||||||
: yield* Stream.toReadableStreamEffect(response.stream)
|
response: new Response(
|
||||||
const output = new Response(body, { status: response.status, headers: response.headers })
|
[204, 205, 304].includes(response.status) ? null : yield* Stream.toReadableStreamEffect(response.stream),
|
||||||
origins.set(output, sent)
|
{ status: response.status, headers: response.headers },
|
||||||
return output
|
),
|
||||||
})
|
})
|
||||||
const dispatch = middlewares.reduce<SessionHttpHandler>(
|
return HttpClientResponse.fromWeb(sent, after.response)
|
||||||
(next, item) => (input: Request) => item(input, next),
|
|
||||||
send,
|
|
||||||
)
|
|
||||||
const response = yield* dispatch(web)
|
|
||||||
const origin = origins.get(response) ?? latest
|
|
||||||
return HttpClientResponse.fromWeb(origin, response)
|
|
||||||
}).pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause))))),
|
}).pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause))))),
|
||||||
}
|
}
|
||||||
if (promptCacheSnapshots) {
|
if (promptCacheSnapshots) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Message, SystemPart } from "@opencode-ai/ai"
|
import { Message, SystemPart } from "@opencode-ai/ai"
|
||||||
import { DateTime, Deferred, Effect, Fiber, Schema } from "effect"
|
import { DateTime, Effect, Schema } from "effect"
|
||||||
import { Agent } from "@opencode-ai/core/agent"
|
import { Agent } from "@opencode-ai/core/agent"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Model } from "@opencode-ai/core/model"
|
import { Model } from "@opencode-ai/core/model"
|
||||||
@@ -15,7 +15,7 @@ import { SessionPending } from "@opencode-ai/core/session/pending"
|
|||||||
import { Tool } from "@opencode-ai/core/tool"
|
import { Tool } from "@opencode-ai/core/tool"
|
||||||
import { Provider } from "@opencode-ai/core/provider"
|
import { Provider } from "@opencode-ai/core/provider"
|
||||||
import { define } from "@opencode-ai/plugin/promise/plugin"
|
import { define } from "@opencode-ai/plugin/promise/plugin"
|
||||||
import type { SessionHooks, SessionHttpHandler } from "@opencode-ai/plugin/effect/session"
|
import type { SessionHooks } from "@opencode-ai/plugin/effect/session"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { PluginTestLayer } from "./fixture"
|
import { PluginTestLayer } from "./fixture"
|
||||||
import { host as testHost } from "./host"
|
import { host as testHost } from "./host"
|
||||||
@@ -223,102 +223,45 @@ describe("fromPromise", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("adapts promise session HTTP hooks", () =>
|
it.effect("adapts promise session HTTP request and response hooks", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
const hooks = yield* PluginHooks.Service
|
const hooks = yield* PluginHooks.Service
|
||||||
const host = yield* PluginHost.make(plugin)
|
const host = yield* PluginHost.make(plugin)
|
||||||
const bodies: string[] = []
|
|
||||||
yield* PluginPromise.fromPromise(
|
yield* PluginPromise.fromPromise(
|
||||||
define({
|
define({
|
||||||
id: "promise-session-http",
|
id: "promise-session-http",
|
||||||
setup: async (ctx) => {
|
setup: async (ctx) => {
|
||||||
await ctx.session.hook("http", (event) => {
|
await ctx.session.hook("http.request", (event) => {
|
||||||
event.use(async (request, next) => {
|
event.request = new Request("https://provider.test/changed", event.request)
|
||||||
request.headers.set("x-hook", "promise")
|
event.request.headers.set("x-hook", "promise")
|
||||||
await next(request)
|
|
||||||
const response = await next(request)
|
|
||||||
return new Response(`${await response.text()}-response`)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
await ctx.session.hook("http", (event) => {
|
await ctx.session.hook("http.response", async (event) => {
|
||||||
event.use(async (request, next) => {
|
event.response = new Response(`${await event.response.text()}-response`, {
|
||||||
const response = await next(request)
|
status: event.response.status,
|
||||||
return new Response(`${await response.text()}-outer`)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).effect(host)
|
).effect(host)
|
||||||
const middlewares: Parameters<PluginHooks.Domains["session"]["http"]["use"]>[0][] = []
|
const context = {
|
||||||
const event: PluginHooks.Domains["session"]["http"] = {
|
|
||||||
sessionID: Session.ID.make("ses_promise_session_http"),
|
sessionID: Session.ID.make("ses_promise_session_http"),
|
||||||
agent: Agent.ID.make("build"),
|
agent: Agent.ID.make("build"),
|
||||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
||||||
use: (item) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
middlewares.push(item)
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* hooks.trigger("session", "http", event)
|
const request = yield* hooks.trigger("session", "http.request", {
|
||||||
const request = middlewares.reduce<SessionHttpHandler>(
|
...context,
|
||||||
(next, item) => (input: Request) => item(input, next),
|
request: new Request("https://provider.test", { method: "POST", body: "payload" }),
|
||||||
(input: Request) =>
|
})
|
||||||
Effect.promise(() => input.text()).pipe(
|
const response = yield* hooks.trigger("session", "http.response", {
|
||||||
Effect.tap((body) => Effect.sync(() => bodies.push(body))),
|
...context,
|
||||||
Effect.as(new Response(input.headers.get("x-hook") ?? "missing")),
|
request: request.request,
|
||||||
),
|
response: new Response(request.request.headers.get("x-hook") ?? "missing"),
|
||||||
)
|
})
|
||||||
const response = yield* request(new Request("https://provider.test", { method: "POST", body: "payload" }))
|
|
||||||
|
|
||||||
expect(bodies).toEqual(["payload", "payload"])
|
expect(request.request.url).toBe("https://provider.test/changed")
|
||||||
expect(yield* Effect.promise(() => response.text())).toBe("promise-response-outer")
|
expect(yield* Effect.promise(() => response.response.text())).toBe("promise-response")
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("interrupts the Effect request through a promise session HTTP hook", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
const hooks = yield* PluginHooks.Service
|
|
||||||
const host = yield* PluginHost.make(plugin)
|
|
||||||
yield* PluginPromise.fromPromise(
|
|
||||||
define({
|
|
||||||
id: "promise-session-http-interrupt",
|
|
||||||
setup: async (ctx) => {
|
|
||||||
await ctx.session.hook("http", (event) => {
|
|
||||||
event.use((request, next) => next(request))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).effect(host)
|
|
||||||
const started = yield* Deferred.make<void>()
|
|
||||||
const interrupted = yield* Deferred.make<void>()
|
|
||||||
const middlewares: Parameters<PluginHooks.Domains["session"]["http"]["use"]>[0][] = []
|
|
||||||
const event: PluginHooks.Domains["session"]["http"] = {
|
|
||||||
sessionID: Session.ID.make("ses_promise_session_http_interrupt"),
|
|
||||||
agent: Agent.ID.make("build"),
|
|
||||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
|
||||||
use: (item) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
middlewares.push(item)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* hooks.trigger("session", "http", event)
|
|
||||||
const request = middlewares.reduce<SessionHttpHandler>(
|
|
||||||
(next, item) => (input: Request) => item(input, next),
|
|
||||||
() =>
|
|
||||||
Deferred.succeed(started, undefined).pipe(
|
|
||||||
Effect.andThen(Effect.never),
|
|
||||||
Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const fiber = yield* request(new Request("https://provider.test")).pipe(Effect.forkChild)
|
|
||||||
yield* Deferred.await(started)
|
|
||||||
yield* Fiber.interrupt(fiber)
|
|
||||||
|
|
||||||
expect(yield* Deferred.isDone(interrupted)).toBeTrue()
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { PluginHost } from "@opencode-ai/core/plugin/host"
|
|||||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||||
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
|
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
|
||||||
import { Provider } from "@opencode-ai/core/provider"
|
import { Provider } from "@opencode-ai/core/provider"
|
||||||
import type { SessionHttpHandler } from "@opencode-ai/plugin/effect/session"
|
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { PluginTestLayer } from "./fixture"
|
import { PluginTestLayer } from "./fixture"
|
||||||
|
|
||||||
@@ -31,26 +30,13 @@ function required<T>(value: T | undefined): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const http = Effect.fn(function* (providerID: Provider.ID, url: string) {
|
const http = Effect.fn(function* (providerID: Provider.ID, url: string) {
|
||||||
const middlewares: Parameters<PluginHooks.Domains["session"]["http"]["use"]>[0][] = []
|
const event = yield* (yield* PluginHooks.Service).trigger("session", "http.request", {
|
||||||
yield* (yield* PluginHooks.Service).trigger("session", "http", {
|
|
||||||
sessionID: Session.ID.make("ses_test"),
|
sessionID: Session.ID.make("ses_test"),
|
||||||
agent: Agent.ID.make("build"),
|
agent: Agent.ID.make("build"),
|
||||||
model: Model.Ref.make({ providerID, id: Model.ID.make("gpt-5.5") }),
|
model: Model.Ref.make({ providerID, id: Model.ID.make("gpt-5.5") }),
|
||||||
use: (item) =>
|
request: new Request(url, { method: "POST", body: "{}" }),
|
||||||
Effect.sync(() => {
|
|
||||||
middlewares.push(item)
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
const request = middlewares.reduce<SessionHttpHandler>(
|
return { url: event.request.url, headers: Object.fromEntries(event.request.headers.entries()) }
|
||||||
(next, item) => (input: Request) => item(input, next),
|
|
||||||
(input: Request) => {
|
|
||||||
const headers = new Headers(input.headers)
|
|
||||||
headers.set("x-seen-url", input.url)
|
|
||||||
return Effect.succeed(new Response(null, { headers }))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const response = yield* request(new Request(url, { method: "POST", body: "{}" }))
|
|
||||||
return { url: response.headers.get("x-seen-url"), headers: Object.fromEntries(response.headers.entries()) }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("OpenAIPlugin", () => {
|
describe("OpenAIPlugin", () => {
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ describe("SessionRunnerLLM recorded", () => {
|
|||||||
describe("SessionModelRequest HTTP bridge", () => {
|
describe("SessionModelRequest HTTP bridge", () => {
|
||||||
const bodies: Uint8Array[] = []
|
const bodies: Uint8Array[] = []
|
||||||
const methods: string[] = []
|
const methods: string[] = []
|
||||||
|
const headers: Array<string | undefined> = []
|
||||||
const response = [
|
const response = [
|
||||||
'data: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":0,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello!"},"finish_reason":null}]}',
|
'data: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":0,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello!"},"finish_reason":null}]}',
|
||||||
'data: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":0,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}',
|
'data: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":0,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}',
|
||||||
@@ -267,6 +268,7 @@ describe("SessionModelRequest HTTP bridge", () => {
|
|||||||
if (request.body._tag !== "Uint8Array") throw new Error(`Unexpected request body: ${request.body._tag}`)
|
if (request.body._tag !== "Uint8Array") throw new Error(`Unexpected request body: ${request.body._tag}`)
|
||||||
methods.push(request.method)
|
methods.push(request.method)
|
||||||
bodies.push(request.body.body.slice())
|
bodies.push(request.body.body.slice())
|
||||||
|
headers.push(request.headers["x-hook"])
|
||||||
return HttpClientResponse.fromWeb(
|
return HttpClientResponse.fromWeb(
|
||||||
request,
|
request,
|
||||||
new Response(response, { headers: { "content-type": "text/event-stream" } }),
|
new Response(response, { headers: { "content-type": "text/event-stream" } }),
|
||||||
@@ -274,14 +276,16 @@ describe("SessionModelRequest HTTP bridge", () => {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const retryIt = testEffect(
|
const httpIt = testEffect(
|
||||||
testLayer(LLMClient.layer.pipe(Layer.provide(RequestExecutor.layer.pipe(Layer.provide(transport))))),
|
testLayer(LLMClient.layer.pipe(Layer.provide(RequestExecutor.layer.pipe(Layer.provide(transport))))),
|
||||||
)
|
)
|
||||||
|
|
||||||
retryIt.effect("lets an Effect plugin send the same POST Request twice", () =>
|
httpIt.effect("runs Effect HTTP request and response hooks around one provider request", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
bodies.length = 0
|
bodies.length = 0
|
||||||
methods.length = 0
|
methods.length = 0
|
||||||
|
headers.length = 0
|
||||||
|
const seen: string[] = []
|
||||||
const agents = yield* Agent.Service
|
const agents = yield* Agent.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const hooks = yield* PluginHooks.Service
|
const hooks = yield* PluginHooks.Service
|
||||||
@@ -296,13 +300,20 @@ describe("SessionModelRequest HTTP bridge", () => {
|
|||||||
catalog: catalogHost(catalog),
|
catalog: catalogHost(catalog),
|
||||||
session: { hook: (name, callback) => hooks.register("session", name, callback) },
|
session: { hook: (name, callback) => hooks.register("session", name, callback) },
|
||||||
})
|
})
|
||||||
yield* pluginHost.session.hook("http", (event) =>
|
yield* pluginHost.session.hook("http.request", (event) =>
|
||||||
event.use((request, next) =>
|
Effect.sync(() => {
|
||||||
Effect.gen(function* () {
|
seen.push("request")
|
||||||
yield* next(request).pipe(Effect.flatMap((response) => Effect.promise(() => response.text())))
|
event.request.headers.set("x-hook", "effect")
|
||||||
return yield* next(request)
|
}),
|
||||||
}),
|
)
|
||||||
),
|
yield* pluginHost.session.hook("http.response", (event) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
seen.push(`response:${event.response.status}:${event.request.headers.get("x-hook")}`)
|
||||||
|
event.response = new Response(
|
||||||
|
(yield* Effect.promise(() => event.response.text())).replace("Hello!", "Hooked!"),
|
||||||
|
event.response,
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), { discard: true })
|
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), { discard: true })
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
@@ -330,10 +341,15 @@ describe("SessionModelRequest HTTP bridge", () => {
|
|||||||
|
|
||||||
yield* session.resume(retrySessionID)
|
yield* session.resume(retrySessionID)
|
||||||
|
|
||||||
expect(methods).toEqual(["POST", "POST"])
|
expect(methods).toEqual(["POST"])
|
||||||
expect(bodies).toHaveLength(2)
|
expect(headers).toEqual(["effect"])
|
||||||
|
expect(seen).toEqual(["request", "response:200:effect"])
|
||||||
|
expect(bodies).toHaveLength(1)
|
||||||
expect(bodies[0]?.byteLength).toBeGreaterThan(0)
|
expect(bodies[0]?.byteLength).toBeGreaterThan(0)
|
||||||
expect(bodies[1]).toEqual(bodies[0])
|
expect((yield* session.context(retrySessionID))[1]).toMatchObject({
|
||||||
|
type: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hooked!" }],
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Message, SystemPart } from "@opencode-ai/ai"
|
|||||||
import type { Agent } from "@opencode-ai/schema/agent"
|
import type { Agent } from "@opencode-ai/schema/agent"
|
||||||
import type { Model } from "@opencode-ai/schema/model"
|
import type { Model } from "@opencode-ai/schema/model"
|
||||||
import type { Session } from "@opencode-ai/schema/session"
|
import type { Session } from "@opencode-ai/schema/session"
|
||||||
import type { Effect, JsonSchema } from "effect"
|
import type { JsonSchema } from "effect"
|
||||||
import type { Hooks } from "./registration.js"
|
import type { Hooks } from "./registration.js"
|
||||||
|
|
||||||
export interface SessionContext {
|
export interface SessionContext {
|
||||||
@@ -15,23 +15,25 @@ export interface SessionContext {
|
|||||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionHttp {
|
export interface SessionHttpRequest {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly agent: Agent.ID
|
readonly agent: Agent.ID
|
||||||
readonly model: Model.Ref
|
readonly model: Model.Ref
|
||||||
readonly use: (middleware: SessionHttpMiddleware) => Effect.Effect<void>
|
request: Request
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionHttpHandler = (request: Request) => Effect.Effect<Response, Error>
|
export interface SessionHttpResponse {
|
||||||
|
readonly sessionID: Session.ID
|
||||||
export type SessionHttpMiddleware = (
|
readonly agent: Agent.ID
|
||||||
request: Request,
|
readonly model: Model.Ref
|
||||||
next: SessionHttpHandler,
|
readonly request: Request
|
||||||
) => Effect.Effect<Response, Error>
|
response: Response
|
||||||
|
}
|
||||||
|
|
||||||
export interface SessionHooks {
|
export interface SessionHooks {
|
||||||
readonly context: SessionContext
|
readonly context: SessionContext
|
||||||
readonly http: SessionHttp
|
readonly "http.request": SessionHttpRequest
|
||||||
|
readonly "http.response": SessionHttpResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionDomain = Pick<
|
export type SessionDomain = Pick<
|
||||||
|
|||||||
@@ -15,23 +15,25 @@ export interface SessionContext {
|
|||||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionHttp {
|
export interface SessionHttpRequest {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly agent: Agent.ID
|
readonly agent: Agent.ID
|
||||||
readonly model: Model.Ref
|
readonly model: Model.Ref
|
||||||
readonly use: (middleware: SessionHttpMiddleware) => void
|
request: Request
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionHttpHandler = (request: Request) => Promise<Response>
|
export interface SessionHttpResponse {
|
||||||
|
readonly sessionID: Session.ID
|
||||||
export type SessionHttpMiddleware = (
|
readonly agent: Agent.ID
|
||||||
request: Request,
|
readonly model: Model.Ref
|
||||||
next: SessionHttpHandler,
|
readonly request: Request
|
||||||
) => Promise<Response> | Response
|
response: Response
|
||||||
|
}
|
||||||
|
|
||||||
export interface SessionHooks {
|
export interface SessionHooks {
|
||||||
readonly context: SessionContext
|
readonly context: SessionContext
|
||||||
readonly http: SessionHttp
|
readonly "http.request": SessionHttpRequest
|
||||||
|
readonly "http.response": SessionHttpResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionDomain = Pick<
|
export type SessionDomain = Pick<
|
||||||
|
|||||||
+16
-8
@@ -246,19 +246,27 @@ Runtime hooks intercept live operations:
|
|||||||
| `ctx.aisdk.hook("sdk", callback)` | `sdk`, after inspecting `model`, `package`, and `options` |
|
| `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.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.hook("context", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch |
|
||||||
| `ctx.session.hook("http", callback)` | `use`, registering request and response handling |
|
| `ctx.session.hook("http.request", callback)` | `request`, immediately before provider dispatch |
|
||||||
|
| `ctx.session.hook("http.response", callback)` | `response`, immediately after the provider responds |
|
||||||
| `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes |
|
| `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 |
|
| `ctx.tool.hook("execute.after", callback)` | Terminal `result` on success or `error` on failure |
|
||||||
|
|
||||||
HTTP hooks can modify requests, inspect responses, retry, or return a
|
HTTP hooks can modify requests and responses. They apply to native models; AI
|
||||||
response without calling the provider. It applies to native models; AI SDK
|
SDK models do not currently pass through these hooks. Request and response
|
||||||
models do not currently pass through this hook.
|
bodies are one-shot streams. Use `clone()` when you intentionally need a
|
||||||
|
separate reader, but be aware that its slower branch may buffer data. To inspect
|
||||||
|
or modify chunks while preserving streaming, replace the body with one piped
|
||||||
|
through a `TransformStream`.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
await ctx.session.hook("http", (event) => {
|
await ctx.session.hook("http.request", (event) => {
|
||||||
event.use((request, next) => {
|
event.request.headers.set("x-session-id", event.sessionID)
|
||||||
request.headers.set("x-session-id", event.sessionID)
|
})
|
||||||
return next(request)
|
|
||||||
|
await ctx.session.hook("http.response", (event) => {
|
||||||
|
event.response = new Response(event.response.body, {
|
||||||
|
status: event.response.status,
|
||||||
|
headers: { ...Object.fromEntries(event.response.headers), "x-plugin": "enabled" },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user