|
|
@@ -20,6 +20,7 @@ import { Plugin } from "../plugin"
|
|
|
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
|
|
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
|
|
import MAX_STEPS from "../session/prompt/max-steps.txt"
|
|
|
+import PROMPT_SUGGEST_NEXT from "../session/prompt/suggest-next.txt"
|
|
|
import { ToolRegistry } from "../tool/registry"
|
|
|
import { Runner } from "@/effect/runner"
|
|
|
import { MCP } from "../mcp"
|
|
|
@@ -249,6 +250,77 @@ export namespace SessionPrompt {
|
|
|
)
|
|
|
})
|
|
|
|
|
|
+ const suggest = Effect.fn("SessionPrompt.suggest")(function* (input: {
|
|
|
+ session: Session.Info
|
|
|
+ sessionID: SessionID
|
|
|
+ message: MessageV2.WithParts
|
|
|
+ }) {
|
|
|
+ if (input.session.parentID) return
|
|
|
+ const message = input.message.info
|
|
|
+ if (message.role !== "assistant") return
|
|
|
+ if (message.error) return
|
|
|
+ if (!message.finish) return
|
|
|
+ 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)
|
|
|
+ })
|
|
|
+ if (!model) return
|
|
|
+
|
|
|
+ const msgs = yield* Effect.promise(() => MessageV2.filterCompacted(MessageV2.stream(input.sessionID)))
|
|
|
+ const history = msgs.slice(-8)
|
|
|
+ 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
|
|
|
+
|
|
|
+ const text = yield* Effect.promise(async (signal) => {
|
|
|
+ const result = await LLM.stream({
|
|
|
+ agent: {
|
|
|
+ ...ag,
|
|
|
+ name: "suggest-next",
|
|
|
+ prompt: PROMPT_SUGGEST_NEXT,
|
|
|
+ },
|
|
|
+ user,
|
|
|
+ system: [],
|
|
|
+ small: true,
|
|
|
+ tools: {},
|
|
|
+ model,
|
|
|
+ abort: signal,
|
|
|
+ sessionID: input.sessionID,
|
|
|
+ retries: 1,
|
|
|
+ toolChoice: "none",
|
|
|
+ messages: await MessageV2.toModelMessages(history, model),
|
|
|
+ })
|
|
|
+ return result.text
|
|
|
+ })
|
|
|
+
|
|
|
+ const line = text
|
|
|
+ .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
|
|
|
+ .split("\n")
|
|
|
+ .map((item) => item.trim())
|
|
|
+ .find((item) => item.length > 0)
|
|
|
+ ?.replace(/^["'`]+|["'`]+$/g, "")
|
|
|
+ if (!line) return
|
|
|
+
|
|
|
+ const tag = line
|
|
|
+ .toUpperCase()
|
|
|
+ .replace(/[\s-]+/g, "_")
|
|
|
+ .replace(/[^A-Z_]/g, "")
|
|
|
+ if (tag === "NO_SUGGESTION") return
|
|
|
+
|
|
|
+ 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 })
|
|
|
+ })
|
|
|
+
|
|
|
const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: {
|
|
|
messages: MessageV2.WithParts[]
|
|
|
agent: Agent.Info
|
|
|
@@ -1319,7 +1391,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|
|
}
|
|
|
|
|
|
if (input.noReply === true) return message
|
|
|
- return yield* loop({ sessionID: input.sessionID })
|
|
|
+ const result = yield* loop({ sessionID: input.sessionID })
|
|
|
+ if (Flag.OPENCODE_EXPERIMENTAL_NEXT_PROMPT) {
|
|
|
+ yield* suggest({
|
|
|
+ session,
|
|
|
+ sessionID: input.sessionID,
|
|
|
+ message: result,
|
|
|
+ }).pipe(Effect.ignore, Effect.forkIn(scope))
|
|
|
+ }
|
|
|
+ return result
|
|
|
},
|
|
|
)
|
|
|
|