@ai_mind/dsh-a2a 1.1.4 → 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 +10 -1
- package/client.js +112 -2
- package/lib/bridge.js +45 -0
- package/lib/config.js +4 -0
- package/lib/panel.js +107 -6
- package/package.json +1 -1
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
|
|
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,6 +108,11 @@ window.__ModuleLoader__.load({
|
|
|
108
108
|
fieldAgentProvider: '模型提供方',
|
|
109
109
|
fieldAgentModel: '模型',
|
|
110
110
|
fieldAgentReasoningEffort: '推理强度',
|
|
111
|
+
fieldAgentWorkspaceTitle: '工作区名称(自动分组)',
|
|
112
|
+
optionDefault: '(使用默认)',
|
|
113
|
+
defaultModel: '默认模型(留空时)',
|
|
114
|
+
modelPairHint: '模型提供方和模型必须同时选择,或都留空使用默认。',
|
|
115
|
+
workspaceHint: 'A2A 会话会归入 agent.cwd 对应的工作区;若该目录还不是工作区,填写“工作区名称”即可为该目录创建分组。',
|
|
111
116
|
fieldAgentSandboxMode: '沙箱模式',
|
|
112
117
|
fieldAgentApprovalPolicy: '审批策略',
|
|
113
118
|
fieldAgentMaxConcurrentTasks: '最大并发任务',
|
|
@@ -220,6 +225,11 @@ window.__ModuleLoader__.load({
|
|
|
220
225
|
fieldAgentProvider: 'Model provider',
|
|
221
226
|
fieldAgentModel: 'Model',
|
|
222
227
|
fieldAgentReasoningEffort: 'Reasoning effort',
|
|
228
|
+
fieldAgentWorkspaceTitle: 'Workspace title (auto-group)',
|
|
229
|
+
optionDefault: '(use default)',
|
|
230
|
+
defaultModel: 'Default model (when empty)',
|
|
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.',
|
|
223
233
|
fieldAgentSandboxMode: 'Sandbox mode',
|
|
224
234
|
fieldAgentApprovalPolicy: 'Approval policy',
|
|
225
235
|
fieldAgentMaxConcurrentTasks: 'Max concurrent tasks',
|
|
@@ -369,6 +379,15 @@ window.__ModuleLoader__.load({
|
|
|
369
379
|
return typeof label === 'string' && label !== key ? label : value
|
|
370
380
|
}
|
|
371
381
|
|
|
382
|
+
function panelOptionsPath(panelPath, provider, model) {
|
|
383
|
+
const base = panelPath.replace(/\/config$/, '/options')
|
|
384
|
+
const params = new URLSearchParams()
|
|
385
|
+
if (provider) params.set('provider', provider)
|
|
386
|
+
if (model) params.set('model', model)
|
|
387
|
+
const query = params.toString()
|
|
388
|
+
return query ? `${base}?${query}` : base
|
|
389
|
+
}
|
|
390
|
+
|
|
372
391
|
function draftFrom(panel) {
|
|
373
392
|
const values = {}
|
|
374
393
|
for (const field of panel && Array.isArray(panel.fields) ? panel.fields : []) values[field.path] = field.value
|
|
@@ -428,6 +447,37 @@ window.__ModuleLoader__.load({
|
|
|
428
447
|
const [saveError, setSaveError] = React.useState(null)
|
|
429
448
|
const [copied, setCopied] = React.useState(null)
|
|
430
449
|
const [reloadKey, setReloadKey] = React.useState(0)
|
|
450
|
+
const [optionSets, setOptionSets] = React.useState({})
|
|
451
|
+
const optionsRequest = React.useRef(0)
|
|
452
|
+
|
|
453
|
+
const loadOptions = async (panelPath, provider, model, signal) => {
|
|
454
|
+
if (!panelPath) return
|
|
455
|
+
const requestId = ++optionsRequest.current
|
|
456
|
+
try {
|
|
457
|
+
const response = await fetch(panelOptionsPath(panelPath, provider, model), {
|
|
458
|
+
cache: 'no-store',
|
|
459
|
+
credentials: 'same-origin',
|
|
460
|
+
signal,
|
|
461
|
+
})
|
|
462
|
+
if (!response.ok) return
|
|
463
|
+
const data = await response.json()
|
|
464
|
+
if (requestId !== optionsRequest.current) return
|
|
465
|
+
setOptionSets(data)
|
|
466
|
+
if (provider && !model && Array.isArray(data.models) && data.models.length > 0) {
|
|
467
|
+
const preferred =
|
|
468
|
+
data.default && data.default.provider === provider && data.models.some((entry) => entry.value === data.default.model)
|
|
469
|
+
? data.default.model
|
|
470
|
+
: data.models[0].value
|
|
471
|
+
setDraft((previous) => {
|
|
472
|
+
if (previous['agent.provider'] !== provider || previous['agent.model']) return previous
|
|
473
|
+
return { ...previous, 'agent.model': preferred, 'agent.reasoningEffort': '' }
|
|
474
|
+
})
|
|
475
|
+
void loadOptions(panelPath, provider, preferred)
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
478
|
+
/* options are optional; the text inputs remain usable */
|
|
479
|
+
}
|
|
480
|
+
}
|
|
431
481
|
|
|
432
482
|
React.useEffect(() => {
|
|
433
483
|
const controller = new AbortController()
|
|
@@ -487,6 +537,10 @@ window.__ModuleLoader__.load({
|
|
|
487
537
|
}
|
|
488
538
|
setState({ status: 'ready', card, health, denied, error: null, panel, panelPath, panelError })
|
|
489
539
|
setDraft(draftFrom(panel))
|
|
540
|
+
if (panel && panelPath) {
|
|
541
|
+
const fieldValue = (path) => panel.fields.find((field) => field.path === path)?.value
|
|
542
|
+
void loadOptions(panelPath, fieldValue('agent.provider'), fieldValue('agent.model'), controller.signal)
|
|
543
|
+
}
|
|
490
544
|
} catch (error) {
|
|
491
545
|
if (error && error.name === 'AbortError') return
|
|
492
546
|
setState({ status: 'error', card: null, health: null, denied: false, error: String((error && error.message) || error), panel: null, panelPath: null, panelError: null })
|
|
@@ -573,11 +627,35 @@ window.__ModuleLoader__.load({
|
|
|
573
627
|
const dirty = panel
|
|
574
628
|
? panel.fields.some((field) => !sameValue(field, draft[field.path])) || token !== '' || clearToken
|
|
575
629
|
: false
|
|
630
|
+
const invalidModelPair = Boolean(draft['agent.provider']) !== Boolean(draft['agent.model'])
|
|
576
631
|
|
|
577
632
|
const setField = (path, value) => {
|
|
578
|
-
setDraft((previous) =>
|
|
633
|
+
setDraft((previous) => {
|
|
634
|
+
const next = { ...previous, [path]: value }
|
|
635
|
+
if (path === 'agent.provider') {
|
|
636
|
+
next['agent.model'] = ''
|
|
637
|
+
next['agent.reasoningEffort'] = ''
|
|
638
|
+
} else if (path === 'agent.model') {
|
|
639
|
+
if (value === '') next['agent.provider'] = ''
|
|
640
|
+
else if (!previous['agent.provider']) {
|
|
641
|
+
next['agent.provider'] = optionSets.provider ?? optionSets.default?.provider ?? ''
|
|
642
|
+
}
|
|
643
|
+
next['agent.reasoningEffort'] = ''
|
|
644
|
+
}
|
|
645
|
+
return next
|
|
646
|
+
})
|
|
579
647
|
setSaved(false)
|
|
580
648
|
setSaveError(null)
|
|
649
|
+
if (path === 'agent.provider') {
|
|
650
|
+
void loadOptions(state.panelPath, value ?? '', '')
|
|
651
|
+
} else if (path === 'agent.model') {
|
|
652
|
+
if (value === '') {
|
|
653
|
+
void loadOptions(state.panelPath, '', '')
|
|
654
|
+
} else {
|
|
655
|
+
const provider = draft['agent.provider'] || optionSets.provider || optionSets.default?.provider || ''
|
|
656
|
+
void loadOptions(state.panelPath, provider, value)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
581
659
|
}
|
|
582
660
|
|
|
583
661
|
const save = async () => {
|
|
@@ -618,6 +696,26 @@ window.__ModuleLoader__.load({
|
|
|
618
696
|
const renderControl = (field) => {
|
|
619
697
|
const value = draft[field.path]
|
|
620
698
|
const onChange = (event) => setField(field.path, event.target.value)
|
|
699
|
+
const dynamicOptions = field.optionsKey ? optionSets[field.optionsKey] : undefined
|
|
700
|
+
if (field.optionsKey && Array.isArray(dynamicOptions) && dynamicOptions.length > 0) {
|
|
701
|
+
const current = value == null ? '' : String(value)
|
|
702
|
+
const options =
|
|
703
|
+
!current || dynamicOptions.some((option) => String(option.value) === current)
|
|
704
|
+
? dynamicOptions
|
|
705
|
+
: [{ value: current, label: `${current} (current)` }, ...dynamicOptions]
|
|
706
|
+
return h(
|
|
707
|
+
'select',
|
|
708
|
+
{ className: 'a2aui-select', value: current, onChange },
|
|
709
|
+
h('option', { value: '' }, t('optionDefault')),
|
|
710
|
+
options.map((option) =>
|
|
711
|
+
h(
|
|
712
|
+
'option',
|
|
713
|
+
{ key: option.value, value: option.value },
|
|
714
|
+
option.description ? `${option.label} — ${option.description}` : option.label,
|
|
715
|
+
),
|
|
716
|
+
),
|
|
717
|
+
)
|
|
718
|
+
}
|
|
621
719
|
if (field.type === 'boolean') {
|
|
622
720
|
return h(
|
|
623
721
|
'label',
|
|
@@ -719,6 +817,15 @@ window.__ModuleLoader__.load({
|
|
|
719
817
|
h('div', null, renderControl(field)),
|
|
720
818
|
),
|
|
721
819
|
),
|
|
820
|
+
group === 'agent' && optionSets.default
|
|
821
|
+
? h(
|
|
822
|
+
'p',
|
|
823
|
+
{ className: 'a2aui-field-note' },
|
|
824
|
+
`${t('defaultModel')}: ${optionSets.default.provider ?? '?'} / ${optionSets.default.model ?? '?'}` +
|
|
825
|
+
(optionSets.default.reasoningEffort ? ` · ${optionSets.default.reasoningEffort}` : ''),
|
|
826
|
+
)
|
|
827
|
+
: null,
|
|
828
|
+
group === 'agent' ? h('p', { className: 'a2aui-field-note' }, t('workspaceHint')) : null,
|
|
722
829
|
)
|
|
723
830
|
return group === 'auth' ? [node, secretBlock] : [node]
|
|
724
831
|
}),
|
|
@@ -727,11 +834,14 @@ window.__ModuleLoader__.load({
|
|
|
727
834
|
{ className: 'a2aui-form-foot' },
|
|
728
835
|
h(
|
|
729
836
|
'button',
|
|
730
|
-
{ type: 'button', className: 'a2aui-btn a2aui-btnPrimary', disabled: !dirty || saving, onClick: save },
|
|
837
|
+
{ type: 'button', className: 'a2aui-btn a2aui-btnPrimary', disabled: !dirty || saving || invalidModelPair, onClick: save },
|
|
731
838
|
saving ? t('saving') : t('save'),
|
|
732
839
|
),
|
|
733
840
|
h('button', { type: 'button', className: 'a2aui-btn', onClick: () => setReloadKey((value) => value + 1) }, t('reload')),
|
|
734
841
|
dirty ? h('span', { className: 'a2aui-status' }, t('dirty')) : null,
|
|
842
|
+
invalidModelPair
|
|
843
|
+
? h('span', { className: 'a2aui-status', 'data-tone': 'warn' }, t('modelPairHint'))
|
|
844
|
+
: null,
|
|
735
845
|
saved ? h('span', { className: 'a2aui-status', 'data-tone': 'ok' }, t('saved')) : null,
|
|
736
846
|
saveError ? h('span', { className: 'a2aui-status', 'data-tone': 'error' }, `${t('saveError')}: ${saveError}`) : null,
|
|
737
847
|
h('span', { className: 'a2aui-status' }, t('configHint')),
|
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,10 +49,11 @@ 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.
|
|
53
|
-
{ path: 'agent.
|
|
54
|
-
{ path: 'agent.
|
|
55
|
-
{ path: 'agent.
|
|
52
|
+
{ path: 'agent.workspaceTitle', type: 'string', group: 'agent', nullable: true, placeholder: 'A2A Agents' },
|
|
53
|
+
{ path: 'agent.preset', type: 'string', group: 'agent', nullable: true, optionsKey: 'presets' },
|
|
54
|
+
{ path: 'agent.provider', type: 'string', group: 'agent', nullable: true, optionsKey: 'providers' },
|
|
55
|
+
{ path: 'agent.model', type: 'string', group: 'agent', nullable: true, optionsKey: 'models' },
|
|
56
|
+
{ path: 'agent.reasoningEffort', type: 'string', group: 'agent', nullable: true, optionsKey: 'reasoning' },
|
|
56
57
|
{
|
|
57
58
|
path: 'agent.sandboxMode',
|
|
58
59
|
type: 'enum',
|
|
@@ -175,6 +176,7 @@ function describe(config, entry, warnings) {
|
|
|
175
176
|
...(field.min === undefined ? {} : { min: field.min }),
|
|
176
177
|
...(field.max === undefined ? {} : { max: field.max }),
|
|
177
178
|
...(field.placeholder === undefined ? {} : { placeholder: field.placeholder }),
|
|
179
|
+
...(field.optionsKey === undefined ? {} : { optionsKey: field.optionsKey }),
|
|
178
180
|
value: value === undefined ? null : value,
|
|
179
181
|
}
|
|
180
182
|
})
|
|
@@ -202,6 +204,23 @@ function describe(config, entry, warnings) {
|
|
|
202
204
|
* @param {() => object} [options.health] live health snapshot
|
|
203
205
|
* @returns {import('express').Router}
|
|
204
206
|
*/
|
|
207
|
+
/** First value of an express query parameter. */
|
|
208
|
+
function firstQuery(value) {
|
|
209
|
+
if (Array.isArray(value)) return value[0]
|
|
210
|
+
return typeof value === 'string' ? value : undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Resolve an optional Host call without letting a slow model catalog block the panel. */
|
|
214
|
+
function withTimeout(promise, fallback, ms) {
|
|
215
|
+
return Promise.race([
|
|
216
|
+
Promise.resolve(promise).catch(() => fallback),
|
|
217
|
+
new Promise((resolve) => {
|
|
218
|
+
const timer = setTimeout(() => resolve(fallback), ms)
|
|
219
|
+
timer.unref?.()
|
|
220
|
+
}),
|
|
221
|
+
])
|
|
222
|
+
}
|
|
223
|
+
|
|
205
224
|
function safeHealth(health, logger) {
|
|
206
225
|
try {
|
|
207
226
|
return health()
|
|
@@ -287,7 +306,7 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
|
|
|
287
306
|
res.setHeader('Cache-Control', 'no-store')
|
|
288
307
|
res.json({
|
|
289
308
|
ok: true,
|
|
290
|
-
plugin: { name: PLUGIN_NAME, version: '1.
|
|
309
|
+
plugin: { name: PLUGIN_NAME, version: '1.3.0' },
|
|
291
310
|
mountPath,
|
|
292
311
|
...payload,
|
|
293
312
|
...(typeof status === 'function' ? { server: status() } : {}),
|
|
@@ -295,6 +314,88 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
|
|
|
295
314
|
})
|
|
296
315
|
})
|
|
297
316
|
|
|
317
|
+
router.get('/options', async (req, res) => {
|
|
318
|
+
if (authorize(req, res)) return
|
|
319
|
+
const llm = ctx.get?.('llm')
|
|
320
|
+
const presetsService = ctx.get?.('agentPresets')
|
|
321
|
+
const defaults = ctx.get?.('agentDefaultModel')
|
|
322
|
+
let selection = null
|
|
323
|
+
try {
|
|
324
|
+
selection = defaults?.currentSelection?.() ?? null
|
|
325
|
+
} catch (error) {
|
|
326
|
+
logger.warn(`[a2a] cannot read the default model selection: ${errorMessage(error)}`)
|
|
327
|
+
}
|
|
328
|
+
const requestedProvider = firstQuery(req.query.provider)?.trim() || null
|
|
329
|
+
const requestedModel = firstQuery(req.query.model)?.trim() || null
|
|
330
|
+
const provider = requestedProvider ?? selection?.provider ?? null
|
|
331
|
+
const model = requestedModel ?? (provider === selection?.provider ? selection?.model : null) ?? null
|
|
332
|
+
|
|
333
|
+
let presets = []
|
|
334
|
+
try {
|
|
335
|
+
const listed = await withTimeout(presetsService?.list?.(), [], 3000)
|
|
336
|
+
presets = (listed ?? []).map((preset) => ({
|
|
337
|
+
value: preset.id,
|
|
338
|
+
label: preset.name ?? preset.id,
|
|
339
|
+
description: preset.broken ? `unavailable: ${preset.broken}` : (preset.description ?? null),
|
|
340
|
+
}))
|
|
341
|
+
} catch (error) {
|
|
342
|
+
logger.warn(`[a2a] cannot list agent presets: ${errorMessage(error)}`)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let providers = []
|
|
346
|
+
try {
|
|
347
|
+
providers = (llm?.listProviders?.() ?? []).map((entry) => ({
|
|
348
|
+
value: entry.id,
|
|
349
|
+
label: entry.name ?? entry.id,
|
|
350
|
+
}))
|
|
351
|
+
} catch (error) {
|
|
352
|
+
logger.warn(`[a2a] cannot list model providers: ${errorMessage(error)}`)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let models = []
|
|
356
|
+
if (provider !== null && typeof llm?.listModels === 'function') {
|
|
357
|
+
try {
|
|
358
|
+
const listed = await withTimeout(llm.listModels(provider), [], 4000)
|
|
359
|
+
models = (listed ?? []).map((entry) => ({
|
|
360
|
+
value: entry.id,
|
|
361
|
+
label: entry.name && entry.name !== entry.id ? `${entry.name} (${entry.id})` : entry.id,
|
|
362
|
+
description: entry.description ?? null,
|
|
363
|
+
}))
|
|
364
|
+
} catch (error) {
|
|
365
|
+
logger.warn(`[a2a] cannot list models for ${provider}: ${errorMessage(error)}`)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let reasoning = []
|
|
370
|
+
let reasoningDefault = null
|
|
371
|
+
if (provider !== null && model !== null && typeof llm?.resolveModelInfo === 'function') {
|
|
372
|
+
try {
|
|
373
|
+
const info = await withTimeout(llm.resolveModelInfo(provider, model), null, 4000)
|
|
374
|
+
reasoning = (info?.reasoning?.efforts ?? []).map((effort) => ({
|
|
375
|
+
value: effort.id,
|
|
376
|
+
label: effort.name ?? effort.id,
|
|
377
|
+
description: effort.description ?? null,
|
|
378
|
+
}))
|
|
379
|
+
reasoningDefault = info?.reasoning?.defaultEffort ?? null
|
|
380
|
+
} catch (error) {
|
|
381
|
+
logger.warn(`[a2a] cannot resolve reasoning options for ${provider}/${model}: ${errorMessage(error)}`)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
386
|
+
res.json({
|
|
387
|
+
ok: true,
|
|
388
|
+
presets,
|
|
389
|
+
providers,
|
|
390
|
+
models,
|
|
391
|
+
reasoning,
|
|
392
|
+
reasoningDefault,
|
|
393
|
+
default: selection,
|
|
394
|
+
provider,
|
|
395
|
+
model,
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
298
399
|
router.put('/config', json, async (req, res) => {
|
|
299
400
|
if (authorize(req, res)) return
|
|
300
401
|
const configEditor = editor()
|
|
@@ -384,7 +485,7 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
|
|
|
384
485
|
logger.info('[a2a] configuration updated from the settings panel')
|
|
385
486
|
res.json({
|
|
386
487
|
ok: true,
|
|
387
|
-
plugin: { name: PLUGIN_NAME, version: '1.
|
|
488
|
+
plugin: { name: PLUGIN_NAME, version: '1.3.0' },
|
|
388
489
|
mountPath,
|
|
389
490
|
...describe(live, liveEntry, []),
|
|
390
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.
|
|
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",
|