dsh-agent-toolkit 0.1.0 → 0.2.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/lib/index.js CHANGED
@@ -60,7 +60,42 @@ function migrateAgentRecord(record) {
60
60
  } : rest;
61
61
  }
62
62
  //#endregion
63
+ //#region src/channels/basic-tools.ts
64
+ const BASIC_TOOLS = [
65
+ {
66
+ id: "@deepseek-ai/dsh-persona",
67
+ config: { text: "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}." }
68
+ },
69
+ {
70
+ id: "@deepseek-ai/dsh-agent-instructions",
71
+ config: { maxBytes: 65536 }
72
+ },
73
+ ...process.platform === "win32" ? [{ id: "@deepseek-ai/dsh-tool-pwsh" }] : [{ id: "@deepseek-ai/dsh-tool-bash" }],
74
+ { id: "@deepseek-ai/dsh-tool-fs" },
75
+ {
76
+ id: "@deepseek-ai/dsh-tool-fs-search",
77
+ config: { sampleOverCapGlobResults: false }
78
+ }
79
+ ];
80
+ /** 原生工具名(白名单 UI 与存量迁移用):与 BASIC_TOOLS 挂载插件注册的工具名一一对应。
81
+ * 名字来源(摘自 deepseek-harness 源码):dsh-tool-pwsh/dsh-tool-bash → 'pwsh'/'bash'(平台互斥);
82
+ * dsh-tool-fs → 'read'/'write'/'edit'/'read_image';dsh-tool-fs-search → 'glob'/'grep'。
83
+ * 这些工具 scoped 挂载在 agentCtx,不出现在顶层 ctx.tools.schemas(),故需显式常量。 */
84
+ const NATIVE_TOOL_NAMES = [
85
+ process.platform === "win32" ? "pwsh" : "bash",
86
+ "read",
87
+ "write",
88
+ "edit",
89
+ "read_image",
90
+ "glob",
91
+ "grep"
92
+ ];
93
+ //#endregion
63
94
  //#region src/agents/builtin.ts
95
+ /** 内置保底 Agent 记录:main + explorer(只读白名单)/ general(不限制)。 */
96
+ /** explorer 默认白名单:原生工具去掉写类(write/edit)。shell 名平台互斥(win32=pwsh、其余=bash),
97
+ * 必须从 NATIVE_TOOL_NAMES 派生不可写死——宿主 tools.restrict 对未知名响亮失败。 */
98
+ const EXPLORER_READONLY_ALLOW = NATIVE_TOOL_NAMES.filter((n) => n !== "write" && n !== "edit");
64
99
  const BUILTIN_AGENTS = [
65
100
  {
66
101
  id: "main",
@@ -74,7 +109,8 @@ const BUILTIN_AGENTS = [
74
109
  persona: `你是代码库探索员。快速定位与任务相关的文件与符号,回答关于代码结构、
75
110
  调用关系、实现位置的问题。你只读不写:不修改任何文件、不运行有副作用的命令。
76
111
  输出结论清单,每条附文件路径与行号;信息不足时说明缺口,不要猜测。`,
77
- builtin: true
112
+ builtin: true,
113
+ tools: { allow: [...EXPLORER_READONLY_ALLOW] }
78
114
  },
79
115
  {
80
116
  id: "general",
@@ -97,20 +133,16 @@ const RoleYamlSchema = z$1.object({
97
133
  persona: z$1.string().min(1),
98
134
  provider: z$1.string().optional(),
99
135
  model: z$1.string().optional(),
100
- tools: z$1.object({
101
- allow: z$1.array(z$1.string()).optional(),
102
- deny: z$1.array(z$1.string()).optional()
103
- }).optional()
136
+ tools: z$1.object({ allow: z$1.array(z$1.string()).optional() }).optional()
104
137
  });
105
138
  /**
106
139
  * 解析校验单个角色 YAML 文件并转成 AgentRecord。
107
140
  * @param text - 文件内容。
108
141
  * @param source - 用于错误信息的来源名(通常是文件路径)。
109
142
  * @param fileName - 文件名(去 .yml),name 省略时的取值;显式 name 须与它一致。
110
- * @param warn - 非致命丢弃(如 tools.deny)的通知通道。
111
143
  * @throws YAML 语法错误、结构非法、name 与文件名不一致、id 非法、tools 空。
112
144
  */
113
- function parseRoleYaml(text, source, fileName, warn) {
145
+ function parseRoleYaml(text, source, fileName) {
114
146
  let parsed;
115
147
  try {
116
148
  parsed = yaml.load(text);
@@ -127,8 +159,7 @@ function parseRoleYaml(text, source, fileName, warn) {
127
159
  const id = raw.name ?? fileName;
128
160
  if (raw.name !== void 0 && raw.name !== fileName) throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 的 name "${raw.name}" 与文件名 "${fileName}" 不一致(省略 name 即取文件名)`);
129
161
  if (!AGENT_ID_RE.test(id)) throw new Error(`dsh-agent-toolkit: 角色 id "${id}" 非法(${source}):只允许小写字母、数字、-,且以小写字母开头`);
130
- if (hasTools && raw.tools !== void 0 && (raw.tools.allow?.length ?? 0) === 0 && (raw.tools.deny?.length ?? 0) === 0) throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 的 tools 为空:allow/deny 至少配一个`);
131
- if (raw.tools?.deny !== void 0 && raw.tools.deny.length > 0) warn?.(`dsh-agent-toolkit: 角色文件 ${source} 的 tools.deny 已忽略(注册表仅支持 allow 白名单)`);
162
+ if (hasTools && raw.tools !== void 0 && (raw.tools.allow?.length ?? 0) === 0) throw new Error(`dsh-agent-toolkit: 角色文件 ${source} 的 tools 为空:allow 至少配一个(不需要限制请整段省略 tools)`);
132
163
  const model = raw.provider !== void 0 && raw.model !== void 0 ? {
133
164
  provider: raw.provider,
134
165
  model: raw.model
@@ -186,7 +217,7 @@ async function importRolesYaml(ctx, rolesDir) {
186
217
  return result;
187
218
  }
188
219
  for (const ref of refs) try {
189
- const record = parseRoleYaml(await readFile(ref.path, "utf8"), ref.path, ref.fileName, ctx.warn);
220
+ const record = parseRoleYaml(await readFile(ref.path, "utf8"), ref.path, ref.fileName);
190
221
  await ctx.agents.put(record.id, record);
191
222
  result.imported++;
192
223
  } catch (error) {
@@ -200,47 +231,17 @@ async function markImported(ctx) {
200
231
  await ctx.meta.put(ROLES_YAML_IMPORTED_KEY, { value: "1" });
201
232
  }
202
233
  //#endregion
203
- //#region src/channels/basic-tools.ts
204
- const BASIC_TOOLS = [
205
- {
206
- id: "@deepseek-ai/dsh-persona",
207
- config: { text: "You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}." }
208
- },
209
- {
210
- id: "@deepseek-ai/dsh-agent-instructions",
211
- config: { maxBytes: 65536 }
212
- },
213
- ...process.platform === "win32" ? [{ id: "@deepseek-ai/dsh-tool-pwsh" }] : [{ id: "@deepseek-ai/dsh-tool-bash" }],
214
- { id: "@deepseek-ai/dsh-tool-fs" },
215
- {
216
- id: "@deepseek-ai/dsh-tool-fs-search",
217
- config: { sampleOverCapGlobResults: false }
218
- }
219
- ];
220
- /** 原生工具名(白名单 UI 与存量迁移用):与 BASIC_TOOLS 挂载插件注册的工具名一一对应。
221
- * 名字来源(摘自 deepseek-harness 源码):dsh-tool-pwsh/dsh-tool-bash → 'pwsh'/'bash'(平台互斥);
222
- * dsh-tool-fs → 'read'/'write'/'edit'/'read_image';dsh-tool-fs-search → 'glob'/'grep'。
223
- * 这些工具 scoped 挂载在 agentCtx,不出现在顶层 ctx.tools.schemas(),故需显式常量。 */
224
- const NATIVE_TOOL_NAMES = [
225
- process.platform === "win32" ? "pwsh" : "bash",
226
- "read",
227
- "write",
228
- "edit",
229
- "read_image",
230
- "glob",
231
- "grep"
232
- ];
233
- //#endregion
234
234
  //#region src/agents/registry.ts
235
235
  /** tools.allow 一次性并入原生工具名的 meta 表标记键。 */
236
236
  const TOOLS_NATIVE_MIGRATED_KEY = "tools_native_migrated";
237
+ /** explorer 只读白名单一次性并入的 meta 表标记键。 */
238
+ const EXPLORER_READONLY_MIGRATED_KEY = "explorer_readonly_migrated";
237
239
  /**
238
- * 打开 dsh_agent_toolkit 域 → 缺 main/explorer/general 时种入内置 → 首启 YAML 导入 →
239
- * 构建内存缓存。域由 apply 统一 open(storage-domain 同名单开),此处只消费表句柄。
240
+ * 打开 dsh_agent_toolkit 域 → 首启 YAML 导入 → 旧记录迁移(promptLayers/原生并入/explorer 只读)→
241
+ * 缺 main/explorer/general 时种入内置 → 构建内存缓存。域由 apply 统一 open(storage-domain 同名单开),此处只消费表句柄。
240
242
  */
241
243
  async function createRegistry(warn, tables) {
242
244
  const { agents, meta } = tables;
243
- await seedBuiltins(agents);
244
245
  await importRolesYaml({
245
246
  agents,
246
247
  meta,
@@ -260,6 +261,15 @@ async function createRegistry(warn, tables) {
260
261
  if (next !== record) await agents.put(id, next);
261
262
  }
262
263
  if (!nativeMigrated) await meta.put(TOOLS_NATIVE_MIGRATED_KEY, { value: "1" });
264
+ if (!(meta.get("explorer_readonly_migrated") !== void 0)) {
265
+ const explorer = agents.get("explorer");
266
+ if (explorer !== void 0 && explorer.tools === void 0) await agents.put("explorer", {
267
+ ...explorer,
268
+ tools: { allow: [...EXPLORER_READONLY_ALLOW] }
269
+ });
270
+ await meta.put(EXPLORER_READONLY_MIGRATED_KEY, { value: "1" });
271
+ }
272
+ await seedBuiltins(agents);
263
273
  const cache = /* @__PURE__ */ new Map();
264
274
  for (const [id, record] of agents.entries()) cache.set(id, record);
265
275
  const listeners = /* @__PURE__ */ new Set();
@@ -1192,7 +1202,7 @@ function withPartialText(error, output) {
1192
1202
  return text.length === 0 ? error : `${error}\n成员中断前的部分产出:\n${text}`;
1193
1203
  }
1194
1204
  /** 收集并释放一次前台运行;dispose 失败不掩盖独立的结果失败。 */
1195
- async function settleForegroundRun(run, roleId) {
1205
+ async function settleForegroundRun(run, roleId, route) {
1196
1206
  const childSessionId = String(run.id);
1197
1207
  const [execution] = await Promise.allSettled([run.result.then((result) => {
1198
1208
  const error = stopReasonError(result);
@@ -1202,7 +1212,11 @@ async function settleForegroundRun(run, roleId) {
1202
1212
  role: roleId,
1203
1213
  runId: String(run.id),
1204
1214
  childSessionId,
1205
- output: result.output
1215
+ output: result.output,
1216
+ ...route !== void 0 ? {
1217
+ provider: route.provider,
1218
+ model: route.model
1219
+ } : {}
1206
1220
  };
1207
1221
  })]);
1208
1222
  const [disposal] = await Promise.allSettled([Promise.resolve().then(() => run.dispose())]);
@@ -1269,7 +1283,9 @@ function createDelegateTool(toolName, deps) {
1269
1283
  type: "array",
1270
1284
  required: true,
1271
1285
  items: { type: "json" }
1272
- }
1286
+ },
1287
+ provider: { type: "string" },
1288
+ model: { type: "string" }
1273
1289
  }
1274
1290
  },
1275
1291
  render: (_args, value) => [{
@@ -1279,7 +1295,11 @@ function createDelegateTool(toolName, deps) {
1279
1295
  presentationMeta: (_args, value) => ({
1280
1296
  role: value.role,
1281
1297
  runId: value.runId,
1282
- childSessionId: value.childSessionId
1298
+ childSessionId: value.childSessionId,
1299
+ ...typeof value.provider === "string" && typeof value.model === "string" ? {
1300
+ provider: value.provider,
1301
+ model: value.model
1302
+ } : {}
1283
1303
  })
1284
1304
  },
1285
1305
  isConcurrencySafe: () => true,
@@ -1312,7 +1332,19 @@ function createDelegateTool(toolName, deps) {
1312
1332
  } } : {},
1313
1333
  ...role.tools !== void 0 ? { toolFilter: { allow: [...role.tools.allow] } } : {}
1314
1334
  };
1315
- return settleForegroundRun(await deps.startRun(deps.provider, request), role.id);
1335
+ const route = (role.model !== void 0 && role.model.provider !== "" && role.model.model !== "" ? role.model : void 0) ?? (typeof parent.options.provider === "string" && parent.options.provider !== "" && typeof parent.options.model === "string" && parent.options.model !== "" ? {
1336
+ provider: parent.options.provider,
1337
+ model: parent.options.model
1338
+ } : void 0);
1339
+ const parentSessionId = String(parent.session.id);
1340
+ if (route !== void 0) deps.active.set(parentSessionId, role.id, route);
1341
+ try {
1342
+ const run = await deps.startRun(deps.provider, request);
1343
+ if (route !== void 0) await deps.recordRoute(String(run.id), route).catch(() => void 0);
1344
+ return await settleForegroundRun(run, role.id, route);
1345
+ } finally {
1346
+ if (route !== void 0) deps.active.delete(parentSessionId, role.id);
1347
+ }
1316
1348
  }
1317
1349
  });
1318
1350
  }
@@ -1326,7 +1358,7 @@ const TEAM_SECTION_ORDER = 116.6;
1326
1358
  * 工具随 provider 在场与否挂载/摘除。名册来自注册表(main 排除),工具与提示段都经
1327
1359
  * 闭包读取 registry,UI 改角色后新会话即生效。
1328
1360
  */
1329
- function setupDelegate(ctx, config, registry) {
1361
+ function setupDelegate(ctx, config, registry, channels) {
1330
1362
  const { provider, toolName } = config;
1331
1363
  let disposeTool;
1332
1364
  let providerFailed = false;
@@ -1339,7 +1371,9 @@ function setupDelegate(ctx, config, registry) {
1339
1371
  roster: () => registry.list(),
1340
1372
  provider,
1341
1373
  buildPersona: (role) => buildAgentPersona({ rules: config.rules }, role, role.model),
1342
- startRun: (pr, request) => ctx.subagents.start(pr, request)
1374
+ startRun: (pr, request) => ctx.subagents.start(pr, request),
1375
+ active: channels.active,
1376
+ recordRoute: channels.recordRoute
1343
1377
  }));
1344
1378
  };
1345
1379
  ctx.on("subagent/provider-added", (p) => {
@@ -1372,6 +1406,86 @@ function setupDelegate(ctx, config, registry) {
1372
1406
  });
1373
1407
  }
1374
1408
  //#endregion
1409
+ //#region src/delegate/active.ts
1410
+ function createActiveRoutes() {
1411
+ const map = /* @__PURE__ */ new Map();
1412
+ const key = (sessionId, roleId) => `${sessionId}:${roleId}`;
1413
+ return {
1414
+ set: (sessionId, roleId, route) => {
1415
+ map.set(key(sessionId, roleId), route);
1416
+ },
1417
+ get: (sessionId, roleId) => map.get(key(sessionId, roleId)),
1418
+ delete: (sessionId, roleId) => {
1419
+ map.delete(key(sessionId, roleId));
1420
+ }
1421
+ };
1422
+ }
1423
+ //#endregion
1424
+ //#region src/delegate/routes.ts
1425
+ /** 委派路由持久存储域:子会话头部 chip 的数据源(schema 与 domain 布局的单一来源在本文件)。 */
1426
+ const DelegationRouteRecordSchema = z$1.object({
1427
+ provider: z$1.string(),
1428
+ model: z$1.string(),
1429
+ at: z$1.number()
1430
+ });
1431
+ const delegationRoutesDomain = defineDomain({
1432
+ name: "dsh_agent_toolkit_routes",
1433
+ version: 1,
1434
+ tables: { routes: domainTable(DelegationRouteRecordSchema) }
1435
+ });
1436
+ //#endregion
1437
+ //#region src/delegate/api.ts
1438
+ function createDelegateApiHandler(deps) {
1439
+ return async (req, res) => {
1440
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
1441
+ const sub = url.pathname.replace(/^\/dsh-agent-toolkit\/api/, "") || "/";
1442
+ if (req.method !== "GET") {
1443
+ json$1(res, 405, { error: "method not allowed" });
1444
+ return;
1445
+ }
1446
+ if (sub === "/delegate/active") {
1447
+ const route = deps.active.get(url.searchParams.get("session") ?? "", url.searchParams.get("role") ?? "");
1448
+ if (route === void 0) {
1449
+ json$1(res, 404, { error: "not found" });
1450
+ return;
1451
+ }
1452
+ json$1(res, 200, {
1453
+ provider: route.provider,
1454
+ model: route.model
1455
+ });
1456
+ return;
1457
+ }
1458
+ if (sub === "/delegate/route") {
1459
+ const record = deps.routes.get(url.searchParams.get("session") ?? "");
1460
+ if (record === void 0) {
1461
+ json$1(res, 404, { error: "not found" });
1462
+ return;
1463
+ }
1464
+ json$1(res, 200, {
1465
+ provider: record.provider,
1466
+ model: record.model
1467
+ });
1468
+ return;
1469
+ }
1470
+ json$1(res, 404, { error: "not found" });
1471
+ };
1472
+ }
1473
+ /**
1474
+ * 注册 delegate 路由(恒启用,与 agents 同策略)。webServer 为可选服务:
1475
+ * 缺席时经 registerOptionalRoutes 惰性不注册。prefix 先于 /api 兜底前缀命中。
1476
+ */
1477
+ function setupDelegateApi(ctx, deps) {
1478
+ const handler = createDelegateApiHandler(deps);
1479
+ registerOptionalRoutes(ctx, (webCtx) => {
1480
+ const unregister = webCtx.webServer.register({
1481
+ kind: "prefix",
1482
+ path: "/dsh-agent-toolkit/api/delegate",
1483
+ handler
1484
+ });
1485
+ return () => unregister();
1486
+ });
1487
+ }
1488
+ //#endregion
1375
1489
  //#region src/agents/api.ts
1376
1490
  function createAgentsApiHandler(deps) {
1377
1491
  return async (req, res) => {
@@ -3107,11 +3221,26 @@ async function apply(ctx, config) {
3107
3221
  source: layerSource,
3108
3222
  rules: config.rules
3109
3223
  });
3224
+ const routesTable = (await openDomainSafely(ctx, delegationRoutesDomain, warn)).table("routes");
3225
+ const activeRoutes = createActiveRoutes();
3110
3226
  setupDelegate(ctx, {
3111
3227
  provider: config.provider,
3112
3228
  toolName: config.toolName,
3113
3229
  rules: config.rules
3114
- }, registry);
3230
+ }, registry, {
3231
+ active: activeRoutes,
3232
+ recordRoute: async (childSessionId, route) => {
3233
+ await routesTable.put(childSessionId, {
3234
+ provider: route.provider,
3235
+ model: route.model,
3236
+ at: Date.now()
3237
+ });
3238
+ }
3239
+ });
3240
+ setupDelegateApi(ctx, {
3241
+ active: activeRoutes,
3242
+ routes: routesTable
3243
+ });
3115
3244
  setupAgentsApi(ctx, {
3116
3245
  registry,
3117
3246
  listTools: () => ctx.tools.schemas().map((s) => s.name),