主循环由持久化消息状态驱动,而不是一次性的 while(tool_call)
runLoop 每步从数据库重建过滤后的消息视图,找最后 user/assistant/finished 与待处理 subtask/compaction;只有已完成、无工具调用且顺序闭合才退出,否则继续调度。
它每一轮都重新看账本决定“接下来做什么”,所以进程中断、工具异步完成和压缩都能落在统一状态机里。
消息/part 是事实源,loop 是其投影;这比仅在内存追加数组更利于恢复和多客户端。
1081 const runLoop: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.run")(
1082 function* (sessionID: SessionID) {
1083 const ctx = yield* InstanceState.context
1084 let structured: unknown
1085 let step = 0
1086 const session = yield* sessions.get(sessionID).pipe(Effect.orDie)
1087
1088 while (true) {
1089 yield* status.set(sessionID, { type: "busy" })
1090 yield* Effect.logInfo("loop", { "session.id": sessionID, step })
1091
1092 let msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe(
1093 Effect.provideService(Database.Service, database),
1094 )
1095
1096 const { user: lastUser, assistant: lastAssistant, finished: lastFinished, tasks } = MessageV2.latest(msgs)
1097
1098 if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
1099
1100 const lastAssistantMsg = msgs.findLast(
1101 (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
1102 )
1103 // Some providers return "stop" even when the assistant message contains
1104 // tool calls. Keep the loop running so tool results can be sent back to
… 16 lines omitted; exact range 1081–1130 …
1121 yield* Effect.logWarning("loop exit with orphaned interrupted tool", {
1122 "session.id": sessionID,
1123 messageID: lastAssistant.id,
1124 tool: orphan.tool,
1125 callID: orphan.callID,
1126 })
1127 }
1128 yield* Effect.logInfo("exiting loop", { "session.id": sessionID })
1129 break
1130 }
查看全部 2 处证据
-
实现
packages/opencode/src/session/prompt.ts:1081–1130loop、状态恢复与退出判定。 -
实现
packages/opencode/src/session/message-v2.ts:578–600按单调 ID 找最新消息和未处理任务。