2
0
Эх сурвалжийг харах

fix(tui): address review feedback for next-prompt suggestion

- Send full chat history + system prompt instead of last 8 messages for
  prompt-cache hit on the conversation prefix
- Use the same model (not small) so the KV cache is shared
- Add SessionStatus.suggest() that publishes Status event without firing
  the Idle hook, avoiding spurious plugin notifications
Ryan Vogel 2 долоо хоног өмнө
parent
commit
93cef701c0

+ 22 - 19
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: {

+ 16 - 1
packages/opencode/src/session/status.ts

@@ -49,6 +49,7 @@ export namespace SessionStatus {
     readonly get: (sessionID: SessionID) => Effect.Effect<Info>
     readonly list: () => Effect.Effect<Map<SessionID, Info>>
     readonly set: (sessionID: SessionID, status: Info) => Effect.Effect<void>
+    readonly suggest: (sessionID: SessionID, suggestion: string) => Effect.Effect<void>
   }
 
   export class Service extends ServiceMap.Service<Service, Interface>()("@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))
+  }
 }