From f0cf9e5de8122cb5e1d7990ffd5f97522e6dd683 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sun, 4 Oct 2026 23:30:53 +0800 Subject: [PATCH 1/2] fix(experts): preserve Library references and MCP selection on summon --- apps/web/pnpm-lock.yaml | 3 +++ .../probe-expert-conversation-entry.mjs | 24 +++++++++++++++++++ packages/contracts/package.json | 4 ++++ packages/contracts/src/composer.ts | 20 ++++++++++++++++ packages/plugins/connectors/package.json | 5 ++-- packages/plugins/connectors/src/client.tsx | 6 +++++ packages/plugins/experts/src/client.tsx | 22 ++++++++++++++--- .../src/client/ConversationExperts.tsx | 24 ++++++++++++++----- packages/plugins/library/src/client.tsx | 12 ++++++++++ 9 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 packages/contracts/src/composer.ts diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 6eb615a13d..78a88dc11f 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -642,6 +642,9 @@ importers: react: specifier: 19.2.4 version: 19.2.4 + workdsh-contracts: + specifier: workspace:* + version: link:../../contracts workdsh-ui: specifier: workspace:* version: link:../../ui diff --git a/apps/web/scripts/probe-expert-conversation-entry.mjs b/apps/web/scripts/probe-expert-conversation-entry.mjs index 68e51db6f1..5e06a803e7 100644 --- a/apps/web/scripts/probe-expert-conversation-entry.mjs +++ b/apps/web/scripts/probe-expert-conversation-entry.mjs @@ -196,6 +196,30 @@ try { await connectorButton.click(); await useConnector.click(); await expect(connectors.getByRole('button', { name: '从本次对话移除 浮层 MCP 验收', exact: true })).toHaveText('已选', { timeout: 20000 }); + await page.keyboard.press('Escape'); + await editor.fill('组合任务草稿'); + await libraryButton.click(); + await libraryPicker.getByRole('button', { name: '打开 浮层验收资料', exact: true }).click(); + await libraryPicker.getByRole('button', { name: '添加 浮层体验验收.md', exact: true }).click(); + await expect(editor.locator('[data-composer-chip="workdsh-library"]')).toHaveCount(1); + await page.getByRole('button', { name: '召唤专家', exact: true }).click(); + const comboResponse = page.waitForResponse(response => response.url().endsWith('/api/workdsh-experts') && response.request().postDataJSON()?.endpoint === 'create-execution'); + await choice.click(); + const comboCreation = (await (await comboResponse).json()).value; + await expect(editor).toContainText('组合任务草稿'); + await expect(editor.locator('[data-composer-chip="workdsh-library"]')).toHaveCount(1); + await expect(page.getByRole('button', { name: '连接器:浮层 MCP 验收', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: '召唤专家', exact: true })).toContainText('门店经营分析验收专家'); + const selectedLibrary = await featureApi('library', 'task-selection', { sessionId: comboCreation.sessionId }); + assert.equal(selectedLibrary.length, 1); + assert.equal(selectedLibrary[0].name, '浮层体验验收.md'); + const selectedMcp = await featureApi('connectors', 'selection', { sessionId: comboCreation.sessionId }); + assert.equal(selectedMcp.length, 1); + await editor.locator('[data-composer-chip="workdsh-library"]').click(); + await expect(page.getByText('内容用于对话引用', { exact: false }).first()).toBeVisible(); + await page.screenshot({ path: join(artifacts, 'expert-library-mcp-handoff.png'), animations: 'disabled' }); + pass('Expert handoff preserves clickable Library chips, target task selection and MCP selection'); + await page.getByRole('button', { name: '连接器:浮层 MCP 验收', exact: true }).click(); await connectors.getByRole('button', { name: '从本次对话移除 浮层 MCP 验收', exact: true }).click(); await expect(useConnector).toHaveText('使用'); pass('MCP popover selects and removes a real local connector, with Escape dismissal'); diff --git a/packages/contracts/package.json b/packages/contracts/package.json index c4ff9b4f96..5740e5741c 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -38,6 +38,10 @@ "./enterprise": { "types": "./dist/enterprise.d.ts", "default": "./dist/enterprise.js" + }, + "./composer": { + "types": "./dist/composer.d.ts", + "default": "./dist/composer.js" } }, "files": [ diff --git a/packages/contracts/src/composer.ts b/packages/contracts/src/composer.ts new file mode 100644 index 0000000000..f04494bdd3 --- /dev/null +++ b/packages/contracts/src/composer.ts @@ -0,0 +1,20 @@ +/** Public draft handoff between independently owned composer features. */ +export interface ComposerReference { + source: string; + ref: string; + readonly label: string; + readonly appearance?: 'session' | 'file' | 'folder'; + readonly clipboardText: string; + readonly offset: number; + readonly length: number; +} +export interface ComposerHandoffSource { + readonly sessionId: string; + readonly draft: string; + readonly references: readonly ComposerReference[]; +} +export interface ComposerHandoff { + readonly sourceSessionId: string; + readonly targetSessionId: string; + readonly references: ComposerReference[]; +} diff --git a/packages/plugins/connectors/package.json b/packages/plugins/connectors/package.json index f9e0c2cd98..723a1245b6 100644 --- a/packages/plugins/connectors/package.json +++ b/packages/plugins/connectors/package.json @@ -24,7 +24,7 @@ "locale/*.json" ], "scripts": { - "build": "node ../../../apps/web/node_modules/pnpm/bin/pnpm.cjs --dir ../../../apps/web --filter workdsh-ui build && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node ../../../apps/web/scripts/build-connectors.mjs", + "build": "node ../../../apps/web/node_modules/pnpm/bin/pnpm.cjs --dir ../../../apps/web --filter workdsh-contracts build && node ../../../apps/web/node_modules/pnpm/bin/pnpm.cjs --dir ../../../apps/web --filter workdsh-ui build && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node ../../../apps/web/scripts/build-connectors.mjs", "typecheck": "tsc -p tsconfig.json --noEmit" }, "peerDependencies": { @@ -64,7 +64,8 @@ "react": "19.2.4", "workdsh-ui": "workspace:*", "zod": "^4.3.6", - "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2" + "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2", + "workdsh-contracts": "workspace:*" }, "dsh": { "bundle": { diff --git a/packages/plugins/connectors/src/client.tsx b/packages/plugins/connectors/src/client.tsx index 24e4e34ad5..736d76140c 100644 --- a/packages/plugins/connectors/src/client.tsx +++ b/packages/plugins/connectors/src/client.tsx @@ -1,3 +1,5 @@ +import type { ComposerHandoff, ComposerHandoffSource } from 'workdsh-contracts/composer'; +declare module '@deepseek-ai/cordis' { interface Events { 'workdsh/composer-handoff'(handoff: ComposerHandoff): Promise; } } import type { Context } from '@deepseek-ai/cordis'; import type {} from '@deepseek-ai/dsh-client-connection/client'; import type {} from '@deepseek-ai/dsh-client-ui-layout/client'; @@ -15,6 +17,10 @@ export function apply(ctx: Context): void { const lifetime = new AbortController(); ctx.effect(() => () => lifetime.abort(), 'workdsh.connectors.client'); const management = createConnectorManagementClient(ctx, lifetime.signal); + ctx.on('workdsh/composer-handoff', async handoff => { + const selected = await management.selection(handoff.sourceSessionId); + await management.setSelection(handoff.targetSessionId, selected); + }); const openCapability = (key: string) => ctx.layout.selectPanel(key as Parameters[0]); const hasCapability = (key: string) => ctx.slots.entriesOfSlot('main').some(entry => entry.options.key === key); ctx.slots.inject('main', () => ctx.slots.register({ diff --git a/packages/plugins/experts/src/client.tsx b/packages/plugins/experts/src/client.tsx index 72721c7af2..82d7525b50 100644 --- a/packages/plugins/experts/src/client.tsx +++ b/packages/plugins/experts/src/client.tsx @@ -1,3 +1,5 @@ +import type { ComposerHandoff, ComposerHandoffSource } from 'workdsh-contracts/composer'; +declare module '@deepseek-ai/cordis' { interface Events { 'workdsh/composer-handoff'(handoff: ComposerHandoff): Promise; } } import type { ActivityPresentation } from 'workdsh-contracts/activity'; declare module '@deepseek-ai/cordis' { interface Context { activityPresentation: ActivityPresentation; } } import { installExpertPresetMenu } from './client/PresetMenu.js'; @@ -19,7 +21,7 @@ import { PendingExpertDraft, pendingExpertDraftKey, pendingExpertDraftEvent, exp import { createExpertManagementClient } from './client/management.js'; export const name = 'workdsh-experts-client'; -export const inject = ['slots', 'layout', 'sessions', 'workspaces', 'remote', 'remote.session', 'connection', 'uiWorkspace']; +export const inject = ['slots', 'layout', 'sessions', 'workspaces', 'remote', 'remote.session', 'connection', 'uiWorkspace', 'conversation']; type SessionId = Awaited>; @@ -105,7 +107,7 @@ export function apply(ctx: Context): void { throw new Error('未能打开专家任务,请稍后在会话列表中查看。'); }; - const summon = async (expertId: string, revisionId: string | undefined, draftText: string | undefined): Promise => { + const summon = async (expertId: string, revisionId: string | undefined, draftText: string | undefined, source?: ComposerHandoffSource): Promise => { lifetime.signal.throwIfAborted(); const workspace = resolveWorkspace(); const plan = await management.prepareExecution(expertId, { @@ -118,11 +120,25 @@ export function apply(ctx: Context): void { } const creation = await management.createExecution(plan.executionPlanId, management.newOperationId('create-execution')); const sessionId = creation.sessionId as SessionId; + const references = source?.references.map(reference => ({ ...reference })) ?? []; + if (source) await ctx.parallel('workdsh/composer-handoff', { sourceSessionId: source.sessionId, targetSessionId: String(sessionId), references }); await openSession(sessionId); ctx.layout.selectPanel(null); if (creation.handoffId) { const handoff = await management.consumeHandoff(creation.handoffId, creation.handoffId); - if (handoff.text) await seedDraft(sessionId, handoff.text); + if (!source && handoff.text) await seedDraft(sessionId, handoff.text); + } + if (source) { + const binding = sessions.binding(sessionId); + if (!binding) throw new Error('专家任务输入框尚未就绪,请重试。'); + const input = ctx.conversation.input.for(binding.ctx); + const state = input.state.getSnapshot(); + if (state.draft || state.attachmentIds.length || state.phase !== 'plain') throw new Error('专家任务已有草稿,不能覆盖。'); + input.setDraft(source.draft); + for (const reference of [...references].sort((a, b) => b.offset - a.offset)) { + const current = input.state.getSnapshot(); + if (!input.insertReference(reference, { start: reference.offset, end: reference.offset + reference.length, draftRev: current.draftRev })) throw new Error('资料引用未能恢复,请在新任务中重新添加。'); + } } }; diff --git a/packages/plugins/experts/src/client/ConversationExperts.tsx b/packages/plugins/experts/src/client/ConversationExperts.tsx index 54ad6ade38..b6f9b99582 100644 --- a/packages/plugins/experts/src/client/ConversationExperts.tsx +++ b/packages/plugins/experts/src/client/ConversationExperts.tsx @@ -1,3 +1,4 @@ +import type { ComposerHandoffSource } from 'workdsh-contracts/composer'; import * as React from 'react'; import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'; import type { ExpertSummary } from '../shared.js'; @@ -6,8 +7,9 @@ import { Icon } from 'workdsh-ui'; const pickerCss = ` .wd-expert-picker{position:relative;display:flex;align-items:center} -.wd-expert-trigger{display:flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;border:0;border-radius:10px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer} +.wd-expert-trigger{display:flex;align-items:center;justify-content:center;min-width:36px;max-width:190px;height:36px;padding:0 9px;gap:7px;border:0;border-radius:10px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer} .wd-expert-trigger:hover,.wd-expert-trigger[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)} +.wd-expert-trigger .expert-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:13px/20px "PingFang SC","Microsoft YaHei",sans-serif}.wd-expert-trigger svg{flex:none} .wd-expert-trigger:disabled{opacity:.5;cursor:default} .wd-expert-popover{position:absolute;z-index:90;left:0;bottom:calc(100% + 10px);width:336px;max-height:min(430px,60vh);overflow:auto;padding:8px;border-radius:15px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-panel);font:14px/20px "PingFang SC","Microsoft YaHei",sans-serif} .wd-expert-row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 5px;border-radius:10px}.wd-expert-row:hover{background:var(--dsw-alias-interactive-bg-hover)} @@ -23,11 +25,11 @@ const pickerCss = ` type Props = PropsRuntime<'conversation.input.left'> & { management: ExpertManagementClient; openManagement: () => void; - summon: (id: string, revision: string | undefined, draft: string | undefined) => Promise; + summon: (id: string, revision: string | undefined, draft: string | undefined, source?: ComposerHandoffSource) => Promise; }; /** Use the governed execution flow, never select an internal preset directly. */ -export function ConversationExperts({ useInput, management, summon, openManagement }: Props) { +export function ConversationExperts({ useInput, sessionId, management, summon, openManagement }: Props) { const input = useInput(state => state); const root = React.useRef(null); const trigger = React.useRef(null); @@ -36,6 +38,16 @@ export function ConversationExperts({ useInput, management, summon, openManageme const [error, setError] = React.useState(''); const [loading, setLoading] = React.useState(false); const [acting, setActing] = React.useState(false); + const [active, setActive] = React.useState<{ id: string; name: string }>(); + React.useEffect(() => { + const controller = new AbortController(); + setActive(undefined); + void management.verifyBinding(String(sessionId), controller.signal).then(async binding => { + const detail = await management.get(binding.expertRevisionRef.expertId, binding.expertRevisionRef.revisionId, controller.signal); + if (!controller.signal.aborted) setActive({ id: detail.expert.id, name: detail.revision?.definition.name ?? '已召唤专家' }); + }).catch(() => { /* Ordinary unbound conversations have no selected expert. */ }); + return () => controller.abort(); + }, [sessionId, management]); React.useEffect(() => { if (!open || acting) return; const outside = (event: PointerEvent) => { if (!root.current?.contains(event.target as Node)) setOpen(false); }; @@ -61,7 +73,7 @@ export function ConversationExperts({ useInput, management, summon, openManageme }, [open, management]); const choose = async (row: ExpertSummary) => { setActing(true); setError(''); - try { await summon(row.id, row.publishedRevisionRef?.revisionId, input.draft || undefined); setOpen(false); } + try { await summon(row.id, row.publishedRevisionRef?.revisionId, input.draft || undefined, { sessionId: String(sessionId), draft: input.draft, references: input.occurrences }); setOpen(false); } catch (cause) { setError(cause instanceof Error ? cause.message : '召唤失败,请重试。'); } finally { setActing(false); } }; @@ -69,14 +81,14 @@ export function ConversationExperts({ useInput, management, summon, openManageme return <>
- + {open &&
{loading &&
正在加载专家…
} {error &&
{error}
} {!loading && !error && !items.length &&
暂无可用专家,请先导入或发布专家。
} {items.map(row =>
{row.name}{row.description}
- +
)}
} diff --git a/packages/plugins/library/src/client.tsx b/packages/plugins/library/src/client.tsx index e4a680ccc8..a31f4ed92b 100644 --- a/packages/plugins/library/src/client.tsx +++ b/packages/plugins/library/src/client.tsx @@ -1,3 +1,5 @@ +import type { ComposerHandoff, ComposerHandoffSource } from 'workdsh-contracts/composer'; +declare module '@deepseek-ai/cordis' { interface Events { 'workdsh/composer-handoff'(handoff: ComposerHandoff): Promise; } } import type { Context } from '@deepseek-ai/cordis'; import type {} from '@deepseek-ai/dsh-client-connection/client'; import type {} from '@deepseek-ai/dsh-client-ui-layout/client'; @@ -52,6 +54,16 @@ export function apply(ctx: Context): void { if (inserted) void management.taskSelection(sessionId).then(current => management.setTaskSelection(sessionId, [...new Set([...current.map(row => row.nodeId), value.nodeId])])).catch(() => []); return inserted; }; + ctx.on('workdsh/composer-handoff', async handoff => { + const selected = await management.taskSelection(handoff.sourceSessionId); + const referenced = handoff.references.filter(reference => reference.source === 'workdsh-library').map(reference => decodeRef(reference.ref).nodeId); + await management.setTaskSelection(handoff.targetSessionId, [...new Set([...selected.map(row => row.nodeId), ...referenced])]); + for (const reference of handoff.references) { + if (reference.source !== 'workdsh-library') continue; + const value = decodeRef(reference.ref); + reference.ref = encodeRef({ ...value, sessionId: handoff.targetSessionId }); + } + }); // alpha.2: the list snapshot has no `current`; the shown Session derives from the // view owner's mainView retention (same rule as the official ui-session publishMain). const currentSessionId = () => { From 80fba473c7ff941617b8996e940aaaba723f162c Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sun, 4 Oct 2026 23:38:52 +0800 Subject: [PATCH 2/2] fix(connectors): resolve bundled example from the current runtime --- apps/web/scripts/probe-expert-conversation-entry.mjs | 1 + .../integration/connector-member-manager.test.mjs | 10 ++++++++++ packages/plugins/connectors/src/connection-api.ts | 4 ++-- packages/plugins/connectors/src/manager.ts | 11 ++++++++++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/web/scripts/probe-expert-conversation-entry.mjs b/apps/web/scripts/probe-expert-conversation-entry.mjs index 5e06a803e7..8f696a42cb 100644 --- a/apps/web/scripts/probe-expert-conversation-entry.mjs +++ b/apps/web/scripts/probe-expert-conversation-entry.mjs @@ -206,6 +206,7 @@ try { const comboResponse = page.waitForResponse(response => response.url().endsWith('/api/workdsh-experts') && response.request().postDataJSON()?.endpoint === 'create-execution'); await choice.click(); const comboCreation = (await (await comboResponse).json()).value; + await expect(picker).toHaveCount(0); await expect(editor).toContainText('组合任务草稿'); await expect(editor.locator('[data-composer-chip="workdsh-library"]')).toHaveCount(1); await expect(page.getByRole('button', { name: '连接器:浮层 MCP 验收', exact: true })).toBeVisible(); diff --git a/apps/web/tests/integration/connector-member-manager.test.mjs b/apps/web/tests/integration/connector-member-manager.test.mjs index 255b2d8b27..f87f2eea4b 100644 --- a/apps/web/tests/integration/connector-member-manager.test.mjs +++ b/apps/web/tests/integration/connector-member-manager.test.mjs @@ -10,6 +10,16 @@ import * as Domain from '@deepseek-ai/dsh-storage-domain'; import { AccessManager, SessionAccessBridge } from '../../../../packages/plugins/access/dist/index.js'; import { AuditJournal } from '../../../../packages/plugins/audit/dist/index.js'; import { ConnectorManager } from '../../../../packages/plugins/connectors/dist/index.js'; +import { connectorProcess } from '../../../../packages/plugins/connectors/dist/manager.js'; + +test('bundled example survives package relocation without rewriting independent MCP commands', async () => { + const stale = {id:'workdsh-example',serverName:'workdsh-example',transport:'stdio',command:'/removed/old-package/node',args:['/removed/old-package/example-server.mjs']}; + const resolved=connectorProcess(stale); + assert.equal(resolved.command,process.execPath); + const {access}=await import('node:fs/promises');await access(resolved.args[0]); + assert.deepEqual(connectorProcess({...stale,id:'custom-example'}),{command:stale.command,args:stale.args}); + assert.deepEqual(connectorProcess({...stale,args:['/custom/company-server.mjs']}),{command:stale.command,args:['/custom/company-server.mjs']}); +}); test('fixed-member connectors authorize native empty Sessions and deny foreign or revoked selections',async()=>{ const root=await mkdtemp(join(tmpdir(),'member-connectors-'));const ctx=new Context();let foreignTouches=0;let active=true; diff --git a/packages/plugins/connectors/src/connection-api.ts b/packages/plugins/connectors/src/connection-api.ts index a31acc2346..71e1289c3b 100644 --- a/packages/plugins/connectors/src/connection-api.ts +++ b/packages/plugins/connectors/src/connection-api.ts @@ -34,10 +34,10 @@ export function registerConnectorManagementConnection(ctx: Context): void { } return Response.json(fail('connector/invalid-request', '连接器管理请求无效。'), { status: 400 }); } catch (cause) { - const known = new Set(['connector/not-found', 'connector/command-required', 'connector/url-required', 'connector/server-name-conflict']); + const known = new Set(['connector/not-found', 'connector/not-ready', 'connector/command-required', 'connector/url-required', 'connector/server-name-conflict']); const code = cause instanceof Error && known.has(cause.message) ? cause.message : 'connector/internal'; const messages: Record = { 'connector/not-found': '未找到该连接器。', 'connector/command-required': 'stdio 连接需要填写启动命令。', 'connector/url-required': 'HTTP 连接需要填写 MCP URL。', 'connector/server-name-conflict': '服务标识已被其他 MCP 使用。' }; - const message = messages[code] ?? '连接器操作失败,请重试。'; + const message = code === 'connector/not-ready' ? '所选 MCP 尚未连接,请在连接器中检查运行状态后重试。' : messages[code] ?? '连接器操作失败,请重试。'; return Response.json(fail(code, message)); } }, diff --git a/packages/plugins/connectors/src/manager.ts b/packages/plugins/connectors/src/manager.ts index 1bdd847f5c..1eff5cc929 100644 --- a/packages/plugins/connectors/src/manager.ts +++ b/packages/plugins/connectors/src/manager.ts @@ -11,6 +11,15 @@ import { connectorDefinitionSchema, connectorSelectionsDomainSpec, connectorsDom declare module '@deepseek-ai/cordis' { interface Context { workdshConnectors: ConnectorManagementService; } } const serverPath = fileURLToPath(new URL('./example-server.mjs', import.meta.url)); +/** The bundled example belongs to the running package, not its installation path. */ +export function connectorProcess(definition: Pick) { + const bundledExample = definition.id === 'workdsh-example' && definition.serverName === 'workdsh-example' + && definition.transport === 'stdio' && !definition.authorizationCredentialRef + && definition.args?.length === 1 && /(?:^|[\\/])example-server\.mjs$/.test(definition.args[0]!) + && /(?:^|[\\/])node(?:\.exe)?$/.test(definition.command ?? ''); + return bundledExample ? { command: process.execPath, args: [serverPath] } + : { command: definition.command!, args: [...(definition.args ?? [])] }; +} type ChildFiber = { dispose(): Promise }; type Runtime = { child?: ChildFiber; state: ConnectorState; diagnostic?: string }; type JsonRecord = Record; @@ -158,7 +167,7 @@ export class ConnectorManager extends Service implements ConnectorManagementServ } const headers: Record = authorization ? { Authorization: authorization.value } : {}; const transportConfig = definition.transport === 'stdio' - ? { serverName: definition.serverName, transport: 'stdio' as const, command: definition.command!, args: [...(definition.args ?? [])], env: {} } + ? { serverName: definition.serverName, transport: 'stdio' as const, ...connectorProcess(definition), env: {} } : { serverName: definition.serverName, transport: 'streamable-http' as const, url: definition.url!, headers }; const fiber = this.ctx.plugin(McpClient, { ...transportConfig, failOnStartupError: true, toolCallTimeoutMs: 10_000, maxInstructionBytes: 8_192, reconnect: { enabled: true, initialDelayMs: 250, maxDelayMs: 5_000, maxAttempts: 4 } }) as ChildFiber & PromiseLike;