dsh-agy-link 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.8 (2026-08-19)
4
+
5
+ - **Fix: real agy 1.1.15 stream-json parsing.** The parser only understood
6
+ flat, hypothesized event shapes; the live CLI nests every step payload
7
+ under a `step_update` envelope and uses a different vocabulary
8
+ (`agent_response` + `text_delta` fragments, `tool` with `tool_name` /
9
+ `tool_info` with `parameters` / `output` / `error`). As a result thinking
10
+ and tool activity were silently dropped and only the final result text
11
+ survived. Both shapes are now parsed (legacy aliases kept).
12
+ - **New: visible thinking turns + tool activity.** agy does not stream
13
+ thinking text in print mode (only `thinking_tokens` usage), so each
14
+ thinking-only `agent_response` turn surfaces as an annotated reasoning
15
+ block (`[agy thinking turn · N thinking tokens]`), tool calls render as
16
+ `[agy tool: name] args -> output` reasoning annotations, and failed tools
17
+ render `[agy tool error: name] message`.
18
+ - **Fix: `result.status=ERROR` with a usable response** (e.g. a tool timed
19
+ out mid-run) no longer discards the answer — the response streams, the
20
+ error is annotated, the turn finishes normally. A bare envelope error
21
+ now maps to a precise `AGY_ERROR` finish instead of INVALID_OUTPUT.
22
+ - **Fix: `agy models` deadlock — stdin pipe never closed.** agy reads stdin
23
+ when it is a pipe and waits for EOF; every non-auth spawn kept the pipe
24
+ open, so model discovery silently timed out and the catalog always fell
25
+ back to the bundled list. Stdin now closes right after spawn (auth probes
26
+ keep it open).
27
+ - **Fix: auth phase was always `idle`** until someone ran `/agy auth`, so
28
+ the settings page claimed "needs Google login" for signed-in users.
29
+ Status surfaces now lazily probe the real login state via `agy models`
30
+ (60s cache) and report `ok` / `signed-out` truthfully.
31
+ - **Fix: `/plugins/agy-link/*` routes never registered on dsh web** (settings
32
+ page stuck on `binary: not found / auth: unknown / models: 0`). Routes are
33
+ now registered through a reactive `ctx.inject(['webServer'])` sub-fiber,
34
+ so they attach whenever the service appears instead of racing it. The
35
+ settings page also renders an honest "endpoint unreachable" state instead
36
+ of misleading placeholders when the route is missing.
37
+ - **Removed: sidebar footer AGY button** (user request). Login (QR +
38
+ authorization code) and mode/effort quick controls moved to Settings →
39
+ Antigravity; the conversation header `AGY` pill stays.
40
+ - Tests: real-1.1.15 fixture modes (`real`, `real-error`, `real-fail`) and
41
+ parser/mapper/adapter coverage for the nested format (73 tests).
42
+
3
43
  ## 0.2.7 (2026-08-19)
4
44
 
5
45
  - Visible agy status in DSH: conversation header `AGY` pill, Settings → Antigravity status page, and sidebar footer label.
package/README.md CHANGED
@@ -38,7 +38,7 @@
38
38
  | 🔗 **会话连续** | 每个 DSH 会话绑定原生 agy 会话(`--conversation`),多轮上下文由 agy 历史承载,不重发全量 |
39
39
  | 📊 **token 用量** | 输入/输出/思考/缓存 token 全部进入 DSH 用量统计 |
40
40
  | ⚙️ **设置页状态** | DSH 设置 → Antigravity 页面显示 agy 连接/登录/工作区/绑定数/最近运行状态 |
41
- | 🔐 **GUI 内 Google 登录** | `/agy auth` 打印授权 URL;侧栏面板提供二维码 + 授权码粘贴框 |
41
+ | 🔐 **GUI 内 Google 登录** | `/agy auth` 打印授权 URL;设置 → Antigravity 页面提供二维码 + 授权码粘贴框 |
42
42
  | 🤝 **`agy_ask` 工具** | 任何 DSH 模型都可以把一次性任务委托给 Antigravity 模型(AskAntigravity 模式) |
43
43
  | ⌨️ **`/agy` 命令族** | `status` / `auth` / `auth-code` / `models` / `mode` / `effort` / `workspace` / `clear` / `doctor` / `help` |
44
44
  | 🖼 **图片多模态(v0.2)** | 图片落盘到本地媒体目录(TTL 清理),prompt 以绝对路径引用 + `--add-dir` 授权,agy 用自己的工具看图 |
@@ -75,7 +75,7 @@ dsh plugin --profile web add dsh-agy-link
75
75
 
76
76
  # 3. 登录(一次性;终端里已经跑过 agy 登录过的可跳过):
77
77
  # /agy auth ← 打开授权 URL,批准并复制授权码
78
- # /agy auth-code <授权码> ← 或直接用侧栏面板的二维码 + 粘贴框
78
+ # /agy auth-code <授权码> ← 或直接用设置 → Antigravity 页面的二维码 + 粘贴框
79
79
 
80
80
  # 4. /model 选择器里选 antigravity 模型,开聊
81
81
  ```
@@ -111,7 +111,7 @@ DSH 侧的工具与权限不受影响——这里只约束 agy 子进程在自
111
111
 
112
112
  ## 🧩 工作原理
113
113
 
114
- 一次 DSH 模型调用 = 一个短生命周期 `agy -p --output-format stream-json` 进程。NDJSON 事件流被解析、归一化并映射为 DSH StreamChunk:思考reasoning 块,文本 → text 块,工具活动 → 注记 reasoning 块,结果信封 → usage + finish。会话 id 优先取自流本身,conversations 目录快照对比兜底。**不逆向数据库、不解码 protobuf、不碰 token 文件**——只启动官方未修改的 agy 二进制。
114
+ 一次 DSH 模型调用 = 一个短生命周期 `agy -p --output-format stream-json` 进程。NDJSON 事件流被解析、归一化并映射为 DSH StreamChunk:文本 → text 块,工具活动 → 注记 reasoning 块(含失败注记),结果信封 → usage + finish。agy 的 print 模式**不输出思考文本**(只有 `thinking_tokens` 计数),因此每个思考轮次以 `[agy thinking turn · N thinking tokens]` 注记呈现,思考 token 也进入用量统计。会话 id 优先取自流本身,conversations 目录快照对比兜底。**不逆向数据库、不解码 protobuf、不碰 token 文件**——只启动官方未修改的 agy 二进制。
115
115
 
116
116
  ## 📋 它做不到什么(诚实清单)
117
117
 
@@ -149,7 +149,7 @@ Bring **Google Antigravity models into DeepSeek Harness (DSH)** — chat, thinki
149
149
  | 🔗 **Session continuity** | Each DSH session binds to a native agy conversation (`--conversation`); multi-turn context rides agy history instead of re-sending everything |
150
150
  | 📊 **Token usage** | Input/output/thinking/cache tokens surface in DSH usage accounting |
151
151
  | ⚙️ **Settings status** | DSH Settings → Antigravity page shows agy connection/login/workspace/bindings/last-run state |
152
- | 🔐 **In-GUI Google login** | `/agy auth` prints the consent URL; the sidebar panel adds a QR code and a paste box for the authorization code |
152
+ | 🔐 **In-GUI Google login** | `/agy auth` prints the consent URL; Settings Antigravity adds a QR code and a paste box for the authorization code |
153
153
  | 🤝 **`agy_ask` tool** | Let any DSH model delegate a one-shot task to an Antigravity model (the AskAntigravity pattern) |
154
154
  | ⌨️ **`/agy` commands** | `status`, `auth`, `auth-code`, `models`, `mode`, `effort`, `workspace`, `clear`, `doctor`, `help` |
155
155
  | 😴 **Dormant-safe** | No agy binary? Not signed in? The plugin loads anyway and tells you what to fix |
@@ -186,7 +186,7 @@ dsh plugin --profile web add dsh-agy-link
186
186
 
187
187
  # 3. Login (one time; skip if you already ran agy in a terminal):
188
188
  # /agy auth <- open the consent URL, approve, copy the code
189
- # /agy auth-code <code> <- or use the sidebar panel (QR + paste box)
189
+ # /agy auth-code <code> <- or use Settings -> Antigravity (QR + paste box)
190
190
 
191
191
  # 4. Pick an antigravity model in /model and chat
192
192
  ```
package/dist/client.js CHANGED
@@ -6,10 +6,7 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  //#region src/client/index.ts
8
8
  const { createElement: h, useState, useEffect } = require("react");
9
- const reactDom = require("react-dom");
10
9
  const win = globalThis;
11
- const bodyEl = win.document?.body;
12
- const portalToBody = bodyEl != null && reactDom.createPortal ? (node) => reactDom.createPortal(node, bodyEl) : (node) => node;
13
10
  const name = "dsh-agy-link-client";
14
11
  const inject = ["slots"];
15
12
  const base = win.location?.origin ?? "";
@@ -73,15 +70,12 @@ window.__ModuleLoader__.load({
73
70
  }
74
71
  };
75
72
  function apply(ctx) {
76
- const AgyPanel = (props) => {
77
- const { wide = false } = props ?? {};
78
- const [open, setOpen] = useState(false);
73
+ const AgySettingsSection = () => {
79
74
  const [status, setStatus] = useState(null);
80
75
  const [code, setCode] = useState("");
81
76
  const [busy, setBusy] = useState(false);
82
77
  const [notice, setNotice] = useState("");
83
78
  useEffect(() => {
84
- if (!open) return;
85
79
  let alive = true;
86
80
  const tick = async () => {
87
81
  const st = await getStatus();
@@ -93,14 +87,15 @@ window.__ModuleLoader__.load({
93
87
  alive = false;
94
88
  clearInterval(timer);
95
89
  };
96
- }, [open]);
97
- const dot = status === null ? "#9aa0a6" : status.dormantReason ? "#f5a623" : status.auth?.phase === "pending" || status.auth?.phase === "failed" || status.auth?.phase === "submitting" ? "#f5a623" : status.lastRun && !status.lastRun.ok ? "#e5484d" : "#30a46c";
90
+ }, []);
91
+ const refresh = async () => {
92
+ setStatus(await getStatus());
93
+ };
98
94
  const startAuth = async () => {
99
95
  setBusy(true);
100
96
  setNotice("");
101
97
  await postJson("/plugins/agy-link/auth", {});
102
- const st = await getStatus();
103
- setStatus(st);
98
+ await refresh();
104
99
  setBusy(false);
105
100
  };
106
101
  const submitCode = async () => {
@@ -119,40 +114,17 @@ window.__ModuleLoader__.load({
119
114
  key,
120
115
  value
121
116
  });
122
- const st = await getStatus();
123
- setStatus(st);
117
+ await refresh();
124
118
  setBusy(false);
125
119
  };
126
- const badge = h("button", {
127
- type: "button",
128
- onClick: () => setOpen(!open),
129
- style: {
130
- background: "transparent",
131
- border: "none",
132
- cursor: "pointer",
133
- padding: "2px 6px",
134
- fontSize: "14px",
135
- lineHeight: 1
136
- },
137
- title: "Antigravity (agy) bridge"
138
- }, h("span", { style: {
139
- display: "inline-block",
140
- width: "9px",
141
- height: "9px",
142
- borderRadius: "50%",
143
- background: dot
144
- } }), wide ? h("span", { style: {
145
- marginLeft: "6px",
146
- fontSize: "12px",
147
- lineHeight: 1
148
- } }, "AGY") : null);
149
- if (!open) return badge;
150
- const needsAuth = status !== null && !status.dormantReason && status.auth !== void 0 && status.auth.phase !== "ok";
151
- const dormantLine = h("div", null, "Bridge dormant: ", status?.dormantReason ?? "");
120
+ const reachable = status !== null;
121
+ const summary = !reachable ? "Status endpoint unreachable — the plugin host did not answer /plugins/agy-link/status. Restart the DSH web server and check its logs." : status.dormantReason ? "Not ready — " + status.dormantReason : status.auth?.phase === "ok" ? "Ready — agy is connected" : "Needs Google login — start the login flow below, or run /agy auth";
122
+ const color = !reachable ? "#e5484d" : status.dormantReason ? "#f5a623" : status.auth?.phase === "ok" ? "#30a46c" : "#f5a623";
152
123
  const infoBlock = h("div", { style: {
153
124
  ...S.muted,
154
- lineHeight: 1.5
155
- } }, h("div", null, "agy: ", status?.bin ?? "not found", " — v", status?.version ?? "?"), h("div", null, "auth: ", status?.auth?.phase ?? "unknown"), h("div", null, "models: ", String(status?.catalog?.count ?? 0), " — ", status?.catalog?.source ?? ""), h("div", null, "workspace: ", status?.workspaceRoot ? status.workspaceRoot : "(session cwd / process cwd)"), h("div", null, "bindings: ", String(status?.bindings ?? 0)), h("div", null, "last run: ", status?.lastRun ? (status.lastRun.ok ? "ok" : status.lastRun.code) + " — " + status.lastRun.model : "none"));
125
+ lineHeight: 1.6,
126
+ margin: "6px 0 12px"
127
+ } }, h("div", null, "agy binary: ", status?.bin ?? "not found", status?.version ? " — v" + status.version : ""), h("div", null, "auth: ", status?.auth?.phase ?? "unknown"), h("div", null, "workspace: ", status?.workspaceRoot ? status.workspaceRoot : "(session cwd / process cwd)"), h("div", null, "models: ", String(status?.catalog?.count ?? 0), " — ", status?.catalog?.source ?? ""), h("div", null, "bindings: ", String(status?.bindings ?? 0)), h("div", null, "last run: ", status?.lastRun ? (status.lastRun.ok ? "ok" : status.lastRun.code) + " — " + status.lastRun.model : "none"));
156
128
  const modeRow = h("div", { style: {
157
129
  ...S.row,
158
130
  marginTop: "8px"
@@ -190,7 +162,10 @@ window.__ModuleLoader__.load({
190
162
  style: status?.defaultEffort === "" ? S.btnPrimary : S.btn,
191
163
  onClick: () => void setCfg("defaultEffort", "")
192
164
  }, "auto"));
193
- const skipWarn = h("div", { style: S.muted }, "skip runs agy tools with --dangerously-skip-permissions (no approval prompts). plan is the safe read-only default.");
165
+ const skipWarn = h("div", { style: {
166
+ ...S.muted,
167
+ marginBottom: "8px"
168
+ } }, "skip runs agy tools with --dangerously-skip-permissions (no approval prompts). plan is the safe read-only default.");
194
169
  const authBlock = h("div", null, h("div", { style: {
195
170
  fontWeight: 600,
196
171
  marginBottom: "4px"
@@ -219,53 +194,8 @@ window.__ModuleLoader__.load({
219
194
  disabled: busy,
220
195
  onClick: () => void startAuth()
221
196
  }, "Restart login")), notice !== "" ? h("div", null, notice) : null);
222
- const body = status?.dormantReason ? dormantLine : needsAuth ? authBlock : h("div", null, infoBlock, modeRow, effortRow, skipWarn);
223
- const header = h("div", { style: {
224
- display: "flex",
225
- justifyContent: "space-between",
226
- alignItems: "center",
227
- marginBottom: "8px"
228
- } }, h("span", { style: {
229
- fontWeight: 600,
230
- fontSize: "13px"
231
- } }, "Antigravity bridge"), h("button", {
232
- type: "button",
233
- style: S.btn,
234
- onClick: () => setOpen(false)
235
- }, "close"));
236
- const popover = h("div", { style: {
237
- position: "fixed",
238
- bottom: "56px",
239
- left: "16px",
240
- width: "300px",
241
- maxHeight: "70vh",
242
- overflowY: "auto",
243
- padding: "12px 14px",
244
- borderRadius: "10px",
245
- border: "1px solid rgba(128,128,128,0.35)",
246
- background: "rgba(28,28,32,0.98)",
247
- boxShadow: "0 8px 32px rgba(0,0,0,0.5)",
248
- zIndex: 1e3
249
- } }, header, body);
250
- return h("div", { style: { display: "contents" } }, badge, portalToBody(popover));
251
- };
252
- const AgySettingsSection = () => {
253
- const [status, setStatus] = useState(null);
254
- useEffect(() => {
255
- let alive = true;
256
- const tick = async () => {
257
- const st = await getStatus();
258
- if (alive) setStatus(st);
259
- };
260
- tick();
261
- const timer = setInterval(tick, 3e3);
262
- return () => {
263
- alive = false;
264
- clearInterval(timer);
265
- };
266
- }, []);
267
- const summary = status === null ? "Loading…" : status.dormantReason ? "Not ready — " + status.dormantReason : status.auth?.phase === "ok" ? "Ready — agy is connected" : "Needs Google login — open the sidebar dot or run /agy auth";
268
- const color = status === null ? "#9aa0a6" : status.dormantReason ? "#f5a623" : status.auth?.phase === "ok" ? "#30a46c" : "#f5a623";
197
+ const needsAuth = reachable && !status.dormantReason && status.auth !== void 0 && status.auth.phase !== "ok";
198
+ const controls = !reachable ? null : status.dormantReason ? h("div", { style: S.muted }, "Bridge dormant: ", status.dormantReason) : needsAuth ? authBlock : h("div", null, infoBlock, modeRow, effortRow, skipWarn);
269
199
  return h("div", { style: { lineHeight: 1.6 } }, h("div", { style: {
270
200
  display: "flex",
271
201
  alignItems: "center",
@@ -281,10 +211,7 @@ window.__ModuleLoader__.load({
281
211
  } }), "Antigravity (agy CLI)"), h("div", { style: {
282
212
  ...S.muted,
283
213
  margin: "6px 0 12px"
284
- } }, summary), h("div", { style: {
285
- ...S.muted,
286
- lineHeight: 1.6
287
- } }, h("div", null, "agy binary: ", status?.bin ?? "not found", status?.version ? " — v" + status.version : ""), h("div", null, "auth: ", status?.auth?.phase ?? "unknown"), h("div", null, "workspace: ", status?.workspaceRoot ? status.workspaceRoot : "(session cwd / process cwd)"), h("div", null, "models: ", String(status?.catalog?.count ?? 0), " — ", status?.catalog?.source ?? ""), h("div", null, "bindings: ", String(status?.bindings ?? 0)), h("div", null, "last run: ", status?.lastRun ? (status.lastRun.ok ? "ok" : status.lastRun.code) + " — " + status.lastRun.model : "none")), h("div", { style: S.muted }, "Run `/agy status` for full diagnostics, or use the sidebar dot for quick login/controls."));
214
+ } }, summary), controls, h("div", { style: S.muted }, "Run `/agy status` for full diagnostics, `/agy doctor` to export a report."));
288
215
  };
289
216
  const AgySessionStatus = () => {
290
217
  const [status, setStatus] = useState(null);
@@ -327,14 +254,6 @@ window.__ModuleLoader__.load({
327
254
  background: color
328
255
  } }), "AGY");
329
256
  };
330
- ctx.slots.inject("sidebar.footer.action", () => {
331
- return ctx.slots.register({
332
- name: "sidebar.footer.action",
333
- id: "agy-link-entry",
334
- order: 30,
335
- label: "Antigravity"
336
- }, AgyPanel);
337
- });
338
257
  ctx.slots.inject("conversation.session.header.actions", () => {
339
258
  return ctx.slots.register({
340
259
  name: "conversation.session.header.actions",
package/dist/index.js CHANGED
@@ -349,6 +349,7 @@ var EventMapper = class {
349
349
  openAcc = "";
350
350
  emittedByKey = /* @__PURE__ */ new Map();
351
351
  toolPhaseSeen = /* @__PURE__ */ new Set();
352
+ thinkingAnnounced = /* @__PURE__ */ new Set();
352
353
  sawTextStep = false;
353
354
  finished = false;
354
355
  constructor(opts = {}) {
@@ -408,11 +409,27 @@ var EventMapper = class {
408
409
  if (ev.kind === "garbage") return;
409
410
  if (ev.kind === "step") {
410
411
  if (ev.stepKind === "text") {
412
+ const thoughtTokens = ev.usage?.thinking_tokens ?? 0;
413
+ if (ev.text === "" && thoughtTokens > 0 && !this.thinkingAnnounced.has(ev.stepKey)) {
414
+ this.thinkingAnnounced.add(ev.stepKey);
415
+ yield* this.ensureBlock("reasoning");
416
+ const d = this.appendDelta("[agy thinking turn · " + thoughtTokens + " thinking tokens]\n");
417
+ if (d) yield d;
418
+ return;
419
+ }
420
+ if (ev.text === "" && !ev.fragment) return;
411
421
  this.sawTextStep = true;
412
422
  yield* this.ensureBlock("text");
413
- const delta = suffixDelta(this.emittedByKey.get(ev.stepKey) ?? "", ev.text);
414
- this.emittedByKey.set(ev.stepKey, ev.text);
415
- const d = this.appendDelta(delta);
423
+ let d;
424
+ if (ev.fragment === true) {
425
+ const acc = (this.emittedByKey.get(ev.stepKey) ?? "") + ev.text;
426
+ this.emittedByKey.set(ev.stepKey, acc);
427
+ d = this.appendDelta(ev.text);
428
+ } else {
429
+ const delta = suffixDelta(this.emittedByKey.get(ev.stepKey) ?? "", ev.text);
430
+ this.emittedByKey.set(ev.stepKey, ev.text);
431
+ d = this.appendDelta(delta);
432
+ }
416
433
  if (d) yield d;
417
434
  return;
418
435
  }
@@ -432,6 +449,12 @@ var EventMapper = class {
432
449
  if (d) yield d;
433
450
  }
434
451
  const outKey = ev.stepKey + ":o";
452
+ const errKey = ev.stepKey + ":e";
453
+ if (ev.tool.error !== void 0 && !this.toolPhaseSeen.has(errKey)) {
454
+ this.toolPhaseSeen.add(errKey);
455
+ const d = this.appendDelta("[agy tool error: " + ev.tool.name + "] " + ev.tool.error + "\n");
456
+ if (d) yield d;
457
+ }
435
458
  if (ev.tool.output !== void 0 && !this.toolPhaseSeen.has(outKey)) {
436
459
  this.toolPhaseSeen.add(outKey);
437
460
  const rendered = this.opts.toolOutput ? this.opts.toolOutput(ev.tool.name, ev.tool.args, ev.tool.output) : defaultOutput(ev.tool.output);
@@ -448,7 +471,32 @@ var EventMapper = class {
448
471
  }
449
472
  return;
450
473
  }
451
- if (!ev.ok) return;
474
+ if (!ev.ok) {
475
+ if (ev.response === "") return;
476
+ if (!this.sawTextStep) {
477
+ yield* this.ensureBlock("text");
478
+ const d = this.appendDelta(ev.response);
479
+ if (d) yield d;
480
+ }
481
+ if (ev.error !== void 0 && ev.error !== "") {
482
+ yield* this.ensureBlock("reasoning");
483
+ const d = this.appendDelta("[agy finished with error] " + ev.error + "\n");
484
+ if (d) yield d;
485
+ }
486
+ const closeErr = this.closeOpen();
487
+ if (closeErr) yield closeErr;
488
+ yield {
489
+ type: "usage",
490
+ usage: usageFromRaw(ev.usage)
491
+ };
492
+ yield {
493
+ type: "finish",
494
+ reason: { kind: "stop" },
495
+ ...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
496
+ };
497
+ this.finished = true;
498
+ return;
499
+ }
452
500
  if (!this.sawTextStep && ev.response !== "") {
453
501
  yield* this.ensureBlock("text");
454
502
  const d = this.appendDelta(ev.response);
@@ -512,7 +560,7 @@ function parseModelsOutput(stdout) {
512
560
  for (const line of text.split(/\n/)) {
513
561
  const t = line.trim();
514
562
  if (t === "" || t.startsWith("Fetching") || t.startsWith("Error")) continue;
515
- const m = t.match(/^(\S+)\s{2,}(.+)$/);
563
+ const m = t.match(/^(\S+)(?:\t+|\s{2,})(.+)$/);
516
564
  if (m && m[1] !== void 0 && m[2] !== void 0) out.push({
517
565
  slug: m[1],
518
566
  label: m[2].trim()
@@ -717,7 +765,9 @@ const TEXT_STEP_TYPES = /* @__PURE__ */ new Set([
717
765
  "model_response",
718
766
  "modelresponse",
719
767
  "message",
720
- "response_text"
768
+ "response_text",
769
+ "agent_response",
770
+ "agentresponse"
721
771
  ]);
722
772
  const THINKING_STEP_TYPES = /* @__PURE__ */ new Set([
723
773
  "thinking",
@@ -802,6 +852,11 @@ function extractText(obj) {
802
852
  }
803
853
  return "";
804
854
  }
855
+ /** agy ≥1.1.15 streams assistant text as `text_delta` fragments. */
856
+ function extractTextDelta(obj) {
857
+ const v = obj.text_delta ?? obj.textDelta;
858
+ return typeof v === "string" ? v : void 0;
859
+ }
805
860
  function extractTool(obj) {
806
861
  const info = pick(obj, [
807
862
  "tool_info",
@@ -818,21 +873,31 @@ function extractTool(obj) {
818
873
  "canonical_name"
819
874
  ]);
820
875
  if (typeof name !== "string" || name === "") return void 0;
876
+ const args = pick(src, [
877
+ "parameters",
878
+ "params",
879
+ "input",
880
+ "args",
881
+ "input_json",
882
+ "inputJson"
883
+ ]);
884
+ const output = pick(src, [
885
+ "output",
886
+ "result",
887
+ "output_text"
888
+ ]);
889
+ const errObj = src.error;
890
+ let error;
891
+ if (typeof errObj === "string") error = errObj;
892
+ else if (errObj && typeof errObj === "object") {
893
+ const msg = errObj.message;
894
+ if (typeof msg === "string" && msg !== "") error = msg;
895
+ }
821
896
  return {
822
897
  name,
823
- args: pick(src, [
824
- "parameters",
825
- "params",
826
- "input",
827
- "args",
828
- "input_json",
829
- "inputJson"
830
- ]),
831
- output: pick(src, [
832
- "output",
833
- "result",
834
- "output_text"
835
- ])
898
+ args,
899
+ output,
900
+ ...error !== void 0 ? { error } : {}
836
901
  };
837
902
  }
838
903
  function parseUsage(v) {
@@ -866,20 +931,36 @@ function classifyEvent(obj, seq) {
866
931
  "session_id",
867
932
  "sessionId"
868
933
  ]);
934
+ const initObj = o.init;
935
+ const initSrc = initObj && typeof initObj === "object" && !Array.isArray(initObj) ? initObj : {};
869
936
  const model = pick(o, [
870
937
  "model",
871
938
  "model_name",
872
939
  "modelName"
940
+ ]) ?? pick(initSrc, [
941
+ "model",
942
+ "model_name",
943
+ "modelName"
873
944
  ]);
945
+ let convId = typeof cid === "string" && cid !== "" ? cid : void 0;
946
+ if (convId === void 0) {
947
+ const nested = pick(initSrc, ["conversation_id", "conversationId"]);
948
+ if (typeof nested === "string" && nested !== "") convId = nested;
949
+ }
874
950
  return {
875
951
  kind: "init",
876
- ...typeof cid === "string" && cid !== "" ? { conversationId: cid } : {},
952
+ ...convId !== void 0 ? { conversationId: convId } : {},
877
953
  ...typeof model === "string" ? { model } : {},
878
954
  raw: o
879
955
  };
880
956
  }
881
957
  if (evt === "step_update" || evt === "step" || evt === "stepUpdate") {
882
- const idxV = pick(o, [
958
+ const nested = o.step_update ?? o.stepUpdate ?? o.step;
959
+ const su = nested && typeof nested === "object" && !Array.isArray(nested) ? nested : {};
960
+ const src = Object.keys(su).length > 0 ? su : o;
961
+ const idxV = pick(src, [
962
+ "step_index",
963
+ "stepIndex",
883
964
  "idx",
884
965
  "index",
885
966
  "step_idx",
@@ -889,18 +970,25 @@ function classifyEvent(obj, seq) {
889
970
  "stepId"
890
971
  ]);
891
972
  const stepKey = typeof idxV === "number" || typeof idxV === "string" ? String(idxV) : String(seq);
892
- const stepKind = normalizeStepKind(pick(o, [
973
+ const stepKind = normalizeStepKind(pick(src, [
893
974
  "step_type",
894
975
  "stepType",
895
976
  "type"
896
977
  ]));
897
- const toolInfo = stepKind === "tool" ? extractTool(o) : void 0;
978
+ const toolInfo = stepKind === "tool" ? extractTool(src) : void 0;
979
+ const delta = extractTextDelta(src);
980
+ const text = delta !== void 0 ? delta : extractText(src);
981
+ const stateV = pick(src, ["state"]);
982
+ const usageRaw = pick(src, ["usage"]);
898
983
  return {
899
984
  kind: "step",
900
985
  stepKey,
901
986
  stepKind,
902
- text: extractText(o),
987
+ text,
988
+ ...delta !== void 0 ? { fragment: true } : {},
903
989
  ...toolInfo !== void 0 ? { tool: toolInfo } : {},
990
+ ...typeof stateV === "string" ? { state: stateV } : {},
991
+ ...usageRaw && typeof usageRaw === "object" ? { usage: parseUsage(usageRaw) } : {},
904
992
  raw: o
905
993
  };
906
994
  }
@@ -938,6 +1026,10 @@ function classifyEvent(obj, seq) {
938
1026
  event: "step_update",
939
1027
  ...o
940
1028
  }, seq);
1029
+ if ((o.step_update !== void 0 || o.stepUpdate !== void 0) && (o.step_update ?? o.stepUpdate) !== null && typeof (o.step_update ?? o.stepUpdate) === "object") return classifyEvent({
1030
+ event: "step_update",
1031
+ step_update: o.step_update ?? o.stepUpdate
1032
+ }, seq);
941
1033
  }
942
1034
  var StreamJsonParser = class {
943
1035
  buffer = "";
@@ -947,7 +1039,8 @@ var StreamJsonParser = class {
947
1039
  garbage: 0,
948
1040
  consecutiveGarbage: 0,
949
1041
  authUrl: void 0,
950
- sawAuthFailure: false
1042
+ sawAuthFailure: false,
1043
+ lastResultError: void 0
951
1044
  };
952
1045
  /** Ring buffer of raw stdout lines for /agy doctor export. */
953
1046
  recentLines = [];
@@ -1003,6 +1096,7 @@ var StreamJsonParser = class {
1003
1096
  line
1004
1097
  };
1005
1098
  }
1099
+ if (ev.kind === "result") this.stats.lastResultError = ev.error ?? void 0;
1006
1100
  this.stats.consecutiveGarbage = 0;
1007
1101
  return ev;
1008
1102
  }
@@ -1225,6 +1319,9 @@ function startAgyProcess(opts) {
1225
1319
  let timedOut = false;
1226
1320
  let aborted = false;
1227
1321
  let settled = false;
1322
+ if (!opts.keepStdin) try {
1323
+ child.stdin?.end();
1324
+ } catch {}
1228
1325
  const watchdog = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
1229
1326
  timedOut = true;
1230
1327
  killTree(child);
@@ -1263,9 +1360,6 @@ function startAgyProcess(opts) {
1263
1360
  opts.onLine?.(pending);
1264
1361
  pending = "";
1265
1362
  }
1266
- if (!opts.keepStdin) try {
1267
- child.stdin?.end();
1268
- } catch {}
1269
1363
  resolve({
1270
1364
  code,
1271
1365
  signal,
@@ -1597,6 +1691,11 @@ var AgyAdapter = class extends LlmAdapter {
1597
1691
  code: Err.PROCESS_EXIT,
1598
1692
  message: "agy exited with code " + outcome.code + (outcome.stderrTail !== "" ? ": " + brief(outcome.stderrTail) : "")
1599
1693
  };
1694
+ else if (parser.stats.lastResultError) failure = {
1695
+ kind: "error",
1696
+ code: Err.AGY_ERROR,
1697
+ message: "agy reported an error: " + parser.stats.lastResultError
1698
+ };
1600
1699
  else failure = {
1601
1700
  kind: "error",
1602
1701
  code: Err.INVALID_OUTPUT,
@@ -1907,12 +2006,63 @@ var AuthHelper = class {
1907
2006
  capturedUrl = null;
1908
2007
  urlWaiter = null;
1909
2008
  urlTimer = null;
2009
+ signedInCache = null;
2010
+ signedInFlight = null;
1910
2011
  constructor(bin) {
1911
2012
  this.bin = bin;
1912
2013
  }
1913
2014
  status() {
1914
2015
  return { ...this.state };
1915
2016
  }
2017
+ /**
2018
+ * Ground-truth login probe: `agy models` succeeds only when signed in and
2019
+ * prints a sign-in error otherwise. Cached for 60s; in-flight calls share
2020
+ * one spawn. Returns true/false, or null when undeterminable (no binary,
2021
+ * spawn failure, ambiguous output). While a login flow is active the
2022
+ * in-memory phase wins and the probe is skipped.
2023
+ */
2024
+ async probeSignedIn(force = false) {
2025
+ if (this.state.phase === "pending" || this.state.phase === "submitting") return null;
2026
+ if (this.state.phase === "ok") return true;
2027
+ const now = Date.now();
2028
+ if (!force && this.signedInCache !== null && now - this.signedInCache.at < 6e4) return this.signedInCache.value;
2029
+ if (this.signedInFlight !== null) return this.signedInFlight;
2030
+ const bin = this.bin();
2031
+ if (!bin) return null;
2032
+ this.signedInFlight = (async () => {
2033
+ let value = null;
2034
+ try {
2035
+ const out = await probeProcess(bin, ["models"], 15e3);
2036
+ const tail = out.stderrTail + "\n" + out.stdout.slice(0, 2e3);
2037
+ if (out.code === 0 && !looksLikeAuthFailure(out.stdout)) value = true;
2038
+ else if (out.code !== 0 && looksLikeAuthFailure(tail)) value = false;
2039
+ } catch {
2040
+ value = null;
2041
+ }
2042
+ this.signedInCache = {
2043
+ value,
2044
+ at: Date.now()
2045
+ };
2046
+ this.signedInFlight = null;
2047
+ return value;
2048
+ })();
2049
+ return this.signedInFlight;
2050
+ }
2051
+ /** status() enriched with a lazily probed login state for idle phases. */
2052
+ async resolvedStatus() {
2053
+ const st = this.status();
2054
+ if (st.phase !== "idle") return st;
2055
+ const signedIn = await this.probeSignedIn();
2056
+ if (signedIn === true) return {
2057
+ phase: "ok",
2058
+ message: "signed in (probed via agy models)"
2059
+ };
2060
+ if (signedIn === false) return {
2061
+ phase: "signed-out",
2062
+ message: "agy is not signed in — run /agy auth"
2063
+ };
2064
+ return st;
2065
+ }
1916
2066
  startProbe() {
1917
2067
  const bin = this.bin();
1918
2068
  if (!bin) return null;
@@ -2090,7 +2240,7 @@ async function handle(deps, raw) {
2090
2240
  const sub = parts[0] ?? "help";
2091
2241
  const arg = parts[1] ?? "";
2092
2242
  try {
2093
- if (sub === "status") return ok(renderStatus(deps));
2243
+ if (sub === "status") return ok(await renderStatus(deps));
2094
2244
  if (sub === "auth") {
2095
2245
  const auth = deps.auth();
2096
2246
  if (!auth) return err("agy binary not found — install the CLI first");
@@ -2163,10 +2313,11 @@ async function handle(deps, raw) {
2163
2313
  return err(e instanceof Error ? e.message : String(e));
2164
2314
  }
2165
2315
  }
2166
- function renderStatus(deps) {
2316
+ async function renderStatus(deps) {
2167
2317
  const cfg = deps.cfg();
2168
2318
  const bin = deps.bin();
2169
- const auth = deps.auth()?.status();
2319
+ const authHelper = deps.auth();
2320
+ const auth = authHelper ? await authHelper.resolvedStatus() : void 0;
2170
2321
  const cat = deps.catalog().get();
2171
2322
  const bindings = Object.keys(deps.store().all()).length;
2172
2323
  const last = deps.lastRun();
@@ -2707,60 +2858,66 @@ function apply(ctx, entryConfig = {}) {
2707
2858
  }
2708
2859
  };
2709
2860
  syncAskTool();
2710
- const webServer = ctx.get("webServer");
2711
- const sendJson = (res, status, body) => {
2712
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
2713
- res.end(JSON.stringify(body));
2714
- };
2715
- const readBody = (req) => {
2716
- const r = req;
2717
- return new Promise((resolve) => {
2718
- const chunks = [];
2719
- r.on?.("data", (c) => chunks.push(c));
2720
- r.on?.("end", () => {
2721
- try {
2722
- const v = JSON.parse(Buffer.concat(chunks).toString("utf8"));
2723
- resolve(v && typeof v === "object" ? v : {});
2724
- } catch {
2725
- resolve({});
2726
- }
2861
+ const registerRoutes = (webServer) => {
2862
+ const disposers = [];
2863
+ const reg = (route) => {
2864
+ const d = webServer.register(route);
2865
+ if (typeof d === "function") disposers.push(d);
2866
+ };
2867
+ const sendJson = (res, status, body) => {
2868
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
2869
+ res.end(JSON.stringify(body));
2870
+ };
2871
+ const readBody = (req) => {
2872
+ const r = req;
2873
+ return new Promise((resolve) => {
2874
+ const chunks = [];
2875
+ r.on?.("data", (c) => chunks.push(c));
2876
+ r.on?.("end", () => {
2877
+ try {
2878
+ const v = JSON.parse(Buffer.concat(chunks).toString("utf8"));
2879
+ resolve(v && typeof v === "object" ? v : {});
2880
+ } catch {
2881
+ resolve({});
2882
+ }
2883
+ });
2727
2884
  });
2728
- });
2729
- };
2730
- const methodOf = (req) => {
2731
- const m = req.method;
2732
- return typeof m === "string" ? m.toUpperCase() : "GET";
2733
- };
2734
- if (webServer) {
2735
- webServer.register({
2885
+ };
2886
+ const methodOf = (req) => {
2887
+ const m = req.method;
2888
+ return typeof m === "string" ? m.toUpperCase() : "GET";
2889
+ };
2890
+ reg({
2736
2891
  kind: "exact",
2737
2892
  path: "/plugins/agy-link/status",
2738
2893
  handler: (_req, res) => {
2739
- const cfg = getConfig();
2740
- const cat = catalog.get();
2741
- sendJson(res, 200, {
2742
- plugin: "dsh-agy-link",
2743
- bin: bin(),
2744
- version: versionCache,
2745
- dormantReason,
2746
- enabled: cfg.enabled,
2747
- permissionMode: cfg.permissionMode,
2748
- workspaceRoot: cfg.workspaceRoot,
2749
- defaultModel: cfg.defaultModel,
2750
- defaultEffort: cfg.defaultEffort,
2751
- askTool: cfg.askTool,
2752
- auth: auth.status(),
2753
- catalog: {
2754
- source: cat.source,
2755
- count: cat.models.length,
2756
- lastError: cat.lastError ?? null
2757
- },
2758
- bindings: Object.keys(store.all()).length,
2759
- lastRun
2760
- });
2894
+ (async () => {
2895
+ const cfg = getConfig();
2896
+ const cat = catalog.get();
2897
+ sendJson(res, 200, {
2898
+ plugin: "dsh-agy-link",
2899
+ bin: bin(),
2900
+ version: versionCache,
2901
+ dormantReason,
2902
+ enabled: cfg.enabled,
2903
+ permissionMode: cfg.permissionMode,
2904
+ workspaceRoot: cfg.workspaceRoot,
2905
+ defaultModel: cfg.defaultModel,
2906
+ defaultEffort: cfg.defaultEffort,
2907
+ askTool: cfg.askTool,
2908
+ auth: await auth.resolvedStatus(),
2909
+ catalog: {
2910
+ source: cat.source,
2911
+ count: cat.models.length,
2912
+ lastError: cat.lastError ?? null
2913
+ },
2914
+ bindings: Object.keys(store.all()).length,
2915
+ lastRun
2916
+ });
2917
+ })();
2761
2918
  }
2762
2919
  });
2763
- webServer.register({
2920
+ reg({
2764
2921
  kind: "exact",
2765
2922
  path: "/plugins/agy-link/auth",
2766
2923
  handler: (req, res) => {
@@ -2775,7 +2932,7 @@ function apply(ctx, entryConfig = {}) {
2775
2932
  })();
2776
2933
  }
2777
2934
  });
2778
- webServer.register({
2935
+ reg({
2779
2936
  kind: "exact",
2780
2937
  path: "/plugins/agy-link/auth-code",
2781
2938
  handler: (req, res) => {
@@ -2796,7 +2953,7 @@ function apply(ctx, entryConfig = {}) {
2796
2953
  })();
2797
2954
  }
2798
2955
  });
2799
- webServer.register({
2956
+ reg({
2800
2957
  kind: "exact",
2801
2958
  path: "/plugins/agy-link/config",
2802
2959
  handler: (req, res) => {
@@ -2827,7 +2984,7 @@ function apply(ctx, entryConfig = {}) {
2827
2984
  })();
2828
2985
  }
2829
2986
  });
2830
- webServer.register({
2987
+ reg({
2831
2988
  kind: "exact",
2832
2989
  path: "/plugins/agy-link/qr",
2833
2990
  handler: (_req, res) => {
@@ -2858,7 +3015,17 @@ function apply(ctx, entryConfig = {}) {
2858
3015
  })();
2859
3016
  }
2860
3017
  });
2861
- }
3018
+ return () => {
3019
+ for (const d of disposers) try {
3020
+ d();
3021
+ } catch {}
3022
+ };
3023
+ };
3024
+ ctx.inject(["webServer"], (sub) => {
3025
+ const webServer = sub.get("webServer");
3026
+ if (!webServer) return;
3027
+ return registerRoutes(webServer);
3028
+ });
2862
3029
  const mediaDir = () => {
2863
3030
  const cfg = getConfig();
2864
3031
  return cfg.mediaDir !== "" ? cfg.mediaDir : defaultMediaDir(stateDir());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agy-link",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
5
5
  "type": "module",
6
6
  "license": "MIT",