@ai_mind/dsh-a2a 1.2.0 → 1.3.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 CHANGED
@@ -115,7 +115,7 @@
115
115
  面板分三部分:
116
116
 
117
117
  1. **概览**:实时 Agent Card、JSON-RPC / HTTP+JSON 地址(一键复制)、流式/推送/扩展卡片能力、鉴权状态、运行状态与可复制的 `curl` / 官方 JS 客户端示例。
118
- 2. **配置表单**:直接编辑名称、描述、对外地址、挂载路径、流式/推送/扩展卡片、鉴权模式与令牌环境变量、访问令牌(写入后不再回显)、Agent 预设/模型/工作目录/沙箱/审批/并发/超时,以及请求体与任务保留限额。预设、模型提供方、模型和推理强度会从当前 Host 的 `agentPresets` / `llm` 服务动态列出选项;留空即使用部署默认,无法列出的自定义值仍可手动输入。
118
+ 2. **配置表单**:直接编辑名称、描述、对外地址、挂载路径、流式/推送/扩展卡片、鉴权模式与令牌环境变量、访问令牌(写入后不再回显)、Agent 预设/模型/工作目录/工作区名称/沙箱/审批/并发/超时,以及请求体与任务保留限额。预设、模型提供方、模型和推理强度会从当前 Host 的 `agentPresets` / `llm` 服务动态列出选项;留空即使用部署默认,无法列出的自定义值仍可手动输入。
119
119
  3. **保存**:写入 profile 的 `cordis.patch.yml`(`a2a-server` 行),校验通过后插件自动重载;修改挂载路径后刷新页面即可。
120
120
 
121
121
  配置接口 `GET/PUT <mountPath>/panel/config` 始终位于 DSH Web 的 origin(独立端口模式下也不在 A2A 端口上),使用 **Harness Web 连接凭据**(不是 A2A token)鉴权:只有能打开 Settings 的已登录浏览器可以读写,远程 A2A 客户端即使知道路径也会收到 401。令牌字段是只写字段,GET 只返回“是否已配置”。
@@ -266,6 +266,14 @@ server:
266
266
 
267
267
  ---
268
268
 
269
+ ### 工作区分组
270
+
271
+ A2A 创建的 Harness 会话使用 `agent.cwd` 作为工作目录,GUI 按会话目录把会话归入已注册的 Workspace。如果该目录还不是工作区,会话就会显示在「未分组」。
272
+
273
+ - 想让 A2A 会话归入**已有工作区**:把 `agent.cwd` 指向该工作区目录即可,插件会复用该工作区并附加新会话。
274
+ - 想为专用目录**新建分组**:设置 `agent.workspaceTitle`(面板里的「工作区名称(自动分组)」),插件会在该目录注册一个同名工作区,并把之后创建的 A2A 会话附加进去。
275
+ - 已有历史会话不会自动追溯重排;新创建的 A2A 任务会出现在分组下。
276
+
269
277
  ## Agent 运行时映射
270
278
 
271
279
  ### 会话与并发
@@ -400,6 +408,7 @@ agent:
400
408
  reasoningEffort: null
401
409
  maxTokens: null
402
410
  cwd: null # 绝对路径;留空用 workingDirectory.defaultDirectory
411
+ workspaceTitle: null # 可选;为 cwd 注册 GUI 工作区并让 A2A 会话归入该分组
403
412
  sandboxMode: workspace-write
404
413
  approvalPolicy: never
405
414
  maxConcurrentTasks: 4
package/client.js CHANGED
@@ -108,9 +108,11 @@ window.__ModuleLoader__.load({
108
108
  fieldAgentProvider: '模型提供方',
109
109
  fieldAgentModel: '模型',
110
110
  fieldAgentReasoningEffort: '推理强度',
111
+ fieldAgentWorkspaceTitle: '工作区名称(自动分组)',
111
112
  optionDefault: '(使用默认)',
112
113
  defaultModel: '默认模型(留空时)',
113
114
  modelPairHint: '模型提供方和模型必须同时选择,或都留空使用默认。',
115
+ workspaceHint: 'A2A 会话会归入 agent.cwd 对应的工作区;若该目录还不是工作区,填写“工作区名称”即可为该目录创建分组。',
114
116
  fieldAgentSandboxMode: '沙箱模式',
115
117
  fieldAgentApprovalPolicy: '审批策略',
116
118
  fieldAgentMaxConcurrentTasks: '最大并发任务',
@@ -223,9 +225,11 @@ window.__ModuleLoader__.load({
223
225
  fieldAgentProvider: 'Model provider',
224
226
  fieldAgentModel: 'Model',
225
227
  fieldAgentReasoningEffort: 'Reasoning effort',
228
+ fieldAgentWorkspaceTitle: 'Workspace title (auto-group)',
226
229
  optionDefault: '(use default)',
227
230
  defaultModel: 'Default model (when empty)',
228
231
  modelPairHint: 'Choose both a provider and a model, or leave both empty for the default.',
232
+ workspaceHint: 'A2A sessions join the workspace that owns agent.cwd. Set a workspace title to create that workspace when the directory is not registered yet.',
229
233
  fieldAgentSandboxMode: 'Sandbox mode',
230
234
  fieldAgentApprovalPolicy: 'Approval policy',
231
235
  fieldAgentMaxConcurrentTasks: 'Max concurrent tasks',
@@ -821,6 +825,7 @@ window.__ModuleLoader__.load({
821
825
  (optionSets.default.reasoningEffort ? ` · ${optionSets.default.reasoningEffort}` : ''),
822
826
  )
823
827
  : null,
828
+ group === 'agent' ? h('p', { className: 'a2aui-field-note' }, t('workspaceHint')) : null,
824
829
  )
825
830
  return group === 'auth' ? [node, secretBlock] : [node]
826
831
  }),
package/lib/bridge.js CHANGED
@@ -275,6 +275,7 @@ class ContextWorker {
275
275
  throw new Error(`A2A agent cwd must be an absolute path, got ${JSON.stringify(cwd)}`)
276
276
  }
277
277
  await mkdir(cwd, { recursive: true })
278
+ const workspace = await runtime.ensureWorkspace(cwd)
278
279
  const sessionId = `session-a2a-${newId()}`
279
280
  const agentOptions = runtime.agentOptions()
280
281
  const presets = runtime.ctx.get?.('agentPresets')
@@ -307,6 +308,13 @@ class ContextWorker {
307
308
  }
308
309
  this.#handle = handle
309
310
  this.#agent = handle.agent
311
+ if (workspace !== null) {
312
+ try {
313
+ await workspace.attachSession(handle.agent.id)
314
+ } catch (error) {
315
+ runtime.logger.warn(`[a2a] could not attach session ${handle.agent.id} to workspace ${workspace.path}: ${errorMessage(error)}`)
316
+ }
317
+ }
310
318
  try {
311
319
  runtime.applyAgentPolicy(handle.agent)
312
320
  } catch (error) {
@@ -361,6 +369,7 @@ export class AgentRuntime {
361
369
  #logger
362
370
  #workers = new Map()
363
371
  #sweepTimer = undefined
372
+ #workspacePromise = undefined
364
373
  #disposed = false
365
374
  #stats = { turns: 0, completed: 0, failed: 0, canceled: 0 }
366
375
 
@@ -422,6 +431,42 @@ export class AgentRuntime {
422
431
  return options
423
432
  }
424
433
 
434
+ /**
435
+ * Resolve the project (GUI workspace) that owns the agent working directory.
436
+ *
437
+ * An existing workspace is reused and its sessions are attached explicitly.
438
+ * When `agent.workspaceTitle` is set, a missing workspace is created for the
439
+ * directory so the GUI can group new A2A sessions under it.
440
+ *
441
+ * @param {string} cwd absolute agent working directory
442
+ * @returns {Promise<object|null>} the Workspace entity, or null when grouping is unavailable
443
+ */
444
+ async ensureWorkspace(cwd) {
445
+ if (this.#workspacePromise === undefined) {
446
+ this.#workspacePromise = (async () => {
447
+ const registry = this.#ctx.get?.('workspaceRegistry')
448
+ if (registry === undefined) {
449
+ if (this.#config.agent.workspaceTitle) {
450
+ this.#logger.warn('[a2a] agent.workspaceTitle is set but workspaceRegistry is unavailable')
451
+ }
452
+ return null
453
+ }
454
+ try {
455
+ const existing = await registry.resolveByPath(cwd)
456
+ if (existing !== undefined) return existing
457
+ if (!this.#config.agent.workspaceTitle) return null
458
+ const created = await registry.create(cwd, this.#config.agent.workspaceTitle)
459
+ this.#logger.info(`[a2a] workspace "${created.title}" registered for ${cwd}`)
460
+ return created
461
+ } catch (error) {
462
+ this.#logger.warn(`[a2a] cannot resolve or create a workspace for ${cwd}: ${errorMessage(error)}`)
463
+ return null
464
+ }
465
+ })()
466
+ }
467
+ return this.#workspacePromise
468
+ }
469
+
425
470
  /** @returns {Promise<string>} the absolute workspace directory for agents */
426
471
  async resolveCwd() {
427
472
  if (this.#config.agent.cwd) return this.#config.agent.cwd
package/lib/config.js CHANGED
@@ -96,6 +96,7 @@ export const DEFAULT_CONFIG = {
96
96
  reasoningEffort: null,
97
97
  maxTokens: null,
98
98
  cwd: null,
99
+ workspaceTitle: null,
99
100
  sandboxMode: 'workspace-write',
100
101
  approvalPolicy: 'never',
101
102
  maxConcurrentTasks: 4,
@@ -463,6 +464,9 @@ export function normalizeConfig(raw) {
463
464
  if (merged.agent.cwd !== null && !merged.agent.cwd.startsWith('/')) {
464
465
  fail('agent.cwd', 'must be an absolute path')
465
466
  }
467
+ merged.agent.workspaceTitle = expectNullableString(merged.agent.workspaceTitle, 'agent.workspaceTitle', {
468
+ allowEmpty: false,
469
+ })
466
470
  merged.agent.sandboxMode = expectEnum(merged.agent.sandboxMode, 'agent.sandboxMode', [
467
471
  'inherit',
468
472
  'read-only',
package/lib/panel.js CHANGED
@@ -49,6 +49,7 @@ const FIELDS = [
49
49
  { path: 'auth.tokenEnv', type: 'string', group: 'auth', nullable: true, placeholder: 'A2A_AUTH_TOKEN' },
50
50
  { path: 'auth.allowRemoteUnauthenticated', type: 'boolean', group: 'auth' },
51
51
  { path: 'agent.cwd', type: 'string', group: 'agent', nullable: true, placeholder: '/srv/a2a-workspace' },
52
+ { path: 'agent.workspaceTitle', type: 'string', group: 'agent', nullable: true, placeholder: 'A2A Agents' },
52
53
  { path: 'agent.preset', type: 'string', group: 'agent', nullable: true, optionsKey: 'presets' },
53
54
  { path: 'agent.provider', type: 'string', group: 'agent', nullable: true, optionsKey: 'providers' },
54
55
  { path: 'agent.model', type: 'string', group: 'agent', nullable: true, optionsKey: 'models' },
@@ -305,7 +306,7 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
305
306
  res.setHeader('Cache-Control', 'no-store')
306
307
  res.json({
307
308
  ok: true,
308
- plugin: { name: PLUGIN_NAME, version: '1.2.0' },
309
+ plugin: { name: PLUGIN_NAME, version: '1.3.0' },
309
310
  mountPath,
310
311
  ...payload,
311
312
  ...(typeof status === 'function' ? { server: status() } : {}),
@@ -484,7 +485,7 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
484
485
  logger.info('[a2a] configuration updated from the settings panel')
485
486
  res.json({
486
487
  ok: true,
487
- plugin: { name: PLUGIN_NAME, version: '1.2.0' },
488
+ plugin: { name: PLUGIN_NAME, version: '1.3.0' },
488
489
  mountPath,
489
490
  ...describe(live, liveEntry, []),
490
491
  ...(typeof status === 'function' ? { server: status() } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai_mind/dsh-a2a",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Production A2A (Agent2Agent) v1.0 server for DeepSeek Harness agents: JSON-RPC + HTTP+JSON bindings, streaming, tasks, push notifications and signed Agent Cards.",
5
5
  "type": "module",
6
6
  "license": "MIT",