dsh-agy-cli 0.12.2 → 0.12.3

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/dist/client.js CHANGED
@@ -3846,7 +3846,7 @@ function apply(ctx) {
3846
3846
  observer.observe(row ?? (parent ?? root), { childList: true });
3847
3847
  return () => observer.disconnect();
3848
3848
  }, []);
3849
- const expanded = manualOpen ?? hostOpen ?? true;
3849
+ const expanded = manualOpen ?? hostOpen ?? false;
3850
3850
  const showToast = (text, type = 'info') => {
3851
3851
  setToast({ text, type, id: Date.now() });
3852
3852
  if (toastTimerRef.current !== null)
@@ -4193,20 +4193,20 @@ function apply(ctx) {
4193
4193
  style: { display: 'block', color: 'var(--agy-text-primary)', fontWeight: 600, fontSize: '12.5px', marginBottom: '6px', cursor: 'help' },
4194
4194
  title: t('models.hint'),
4195
4195
  }, t('models.label')), visibilityList));
4196
- return h('div', { ref: cardRef, style: S.container }, h('style', null, GLOBAL_CSS), h('div', { style: S.headerCard }, h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', minWidth: 0 } }, h('span', { style: { fontWeight: 700, fontSize: '13.5px', color: 'var(--agy-text-primary)' }, title: accountAlias }, accountAlias), h('span', { style: isAuthed ? S.badgeReady : S.badgeUnready }, isAuthed ? t('status.ready') : t('status.needsAuth')), tokenLine), h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexShrink: 0 } }, h('button', {
4196
+ return h('div', { ref: cardRef, style: S.container }, h('style', null, GLOBAL_CSS), h('div', { style: S.headerCard }, h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', minWidth: 0 } }, expanded ? h('span', { style: { fontWeight: 700, fontSize: '13.5px', color: 'var(--agy-text-primary)' }, title: accountAlias }, accountAlias) : null, h('span', { style: isAuthed ? S.badgeReady : S.badgeUnready }, isAuthed ? t('status.ready') : t('status.needsAuth')), expanded ? tokenLine : null), h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexShrink: 0 } }, h('button', {
4197
4197
  type: 'button',
4198
4198
  className: 'agy-btn',
4199
4199
  style: { ...S.btn, padding: '4px 7px', gap: '0', minWidth: '24px', justifyContent: 'center' },
4200
4200
  title: expanded ? t('panel.collapse') : t('panel.expand'),
4201
4201
  'aria-expanded': expanded ? 'true' : 'false',
4202
4202
  onClick: () => setManualOpen(!expanded),
4203
- }, expanded ? '▾' : '▸'), h('button', {
4203
+ }, expanded ? '▾' : '▸'), expanded ? h('button', {
4204
4204
  type: 'button',
4205
4205
  className: 'agy-btn',
4206
4206
  style: { ...S.btn, gap: '4px', flexShrink: 0 },
4207
4207
  disabled: isBusy,
4208
4208
  onClick: () => void refreshQuota(),
4209
- }, loadingAction === 'refresh:all' ? [renderSpinner(), t('header.refreshing')] : [uiIcon('refresh', 12, 'var(--agy-text-btn)'), ` ${t('header.refreshQuota')}`]))), renderToastBanner(), expanded
4209
+ }, loadingAction === 'refresh:all' ? [renderSpinner(), t('header.refreshing')] : [uiIcon('refresh', 12, 'var(--agy-text-btn)'), ` ${t('header.refreshQuota')}`]) : null)), renderToastBanner(), expanded
4210
4210
  ? h('div', { style: { display: 'contents' } }, expiredBanner, quotaCard, settingsBlock)
4211
4211
  : null);
4212
4212
  };
package/lib/index.js CHANGED
@@ -229,6 +229,7 @@ export function apply(ctx, config) {
229
229
  // controller bundle finishes mounting, and a value captured here
230
230
  // stays undefined for the life of the host.
231
231
  sessionControllerLookup: () => ctx.get('sessionController'),
232
+ agentsLookup: () => ctx.get('agents'),
232
233
  ...((activeConfig.mcpToolAllowlist ?? []).length > 0 ? { toolAllowlist: activeConfig.mcpToolAllowlist } : {}),
233
234
  serverVersion: '0.0.0',
234
235
  logger: message => {
@@ -38,6 +38,10 @@ export interface DshMcpBridgeOptions {
38
38
  * agent plane). Resolving per turn removes the load-order dependency.
39
39
  */
40
40
  readonly sessionControllerLookup?: (() => DshSessionControllerLike | undefined) | undefined;
41
+ /** Direct Cordis agent registry lookup, resolving child/subagent sessions. */
42
+ readonly agentsLookup?: (() => {
43
+ get(sessionId: unknown): unknown;
44
+ } | undefined) | undefined;
41
45
  /** Optional allowlist of DSH tool names exposed to AGY. */
42
46
  readonly toolAllowlist?: readonly string[] | undefined;
43
47
  readonly serverVersion: string;
package/lib/mcp/bridge.js CHANGED
@@ -174,14 +174,29 @@ async function agentOf(options, sessionHandle) {
174
174
  }
175
175
  if (sessionHandle.length === 0)
176
176
  return undefined;
177
- const resolved = await controller.resolveAgent(sessionHandle).catch((error) => {
178
- const rejection = subagentRejectionIn(error);
179
- options.logger?.(rejection === undefined
180
- ? `dsh mcp bridge: resolveAgent("${sessionHandle}") threw ${String(error)}`
181
- : `dsh mcp bridge: session "${sessionHandle}" is owned by subagent routing (${rejection}); the host refuses an agent handle for a child session by design, so this turn sees the global tool view`);
182
- return undefined;
183
- });
184
- const agent = agentIn(resolved);
177
+ let resolved;
178
+ if (controller !== undefined) {
179
+ resolved = await controller.resolveAgent(sessionHandle).catch((error) => {
180
+ const rejection = subagentRejectionIn(error);
181
+ options.logger?.(rejection === undefined
182
+ ? `dsh mcp bridge: resolveAgent("${sessionHandle}") threw ${String(error)}`
183
+ : `dsh mcp bridge: session "${sessionHandle}" is owned by subagent routing (${rejection}); attempting direct agents lookup`);
184
+ return undefined;
185
+ });
186
+ }
187
+ let agent = agentIn(resolved);
188
+ if (agent === undefined && options.agentsLookup !== undefined) {
189
+ try {
190
+ const direct = options.agentsLookup()?.get(sessionHandle);
191
+ if (direct !== undefined) {
192
+ agent = direct;
193
+ options.logger?.(`dsh mcp bridge: resolved agent for session "${sessionHandle}" via direct agents registry`);
194
+ }
195
+ }
196
+ catch (err) {
197
+ options.logger?.(`dsh mcp bridge: direct agents lookup threw ${String(err)}`);
198
+ }
199
+ }
185
200
  if (agent === undefined) {
186
201
  options.logger?.(`dsh mcp bridge: no agent resolved for session "${sessionHandle}"`);
187
202
  }
@@ -107,7 +107,7 @@ function expectedDescriptionOf(expected) {
107
107
  }
108
108
  export function projectDshTools(registry, options = {}) {
109
109
  const excluded = [];
110
- const maxTools = options.maxTools ?? TOOL_PROTOCOL_LIMITS.maxTools;
110
+ const maxTools = options.maxTools ?? Number.POSITIVE_INFINITY;
111
111
  if (registry === undefined || typeof registry.schemas !== 'function') {
112
112
  return { tools: [], excluded };
113
113
  }
@@ -911,8 +911,14 @@ export class AgyAdapter extends LlmAdapter {
911
911
  this.resolveToolBridge = dependencies.resolveToolBridge;
912
912
  this.mcpBridgeEnabled = config.mcpBridge !== false;
913
913
  this.agyPath = config.agyPath?.trim() === '' ? undefined : config.agyPath?.trim();
914
- this.timeoutMs = config.timeoutMs ?? 900_000;
915
- this.idleTimeoutMs = config.idleTimeoutMs ?? 300_000;
914
+ const envTimeout = process.env.DSH_AGY_TIMEOUT_MS ? Number(process.env.DSH_AGY_TIMEOUT_MS) : undefined;
915
+ this.timeoutMs = envTimeout !== undefined && Number.isFinite(envTimeout) && envTimeout > 0
916
+ ? envTimeout
917
+ : (config.timeoutMs ?? 600_000);
918
+ const envIdleTimeout = process.env.DSH_AGY_IDLE_TIMEOUT_MS ? Number(process.env.DSH_AGY_IDLE_TIMEOUT_MS) : undefined;
919
+ this.idleTimeoutMs = envIdleTimeout !== undefined && Number.isFinite(envIdleTimeout) && envIdleTimeout > 0
920
+ ? envIdleTimeout
921
+ : (config.idleTimeoutMs ?? 60_000);
916
922
  this.stdinMode = config.stdinMode ?? 'hold';
917
923
  this.run = dependencies.runAgyProcess ?? runAgyProcess;
918
924
  this.sessionMode = config.sessionMode === 'resume' ? 'resume' : 'full';
@@ -1288,6 +1294,16 @@ export class AgyAdapter extends LlmAdapter {
1288
1294
  }
1289
1295
  await persistentRequest;
1290
1296
  }
1297
+ catch (error) {
1298
+ if (modelText.trim().length > 0) {
1299
+ telemetry.partialAnswerDelivered = true;
1300
+ lastResultError = error instanceof Error ? error.message : String(error);
1301
+ finalStatus = "ERROR";
1302
+ }
1303
+ else {
1304
+ throw error;
1305
+ }
1306
+ }
1291
1307
  finally {
1292
1308
  queue.end();
1293
1309
  persistentRequest?.catch(() => undefined);
@@ -1355,9 +1371,8 @@ export class AgyAdapter extends LlmAdapter {
1355
1371
  // text already streamed, keep that text and park AGY's reason as a
1356
1372
  // reasoning annotation; otherwise fail the turn exactly like one-shot —
1357
1373
  // delivering the placeholder as the answer is worse than either.
1358
- const streamedPartialAnswer = finalStatus !== undefined
1359
- && !isSuccessStatus(finalStatus)
1360
- && modelText.trim().length > 0;
1374
+ const streamedPartialAnswer = ((finalStatus !== undefined && !isSuccessStatus(finalStatus))
1375
+ || lastResultError !== undefined) && modelText.trim().length > 0;
1361
1376
  if (finalStatus !== undefined && !isSuccessStatus(finalStatus) && !streamedPartialAnswer) {
1362
1377
  const detail = [finalStatus, finalErrorDetail].filter(value => value !== undefined).join(' ');
1363
1378
  const code = classifyAgyFailure(detail, 'AGY_STATUS');
@@ -2013,7 +2028,11 @@ export class AgyAdapter extends LlmAdapter {
2013
2028
  : result === undefined
2014
2029
  ? new LlmError('AGY process did not return a result', 'AGY_PROCESS')
2015
2030
  : processFailure(result);
2016
- if (failure !== undefined)
2031
+ const isFailureOrCutShort = failure !== undefined || (resultSeen && !isSuccessStatus(finalStatus));
2032
+ const streamedPartialAnswer = isFailureOrCutShort && modelText.trim().length > 0;
2033
+ if (streamedPartialAnswer)
2034
+ telemetry.partialAnswerDelivered = true;
2035
+ if (failure !== undefined && !streamedPartialAnswer)
2017
2036
  throw failure;
2018
2037
  // AGY reports `status: ERROR` on a turn it cut short, and it can carry a
2019
2038
  // placeholder under `response` ("(no content)") instead of the answer. When
@@ -2025,9 +2044,6 @@ export class AgyAdapter extends LlmAdapter {
2025
2044
  // 1508 and the result branch at src/host/mapper.ts:400-428); the difference
2026
2045
  // is that a placeholder response is not deliverable content, so the streamed
2027
2046
  // text decides whether anything survives.
2028
- const streamedPartialAnswer = resultSeen && !isSuccessStatus(finalStatus) && modelText.trim().length > 0;
2029
- if (streamedPartialAnswer)
2030
- telemetry.partialAnswerDelivered = true;
2031
2047
  if (resultSeen && !isSuccessStatus(finalStatus) && !streamedPartialAnswer) {
2032
2048
  const detail = [finalStatus, finalErrorDetail].filter(value => value !== undefined).join(' ');
2033
2049
  const code = classifyAgyFailure(detail, 'AGY_STATUS');
@@ -2119,7 +2135,7 @@ export class AgyAdapter extends LlmAdapter {
2119
2135
  // AGY's own reason for a cut-short turn belongs in the same reasoning block
2120
2136
  // as the bridged tool steps, so the transcript gets one annotation, not two.
2121
2137
  if (streamedPartialAnswer) {
2122
- const reason = (lastResultError ?? finalErrorDetail ?? 'AGY reported status ERROR').trim();
2138
+ const reason = (lastResultError ?? finalErrorDetail ?? failure?.message ?? 'AGY reported status ERROR').trim();
2123
2139
  const line = `[agy finished with error] ${reason}\n\n`;
2124
2140
  if (!mcpStepsOpen) {
2125
2141
  mcpStepsOpen = true;
@@ -6,6 +6,35 @@ export class AgyPromptError extends Error {
6
6
  this.name = 'AgyPromptError';
7
7
  }
8
8
  }
9
+ function imageBlockText(block) {
10
+ const att = block.attachment;
11
+ const meta = [];
12
+ if (att?.name)
13
+ meta.push(`name=${att.name}`);
14
+ if (att?.mediaType)
15
+ meta.push(`media_type=${att.mediaType}`);
16
+ if (att?.width !== undefined && att?.height !== undefined) {
17
+ meta.push(`width=${att.width} height=${att.height}`);
18
+ }
19
+ if (att?.bytes !== undefined)
20
+ meta.push(`bytes=${att.bytes}`);
21
+ if (att?.attachmentId !== undefined)
22
+ meta.push(`id=${att.attachmentId}`);
23
+ const metaStr = meta.length > 0 ? ` ${meta.join(' ')}` : '';
24
+ return `[IMAGE_ATTACHMENT${metaStr}]`;
25
+ }
26
+ function fileBlockText(block) {
27
+ const att = block.attachment;
28
+ const meta = [];
29
+ if (att?.name)
30
+ meta.push(`name=${att.name}`);
31
+ if (att?.bytes !== undefined)
32
+ meta.push(`bytes=${att.bytes}`);
33
+ if (att?.attachmentId !== undefined)
34
+ meta.push(`id=${att.attachmentId}`);
35
+ const metaStr = meta.length > 0 ? ` ${meta.join(' ')}` : '';
36
+ return `[FILE_ATTACHMENT${metaStr}]`;
37
+ }
9
38
  function blockText(block, messageIndex, blockIndex) {
10
39
  switch (block.type) {
11
40
  case 'text':
@@ -18,8 +47,18 @@ function blockText(block, messageIndex, blockIndex) {
18
47
  name: block.name,
19
48
  arguments: block.arguments,
20
49
  })}`;
21
- default:
22
- throw new AgyPromptError(`AGY text MVP does not support content block ${block.type} at message ${messageIndex}, block ${blockIndex}`, 'UNSUPPORTED_CONTENT');
50
+ case 'image':
51
+ return imageBlockText(block);
52
+ case 'file':
53
+ return fileBlockText(block);
54
+ case 'tool-addition':
55
+ return `[DSH TOOL ADDITION: ${block.toolName}]`;
56
+ case 'tool-removal':
57
+ return `[DSH TOOL REMOVAL: ${block.toolName}]`;
58
+ default: {
59
+ const unknownBlock = block;
60
+ throw new AgyPromptError(`AGY text MVP does not support content block ${unknownBlock.type} at message ${messageIndex}, block ${blockIndex}`, 'UNSUPPORTED_CONTENT');
61
+ }
23
62
  }
24
63
  }
25
64
  function blockValue(block, messageIndex, blockIndex) {
@@ -30,8 +69,35 @@ function blockValue(block, messageIndex, blockIndex) {
30
69
  return { type: 'reasoning', text: block.text };
31
70
  case 'tool-call':
32
71
  return { type: 'tool-call', id: block.id, name: block.name, arguments: block.arguments };
33
- default:
34
- throw new AgyPromptError(`AGY text MVP does not support content block ${block.type} at message ${messageIndex}, block ${blockIndex}`, 'UNSUPPORTED_CONTENT');
72
+ case 'image': {
73
+ const att = block.attachment;
74
+ return {
75
+ type: 'image',
76
+ ...(att?.attachmentId !== undefined ? { attachmentId: att.attachmentId } : {}),
77
+ ...(att?.mediaType !== undefined ? { mediaType: att.mediaType } : {}),
78
+ ...(att?.width !== undefined ? { width: att.width } : {}),
79
+ ...(att?.height !== undefined ? { height: att.height } : {}),
80
+ ...(att?.name !== undefined ? { name: att.name } : {}),
81
+ ...(att?.bytes !== undefined ? { bytes: att.bytes } : {}),
82
+ };
83
+ }
84
+ case 'file': {
85
+ const att = block.attachment;
86
+ return {
87
+ type: 'file',
88
+ ...(att?.attachmentId !== undefined ? { attachmentId: att.attachmentId } : {}),
89
+ ...(att?.name !== undefined ? { name: att.name } : {}),
90
+ ...(att?.bytes !== undefined ? { bytes: att.bytes } : {}),
91
+ };
92
+ }
93
+ case 'tool-addition':
94
+ return { type: 'tool-addition', toolName: block.toolName };
95
+ case 'tool-removal':
96
+ return { type: 'tool-removal', toolName: block.toolName };
97
+ default: {
98
+ const unknownBlock = block;
99
+ throw new AgyPromptError(`AGY text MVP does not support content block ${unknownBlock.type} at message ${messageIndex}, block ${blockIndex}`, 'UNSUPPORTED_CONTENT');
100
+ }
35
101
  }
36
102
  }
37
103
  function messageText(message, messageIndex) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agy-cli",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"