dsh-agent-plugins-market 0.6.2 → 0.7.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/README.md +117 -111
- package/README.zh.md +112 -98
- package/client/client.js +547 -540
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +181 -0
- package/lib/application/catalog.js +217 -1110
- package/lib/application/details.js +6 -6
- package/lib/application/install-store.js +95 -0
- package/lib/application/lsp-service.js +97 -0
- package/lib/application/mcp-service.js +246 -0
- package/lib/application/panel-resources.js +29 -12
- package/lib/application/ports.js +21 -0
- package/lib/application/project-agent-roles.js +1 -1
- package/lib/application/snapshot-cache.js +160 -0
- package/lib/application/source-store.js +521 -0
- package/lib/catalog/archive.js +34 -29
- 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 +66 -55
- package/lib/catalog/project-config.js +2 -1
- package/lib/catalog/project-hooks.js +9 -1
- package/lib/catalog/scan-resolvers.js +29 -45
- package/lib/catalog/skills-parse.js +14 -10
- package/lib/catalog/source-catalog.js +3 -2
- package/lib/catalog/suite-components.js +5 -5
- 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 +1 -0
- package/lib/index.js +91 -208
- package/lib/model/types.js +26 -3
- package/lib/routes.js +45 -15
- package/lib/runtime/agent-role-names.js +70 -0
- package/lib/runtime/agent-role-router.js +116 -102
- package/lib/runtime/commands-mounts.js +9 -6
- package/lib/runtime/deadline.js +28 -0
- package/lib/runtime/feedback-tool.js +94 -43
- package/lib/runtime/hooks-mounts.js +9 -11
- package/lib/runtime/host-locale.js +10 -21
- package/lib/runtime/lsp-direct-config.js +19 -19
- package/lib/runtime/lsp-mounts.js +159 -68
- 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 +6 -5
- package/lib/runtime/mcp-client/config.js +0 -1
- package/lib/runtime/mcp-client/connection.js +32 -37
- package/lib/runtime/mcp-client/host-contract.js +10 -0
- 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 +29 -10
- 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 +11 -8
- package/lib/runtime/reconcile-scheduler.js +99 -0
- package/lib/runtime/reconciler.js +4 -0
- package/lib/runtime/server-config.js +6 -4
- package/lib/runtime/settings-namespace.js +136 -0
- package/lib/runtime/skills-provider.js +5 -3
- package/lib/runtime/source-auto-update.js +70 -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/user-commands.js +2 -2
- package/lib/runtime/user-panels.js +31 -21
- package/lib/runtime/user-store.js +12 -13
- package/lib/types/application/catalog-context.d.ts +118 -0
- package/lib/types/application/catalog.d.ts +99 -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 +71 -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 +13 -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 +47 -15
- 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 +0 -7
- package/lib/types/catalog/source-catalog.d.ts +3 -3
- 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 +3 -2
- package/lib/types/contracts/mcp-status.d.ts +4 -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 +54 -51
- package/lib/types/runtime/deadline.d.ts +18 -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 +1 -1
- package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
- package/lib/types/runtime/lsp-mounts.d.ts +62 -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 +7 -4
- package/lib/types/runtime/mcp-client/config.d.ts +0 -2
- package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
- 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/reconcile-scheduler.d.ts +45 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/settings-namespace.d.ts +76 -0
- package/lib/types/runtime/source-auto-update.d.ts +33 -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/user-panels.d.ts +8 -11
- package/lib/types/runtime/user-store.d.ts +2 -4
- package/package.json +48 -21
- package/schemas/README.md +1 -1
|
@@ -4,7 +4,13 @@ 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
|
+
export const AGENT_ROLE_TOOL_NAME = 'subagent_run';
|
|
9
|
+
/**
|
|
10
|
+
* Absolute delegation-depth cap applied to every role child. The host default is
|
|
11
|
+
* the same value; keeping it explicit preserves the shipped recursion budget.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_AGENT_ROLE_DEPTH = 3;
|
|
8
14
|
function optionalText(value, key) {
|
|
9
15
|
if (value === undefined)
|
|
10
16
|
return undefined;
|
|
@@ -12,19 +18,7 @@ function optionalText(value, key) {
|
|
|
12
18
|
throw new Error(`agent metadata ${key} must be a non-empty string`);
|
|
13
19
|
return value.trim();
|
|
14
20
|
}
|
|
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. */
|
|
21
|
+
/** Strict YAML parsing prevents malformed routing metadata from silently inheriting wider privileges. */
|
|
28
22
|
export function parseAgentRole(text) {
|
|
29
23
|
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
|
|
30
24
|
if (match === null) {
|
|
@@ -32,7 +26,11 @@ export function parseAgentRole(text) {
|
|
|
32
26
|
throw new Error('agent frontmatter is not closed');
|
|
33
27
|
return { content: text, disabled: false };
|
|
34
28
|
}
|
|
35
|
-
|
|
29
|
+
// A matched block always carries its body; the check keeps the type honest.
|
|
30
|
+
const frontmatter = match[1];
|
|
31
|
+
if (frontmatter === undefined)
|
|
32
|
+
throw new Error('agent frontmatter is not closed');
|
|
33
|
+
const parsed = parseYaml(frontmatter);
|
|
36
34
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
37
35
|
throw new Error('agent frontmatter must be a YAML object');
|
|
38
36
|
const metadata = parsed;
|
|
@@ -56,43 +54,63 @@ export function parseAgentRole(text) {
|
|
|
56
54
|
...(provider === undefined ? {} : { provider }),
|
|
57
55
|
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
58
56
|
...(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') })
|
|
57
|
+
...(description === undefined ? {} : { description })
|
|
62
58
|
};
|
|
63
59
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
function parentRouteOf(parent) {
|
|
61
|
+
const agent = parent;
|
|
62
|
+
return agent?.session?.requestHeader?.()?.config ?? agent?.options ?? {};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the child LLM options one card may contribute.
|
|
66
|
+
*
|
|
67
|
+
* A card route is advisory. Only an exact `provider` plus `model` pair is
|
|
68
|
+
* applied, and only when the live LLM runtime accepts it; every other
|
|
69
|
+
* declaration — a bare id, a Claude alias, a lone `provider`, an unsupported
|
|
70
|
+
* effort — degrades to inheriting the parent route and reports a diagnostic.
|
|
71
|
+
* @param policy - the card's parsed routing metadata.
|
|
72
|
+
* @param parent - the calling Agent whose route an unusable declaration falls back to.
|
|
73
|
+
* @param llm - live LLM runtime owning provider, model and effort validation.
|
|
74
|
+
* @param signal - tool-call cancellation signal.
|
|
75
|
+
* @param subject - role name used in diagnostics.
|
|
76
|
+
* @param diagnose - diagnostic sink; degradation is reported, never silent.
|
|
77
|
+
* @returns the options to send, or undefined for pure inheritance.
|
|
78
|
+
*/
|
|
79
|
+
export async function resolveAgentOptions(policy, parent, llm, signal, subject, diagnose) {
|
|
80
|
+
// One value, so a half-specified route can never be forwarded downstream.
|
|
81
|
+
const requested = policy.provider === undefined || policy.model === undefined ? undefined : { provider: policy.provider, model: policy.model };
|
|
82
|
+
if (requested === undefined && (policy.provider !== undefined || policy.model !== undefined)) {
|
|
83
|
+
const declared = [policy.provider, policy.model].filter(value => value !== undefined).join('/');
|
|
84
|
+
diagnose(`agent "${subject}": route "${declared}" needs an exact provider and model pair; it is ignored and the child inherits the parent route`);
|
|
73
85
|
}
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
const effort = policy.reasoningEffort;
|
|
87
|
+
if (effort === undefined && requested === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
const inherited = parentRouteOf(parent);
|
|
90
|
+
const provider = requested?.provider ?? inherited.provider;
|
|
91
|
+
const model = requested?.model ?? inherited.model;
|
|
92
|
+
if (provider === undefined || model === undefined) {
|
|
93
|
+
/* v8 ignore next 3 -- an effective route is always known once a parent Agent ran a request. */
|
|
94
|
+
if (effort !== undefined)
|
|
95
|
+
diagnose(`agent "${subject}": reasoning effort "${effort}" has no effective route to validate against; it is ignored`);
|
|
96
|
+
return undefined;
|
|
80
97
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
const routeChanged = provider !== inherited.provider || model !== inherited.model;
|
|
99
|
+
const effectiveEffort = effort ?? (routeChanged ? undefined : inherited.reasoningEffort);
|
|
100
|
+
try {
|
|
101
|
+
await llm.resolveCallConfig({ provider, model, ...(effectiveEffort === undefined ? {} : { reasoningEffort: effectiveEffort }) }, signal);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
signal.throwIfAborted();
|
|
105
|
+
diagnose(`agent "${subject}": route ${provider}/${model} is unusable (${String(error)}); it is ignored and the child inherits the parent route`);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
// Cancellation raised during the adapter lookup must not create a child.
|
|
109
|
+
signal.throwIfAborted();
|
|
110
|
+
return {
|
|
111
|
+
...(requested ?? {}),
|
|
112
|
+
...(effort === undefined ? {} : { reasoningEffort: effort })
|
|
113
|
+
};
|
|
96
114
|
}
|
|
97
115
|
/** Stable summaries; invalid declarations are excluded and transient read failures abort publication. */
|
|
98
116
|
export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
@@ -100,7 +118,7 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
|
100
118
|
const counts = new Map();
|
|
101
119
|
for (const entry of entries)
|
|
102
120
|
counts.set(entry.name, (counts.get(entry.name) ?? 0) + 1);
|
|
103
|
-
for (const entry of entries) {
|
|
121
|
+
for (const entry of namedAgentRoles(entries)) {
|
|
104
122
|
signal.throwIfAborted();
|
|
105
123
|
if (entry.disabled)
|
|
106
124
|
continue;
|
|
@@ -128,86 +146,82 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
|
|
|
128
146
|
if (policy.disabled)
|
|
129
147
|
continue;
|
|
130
148
|
const description = (policy.description ?? entry.description).replaceAll(/\s+/g, ' ').trim();
|
|
149
|
+
// Advertise a route only when the executor would apply it.
|
|
150
|
+
const exact = policy.provider !== undefined && policy.model !== undefined;
|
|
131
151
|
summaries.push({
|
|
132
|
-
name: entry.
|
|
133
|
-
|
|
152
|
+
name: entry.callName,
|
|
153
|
+
roleId: entry.name,
|
|
154
|
+
title: policy.title ?? entry.title ?? entry.callName,
|
|
134
155
|
description: description.length <= 500 ? description : `${description.slice(0, 497)}...`,
|
|
135
|
-
...(
|
|
136
|
-
...(policy.model === undefined ? {} : { model: policy.model }),
|
|
156
|
+
...(exact ? { provider: policy.provider, model: policy.model } : {}),
|
|
137
157
|
...(policy.reasoningEffort === undefined ? {} : { reasoningEffort: policy.reasoningEffort })
|
|
138
158
|
});
|
|
139
159
|
}
|
|
140
160
|
signal.throwIfAborted();
|
|
141
161
|
return summaries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
142
162
|
}
|
|
143
|
-
/**
|
|
144
|
-
export async function
|
|
163
|
+
/** Read the same declaration for catalog summaries and execution, including inline resources. */
|
|
164
|
+
export async function readAgentRole(entry) {
|
|
165
|
+
return parseAgentRole(entry.rawText ?? (await readFile(entry.path, 'utf8')));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Start one role child and return its durable id without waiting for the result.
|
|
169
|
+
* Resume state, steering, and the settlement notice belong to the host's
|
|
170
|
+
* continuation manager; this executor owns only role resolution.
|
|
171
|
+
*/
|
|
172
|
+
export async function executeAgentRole(host, listRoles, agentName, prompt, parent, signal, diagnose = () => { }) {
|
|
145
173
|
if (parent === undefined)
|
|
146
|
-
throw new Error('
|
|
174
|
+
throw new Error('subagent_run requires a calling agent');
|
|
147
175
|
if (prompt.trim() === '')
|
|
148
|
-
throw new Error('
|
|
176
|
+
throw new Error('subagent_run requires a non-empty prompt');
|
|
149
177
|
signal.throwIfAborted();
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
throw new Error(`agent
|
|
153
|
-
const entry = entries[0];
|
|
178
|
+
const [entry, ...duplicates] = namedAgentRoles(await listRoles(parent)).filter(candidate => candidate.callName === agentName);
|
|
179
|
+
if (entry === undefined || duplicates.length > 0)
|
|
180
|
+
throw new Error(`agent "${agentName}" is unavailable or ambiguous`);
|
|
154
181
|
if (entry.disabled)
|
|
155
|
-
throw new Error(`agent
|
|
182
|
+
throw new Error(`agent "${agentName}" is disabled`);
|
|
156
183
|
const policy = await readAgentRole(entry);
|
|
157
184
|
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);
|
|
185
|
+
throw new Error(`agent "${agentName}" is disabled`);
|
|
186
|
+
const agentOptions = await resolveAgentOptions(policy, parent, host.llm, signal, agentName, diagnose);
|
|
169
187
|
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
|
-
})
|
|
188
|
+
const started = await host.subagents.startContinuable({
|
|
189
|
+
provider: 'spawn',
|
|
190
|
+
label: agentName,
|
|
191
|
+
request: {
|
|
192
|
+
prompt: [{ type: 'text', text: prompt }],
|
|
193
|
+
parent,
|
|
194
|
+
maxDepth: MAX_AGENT_ROLE_DEPTH,
|
|
195
|
+
persona: policy.content,
|
|
196
|
+
...(agentOptions === undefined ? {} : { agentOptions })
|
|
197
|
+
},
|
|
198
|
+
signal
|
|
186
199
|
});
|
|
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
|
-
}
|
|
200
|
+
return { subagentId: started.childId };
|
|
196
201
|
}
|
|
197
202
|
/** Mount once after tools, llm and subagents are available; the returned disposer belongs to the plugin lifecycle. */
|
|
198
203
|
export function mountAgentRoleTool(ctx, listRoles, t = bindHostLocale(undefined)) {
|
|
199
204
|
const host = ctx;
|
|
200
205
|
const tool = defineTool({
|
|
201
206
|
name: AGENT_ROLE_TOOL_NAME,
|
|
202
|
-
description: 'Delegate a self-contained task to a role from the current subagent catalog
|
|
207
|
+
description: 'Delegate a self-contained task to a role from the current subagent catalog. The child starts without the parent conversation and runs ' +
|
|
208
|
+
'in the background: this returns a durable subagent id immediately. When the run settles the runtime sends you a notice carrying its ' +
|
|
209
|
+
"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
210
|
parameters: {
|
|
204
|
-
|
|
211
|
+
agent: { type: 'string', required: true, description: 'Exact callable role name from the current subagent catalog.' },
|
|
205
212
|
prompt: { type: 'string', required: true, description: 'Complete task and context for the isolated child.' }
|
|
206
213
|
},
|
|
207
|
-
output: {
|
|
214
|
+
output: {
|
|
215
|
+
schema: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
additionalProperties: false,
|
|
218
|
+
properties: { subagentId: { type: 'string', required: true } }
|
|
219
|
+
},
|
|
220
|
+
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }]
|
|
221
|
+
},
|
|
208
222
|
isConcurrencySafe: () => true,
|
|
209
223
|
async execute(args, exec) {
|
|
210
|
-
return executeAgentRole(host, listRoles, args.
|
|
224
|
+
return executeAgentRole(host, listRoles, args.agent, args.prompt, exec.agent, exec.signal, message => ctx.logger?.warn(message));
|
|
211
225
|
}
|
|
212
226
|
});
|
|
213
227
|
const disposeTool = host.tools.register(tool);
|
|
@@ -19,7 +19,7 @@ export class CommandMountRegistry {
|
|
|
19
19
|
const diagnostics = [];
|
|
20
20
|
const wanted = new Map();
|
|
21
21
|
for (const suite of enabledSuites) {
|
|
22
|
-
const specs = suite.activeSurfaces
|
|
22
|
+
const specs = suite.activeSurfaces.commands === false ? [] : await readCommands(suite.root, suite.resources?.commands);
|
|
23
23
|
for (const spec of specs) {
|
|
24
24
|
// The registry key is source-qualified: bare suite ids are unique per
|
|
25
25
|
// source only, so two sources' same-named suites would collide.
|
|
@@ -58,9 +58,12 @@ export class CommandMountRegistry {
|
|
|
58
58
|
...(spec.hint === undefined ? {} : { input: { hint: spec.hint } }),
|
|
59
59
|
handler: invocation => {
|
|
60
60
|
const agent = invocation.agent;
|
|
61
|
-
|
|
61
|
+
// The template rides the agent verbatim: a decorator line naming
|
|
62
|
+
// this plugin or the suite would be text the command author never
|
|
63
|
+
// wrote, and the follow-up's `source` already records provenance.
|
|
64
|
+
const text = spec.body.replaceAll('$ARGUMENTS', invocation.rawInput.trim());
|
|
62
65
|
agent.followup({ content: [{ type: 'text', text }], source: { kind: 'plugin', plugin: 'dsh-agent-plugins-market' } });
|
|
63
|
-
return { kind: 'success', text: this.t('commandAcknowledged', { command: spec.name
|
|
66
|
+
return { kind: 'success', text: this.t('commandAcknowledged', { command: spec.name }) };
|
|
64
67
|
}
|
|
65
68
|
});
|
|
66
69
|
this.live.set(key, disposer);
|
|
@@ -111,11 +114,11 @@ export async function readCommands(root, resources) {
|
|
|
111
114
|
return specs;
|
|
112
115
|
}
|
|
113
116
|
function commandMeta(text) {
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
117
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1];
|
|
118
|
+
if (frontmatter === undefined)
|
|
116
119
|
return undefined;
|
|
117
120
|
try {
|
|
118
|
-
const raw = parseYaml(
|
|
121
|
+
const raw = parseYaml(frontmatter);
|
|
119
122
|
if (typeof raw !== 'object' || raw === null)
|
|
120
123
|
return undefined;
|
|
121
124
|
const record = raw;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded waits for work this plugin cannot cancel.
|
|
3
|
+
*
|
|
4
|
+
* Every wait here is a race against a timer: the caller stops waiting and the
|
|
5
|
+
* underlying work keeps running. That is the only safe shape for a mount and
|
|
6
|
+
* for the change pipeline that drives it — a spawned server process or a
|
|
7
|
+
* browser authorization the user is completing must not be abandoned just
|
|
8
|
+
* because nobody is waiting for it any more.
|
|
9
|
+
*
|
|
10
|
+
* @module runtime/deadline
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Resolve `true` once `work` settles, or `false` when `deadlineMs` elapses.
|
|
14
|
+
*
|
|
15
|
+
* A rejection counts as settled: this helper only reports timing, so callers
|
|
16
|
+
* that need the failure attach their own handler to `work`.
|
|
17
|
+
*/
|
|
18
|
+
export function settlesWithin(work, deadlineMs) {
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
const timer = setTimeout(() => resolve(false), deadlineMs);
|
|
21
|
+
timer.unref?.();
|
|
22
|
+
const settle = () => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
resolve(true);
|
|
25
|
+
};
|
|
26
|
+
void work.then(settle, settle);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -5,18 +5,26 @@
|
|
|
5
5
|
* default true), the model can call `report_market_issue` when it concludes a
|
|
6
6
|
* problem it is looking at is caused by dsh-agent-plugins-market — a failed
|
|
7
7
|
* install, a misdetected source, a market UI defect the user just described.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* Filing happens in three steps, most preferred first: the `gh` CLI when it is
|
|
10
|
+
* installed and authenticated, the GitHub REST API when the process carries a
|
|
11
|
+
* `GITHUB_TOKEN` / `GH_TOKEN`, and otherwise nothing is filed — the caller
|
|
12
|
+
* gets the complete issue text plus a prefilled "new issue" URL that is opened
|
|
13
|
+
* in the browser and handed back to the user to submit.
|
|
11
14
|
*
|
|
12
15
|
* The tool is deliberately narrow: the title/body must describe the problem,
|
|
13
|
-
* submissions are rate-limited, and the setting switch unregisters the
|
|
14
|
-
* on the next reconcile pass.
|
|
16
|
+
* real submissions are rate-limited, and the setting switch unregisters the
|
|
17
|
+
* tool on the next reconcile pass.
|
|
15
18
|
* @module runtime/feedback-tool
|
|
16
19
|
*/
|
|
17
|
-
import {
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { readFile } from 'node:fs/promises';
|
|
18
22
|
import { join } from 'node:path';
|
|
23
|
+
import { promisify } from 'node:util';
|
|
24
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
19
25
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
26
|
+
import { platformBrowserOpener } from './mcp-client/oauth.js';
|
|
27
|
+
const execFileAsync = promisify(execFile);
|
|
20
28
|
/** The GitHub repository experience feedback files issues against. */
|
|
21
29
|
export const FEEDBACK_REPO_OWNER = 'Sivan757';
|
|
22
30
|
export const FEEDBACK_REPO_NAME = 'dsh-agent-plugins-market';
|
|
@@ -24,12 +32,19 @@ export const FEEDBACK_REPO_NAME = 'dsh-agent-plugins-market';
|
|
|
24
32
|
export const FEEDBACK_LABEL = 'agent-feedback';
|
|
25
33
|
/** Minimum milliseconds between two accepted submissions (burst guard). */
|
|
26
34
|
const SUBMIT_COOLDOWN_MS = 60_000;
|
|
35
|
+
/** Timeout for one `gh issue create` invocation. */
|
|
36
|
+
const GH_TIMEOUT_MS = 30_000;
|
|
37
|
+
/** The public "new issue" page for one report, prefilled with title and body. */
|
|
38
|
+
export function newIssueUrl(title, body) {
|
|
39
|
+
const query = new URLSearchParams({ title, body, labels: FEEDBACK_LABEL });
|
|
40
|
+
return `https://github.com/${FEEDBACK_REPO_OWNER}/${FEEDBACK_REPO_NAME}/issues/new?${query.toString()}`;
|
|
41
|
+
}
|
|
27
42
|
/**
|
|
28
|
-
* Submit one experience report
|
|
29
|
-
*
|
|
30
|
-
*
|
|
43
|
+
* Submit one experience report through `gh`, then the REST API, then a
|
|
44
|
+
* prefilled issue page the user submits themselves. Only a real submission
|
|
45
|
+
* advances the burst cooldown.
|
|
31
46
|
*/
|
|
32
|
-
export async function submitFeedback(dataRoot, report, now = Date.now()) {
|
|
47
|
+
export async function submitFeedback(dataRoot, report, now = Date.now(), ports = {}) {
|
|
33
48
|
const title = report.title.trim();
|
|
34
49
|
if (title === '')
|
|
35
50
|
return { ok: false, reason: 'title is required' };
|
|
@@ -38,6 +53,18 @@ export async function submitFeedback(dataRoot, report, now = Date.now()) {
|
|
|
38
53
|
if (now - lastAt < SUBMIT_COOLDOWN_MS) {
|
|
39
54
|
return { ok: false, reason: 'a feedback report was submitted less than a minute ago; wait and try again' };
|
|
40
55
|
}
|
|
56
|
+
const ghIssue = ports.ghIssue ?? createIssueWithGh;
|
|
57
|
+
let ghFailure;
|
|
58
|
+
try {
|
|
59
|
+
const url = await ghIssue(title, body);
|
|
60
|
+
if (url !== undefined) {
|
|
61
|
+
await stampSubmitAt(dataRoot, now);
|
|
62
|
+
return { ok: true, location: url };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
ghFailure = error instanceof Error ? error.message : String(error);
|
|
67
|
+
}
|
|
41
68
|
const token = process.env['GITHUB_TOKEN'] ?? process.env['GH_TOKEN'];
|
|
42
69
|
if (token !== undefined && token !== '') {
|
|
43
70
|
try {
|
|
@@ -46,22 +73,40 @@ export async function submitFeedback(dataRoot, report, now = Date.now()) {
|
|
|
46
73
|
return { ok: true, location: url };
|
|
47
74
|
}
|
|
48
75
|
catch (error) {
|
|
49
|
-
|
|
50
|
-
// report is never lost, and say so in the outcome.
|
|
51
|
-
const local = await spoolLocally(dataRoot, title, body);
|
|
52
|
-
await stampSubmitAt(dataRoot, now);
|
|
53
|
-
return {
|
|
54
|
-
ok: true,
|
|
55
|
-
location: local,
|
|
56
|
-
reason: `GitHub issue failed (${error instanceof Error ? error.message : String(error)}); saved locally instead`
|
|
57
|
-
};
|
|
76
|
+
ghFailure ??= error instanceof Error ? error.message : String(error);
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
const url = newIssueUrl(title, body);
|
|
80
|
+
let opened = true;
|
|
81
|
+
try {
|
|
82
|
+
await (ports.openPage ?? openInBrowser)(url);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// A headless host has no browser to open; the link in `location` still works.
|
|
86
|
+
opened = false;
|
|
87
|
+
}
|
|
88
|
+
const detail = ghFailure === undefined ? 'nothing could file the issue automatically' : `filing failed (${ghFailure})`;
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
location: url,
|
|
92
|
+
issueText: `${title}\n\n${body}`,
|
|
93
|
+
reason: opened ? `${detail}; the prefilled issue page was opened — submit it in the browser` : `${detail}; open the issue page at the returned location to submit it`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Open one URL in the platform browser; a failure is the caller's to report. */
|
|
97
|
+
async function openInBrowser(url) {
|
|
98
|
+
await platformBrowserOpener(new URL(url), 'feedback', () => { });
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* File the issue with the `gh` CLI. Undefined means gh is absent, not
|
|
102
|
+
* authenticated, or otherwise unable to create the issue — never a thrown
|
|
103
|
+
* failure, so the caller can fall through to the REST API.
|
|
104
|
+
*/
|
|
105
|
+
async function createIssueWithGh(title, body) {
|
|
106
|
+
const { stdout } = await execFileAsync('gh', ['issue', 'create', '--repo', `${FEEDBACK_REPO_OWNER}/${FEEDBACK_REPO_NAME}`, '--title', title, '--body', body, '--label', FEEDBACK_LABEL], { timeout: GH_TIMEOUT_MS });
|
|
107
|
+
return /https:\/\/\S+/.exec(stdout)?.[0];
|
|
63
108
|
}
|
|
64
|
-
/** POST one issue; returns its html_url. */
|
|
109
|
+
/** POST one issue through the REST API; returns its html_url. */
|
|
65
110
|
async function createIssue(token, title, body) {
|
|
66
111
|
const response = await fetch(`https://api.github.com/repos/${FEEDBACK_REPO_OWNER}/${FEEDBACK_REPO_NAME}/issues`, {
|
|
67
112
|
method: 'POST',
|
|
@@ -84,25 +129,26 @@ async function createIssue(token, title, body) {
|
|
|
84
129
|
throw new Error('GitHub API returned no issue URL');
|
|
85
130
|
return payload.html_url;
|
|
86
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Render one tool argument as text. Arguments arrive as untrusted values, so
|
|
134
|
+
* `String` keeps the coercion the report has always applied for anything the
|
|
135
|
+
* declared `type: 'string'` schema does not already cover.
|
|
136
|
+
*/
|
|
137
|
+
function textOf(value) {
|
|
138
|
+
return String(value);
|
|
139
|
+
}
|
|
87
140
|
/** Render the model's structured report into a deterministic issue body. */
|
|
88
141
|
export function renderFeedbackBody(report) {
|
|
89
142
|
const lines = ['## Experience feedback', '', `**Problem:** ${report.description.trim()}`, ''];
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
lines.push(`**
|
|
143
|
+
const expected = report.expected?.trim();
|
|
144
|
+
const actual = report.actual?.trim();
|
|
145
|
+
if (expected !== undefined && expected !== '')
|
|
146
|
+
lines.push(`**Expected:** ${expected}`, '');
|
|
147
|
+
if (actual !== undefined && actual !== '')
|
|
148
|
+
lines.push(`**Actual:** ${actual}`, '');
|
|
94
149
|
lines.push('---', `Filed automatically by the dsh-agent-plugins-market \`${FEEDBACK_TOOL_NAME}\` tool.`);
|
|
95
150
|
return lines.join('\n');
|
|
96
151
|
}
|
|
97
|
-
/** Append one report to the local spool; returns the spool file path. */
|
|
98
|
-
async function spoolLocally(dataRoot, title, body) {
|
|
99
|
-
const dir = join(dataRoot, 'feedback');
|
|
100
|
-
await mkdir(dir, { recursive: true });
|
|
101
|
-
const file = join(dir, 'reports.jsonl');
|
|
102
|
-
const record = JSON.stringify({ at: new Date().toISOString(), title, body });
|
|
103
|
-
await appendFile(file, `${record}\n`, 'utf8');
|
|
104
|
-
return file;
|
|
105
|
-
}
|
|
106
152
|
/** The last accepted submission's timestamp; 0 when the stamp file is absent. */
|
|
107
153
|
async function readLastSubmitAt(dataRoot) {
|
|
108
154
|
try {
|
|
@@ -115,11 +161,9 @@ async function readLastSubmitAt(dataRoot) {
|
|
|
115
161
|
}
|
|
116
162
|
}
|
|
117
163
|
async function stampSubmitAt(dataRoot, now) {
|
|
118
|
-
const dir = join(dataRoot, 'feedback');
|
|
119
|
-
await mkdir(dir, { recursive: true });
|
|
120
164
|
// Overwrite, not append: an appending stamp concatenates timestamps and
|
|
121
165
|
// parseInt on the merged digits disables the cooldown permanently.
|
|
122
|
-
await
|
|
166
|
+
await writeFileAtomic(join(dataRoot, 'feedback', 'last-submit-at'), String(now), { mode: 0o600, dirMode: 0o700 });
|
|
123
167
|
}
|
|
124
168
|
export const FEEDBACK_TOOL_NAME = 'report_market_issue';
|
|
125
169
|
const FEEDBACK_DESCRIPTION = 'File an experience-feedback issue for dsh-agent-plugins-market (the DeepSeek Harness agent plugin market). ' +
|
|
@@ -127,8 +171,14 @@ const FEEDBACK_DESCRIPTION = 'File an experience-feedback issue for dsh-agent-pl
|
|
|
127
171
|
'stuck install/uninstall, a market source that will not scan, wrong market UI behavior, or a runtime injection ' +
|
|
128
172
|
'defect the user attributes to a market suite. Do not call it for problems in other plugins, in the harness, ' +
|
|
129
173
|
"or in the user's own repositories. Compose a specific title; describe what happened, what was expected, and " +
|
|
130
|
-
'the exact error text when one appeared.'
|
|
131
|
-
|
|
174
|
+
'the exact error text when one appeared. When the result carries `issueText`, the issue was not filed for you: ' +
|
|
175
|
+
'show the human that text and the `location` link so they can submit it on GitHub.';
|
|
176
|
+
/**
|
|
177
|
+
* Register the feedback tool; returns the disposer. Undefined when the host has
|
|
178
|
+
* no tools service to register on — the caller turns that into a mount-path
|
|
179
|
+
* diagnostic instead of an error, since the setting can simply have no effect
|
|
180
|
+
* on this host.
|
|
181
|
+
*/
|
|
132
182
|
export function mountFeedbackTool(hostCtx, dataRoot, t) {
|
|
133
183
|
const host = hostCtx;
|
|
134
184
|
if (typeof host.tools?.register !== 'function')
|
|
@@ -152,6 +202,7 @@ export function mountFeedbackTool(hostCtx, dataRoot, t) {
|
|
|
152
202
|
properties: {
|
|
153
203
|
ok: { type: 'boolean', required: true },
|
|
154
204
|
location: { type: 'string' },
|
|
205
|
+
issueText: { type: 'string' },
|
|
155
206
|
reason: { type: 'string' }
|
|
156
207
|
}
|
|
157
208
|
},
|
|
@@ -160,8 +211,8 @@ export function mountFeedbackTool(hostCtx, dataRoot, t) {
|
|
|
160
211
|
async execute(args) {
|
|
161
212
|
const input = args;
|
|
162
213
|
const report = {
|
|
163
|
-
title:
|
|
164
|
-
description:
|
|
214
|
+
title: textOf(input['title'] ?? ''),
|
|
215
|
+
description: textOf(input['description'] ?? ''),
|
|
165
216
|
...(typeof input['expected'] === 'string' ? { expected: input['expected'] } : {}),
|
|
166
217
|
...(typeof input['actual'] === 'string' ? { actual: input['actual'] } : {})
|
|
167
218
|
};
|
|
@@ -10,29 +10,29 @@
|
|
|
10
10
|
* on every enable/disable/install/uninstall; a missing bridge package, a
|
|
11
11
|
* broken hook file, or a mount failure is contained per suite.
|
|
12
12
|
*/
|
|
13
|
-
import { mkdtemp, readFile, rm, stat
|
|
13
|
+
import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
|
14
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
14
15
|
import { tmpdir } from 'node:os';
|
|
15
16
|
import { createHash } from 'node:crypto';
|
|
16
17
|
import { join } from 'node:path';
|
|
18
|
+
import { SerialPassQueue } from './mount-lifecycle.js';
|
|
17
19
|
import { qualifiedSuiteId } from '../catalog/paths.js';
|
|
18
20
|
export class HooksMountRegistry {
|
|
19
21
|
ctx;
|
|
20
22
|
live = new Map();
|
|
21
23
|
fingerprints = new Map();
|
|
22
24
|
temporary = new Map();
|
|
23
|
-
|
|
25
|
+
passes = new SerialPassQueue();
|
|
24
26
|
constructor(ctx) {
|
|
25
27
|
this.ctx = ctx;
|
|
26
28
|
}
|
|
27
29
|
/** Mount/unmount one bridge per suite to match the enabled suites exactly. */
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.queue = run.then(() => { }, () => { });
|
|
31
|
-
return run;
|
|
30
|
+
reconcile(enabledSuites) {
|
|
31
|
+
return this.passes.run(() => this.reconcileNow(enabledSuites));
|
|
32
32
|
}
|
|
33
33
|
async reconcileNow(enabledSuites) {
|
|
34
34
|
const diagnostics = [];
|
|
35
|
-
const active = enabledSuites.filter(suite => suite.activeSurfaces
|
|
35
|
+
const active = enabledSuites.filter(suite => suite.activeSurfaces.hooks !== false && (suite.resources === undefined || suite.hooks !== undefined));
|
|
36
36
|
// Keys are the qualified suite id: bare ids are unique per source only.
|
|
37
37
|
const wanted = new Set(active.map(suite => qualifiedSuiteId(suite.sourceId, suite.id)));
|
|
38
38
|
for (const [suiteId, handle] of [...this.live]) {
|
|
@@ -68,12 +68,10 @@ export class HooksMountRegistry {
|
|
|
68
68
|
}
|
|
69
69
|
/** Dispose every live bridge; used at plugin teardown. */
|
|
70
70
|
async disposeAll() {
|
|
71
|
-
|
|
71
|
+
await this.passes.run(async () => {
|
|
72
72
|
for (const [suiteId, handle] of [...this.live])
|
|
73
73
|
await this.unmount(suiteId, handle);
|
|
74
74
|
});
|
|
75
|
-
this.queue = run.catch(() => { });
|
|
76
|
-
await run;
|
|
77
75
|
}
|
|
78
76
|
async mount(key, suite, content, fingerprint, originalPath) {
|
|
79
77
|
let bridge;
|
|
@@ -93,7 +91,7 @@ export class HooksMountRegistry {
|
|
|
93
91
|
if (configPath === undefined) {
|
|
94
92
|
temporary = await mkdtemp(join(tmpdir(), 'dsh-project-hooks-'));
|
|
95
93
|
configPath = join(temporary, 'hooks.json');
|
|
96
|
-
await
|
|
94
|
+
await writeFileAtomic(configPath, content, { mode: 0o600 });
|
|
97
95
|
}
|
|
98
96
|
handle = mountCtx.plugin(bridge, {
|
|
99
97
|
configPath,
|