@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.
- package/CHANGELOG.md +31 -0
- package/README.en.md +23 -164
- package/README.i18n.yaml +2 -2
- package/README.md +23 -164
- package/lib/client.js +23707 -4768
- package/lib/index.js +827 -62
- package/lib/member-bridge.js +195 -25
- package/lib/tool-factory.js +52 -19
- package/lib/typert.host.js +1440 -353
- package/lib/typert.remote-client.d.ts +43 -3
- package/lib/typert.remote-client.js +973 -335
- package/lib/types/client/FamilyCard.d.ts +37 -0
- package/lib/types/client/FamilyCard.js +55 -0
- package/lib/types/client/HarnessModelPicker.d.ts +21 -1
- package/lib/types/client/HarnessModelPicker.js +31 -3
- package/lib/types/client/MemberLiveNode.js +38 -4
- package/lib/types/client/ModelListSection.d.ts +49 -0
- package/lib/types/client/ModelListSection.js +132 -0
- package/lib/types/client/ProviderAuthBlock.d.ts +10 -2
- package/lib/types/client/ProviderAuthBlock.js +13 -6
- package/lib/types/client/auth-status.d.ts +6 -0
- package/lib/types/client/auth-status.js +8 -0
- package/lib/types/client/harness-cards.d.ts +72 -0
- package/lib/types/client/harness-cards.js +57 -0
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.d.ts +32 -7
- package/lib/types/client/index.js +66 -9
- package/lib/types/client/locales.d.ts +25 -1
- package/lib/types/client/locales.js +50 -2
- package/lib/types/client/model-directory-scope.d.ts +14 -0
- package/lib/types/client/model-directory-scope.js +20 -0
- package/lib/types/gateway.d.ts +49 -1
- package/lib/types/gateway.js +89 -0
- package/lib/types/index.d.ts +91 -38
- package/lib/types/index.js +190 -58
- package/lib/types/live-stream.d.ts +11 -0
- package/lib/types/live-stream.js +26 -0
- package/lib/types/member-bridge.d.ts +11 -1
- package/lib/types/member-bridge.js +81 -13
- package/lib/types/member-channel.d.ts +44 -1
- package/lib/types/member-channel.js +65 -2
- package/lib/types/member-controls.d.ts +8 -0
- package/lib/types/member-controls.js +15 -2
- package/lib/types/mode-skills.d.ts +18 -0
- package/lib/types/mode-skills.js +56 -0
- package/lib/types/model-list.d.ts +55 -0
- package/lib/types/model-list.js +101 -0
- package/lib/types/preset-tool-host.d.ts +70 -0
- package/lib/types/preset-tool-host.js +210 -0
- package/lib/types/tool-factory.d.ts +9 -3
- package/lib/types/tool-factory.js +55 -18
- package/lib/types/types.d.ts +62 -5
- package/package.json +27 -23
- package/lib/invariant.js +0 -85
- package/lib/types/invariant.d.ts +0 -16
- 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.
|
|
18
|
-
* events (`subagent/start`/`subagent/end`) and
|
|
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.
|
|
18
|
-
* events (`subagent/start`/`subagent/end`) and
|
|
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
|
|
101
|
-
*
|
|
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
|
-
|
|
274
|
-
|
|
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(
|
|
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
|
|
292
|
-
//
|
|
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}${
|
|
334
|
+
text: `${RESUME_HINT_PREFIX}${childSessionId}"`,
|
|
298
335
|
}],
|
|
299
336
|
};
|
|
300
337
|
}
|
package/lib/types/types.d.ts
CHANGED
|
@@ -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.
|
|
700
|
-
* so a per-(parent, provider) FIFO keeps fresh and resume rounds
|
|
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
|
|
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
|
|
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-
|
|
93
|
-
"@deepseek-ai/dsh-
|
|
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": {
|