diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index d28a1988d7..55532a7946 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -29,7 +29,7 @@ import type { Provider } from "@/provider/provider" import { Permission } from "@/permission" import { Global } from "@/global" import type { LanguageModelV2Usage } from "@ai-sdk/provider" -import { Effect, Layer, Context } from "effect" +import { Effect, Layer, Option, Context } from "effect" import { makeRuntime } from "@/effect/run-service" export namespace Session { @@ -352,6 +352,11 @@ export namespace Session { field: string delta: string }) => Effect.Effect + /** Finds the first message matching the predicate, searching newest-first. */ + readonly findMessage: ( + sessionID: SessionID, + predicate: (msg: MessageV2.WithParts) => boolean, + ) => Effect.Effect> } export class Service extends Context.Service()("@opencode/Session") {} @@ -636,6 +641,17 @@ export namespace Session { yield* bus.publish(MessageV2.Event.PartDelta, input) }) + /** Finds the first message matching the predicate, searching newest-first. */ + const findMessage = Effect.fn("Session.findMessage")(function* ( + sessionID: SessionID, + predicate: (msg: MessageV2.WithParts) => boolean, + ) { + for (const item of MessageV2.stream(sessionID)) { + if (predicate(item)) return Option.some(item) + } + return Option.none() + }) + return Service.of({ create, fork, @@ -657,6 +673,7 @@ export namespace Session { updatePart, getPart, updatePartDelta, + findMessage, }) }), ) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index de26e03286..2b092fc8fe 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -902,12 +902,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the }) const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) { - const model = yield* Effect.promise(async () => { - for await (const item of MessageV2.stream(sessionID)) { - if (item.info.role === "user" && item.info.model) return item.info.model - } - }) - if (model) return model + const match = yield* sessions.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model) + if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model return yield* provider.defaultModel() }) @@ -1290,16 +1286,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, ) - const lastAssistant = (sessionID: SessionID) => - Effect.promise(async () => { - let latest: MessageV2.WithParts | undefined - for await (const item of MessageV2.stream(sessionID)) { - latest ??= item - if (item.info.role !== "user") return item - } - if (latest) return latest - throw new Error("Impossible") - }) + const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID) { + const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user") + if (Option.isSome(match)) return match.value + const msgs = yield* sessions.messages({ sessionID, limit: 1 }) + if (msgs.length > 0) return msgs[0] + throw new Error("Impossible") + }) const runLoop: (sessionID: SessionID) => Effect.Effect = Effect.fn("SessionPrompt.run")( function* (sessionID: SessionID) {