qm runtime router
这一页专拆 QM 如何从 Web turn 请求一路解析到具体 harness/model,并让 worker 用租约语义执行后台 run。
从请求到 harness 的硬边界
src/api/app-turn.ts:127 先定义 fallback harness,src/api/app-turn.ts:136 解析 org runtime,src/api/app-turn.ts:141 再解析目标 scope runtime。它不是把 UI 选项直接传给模型,而是先经过 durable config。
src/harness/harness-router.ts:63 读取 approved harnesses,src/harness/harness-router.ts:70 构造同步 view 复用同一套 resolve 逻辑,这让 durable 和内存路径不会分叉。
▸RuntimeChoice 结构很小,语义很重
7export interface RuntimeChoice { 8 harnessId: HarnessId; 9 modelId: string; 10} 11 12export function resolveRuntimeChoice( 13 config: Pick<ScopedConfigStore, "getApprovedHarnesses" | "getRuntimeSelection" | "getBaseModel">, 14 orgScopeId: ScopeId, 15 scope: ScopeId, 16 fallback: RuntimeChoice, 17 requested?: Partial<RuntimeChoice>, 18): RuntimeChoice { 19 const approved = config.getApprovedHarnesses() ?? [fallback.harnessId]; 20 const orgStored = config.getRuntimeSelection(orgScopeId); 21 const orgLegacy = config.getBaseModel(orgScopeId); 22 const configuredOrg = 23 orgStored && isHarnessId(orgStored.harnessId) 24 ? { harnessId: orgStored.harnessId, modelId: orgStored.modelId } 25 : { harnessId: fallback.harnessId, modelId: orgLegacy ?? fallback.modelId }; 26 const firstApproved = approved.find(isHarnessId) ?? fallback.harnessId; 27 const safeFallback = 28 approved.includes(fallback.harnessId) && modelSupportedByHarness(fallback.modelId, fallback.harnessId) 29 ? fallback 30 : { harnessId: firstApproved, modelId: defaultModelForHarness(firstApproved, fallback.modelId) }; 31 const org = 32 approved.includes(configuredOrg.harnessId) && 33 modelSupportedByHarness(configuredOrg.modelId, configuredOrg.harnessId) 34 ? configuredOrg 35 : safeFallback; 36 const scopedStored = scope === orgScopeId ? null : config.getRuntimeSelection(scope); 37 const scopedLegacy = scope === orgScopeId ? null : config.getBaseModel(scope); 38 let inherited = org; 39 if (scopedStored && isHarnessId(scopedStored.harnessId)) { 40 inherited = { harnessId: scopedStored.harnessId, modelId: scopedStored.modelId }; 41 } else if (scopedLegacy) { 42 inherited = { harnessId: fallback.harnessId, modelId: scopedLegacy }; 43 } 44 const choice = 45 requested?.harnessId || requested?.modelId 46 ? { harnessId: requested.harnessId ?? inherited.harnessId, modelId: requested.modelId ?? inherited.modelId } 47 : inherited; 48 if (!approved.includes(choice.harnessId) || !modelSupportedByHarness(choice.modelId, choice.harnessId)) { 49 if (requested?.harnessId || requested?.modelId) 50 throw new NonRetryableTurnError(`runtime ${choice.harnessId}/${choice.modelId} is not approved`); 51 return org; 52 } 53 return choice; 54}
Adapter map 让多个 agent runtime 共存
src/harness/harness-router.ts:84 接收 adapters map,src/harness/harness-router.ts:95 在每个 turn 解析 choice,src/harness/harness-router.ts:105 把 harness/model 注入 runTurn。这个封装让 Codex、Claude、OpenCode、Pi 等 runtime 可以被同一层调度。
▸切换 harness 时主动 reset session
84export function createHarnessRouter( 85 adapters: ReadonlyMap<HarnessId, Harness>, 86 utility: Harness, 87 resolve: (input: HarnessTurnInput) => RuntimeChoice | Promise<RuntimeChoice>, 88): Harness { 89 const lastHarness = new Map<string, HarnessId>(); 90 return { 91 profile: utility.profile, 92 models: utility.models, 93 tools: utility.tools, 94 turns: { 95 async runTurn(input) { 96 const choice = await resolve(input); 97 const adapter = adapters.get(choice.harnessId); 98 if (!adapter) throw new Error(`harness ${choice.harnessId} is unavailable`); 99 const prior = lastHarness.get(input.session.id); 100 if (prior && prior !== choice.harnessId) { 101 await adapters.get(prior)?.turns.resetSession?.(input.session.id); 102 await adapter.turns.resetSession?.(input.session.id); 103 } 104 lastHarness.set(input.session.id, choice.harnessId); 105 return adapter.turns.runTurn({ ...input, harness: choice.harnessId, model: choice.modelId }); 106 }, 107 async resetSession(sessionId) { 108 lastHarness.delete(sessionId); 109 await Promise.all([...adapters.values()].map((adapter) => adapter.turns.resetSession?.(sessionId))); 110 }, 111 async close() { 112 await Promise.all([...new Set(adapters.values())].map((adapter) => adapter.turns.close?.())); 113 }, 114 }, 115 }; 116}
src/harness/harness-router.ts:99 读取上一轮 harness,src/harness/harness-router.ts:101 reset prior,src/harness/harness-router.ts:102 reset current。这个小细节防止模型运行时之间共享不兼容的 session 状态。
Web 入口同时检查成员、模型和 provider
src/api/app-turn.ts:108 处理 web thread,src/api/app-turn.ts:116 防止同一个 threadRef 被不同 scope 复用,src/api/app-turn.ts:151 拒绝非法 harness 字符串。这些都是模型调用前的系统边界。
▸Web turn runtime gate
122 const org = scopeId("org", orgIdOf()); 123 const targetScope = 124 req.conversation.kind === "dm" 125 ? scopeId("personal", actor.id) 126 : scopeId(req.conversation.kind, req.conversation.channelRef ?? threadRef); 127 const fallbackHarness = isHarnessId(deps.harnessId) ? deps.harnessId : "pi"; 128 const runtimeFallback = deps.runtimeFallback ?? { 129 harnessId: fallbackHarness, 130 modelId: defaultModelForHarness(fallbackHarness), 131 }; 132 let orgRuntime; 133 let configuredRuntime; 134 let runtime; 135 try { 136 orgRuntime = await resolveRuntimeChoiceDurable(deps.config, org, org, runtimeFallback); 137 configuredRuntime = 138 targetScope === org 139 ? orgRuntime 140 : await resolveRuntimeChoiceDurable(deps.config, org, targetScope, runtimeFallback); 141 runtime = 142 req.harness || req.model 143 ? await resolveRuntimeChoiceDurable(deps.config, org, targetScope, runtimeFallback, { 144 ...(req.harness && isHarnessId(req.harness) ? { harnessId: req.harness } : {}), 145 ...(req.model ? { modelId: req.model } : {}), 146 }) 147 : configuredRuntime; 148 } catch (error) { 149 return { status: "refused", reason: errMessage(error) }; 150 } 151 if (req.harness && !isHarnessId(req.harness)) { 152 return { status: "refused", reason: `runtime ${req.harness} is not approved` }; 153 } 154 const configuredKeys = deps.providerKeys ?? 155 deps.modelProviders ?? { anthropic: false, openai: false, openrouter: false }; 156 let providers = deps.modelProviders; 157 if (deps.modelCredentials) { 158 providers = modelProviderAvailabilityFor( 159 runtime.harnessId, 160 configuredKeys, 161 await deps.modelCredentials.availability(), 162 ); 163 } else if (deps.providerKeys) { 164 providers = modelProviderAvailabilityFor(runtime.harnessId, configuredKeys); 165 } 166 if (providers && !modelServiceable(runtime.modelId, providers)) { 167 return { 168 status: "refused", 169 reason: "that model isn't available on this deployment (its provider isn't configured)", 170 }; 171 } 172 const configuredWebuiModels = await deps.config.getWebuiModelsDurable(org); 173 let enabledWebuiModels: string[] | null = null; 174 if (configuredWebuiModels?.length) { 175 enabledWebuiModels = [...new Set([...configuredWebuiModels, orgRuntime.modelId])]; 176 } else if (providers?.openrouter) { 177 enabledWebuiModels = [ 178 ...new Set([ 179 ...selectableCatalogForHarness( 180 await selectableModelCatalog(deps.modelCredentialFetch), 181 runtime.harnessId, 182 ).map((model) => model.id), 183 ...(orgRuntime.harnessId === runtime.harnessId ? [orgRuntime.modelId] : []), 184 ]), 185 ]; 186 } 187 const invalidModelOption = 188 validateWebTurnModelOptions(req, enabledWebuiModels, providers) ?? 189 webTurnRuntimeModelRefusal(runtime.modelId, orgRuntime.modelId, configuredWebuiModels); 190 if (invalidModelOption) return { status: "refused", reason: invalidModelOption };
src/api/app-turn.ts:154 合并 provider keys,src/api/app-turn.ts:157 查询 model credential availability,src/api/app-turn.ts:166 拒绝 provider 未配置的模型。它把“用户想用哪个模型”和“部署真的能服务哪个模型”分开处理。
Worker 把 runtime choice 放进可靠执行循环
src/runs/worker.ts:59 计算 queueMs,src/runs/worker.ts:60 调 orchestrator.handleTurn,src/runs/worker.ts:71 complete run;异常路径在 src/runs/worker.ts:75 fail 并设置 retry。
▸processRun 的 heartbeat/cancel 结构
22export async function processRun(deps: ProcessDeps, run: Run, opts?: { background?: boolean }): Promise<TurnResult> { 23 const token = run.leaseToken; 24 if (token === null) throw new Error(`processRun called with an unleased run ${run.id}`); 25 const intervalMs = deps.heartbeatIntervalMs ?? Math.max(1_000, Math.floor(deps.leaseTtlMs / 3)); 26 const cancel = new AbortController(); 27 let consecutiveLost = 0; 28 let leaseLost = false; 29 const beat = setInterval(() => { 30 void deps.runs 31 .heartbeat(run.id, token, deps.leaseTtlMs) 32 .then((alive) => { 33 if (alive) { 34 consecutiveLost = 0; 35 return; 36 } 37 consecutiveLost += 1; 38 if (consecutiveLost >= LEASE_LOST_CONSECUTIVE && !leaseLost) { 39 leaseLost = true; 40 clearInterval(beat); 41 console.warn( 42 `[worker] run ${run.id} lost its lease after ${consecutiveLost} consecutive beats; cancelling the in-process turn`, 43 ); 44 cancel.abort(); 45 } 46 }) 47 .catch((err: unknown) => { 48 consecutiveLost = 0; 49 console.warn(`[worker] heartbeat failed for run ${run.id} (transient, ignored): ${errMessage(err)}`); 50 }); 51 }, intervalMs); 52 let beatStopped = false; 53 const stopBeat = (): void => { 54 if (beatStopped) return; 55 beatStopped = true; 56 clearInterval(beat); 57 }; 58 try { 59 const queueMs = run.startedAt !== null ? Math.max(0, run.startedAt - run.createdAt) : undefined; 60 const result = await deps.orchestrator.handleTurn({ 61 ...run.request, 62 origin: resolveTurnOrigin(run.request), 63 runId: run.id, 64 attempt: run.attempts, 65 finalAttempt: errorParks(run, deps.runs.maxClaims), 66 background: opts?.background ?? false, 67 cancel: cancel.signal, 68 ...(queueMs !== undefined ? { queueMs } : {}), 69 }); 70 stopBeat(); 71 await deps.runs.complete(run.id, token, result); 72 return result; 73 } catch (err) { 74 stopBeat(); 75 await deps.runs.fail(run.id, token, errMessage(err), { 76 retry: !(err instanceof NonRetryableTurnError), 77 }); 78 throw err; 79 } finally { 80 stopBeat(); 81 } 82}
src/runs/worker.ts:29 的 heartbeat loop 和 src/runs/worker.ts:44 的 cancel.abort 让 lease 丢失能中止本地 turn。对后台自动化来说,这比“进程还活着就继续跑”安全得多。
▸claim loop 的退避和释放
101export function createWorker(deps: WorkerDeps): Worker { 102 const workerId = deps.workerId ?? `w-${randomUUID().slice(0, 8)}`; 103 const pollMs = deps.pollMs ?? 50; 104 let stopped = false; 105 let loopDone: Promise<void> | null = null; 106 let inFlight: { runId: string; leaseToken: string; threadRef: string } | null = null; 107 let releasedLeaseToken: string | null = null; 108 let releasing: Promise<void> | null = null; 109 110 async function loop(): Promise<void> { 111 let claimFailures = 0; 112 while (!stopped) { 113 if (deps.canClaim && !deps.canClaim()) { 114 await sleep(pollMs); 115 continue; 116 } 117 let run: Run | null; 118 try { 119 run = await deps.runs.claim(workerId, deps.leaseTtlMs); 120 claimFailures = 0; 121 } catch (e) { 122 claimFailures += 1; 123 if (claimFailures >= CLAIM_FAIL_CRASH_CONSECUTIVE) throw e; 124 swallow("worker: claim failed (transient, retrying)", e); 125 await sleep(Math.min(pollMs * 2 ** Math.min(claimFailures, 5), 5_000)); 126 continue; 127 } 128 if (!run) { 129 await sleep(pollMs); 130 continue; 131 } 132 if (stopped) { 133 if (run.leaseToken !== null) 134 await deps.runs 135 .releaseLease(run.id, run.leaseToken) 136 .catch((e) => swallow("worker: post-stop claim handback failed", e)); 137 break; 138 } 139 inFlight = 140 run.leaseToken !== null ? { runId: run.id, leaseToken: run.leaseToken, threadRef: run.sessionId } : null; 141 deps.onClaimed?.(); 142 try { 143 await processRun(deps, run, { background: true }); 144 } catch (e) { 145 swallow("worker: background run crashed", e); 146 } finally { 147 inFlight = null; 148 } 149 } 150 } 151 152 return { 153 start() { 154 if (loopDone) return; 155 stopped = false; 156 loopDone = loop(); 157 }, 158 busy() { 159 return inFlight !== null; 160 }, 161 releaseInFlight() { 162 const held = inFlight; 163 if (!held || held.leaseToken === releasedLeaseToken) return Promise.resolve(); 164 if (releasing) return releasing; 165 releasing = (async () => { 166 try { 167 if (await deps.runs.heartbeat(held.runId, held.leaseToken, deps.leaseTtlMs)) { 168 const session = await deps.sessions.getByThread(held.threadRef); 169 if (session) await deps.sessions.forceReleaseLease(session.id); 170 await deps.runs.releaseLease(held.runId, held.leaseToken); 171 } 172 releasedLeaseToken = held.leaseToken; 173 } catch (e) { 174 swallow("worker: releaseInFlight failed", e); 175 } finally { 176 releasing = null; 177 } 178 })(); 179 return releasing; 180 }, 181 async stop(drainMs = STOP_DRAIN_MS) { 182 stopped = true; 183 await Promise.race([loopDone, sleep(drainMs, { unref: true })]); 184 loopDone = null; 185 }, 186 }; 187}
src/runs/worker.ts:119 claim,src/runs/worker.ts:125 exponential backoff,src/runs/worker.ts:161 releaseInFlight。三段合起来让 worker 更像队列消费者,而不是脆弱的定时轮询器。
边界条件
▸非法 runtime 是非重试错误
src/harness/harness-router.ts:49 在 requested runtime 非法时抛 NonRetryableTurnError,避免队列重复重试一个永远不会成功的用户选择。
▸Web thread 绑定 scope
src/api/app-turn.ts:110 读取 existing session,src/api/app-turn.ts:116 对比 claimed scope,能防止同一 browser thread 被拿来穿越 scope。
为什么这套路由值得学
▸模型选择是治理对象
src/api/app-turn.ts:172 读取 Web UI 模型配置,src/api/app-turn.ts:187 验证请求选项,这让模型列表可以面向部署动态变化。
风险
▸状态多,排障要靠可观测性
src/runs/worker.ts:41 只有 console.warn,真实生产中还需要把 lease lost、claim fail 和 runtime refusal 全部接到指标与审计。