diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index c97f9530cf..a339deecf8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -263,41 +263,44 @@ export namespace SessionPrompt { if (["tool-calls", "unknown"].includes(message.finish)) return if ((yield* status.get(input.sessionID)).type !== "idle") return - const ag = yield* agents.get("title") - if (!ag) return - - const model = yield* Effect.promise(async () => { - const small = await Provider.getSmallModel(message.providerID).catch(() => undefined) - if (small) return small - return Provider.getModel(message.providerID, message.modelID).catch(() => undefined) - }) + // Use the same model for prompt-cache hit on the conversation prefix + const model = yield* Effect.promise(async () => + Provider.getModel(message.providerID, message.modelID).catch(() => undefined), + ) if (!model) return - const msgs = yield* Effect.promise(() => MessageV2.filterCompacted(MessageV2.stream(input.sessionID))) - const history = msgs.slice(-8) + const ag = yield* agents.get(message.agent ?? "code") + if (!ag) return + + // Full message history so the cached KV from the main conversation is reused + const msgs = yield* MessageV2.filterCompactedEffect(input.sessionID) const real = (item: MessageV2.WithParts) => item.info.role === "user" && !item.parts.every((part) => "synthetic" in part && part.synthetic) const parent = msgs.find((item) => item.info.id === message.parentID) const user = parent && real(parent) ? parent.info : msgs.findLast((item) => real(item))?.info if (!user || user.role !== "user") return + // Rebuild system prompt identical to the main loop for cache hit + const skills = yield* Effect.promise(() => SystemPrompt.skills(ag)) + const env = yield* Effect.promise(() => SystemPrompt.environment(model)) + const instructions = yield* instruction.system().pipe(Effect.orDie) + const modelMsgs = yield* Effect.promise(() => MessageV2.toModelMessages(msgs, model)) + const system = [...env, ...(skills ? [skills] : []), ...instructions] + const text = yield* Effect.promise(async (signal) => { const result = await LLM.stream({ - agent: { - ...ag, - name: "suggest-next", - prompt: PROMPT_SUGGEST_NEXT, - }, + agent: ag, user, - system: [], - small: true, + system, + small: false, tools: {}, model, abort: signal, sessionID: input.sessionID, retries: 1, toolChoice: "none", - messages: await MessageV2.toModelMessages(history, model), + // Append suggestion instruction after the full conversation + messages: [...modelMsgs, { role: "user" as const, content: PROMPT_SUGGEST_NEXT }], }) return result.text }) @@ -318,7 +321,7 @@ export namespace SessionPrompt { const suggestion = line.length > 240 ? line.slice(0, 237) + "..." : line if ((yield* status.get(input.sessionID)).type !== "idle") return - yield* status.set(input.sessionID, { type: "idle", suggestion }) + yield* status.suggest(input.sessionID, suggestion) }) const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: { diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index 5b14764cee..6a85c77127 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -49,6 +49,7 @@ export namespace SessionStatus { readonly get: (sessionID: SessionID) => Effect.Effect readonly list: () => Effect.Effect> readonly set: (sessionID: SessionID, status: Info) => Effect.Effect + readonly suggest: (sessionID: SessionID, suggestion: string) => Effect.Effect } export class Service extends ServiceMap.Service()("@opencode/SessionStatus") {} @@ -82,7 +83,17 @@ export namespace SessionStatus { data.set(sessionID, status) }) - return Service.of({ get, list, set }) + const suggest = Effect.fn("SessionStatus.suggest")(function* (sessionID: SessionID, suggestion: string) { + const data = yield* InstanceState.get(state) + const current = data.get(sessionID) + if (current && current.type !== "idle") return + const status: Info = { type: "idle", suggestion } + // only publish Status so the TUI sees the suggestion; + // skip Event.Idle to avoid spurious plugin notifications + yield* bus.publish(Event.Status, { sessionID, status }) + }) + + return Service.of({ get, list, set, suggest }) }), ) @@ -100,4 +111,8 @@ export namespace SessionStatus { export async function set(sessionID: SessionID, status: Info) { return runPromise((svc) => svc.set(sessionID, status)) } + + export async function suggest(sessionID: SessionID, suggestion: string) { + return runPromise((svc) => svc.suggest(sessionID, suggestion)) + } }