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 notificationspull/20309/head
parent
0c3ff84f44
commit
93cef701c0
|
|
@ -263,41 +263,44 @@ export namespace SessionPrompt {
|
||||||
if (["tool-calls", "unknown"].includes(message.finish)) return
|
if (["tool-calls", "unknown"].includes(message.finish)) return
|
||||||
if ((yield* status.get(input.sessionID)).type !== "idle") return
|
if ((yield* status.get(input.sessionID)).type !== "idle") return
|
||||||
|
|
||||||
const ag = yield* agents.get("title")
|
// Use the same model for prompt-cache hit on the conversation prefix
|
||||||
if (!ag) return
|
const model = yield* Effect.promise(async () =>
|
||||||
|
Provider.getModel(message.providerID, message.modelID).catch(() => undefined),
|
||||||
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)
|
|
||||||
})
|
|
||||||
if (!model) return
|
if (!model) return
|
||||||
|
|
||||||
const msgs = yield* Effect.promise(() => MessageV2.filterCompacted(MessageV2.stream(input.sessionID)))
|
const ag = yield* agents.get(message.agent ?? "code")
|
||||||
const history = msgs.slice(-8)
|
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) =>
|
const real = (item: MessageV2.WithParts) =>
|
||||||
item.info.role === "user" && !item.parts.every((part) => "synthetic" in part && part.synthetic)
|
item.info.role === "user" && !item.parts.every((part) => "synthetic" in part && part.synthetic)
|
||||||
const parent = msgs.find((item) => item.info.id === message.parentID)
|
const parent = msgs.find((item) => item.info.id === message.parentID)
|
||||||
const user = parent && real(parent) ? parent.info : msgs.findLast((item) => real(item))?.info
|
const user = parent && real(parent) ? parent.info : msgs.findLast((item) => real(item))?.info
|
||||||
if (!user || user.role !== "user") return
|
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 text = yield* Effect.promise(async (signal) => {
|
||||||
const result = await LLM.stream({
|
const result = await LLM.stream({
|
||||||
agent: {
|
agent: ag,
|
||||||
...ag,
|
|
||||||
name: "suggest-next",
|
|
||||||
prompt: PROMPT_SUGGEST_NEXT,
|
|
||||||
},
|
|
||||||
user,
|
user,
|
||||||
system: [],
|
system,
|
||||||
small: true,
|
small: false,
|
||||||
tools: {},
|
tools: {},
|
||||||
model,
|
model,
|
||||||
abort: signal,
|
abort: signal,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
retries: 1,
|
retries: 1,
|
||||||
toolChoice: "none",
|
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
|
return result.text
|
||||||
})
|
})
|
||||||
|
|
@ -318,7 +321,7 @@ export namespace SessionPrompt {
|
||||||
|
|
||||||
const suggestion = line.length > 240 ? line.slice(0, 237) + "..." : line
|
const suggestion = line.length > 240 ? line.slice(0, 237) + "..." : line
|
||||||
if ((yield* status.get(input.sessionID)).type !== "idle") return
|
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: {
|
const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export namespace SessionStatus {
|
||||||
readonly get: (sessionID: SessionID) => Effect.Effect<Info>
|
readonly get: (sessionID: SessionID) => Effect.Effect<Info>
|
||||||
readonly list: () => Effect.Effect<Map<SessionID, Info>>
|
readonly list: () => Effect.Effect<Map<SessionID, Info>>
|
||||||
readonly set: (sessionID: SessionID, status: Info) => Effect.Effect<void>
|
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") {}
|
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionStatus") {}
|
||||||
|
|
@ -82,7 +83,17 @@ export namespace SessionStatus {
|
||||||
data.set(sessionID, status)
|
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) {
|
export async function set(sessionID: SessionID, status: Info) {
|
||||||
return runPromise((svc) => svc.set(sessionID, status))
|
return runPromise((svc) => svc.set(sessionID, status))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function suggest(sessionID: SessionID, suggestion: string) {
|
||||||
|
return runPromise((svc) => svc.suggest(sessionID, suggestion))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue