From 89ef53537e59c8e9e1eb2124cefc63368532aa8c Mon Sep 17 00:00:00 2001 From: Kit Langton <7587245+kitlangton@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:34:35 +0000 Subject: [PATCH] refactor(core): add location session runtime --- packages/core/src/location-services.ts | 2 + packages/core/src/plugin.ts | 8 ++ packages/core/src/plugin/host.ts | 60 +++++++++ packages/core/src/plugin/promise.ts | 10 ++ packages/core/src/session.ts | 38 ++---- packages/core/src/session/runtime.ts | 157 ++++++++++++++++++++++ packages/plugin/src/v2/effect/context.ts | 2 + packages/plugin/src/v2/effect/session.ts | 30 +++++ packages/plugin/src/v2/promise/context.ts | 2 + packages/plugin/src/v2/promise/session.ts | 29 ++++ packages/server/src/handlers/session.ts | 56 ++++++-- specs/v2/plugin-session-tools-plan.md | 8 +- 12 files changed, 356 insertions(+), 46 deletions(-) create mode 100644 packages/core/src/session/runtime.ts create mode 100644 packages/plugin/src/v2/effect/session.ts create mode 100644 packages/plugin/src/v2/promise/session.ts diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 07669c0843..38be4be613 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -26,6 +26,7 @@ import { Reference } from "./reference" import { ReferenceGuidance } from "./reference/guidance" import * as SessionRunnerLLM from "./session/runner/llm" import { SessionRunnerModel } from "./session/runner/model" +import { SessionRuntime } from "./session/runtime" import { SessionTodo } from "./session/todo" import { SkillV2 } from "./skill" import { SkillGuidance } from "./skill/guidance" @@ -75,6 +76,7 @@ export const locationServices = LayerNode.group([ BuiltInTools.node, SessionRunnerModel.node, Snapshot.node, + SessionRuntime.node, SessionRunnerLLM.node, ]) diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index 2776ee20f7..f6ec6280b3 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -13,7 +13,10 @@ import { Integration } from "./integration" import { KeyedMutex } from "./effect/keyed-mutex" import { PluginHost } from "./plugin/host" import { Reference } from "./reference" +import { SessionV2 } from "./session" +import { SessionRuntime } from "./session/runtime" import { SkillV2 } from "./skill" +import { Location } from "./location" import { State } from "./state" export const ID = Plugin.ID @@ -149,6 +152,8 @@ export const locationLayer = layer.pipe( Layer.provideMerge(CommandV2.locationLayer), Layer.provideMerge(Integration.locationLayer), Layer.provideMerge(Reference.locationLayer), + Layer.provideMerge(SessionV2.defaultLayer), + Layer.provideMerge(SessionRuntime.layer), Layer.provideMerge(SkillV2.locationLayer), ) @@ -163,6 +168,9 @@ export const node = makeLocationNode({ CommandV2.node, Integration.node, Reference.node, + SessionV2.node, + SessionRuntime.node, SkillV2.node, + Location.node, ], }) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index a9d084709e..e52a27a09c 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -13,7 +13,11 @@ import { PluginV2 } from "../plugin" import { ProviderV2 } from "../provider" import { Reference } from "../reference" import type { DeepMutable } from "../schema" +import { SessionV2 } from "../session" +import { SessionMessage } from "../session/message" +import { SessionRuntime } from "../session/runtime" import { SkillV2 } from "../skill" +import { Location } from "../location" const mutable = (value: T) => value as DeepMutable @@ -24,7 +28,10 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int const commands = yield* CommandV2.Service const integration = yield* Integration.Service const reference = yield* Reference.Service + const session = yield* SessionV2.Service + const sessionRuntime = yield* SessionRuntime.Service const skill = yield* SkillV2.Service + const location = yield* Location.Service return { options: {}, @@ -205,6 +212,59 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int }), ), }, + session: { + create: (input) => + session.create( + input.parentID + ? { + parentID: SessionV2.ID.make(input.parentID), + title: input.title, + agent: input.agent ? AgentV2.ID.make(input.agent) : undefined, + model: input.model + ? { + id: ModelV2.ID.make(input.model.id), + providerID: ProviderV2.ID.make(input.model.providerID), + variant: input.model.variant ? ModelV2.VariantID.make(input.model.variant) : undefined, + } + : undefined, + } + : { + id: input.id ? SessionV2.ID.make(input.id) : undefined, + location: { directory: location.directory, workspaceID: location.workspaceID }, + title: input.title, + agent: input.agent ? AgentV2.ID.make(input.agent) : undefined, + model: input.model + ? { + id: ModelV2.ID.make(input.model.id), + providerID: ProviderV2.ID.make(input.model.providerID), + variant: input.model.variant ? ModelV2.VariantID.make(input.model.variant) : undefined, + } + : undefined, + }, + ), + get: (sessionID) => session.get(SessionV2.ID.make(sessionID)), + messages: (input) => + session.messages({ + sessionID: SessionV2.ID.make(input.sessionID), + limit: input.limit, + order: input.order, + cursor: input.cursor + ? { id: SessionMessage.ID.make(input.cursor.id), direction: input.cursor.direction } + : undefined, + }), + context: (sessionID) => session.context(SessionV2.ID.make(sessionID)), + prompt: (input) => + sessionRuntime.prompt({ + id: input.id ? SessionMessage.ID.make(input.id) : undefined, + sessionID: SessionV2.ID.make(input.sessionID), + prompt: input.prompt, + delivery: input.delivery, + resume: input.resume, + }), + resume: (sessionID) => sessionRuntime.resume(SessionV2.ID.make(sessionID)), + wait: (sessionID) => sessionRuntime.wait(SessionV2.ID.make(sessionID)), + interrupt: (sessionID) => sessionRuntime.interrupt(SessionV2.ID.make(sessionID)), + }, skill: { reload: skill.reload, transform: (callback) => diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index d0bf82c2a0..a0ee402917 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -81,6 +81,16 @@ export function fromPromise(plugin: Plugin) { transform: transform(host.reference), reload: () => run(host.reference.reload()), }, + session: { + create: (input) => run(host.session.create(input)), + get: (sessionID) => run(host.session.get(sessionID)), + messages: (input) => run(host.session.messages(input)), + context: (sessionID) => run(host.session.context(sessionID)), + prompt: (input) => run(host.session.prompt(input)), + resume: (sessionID) => run(host.session.resume(sessionID)), + wait: (sessionID) => run(host.session.wait(sessionID)), + interrupt: (sessionID) => run(host.session.interrupt(sessionID)), + }, skill: { transform: transform(host.skill), reload: () => run(host.skill.reload()), diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 636f9a535c..c63ffbf5a2 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -26,9 +26,7 @@ import path from "path" import { fromRow } from "./session/info" import { SessionRunner } from "./session/runner/index" import { SessionStore } from "./session/store" -import { SessionExecution } from "./session/execution" import { makeGlobalNode } from "./effect/app-node" -import { LocationServiceMap } from "./location-service-map" import { MessageDecodeError } from "./session/error" import { SessionEvent } from "./session/event" import { SessionInput } from "./session/input" @@ -194,9 +192,7 @@ export const layer = Layer.effect( const db = database.db const events = yield* EventV2.Service const projects = yield* ProjectV2.Service - const execution = yield* SessionExecution.Service const store = yield* SessionStore.Service - const locations = yield* LocationServiceMap.Service const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) const isDurableSessionEvent = Schema.is(SessionEvent.Durable) const decode = (row: typeof SessionMessageTable.$inferSelect) => @@ -389,7 +385,8 @@ export const layer = Layer.effect( ) if (!SessionInput.equivalent(admitted, expected)) return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) - if (input.resume !== false) yield* execution.wake(admitted.sessionID) + if (input.resume !== false) + return yield* Effect.die("SessionV2.prompt with resume moved to SessionRuntime.Service") return admitted }), ), @@ -438,38 +435,25 @@ export const layer = Layer.effect( }), wait: Effect.fn("V2Session.wait")(function* (sessionID) { yield* result.get(sessionID) - yield* execution.awaitIdle(sessionID) + return yield* Effect.die("SessionV2.wait moved to SessionRuntime.Service") }), - active: execution.active, + active: Effect.succeed(new Set()), resume: Effect.fn("V2Session.resume")(function* (sessionID) { yield* result.get(sessionID) - yield* execution.resume(sessionID) + return yield* Effect.die("SessionV2.resume moved to SessionRuntime.Service") }), - interrupt: Effect.fn("V2Session.interrupt")((sessionID) => - Effect.uninterruptible(execution.interrupt(sessionID)), - ), + interrupt: Effect.fn("V2Session.interrupt")(() => Effect.die("SessionV2.interrupt moved to SessionRuntime.Service")), revert: { stage: Effect.fn("V2Session.revert.stage")(function* (input) { - const session = yield* result.get(input.sessionID) - if ((yield* execution.active).has(input.sessionID)) - return yield* new BusyError({ sessionID: input.sessionID }) - return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe( - Effect.provideService(Database.Service, database), - Effect.provideService(EventV2.Service, events), - Effect.provide(locations.get(session.location)), - ) + yield* result.get(input.sessionID) + return yield* Effect.die("SessionV2.revert.stage moved to SessionRuntime.Service") }), clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) { - const session = yield* result.get(sessionID) - if ((yield* execution.active).has(sessionID)) return yield* new BusyError({ sessionID }) - return yield* SessionRevert.clear(session).pipe( - Effect.provideService(EventV2.Service, events), - Effect.provide(locations.get(session.location)), - ) + yield* result.get(sessionID) + return yield* Effect.die("SessionV2.revert.clear moved to SessionRuntime.Service") }), commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) { const session = yield* result.get(sessionID) - if ((yield* execution.active).has(sessionID)) return yield* new BusyError({ sessionID }) return yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) }), }, @@ -510,9 +494,7 @@ export const node = makeGlobalNode({ Database.node, EventV2.node, ProjectV2.node, - SessionExecution.node, SessionStore.node, - LocationServiceMap.node, SessionProjector.node, ], }) diff --git a/packages/core/src/session/runtime.ts b/packages/core/src/session/runtime.ts new file mode 100644 index 0000000000..1f1e60088c --- /dev/null +++ b/packages/core/src/session/runtime.ts @@ -0,0 +1,157 @@ +export * as SessionRuntime from "./runtime" + +import { Context, Effect, Layer } from "effect" +import { Database } from "../database/database" +import { EventV2 } from "../event" +import { Location } from "../location" +import { PromptInput } from "@opencode-ai/schema/prompt-input" +import { SessionMessage } from "./message" +import { Prompt } from "./prompt" +import { SessionInput } from "./input" +import { SessionRevert } from "./revert" +import { SessionRunner } from "./runner" +import * as SessionRunnerLLM from "./runner/llm" +import { SessionRunCoordinator } from "./run-coordinator" +import { SessionSchema } from "./schema" +import { Snapshot } from "../snapshot" +import { FSUtil } from "../fs-util" +import { makeLocationNode } from "../effect/app-node" +import { SessionV2 } from "../session" +import { + BusyError, + MessageNotFoundError, + NotFoundError, + PromptConflictError, + type RevertState, +} from "../session" + +export interface Interface { + readonly prompt: (input: { + id?: SessionMessage.ID + sessionID: SessionSchema.ID + prompt: PromptInput.Prompt + delivery?: SessionInput.Delivery + resume?: boolean + }) => Effect.Effect + readonly wait: (id: SessionSchema.ID) => Effect.Effect + readonly active: Effect.Effect> + readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect + readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect + readonly revert: { + readonly stage: (input: { + sessionID: SessionSchema.ID + messageID: SessionMessage.ID + files?: boolean + }) => Effect.Effect + readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect + readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect + } +} + +export class Service extends Context.Service()("@opencode/v2/SessionRuntime") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const database = yield* Database.Service + const db = database.db + const events = yield* EventV2.Service + const location = yield* Location.Service + const sessions = yield* SessionV2.Service + const runner = yield* SessionRunner.Service + + const local = Effect.fn("SessionRuntime.local")(function* (sessionID: SessionSchema.ID) { + const session = yield* sessions.get(sessionID) + if (session.location.directory !== location.directory || session.location.workspaceID !== location.workspaceID) + return yield* new NotFoundError({ sessionID }) + return session + }) + + const coordinator = yield* SessionRunCoordinator.make({ + drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) { + yield* local(sessionID).pipe(Effect.orDie) + return yield* runner.run({ sessionID, force }) + }), + }) + + return Service.of({ + prompt: Effect.fn("SessionRuntime.prompt")((input) => + Effect.uninterruptible( + Effect.gen(function* () { + yield* local(input.sessionID) + const prompt = resolvePrompt(input.prompt) + const messageID = input.id ?? SessionMessage.ID.create() + const delivery = input.delivery ?? "steer" + const expected = { sessionID: input.sessionID, messageID, prompt, delivery } + const admitted = yield* SessionInput.admit(db, events, { + id: messageID, + sessionID: input.sessionID, + prompt, + delivery, + }).pipe( + Effect.catchDefect((defect) => + defect instanceof SessionInput.LifecycleConflict + ? new PromptConflictError({ sessionID: input.sessionID, messageID }) + : Effect.die(defect), + ), + ) + if (!SessionInput.equivalent(admitted, expected)) + return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) + if (input.resume !== false) yield* coordinator.wake(admitted.sessionID) + return admitted + }), + ), + ), + wait: Effect.fn("SessionRuntime.wait")(function* (sessionID) { + yield* local(sessionID) + yield* coordinator.awaitIdle(sessionID) + }), + active: coordinator.active, + resume: Effect.fn("SessionRuntime.resume")(function* (sessionID) { + yield* local(sessionID) + yield* coordinator.run(sessionID) + }), + interrupt: Effect.fn("SessionRuntime.interrupt")((sessionID) => Effect.uninterruptible(coordinator.interrupt(sessionID))), + revert: { + stage: Effect.fn("SessionRuntime.revert.stage")(function* (input) { + const session = yield* local(input.sessionID) + if ((yield* coordinator.active).has(input.sessionID)) return yield* new BusyError({ sessionID: input.sessionID }) + return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe( + Effect.provideService(Database.Service, database), + Effect.provideService(EventV2.Service, events), + ) + }), + clear: Effect.fn("SessionRuntime.revert.clear")(function* (sessionID) { + const session = yield* local(sessionID) + if ((yield* coordinator.active).has(sessionID)) return yield* new BusyError({ sessionID }) + return yield* SessionRevert.clear(session).pipe(Effect.provideService(EventV2.Service, events)) + }), + commit: Effect.fn("SessionRuntime.revert.commit")(function* (sessionID) { + const session = yield* local(sessionID) + if ((yield* coordinator.active).has(sessionID)) return yield* new BusyError({ sessionID }) + return yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) + }), + }, + }) + }), +) + +const resolvePrompt = (input: PromptInput.Prompt) => + Prompt.make({ + text: input.text, + agents: input.agents, + files: input.files?.map((file) => { + const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1] + const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri) + return { + ...file, + mime: dataMime ?? (target.endsWith("/") ? "application/x-directory" : FSUtil.mimeType(target)), + } + }), + }) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [Database.node, EventV2.node, Location.node, SessionV2.node, SessionRunnerLLM.node, Snapshot.node], +}) diff --git a/packages/plugin/src/v2/effect/context.ts b/packages/plugin/src/v2/effect/context.ts index 9089334ee3..64c7c9e622 100644 --- a/packages/plugin/src/v2/effect/context.ts +++ b/packages/plugin/src/v2/effect/context.ts @@ -6,6 +6,7 @@ import type { CommandHooks } from "./command.js" import type { IntegrationHooks } from "./integration.js" import type { PluginDomain } from "./plugin.js" import type { ReferenceHooks } from "./reference.js" +import type { SessionDomain } from "./session.js" import type { SkillHooks } from "./skill.js" import type { Reload } from "./registration.js" @@ -18,5 +19,6 @@ export interface PluginContext { readonly integration: IntegrationHooks & Reload readonly plugin: PluginDomain readonly reference: ReferenceHooks & Reload + readonly session: SessionDomain readonly skill: SkillHooks & Reload } diff --git a/packages/plugin/src/v2/effect/session.ts b/packages/plugin/src/v2/effect/session.ts new file mode 100644 index 0000000000..6bc628f918 --- /dev/null +++ b/packages/plugin/src/v2/effect/session.ts @@ -0,0 +1,30 @@ +import type { Effect } from "effect" +import type { PromptInput, SessionInputAdmitted, SessionMessage, SessionV2Info } from "@opencode-ai/sdk/v2/types" + +export interface SessionDomain { + readonly create: (input: { + readonly id?: string + readonly parentID?: string + readonly title?: string + readonly agent?: string + readonly model?: SessionV2Info["model"] + }) => Effect.Effect + readonly get: (sessionID: string) => Effect.Effect + readonly messages: (input: { + readonly sessionID: string + readonly limit?: number + readonly order?: "asc" | "desc" + readonly cursor?: { readonly id: string; readonly direction: "previous" | "next" } + }) => Effect.Effect> + readonly context: (sessionID: string) => Effect.Effect> + readonly prompt: (input: { + readonly id?: string + readonly sessionID: string + readonly prompt: PromptInput + readonly delivery?: "steer" | "queue" + readonly resume?: boolean + }) => Effect.Effect + readonly resume: (sessionID: string) => Effect.Effect + readonly wait: (sessionID: string) => Effect.Effect + readonly interrupt: (sessionID: string) => Effect.Effect +} diff --git a/packages/plugin/src/v2/promise/context.ts b/packages/plugin/src/v2/promise/context.ts index 9089334ee3..64c7c9e622 100644 --- a/packages/plugin/src/v2/promise/context.ts +++ b/packages/plugin/src/v2/promise/context.ts @@ -6,6 +6,7 @@ import type { CommandHooks } from "./command.js" import type { IntegrationHooks } from "./integration.js" import type { PluginDomain } from "./plugin.js" import type { ReferenceHooks } from "./reference.js" +import type { SessionDomain } from "./session.js" import type { SkillHooks } from "./skill.js" import type { Reload } from "./registration.js" @@ -18,5 +19,6 @@ export interface PluginContext { readonly integration: IntegrationHooks & Reload readonly plugin: PluginDomain readonly reference: ReferenceHooks & Reload + readonly session: SessionDomain readonly skill: SkillHooks & Reload } diff --git a/packages/plugin/src/v2/promise/session.ts b/packages/plugin/src/v2/promise/session.ts new file mode 100644 index 0000000000..38064eebe1 --- /dev/null +++ b/packages/plugin/src/v2/promise/session.ts @@ -0,0 +1,29 @@ +import type { PromptInput, SessionInputAdmitted, SessionMessage, SessionV2Info } from "@opencode-ai/sdk/v2/types" + +export interface SessionDomain { + readonly create: (input: { + readonly id?: string + readonly parentID?: string + readonly title?: string + readonly agent?: string + readonly model?: SessionV2Info["model"] + }) => Promise + readonly get: (sessionID: string) => Promise + readonly messages: (input: { + readonly sessionID: string + readonly limit?: number + readonly order?: "asc" | "desc" + readonly cursor?: { readonly id: string; readonly direction: "previous" | "next" } + }) => Promise> + readonly context: (sessionID: string) => Promise> + readonly prompt: (input: { + readonly id?: string + readonly sessionID: string + readonly prompt: PromptInput + readonly delivery?: "steer" | "queue" + readonly resume?: boolean + }) => Promise + readonly resume: (sessionID: string) => Promise + readonly wait: (sessionID: string) => Promise + readonly interrupt: (sessionID: string) => Promise +} diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index ef0c34fe69..3426c2adfc 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -1,4 +1,6 @@ import { SessionV2 } from "@opencode-ai/core/session" +import { LocationServiceMap } from "@opencode-ai/core/location-service-map" +import { SessionRuntime } from "@opencode-ai/core/session/runtime" import { DateTime, Effect, Stream } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { Api } from "../api" @@ -20,6 +22,14 @@ const DefaultSessionHistoryLimit = 50 export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) => Effect.gen(function* () { const session = yield* SessionV2.Service + const locations = yield* LocationServiceMap.Service + const route = Effect.fn("SessionHandler.route")(function* ( + sessionID: SessionV2.ID, + effect: Effect.Effect, + ) { + const info = yield* session.get(sessionID) + return yield* effect.pipe(Effect.provide(locations.get(info.location))) + }) return handlers .handle( @@ -159,15 +169,18 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl "session.prompt", Effect.fn(function* (ctx) { return { - data: yield* session - .prompt({ - sessionID: ctx.params.sessionID, - id: ctx.payload.id, - prompt: ctx.payload.prompt, - delivery: ctx.payload.delivery, - resume: ctx.payload.resume, - }) - .pipe( + data: yield* route( + ctx.params.sessionID, + SessionRuntime.Service.use((runtime) => + runtime.prompt({ + sessionID: ctx.params.sessionID, + id: ctx.payload.id, + prompt: ctx.payload.prompt, + delivery: ctx.payload.delivery, + resume: ctx.payload.resume, + }), + ), + ).pipe( Effect.catchTag("Session.NotFoundError", (error) => Effect.fail( new SessionNotFoundError({ @@ -215,7 +228,10 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl .handle( "session.wait", Effect.fn(function* (ctx) { - yield* session.wait(ctx.params.sessionID).pipe( + yield* route( + ctx.params.sessionID, + SessionRuntime.Service.use((runtime) => runtime.wait(ctx.params.sessionID)), + ).pipe( Effect.catchTag("Session.NotFoundError", (error) => Effect.fail( new SessionNotFoundError({ @@ -237,7 +253,10 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl files: ctx.payload.files, }) return { - data: yield* session.revert.stage({ ...ctx.params, ...ctx.payload }).pipe( + data: yield* route( + ctx.params.sessionID, + SessionRuntime.Service.use((runtime) => runtime.revert.stage({ ...ctx.params, ...ctx.payload })), + ).pipe( Effect.catchTag( "Session.NotFoundError", (error) => @@ -284,7 +303,10 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl "session.revert.clear", Effect.fn(function* (ctx) { yield* Effect.log("session.revert.clear", { sessionID: ctx.params.sessionID }) - yield* session.revert.clear(ctx.params.sessionID).pipe( + yield* route( + ctx.params.sessionID, + SessionRuntime.Service.use((runtime) => runtime.revert.clear(ctx.params.sessionID)), + ).pipe( Effect.catchTag( "Session.NotFoundError", (error) => @@ -322,7 +344,10 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl "session.revert.commit", Effect.fn(function* (ctx) { yield* Effect.log("session.revert.commit", { sessionID: ctx.params.sessionID }) - yield* session.revert.commit(ctx.params.sessionID).pipe( + yield* route( + ctx.params.sessionID, + SessionRuntime.Service.use((runtime) => runtime.revert.commit(ctx.params.sessionID)), + ).pipe( Effect.catchTag( "Session.NotFoundError", (error) => @@ -407,7 +432,10 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl .handle( "session.interrupt", Effect.fn(function* (ctx) { - yield* session.interrupt(ctx.params.sessionID) + yield* route( + ctx.params.sessionID, + SessionRuntime.Service.use((runtime) => runtime.interrupt(ctx.params.sessionID)), + ) return HttpApiSchema.NoContent.make() }), ) diff --git a/specs/v2/plugin-session-tools-plan.md b/specs/v2/plugin-session-tools-plan.md index 3551bc55e1..a671113453 100644 --- a/specs/v2/plugin-session-tools-plan.md +++ b/specs/v2/plugin-session-tools-plan.md @@ -107,10 +107,10 @@ The SDK should be implemented as one plugin instance: it receives a plugin host/ ## Concrete implementation slices -1. Add `packages/core/src/session/runtime.ts` as a location node. -2. Move `prompt`, `resume`, `wait`, `interrupt`, `active`, and location-sensitive `revert` operations from `SessionV2.Service` into the runtime service. -3. Update server route handlers to route location-sensitive requests at the API boundary by resolving the session location and providing that location runtime. -4. Add `ctx.session` to `PluginHost` by composing `SessionV2.Service` and the location session runtime. +1. Add `packages/core/src/session/runtime.ts` as a location node. **Implemented in this draft.** +2. Move `prompt`, `resume`, `wait`, `interrupt`, `active`, and location-sensitive `revert` operations from `SessionV2.Service` into the runtime service. **Implemented in this draft for the new runtime path; old `SessionV2` entrypoints are left as compatibility stubs and should be removed once callers migrate.** +3. Update server route handlers to route location-sensitive requests at the API boundary by resolving the session location and providing that location runtime. **Implemented in this draft.** +4. Add `ctx.session` to `PluginHost` by composing `SessionV2.Service` and the location session runtime. **Implemented in this draft.** 5. Add public plugin `ctx.tool.transform` types and adapt it to the existing canonical core `Tool.make` representation. 6. Convert `ToolRegistry` registration to transform/rebuild semantics. 7. Port `subagent` to a built-in plugin that registers a normal location tool.