@adhdev/daemon-core 0.9.76-rc.61 → 0.9.76-rc.62

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.
@@ -4,7 +4,7 @@
4
4
  * Lifecycle layer on top of ProviderCliAdapter.
5
5
  * collectCliData() + status transition logic from daemon-status.ts moved here.
6
6
  */
7
- import { type ProviderModule } from './contracts.js';
7
+ import { type ProviderModule, type InputEnvelope } from './contracts.js';
8
8
  import type { ProviderInstance, ProviderState, InstanceContext, HotChatSessionState, SessionModalState } from './provider-instance.js';
9
9
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
10
10
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
@@ -16,6 +16,9 @@ type PersistableCliHistoryMessage = {
16
16
  senderName?: string;
17
17
  receivedAt?: number;
18
18
  };
19
+ export declare function buildCliStructuredInputPrompt(input: InputEnvelope, options?: {
20
+ materializeDir?: string;
21
+ }): string;
19
22
  export declare function buildIncrementalHistoryAppendMessages(previousMessages: PersistableCliHistoryMessage[], currentMessages: PersistableCliHistoryMessage[]): PersistableCliHistoryMessage[];
20
23
  export declare function getForcedNewSessionScriptName(provider: ProviderModule | undefined, launchMode: 'new' | 'resume' | 'manual'): string | null;
21
24
  export declare function waitForCliAdapterReady(adapter: {
@@ -87,7 +87,7 @@ export interface ProviderEffect {
87
87
  * ContentBlock — ACP ContentBlock union type
88
88
  * Represents displayable content in messages, tool call results, etc.
89
89
  */
90
- export type ContentBlock = TextBlock | ImageBlock | AudioBlock | ResourceLinkBlock | ResourceBlock;
90
+ export type ContentBlock = TextBlock | ImageBlock | AudioBlock | VideoBlock | ResourceLinkBlock | ResourceBlock;
91
91
  /** Text content — ACP TextContent */
92
92
  export interface TextBlock {
93
93
  type: 'text';
@@ -100,6 +100,7 @@ export interface ImageBlock {
100
100
  data: string;
101
101
  mimeType: string;
102
102
  uri?: string;
103
+ alt?: string;
103
104
  annotations?: ContentAnnotations;
104
105
  }
105
106
  /** Audio content — ACP AudioContent */
@@ -107,6 +108,18 @@ export interface AudioBlock {
107
108
  type: 'audio';
108
109
  data: string;
109
110
  mimeType: string;
111
+ uri?: string;
112
+ transcript?: string;
113
+ annotations?: ContentAnnotations;
114
+ }
115
+ /** Video content — ADHDev canonical display block. ACP prompt input degrades video to resource_link/text. */
116
+ export interface VideoBlock {
117
+ type: 'video';
118
+ data?: string;
119
+ mimeType: string;
120
+ uri?: string;
121
+ transcript?: string;
122
+ posterUri?: string;
110
123
  annotations?: ContentAnnotations;
111
124
  }
112
125
  /** Resource link (file reference) — ACP ResourceLink */
@@ -487,6 +500,12 @@ export interface ProviderModule {
487
500
  input?: {
488
501
  multipart?: boolean;
489
502
  mediaTypes?: Array<'text' | 'image' | 'audio' | 'video' | 'resource'>;
503
+ strategies?: Array<{
504
+ mediaType: 'text' | 'image' | 'audio' | 'video' | 'resource';
505
+ strategies?: Array<'native' | 'native_acp' | 'resource_link' | 'text_fallback' | 'paste' | 'upload'>;
506
+ native?: boolean;
507
+ degradation?: Array<'native' | 'native_acp' | 'resource_link' | 'text_fallback' | 'paste' | 'upload'>;
508
+ }>;
490
509
  };
491
510
  output?: {
492
511
  richContent?: boolean;
@@ -1,5 +1,5 @@
1
1
  import type { ContentAnnotations } from './contracts.js';
2
- export type InputPart = TextInputPart | ImageInputPart | AudioInputPart | VideoInputPart | ResourceInputPart;
2
+ export type InputPart = TextInputPart | ImageInputPart | AudioInputPart | VideoInputPart | ResourceLinkInputPart | ResourceInputPart;
3
3
  export interface TextInputPart {
4
4
  type: 'text';
5
5
  text: string;
@@ -23,6 +23,7 @@ export interface VideoInputPart {
23
23
  mimeType: string;
24
24
  uri?: string;
25
25
  data?: string;
26
+ transcript?: string;
26
27
  posterUri?: string;
27
28
  }
28
29
  export interface ResourceInputPart {
@@ -33,6 +34,16 @@ export interface ResourceInputPart {
33
34
  text?: string;
34
35
  data?: string;
35
36
  }
37
+ export interface ResourceLinkInputPart {
38
+ type: 'resource_link';
39
+ uri: string;
40
+ name: string;
41
+ title?: string;
42
+ description?: string;
43
+ mimeType?: string;
44
+ size?: number;
45
+ annotations?: ContentAnnotations;
46
+ }
36
47
  export interface InputEnvelope {
37
48
  parts: InputPart[];
38
49
  textFallback: string;
@@ -52,6 +63,7 @@ export interface ImageMessagePart {
52
63
  mimeType: string;
53
64
  uri?: string;
54
65
  data?: string;
66
+ alt?: string;
55
67
  annotations?: ContentAnnotations;
56
68
  }
57
69
  export interface AudioMessagePart {
@@ -67,6 +79,7 @@ export interface VideoMessagePart {
67
79
  mimeType: string;
68
80
  uri?: string;
69
81
  data?: string;
82
+ transcript?: string;
70
83
  posterUri?: string;
71
84
  annotations?: ContentAnnotations;
72
85
  }
@@ -74,8 +87,11 @@ export interface ResourceLinkMessagePart {
74
87
  type: 'resource_link';
75
88
  uri: string;
76
89
  name: string;
90
+ title?: string;
91
+ description?: string;
77
92
  mimeType?: string;
78
93
  size?: number;
94
+ annotations?: ContentAnnotations;
79
95
  }
80
96
  export interface ResourceMessagePart {
81
97
  type: 'resource';
@@ -1,9 +1,25 @@
1
1
  import type { InputEnvelope, ProviderModule } from './contracts.js';
2
- type InputMediaType = 'text' | 'image' | 'audio' | 'video' | 'resource';
2
+ export type InputMediaType = 'text' | 'image' | 'audio' | 'video' | 'resource';
3
+ export type InputAttachmentStrategy = 'native' | 'native_acp' | 'resource_link' | 'text_fallback' | 'paste' | 'upload';
4
+ export interface InputMediaStrategyDescriptor {
5
+ mediaType: InputMediaType;
6
+ strategies: InputAttachmentStrategy[];
7
+ native?: boolean;
8
+ degradation?: InputAttachmentStrategy[];
9
+ }
10
+ export interface MessageInputSupport {
11
+ text: boolean;
12
+ multipart: boolean;
13
+ mediaTypes: InputMediaType[];
14
+ strategies: InputMediaStrategyDescriptor[];
15
+ }
16
+ export declare const TEXT_ONLY_MESSAGE_INPUT_SUPPORT: MessageInputSupport;
3
17
  export declare function assertTextOnlyInput(provider: Pick<ProviderModule, 'name' | 'type'> | null | undefined, input: InputEnvelope): void;
4
18
  export declare function getDeclaredProviderInputSupport(provider?: Pick<ProviderModule, 'capabilities'> | null): {
5
19
  multipart: boolean;
6
20
  mediaTypes: Set<InputMediaType>;
21
+ strategies: InputMediaStrategyDescriptor[];
7
22
  };
23
+ export declare function normalizeInputStrategyDescriptors(raw: unknown): InputMediaStrategyDescriptor[];
24
+ export declare function getEffectiveMessageInputSupport(provider?: Pick<ProviderModule, 'category' | 'capabilities'> | null, runtimeCapabilities?: Record<string, any> | null): MessageInputSupport;
8
25
  export declare function assertProviderSupportsDeclaredInput(provider: Pick<ProviderModule, 'name' | 'type' | 'capabilities'> | null | undefined, input: InputEnvelope): void;
9
- export {};
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { ProviderModule, ProviderResumeCapability } from './contracts.js';
11
11
  import type { AcpConfigOption, AcpMode, ProviderControlSchema, ProviderSummaryMetadata, SessionCapability } from '../shared-types.js';
12
+ import type { MessageInputSupport } from './provider-input-support.js';
12
13
  import type { ChatMessage } from '../types.js';
13
14
  export type ProviderStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
14
15
  export interface ProviderRuntimeWriteOwner {
@@ -71,6 +72,7 @@ interface ProviderStateBase {
71
72
  runtime?: ProviderRuntimeInfo;
72
73
  resume?: ProviderResumeCapability;
73
74
  sessionCapabilities?: SessionCapability[];
75
+ messageInput?: MessageInputSupport;
74
76
  /** Dynamic control current values */
75
77
  controlValues?: Record<string, string | number | boolean>;
76
78
  /** Provider-declared controls schema (from provider.controls) */
@@ -240,7 +240,9 @@ export type SessionTransport = 'cdp-page' | 'cdp-webview' | 'pty' | 'acp';
240
240
  export type SessionKind = 'workspace' | 'agent';
241
241
  export type SessionCapability = 'read_chat' | 'send_message' | 'new_session' | 'list_sessions' | 'switch_session' | 'resolve_action' | 'open_panel' | 'terminal_io' | 'resize_terminal' | 'change_model' | 'set_mode' | 'set_thought_level' | 'delete_notification' | 'mark_notification_unread';
242
242
  import type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
243
+ import type { MessageInputSupport } from './providers/provider-input-support.js';
243
244
  export type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
245
+ export type { MessageInputSupport, InputMediaStrategyDescriptor, InputAttachmentStrategy, InputMediaType } from './providers/provider-input-support.js';
244
246
  export interface SessionEntry {
245
247
  id: string;
246
248
  parentId: string | null;
@@ -267,6 +269,8 @@ export interface SessionEntry {
267
269
  resume?: ProviderResumeCapability;
268
270
  activeChat: SessionActiveChatData | null;
269
271
  capabilities?: SessionCapability[];
272
+ /** Effective message input/media support for this session. Defaults fail-closed to text-only. */
273
+ messageInput?: MessageInputSupport;
270
274
  cdpConnected?: boolean;
271
275
  /** Dynamic control current values (generic key-value) */
272
276
  controlValues?: Record<string, string | number | boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.76-rc.61",
3
+ "version": "0.9.76-rc.62",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,7 +16,7 @@ import type { ProviderInstance } from '../providers/provider-instance.js';
16
16
  import { readProviderChatHistory } from '../config/chat-history.js';
17
17
  import { LOG, getRecentLogs } from '../logging/logger.js';
18
18
  import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
19
- import { buildChatMessageSignature } from '../chat/chat-signatures.js';
19
+ import { buildChatMessageSignature, hashSignatureParts } from '../chat/chat-signatures.js';
20
20
  import type { ChatMessage } from '../types.js';
21
21
  import type { SessionTransport } from '../shared-types.js';
22
22
  import { filterUserFacingChatMessages, normalizeChatMessages } from '../providers/chat-message-normalization.js';
@@ -93,10 +93,32 @@ function buildRecentSendKey(h: CommandHelpers, args: any, provider: ProviderModu
93
93
  return `${transport}:${target}:${signature.trim()}`;
94
94
  }
95
95
 
96
- function buildSendInputSignature(input: InputEnvelope): string {
96
+ function summarizeSendInputPart(part: any): string {
97
+ if (!part || typeof part !== 'object') return String(part ?? '');
98
+ if (part.type === 'text') return `text:${String(part.text || '').trim()}`;
99
+ const fields = [
100
+ `type=${String(part.type || '')}`,
101
+ `mime=${String(part.mimeType || '')}`,
102
+ `uri=${String(part.uri || '')}`,
103
+ `name=${String(part.name || '')}`,
104
+ ];
105
+ const data = typeof part.data === 'string'
106
+ ? part.data
107
+ : typeof part.resource?.blob === 'string'
108
+ ? part.resource.blob
109
+ : '';
110
+ if (data) fields.push(`dataLen=${data.length}`, `dataHash=${hashSignatureParts([data]).slice(0, 12)}`);
111
+ const textish = [part.alt, part.transcript, part.description, part.title, part.resource?.uri]
112
+ .filter((value) => typeof value === 'string' && value.trim())
113
+ .join('\u001f');
114
+ if (textish) fields.push(`meta=${hashSignatureParts([textish]).slice(0, 12)}`);
115
+ return fields.join(';');
116
+ }
117
+
118
+ export function buildSendInputSignature(input: InputEnvelope): string {
97
119
  const text = typeof input.textFallback === 'string' ? input.textFallback.trim() : '';
98
- if (text) return text;
99
- return JSON.stringify(input.parts || []);
120
+ const partSummaries = (input.parts || []).map(summarizeSendInputPart);
121
+ return hashSignatureParts([text, ...partSummaries]);
100
122
  }
101
123
 
102
124
  function getSendChatInputEnvelope(args: any): InputEnvelope {
@@ -1140,12 +1162,25 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
1140
1162
  }
1141
1163
  }
1142
1164
 
1143
- // PTY transport: text-only send via adapter
1165
+ // PTY transport: route structured input through the provider instance so
1166
+ // provider-specific CLI attachment strategies (for example Hermes file-path
1167
+ // image prompts) are applied instead of collapsing everything to text.
1144
1168
  if (transport === 'pty') {
1145
1169
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
1146
1170
  if (adapter) {
1147
1171
  _log(`${transport} adapter: ${adapter.cliType}`);
1148
1172
  try {
1173
+ const hasStructuredParts = input.parts.some((part) => part.type !== 'text');
1174
+ if (hasStructuredParts) {
1175
+ const target = getTargetInstance(h, args);
1176
+ if (!target || target.category !== 'cli') {
1177
+ return { success: false, error: `CLI instance not found for ${provider?.type || args?.agentType || 'unknown'}` };
1178
+ }
1179
+ assertProviderSupportsDeclaredInput(provider, input);
1180
+ await waitOnceForFreshHermesCliStart(adapter, _log);
1181
+ target.onEvent('send_message', { input });
1182
+ return _logSendSuccess(`${transport}-instance`, target.type);
1183
+ }
1149
1184
  assertTextOnlyInput(provider, input);
1150
1185
  if (!text) return { success: false, error: 'text required for PTY send' };
1151
1186
  await waitOnceForFreshHermesCliStart(adapter, _log);
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
2
3
  import { existsSync, readdirSync, realpathSync } from 'node:fs'
3
4
  import { createRequire } from 'node:module'
4
5
  import * as os from 'node:os'
@@ -64,7 +65,7 @@ function resolveHermesMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupO
64
65
  reason: 'Could not resolve the ADHDev MCP server entrypoint and a Node runtime with WebSocket support for daemon IPC mode',
65
66
  }
66
67
  }
67
- const configPath = resolveMcpConfigPath(HERMES_MCP_CONFIG_PATH, options.workspace)
68
+ const configPath = join(resolveHermesCoordinatorHome(options.meshId, options.workspace), 'config.yaml')
68
69
  if (!configPath.trim()) {
69
70
  return createHermesManualMeshCoordinatorSetup(options.meshId, options.workspace)
70
71
  }
@@ -177,6 +178,12 @@ function renderMeshCoordinatorTemplate(template: string, values: Record<string,
177
178
  return template.replace(/\{\{\s*(meshId|workspace|serverName|adhdevMcpCommand)\s*\}\}/g, (_, key: string) => values[key] || '')
178
179
  }
179
180
 
181
+ function resolveHermesCoordinatorHome(meshId: string, workspace: string): string {
182
+ const key = `${meshId || 'mesh'}\n${resolve(workspace || os.tmpdir())}`
183
+ const hash = createHash('sha256').update(key).digest('hex').slice(0, 16)
184
+ return join(os.tmpdir(), `adhdev-hermes-mesh-coordinator-${hash}`)
185
+ }
186
+
180
187
  function resolveMcpConfigPath(configPath: string, workspace: string): string {
181
188
  const trimmed = configPath.trim()
182
189
  if (trimmed === '~') return os.homedir()
@@ -1672,6 +1672,10 @@ export class DaemonCommandRouter {
1672
1672
 
1673
1673
  const cliArgs: string[] = [];
1674
1674
  const launchEnv: Record<string, string> = {};
1675
+ if (configFormat === 'hermes_config_yaml') {
1676
+ launchEnv.HERMES_HOME = dirname(mcpConfigPath);
1677
+ launchEnv.HERMES_IGNORE_USER_CONFIG = '';
1678
+ }
1675
1679
  if (systemPrompt) {
1676
1680
  if (configFormat === 'hermes_config_yaml') {
1677
1681
  launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
@@ -52,6 +52,9 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
52
52
  // ── Tools ──
53
53
  sections.push(TOOLS_SECTION);
54
54
 
55
+ // ── Tool Exposure Preflight ──
56
+ sections.push(TOOL_EXPOSURE_PREFLIGHT_SECTION);
57
+
55
58
  // ── Workflow ──
56
59
  sections.push(WORKFLOW_SECTION);
57
60
 
@@ -136,6 +139,10 @@ const TOOLS_SECTION = `## Available Tools
136
139
  | \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
137
140
  | \`mesh_remove_node\` | Remove a node (cleans up worktree if applicable) |`;
138
141
 
142
+ const TOOL_EXPOSURE_PREFLIGHT_SECTION = `## Tool Exposure Preflight
143
+
144
+ Before doing any coordinator work, confirm that the actual callable tool list includes \`mesh_status\` and the other \`mesh_*\` tools from the table above. If this Repo Mesh coordinator prompt is present but the callable \`mesh_*\` tools are missing, the MCP server/tool manifest is stale or not injected yet. Do not substitute terminal/file/git tools, do not inspect or edit the repository directly, and do not continue as a non-mesh local coding agent. Stop immediately and tell the user to run \`/reload-mcp\` or start a fresh coordinator session so ADHDev can reconnect \`adhdev-mesh\`.`;
145
+
139
146
  const WORKFLOW_SECTION = `## Orchestration Workflow
140
147
 
141
148
  1. **Assess** — Call \`mesh_status\` to see which nodes are healthy and available.
@@ -48,7 +48,7 @@ import {
48
48
  } from '@agentclientprotocol/sdk';
49
49
  import type { ProviderModule, ContentBlock, InputEnvelope, ToolCallInfo, ToolCallContent as TCC, ToolKind, ToolCallStatus as TCS } from './contracts.js';
50
50
  import { normalizeContent, flattenContent, normalizeInputEnvelope } from './contracts.js';
51
- import { assertProviderSupportsDeclaredInput } from './provider-input-support.js';
51
+ import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport } from './provider-input-support.js';
52
52
  import type { ProviderInstance, ProviderState, AcpProviderState, ProviderErrorReason, ProviderEvent, InstanceContext, SessionModalState } from './provider-instance.js';
53
53
  import { StatusMonitor } from './status-monitor.js';
54
54
  import { buildLegacyModelModeSummaryMetadata } from './summary-metadata.js';
@@ -121,6 +121,41 @@ function appendPromptText(promptParts: ContentBlock[], text: string | undefined)
121
121
  promptParts.push({ type: 'text', text: normalized });
122
122
  }
123
123
 
124
+ function getUriDisplayName(uri: string | undefined, fallback: string): string {
125
+ if (!uri) return fallback;
126
+ try {
127
+ const pathname = uri.startsWith('file://') ? new URL(uri).pathname : uri;
128
+ return pathname.split(/[\\/]/).filter(Boolean).pop() || fallback;
129
+ } catch {
130
+ return uri.split(/[\\/]/).filter(Boolean).pop() || fallback;
131
+ }
132
+ }
133
+
134
+ function appendResourceLink(
135
+ promptParts: ContentBlock[],
136
+ uri: string,
137
+ fallbackName: string,
138
+ mimeType?: string,
139
+ description?: string,
140
+ metadata?: Pick<Extract<ContentBlock, { type: 'resource_link' }>, 'title' | 'size' | 'annotations'> & { name?: string },
141
+ ): void {
142
+ promptParts.push({
143
+ type: 'resource_link',
144
+ uri,
145
+ name: metadata?.name || getUriDisplayName(uri, fallbackName),
146
+ ...(metadata?.title ? { title: metadata.title } : {}),
147
+ ...(mimeType ? { mimeType } : {}),
148
+ ...(description ? { description } : {}),
149
+ ...(typeof metadata?.size === 'number' ? { size: metadata.size } : {}),
150
+ ...(metadata?.annotations ? { annotations: metadata.annotations } : {}),
151
+ });
152
+ }
153
+
154
+ function appendMediaFallbackText(promptParts: ContentBlock[], label: string, details: Array<string | undefined>): void {
155
+ const normalizedDetails = details.map((value) => typeof value === 'string' ? value.trim() : '').filter(Boolean);
156
+ appendPromptText(promptParts, `[${[label, ...normalizedDetails].join(': ')}]`);
157
+ }
158
+
124
159
  export function buildAcpPromptParts(input: InputEnvelope, agentCapabilities?: Record<string, any>): ContentBlock[] {
125
160
  const caps = getPromptCapabilityFlags(agentCapabilities);
126
161
  const promptParts: ContentBlock[] = [];
@@ -132,59 +167,82 @@ export function buildAcpPromptParts(input: InputEnvelope, agentCapabilities?: Re
132
167
  }
133
168
 
134
169
  if (part.type === 'image') {
135
- if (!caps.image) {
136
- throw new Error('ACP agent does not support input type: image');
137
- }
138
- if (!part.data) {
139
- throw new Error('ACP image input requires inline image data');
170
+ if (caps.image && part.data) {
171
+ promptParts.push({
172
+ type: 'image',
173
+ data: part.data,
174
+ mimeType: part.mimeType,
175
+ ...(part.uri ? { uri: part.uri } : {}),
176
+ ...(part.alt ? { alt: part.alt } : {}),
177
+ });
178
+ if (part.alt) appendPromptText(promptParts, part.alt);
179
+ } else if (part.uri) {
180
+ appendResourceLink(promptParts, part.uri, 'image', part.mimeType, part.alt);
181
+ if (part.alt) appendPromptText(promptParts, part.alt);
182
+ } else {
183
+ appendMediaFallbackText(promptParts, 'Image attachment', [part.alt, part.mimeType]);
140
184
  }
141
- promptParts.push({
142
- type: 'image',
143
- data: part.data,
144
- mimeType: part.mimeType,
145
- ...(part.uri ? { uri: part.uri } : {}),
146
- });
147
185
  continue;
148
186
  }
149
187
 
150
188
  if (part.type === 'audio') {
151
- if (!caps.audio) {
152
- throw new Error('ACP agent does not support input type: audio');
153
- }
154
- if (!part.data) {
155
- throw new Error('ACP audio input requires inline audio data');
189
+ if (caps.audio && part.data) {
190
+ promptParts.push({
191
+ type: 'audio',
192
+ data: part.data,
193
+ mimeType: part.mimeType,
194
+ ...(part.uri ? { uri: part.uri } : {}),
195
+ ...(part.transcript ? { transcript: part.transcript } : {}),
196
+ });
197
+ if (part.transcript) appendPromptText(promptParts, part.transcript);
198
+ } else if (part.uri) {
199
+ appendResourceLink(promptParts, part.uri, 'audio', part.mimeType, part.transcript);
200
+ if (part.transcript) appendPromptText(promptParts, part.transcript);
201
+ } else {
202
+ appendMediaFallbackText(promptParts, 'Audio attachment', [part.transcript, part.mimeType]);
156
203
  }
157
- promptParts.push({
158
- type: 'audio',
159
- data: part.data,
160
- mimeType: part.mimeType,
161
- });
162
204
  continue;
163
205
  }
164
206
 
165
207
  if (part.type === 'resource') {
166
- if (!caps.embeddedContext) {
167
- throw new Error('ACP agent does not support input type: resource');
168
- }
169
- if (part.text) {
208
+ if (caps.embeddedContext && part.text) {
170
209
  promptParts.push({
171
210
  type: 'resource',
172
211
  resource: { uri: part.uri, text: part.text, mimeType: part.mimeType ?? null },
173
212
  });
174
213
  continue;
175
214
  }
176
- if (part.data) {
215
+ if (caps.embeddedContext && part.data) {
177
216
  promptParts.push({
178
217
  type: 'resource',
179
218
  resource: { uri: part.uri, blob: part.data, mimeType: part.mimeType ?? null },
180
219
  });
181
220
  continue;
182
221
  }
183
- throw new Error('ACP resource input requires embedded text or binary data');
222
+ appendResourceLink(promptParts, part.uri, part.name || 'resource', part.mimeType, part.text);
223
+ if (part.text) appendPromptText(promptParts, part.text);
224
+ continue;
225
+ }
226
+
227
+ if (part.type === 'resource_link') {
228
+ appendResourceLink(promptParts, part.uri, part.name, part.mimeType, part.description, {
229
+ name: part.name,
230
+ ...(part.title ? { title: part.title } : {}),
231
+ ...(typeof part.size === 'number' ? { size: part.size } : {}),
232
+ ...(part.annotations ? { annotations: part.annotations } : {}),
233
+ });
234
+ continue;
184
235
  }
185
236
 
186
237
  if (part.type === 'video') {
187
- throw new Error('ACP agent does not support input type: video');
238
+ // ACP v0.16 prompt capabilities do not advertise native video input. Preserve meaning by
239
+ // sending a linked resource when possible, plus transcript/descriptive text when present.
240
+ if (part.uri) {
241
+ appendResourceLink(promptParts, part.uri, 'video', part.mimeType, part.transcript);
242
+ if (part.transcript) appendPromptText(promptParts, part.transcript);
243
+ } else {
244
+ appendMediaFallbackText(promptParts, 'Video attachment', [part.transcript, part.mimeType]);
245
+ }
188
246
  }
189
247
  }
190
248
 
@@ -346,6 +404,7 @@ export class AcpProviderInstance implements ProviderInstance {
346
404
  lastUpdated: Date.now(),
347
405
  settings: this.settings,
348
406
  pendingEvents: this.flushEvents(),
407
+ messageInput: getEffectiveMessageInputSupport(this.provider, this.agentCapabilities),
349
408
  // ACP-specific: expose available models/modes for dashboard
350
409
  acpConfigOptions: this.configOptions,
351
410
  acpModes: this.availableModes,
@@ -962,6 +1021,7 @@ export class AcpProviderInstance implements ProviderInstance {
962
1021
  data: b.data,
963
1022
  mimeType: b.mimeType,
964
1023
  ...(b.uri ? { uri: b.uri } : {}),
1024
+ ...(b.alt ? { alt: b.alt } : {}),
965
1025
  };
966
1026
  }
967
1027
  if (b.type === 'audio') {
@@ -969,14 +1029,31 @@ export class AcpProviderInstance implements ProviderInstance {
969
1029
  type: 'audio',
970
1030
  data: b.data,
971
1031
  mimeType: b.mimeType,
1032
+ ...(b.uri ? { uri: b.uri } : {}),
1033
+ ...(b.transcript ? { transcript: b.transcript } : {}),
972
1034
  };
973
1035
  }
1036
+ if (b.type === 'video') {
1037
+ return b.uri
1038
+ ? {
1039
+ type: 'resource_link',
1040
+ uri: b.uri,
1041
+ name: path.basename(b.uri),
1042
+ mimeType: b.mimeType,
1043
+ ...(b.transcript ? { description: b.transcript } : {}),
1044
+ }
1045
+ : { type: 'text', text: b.transcript || `[Video attachment: ${b.mimeType}]` };
1046
+ }
974
1047
  if (b.type === 'resource_link') {
975
1048
  return {
976
1049
  type: 'resource_link',
977
1050
  uri: b.uri,
978
1051
  name: b.name,
1052
+ ...(b.title ? { title: b.title } : {}),
1053
+ ...(b.description ? { description: b.description } : {}),
979
1054
  ...(b.mimeType ? { mimeType: b.mimeType } : {}),
1055
+ ...(typeof b.size === 'number' ? { size: b.size } : {}),
1056
+ ...(b.annotations ? { annotations: b.annotations } : {}),
980
1057
  };
981
1058
  }
982
1059
  if (b.type === 'resource') return { type: 'resource', resource: b.resource };
@@ -1056,7 +1133,7 @@ export class AcpProviderInstance implements ProviderInstance {
1056
1133
 
1057
1134
  switch (update.sessionUpdate) {
1058
1135
  case 'agent_message_chunk': {
1059
- const content = update.content;
1136
+ const content: any = update.content;
1060
1137
  if (content.type === 'text') {
1061
1138
  this.partialContent += content.text;
1062
1139
  } else if (content.type === 'image') {
@@ -1071,6 +1148,17 @@ export class AcpProviderInstance implements ProviderInstance {
1071
1148
  type: 'audio',
1072
1149
  data: content.data,
1073
1150
  mimeType: content.mimeType,
1151
+ ...(content.uri ? { uri: content.uri } : {}),
1152
+ ...(content.transcript ? { transcript: content.transcript } : {}),
1153
+ });
1154
+ } else if (content.type === 'video') {
1155
+ this.partialBlocks.push({
1156
+ type: 'video',
1157
+ data: content.data,
1158
+ mimeType: content.mimeType,
1159
+ ...(content.uri ? { uri: content.uri } : {}),
1160
+ ...(content.transcript ? { transcript: content.transcript } : {}),
1161
+ ...(content.posterUri ? { posterUri: content.posterUri } : {}),
1074
1162
  });
1075
1163
  } else if (content.type === 'resource_link') {
1076
1164
  this.partialBlocks.push({