dsh-agent-plugins-market 0.6.2 → 0.7.1
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/README.md +117 -111
- package/README.zh.md +112 -98
- package/client/client.js +28533 -1511
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +188 -0
- package/lib/application/catalog.js +229 -1109
- package/lib/application/details.js +19 -8
- package/lib/application/install-store.js +95 -0
- package/lib/application/lsp-service.js +97 -0
- package/lib/application/mcp-service.js +263 -0
- package/lib/application/panel-resources.js +43 -14
- package/lib/application/ports.js +21 -0
- package/lib/application/project-agent-roles.js +8 -2
- package/lib/application/snapshot-cache.js +160 -0
- package/lib/application/source-store.js +521 -0
- package/lib/catalog/archive.js +35 -30
- package/lib/catalog/component-files.js +14 -6
- package/lib/catalog/fs-probes.js +43 -0
- package/lib/catalog/manifests.js +53 -92
- package/lib/catalog/native-project.js +2 -2
- package/lib/catalog/paths.js +74 -55
- package/lib/catalog/plugin-variables.js +38 -0
- package/lib/catalog/project-config.js +2 -1
- package/lib/catalog/project-hooks.js +23 -2
- package/lib/catalog/scan-resolvers.js +29 -45
- package/lib/catalog/skills-parse.js +61 -10
- package/lib/catalog/source-catalog.js +10 -4
- package/lib/catalog/suite-components.js +7 -7
- package/lib/catalog/suite-scanner.js +3 -1
- package/lib/catalog/surfaces.js +5 -26
- package/lib/catalog/validate.js +7 -11
- package/lib/contracts/market.js +2 -0
- package/lib/contracts/settings.js +63 -0
- package/lib/index.js +115 -217
- package/lib/model/layouts.js +10 -1
- package/lib/model/types.js +26 -3
- package/lib/routes.js +57 -15
- package/lib/runtime/agent-role-names.js +70 -0
- package/lib/runtime/agent-role-router.js +142 -106
- package/lib/runtime/commands-mounts.js +55 -11
- package/lib/runtime/deadline.js +54 -0
- package/lib/runtime/dynamic-context.js +140 -0
- package/lib/runtime/feedback-tool.js +101 -46
- package/lib/runtime/hooks-mounts.js +9 -11
- package/lib/runtime/host-locale.js +49 -24
- package/lib/runtime/lsp-direct-config.js +19 -19
- package/lib/runtime/lsp-mounts.js +182 -70
- package/lib/runtime/lsp-server-state.js +4 -4
- package/lib/runtime/lsp-status.js +1 -1
- package/lib/runtime/mcp-auth-record.js +42 -0
- package/lib/runtime/mcp-backend.js +14 -14
- package/lib/runtime/mcp-client/config.js +8 -1
- package/lib/runtime/mcp-client/connection.js +38 -43
- package/lib/runtime/mcp-client/host-contract.js +10 -0
- package/lib/runtime/mcp-client/host-seams.js +8 -88
- package/lib/runtime/mcp-client/oauth.js +5 -7
- package/lib/runtime/mcp-client/plugin-identity.js +28 -0
- package/lib/runtime/mcp-client/projection.js +202 -0
- package/lib/runtime/mcp-client/tools.js +4 -189
- package/lib/runtime/mcp-config.js +36 -14
- package/lib/runtime/mcp-direct-config.js +33 -16
- package/lib/runtime/mcp-mounts.js +35 -75
- package/lib/runtime/mcp-overrides.js +5 -5
- package/lib/runtime/mcp-redaction.js +3 -2
- package/lib/runtime/mcp-status.js +7 -8
- package/lib/runtime/mount-lifecycle.js +73 -0
- package/lib/runtime/profile-seam.js +321 -0
- package/lib/runtime/project-runtime.js +34 -17
- package/lib/runtime/reconcile-scheduler.js +97 -0
- package/lib/runtime/reconciler.js +6 -1
- package/lib/runtime/regions.js +0 -19
- package/lib/runtime/server-config.js +6 -4
- package/lib/runtime/settings-namespace.js +144 -0
- package/lib/runtime/skills-provider.js +38 -14
- package/lib/runtime/source-auto-update.js +66 -0
- package/lib/{model/state.js → runtime/state-store.js} +9 -11
- package/lib/runtime/storage-migration.js +35 -18
- package/lib/runtime/subagent-catalog.js +5 -4
- package/lib/runtime/timer-seat.js +80 -0
- package/lib/runtime/tool-registry-observer.js +20 -20
- package/lib/runtime/user-commands.js +7 -3
- package/lib/runtime/user-panels.js +125 -46
- package/lib/runtime/user-store.js +124 -69
- package/lib/types/application/catalog-context.d.ts +124 -0
- package/lib/types/application/catalog.d.ts +108 -304
- package/lib/types/application/install-store.d.ts +21 -0
- package/lib/types/application/lsp-service.d.ts +36 -0
- package/lib/types/application/mcp-service.d.ts +77 -0
- package/lib/types/application/panel-resources.d.ts +8 -0
- package/lib/types/application/ports.d.ts +72 -0
- package/lib/types/application/queries.d.ts +15 -26
- package/lib/types/application/snapshot-cache.d.ts +51 -0
- package/lib/types/application/source-store.d.ts +100 -0
- package/lib/types/catalog/archive.d.ts +3 -11
- package/lib/types/catalog/component-files.d.ts +4 -0
- package/lib/types/catalog/fs-probes.d.ts +10 -0
- package/lib/types/catalog/manifests.d.ts +17 -16
- package/lib/types/catalog/native-project.d.ts +2 -2
- package/lib/types/catalog/paths.d.ts +53 -15
- package/lib/types/catalog/plugin-variables.d.ts +16 -0
- package/lib/types/catalog/scan-pipeline.d.ts +3 -3
- package/lib/types/catalog/scan-resolvers.d.ts +3 -9
- package/lib/types/catalog/skills-parse.d.ts +13 -7
- package/lib/types/catalog/source-catalog.d.ts +10 -5
- package/lib/types/catalog/suite-scanner.d.ts +8 -6
- package/lib/types/catalog/validate.d.ts +0 -2
- package/lib/types/contracts/lsp-status.d.ts +35 -0
- package/lib/types/contracts/market.d.ts +13 -2
- package/lib/types/contracts/mcp-status.d.ts +4 -0
- package/lib/types/contracts/settings.d.ts +59 -0
- package/lib/types/model/layouts.d.ts +10 -0
- package/lib/types/model/types.d.ts +29 -5
- package/lib/types/runtime/agent-role-names.d.ts +6 -0
- package/lib/types/runtime/agent-role-router.d.ts +59 -52
- package/lib/types/runtime/commands-mounts.d.ts +15 -7
- package/lib/types/runtime/deadline.d.ts +17 -0
- package/lib/types/runtime/dynamic-context.d.ts +81 -0
- package/lib/types/runtime/feedback-tool.d.ts +33 -11
- package/lib/types/runtime/hooks-mounts.d.ts +1 -1
- package/lib/types/runtime/host-locale.d.ts +20 -2
- package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
- package/lib/types/runtime/lsp-mounts.d.ts +69 -19
- package/lib/types/runtime/lsp-status.d.ts +2 -1
- package/lib/types/runtime/mcp-auth-record.d.ts +29 -0
- package/lib/types/runtime/mcp-backend.d.ts +10 -14
- package/lib/types/runtime/mcp-client/config.d.ts +8 -2
- package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
- package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
- package/lib/types/runtime/mcp-client/oauth.d.ts +1 -1
- package/lib/types/runtime/mcp-client/plugin-identity.d.ts +16 -0
- package/lib/types/runtime/mcp-client/projection.d.ts +45 -0
- package/lib/types/runtime/mcp-client/tools.d.ts +5 -72
- package/lib/types/runtime/mcp-config.d.ts +25 -4
- package/lib/types/runtime/mcp-direct-config.d.ts +18 -4
- package/lib/types/runtime/mcp-mounts.d.ts +10 -15
- package/lib/types/runtime/mcp-overrides.d.ts +1 -1
- package/lib/types/runtime/mcp-status.d.ts +1 -1
- package/lib/types/runtime/mount-lifecycle.d.ts +53 -0
- package/lib/types/runtime/profile-seam.d.ts +64 -0
- package/lib/types/runtime/project-runtime.d.ts +10 -3
- package/lib/types/runtime/reconcile-scheduler.d.ts +45 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/regions.d.ts +1 -4
- package/lib/types/runtime/settings-namespace.d.ts +76 -0
- package/lib/types/runtime/skills-provider.d.ts +11 -2
- package/lib/types/runtime/source-auto-update.d.ts +32 -0
- package/lib/types/{model/state.d.ts → runtime/state-store.d.ts} +2 -2
- package/lib/types/runtime/storage-migration.d.ts +5 -3
- package/lib/types/runtime/subagent-catalog.d.ts +2 -0
- package/lib/types/runtime/timer-seat.d.ts +51 -0
- package/lib/types/runtime/tool-registry-observer.d.ts +12 -6
- package/lib/types/runtime/user-panels.d.ts +69 -19
- package/lib/types/runtime/user-store.d.ts +67 -20
- package/package.json +68 -24
- package/schemas/README.md +1 -1
package/lib/routes.js
CHANGED
|
@@ -130,7 +130,7 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
130
130
|
}
|
|
131
131
|
});
|
|
132
132
|
post(MARKET_ROUTES.addSource, async (body) => {
|
|
133
|
-
const url =
|
|
133
|
+
const url = textField(body['url'] ?? '', 'source url').trim();
|
|
134
134
|
if (url === '')
|
|
135
135
|
throw new Error('missing source url');
|
|
136
136
|
const local = body['local'] === true;
|
|
@@ -145,11 +145,11 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
145
145
|
if (!url.startsWith('~/') && url !== '~' && !isAbsolute(expanded))
|
|
146
146
|
throw new Error('local source url must be an absolute path or start with ~/');
|
|
147
147
|
}
|
|
148
|
-
const branch = body['branch'];
|
|
148
|
+
const branch = body['branch'] === undefined ? undefined : textField(body['branch'], 'branch');
|
|
149
149
|
const sha256 = parseSha256(body['sha256']);
|
|
150
150
|
const source = await manager.addSource({
|
|
151
151
|
url: local || kind === 'local' ? expandHome(url) : url,
|
|
152
|
-
...(
|
|
152
|
+
...(branch !== undefined && branch.trim() !== '' ? { branch: branch.trim() } : {}),
|
|
153
153
|
...(local ? { local: true } : {}),
|
|
154
154
|
...(kind === undefined ? {} : { kind }),
|
|
155
155
|
...(sha256 === undefined ? {} : { sha256 })
|
|
@@ -162,13 +162,13 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
162
162
|
throw new Error('missing source id');
|
|
163
163
|
const patch = {};
|
|
164
164
|
if (body['url'] !== undefined) {
|
|
165
|
-
const url =
|
|
165
|
+
const url = textField(body['url'], 'source url').trim();
|
|
166
166
|
if (url === '')
|
|
167
167
|
throw new Error('missing source url');
|
|
168
168
|
patch.url = url;
|
|
169
169
|
}
|
|
170
170
|
if (body['branch'] !== undefined)
|
|
171
|
-
patch.branch =
|
|
171
|
+
patch.branch = textField(body['branch'], 'branch').trim();
|
|
172
172
|
if (body['local'] !== undefined)
|
|
173
173
|
patch.local = body['local'] === true;
|
|
174
174
|
const kind = parseSourceKind(body['kind']);
|
|
@@ -197,8 +197,8 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
197
197
|
return {};
|
|
198
198
|
});
|
|
199
199
|
post(MARKET_ROUTES.refreshSource, async (body) => {
|
|
200
|
-
const id = body['id'];
|
|
201
|
-
await manager.refreshSource(
|
|
200
|
+
const id = body['id'] === undefined ? undefined : textField(body['id'], 'source id');
|
|
201
|
+
await manager.refreshSource(id === '' ? undefined : id);
|
|
202
202
|
return {};
|
|
203
203
|
});
|
|
204
204
|
post(MARKET_ROUTES.install, async (body) => {
|
|
@@ -257,6 +257,18 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
257
257
|
await manager.setMcpOverride(sourceId, suiteId, serverKey, sanitized ?? null);
|
|
258
258
|
return {};
|
|
259
259
|
});
|
|
260
|
+
// Enable or disable one declared MCP server. Addressed by the same
|
|
261
|
+
// source-qualified suite id the status rows carry, so the card can toggle a
|
|
262
|
+
// server without holding the source and suite ids apart.
|
|
263
|
+
post(MARKET_ROUTES.setMcpServerEnabled, async (body) => {
|
|
264
|
+
const suiteKey = textField(body['suiteId'] ?? '', 'MCP suite id');
|
|
265
|
+
const serverKey = textField(body['serverKey'] ?? '', 'MCP server key');
|
|
266
|
+
const enabled = body['enabled'];
|
|
267
|
+
if (typeof enabled !== 'boolean')
|
|
268
|
+
throw new Error('missing boolean enabled');
|
|
269
|
+
await manager.setMcpServerEnabled(suiteKey, serverKey, enabled);
|
|
270
|
+
return {};
|
|
271
|
+
});
|
|
260
272
|
// Manual MCP reconcile: retries failed mounts and clears residual tools
|
|
261
273
|
// without touching any catalog state.
|
|
262
274
|
post(MARKET_ROUTES.mcpRetry, async () => {
|
|
@@ -299,12 +311,24 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
299
311
|
return {};
|
|
300
312
|
});
|
|
301
313
|
post(`${MARKET_ROUTES.lspServers}/enabled`, async (body) => {
|
|
302
|
-
const id =
|
|
314
|
+
const id = textField(body['id'] ?? '', 'LSP server id');
|
|
303
315
|
if (id === '')
|
|
304
316
|
throw new Error('missing LSP server id');
|
|
305
|
-
|
|
317
|
+
const enabled = body['enabled'];
|
|
318
|
+
if (typeof enabled !== 'boolean')
|
|
319
|
+
throw new Error('missing boolean enabled');
|
|
320
|
+
await manager.setLspServerEnabled(id, enabled);
|
|
306
321
|
return {};
|
|
307
322
|
});
|
|
323
|
+
// Upgrade repair: drop the hand-written profile layer an older release told
|
|
324
|
+
// the user to add for LSP. It edits a file the user owns, so the profile
|
|
325
|
+
// name is matched against what is on disk and nothing else is touched.
|
|
326
|
+
post(MARKET_ROUTES.migrateLspSeam, async (body) => {
|
|
327
|
+
const profile = textField(body['profile'] ?? '', 'profile name').trim();
|
|
328
|
+
if (profile === '')
|
|
329
|
+
throw new Error('missing profile name');
|
|
330
|
+
return { migration: await manager.migrateLegacyLspSeam(profile) };
|
|
331
|
+
});
|
|
308
332
|
// User panel CRUD (skills / commands / agent personas). The host web
|
|
309
333
|
// server matches exact pathnames, so the entry name rides the `name` query
|
|
310
334
|
// parameter (GET read, POST replace, POST .../delete remove). The routes
|
|
@@ -327,8 +351,8 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
327
351
|
sendJson(response, 200, { entry });
|
|
328
352
|
});
|
|
329
353
|
post(`${userPanelRoute(kind)}/create`, async (body) => {
|
|
330
|
-
const name =
|
|
331
|
-
const text =
|
|
354
|
+
const name = textField(body['name'] ?? '', 'entry name').trim();
|
|
355
|
+
const text = textField(body['text'] ?? '', 'entry text');
|
|
332
356
|
if (name === '')
|
|
333
357
|
throw new Error('missing entry name');
|
|
334
358
|
const entry = await storeOf(kind).create(name, text);
|
|
@@ -337,7 +361,7 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
337
361
|
});
|
|
338
362
|
post(`${userPanelRoute(kind)}/update`, async (body, request) => {
|
|
339
363
|
const name = queryOf(request).get('name') ?? '';
|
|
340
|
-
const text =
|
|
364
|
+
const text = textField(body['text'] ?? '', 'entry text');
|
|
341
365
|
if (name === '')
|
|
342
366
|
throw new Error('missing entry name');
|
|
343
367
|
await storeOf(kind).update(name, text);
|
|
@@ -345,7 +369,7 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
345
369
|
return {};
|
|
346
370
|
});
|
|
347
371
|
post(`${userPanelRoute(kind)}/delete`, async (body) => {
|
|
348
|
-
const name =
|
|
372
|
+
const name = textField(body['name'] ?? '', 'entry name');
|
|
349
373
|
if (name === '')
|
|
350
374
|
throw new Error('missing entry name');
|
|
351
375
|
await storeOf(kind).remove(name);
|
|
@@ -359,6 +383,23 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
359
383
|
dispose();
|
|
360
384
|
};
|
|
361
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Read a request field this route documents as a string. Bodies are arbitrary
|
|
388
|
+
* JSON, so the field may arrive as any value; anything but a string is
|
|
389
|
+
* rejected rather than coerced, because `String({})` is `"[object Object]"` —
|
|
390
|
+
* a non-empty value that every check downstream would accept and store as a
|
|
391
|
+
* source url, a branch name, or an entry name. Empty stays empty so each
|
|
392
|
+
* caller keeps owning whether that means "missing".
|
|
393
|
+
*/
|
|
394
|
+
function textField(value, label) {
|
|
395
|
+
if (typeof value !== 'string')
|
|
396
|
+
throw new Error(`${label} must be a string`);
|
|
397
|
+
return value;
|
|
398
|
+
}
|
|
399
|
+
/** Render an untrusted field for a diagnostic message. Never a value reader — see `textField`. */
|
|
400
|
+
function describe(value) {
|
|
401
|
+
return String(value);
|
|
402
|
+
}
|
|
362
403
|
function parseTarget(body) {
|
|
363
404
|
const sourceId = body['sourceId'];
|
|
364
405
|
const suiteId = body['suiteId'];
|
|
@@ -374,13 +415,13 @@ function parseSourceKind(raw) {
|
|
|
374
415
|
return undefined;
|
|
375
416
|
if (raw === 'git' || raw === 'local' || raw === 'archive')
|
|
376
417
|
return raw;
|
|
377
|
-
throw new Error(`invalid source kind "${
|
|
418
|
+
throw new Error(`invalid source kind "${describe(raw)}"`);
|
|
378
419
|
}
|
|
379
420
|
/** Parse an optional SHA-256 hex digest; rejects malformed values. */
|
|
380
421
|
function parseSha256(raw) {
|
|
381
422
|
if (raw === undefined || raw === null || raw === '')
|
|
382
423
|
return undefined;
|
|
383
|
-
const value =
|
|
424
|
+
const value = textField(raw, 'sha256').trim().toLowerCase();
|
|
384
425
|
if (!/^[0-9a-f]{64}$/.test(value))
|
|
385
426
|
throw new Error('sha256 must be a 64-character hex digest');
|
|
386
427
|
return value;
|
|
@@ -396,6 +437,7 @@ function sameOrigin(request) {
|
|
|
396
437
|
return false;
|
|
397
438
|
}
|
|
398
439
|
}
|
|
440
|
+
/** Parses the request body; `undefined` for an oversized, unparsable, or failed read. */
|
|
399
441
|
function readJsonBody(request) {
|
|
400
442
|
return new Promise(resolve => {
|
|
401
443
|
let size = 0;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* The `[source, suite, kind, name]` identity the host encodes into a discovery
|
|
4
|
+
* name, or `undefined` for a name that is not one — a user-authored role name
|
|
5
|
+
* is a plain string, and anything else carries no identity.
|
|
6
|
+
*/
|
|
7
|
+
function roleIdentity(name) {
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = JSON.parse(name);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (!Array.isArray(parsed))
|
|
16
|
+
return undefined;
|
|
17
|
+
const parts = parsed;
|
|
18
|
+
if (parts.length !== 4)
|
|
19
|
+
return undefined;
|
|
20
|
+
const [source, suite, kind, id] = parts;
|
|
21
|
+
if (typeof source !== 'string' || typeof suite !== 'string' || typeof kind !== 'string' || typeof id !== 'string')
|
|
22
|
+
return undefined;
|
|
23
|
+
return [source, suite, kind, id];
|
|
24
|
+
}
|
|
25
|
+
/** Internal IDs stay opaque to callers; only colliding resource names need qualification. */
|
|
26
|
+
export function namedAgentRoles(entries) {
|
|
27
|
+
const roles = entries.map(entry => {
|
|
28
|
+
const identity = roleIdentity(entry.name);
|
|
29
|
+
const base = identity === undefined ? entry.name : identity[3].replace(/\.agent$/, '');
|
|
30
|
+
return { entry, base, suite: identity?.[1] ?? 'user', source: identity?.[0] ?? 'user', level: 0 };
|
|
31
|
+
});
|
|
32
|
+
const baseCounts = new Map();
|
|
33
|
+
for (const role of roles)
|
|
34
|
+
baseCounts.set(role.base, (baseCounts.get(role.base) ?? 0) + 1);
|
|
35
|
+
for (const role of roles)
|
|
36
|
+
if ((baseCounts.get(role.base) ?? 0) > 1)
|
|
37
|
+
role.level = 1;
|
|
38
|
+
const candidate = (role) => {
|
|
39
|
+
if (role.level === 0)
|
|
40
|
+
return role.base;
|
|
41
|
+
const qualified = `${role.suite}/${role.base}`;
|
|
42
|
+
const fullyQualified = `${role.source}/${role.suite}/${role.base}`;
|
|
43
|
+
if (role.level === 1)
|
|
44
|
+
return qualified;
|
|
45
|
+
if (role.level === 2)
|
|
46
|
+
return fullyQualified;
|
|
47
|
+
return `${fullyQualified}~${createHash('sha256').update(role.entry.name).digest('hex')}`;
|
|
48
|
+
};
|
|
49
|
+
// Recheck all names: a qualified collision may overlap another role's literal name.
|
|
50
|
+
for (;;) {
|
|
51
|
+
const groups = new Map();
|
|
52
|
+
for (const role of roles) {
|
|
53
|
+
const key = candidate(role);
|
|
54
|
+
groups.set(key, [...(groups.get(key) ?? []), role]);
|
|
55
|
+
}
|
|
56
|
+
let changed = false;
|
|
57
|
+
for (const group of groups.values()) {
|
|
58
|
+
if (group.length < 2)
|
|
59
|
+
continue;
|
|
60
|
+
for (const role of group) {
|
|
61
|
+
if (role.level < 3) {
|
|
62
|
+
role.level++;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!changed)
|
|
68
|
+
return roles.map(role => ({ ...role.entry, callName: candidate(role) }));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -4,7 +4,14 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
5
|
import { mountSubagentCatalog } from './subagent-catalog.js';
|
|
6
6
|
import { bindHostLocale } from './host-locale.js';
|
|
7
|
-
|
|
7
|
+
import { namedAgentRoles } from './agent-role-names.js';
|
|
8
|
+
import { expandPluginPaths } from '../catalog/plugin-variables.js';
|
|
9
|
+
export const AGENT_ROLE_TOOL_NAME = 'subagent_run';
|
|
10
|
+
/**
|
|
11
|
+
* Absolute delegation-depth cap applied to every role child. The host default is
|
|
12
|
+
* the same value; keeping it explicit preserves the shipped recursion budget.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_AGENT_ROLE_DEPTH = 3;
|
|
8
15
|
function optionalText(value, key) {
|
|
9
16
|
if (value === undefined)
|
|
10
17
|
return undefined;
|
|
@@ -12,19 +19,7 @@ function optionalText(value, key) {
|
|
|
12
19
|
throw new Error(`agent metadata ${key} must be a non-empty string`);
|
|
13
20
|
return value.trim();
|
|
14
21
|
}
|
|
15
|
-
|
|
16
|
-
if (value === undefined)
|
|
17
|
-
return undefined;
|
|
18
|
-
const entries = typeof value === 'string' ? value.split(',') : Array.isArray(value) ? value : [value];
|
|
19
|
-
return [
|
|
20
|
-
...new Set(entries.map(entry => {
|
|
21
|
-
if (typeof entry !== 'string' || !/^[\w.:-]+$/.test(entry.trim()))
|
|
22
|
-
throw new Error(`agent metadata ${key} must contain tool names (comma-separated or a YAML array)`);
|
|
23
|
-
return entry.trim();
|
|
24
|
-
}))
|
|
25
|
-
];
|
|
26
|
-
}
|
|
27
|
-
/** Strict YAML parsing prevents malformed routing/restriction metadata from silently inheriting wider privileges. */
|
|
22
|
+
/** Strict YAML parsing prevents malformed routing metadata from silently inheriting wider privileges. */
|
|
28
23
|
export function parseAgentRole(text) {
|
|
29
24
|
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
|
|
30
25
|
if (match === null) {
|
|
@@ -32,7 +27,11 @@ export function parseAgentRole(text) {
|
|
|
32
27
|
throw new Error('agent frontmatter is not closed');
|
|
33
28
|
return { content: text, disabled: false };
|
|
34
29
|
}
|
|
35
|
-
|
|
30
|
+
// A matched block always carries its body; the check keeps the type honest.
|
|
31
|
+
const frontmatter = match[1];
|
|
32
|
+
if (frontmatter === undefined)
|
|
33
|
+
throw new Error('agent frontmatter is not closed');
|
|
34
|
+
const parsed = parseYaml(frontmatter);
|
|
36
35
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
37
36
|
throw new Error('agent frontmatter must be a YAML object');
|
|
38
37
|
const metadata = parsed;
|
|
@@ -56,51 +55,71 @@ export function parseAgentRole(text) {
|
|
|
56
55
|
...(provider === undefined ? {} : { provider }),
|
|
57
56
|
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
58
57
|
...(title === undefined ? {} : { title }),
|
|
59
|
-
...(description === undefined ? {} : { description })
|
|
60
|
-
...(metadata.tools === undefined ? {} : { tools: toolList(metadata.tools, 'tools') }),
|
|
61
|
-
...(metadata.disallowedTools === undefined ? {} : { disallowedTools: toolList(metadata.disallowedTools, 'disallowedTools') })
|
|
58
|
+
...(description === undefined ? {} : { description })
|
|
62
59
|
};
|
|
63
60
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
function parentRouteOf(parent) {
|
|
62
|
+
const agent = parent;
|
|
63
|
+
return agent?.session?.requestHeader?.()?.config ?? agent?.options ?? {};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the child LLM options one card may contribute.
|
|
67
|
+
*
|
|
68
|
+
* A card route is advisory. Only an exact `provider` plus `model` pair is
|
|
69
|
+
* applied, and only when the live LLM runtime accepts it; every other
|
|
70
|
+
* declaration — a bare id, a Claude alias, a lone `provider`, an unsupported
|
|
71
|
+
* effort — degrades to inheriting the parent route and reports a diagnostic.
|
|
72
|
+
* @param policy - the card's parsed routing metadata.
|
|
73
|
+
* @param parent - the calling Agent whose route an unusable declaration falls back to.
|
|
74
|
+
* @param llm - live LLM runtime owning provider, model and effort validation.
|
|
75
|
+
* @param signal - tool-call cancellation signal.
|
|
76
|
+
* @param subject - role name used in diagnostics.
|
|
77
|
+
* @param diagnose - diagnostic sink; degradation is reported, never silent.
|
|
78
|
+
* @returns the options to send, or undefined for pure inheritance.
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveAgentOptions(policy, parent, llm, signal, subject, diagnose) {
|
|
81
|
+
// One value, so a half-specified route can never be forwarded downstream.
|
|
82
|
+
const requested = policy.provider === undefined || policy.model === undefined ? undefined : { provider: policy.provider, model: policy.model };
|
|
83
|
+
if (requested === undefined && (policy.provider !== undefined || policy.model !== undefined)) {
|
|
84
|
+
const declared = [policy.provider, policy.model].filter(value => value !== undefined).join('/');
|
|
85
|
+
diagnose(`agent "${subject}": route "${declared}" needs an exact provider and model pair; it is ignored and the child inherits the parent route`);
|
|
73
86
|
}
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
const effort = policy.reasoningEffort;
|
|
88
|
+
if (effort === undefined && requested === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
const inherited = parentRouteOf(parent);
|
|
91
|
+
const provider = requested?.provider ?? inherited.provider;
|
|
92
|
+
const model = requested?.model ?? inherited.model;
|
|
93
|
+
if (provider === undefined || model === undefined) {
|
|
94
|
+
/* v8 ignore next 3 -- an effective route is always known once a parent Agent ran a request. */
|
|
95
|
+
if (effort !== undefined)
|
|
96
|
+
diagnose(`agent "${subject}": reasoning effort "${effort}" has no effective route to validate against; it is ignored`);
|
|
97
|
+
return undefined;
|
|
80
98
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
const routeChanged = provider !== inherited.provider || model !== inherited.model;
|
|
100
|
+
const effectiveEffort = effort ?? (routeChanged ? undefined : inherited.reasoningEffort);
|
|
101
|
+
try {
|
|
102
|
+
await llm.resolveCallConfig({ provider, model, ...(effectiveEffort === undefined ? {} : { reasoningEffort: effectiveEffort }) }, signal);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
signal.throwIfAborted();
|
|
106
|
+
diagnose(`agent "${subject}": route ${provider}/${model} is unusable (${String(error)}); it is ignored and the child inherits the parent route`);
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
// Cancellation raised during the adapter lookup must not create a child.
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
return {
|
|
112
|
+
...(requested ?? {}),
|
|
113
|
+
...(effort === undefined ? {} : { reasoningEffort: effort })
|
|
114
|
+
};
|
|
96
115
|
}
|
|
97
116
|
/** Stable summaries; invalid declarations are excluded and transient read failures abort publication. */
|
|
98
|
-
export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
117
|
+
export async function agentRoleCatalog(entries, signal, diagnose = () => { }, projectDir) {
|
|
99
118
|
const summaries = [];
|
|
100
119
|
const counts = new Map();
|
|
101
120
|
for (const entry of entries)
|
|
102
121
|
counts.set(entry.name, (counts.get(entry.name) ?? 0) + 1);
|
|
103
|
-
for (const entry of entries) {
|
|
122
|
+
for (const entry of namedAgentRoles(entries)) {
|
|
104
123
|
signal.throwIfAborted();
|
|
105
124
|
if (entry.disabled)
|
|
106
125
|
continue;
|
|
@@ -119,7 +138,7 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
|
119
138
|
}
|
|
120
139
|
let policy;
|
|
121
140
|
try {
|
|
122
|
-
policy = parseAgentRole(text);
|
|
141
|
+
policy = expandPolicyPaths(parseAgentRole(text), entry, projectDir);
|
|
123
142
|
}
|
|
124
143
|
catch (error) {
|
|
125
144
|
diagnose(`${entry.path}: ${String(error)}`);
|
|
@@ -128,92 +147,109 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
|
128
147
|
if (policy.disabled)
|
|
129
148
|
continue;
|
|
130
149
|
const description = (policy.description ?? entry.description).replaceAll(/\s+/g, ' ').trim();
|
|
150
|
+
// Advertise a route only when the executor would apply it.
|
|
151
|
+
const exact = policy.provider !== undefined && policy.model !== undefined;
|
|
131
152
|
summaries.push({
|
|
132
|
-
name: entry.
|
|
133
|
-
|
|
153
|
+
name: entry.callName,
|
|
154
|
+
roleId: entry.name,
|
|
155
|
+
title: policy.title ?? entry.title ?? entry.callName,
|
|
134
156
|
description: description.length <= 500 ? description : `${description.slice(0, 497)}...`,
|
|
135
|
-
...(
|
|
136
|
-
...(policy.model === undefined ? {} : { model: policy.model }),
|
|
157
|
+
...(exact ? { provider: policy.provider, model: policy.model } : {}),
|
|
137
158
|
...(policy.reasoningEffort === undefined ? {} : { reasoningEffort: policy.reasoningEffort })
|
|
138
159
|
});
|
|
139
160
|
}
|
|
140
161
|
signal.throwIfAborted();
|
|
141
162
|
return summaries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
142
163
|
}
|
|
143
|
-
/**
|
|
144
|
-
|
|
164
|
+
/** A suite card carries the same path variables as every other surface its author wrote for. */
|
|
165
|
+
function expandPolicyPaths(policy, entry, projectDir) {
|
|
166
|
+
if (entry.suiteRoot === undefined && entry.suiteData === undefined && projectDir === undefined)
|
|
167
|
+
return policy;
|
|
168
|
+
const context = {
|
|
169
|
+
...(entry.suiteRoot === undefined ? {} : { root: entry.suiteRoot }),
|
|
170
|
+
...(entry.suiteData === undefined ? {} : { data: entry.suiteData }),
|
|
171
|
+
...(projectDir === undefined ? {} : { projectDir })
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
...policy,
|
|
175
|
+
content: expandPluginPaths(policy.content, context),
|
|
176
|
+
...(policy.title === undefined ? {} : { title: expandPluginPaths(policy.title, context) }),
|
|
177
|
+
...(policy.description === undefined ? {} : { description: expandPluginPaths(policy.description, context) })
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Read the same declaration for catalog summaries and execution, including inline resources. */
|
|
181
|
+
export async function readAgentRole(entry, projectDir) {
|
|
182
|
+
return expandPolicyPaths(parseAgentRole(entry.rawText ?? (await readFile(entry.path, 'utf8'))), entry, projectDir);
|
|
183
|
+
}
|
|
184
|
+
/** The calling session's directory, which is what a card's `${CLAUDE_PROJECT_DIR}` names. */
|
|
185
|
+
function sessionCwd(agent) {
|
|
186
|
+
const cwd = agent?.session?.header?.cwd;
|
|
187
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : undefined;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Start one role child and return its durable id without waiting for the result.
|
|
191
|
+
* Resume state, steering, and the settlement notice belong to the host's
|
|
192
|
+
* continuation manager; this executor owns only role resolution.
|
|
193
|
+
*/
|
|
194
|
+
export async function executeAgentRole(host, listRoles, agentName, prompt, parent, signal, diagnose = () => { }) {
|
|
145
195
|
if (parent === undefined)
|
|
146
|
-
throw new Error('
|
|
196
|
+
throw new Error('subagent_run requires a calling agent');
|
|
147
197
|
if (prompt.trim() === '')
|
|
148
|
-
throw new Error('
|
|
198
|
+
throw new Error('subagent_run requires a non-empty prompt');
|
|
149
199
|
signal.throwIfAborted();
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
throw new Error(`agent
|
|
153
|
-
const entry = entries[0];
|
|
200
|
+
const [entry, ...duplicates] = namedAgentRoles(await listRoles(parent)).filter(candidate => candidate.callName === agentName);
|
|
201
|
+
if (entry === undefined || duplicates.length > 0)
|
|
202
|
+
throw new Error(`agent "${agentName}" is unavailable or ambiguous`);
|
|
154
203
|
if (entry.disabled)
|
|
155
|
-
throw new Error(`agent
|
|
156
|
-
const policy = await readAgentRole(entry);
|
|
204
|
+
throw new Error(`agent "${agentName}" is disabled`);
|
|
205
|
+
const policy = await readAgentRole(entry, sessionCwd(parent));
|
|
157
206
|
if (policy.disabled)
|
|
158
|
-
throw new Error(`agent
|
|
159
|
-
const agentOptions =
|
|
160
|
-
const parentAgent = parent;
|
|
161
|
-
const parentOptions = parentAgent.session?.requestHeader?.()?.config ?? parentAgent.options ?? {};
|
|
162
|
-
const provider = agentOptions.provider ?? parentOptions.provider;
|
|
163
|
-
const model = agentOptions.model ?? parentOptions.model;
|
|
164
|
-
if (provider === undefined || model === undefined)
|
|
165
|
-
throw new Error('subagents_run requires an effective provider and model');
|
|
166
|
-
const routeChanged = provider !== parentOptions.provider || model !== parentOptions.model;
|
|
167
|
-
const reasoningEffort = agentOptions.reasoningEffort ?? (routeChanged ? undefined : parentOptions.reasoningEffort);
|
|
168
|
-
await host.llm.resolveCallConfig({ provider, model, ...(reasoningEffort === undefined ? {} : { reasoningEffort }) }, signal);
|
|
207
|
+
throw new Error(`agent "${agentName}" is disabled`);
|
|
208
|
+
const agentOptions = await resolveAgentOptions(policy, parent, host.llm, signal, agentName, diagnose);
|
|
169
209
|
signal.throwIfAborted();
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
toolFilter: {
|
|
182
|
-
...(policy.tools === undefined ? {} : { allow: policy.tools }),
|
|
183
|
-
...(policy.disallowedTools === undefined ? {} : { deny: policy.disallowedTools })
|
|
184
|
-
}
|
|
185
|
-
})
|
|
210
|
+
const started = await host.subagents.startContinuable({
|
|
211
|
+
provider: 'spawn',
|
|
212
|
+
label: agentName,
|
|
213
|
+
request: {
|
|
214
|
+
prompt: [{ type: 'text', text: prompt }],
|
|
215
|
+
parent,
|
|
216
|
+
maxDepth: MAX_AGENT_ROLE_DEPTH,
|
|
217
|
+
persona: policy.content,
|
|
218
|
+
...(agentOptions === undefined ? {} : { agentOptions })
|
|
219
|
+
},
|
|
220
|
+
signal
|
|
186
221
|
});
|
|
187
|
-
|
|
188
|
-
const result = await run.result;
|
|
189
|
-
if (result.stopReason !== 'completed')
|
|
190
|
-
throw new Error(`agent role "${name}" stopped: ${result.stopReason}; partial output: ${JSON.stringify(result.output)}`);
|
|
191
|
-
return { runId: run.id, output: result.output };
|
|
192
|
-
}
|
|
193
|
-
finally {
|
|
194
|
-
await run.dispose();
|
|
195
|
-
}
|
|
222
|
+
return { subagentId: started.childId };
|
|
196
223
|
}
|
|
197
224
|
/** Mount once after tools, llm and subagents are available; the returned disposer belongs to the plugin lifecycle. */
|
|
198
225
|
export function mountAgentRoleTool(ctx, listRoles, t = bindHostLocale(undefined)) {
|
|
199
226
|
const host = ctx;
|
|
200
227
|
const tool = defineTool({
|
|
201
228
|
name: AGENT_ROLE_TOOL_NAME,
|
|
202
|
-
description: 'Delegate a self-contained task to a role from the current subagent catalog
|
|
229
|
+
description: 'Delegate a self-contained task to a role from the current subagent catalog. The child starts without the parent conversation and runs ' +
|
|
230
|
+
'in the background: this returns a durable subagent id immediately. When the run settles the runtime sends you a notice carrying its ' +
|
|
231
|
+
"outcome and closing message, and send_message steers the child while it runs. The role's saved persona and model settings are applied automatically.",
|
|
203
232
|
parameters: {
|
|
204
|
-
|
|
233
|
+
agent: { type: 'string', required: true, description: 'Exact callable role name from the current subagent catalog.' },
|
|
205
234
|
prompt: { type: 'string', required: true, description: 'Complete task and context for the isolated child.' }
|
|
206
235
|
},
|
|
207
|
-
output: {
|
|
236
|
+
output: {
|
|
237
|
+
schema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
additionalProperties: false,
|
|
240
|
+
properties: { subagentId: { type: 'string', required: true } }
|
|
241
|
+
},
|
|
242
|
+
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }]
|
|
243
|
+
},
|
|
208
244
|
isConcurrencySafe: () => true,
|
|
209
245
|
async execute(args, exec) {
|
|
210
|
-
return executeAgentRole(host, listRoles, args.
|
|
246
|
+
return executeAgentRole(host, listRoles, args.agent, args.prompt, exec.agent, exec.signal, message => ctx.logger?.warn(message));
|
|
211
247
|
}
|
|
212
248
|
});
|
|
213
249
|
const disposeTool = host.tools.register(tool);
|
|
214
250
|
let disposeCatalog;
|
|
215
251
|
try {
|
|
216
|
-
disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message)), t);
|
|
252
|
+
disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message), sessionCwd(agent)), t);
|
|
217
253
|
}
|
|
218
254
|
catch (error) {
|
|
219
255
|
disposeTool();
|