@adhdev/daemon-core 0.8.65 → 0.8.67

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.
@@ -46,10 +46,12 @@ export declare class ExtensionProviderInstance implements ProviderInstance {
46
46
  private applyProviderResponse;
47
47
  private appendRuntimeSystemMessage;
48
48
  private appendRuntimeMessage;
49
+ private buildSyntheticTurnKey;
49
50
  /**
50
- * Assign stable receivedAt to extension messages.
51
- * Same pattern as IdeProviderInstance.readChat() prevByHash
52
- * preserves first-seen timestamp across polling cycles.
51
+ * Assign stable receivedAt / synthetic _turnKey to extension messages.
52
+ * Same transcript should keep the same identity across polling cycles and
53
+ * stream resets, while repeated identical text later in the transcript still
54
+ * produces a distinct completion marker via the occurrence suffix.
53
55
  */
54
56
  private assignReceivedAt;
55
57
  private mergeConversationMessages;
@@ -0,0 +1,9 @@
1
+ import type { InputEnvelope, ProviderModule } from './contracts.js';
2
+ type InputMediaType = 'text' | 'image' | 'audio' | 'video' | 'resource';
3
+ export declare function assertTextOnlyInput(provider: Pick<ProviderModule, 'name' | 'type'> | null | undefined, input: InputEnvelope): void;
4
+ export declare function getDeclaredProviderInputSupport(provider?: Pick<ProviderModule, 'capabilities'> | null): {
5
+ multipart: boolean;
6
+ mediaTypes: Set<InputMediaType>;
7
+ };
8
+ export declare function assertProviderSupportsDeclaredInput(provider: Pick<ProviderModule, 'name' | 'type' | 'capabilities'> | null | undefined, input: InputEnvelope): void;
9
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ReadChatResult } from './contracts.js';
2
+ export declare function validateReadChatResultPayload(raw: unknown, source?: string): ReadChatResult & Record<string, unknown>;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.65",
4
- "description": "ADHDev local session host core session registry, protocol, buffers",
3
+ "version": "0.8.67",
4
+ "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.65",
4
- "description": "ADHDev daemon core CDP, IDE detection, providers, command execution",
3
+ "version": "0.8.67",
4
+ "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -13,6 +13,7 @@ import type {
13
13
  } from './types.js';
14
14
  import type { ProviderModule, ProviderScripts } from '../providers/contracts.js';
15
15
  import { extractProviderControlValues, normalizeProviderEffects } from '../providers/control-effects.js';
16
+ import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
16
17
  import { resolveProviderStateSurface } from '../providers/provider-patch-state.js';
17
18
  import { normalizeChatMessages } from '../providers/chat-message-normalization.js';
18
19
 
@@ -149,26 +150,31 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
149
150
  }
150
151
  return state;
151
152
  }
153
+ const validated = validateReadChatResultPayload(data, `${this.agentType} readChat`);
154
+ const validatedStatus = (validated as any).status as string;
155
+ const streamStatus = validatedStatus === 'generating' || validatedStatus === 'long_generating'
156
+ ? 'streaming'
157
+ : validatedStatus;
152
158
  const state: AgentStreamState = {
153
159
  agentType: this.agentType,
154
160
  agentName: this.agentName,
155
161
  extensionId: this.extensionId,
156
- status: data.status || 'idle',
157
- messages: normalizeChatMessages(Array.isArray(data.messages) ? data.messages : []) as any,
158
- inputContent: data.inputContent || '',
159
- activeModal: data.activeModal,
162
+ status: streamStatus as AgentStreamState['status'],
163
+ messages: normalizeChatMessages(validated.messages) as any,
164
+ inputContent: typeof validated.inputContent === 'string' ? validated.inputContent : '',
165
+ ...(validated.activeModal ? { activeModal: validated.activeModal } : {}),
160
166
  };
161
- if (typeof data.title === 'string' && data.title.trim()) {
162
- state.title = data.title.trim();
167
+ if (typeof validated.title === 'string' && validated.title.trim()) {
168
+ state.title = validated.title.trim();
163
169
  }
164
- const controlValues = extractProviderControlValues(this.provider.controls, data);
170
+ const controlValues = extractProviderControlValues(this.provider.controls, validated);
165
171
  const surface = resolveProviderStateSurface({
166
172
  controlValues,
167
- summaryMetadata: data.summaryMetadata,
173
+ summaryMetadata: validated.summaryMetadata,
168
174
  });
169
175
  if (surface.controlValues) state.controlValues = surface.controlValues;
170
176
  if (surface.summaryMetadata) state.summaryMetadata = surface.summaryMetadata as any;
171
- const effects = normalizeProviderEffects(data);
177
+ const effects = normalizeProviderEffects(validated);
172
178
  if (effects.length > 0) state.effects = effects;
173
179
  if (state.messages.length > 0) {
174
180
  this.lastSuccessState = state;
@@ -45,6 +45,7 @@ import {
45
45
  type CliTraceEntry,
46
46
  } from './provider-cli-shared.js';
47
47
  import { buildChatMessage } from '../providers/chat-message-normalization.js';
48
+ import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
48
49
  import {
49
50
  buildCliParseInput,
50
51
  buildCliTraceParseSnapshot,
@@ -1395,6 +1396,9 @@ export class ProviderCliAdapter implements CliAdapter {
1395
1396
  runtimeSettings: this.runtimeSettings,
1396
1397
  });
1397
1398
  const parsed = this.cliScripts.parseOutput(input);
1399
+ if (parsed && typeof parsed === 'object') {
1400
+ Object.assign(parsed, validateReadChatResultPayload(parsed, `${this.cliType} parseOutput`));
1401
+ }
1398
1402
  const refinedStatus = this.refineDetectedStatus(typeof parsed?.status === 'string' ? parsed.status : null, input.recentBuffer, input.screenText);
1399
1403
  if (parsed && refinedStatus && parsed.status !== refinedStatus) {
1400
1404
  parsed.status = refinedStatus;
@@ -6,6 +6,8 @@
6
6
  import type { CommandResult, CommandHelpers } from './handler.js';
7
7
  import type { CliAdapter } from '../cli-adapter-types.js';
8
8
  import { flattenContent, normalizeInputEnvelope, type InputEnvelope, type ProviderModule, type ProviderScripts } from '../providers/contracts.js';
9
+ import { assertProviderSupportsDeclaredInput, assertTextOnlyInput } from '../providers/provider-input-support.js';
10
+ import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
9
11
  import type { ProviderInstance } from '../providers/provider-instance.js';
10
12
  import { readChatHistory } from '../config/chat-history.js';
11
13
  import { LOG } from '../logging/logger.js';
@@ -80,7 +82,7 @@ function isExtensionTransport(transport: SessionTransport | null): boolean {
80
82
  return transport === 'cdp-webview';
81
83
  }
82
84
 
83
- function buildRecentSendKey(h: CommandHelpers, args: any, provider: ProviderModule | undefined, text: string): string {
85
+ function buildRecentSendKey(h: CommandHelpers, args: any, provider: ProviderModule | undefined, signature: string): string {
84
86
  const transport = getTargetTransport(h, provider) || 'unknown';
85
87
  const target =
86
88
  args?.targetSessionId
@@ -89,7 +91,13 @@ function buildRecentSendKey(h: CommandHelpers, args: any, provider: ProviderModu
89
91
  || h.currentProviderType
90
92
  || h.currentManagerKey
91
93
  || 'unknown';
92
- return `${transport}:${target}:${text.trim()}`;
94
+ return `${transport}:${target}:${signature.trim()}`;
95
+ }
96
+
97
+ function buildSendInputSignature(input: InputEnvelope): string {
98
+ const text = typeof input.textFallback === 'string' ? input.textFallback.trim() : '';
99
+ if (text) return text;
100
+ return JSON.stringify(input.parts || []);
93
101
  }
94
102
 
95
103
  function getSendChatInputEnvelope(args: any): InputEnvelope {
@@ -292,14 +300,20 @@ function computeReadChatSync(messages: ChatMessage[], cursor: Required<ReadChatC
292
300
  }
293
301
 
294
302
  function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
295
- const messages = normalizeReadChatMessages(payload);
303
+ let validatedPayload: Record<string, any>;
304
+ try {
305
+ validatedPayload = validateReadChatResultPayload(payload, 'read_chat command result') as Record<string, any>;
306
+ } catch (error: any) {
307
+ return { success: false, error: error?.message || String(error) };
308
+ }
309
+ const messages = normalizeReadChatMessages(validatedPayload);
296
310
  const cursor = normalizeReadChatCursor(args);
297
311
  if (!cursor.knownMessageCount && !cursor.lastMessageSignature && cursor.tailLimit > 0 && messages.length > cursor.tailLimit) {
298
312
  const tailMessages = messages.slice(-cursor.tailLimit);
299
313
  const lastMessageSignature = getChatMessageSignature(tailMessages[tailMessages.length - 1]);
300
314
  return {
301
315
  success: true,
302
- ...payload,
316
+ ...validatedPayload,
303
317
  messages: tailMessages,
304
318
  syncMode: 'full',
305
319
  replaceFrom: 0,
@@ -310,7 +324,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
310
324
  const sync = computeReadChatSync(messages, cursor);
311
325
  return {
312
326
  success: true,
313
- ...payload,
327
+ ...validatedPayload,
314
328
  messages: sync.messages,
315
329
  syncMode: sync.syncMode,
316
330
  replaceFrom: sync.replaceFrom,
@@ -405,12 +419,24 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
405
419
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
406
420
  if (adapter) {
407
421
  _log(`${transport} adapter: ${adapter.cliType}`);
408
- const status = adapter.getStatus();
422
+ const parsedStatus = typeof adapter.getScriptParsedStatus === 'function'
423
+ ? parseMaybeJson(adapter.getScriptParsedStatus())
424
+ : null;
425
+ const parsedRecord = parsedStatus && typeof parsedStatus === 'object'
426
+ ? parsedStatus as Record<string, any>
427
+ : null;
428
+ const status = parsedRecord || adapter.getStatus();
429
+ const title = typeof parsedRecord?.title === 'string' ? parsedRecord.title : undefined;
430
+ const providerSessionId = typeof parsedRecord?.providerSessionId === 'string'
431
+ ? parsedRecord.providerSessionId
432
+ : undefined;
409
433
  if (status) {
410
434
  return buildReadChatCommandResult({
411
435
  messages: status.messages || [],
412
436
  status: status.status,
413
437
  activeModal: status.activeModal,
438
+ ...(title ? { title } : {}),
439
+ ...(providerSessionId ? { providerSessionId } : {}),
414
440
  }, args);
415
441
  }
416
442
  }
@@ -425,25 +451,26 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
425
451
  let parsed = evalResult.result;
426
452
  if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { } }
427
453
  if (parsed && typeof parsed === 'object') {
428
- _log(`Extension OK: ${parsed.messages?.length || 0} msgs`);
454
+ const validated = validateReadChatResultPayload(parsed, 'extension read_chat');
455
+ _log(`Extension OK: ${validated.messages?.length || 0} msgs`);
429
456
  traceProviderEvent(args, 'provider', 'extension.read_chat.success', {
430
457
  h,
431
458
  provider,
432
459
  payload: {
433
460
  method: 'evaluateProviderScript',
434
461
  result: evalResult.result,
435
- parsed,
436
- messageCount: Array.isArray(parsed.messages) ? parsed.messages.length : 0,
462
+ parsed: validated,
463
+ messageCount: Array.isArray(validated.messages) ? validated.messages.length : 0,
437
464
  },
438
465
  });
439
466
  h.historyWriter.appendNewMessages(
440
467
  provider?.type || 'unknown_extension',
441
- toHistoryPersistedMessages(normalizeReadChatMessages(parsed)),
442
- parsed.title,
468
+ toHistoryPersistedMessages(normalizeReadChatMessages(validated)),
469
+ validated.title,
443
470
  args?.targetSessionId,
444
471
  historySessionId,
445
472
  );
446
- return buildReadChatCommandResult(parsed, args);
473
+ return buildReadChatCommandResult(validated as Record<string, any>, args);
447
474
  }
448
475
  }
449
476
  } catch (e: any) {
@@ -500,15 +527,16 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
500
527
  let parsed: any = raw;
501
528
  if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { } }
502
529
  if (parsed && typeof parsed === 'object') {
503
- _log(`Webview OK: ${parsed.messages?.length || 0} msgs`);
530
+ const validated = validateReadChatResultPayload(parsed, 'webview read_chat');
531
+ _log(`Webview OK: ${validated.messages?.length || 0} msgs`);
504
532
  h.historyWriter.appendNewMessages(
505
533
  provider?.type || getCurrentProviderType(h, 'unknown_webview'),
506
- toHistoryPersistedMessages(normalizeReadChatMessages(parsed)),
507
- parsed.title,
534
+ toHistoryPersistedMessages(normalizeReadChatMessages(validated)),
535
+ validated.title,
508
536
  args?.targetSessionId,
509
537
  historySessionId,
510
538
  );
511
- return buildReadChatCommandResult(parsed, args);
539
+ return buildReadChatCommandResult(validated as Record<string, any>, args);
512
540
  }
513
541
  }
514
542
  } catch (e: any) {
@@ -526,25 +554,26 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
526
554
  let parsed: any = evalResult.result;
527
555
  if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { } }
528
556
  if (parsed && typeof parsed === 'object' && parsed.messages?.length > 0) {
529
- _log(`OK: ${parsed.messages?.length} msgs`);
557
+ const validated = validateReadChatResultPayload(parsed, 'ide read_chat');
558
+ _log(`OK: ${validated.messages?.length} msgs`);
530
559
  traceProviderEvent(args, 'provider', 'ide.read_chat.success', {
531
560
  h,
532
561
  provider,
533
562
  payload: {
534
563
  method: 'evaluate',
535
564
  result: evalResult.result,
536
- parsed,
537
- messageCount: Array.isArray(parsed.messages) ? parsed.messages.length : 0,
565
+ parsed: validated,
566
+ messageCount: Array.isArray(validated.messages) ? validated.messages.length : 0,
538
567
  },
539
568
  });
540
569
  h.historyWriter.appendNewMessages(
541
570
  provider?.type || getCurrentProviderType(h, 'unknown_ide'),
542
- toHistoryPersistedMessages(normalizeReadChatMessages(parsed)),
543
- parsed.title,
571
+ toHistoryPersistedMessages(normalizeReadChatMessages(validated)),
572
+ validated.title,
544
573
  args?.targetSessionId,
545
574
  historySessionId,
546
575
  );
547
- return buildReadChatCommandResult(parsed, args);
576
+ return buildReadChatCommandResult(validated as Record<string, any>, args);
548
577
  }
549
578
  }
550
579
  } catch (e: any) {
@@ -564,11 +593,12 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
564
593
  export async function handleSendChat(h: CommandHelpers, args: any): Promise<CommandResult> {
565
594
  const input = getSendChatInputEnvelope(args);
566
595
  const text = input.textFallback;
567
- if (!text) return { success: false, error: 'text required' };
596
+ const hasInput = input.parts.length > 0 || (typeof text === 'string' && text.trim().length > 0);
597
+ if (!hasInput) return { success: false, error: 'input required' };
568
598
  const _log = (msg: string) => LOG.debug('Command', `[send_chat] ${msg}`);
569
599
  const provider = h.getProvider(args?.agentType);
570
600
  const transport = getTargetTransport(h, provider);
571
- const dedupeKey = buildRecentSendKey(h, args, provider, text);
601
+ const dedupeKey = buildRecentSendKey(h, args, provider, buildSendInputSignature(input));
572
602
 
573
603
  const _logSendSuccess = (method: string, targetAgent?: string) => {
574
604
  // Sending and transcript persistence are intentionally decoupled.
@@ -582,12 +612,28 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
582
612
  return { success: true, sent: false, deduplicated: true };
583
613
  }
584
614
 
585
- // PTY / ACP transport: transmit via adapter
586
- if (isCliLikeTransport(transport)) {
615
+ if (transport === 'acp') {
616
+ const target = getTargetInstance(h, args);
617
+ if (!target || target.category !== 'acp') {
618
+ return { success: false, error: `ACP instance not found for ${provider?.type || args?.agentType || 'unknown'}` };
619
+ }
620
+ try {
621
+ assertProviderSupportsDeclaredInput(provider, input);
622
+ target.onEvent('send_message', { input });
623
+ return _logSendSuccess('acp-instance', target.type);
624
+ } catch (e: any) {
625
+ return { success: false, error: `acp send failed: ${e.message}` };
626
+ }
627
+ }
628
+
629
+ // PTY transport: text-only send via adapter
630
+ if (transport === 'pty') {
587
631
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
588
632
  if (adapter) {
589
633
  _log(`${transport} adapter: ${adapter.cliType}`);
590
634
  try {
635
+ assertTextOnlyInput(provider, input);
636
+ if (!text) return { success: false, error: 'text required for PTY send' };
591
637
  await adapter.sendMessage(text);
592
638
  return _logSendSuccess(`${transport}-adapter`, adapter.cliType);
593
639
  } catch (e: any) {
@@ -596,6 +642,9 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
596
642
  }
597
643
  }
598
644
 
645
+ assertTextOnlyInput(provider, input);
646
+ if (!text) return { success: false, error: 'text required' };
647
+
599
648
  // Extension transport: via AgentStreamManager
600
649
  if (isExtensionTransport(transport)) {
601
650
  _log(`Extension: ${provider?.type || 'unknown_extension'}`);
@@ -23,6 +23,7 @@ import { AcpProviderInstance } from '../providers/acp-provider-instance.js';
23
23
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
24
24
  import { ProviderLoader } from '../providers/provider-loader.js';
25
25
  import { normalizeInputEnvelope, type ProviderModule, type ProviderResumeCapability } from '../providers/contracts.js';
26
+ import { assertProviderSupportsDeclaredInput, assertTextOnlyInput } from '../providers/provider-input-support.js';
26
27
  import type { CliAdapter } from '../cli-adapter-types.js';
27
28
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
28
29
  import type { SessionRegistry } from '../sessions/registry.js';
@@ -950,6 +951,12 @@ export class DaemonCliManager {
950
951
 
951
952
  if (action === 'send_chat') {
952
953
  const input = normalizeInputEnvelope(args?.input ? { input: args.input } : args);
954
+ const provider = this.providerLoader.resolve(agentType) || this.providerLoader.getMeta(agentType);
955
+ if (provider?.category === 'acp') {
956
+ assertProviderSupportsDeclaredInput(provider, input);
957
+ } else {
958
+ assertTextOnlyInput(provider, input);
959
+ }
953
960
  const message = input.textFallback;
954
961
  if (!message) throw new Error('message required for send_chat');
955
962
  await adapter.sendMessage(message);
@@ -179,7 +179,7 @@ export function normalizeProviderScriptArgs(args: any, scriptName?: string): Rec
179
179
 
180
180
  function buildControlScriptResult(scriptName: string, payload: any): Record<string, unknown> {
181
181
  if (!payload || typeof payload !== 'object') return {};
182
- if (Array.isArray(payload.options) || Array.isArray(payload.models) || Array.isArray(payload.modes)) {
182
+ if (Array.isArray(payload.options)) {
183
183
  return { controlResult: normalizeControlListResult(payload) };
184
184
  }
185
185
 
@@ -189,7 +189,7 @@ function buildControlScriptResult(scriptName: string, payload: any): Record<stri
189
189
  if (looksLikeValueMutation) {
190
190
  return { controlResult: normalizeControlSetResult(payload) };
191
191
  }
192
- if (payload.ok !== undefined || payload.success !== undefined || Array.isArray(payload.effects)) {
192
+ if (payload.ok !== undefined || Array.isArray(payload.effects) || typeof payload.error === 'string') {
193
193
  return { controlResult: normalizeControlInvokeResult(payload) };
194
194
  }
195
195
  return {};
@@ -46,8 +46,9 @@ import {
46
46
  type ToolCallStatus,
47
47
  type SessionConfigOption,
48
48
  } from '@agentclientprotocol/sdk';
49
- import type { ProviderModule, ContentBlock, InputEnvelope, InputPart, ToolCallInfo, ToolCallContent as TCC, ToolKind, ToolCallStatus as TCS } from './contracts.js';
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
52
  import type { ProviderInstance, ProviderState, AcpProviderState, ProviderErrorReason, ProviderEvent, InstanceContext } from './provider-instance.js';
52
53
  import { StatusMonitor } from './status-monitor.js';
53
54
  import { buildLegacyModelModeSummaryMetadata } from './summary-metadata.js';
@@ -112,27 +113,6 @@ function getPromptCapabilityFlags(agentCapabilities?: Record<string, any>): Prom
112
113
  };
113
114
  }
114
115
 
115
- function getResourceNameFromUri(uri: string, fallback: string): string {
116
- try {
117
- if (uri.startsWith('file://')) {
118
- return path.basename(new URL(uri).pathname) || fallback;
119
- }
120
- return path.basename(uri) || fallback;
121
- } catch {
122
- return fallback;
123
- }
124
- }
125
-
126
- function inputPartToResourceLink(part: Extract<InputPart, { type: 'image' | 'audio' | 'video' | 'resource' }>, fallbackName: string): ContentBlock | null {
127
- if (!part.uri) return null;
128
- return {
129
- type: 'resource_link',
130
- uri: part.uri,
131
- name: getResourceNameFromUri(part.uri, fallbackName),
132
- ...(part.mimeType ? { mimeType: part.mimeType } : {}),
133
- };
134
- }
135
-
136
116
  function appendPromptText(promptParts: ContentBlock[], text: string | undefined): void {
137
117
  const normalized = typeof text === 'string' ? text.trim() : '';
138
118
  if (!normalized) return;
@@ -152,61 +132,64 @@ export function buildAcpPromptParts(input: InputEnvelope, agentCapabilities?: Re
152
132
  }
153
133
 
154
134
  if (part.type === 'image') {
155
- if (caps.image && part.data) {
156
- promptParts.push({
157
- type: 'image',
158
- data: part.data,
159
- mimeType: part.mimeType,
160
- ...(part.uri ? { uri: part.uri } : {}),
161
- });
162
- continue;
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');
163
140
  }
164
- const fallback = inputPartToResourceLink(part, 'image');
165
- if (fallback) promptParts.push(fallback);
166
- appendPromptText(promptParts, part.alt || (!part.uri ? `Attached image (${part.mimeType})` : undefined));
141
+ promptParts.push({
142
+ type: 'image',
143
+ data: part.data,
144
+ mimeType: part.mimeType,
145
+ ...(part.uri ? { uri: part.uri } : {}),
146
+ });
167
147
  continue;
168
148
  }
169
149
 
170
150
  if (part.type === 'audio') {
171
- if (caps.audio && part.data) {
172
- promptParts.push({
173
- type: 'audio',
174
- data: part.data,
175
- mimeType: part.mimeType,
176
- });
177
- continue;
151
+ if (!caps.audio) {
152
+ throw new Error('ACP agent does not support input type: audio');
178
153
  }
179
- const fallback = inputPartToResourceLink(part, 'audio');
180
- if (fallback) promptParts.push(fallback);
181
- appendPromptText(promptParts, part.transcript || (!part.uri ? `Attached audio (${part.mimeType})` : undefined));
154
+ if (!part.data) {
155
+ throw new Error('ACP audio input requires inline audio data');
156
+ }
157
+ promptParts.push({
158
+ type: 'audio',
159
+ data: part.data,
160
+ mimeType: part.mimeType,
161
+ });
182
162
  continue;
183
163
  }
184
164
 
185
165
  if (part.type === 'resource') {
186
- if (caps.embeddedContext && (part.text || part.data)) {
166
+ if (!caps.embeddedContext) {
167
+ throw new Error('ACP agent does not support input type: resource');
168
+ }
169
+ if (part.text) {
187
170
  promptParts.push({
188
171
  type: 'resource',
189
- resource: part.text
190
- ? { uri: part.uri, text: part.text, mimeType: part.mimeType ?? null }
191
- : { uri: part.uri, blob: part.data || '', mimeType: part.mimeType ?? null },
172
+ resource: { uri: part.uri, text: part.text, mimeType: part.mimeType ?? null },
192
173
  });
193
174
  continue;
194
175
  }
195
- const fallback = inputPartToResourceLink(part, part.name || 'resource');
196
- if (fallback) promptParts.push(fallback);
197
- appendPromptText(promptParts, part.text || (!part.uri && part.name ? part.name : undefined));
198
- continue;
176
+ if (part.data) {
177
+ promptParts.push({
178
+ type: 'resource',
179
+ resource: { uri: part.uri, blob: part.data, mimeType: part.mimeType ?? null },
180
+ });
181
+ continue;
182
+ }
183
+ throw new Error('ACP resource input requires embedded text or binary data');
199
184
  }
200
185
 
201
186
  if (part.type === 'video') {
202
- const fallback = inputPartToResourceLink(part, 'video');
203
- if (fallback) promptParts.push(fallback);
204
- appendPromptText(promptParts, !part.uri ? `Attached video (${part.mimeType})` : undefined);
187
+ throw new Error('ACP agent does not support input type: video');
205
188
  }
206
189
  }
207
190
 
208
191
  if (!promptParts.some((part) => part.type === 'text') && input.textFallback) {
209
- promptParts.unshift({ type: 'text', text: input.textFallback });
192
+ appendPromptText(promptParts, input.textFallback);
210
193
  }
211
194
 
212
195
  return promptParts;
@@ -366,6 +349,7 @@ export class AcpProviderInstance implements ProviderInstance {
366
349
  onEvent(event: string, data?: any): void {
367
350
  if (event === 'send_message') {
368
351
  const input = normalizeInputEnvelope(data)
352
+ assertProviderSupportsDeclaredInput(this.provider, input)
369
353
  const promptParts = buildAcpPromptParts(input, this.agentCapabilities)
370
354
  this.sendPrompt(input.textFallback, promptParts.length > 0 ? promptParts : undefined).catch(e =>
371
355
  this.log.warn(`[${this.type}] sendPrompt error: ${e?.message}`)
@@ -11,6 +11,7 @@ import * as crypto from 'crypto';
11
11
  import * as fs from 'fs';
12
12
  import { createRequire } from 'node:module';
13
13
  import { normalizeInputEnvelope, type ProviderModule, flattenContent } from './contracts.js';
14
+ import { assertTextOnlyInput } from './provider-input-support.js';
14
15
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
15
16
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
16
17
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
@@ -412,6 +413,7 @@ export class CliProviderInstance implements ProviderInstance {
412
413
  onEvent(event: string, data?: any): void {
413
414
  if (event === 'send_message') {
414
415
  const input = normalizeInputEnvelope(data);
416
+ assertTextOnlyInput(this.provider, input);
415
417
  if (input.textFallback) {
416
418
  void this.adapter.sendMessage(input.textFallback).catch((e: any) => {
417
419
  LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
@@ -171,51 +171,43 @@ export function buildPersistedProviderEffectMessage(effect: ProviderEffect | nul
171
171
  }
172
172
 
173
173
  export function normalizeControlListResult(data: any): ControlListResult {
174
- if (data && typeof data === 'object' && Array.isArray(data.options)) {
175
- return {
176
- options: normalizeControlOptions(data.options),
177
- ...(isScalarControlValue(data.currentValue) ? { currentValue: data.currentValue } : {}),
178
- ...(typeof data.error === 'string' ? { error: data.error } : {}),
179
- };
174
+ if (!data || typeof data !== 'object' || !Array.isArray(data.options)) {
175
+ throw new Error('Provider control list results must use the typed shape { options, currentValue?, error? }');
180
176
  }
181
-
182
- const rawOptions = Array.isArray(data?.models)
183
- ? data.models
184
- : Array.isArray(data?.modes)
185
- ? data.modes
186
- : Array.isArray(data?.options)
187
- ? data.options
188
- : [];
189
- const options = normalizeControlOptions(rawOptions);
190
177
  return {
191
- options,
192
- ...(isScalarControlValue(data?.current) ? { currentValue: data.current } : {}),
193
- ...(isScalarControlValue(data?.currentValue) ? { currentValue: data.currentValue } : {}),
194
- ...(typeof data?.error === 'string' ? { error: data.error } : {}),
178
+ options: normalizeControlOptions(data.options),
179
+ ...(isScalarControlValue(data.currentValue) ? { currentValue: data.currentValue } : {}),
180
+ ...(typeof data.error === 'string' ? { error: data.error } : {}),
195
181
  };
196
182
  }
197
183
 
198
184
  export function normalizeControlSetResult(data: any): ControlSetResult {
199
- const currentValue = isScalarControlValue(data?.currentValue)
185
+ if (!data || typeof data !== 'object' || typeof data.ok !== 'boolean') {
186
+ throw new Error('Provider control set results must use the typed shape { ok, currentValue?, effects?, error? }');
187
+ }
188
+ const currentValue = isScalarControlValue(data.currentValue)
200
189
  ? data.currentValue
201
- : (isScalarControlValue(data?.value) ? data.value : undefined);
190
+ : (isScalarControlValue(data.value) ? data.value : undefined);
202
191
  return {
203
- ok: data?.ok === true || data?.success === true,
192
+ ok: data.ok,
204
193
  ...(currentValue !== undefined ? { currentValue } : {}),
205
- ...(Array.isArray(data?.effects) ? { effects: normalizeProviderEffects(data) } : {}),
206
- ...(typeof data?.error === 'string' ? { error: data.error } : {}),
194
+ ...(Array.isArray(data.effects) ? { effects: normalizeProviderEffects(data) } : {}),
195
+ ...(typeof data.error === 'string' ? { error: data.error } : {}),
207
196
  };
208
197
  }
209
198
 
210
199
  export function normalizeControlInvokeResult(data: any): ControlInvokeResult {
211
- const currentValue = isScalarControlValue(data?.currentValue)
200
+ if (!data || typeof data !== 'object' || typeof data.ok !== 'boolean') {
201
+ throw new Error('Provider control invoke results must use the typed shape { ok, currentValue?, effects?, error? }');
202
+ }
203
+ const currentValue = isScalarControlValue(data.currentValue)
212
204
  ? data.currentValue
213
- : (isScalarControlValue(data?.value) ? data.value : undefined);
205
+ : (isScalarControlValue(data.value) ? data.value : undefined);
214
206
  return {
215
- ok: data?.ok === true || data?.success === true,
207
+ ok: data.ok,
216
208
  ...(currentValue !== undefined ? { currentValue } : {}),
217
- ...(Array.isArray(data?.effects) ? { effects: normalizeProviderEffects(data) } : {}),
218
- ...(typeof data?.error === 'string' ? { error: data.error } : {}),
209
+ ...(Array.isArray(data.effects) ? { effects: normalizeProviderEffects(data) } : {}),
210
+ ...(typeof data.error === 'string' ? { error: data.error } : {}),
219
211
  };
220
212
  }
221
213