@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
@@ -0,0 +1,70 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ /** One plugin tool as the bridge lists it (MCP `tools/list` shape). */
3
+ export interface BridgedTool {
4
+ readonly name: string;
5
+ readonly description: string;
6
+ readonly inputSchema: Record<string, unknown>;
7
+ }
8
+ /** One bridged call's outcome (MCP `tools/call` result shape). */
9
+ export interface BridgedToolResult {
10
+ readonly content: readonly Record<string, unknown>[];
11
+ readonly isError: boolean;
12
+ }
13
+ /** One skill the mode itself contributes (absent from the global view). */
14
+ export interface ModeSkill {
15
+ readonly name: string;
16
+ readonly description: string;
17
+ readonly content: string;
18
+ }
19
+ /** The per-member preset agents and the list/call operations the bridge forwards. */
20
+ export declare class PresetToolHost {
21
+ private readonly ctx;
22
+ private readonly diagnostics?;
23
+ private readonly hosts;
24
+ /** Stand-in → the member's child session, for routing approvals its calls raise. */
25
+ private readonly standIns;
26
+ /** The member a stand-in agent acts for, if it is one of ours. */
27
+ memberOf(agent: unknown): string | undefined;
28
+ /**
29
+ * @param ctx - the local-agent plugin context.
30
+ * @param diagnostics - optional JSONL file each listing appends a summary to
31
+ * (what the preset agent's scope saw and which tools passed the filter).
32
+ */
33
+ constructor(ctx: Context, diagnostics?: string | undefined);
34
+ /** Append one diagnostics line (best-effort). */
35
+ note(entry: Record<string, unknown>): void;
36
+ private probe;
37
+ /** The member's stand-in, scoped with its preset on first use (and re-scoped when the preset changed). */
38
+ private agentFor;
39
+ /**
40
+ * The preset's plugin tools, for a member's `tools/list`.
41
+ * @param childSessionId - the member's child session.
42
+ * @param preset - the member's mode.
43
+ * @param cwd - the member's working directory.
44
+ * @returns the tools, empty when the host lacks the capabilities.
45
+ */
46
+ list(childSessionId: string, preset: string, cwd?: string): Promise<BridgedTool[]>;
47
+ /**
48
+ * Run one plugin tool for a member through the registry's execution
49
+ * pipeline (policies, approvals, timeouts), as the member's preset agent.
50
+ * @param childSessionId - the member's child session.
51
+ * @param preset - the member's mode.
52
+ * @param name - the tool name.
53
+ * @param args - the tool arguments.
54
+ * @param signal - cancellation.
55
+ * @param cwd - the member's working directory.
56
+ * @returns the MCP result.
57
+ */
58
+ call(childSessionId: string, preset: string, name: string, args: unknown, signal: AbortSignal, cwd?: string): Promise<BridgedToolResult>;
59
+ /**
60
+ * The skills the member's mode contributes: visible in its scope, absent
61
+ * from the global view (installed skills every CLI already sees stay out).
62
+ * @param childSessionId - the member's child session.
63
+ * @param preset - the member's mode.
64
+ * @returns the mode's skills with their content; empty without a skill registry.
65
+ */
66
+ skills(childSessionId: string, preset: string): Promise<ModeSkill[]>;
67
+ /** Dispose every preset agent. */
68
+ dispose(): Promise<void>;
69
+ }
70
+ //# sourceMappingURL=preset-tool-host.d.ts.map
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Preset tool host: lets an external CLI member use the plugin tools of a
3
+ * host agent preset (a mode), through its member bridge, ON THE MEMBER'S OWN
4
+ * SESSION. The tool registry keys scoped tools by an agent identity, and
5
+ * plugin tools read only `agent.id` and `agent.session` from it — so the
6
+ * member gets a model-less stand-in: `{ id, session }` over its own child
7
+ * session, given a scope (`createScope`) with the preset mounted. Calls run
8
+ * through the registry's normal pipeline (policies, approvals, timeouts) as
9
+ * that stand-in, so plugin state (a video project) belongs to the member's
10
+ * session and the mode's panels show there; the session also records the mode
11
+ * (`agent-preset/selected`), so per-session plugin visibility follows it.
12
+ * Only PLUGIN tools are offered (`Symbol.for('dsh.tool.origin')` channel
13
+ * `plugin`; the CLI brings its own shell, files and todo).
14
+ *
15
+ * Everything host-side is probed, never injected: without `sessions`,
16
+ * `agentPresets` or `tools` the member simply gets no extra tools.
17
+ * @module @khorsheed/dsh-local-agent/preset-tool-host
18
+ */
19
+ import { randomUUID } from 'node:crypto';
20
+ import { appendFileSync } from 'node:fs';
21
+ import { createScope } from '@deepseek-ai/dsh-scope';
22
+ import { SessionId } from '@deepseek-ai/dsh-session';
23
+ /** Owners whose tools the bridge already serves itself (room), or that delegate from dsh (the local-agent family). */
24
+ const skippedOwner = (owner) => owner === '@khorsheed/dsh-room' || owner.startsWith('@khorsheed/dsh-local-agent');
25
+ /** How long a freshly mounted preset may take to finish registering its tools, and how often to look. */
26
+ const SETTLE_MS = 3_000;
27
+ const SETTLE_STEP_MS = 150;
28
+ const ORIGIN = Symbol.for('dsh.tool.origin');
29
+ /** The mode a session records: its header, overridden by the latest selection. */
30
+ function sessionPreset(session) {
31
+ let preset = session.header.agentPreset ?? undefined;
32
+ for (const event of session.snapshotEvents()) {
33
+ if (event.type === 'agent-preset/selected')
34
+ preset = event.data?.agentPreset ?? preset;
35
+ }
36
+ return preset;
37
+ }
38
+ /** Whether a definition is a plugin tool the bridge may forward. */
39
+ function isPluginTool(definition) {
40
+ const origin = definition?.[ORIGIN];
41
+ return origin?.channel === 'plugin' && !skippedOwner(String(origin.owner));
42
+ }
43
+ /** Map registry content blocks to MCP content (text and images; anything else as text). */
44
+ function mcpContent(blocks) {
45
+ return blocks.map((block) => {
46
+ if (block.type === 'text')
47
+ return { type: 'text', text: block.text ?? '' };
48
+ if (block.type === 'image' && typeof block.data === 'string')
49
+ return { type: 'image', data: block.data, mimeType: block.mediaType ?? block.mimeType ?? 'image/png' };
50
+ return { type: 'text', text: JSON.stringify(block) };
51
+ });
52
+ }
53
+ /** The per-member preset agents and the list/call operations the bridge forwards. */
54
+ export class PresetToolHost {
55
+ ctx;
56
+ diagnostics;
57
+ hosts = new Map();
58
+ /** Stand-in → the member's child session, for routing approvals its calls raise. */
59
+ standIns = new WeakMap();
60
+ /** The member a stand-in agent acts for, if it is one of ours. */
61
+ memberOf(agent) {
62
+ return typeof agent === 'object' && agent !== null ? this.standIns.get(agent) : undefined;
63
+ }
64
+ /**
65
+ * @param ctx - the local-agent plugin context.
66
+ * @param diagnostics - optional JSONL file each listing appends a summary to
67
+ * (what the preset agent's scope saw and which tools passed the filter).
68
+ */
69
+ constructor(ctx, diagnostics) {
70
+ this.ctx = ctx;
71
+ this.diagnostics = diagnostics;
72
+ }
73
+ /** Append one diagnostics line (best-effort). */
74
+ note(entry) {
75
+ if (this.diagnostics === undefined)
76
+ return;
77
+ try {
78
+ appendFileSync(this.diagnostics, `${JSON.stringify({ time: new Date().toISOString(), ...entry })}\n`);
79
+ }
80
+ catch { /* diagnostics never fail a call */ }
81
+ }
82
+ probe(name) {
83
+ return this.ctx.get.call(this.ctx, name);
84
+ }
85
+ /** The member's stand-in, scoped with its preset on first use (and re-scoped when the preset changed). */
86
+ async agentFor(childSessionId, preset, _cwd) {
87
+ const sessions = this.probe('sessions');
88
+ const presets = this.probe('agentPresets');
89
+ const session = sessions?.get(SessionId(childSessionId));
90
+ if (presets?.mount === undefined || session === undefined)
91
+ return undefined;
92
+ const current = this.hosts.get(childSessionId);
93
+ if (current !== undefined && current.preset === preset && (await current.agent).agent.session === session)
94
+ return (await current.agent).agent;
95
+ if (current !== undefined) {
96
+ this.hosts.delete(childSessionId);
97
+ void current.agent.then(handle => handle.dispose()).catch(() => { });
98
+ }
99
+ const agent = (async () => {
100
+ const resolved = await presets.resolve(preset);
101
+ const member = { id: childSessionId, session };
102
+ this.standIns.set(member, childSessionId);
103
+ const scope = createScope(this.ctx, member);
104
+ member.ctx = scope.ctx;
105
+ try {
106
+ await presets.mount(scope.ctx, resolved.id);
107
+ }
108
+ catch (error) {
109
+ await scope.dispose();
110
+ throw error;
111
+ }
112
+ // The member's session runs this mode now: the host's per-session
113
+ // plugin visibility reads it from here.
114
+ if (sessionPreset(session) !== resolved.id)
115
+ session.append('agent-preset/selected', { agentPreset: resolved.id });
116
+ return { agent: member, dispose: () => scope.dispose() };
117
+ })();
118
+ this.hosts.set(childSessionId, { preset, agent });
119
+ try {
120
+ return (await agent).agent;
121
+ }
122
+ catch (error) {
123
+ this.hosts.delete(childSessionId);
124
+ throw error;
125
+ }
126
+ }
127
+ /**
128
+ * The preset's plugin tools, for a member's `tools/list`.
129
+ * @param childSessionId - the member's child session.
130
+ * @param preset - the member's mode.
131
+ * @param cwd - the member's working directory.
132
+ * @returns the tools, empty when the host lacks the capabilities.
133
+ */
134
+ async list(childSessionId, preset, cwd) {
135
+ const tools = this.probe('tools');
136
+ const fresh = this.hosts.get(childSessionId)?.preset !== preset;
137
+ const agent = await this.agentFor(childSessionId, preset, cwd);
138
+ if (tools === undefined || agent === undefined) {
139
+ this.note({ childSessionId, preset, tools: tools !== undefined, agent: agent !== undefined });
140
+ return [];
141
+ }
142
+ // A preset mounted for the first time registers some rows' tools a
143
+ // moment later (deferred injects): wait until the scope stops growing.
144
+ let schemas = tools.schemas(agent);
145
+ if (fresh) {
146
+ for (let waited = 0; waited < SETTLE_MS; waited += SETTLE_STEP_MS) {
147
+ await new Promise(resolve => setTimeout(resolve, SETTLE_STEP_MS));
148
+ const next = tools.schemas(agent);
149
+ if (next.length === schemas.length && waited >= SETTLE_STEP_MS * 3)
150
+ break;
151
+ schemas = next;
152
+ }
153
+ }
154
+ const listed = schemas.filter(schema => isPluginTool(tools.get(schema.name, agent)));
155
+ this.note({ childSessionId, preset, agent: agent.id, scope: schemas.map(schema => {
156
+ const origin = tools.get(schema.name, agent)?.[ORIGIN];
157
+ return origin === undefined ? schema.name : `${schema.name}<${String(origin.channel)}:${String(origin.owner)}>`;
158
+ }), listed: listed.map(schema => schema.name) });
159
+ return listed
160
+ .map(schema => ({
161
+ name: schema.name,
162
+ description: schema.description ?? '',
163
+ inputSchema: (schema.parameters ?? { type: 'object', properties: {} }),
164
+ }));
165
+ }
166
+ /**
167
+ * Run one plugin tool for a member through the registry's execution
168
+ * pipeline (policies, approvals, timeouts), as the member's preset agent.
169
+ * @param childSessionId - the member's child session.
170
+ * @param preset - the member's mode.
171
+ * @param name - the tool name.
172
+ * @param args - the tool arguments.
173
+ * @param signal - cancellation.
174
+ * @param cwd - the member's working directory.
175
+ * @returns the MCP result.
176
+ */
177
+ async call(childSessionId, preset, name, args, signal, cwd) {
178
+ const tools = this.probe('tools');
179
+ const agent = await this.agentFor(childSessionId, preset, cwd);
180
+ if (tools === undefined || agent === undefined)
181
+ return { content: [{ type: 'text', text: 'Mode tools are unavailable on this host' }], isError: true };
182
+ if (!isPluginTool(tools.get(name, agent)))
183
+ return { content: [{ type: 'text', text: `Unknown tool ${name} in mode ${preset}` }], isError: true };
184
+ const result = await tools.execute({ callId: `bridge-${randomUUID()}`, name, arguments: args, agent, signal });
185
+ return { content: mcpContent(result.content), isError: result.isError };
186
+ }
187
+ /**
188
+ * The skills the member's mode contributes: visible in its scope, absent
189
+ * from the global view (installed skills every CLI already sees stay out).
190
+ * @param childSessionId - the member's child session.
191
+ * @param preset - the member's mode.
192
+ * @returns the mode's skills with their content; empty without a skill registry.
193
+ */
194
+ async skills(childSessionId, preset) {
195
+ const registry = this.probe('skills');
196
+ const agent = await this.agentFor(childSessionId, preset, undefined);
197
+ if (registry?.list === undefined || agent === undefined)
198
+ return [];
199
+ const global = new Set((await registry.list()).map(skill => skill.name));
200
+ const own = (await registry.list({ scope: agent })).filter(skill => !global.has(skill.name));
201
+ const loaded = await Promise.all(own.map(skill => registry.get(skill.name, { scope: agent })));
202
+ return loaded.flatMap(skill => skill === undefined ? [] : [{ name: skill.name, description: skill.description, content: skill.content }]);
203
+ }
204
+ /** Dispose every preset agent. */
205
+ async dispose() {
206
+ const hosts = [...this.hosts.values()];
207
+ this.hosts.clear();
208
+ await Promise.all(hosts.map(host => host.agent.then(handle => handle.dispose()).catch(() => { })));
209
+ }
210
+ }
@@ -14,9 +14,15 @@
14
14
  * The tool instead carries the handle as a first-class `resume` parameter and
15
15
  * resolves it through the localAgent service, which rejects any handle that
16
16
  * does not name a delegation recorded for THIS parent session and provider.
17
- * Continuation rounds still go through `ctx.subagents.start()`, so lifecycle
18
- * events (`subagent/start`/`subagent/end`) and the standard 子代理 surface are
19
- * unchanged.
17
+ * Continuation rounds still go through `ctx.subagents.startActivation()` with
18
+ * caller delivery, so lifecycle events (`subagent/start`/`subagent/end`) and
19
+ * the standard 子代理 surface are unchanged.
20
+ *
21
+ * Host 0.2.1 note: `startActivation` rejects a provider run id that names a
22
+ * live Session (DUPLICATE_CHILD) — and every family provider's child session
23
+ * IS live. Providers therefore publish per-execution run ids, and the durable
24
+ * dsh child session id is minted HERE (the tool), staged on the delegation
25
+ * intent, and re-attached to the run adapter the rest of the family tracks.
20
26
  *
21
27
  * This module used to ship as the standalone companion package
22
28
  * `@khorsheed/dsh-local-agent-tool-subagent` (0.1.x); the family fold moved it
@@ -14,9 +14,15 @@
14
14
  * The tool instead carries the handle as a first-class `resume` parameter and
15
15
  * resolves it through the localAgent service, which rejects any handle that
16
16
  * does not name a delegation recorded for THIS parent session and provider.
17
- * Continuation rounds still go through `ctx.subagents.start()`, so lifecycle
18
- * events (`subagent/start`/`subagent/end`) and the standard 子代理 surface are
19
- * unchanged.
17
+ * Continuation rounds still go through `ctx.subagents.startActivation()` with
18
+ * caller delivery, so lifecycle events (`subagent/start`/`subagent/end`) and
19
+ * the standard 子代理 surface are unchanged.
20
+ *
21
+ * Host 0.2.1 note: `startActivation` rejects a provider run id that names a
22
+ * live Session (DUPLICATE_CHILD) — and every family provider's child session
23
+ * IS live. Providers therefore publish per-execution run ids, and the durable
24
+ * dsh child session id is minted HERE (the tool), staged on the delegation
25
+ * intent, and re-attached to the run adapter the rest of the family tracks.
20
26
  *
21
27
  * This module used to ship as the standalone companion package
22
28
  * `@khorsheed/dsh-local-agent-tool-subagent` (0.1.x); the family fold moved it
@@ -29,8 +35,10 @@
29
35
  *
30
36
  * @module @khorsheed/dsh-local-agent/tool-factory
31
37
  */
38
+ import { randomUUID } from 'node:crypto';
32
39
  import z from '@deepseek-ai/schemastery';
33
40
  import { defineTool } from '@deepseek-ai/dsh-tools';
41
+ import { SessionId } from '@deepseek-ai/dsh-session';
34
42
  /**
35
43
  * Tag the model-visible tool with its origin (AGENTS.md § Tool origin tagging;
36
44
  * seam S12): the capability catalog reads this `Symbol.for`-keyed tag back
@@ -97,8 +105,10 @@ function stopReasonError(result) {
97
105
  /**
98
106
  * Fuse a caller-owned controller with the tool execution's signal so EITHER
99
107
  * abort cancels the run — the same shape the facade's tracked controller
100
- * uses. The active-delegation registry's stop lever aborts the controller;
101
- * the tool's own execution abort flows through the execution signal.
108
+ * uses. The fused signal reaches `startActivation`'s `spec.signal`, which
109
+ * owns cancellation only until publication (host 0.2.1); the published
110
+ * activation's `dispose()` is the post-publication channel, wired below in
111
+ * the exec body and in the registry's stop lever.
102
112
  */
103
113
  function fusedSignal(controller, caller) {
104
114
  return caller === undefined ? controller.signal : AbortSignal.any([controller.signal, caller]);
@@ -245,8 +255,14 @@ export function mountSubagentDelegationTool(ctx, config, owner) {
245
255
  provider: config.provider,
246
256
  parentSessionId,
247
257
  });
258
+ // The durable child identity: the recorded resume handle, or minted
259
+ // here for a fresh delegation (host 0.2.1 activations never surface
260
+ // it — a provider run id naming a live Session is DUPLICATE_CHILD,
261
+ // so the provider publishes a per-execution id and this id rides the
262
+ // staged intent instead).
263
+ const childSessionId = resumeHandle ?? randomUUID();
248
264
  if (resumeHandle === undefined) {
249
- ctx.localAgent.stageDelegationIntent(parentSessionId, config.provider, { kind: 'fresh' });
265
+ ctx.localAgent.stageDelegationIntent(parentSessionId, config.provider, { kind: 'fresh', childSessionId });
250
266
  }
251
267
  else if (resume !== undefined) {
252
268
  // A host restart evicts the child session from the live store, and
@@ -261,19 +277,35 @@ export function mountSubagentDelegationTool(ctx, config, owner) {
261
277
  cliSessionId: resume.cliSessionId,
262
278
  });
263
279
  }
264
- const request = {
265
- label: args.description,
266
- prompt: [{ type: 'text', text: args.prompt }],
267
- parent,
268
- };
269
280
  // The run's own cancel lever: a controller fused with the tool
270
281
  // execution signal, so the active-delegation registry's stop can
271
- // abort it without touching the execution's own signal.
282
+ // abort it without touching the execution's own signal. The fused
283
+ // signal cancels only up to publication (host 0.2.1); after that the
284
+ // activation's dispose() is the cancel channel, wired below.
272
285
  const controller = new AbortController();
273
- const run = await ctx.subagents.start(config.provider, {
274
- ...request,
286
+ // Caller delivery: the tool awaits and reports the result itself; the
287
+ // parent agent gets no completion notice doubling the tool result.
288
+ const activation = await ctx.subagents.startActivation({
289
+ provider: config.provider,
290
+ label: args.description,
291
+ request: {
292
+ prompt: [{ type: 'text', text: args.prompt }],
293
+ parent,
294
+ },
275
295
  signal: fusedSignal(controller, exec.signal),
296
+ delivery: 'caller',
276
297
  });
298
+ const dispose = () => activation.dispose();
299
+ const onExecAbort = () => {
300
+ void dispose().catch((error) => {
301
+ ctx.logger.warn(`subagent tool: execution-signal disposal of ${childSessionId} failed: ${String(error)}`);
302
+ });
303
+ };
304
+ exec.signal.addEventListener('abort', onExecAbort, { once: true });
305
+ void activation.result.then(() => exec.signal.removeEventListener('abort', onExecAbort), () => exec.signal.removeEventListener('abort', onExecAbort));
306
+ // The run the family tracks: keyed by the dsh child session id (the
307
+ // resume handle the model sees), not the activation's per-execution id.
308
+ const run = { id: SessionId(childSessionId), result: activation.result, dispose };
277
309
  // Register the in-flight run with the family's active-delegation
278
310
  // registry (keyed by child session id) so `/local-agent stop
279
311
  // <childSessionId>` — and the taskpilot stop button that dispatches
@@ -283,18 +315,23 @@ export function mountSubagentDelegationTool(ctx, config, owner) {
283
315
  // predates the registry simply leaves the run untracked, as before.
284
316
  const registry = ctx.localAgent;
285
317
  if (typeof registry.trackDelegationRun === 'function') {
286
- registry.trackDelegationRun(String(run.id), run, () => controller.abort());
318
+ registry.trackDelegationRun(childSessionId, run, () => {
319
+ controller.abort();
320
+ void dispose().catch((error) => {
321
+ ctx.logger.warn(`subagent tool: stop disposal of ${childSessionId} failed: ${String(error)}`);
322
+ });
323
+ });
287
324
  }
288
325
  const settled = await settleForegroundRun(run);
289
326
  if (resume === undefined) {
290
327
  // Fresh delegation: self-describe the resume handle (the dsh child
291
- // session id, which equals the run id for a session-backed run) so
292
- // the model can continue the same conversation in a later round.
328
+ // session id minted above and staged on the intent) so the model
329
+ // can continue the same conversation in a later round.
293
330
  return {
294
331
  ...settled,
295
332
  output: [...settled.output, {
296
333
  type: 'text',
297
- text: `${RESUME_HINT_PREFIX}${run.id}"`,
334
+ text: `${RESUME_HINT_PREFIX}${childSessionId}"`,
298
335
  }],
299
336
  };
300
337
  }
@@ -94,6 +94,29 @@ export interface LocalAgentSessionRecord {
94
94
  * this field's job.
95
95
  */
96
96
  export type LocalAgentCredentialState = 'absent' | 'present-unverified' | 'verified' | 'rejected';
97
+ /**
98
+ * One harness's endpoint state, per field, for the settings card: the
99
+ * resolved baseUrl verbatim (not a secret) and the two credentials as
100
+ * presence booleans — secrets never cross this seam. The state is the
101
+ * RESOLVED view (scope file over the deployment config), which is exactly
102
+ * what a delegation round would use.
103
+ */
104
+ export interface LocalAgentEndpointAuthState {
105
+ readonly baseUrl?: string;
106
+ readonly apiKeySet: boolean;
107
+ readonly authTokenSet: boolean;
108
+ }
109
+ /**
110
+ * One endpoint-credential edit: a string SETS the field, `null` (or a blank
111
+ * string) CLEARS it, and an absent key LEAVES it. Clearing lets the
112
+ * deployment-config layer answer for that field again (the merge is per
113
+ * field, file over config).
114
+ */
115
+ export interface LocalAgentEndpointAuthFields {
116
+ readonly baseUrl?: string | null;
117
+ readonly apiKey?: string | null;
118
+ readonly authToken?: string | null;
119
+ }
97
120
  /** One harness's auth-status snapshot, shared by the command and Remote channels. */
98
121
  export interface LocalAgentStatus {
99
122
  /** Harness command prefix. */
@@ -630,6 +653,14 @@ export interface LocalAgentMemberMessage {
630
653
  parentSessionId: string;
631
654
  /** The sender's delegation view, recorded as the delivery's provenance. */
632
655
  provenance: Record<string, string>;
656
+ /** `task` (default): a claiming room reports the reply back; `notify`: FYI only. */
657
+ kind?: 'task' | 'notify';
658
+ /** The message id this one answers (e.g. `m12`). */
659
+ inReplyTo?: string;
660
+ /** What the sender waits for before acting on the result (a claiming room's report gate reads it). */
661
+ note?: string;
662
+ /** `new` or a task number (a claiming room files the message under it). */
663
+ task?: string;
633
664
  }
634
665
  /**
635
666
  * The room gate's answer to a member notification, passed back to the sender
@@ -638,7 +669,7 @@ export interface LocalAgentMemberMessage {
638
669
  * DECLINE is not a value: the gate throws (the parent session is not a room
639
670
  * this instance manages) and the family direct-sends.
640
671
  */
641
- export type RoomMemberMessageReceipt = 'sent' | 'pending-confirm' | 'busy';
672
+ export type RoomMemberMessageReceipt = 'sent' | 'pending-confirm' | 'busy' | `sent ${string}` | `pending-confirm ${string}`;
642
673
  /**
643
674
  * Duck-typed shape of the room service's member-message gate, probed via
644
675
  * `ctx.get('room')` — deliberately NO import of any room package, so the
@@ -646,7 +677,7 @@ export type RoomMemberMessageReceipt = 'sent' | 'pending-confirm' | 'busy';
646
677
  * independence-checker sanction: room absent or declining is invisible to the
647
678
  * family path. Room implements this shape to own the dispatch gate.
648
679
  */
649
- export type MemberRoomCommandName = 'room_plan' | 'room_read' | 'room_invite' | 'room_message';
680
+ export type MemberRoomCommandName = 'room_plan' | 'room_read' | 'room_invite' | 'room_message' | 'ask_user_question' | 'room_help';
650
681
  export interface MemberRoomCommand {
651
682
  name: MemberRoomCommandName;
652
683
  arguments: Record<string, unknown>;
@@ -658,6 +689,23 @@ export interface RoomMemberMessageGate {
658
689
  }): Promise<boolean>;
659
690
  receiveMemberCommand?(actor: LocalAgentMemberRun, command: MemberRoomCommand): Promise<MemberMessageOutcome>;
660
691
  receiveMemberMessage(message: LocalAgentMemberMessage): Promise<RoomMemberMessageReceipt>;
692
+ /**
693
+ * The member's mode (a host agent preset id) whose plugin tools its bridge
694
+ * offers, with the member's working directory; undefined = no mode tools.
695
+ */
696
+ memberMode?(member: LocalAgentMemberRun): Promise<{
697
+ readonly preset: string;
698
+ readonly cwd?: string;
699
+ } | undefined>;
700
+ /**
701
+ * A member's mode tool needs the human's approval: the room asks in its
702
+ * conversation and answers with the registry's approval outcome.
703
+ */
704
+ askMemberApproval?(member: LocalAgentMemberRun, request: {
705
+ readonly toolName: string;
706
+ readonly reason?: string;
707
+ readonly signal?: AbortSignal;
708
+ }): Promise<'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'>;
661
709
  }
662
710
  /**
663
711
  * Outcome of one `member_message` delivery. Channel-level failures (unknown or
@@ -696,14 +744,23 @@ export interface LocalAgentMemberRun {
696
744
  /**
697
745
  * What one delegation tool call intends for the provider's next `start()`.
698
746
  * The tool stages exactly one intent per call before awaiting
699
- * `ctx.subagents.start()`, and the provider consumes exactly one per start,
700
- * so a per-(parent, provider) FIFO keeps fresh and resume rounds paired even
701
- * under parallel delegation.
747
+ * `ctx.subagents.startActivation()`, and the provider consumes exactly one per
748
+ * start, so a per-(parent, provider) FIFO keeps fresh and resume rounds
749
+ * paired even under parallel delegation.
702
750
  */
703
751
  export type LocalAgentDelegationIntent = {
704
752
  readonly kind: 'fresh';
705
753
  readonly onAdmitted?: () => void | Promise<void>;
706
754
  readonly preparedMemberId?: string;
755
+ /**
756
+ * The dsh child session id the caller minted for this round (facade/tool
757
+ * fresh starts). Host 0.2.1 activation semantics forbid a provider run id
758
+ * that names a live Session (`startActivation` rejects it as
759
+ * DUPLICATE_CHILD), so the provider's returned run carries a per-execution
760
+ * id and the durable child identity rides this channel instead. Absent
761
+ * means the provider mints one (a direct start with no staged intent).
762
+ */
763
+ readonly childSessionId?: string;
707
764
  readonly effort?: string;
708
765
  readonly configurationLock?: string;
709
766
  /**
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "local-agent",
9
9
  "cli"
10
10
  ],
11
- "version": "0.2.0-rc.1",
11
+ "version": "0.2.0",
12
12
  "type": "module",
13
13
  "main": "lib/index.js",
14
14
  "types": "lib/types/index.d.ts",
@@ -20,10 +20,6 @@
20
20
  "types": "./lib/types/index.d.ts",
21
21
  "default": "./lib/index.js"
22
22
  },
23
- "./invariant": {
24
- "types": "./lib/types/invariant.d.ts",
25
- "default": "./lib/invariant.js"
26
- },
27
23
  "./tool-factory": {
28
24
  "types": "./lib/types/tool-factory.d.ts",
29
25
  "default": "./lib/tool-factory.js"
@@ -55,6 +51,7 @@
55
51
  "@deepseek-ai/dsh-client-locale",
56
52
  "@deepseek-ai/dsh-client-ui-conversation",
57
53
  "@deepseek-ai/dsh-client-ui-primitives",
54
+ "@deepseek-ai/dsh-client-ui-settings",
58
55
  "@deepseek-ai/dsh-client-ui-chat"
59
56
  ],
60
57
  "platform": "web"
@@ -62,9 +59,13 @@
62
59
  "bundle": {
63
60
  "patch": "./cordis.patch.yml"
64
61
  },
62
+ "references": [
63
+ "@khorsheed/dsh-room"
64
+ ],
65
65
  "compat": {
66
66
  "minHost": "0.1.5-rc.1",
67
- "verifiedHost": "0.1.5-rc.1"
67
+ "verifiedHost": "0.2.1-rc.1",
68
+ "notes": "The「本机 Agent 委派」family card (one Plugins-page plugins.item entry rendering each harness's registered settings section) mounts only on host 0.1.7-alpha.1+ (the Plugins page plugins.item seat, gated by configForms.whileServed on the served local-agent namespace); older hosts render no card, and companions on such a composition keep their own standalone cards — every other surface is unchanged."
68
69
  }
69
70
  },
70
71
  "files": [
@@ -78,28 +79,31 @@
78
79
  "CHANGELOG.md"
79
80
  ],
80
81
  "license": "MIT",
82
+ "engines": {
83
+ "dsh": ">=0.1.5-rc.1 <0.3.0-0"
84
+ },
81
85
  "dependencies": {
82
86
  "zod": "^4.4.3"
83
87
  },
84
88
  "peerDependencies": {
85
89
  "@deepseek-ai/cordis": "^4.0.1",
86
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || ^0.2.0-rc.1",
87
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6 || ^0.2.0-rc.1",
88
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6 || ^0.2.0-rc.1",
89
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6 || ^0.2.0-rc.1",
90
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || ^0.2.0-rc.1",
91
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.6 || ^0.2.0-rc.1",
92
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6 || ^0.2.0-rc.1",
93
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.2.0-rc.1",
94
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.2.0-rc.1",
95
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.2.0-rc.1",
96
- "@deepseek-ai/dsh-subagent": "^0.1.0-rc.6 || ^0.2.0-rc.1",
97
- "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6 || ^0.2.0-rc.1",
98
- "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.6 || ^0.2.0-rc.1",
99
- "@deepseek-ai/dsh-tool-todo": "^0.1.0-rc.6 || ^0.2.0-rc.1",
100
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.2.0-rc.1",
101
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6 || ^0.2.0-rc.1",
102
- "@deepseek-ai/dsh-client-ui-chat": "^0.1.0-rc.6 || ^0.2.0-rc.1"
90
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
91
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
92
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
93
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
94
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
95
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
96
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
97
+ "@deepseek-ai/dsh-scope": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
98
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
99
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
100
+ "@deepseek-ai/dsh-subagent": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
101
+ "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
102
+ "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
103
+ "@deepseek-ai/dsh-tool-todo": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
104
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
105
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
106
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1"
103
107
  },
104
108
  "peerDependenciesMeta": {
105
109
  "@deepseek-ai/dsh-token-meter": {