Skip to content

Commit fb6dd24

Browse files
deepagent-aiclaude
andcommitted
fix(goal): "转为 Goal" no longer crashes with [object Object] (#57)
## Summary Fixes the "[object Object] 失败" toast when clicking **转为 Goal** (start goal loop). This fix was authored after #56 was squash-merged, so it did not make it into that PR — re-submitting it cleanly on top of the latest `dev`. Two root causes: 1. **Backend crash (HTTP 500).** `goal-manager` start/resume did `provider.defaultModel().pipe(Effect.orDie)`. When the workspace has no *config-default* model — even though the user has a model selected for the session (e.g. GLM-5.2) — this threw `ProviderNoProvidersError`, and `.orDie` turned it into a defect → 500 `UnknownError`. Fix: `resolveGoalModel()` prefers the **session's** selected model (`session.model`), falls back to the config default, and if neither resolves returns a clean `InvalidGoalError` (400, readable reason) instead of dying. `resume()` degrades to "not resumed" rather than crashing. This also makes the goal run on the session's model, like normal turns. 2. **Frontend rendering ("[object Object]").** The raw SDK client throws the parsed error *body* (a plain object, not an `Error`) on non-2xx, so `String(err)` produced "[object Object]". `GoalStartButton` now extracts the message via an `errorMessage()` helper (`{message}` for 400, `{data:{message}}` for 500). ## Verification - deepagent-code + app typecheck exit 0; goal + agent suites pass. - Live: `goal/start` with no model now returns a clean 400 `"no model is configured…"` instead of a 500 `[object Object]`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: deepagent-ai <jamessmithm539@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ecae49f commit fb6dd24

5 files changed

Lines changed: 84 additions & 8 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<a href="README.zh.md">简体中文</a>
1414
</p>
1515

16+
<p align="center"><sub>Desktop v1.3</sub></p>
17+
1618
---
1719

1820
DeepAgent Code is an AI coding agent built on persistent document memory. It keeps [opencode](https://github.com/sst/opencode)'s runtime foundations and adds a control plane so the agent behaves less like a one-shot chat and more like a teammate that remembers your project, sharpens vague asks, and goes deep on hard problems.
@@ -45,6 +47,24 @@ The features below start from a real need — something a plain coding agent, op
4547

4648
**What DeepAgent does:** Two scenario modes on the composer. **Direct** sends your prompt as-is — you own the wording. **Intelligence** refines a rough ask into a sharper prompt, surfaces a draft plan and decision suggestions, and waits for your confirmation before it automates anything. You decide how much the agent shapes the request.
4749

50+
### Choose how much the agent drives — and who writes the plan
51+
52+
**The need:** Sometimes you want the agent to just take a request and run; sometimes you want to steer it with a plan you control; sometimes you've already written the plan and just want it executed faithfully.
53+
54+
**What DeepAgent does:** Three collaboration modes on the composer, picked from a single selector. **Auto** — the agent sets the objective, designs and plans as needed, and executes to completion. **Loop** — you describe the goal, the agent writes a `goal+plan.md` you can edit, then a supervised loop drives it to completion (plan → execute → verify per tick, with hard budget/step ceilings and objective completion checks). **Design** — you author `goal+plan.md` yourself and the agent executes your plan faithfully without redefining the goal. Orthogonal to mode, a permission control offers three presets — **Read-only**, **Request approval** (default), **Full access** — so autonomy and approval are separate, explicit choices.
55+
56+
### Get a second opinion before high-risk decisions
57+
58+
**The need:** Some decisions — a breaking migration, a security-sensitive change, an architecture call — deserve more than one confident pass agreeing with itself.
59+
60+
**What DeepAgent does:** Convene an **Expert Panel** from the composer. Differentiated expert lenses (correctness, security, performance, architecture, repro) review the same frozen question independently, debate anonymously, and a deterministic (non-LLM) arbiter aggregates a verdict — with minority opinions preserved and a fail-closed bias toward escalating to you when the panel can't safely agree.
61+
62+
### Read and govern what the agent knows
63+
64+
**The need:** Persistent memory is only trustworthy if you can see it and correct it.
65+
66+
**What DeepAgent does:** A **Repo & Wiki** view projects the four graphs into human-readable pages — browse and full-text-search the agent's knowledge, follow docs↔code cross-links, and edit governable Knowledge/Memory pages through the same evidence-gate the agent uses (Documents and Code stay read-only). A separate governance view lists learned facts grouped by project and global scope, so you approve what becomes durable.
67+
4868
### Go deep on genuinely hard problems
4969

5070
**The need:** Complex work — an architecture decision, a tricky migration, a subtle bug — needs more than a single confident pass. It needs research, a second opinion, and someone actively trying to poke holes.

README.zh.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<a href="README.zh.md">简体中文</a>
1414
</p>
1515

16+
<p align="center"><sub>桌面版 v1.3</sub></p>
17+
1618
---
1719

1820
DeepAgent Code 是一个构建在持久文档记忆之上的 AI 编程智能体。它保留了 [opencode](https://github.com/sst/opencode) 的运行时基座,并在其上叠加了一层控制平面——让智能体不再像一次性的对话工具,而更像一位记得住你项目、会替你打磨模糊需求、能对硬骨头深挖到底的队友。
@@ -45,6 +47,24 @@ DeepAgent Code 是一个构建在持久文档记忆之上的 AI 编程智能体
4547

4648
**DeepAgent 的做法:** 输入框上有两种情景模式。**直接**模式原样发送你的提示——措辞由你做主。**智能**模式会把粗糙的想法打磨成更精准的提示,给出草拟的方案和决策建议,并在自动执行任何操作前等你确认。智能体替你塑形到什么程度,由你决定。
4749

50+
### 选择智能体驱动的方式——以及由谁来写计划
51+
52+
**需求:** 有时你希望智能体拿到需求就自己跑;有时你想用一份由自己掌控的计划来引导它;有时你已经把计划写好了,只需要它忠实执行。
53+
54+
**DeepAgent 的做法:** 输入框上的协作模式选择器提供三档。**自动** ——智能体自行定目标、做计划、执行到完成。**目标** ——你说明需求,智能体生成一份你可以编辑的 `goal+plan.md`,再由监督循环驱动执行(计划→执行→验证逐步推进,有硬性预算/步数上限和客观完成判据)。**设计** ——你自己写好 `goal+plan.md`,智能体读取并忠实执行你的方案,不会重新定义目标。与协作模式正交,权限控制提供三个预设——**只读****请求批准**(默认)、**完全访问**——自主程度与审批方式是两个独立的、明确的选项。
55+
56+
### 在高风险决策前听一次会诊
57+
58+
**需求:** 有些决策——一次破坏性迁移、一个安全敏感的改动、一个架构抉择——值得不止一次自信的作答自我认同。
59+
60+
**DeepAgent 的做法:** 在输入框旁召集**专家团**。差异化的专家视角(正确性、安全、性能、架构、可复现)对同一个冻结问题各自独立审阅,匿名辩论,再由一个确定性(非 LLM)的仲裁者聚合裁定——保留少数派意见,并在专家团无法安全达成一致时偏向将决策权升级给你。
61+
62+
### 读懂并治理智能体所知道的
63+
64+
**需求:** 持久记忆只有在你能看见、能纠错的情况下才值得信任。
65+
66+
**DeepAgent 的做法:** **仓库与百科**视图把四张图投影成人类可读的页面——浏览并全文检索智能体的知识,跟随文档↔代码的交叉链接,并通过与智能体相同的证据门编辑可治理的知识/记忆页面(文档与代码页面只读)。单独的知识治理视图按项目和全局分组列出已学事实,由你审批哪些成为永久知识。
67+
4868
### 对真正的难题深挖到底
4969

5070
**需求:** 复杂的工作——一个架构决策、一次棘手的迁移、一个隐蔽的 bug——需要的不止一次自信的单程作答。它需要调研、需要第二意见、需要有人主动来挑刺。

packages/app/src/components/deepagent/goal-start-button.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ import { fetchCapabilities, startGoal, type PanelGoalClient } from "./panel-goal
1818
* Visibility: only when goalLoop is enabled (capability), a plan exists for this session, and no goal
1919
* is already active (once a goal starts, GoalStatusBar takes over via the goal.updated event).
2020
*/
21+
// The raw SDK client THROWS the parsed error BODY on a non-2xx response — which is a plain object,
22+
// not an Error. So `String(err)` yields "[object Object]". Extract a human-readable message from the
23+
// shapes the server returns: a 400 DeepAgentPromotionError `{message}`, or a 500 `{data:{message}}`.
24+
function errorMessage(err: unknown): string {
25+
if (err instanceof Error) return err.message
26+
if (err && typeof err === "object") {
27+
const o = err as { message?: unknown; data?: { message?: unknown } }
28+
if (typeof o.message === "string") return o.message
29+
if (o.data && typeof o.data.message === "string") return o.data.message
30+
}
31+
return String(err)
32+
}
33+
2134
export function GoalStartButton(props: { sessionID: string }) {
2235
const sdk = useSDK()
2336
const serverSync = useServerSync()
@@ -51,8 +64,7 @@ export function GoalStartButton(props: { sessionID: string }) {
5164
}
5265
// On success the goal.updated event drives GoalStatusBar to appear; nothing to do here.
5366
} catch (err) {
54-
const description = err instanceof Error ? err.message : String(err)
55-
showToast({ title: language.t("goal.start.failed"), description })
67+
showToast({ title: language.t("goal.start.failed"), description: errorMessage(err) })
5668
} finally {
5769
setBusy(false)
5870
}

packages/deepagent-code/src/session/goal-manager.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Effect, Layer, Context, SynchronizedRef } from "effect"
1+
import { Effect, Layer, Context, SynchronizedRef, Option } from "effect"
22
import path from "node:path"
33
import fs from "node:fs"
44
import { Global } from "@deepagent-code/core/global"
@@ -144,6 +144,27 @@ export const layer = Layer.effect(
144144
lenses: ["correctness", "security", "architecture"],
145145
})
146146

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+
147168
// Per-session control state, observed by the running driver's ports.
148169
const controls = yield* SynchronizedRef.make(new Map<string, GoalControl>())
149170

@@ -230,14 +251,14 @@ export const layer = Layer.effect(
230251
? GoalDriver.materializePlanDoc({ store, sessionId: sessionID, plan })
231252
: GoalDriver.goalPlanScope(sessionID) // no plan + no objective → startGoal rejects (no doc)
232253

233-
const model = yield* provider.defaultModel().pipe(Effect.orDie)
254+
const model = yield* resolveGoalModel(session)
234255

235256
const runTurn = makeTaskSubagentRunner({
236257
sessions,
237258
agents,
238259
sessionPrompt,
239260
parentSessionID: SessionID.make(sessionID),
240-
model: { providerID: model.providerID, modelID: model.modelID },
261+
model,
241262
})
242263

243264
const deps = yield* GoalLoopWiring.makeGoalLoopWiring({
@@ -334,14 +355,17 @@ export const layer = Layer.effect(
334355
// Re-drive: the persisted run_context doc resumes exactly where it paused. A fresh store handle
335356
// over the same root re-reads the loop state.
336357
const store = new DocumentStore(goalStoreRoot(sessionID))
337-
const model = yield* provider.defaultModel().pipe(Effect.orDie)
338358
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
339363
const runTurn = makeTaskSubagentRunner({
340364
sessions,
341365
agents,
342366
sessionPrompt,
343367
parentSessionID: SessionID.make(sessionID),
344-
model: { providerID: model.providerID, modelID: model.modelID },
368+
model,
345369
})
346370
const deps = yield* GoalLoopWiring.makeGoalLoopWiring({
347371
store,

packages/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@deepagent-code/desktop",
33
"private": true,
4-
"version": "1.2.0",
4+
"version": "1.3.0",
55
"type": "module",
66
"license": "AGPL-3.0-or-later",
77
"homepage": "https://deepagent-code.ai",

0 commit comments

Comments
 (0)