|
1 | | -import { Effect, Layer, Context, SynchronizedRef } from "effect" |
| 1 | +import { Effect, Layer, Context, SynchronizedRef, Option } from "effect" |
2 | 2 | import path from "node:path" |
3 | 3 | import fs from "node:fs" |
4 | 4 | import { Global } from "@deepagent-code/core/global" |
@@ -144,6 +144,27 @@ export const layer = Layer.effect( |
144 | 144 | lenses: ["correctness", "security", "architecture"], |
145 | 145 | }) |
146 | 146 |
|
| 147 | + // Resolve the model the goal loop should run with. Prefer the SESSION's selected model (what the |
| 148 | + // user picked in the composer — e.g. GLM-5.2), since a goal is a continuation of that conversation; |
| 149 | + // fall back to the config default. If NEITHER resolves, fail with a clean InvalidGoalError instead |
| 150 | + // of dying — otherwise a workspace with no config-default model turns the whole request into an |
| 151 | + // opaque 500 ("[object Object]") even though the user has a working model selected. |
| 152 | + const resolveGoalModel = (session: { model?: { providerID: string; id: string } }) => |
| 153 | + Effect.gen(function* () { |
| 154 | + if (session.model) { |
| 155 | + return { providerID: session.model.providerID, modelID: session.model.id } |
| 156 | + } |
| 157 | + const fallback = yield* provider.defaultModel().pipe(Effect.option) |
| 158 | + if (Option.isNone(fallback)) { |
| 159 | + return yield* Effect.fail( |
| 160 | + new InvalidGoalError({ |
| 161 | + reason: "no model is configured for this session — select a model, then start the goal", |
| 162 | + }), |
| 163 | + ) |
| 164 | + } |
| 165 | + return { providerID: fallback.value.providerID, modelID: fallback.value.modelID } |
| 166 | + }) |
| 167 | + |
147 | 168 | // Per-session control state, observed by the running driver's ports. |
148 | 169 | const controls = yield* SynchronizedRef.make(new Map<string, GoalControl>()) |
149 | 170 |
|
@@ -230,14 +251,14 @@ export const layer = Layer.effect( |
230 | 251 | ? GoalDriver.materializePlanDoc({ store, sessionId: sessionID, plan }) |
231 | 252 | : GoalDriver.goalPlanScope(sessionID) // no plan + no objective → startGoal rejects (no doc) |
232 | 253 |
|
233 | | - const model = yield* provider.defaultModel().pipe(Effect.orDie) |
| 254 | + const model = yield* resolveGoalModel(session) |
234 | 255 |
|
235 | 256 | const runTurn = makeTaskSubagentRunner({ |
236 | 257 | sessions, |
237 | 258 | agents, |
238 | 259 | sessionPrompt, |
239 | 260 | parentSessionID: SessionID.make(sessionID), |
240 | | - model: { providerID: model.providerID, modelID: model.modelID }, |
| 261 | + model, |
241 | 262 | }) |
242 | 263 |
|
243 | 264 | const deps = yield* GoalLoopWiring.makeGoalLoopWiring({ |
@@ -334,14 +355,17 @@ export const layer = Layer.effect( |
334 | 355 | // Re-drive: the persisted run_context doc resumes exactly where it paused. A fresh store handle |
335 | 356 | // over the same root re-reads the loop state. |
336 | 357 | const store = new DocumentStore(goalStoreRoot(sessionID)) |
337 | | - const model = yield* provider.defaultModel().pipe(Effect.orDie) |
338 | 358 | const session = yield* sessions.get(SessionID.make(sessionID)).pipe(Effect.orDie) |
| 359 | + // Resume can't run without a model; if none resolves, don't crash — just report "not resumed". |
| 360 | + const modelOpt = yield* resolveGoalModel(session).pipe(Effect.option) |
| 361 | + if (Option.isNone(modelOpt)) return false |
| 362 | + const model = modelOpt.value |
339 | 363 | const runTurn = makeTaskSubagentRunner({ |
340 | 364 | sessions, |
341 | 365 | agents, |
342 | 366 | sessionPrompt, |
343 | 367 | parentSessionID: SessionID.make(sessionID), |
344 | | - model: { providerID: model.providerID, modelID: model.modelID }, |
| 368 | + model, |
345 | 369 | }) |
346 | 370 | const deps = yield* GoalLoopWiring.makeGoalLoopWiring({ |
347 | 371 | store, |
|
0 commit comments