diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index e002d4452b4..02927d583d0 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -425,6 +425,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int get: (input) => runtime.session.get(input.sessionID), prompt: runtime.session.prompt, command: runtime.session.command, + synthetic: runtime.session.synthetic, interrupt: (input) => runtime.session.interrupt(input.sessionID), }, } satisfies Plugin.Context diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index 57bac546e01..ad9cfc3bf12 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -244,6 +244,17 @@ export function fromPromise(plugin: Plugin) { resume: input.resume ?? undefined, }), ), + synthetic: (input) => + run( + host.session.synthetic({ + ...input, + sessionID: Session.ID.make(input.sessionID), + id: input.id == null ? undefined : SessionMessage.ID.make(input.id), + description: input.description ?? undefined, + delivery: input.delivery ?? undefined, + resume: input.resume ?? undefined, + }), + ), interrupt: (input) => run(host.session.interrupt({ sessionID: Session.ID.make(input.sessionID) })), }, } diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index b999e462528..07a06e7744e 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -97,6 +97,7 @@ export function host(overrides: Overrides = {}): PluginContext { get: overrides.session?.get ?? (() => Effect.die("unused session.get")), prompt: overrides.session?.prompt ?? (() => Effect.die("unused session.prompt")), command: overrides.session?.command ?? (() => Effect.die("unused session.command")), + synthetic: overrides.session?.synthetic ?? (() => Effect.die("unused session.synthetic")), interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")), }, } diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 8619b0e39ca..34d8e1f975f 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Message, SystemPart } from "@opencode-ai/ai" -import { Effect, Schema } from "effect" +import { DateTime, Effect, Schema } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { ModelV2 } from "@opencode-ai/core/model" @@ -10,6 +10,7 @@ import { PluginHost } from "@opencode-ai/core/plugin/host" import { PluginPromise } from "@opencode-ai/core/plugin/promise" import { SessionV2 } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" +import { SessionPending } from "@opencode-ai/core/session/pending" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ProviderV2 } from "@opencode-ai/core/provider" import { Plugin } from "@opencode-ai/plugin/v2" @@ -18,10 +19,63 @@ import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" +import { host as testHost } from "./host" const it = testEffect(PluginTestLayer) describe("fromPromise", () => { + it.effect("forwards synthetic session input", () => + Effect.gen(function* () { + const input = { + sessionID: "ses_synthetic", + id: "msg_synthetic", + text: "Background work completed", + description: null, + metadata: { shellID: "shell_1" }, + delivery: null, + resume: null, + } + let seen: unknown + const host = testHost({ + session: { + synthetic: (value) => { + seen = value + return Effect.succeed( + SessionPending.Synthetic.make({ + admittedSeq: 1, + id: SessionMessage.ID.make(input.id), + sessionID: SessionV2.ID.make(input.sessionID), + timeCreated: DateTime.makeUnsafe(0), + type: "synthetic", + data: { + text: input.text, + metadata: input.metadata, + }, + delivery: "queue", + }), + ) + }, + }, + }) + + yield* PluginPromise.fromPromise( + Plugin.define({ + id: "promise-session-synthetic", + setup: async (ctx) => { + await ctx.session.synthetic(input) + }, + }), + ).effect(host) + + expect(seen).toEqual({ + ...input, + description: undefined, + delivery: undefined, + resume: undefined, + }) + }), + ) + it.effect("forwards standard client reads", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service diff --git a/packages/docs/build/plugins.mdx b/packages/docs/build/plugins.mdx index 0a8c1c7d0f6..01a8b5746b9 100644 --- a/packages/docs/build/plugins.mdx +++ b/packages/docs/build/plugins.mdx @@ -177,7 +177,7 @@ and plugin options. | `ctx.integration` | `list`, `get`, `connect`, `attempt`, `transform`, `reload`, and connection lookup/resolution | | `ctx.plugin` | `list` currently active plugin IDs | | `ctx.reference` | `list`, `transform`, `reload` | -| `ctx.session` | `create`, `get`, `prompt`, `command`, `interrupt`, and `hook` | +| `ctx.session` | `create`, `get`, `prompt`, `command`, `synthetic`, `interrupt`, and `hook` | | `ctx.skill` | `list`, `transform`, `reload` | | `ctx.tool` | `transform` and `hook` | | `ctx.aisdk` | `hook` | diff --git a/packages/plugin/src/v2/effect/session.ts b/packages/plugin/src/v2/effect/session.ts index 541a8da28c6..836f4431c6b 100644 --- a/packages/plugin/src/v2/effect/session.ts +++ b/packages/plugin/src/v2/effect/session.ts @@ -19,6 +19,9 @@ export interface SessionHooks { readonly context: SessionContext } -export type SessionDomain = Pick, "create" | "get" | "prompt" | "command" | "interrupt"> & { +export type SessionDomain = Pick< + SessionApi, + "create" | "get" | "prompt" | "command" | "synthetic" | "interrupt" +> & { readonly hook: Hooks } diff --git a/packages/plugin/src/v2/promise/session.ts b/packages/plugin/src/v2/promise/session.ts index b2f35c867bc..387d6fe0073 100644 --- a/packages/plugin/src/v2/promise/session.ts +++ b/packages/plugin/src/v2/promise/session.ts @@ -19,6 +19,6 @@ export interface SessionHooks { readonly context: SessionContext } -export type SessionDomain = Pick & { +export type SessionDomain = Pick & { readonly hook: Hooks }