@khorsheed/dsh-local-agent 0.2.0-rc.1 → 0.2.0

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.en.md +23 -164
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +23 -164
  5. package/lib/client.js +23707 -4768
  6. package/lib/index.js +827 -62
  7. package/lib/member-bridge.js +195 -25
  8. package/lib/tool-factory.js +52 -19
  9. package/lib/typert.host.js +1440 -353
  10. package/lib/typert.remote-client.d.ts +43 -3
  11. package/lib/typert.remote-client.js +973 -335
  12. package/lib/types/client/FamilyCard.d.ts +37 -0
  13. package/lib/types/client/FamilyCard.js +55 -0
  14. package/lib/types/client/HarnessModelPicker.d.ts +21 -1
  15. package/lib/types/client/HarnessModelPicker.js +31 -3
  16. package/lib/types/client/MemberLiveNode.js +38 -4
  17. package/lib/types/client/ModelListSection.d.ts +49 -0
  18. package/lib/types/client/ModelListSection.js +132 -0
  19. package/lib/types/client/ProviderAuthBlock.d.ts +10 -2
  20. package/lib/types/client/ProviderAuthBlock.js +13 -6
  21. package/lib/types/client/auth-status.d.ts +6 -0
  22. package/lib/types/client/auth-status.js +8 -0
  23. package/lib/types/client/harness-cards.d.ts +72 -0
  24. package/lib/types/client/harness-cards.js +57 -0
  25. package/lib/types/client/icons.d.ts +2 -1
  26. package/lib/types/client/icons.js +1 -0
  27. package/lib/types/client/index.d.ts +32 -7
  28. package/lib/types/client/index.js +66 -9
  29. package/lib/types/client/locales.d.ts +25 -1
  30. package/lib/types/client/locales.js +50 -2
  31. package/lib/types/client/model-directory-scope.d.ts +14 -0
  32. package/lib/types/client/model-directory-scope.js +20 -0
  33. package/lib/types/gateway.d.ts +49 -1
  34. package/lib/types/gateway.js +89 -0
  35. package/lib/types/index.d.ts +91 -38
  36. package/lib/types/index.js +190 -58
  37. package/lib/types/live-stream.d.ts +11 -0
  38. package/lib/types/live-stream.js +26 -0
  39. package/lib/types/member-bridge.d.ts +11 -1
  40. package/lib/types/member-bridge.js +81 -13
  41. package/lib/types/member-channel.d.ts +44 -1
  42. package/lib/types/member-channel.js +65 -2
  43. package/lib/types/member-controls.d.ts +8 -0
  44. package/lib/types/member-controls.js +15 -2
  45. package/lib/types/mode-skills.d.ts +18 -0
  46. package/lib/types/mode-skills.js +56 -0
  47. package/lib/types/model-list.d.ts +55 -0
  48. package/lib/types/model-list.js +101 -0
  49. package/lib/types/preset-tool-host.d.ts +70 -0
  50. package/lib/types/preset-tool-host.js +210 -0
  51. package/lib/types/tool-factory.d.ts +9 -3
  52. package/lib/types/tool-factory.js +55 -18
  53. package/lib/types/types.d.ts +62 -5
  54. package/package.json +27 -23
  55. package/lib/invariant.js +0 -85
  56. package/lib/types/invariant.d.ts +0 -16
  57. package/lib/types/invariant.js +0 -105
@@ -21,15 +21,22 @@ const MEMBER_BRIDGE_TOKEN_ENV = "DSH_MEMBER_TOKEN";
21
21
  * it bare: `node <this file>`, with the socket path and token in its
22
22
  * environment (written into the CLI's scoped MCP config by the provider).
23
23
  * Covers the MCP handshake subset a client needs: `initialize`,
24
- * `notifications/initialized`, `ping`, `tools/list`, `tools/call`.
24
+ * `notifications/initialized`, `ping`, `tools/list`, `tools/call`. A member
25
+ * with a mode also gets that preset's plugin tools: `tools/list` asks the
26
+ * host for them and any other tool name is forwarded as a mode-tool call.
25
27
  * @module @khorsheed/dsh-local-agent/member-bridge
26
28
  */
27
29
  /** The one tool this server exposes. */
28
30
  const MEMBER_MESSAGE_TOOL = "member_message";
29
31
  const TOOL_DESCRIPTION = [
30
- "Notify another CLI member of this room/session group (member-to-member message).",
31
- "`to` is the target member's dsh child session id (or a member name when a room manages the group).",
32
- "The delivery receipt is returned verbatim: sent / pending-confirm / busy / an error reason."
32
+ "Send a message to another member of this room/session group.",
33
+ "`to` is the member name (or its dsh child session id).",
34
+ "kind \"task\" (default) hands it work: in a room, its final reply is reported back to you automatically, citing the message id — do not poll.",
35
+ "kind \"notify\" is an FYI and is never reported back. Pass inReplyTo (e.g. m12) when answering a message.",
36
+ "Never sleep or poll for the result (room_read included): end your turn, the report arrives on its own.",
37
+ "Writing \"@name\" in a reply reaches nobody; only this tool does.",
38
+ "Optional note: what you will wait for before acting on the result; optional task: \"new\" or a task number from room_read.",
39
+ "The receipt is returned verbatim: sent #m<id> … / pending-confirm … / busy / an error reason."
33
40
  ].join(" ");
34
41
  const TOOL_SCHEMA = {
35
42
  type: "object",
@@ -40,7 +47,24 @@ const TOOL_SCHEMA = {
40
47
  },
41
48
  text: {
42
49
  type: "string",
43
- description: "The notification text, delivered with your provenance."
50
+ description: "The message text, delivered with your provenance."
51
+ },
52
+ kind: {
53
+ type: "string",
54
+ enum: ["task", "notify"],
55
+ description: "task (default): the reply is reported back to you; notify: FYI only."
56
+ },
57
+ inReplyTo: {
58
+ type: "string",
59
+ description: "Optional id of the message this answers, e.g. m12."
60
+ },
61
+ note: {
62
+ type: "string",
63
+ description: "Optional: what you will wait for before acting on the result (the room uses it to decide when to hand you the report)."
64
+ },
65
+ task: {
66
+ type: "string",
67
+ description: "Optional: \"new\" or a task number from room_read; omit to stay in the task you are working on."
44
68
  }
45
69
  },
46
70
  required: ["to", "text"],
@@ -62,7 +86,7 @@ const ROOM_TOOLS = [
62
86
  },
63
87
  {
64
88
  name: "room_read",
65
- description: "Read your room roster, coordinator, deliveries, recent outcomes and available harness providers. Identity comes from the host.",
89
+ description: "Read your room: roster with roles, coordinator, tasks with their numbers, deliveries, recent outcomes and available harness providers. Identity comes from the host. Use it to look around, not to wait for a result.",
66
90
  inputSchema: {
67
91
  type: "object",
68
92
  properties: {},
@@ -77,27 +101,128 @@ const ROOM_TOOLS = [
77
101
  properties: {
78
102
  provider: { type: "string" },
79
103
  name: { type: "string" },
80
- instructions: { type: "string" },
104
+ note: {
105
+ type: "string",
106
+ description: "What you will wait for before acting on the first task result."
107
+ },
108
+ role: {
109
+ type: "string",
110
+ description: "Short public role (≤30 chars) the other members see, e.g. 代码审查."
111
+ },
112
+ instructions: {
113
+ type: "string",
114
+ description: "Private instructions only this member reads."
115
+ },
81
116
  cwd: { type: "string" },
82
117
  model: { type: "string" },
83
- firstTask: { type: "string" }
118
+ firstTask: { type: "string" },
119
+ mode: {
120
+ type: "string",
121
+ description: "The member's dsh mode (a host preset id, e.g. dsh-gamedev). Omit to keep this room's own mode; \"none\" for only its own CLI tools."
122
+ }
84
123
  },
85
124
  required: ["provider", "name"],
86
125
  additionalProperties: false
87
126
  }
88
127
  },
128
+ {
129
+ name: "ask_user_question",
130
+ description: "Ask the human questions they answer by picking options (or typing), instead of writing the questions into your reply. Use it whenever you need the human to decide or fill in something before you go on: give each question 2-4 short options with a one-line description each. The call waits for the answer and returns {answers:[{id, selected:[labels], custom?}]}; if nobody answers it fails — then go on with your best judgement and say what you assumed.",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: { questions: {
134
+ type: "array",
135
+ description: "At most 4 questions.",
136
+ items: {
137
+ type: "object",
138
+ properties: {
139
+ id: {
140
+ type: "string",
141
+ description: "Short unique id, echoed in the answer."
142
+ },
143
+ question: {
144
+ type: "string",
145
+ description: "The full question."
146
+ },
147
+ header: {
148
+ type: "string",
149
+ description: "Optional 2-6 word topic chip."
150
+ },
151
+ options: {
152
+ type: "array",
153
+ items: {
154
+ type: "object",
155
+ properties: {
156
+ label: { type: "string" },
157
+ description: { type: "string" }
158
+ },
159
+ required: ["label"]
160
+ }
161
+ },
162
+ multi_select: {
163
+ type: "boolean",
164
+ description: "True when several options may be picked."
165
+ }
166
+ },
167
+ required: ["id", "question"]
168
+ }
169
+ } },
170
+ required: ["questions"],
171
+ additionalProperties: false
172
+ }
173
+ },
89
174
  {
90
175
  name: "room_message",
91
- description: "Coordinator only: dispatch background work to a room member by name. Returns acceptance immediately; completion arrives as a correlated report. Do not poll.",
176
+ description: "Send a room member a message by name (same as member_message). kind \"task\" (default): its reply comes back to you as a report citing the message id; kind \"notify\": FYI only. Do not poll.",
92
177
  inputSchema: {
93
178
  type: "object",
94
179
  properties: {
95
180
  member: { type: "string" },
96
- text: { type: "string" }
181
+ text: { type: "string" },
182
+ kind: {
183
+ type: "string",
184
+ enum: ["task", "notify"]
185
+ },
186
+ inReplyTo: { type: "string" },
187
+ note: {
188
+ type: "string",
189
+ description: "What you will wait for before acting on the result."
190
+ },
191
+ task: {
192
+ type: "string",
193
+ description: "\"new\" or a task number; omit to stay in your current task."
194
+ }
97
195
  },
98
196
  required: ["member", "text"],
99
197
  additionalProperties: false
100
198
  }
199
+ },
200
+ {
201
+ name: "room_help",
202
+ description: "Ask another session outside this room for help — typically the one that built what you are stuck on. It works in its own session (a busy one queues you); its final answer plus a short handoff comes back to you on its own, so end your turn after asking and do not poll. Name the session by title, id or the @ mention the human wrote; a session that is another room goes to its coordinator, or to the member you name. The first ask of a session the human did not @ in the room waits for their approval. To answer its question or ask more, call again with the help id and no session.",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ session: {
207
+ type: "string",
208
+ description: "The session to ask (title, id, or @ mention). Omit when continuing a thread."
209
+ },
210
+ member: {
211
+ type: "string",
212
+ description: "When the session is another room: the member to ask. Omit to let its coordinator decide."
213
+ },
214
+ help: {
215
+ type: "string",
216
+ description: "The help id of an existing thread."
217
+ },
218
+ text: {
219
+ type: "string",
220
+ description: "What you need, self-contained: the helper does not see this room."
221
+ }
222
+ },
223
+ required: ["text"],
224
+ additionalProperties: false
225
+ }
101
226
  }
102
227
  ];
103
228
  async function callRoomCommand(env, name, args) {
@@ -116,6 +241,35 @@ async function callRoomCommand(env, name, args) {
116
241
  return toolText(`Room command unavailable: ${String(error)}`, true);
117
242
  }
118
243
  }
244
+ /** The member's mode tools (the preset's plugin tools), empty when it has none or the host is unreachable. */
245
+ async function listModeTools(env) {
246
+ if (env.socket === void 0 || env.token === void 0) return [];
247
+ try {
248
+ const outcome = await callHost(env.socket, {
249
+ token: env.token,
250
+ modeTools: { list: true }
251
+ });
252
+ return outcome.ok && Array.isArray(outcome.tools) ? outcome.tools : [];
253
+ } catch {
254
+ return [];
255
+ }
256
+ }
257
+ /** Run one mode tool on the host; the host's MCP result passes through. */
258
+ async function callModeTool(env, name, args) {
259
+ if (env.socket === void 0 || env.token === void 0) return toolText("member channel is not configured for this run", true);
260
+ try {
261
+ const outcome = await callHost(env.socket, {
262
+ token: env.token,
263
+ modeTools: { call: {
264
+ name,
265
+ arguments: args
266
+ } }
267
+ });
268
+ return outcome.ok && outcome.result !== void 0 ? outcome.result : toolText(outcome.error ?? `Tool ${name} failed`, true);
269
+ } catch (error) {
270
+ return toolText(`Mode tool unavailable: ${String(error)}`, true);
271
+ }
272
+ }
119
273
  /** One socket round trip: send the request line, read the one-line response. */
120
274
  function callHost(socket, payload) {
121
275
  return new Promise((resolve, reject) => {
@@ -163,7 +317,11 @@ async function callMemberMessage(env, args) {
163
317
  const outcome = await callHost(env.socket, {
164
318
  token: env.token,
165
319
  to: args.to,
166
- text: args.text
320
+ text: args.text,
321
+ ...args.kind === "task" || args.kind === "notify" ? { kind: args.kind } : {},
322
+ ...typeof args.inReplyTo === "string" ? { inReplyTo: args.inReplyTo } : {},
323
+ ...typeof args.note === "string" ? { note: args.note } : {},
324
+ ...typeof args.task === "string" ? { task: args.task } : {}
167
325
  });
168
326
  if (outcome.ok) return toolText(outcome.receipt ?? "sent");
169
327
  return toolText(outcome.error ?? "member channel rejected the message", true);
@@ -229,15 +387,19 @@ function serveMemberBridge(env, input, output) {
229
387
  });
230
388
  return;
231
389
  case "tools/list":
232
- write({
390
+ listModeTools(env).then((modeTools) => write({
233
391
  jsonrpc: "2.0",
234
392
  id: id ?? null,
235
- result: { tools: [{
236
- name: MEMBER_MESSAGE_TOOL,
237
- description: TOOL_DESCRIPTION,
238
- inputSchema: TOOL_SCHEMA
239
- }, ...ROOM_TOOLS] }
240
- });
393
+ result: { tools: [
394
+ {
395
+ name: MEMBER_MESSAGE_TOOL,
396
+ description: TOOL_DESCRIPTION,
397
+ inputSchema: TOOL_SCHEMA
398
+ },
399
+ ...ROOM_TOOLS,
400
+ ...modeTools.filter((tool) => tool["name"] !== "member_message" && !ROOM_TOOLS.some((room) => room.name === tool["name"]))
401
+ ] }
402
+ }));
241
403
  return;
242
404
  case "tools/call": {
243
405
  const params = request.params;
@@ -250,14 +412,22 @@ function serveMemberBridge(env, input, output) {
250
412
  return;
251
413
  }
252
414
  if (params?.name !== "member_message") {
253
- write({
415
+ if (typeof params?.name !== "string") {
416
+ write({
417
+ jsonrpc: "2.0",
418
+ id: id ?? null,
419
+ error: {
420
+ code: -32602,
421
+ message: "tools/call requires a tool name"
422
+ }
423
+ });
424
+ return;
425
+ }
426
+ callModeTool(env, params.name, params.arguments ?? {}).then((result) => write({
254
427
  jsonrpc: "2.0",
255
428
  id: id ?? null,
256
- error: {
257
- code: -32602,
258
- message: `unknown tool ${String(params?.name)}`
259
- }
260
- });
429
+ result
430
+ }));
261
431
  return;
262
432
  }
263
433
  callMemberMessage(env, params.arguments ?? {}).then((result) => {
@@ -295,4 +465,4 @@ if (isMain()) serveMemberBridge({
295
465
  token: process.env[MEMBER_BRIDGE_TOKEN_ENV]
296
466
  }, process.stdin, process.stdout);
297
467
  //#endregion
298
- export { MEMBER_MESSAGE_TOOL, callMemberMessage, callRoomCommand, serveMemberBridge };
468
+ export { MEMBER_MESSAGE_TOOL, callMemberMessage, callModeTool, callRoomCommand, listModeTools, serveMemberBridge };
@@ -1,4 +1,6 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import z from "@deepseek-ai/schemastery";
3
+ import { SessionId } from "@deepseek-ai/dsh-session";
2
4
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
5
  //#region lib/types/tool-factory.js
4
6
  /**
@@ -17,9 +19,15 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
17
19
  * The tool instead carries the handle as a first-class `resume` parameter and
18
20
  * resolves it through the localAgent service, which rejects any handle that
19
21
  * does not name a delegation recorded for THIS parent session and provider.
20
- * Continuation rounds still go through `ctx.subagents.start()`, so lifecycle
21
- * events (`subagent/start`/`subagent/end`) and the standard 子代理 surface are
22
- * unchanged.
22
+ * Continuation rounds still go through `ctx.subagents.startActivation()` with
23
+ * caller delivery, so lifecycle events (`subagent/start`/`subagent/end`) and
24
+ * the standard 子代理 surface are unchanged.
25
+ *
26
+ * Host 0.2.1 note: `startActivation` rejects a provider run id that names a
27
+ * live Session (DUPLICATE_CHILD) — and every family provider's child session
28
+ * IS live. Providers therefore publish per-execution run ids, and the durable
29
+ * dsh child session id is minted HERE (the tool), staged on the delegation
30
+ * intent, and re-attached to the run adapter the rest of the family tracks.
23
31
  *
24
32
  * This module used to ship as the standalone companion package
25
33
  * `@khorsheed/dsh-local-agent-tool-subagent` (0.1.x); the family fold moved it
@@ -84,8 +92,10 @@ function stopReasonError(result) {
84
92
  /**
85
93
  * Fuse a caller-owned controller with the tool execution's signal so EITHER
86
94
  * abort cancels the run — the same shape the facade's tracked controller
87
- * uses. The active-delegation registry's stop lever aborts the controller;
88
- * the tool's own execution abort flows through the execution signal.
95
+ * uses. The fused signal reaches `startActivation`'s `spec.signal`, which
96
+ * owns cancellation only until publication (host 0.2.1); the published
97
+ * activation's `dispose()` is the post-publication channel, wired below in
98
+ * the exec body and in the registry's stop lever.
89
99
  */
90
100
  function fusedSignal(controller, caller) {
91
101
  return caller === void 0 ? controller.signal : AbortSignal.any([controller.signal, caller]);
@@ -205,7 +215,11 @@ function mountSubagentDelegationTool(ctx, config, owner) {
205
215
  provider: config.provider,
206
216
  parentSessionId
207
217
  });
208
- if (resumeHandle === void 0) ctx.localAgent.stageDelegationIntent(parentSessionId, config.provider, { kind: "fresh" });
218
+ const childSessionId = resumeHandle ?? randomUUID();
219
+ if (resumeHandle === void 0) ctx.localAgent.stageDelegationIntent(parentSessionId, config.provider, {
220
+ kind: "fresh",
221
+ childSessionId
222
+ });
209
223
  else if (resume !== void 0) {
210
224
  await ctx.localAgent.ensureChildLive?.(resumeHandle);
211
225
  ctx.localAgent.stageDelegationIntent(parentSessionId, config.provider, {
@@ -214,27 +228,46 @@ function mountSubagentDelegationTool(ctx, config, owner) {
214
228
  cliSessionId: resume.cliSessionId
215
229
  });
216
230
  }
217
- const request = {
218
- label: args.description,
219
- prompt: [{
220
- type: "text",
221
- text: args.prompt
222
- }],
223
- parent
224
- };
225
231
  const controller = new AbortController();
226
- const run = await ctx.subagents.start(config.provider, {
227
- ...request,
228
- signal: fusedSignal(controller, exec.signal)
232
+ const activation = await ctx.subagents.startActivation({
233
+ provider: config.provider,
234
+ label: args.description,
235
+ request: {
236
+ prompt: [{
237
+ type: "text",
238
+ text: args.prompt
239
+ }],
240
+ parent
241
+ },
242
+ signal: fusedSignal(controller, exec.signal),
243
+ delivery: "caller"
229
244
  });
245
+ const dispose = () => activation.dispose();
246
+ const onExecAbort = () => {
247
+ dispose().catch((error) => {
248
+ ctx.logger.warn(`subagent tool: execution-signal disposal of ${childSessionId} failed: ${String(error)}`);
249
+ });
250
+ };
251
+ exec.signal.addEventListener("abort", onExecAbort, { once: true });
252
+ activation.result.then(() => exec.signal.removeEventListener("abort", onExecAbort), () => exec.signal.removeEventListener("abort", onExecAbort));
253
+ const run = {
254
+ id: SessionId(childSessionId),
255
+ result: activation.result,
256
+ dispose
257
+ };
230
258
  const registry = ctx.localAgent;
231
- if (typeof registry.trackDelegationRun === "function") registry.trackDelegationRun(String(run.id), run, () => controller.abort());
259
+ if (typeof registry.trackDelegationRun === "function") registry.trackDelegationRun(childSessionId, run, () => {
260
+ controller.abort();
261
+ dispose().catch((error) => {
262
+ ctx.logger.warn(`subagent tool: stop disposal of ${childSessionId} failed: ${String(error)}`);
263
+ });
264
+ });
232
265
  const settled = await settleForegroundRun(run);
233
266
  if (resume === void 0) return {
234
267
  ...settled,
235
268
  output: [...settled.output, {
236
269
  type: "text",
237
- text: `${RESUME_HINT_PREFIX}${run.id}"`
270
+ text: `${RESUME_HINT_PREFIX}${childSessionId}"`
238
271
  }]
239
272
  };
240
273
  return settled;