@adhdev/daemon-core 0.9.76-rc.9 → 0.9.76
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/cli-adapters/provider-cli-adapter.d.ts +5 -2
- package/dist/cli-adapters/provider-cli-runtime.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +24 -0
- package/dist/commands/chat-commands.d.ts +2 -0
- package/dist/commands/cli-manager.d.ts +17 -4
- package/dist/commands/mesh-coordinator.d.ts +2 -0
- package/dist/commands/router.d.ts +11 -0
- package/dist/config/mesh-config.d.ts +3 -0
- package/dist/git/git-types.d.ts +1 -1
- package/dist/git/git-worktree.d.ts +64 -0
- package/dist/git/index.d.ts +2 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2427 -561
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2432 -584
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +1 -0
- package/dist/mesh/mesh-events.d.ts +18 -0
- package/dist/providers/chat-message-normalization.d.ts +40 -0
- package/dist/providers/cli-provider-instance.d.ts +7 -1
- package/dist/providers/contracts.d.ts +20 -1
- package/dist/providers/io-contracts.d.ts +17 -1
- package/dist/providers/provider-input-support.d.ts +18 -2
- package/dist/providers/provider-instance-manager.d.ts +1 -0
- package/dist/providers/provider-instance.d.ts +4 -0
- package/dist/repo-mesh-types.d.ts +34 -0
- package/dist/session-host/runtime-support.d.ts +2 -1
- package/dist/shared-types.d.ts +8 -0
- package/dist/types.d.ts +9 -0
- package/package.json +4 -5
- package/src/chat/subscription-updates.ts +3 -1
- package/src/cli-adapters/provider-cli-adapter.ts +44 -11
- package/src/cli-adapters/provider-cli-runtime.ts +3 -2
- package/src/cli-adapters/provider-cli-shared.ts +201 -15
- package/src/commands/chat-commands.ts +166 -16
- package/src/commands/cli-manager.ts +78 -5
- package/src/commands/handler.ts +13 -4
- package/src/commands/mesh-coordinator.ts +155 -5
- package/src/commands/router.d.ts +1 -0
- package/src/commands/router.ts +606 -32
- package/src/config/mesh-config.ts +27 -2
- package/src/git/git-commands.ts +5 -1
- package/src/git/git-types.ts +1 -0
- package/src/git/git-worktree.ts +214 -0
- package/src/git/index.ts +14 -0
- package/src/index.ts +20 -1
- package/src/mesh/coordinator-prompt.ts +36 -14
- package/src/mesh/mesh-events.ts +173 -42
- package/src/providers/acp-provider-instance.ts +118 -30
- package/src/providers/chat-message-normalization.ts +241 -0
- package/src/providers/cli-provider-instance.d.ts +2 -0
- package/src/providers/cli-provider-instance.ts +219 -13
- package/src/providers/contracts.ts +25 -1
- package/src/providers/io-contracts.ts +63 -5
- package/src/providers/provider-input-support.ts +125 -1
- package/src/providers/provider-instance-manager.ts +20 -1
- package/src/providers/provider-instance.ts +4 -0
- package/src/providers/provider-schema.ts +38 -8
- package/src/providers/read-chat-contract.ts +8 -0
- package/src/repo-mesh-types.ts +38 -0
- package/src/session-host/runtime-support.ts +55 -7
- package/src/shared-types.ts +8 -0
- package/src/status/builders.ts +5 -3
- package/src/status/reporter.ts +6 -0
- package/src/types.ts +9 -0
|
@@ -5,6 +5,43 @@ export const BUILTIN_CHAT_MESSAGE_KINDS = ['standard', 'thought', 'tool', 'termi
|
|
|
5
5
|
export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
|
|
6
6
|
export type ChatMessageKind = BuiltinChatMessageKind | (string & {});
|
|
7
7
|
|
|
8
|
+
export const CHAT_MESSAGE_VISIBILITIES = ['user', 'debug', 'internal', 'hidden'] as const;
|
|
9
|
+
export const CHAT_MESSAGE_TRANSCRIPT_VISIBILITIES = ['visible', 'chat', 'user', 'debug', 'internal', 'hidden'] as const;
|
|
10
|
+
export const CHAT_MESSAGE_AUDIENCES = ['chat', 'debug', 'trace', 'internal'] as const;
|
|
11
|
+
export const CHAT_MESSAGE_SOURCES = [
|
|
12
|
+
'assistant_text',
|
|
13
|
+
'tool_call',
|
|
14
|
+
'terminal_command',
|
|
15
|
+
'runtime_activity',
|
|
16
|
+
'runtime_status',
|
|
17
|
+
'provider_chrome',
|
|
18
|
+
'control',
|
|
19
|
+
] as const;
|
|
20
|
+
export const CHAT_MESSAGE_ACTIVITY_SOURCES = ['tool_call', 'terminal_command', 'runtime_activity'] as const;
|
|
21
|
+
export const CHAT_MESSAGE_INTERNAL_SOURCES = ['runtime_status', 'provider_chrome', 'control'] as const;
|
|
22
|
+
|
|
23
|
+
export type ChatMessageVisibility = typeof CHAT_MESSAGE_VISIBILITIES[number] | (string & {});
|
|
24
|
+
export type ChatMessageTranscriptVisibility = typeof CHAT_MESSAGE_TRANSCRIPT_VISIBILITIES[number] | (string & {});
|
|
25
|
+
export type ChatMessageAudience = typeof CHAT_MESSAGE_AUDIENCES[number] | (string & {});
|
|
26
|
+
export type ChatMessageSource = typeof CHAT_MESSAGE_SOURCES[number] | (string & {});
|
|
27
|
+
export type ChatMessageTranscriptSurface = 'chat' | 'activity' | 'internal';
|
|
28
|
+
|
|
29
|
+
export interface ChatMessageVisibilityClassification {
|
|
30
|
+
surface: ChatMessageTranscriptSurface;
|
|
31
|
+
isUserFacing: boolean;
|
|
32
|
+
isActivityFacing: boolean;
|
|
33
|
+
isInternal: boolean;
|
|
34
|
+
explicitUserFacing: boolean;
|
|
35
|
+
explicitHidden: boolean;
|
|
36
|
+
role: string;
|
|
37
|
+
kind: ChatMessageKind;
|
|
38
|
+
visibility: string;
|
|
39
|
+
transcriptVisibility: string;
|
|
40
|
+
audience: string;
|
|
41
|
+
source: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
8
45
|
const KNOWN_CHAT_MESSAGE_KINDS = new Set<string>(BUILTIN_CHAT_MESSAGE_KINDS);
|
|
9
46
|
const CHAT_MESSAGE_KIND_ALIASES: Record<string, BuiltinChatMessageKind> = {
|
|
10
47
|
text: 'standard',
|
|
@@ -171,3 +208,207 @@ export function normalizeChatMessage<T extends ChatMessage>(message: T): T {
|
|
|
171
208
|
export function normalizeChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
|
|
172
209
|
return (Array.isArray(messages) ? messages : []).map((message) => normalizeChatMessage(message));
|
|
173
210
|
}
|
|
211
|
+
|
|
212
|
+
function readMessageMeta(message: ChatMessage): Record<string, unknown> | null {
|
|
213
|
+
const meta = message?.meta;
|
|
214
|
+
return meta && typeof meta === 'object' && !Array.isArray(meta)
|
|
215
|
+
? meta as Record<string, unknown>
|
|
216
|
+
: null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readStringField(value: unknown): string {
|
|
220
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readRecordField(message: ChatMessage, meta: Record<string, unknown> | null, key: string): unknown {
|
|
224
|
+
const record = message as ChatMessage & Record<string, unknown>;
|
|
225
|
+
return record[key] ?? meta?.[key];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readVisibilityField(message: ChatMessage, meta: Record<string, unknown> | null): string {
|
|
229
|
+
return readStringField(readRecordField(message, meta, 'visibility'));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function readTranscriptVisibilityField(message: ChatMessage, meta: Record<string, unknown> | null): string {
|
|
233
|
+
const record = message as ChatMessage & Record<string, unknown>;
|
|
234
|
+
return readStringField(record.transcriptVisibility ?? meta?.transcriptVisibility ?? record.visibility ?? meta?.visibility);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const EXPLICIT_HIDDEN_VISIBILITIES = new Set(['hidden', 'debug', 'internal']);
|
|
238
|
+
const EXPLICIT_VISIBLE_VISIBILITIES = new Set(['visible', 'user', 'chat']);
|
|
239
|
+
const HIDDEN_AUDIENCES = new Set(['debug', 'trace', 'internal']);
|
|
240
|
+
const ACTIVITY_SOURCE_SET = new Set<string>(CHAT_MESSAGE_ACTIVITY_SOURCES);
|
|
241
|
+
const INTERNAL_SOURCE_SET = new Set<string>(CHAT_MESSAGE_INTERNAL_SOURCES);
|
|
242
|
+
|
|
243
|
+
function hasBooleanMarker(message: ChatMessage, meta: Record<string, unknown> | null, keys: string[]): boolean {
|
|
244
|
+
const record = message as ChatMessage & Record<string, unknown>;
|
|
245
|
+
return keys.some((key) => record[key] === true || meta?.[key] === true);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isActivityKind(kind: ChatMessageKind): boolean {
|
|
249
|
+
return kind === 'thought' || kind === 'tool' || kind === 'terminal';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isOrdinaryVisibleTurn(message: ChatMessage, role: string, kind: ChatMessageKind): boolean {
|
|
253
|
+
if (role === 'user' || role === 'human') return kind === 'standard' || kind === '';
|
|
254
|
+
if (role === 'assistant') return kind === 'standard' || kind === '';
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Shared transcript visibility protocol for all ADHDev provider chat messages.
|
|
260
|
+
*
|
|
261
|
+
* Producers can stamp visibility/audience/source/userFacing/internal/debug either
|
|
262
|
+
* at the top level or under `meta`. Consumers should use this classifier instead
|
|
263
|
+
* of matching command text, icons, provider names, or terminal UI fragments.
|
|
264
|
+
*/
|
|
265
|
+
export function classifyChatMessageVisibility(message: ChatMessage | null | undefined): ChatMessageVisibilityClassification {
|
|
266
|
+
if (!message) {
|
|
267
|
+
return {
|
|
268
|
+
surface: 'internal',
|
|
269
|
+
isUserFacing: false,
|
|
270
|
+
isActivityFacing: false,
|
|
271
|
+
isInternal: true,
|
|
272
|
+
explicitUserFacing: false,
|
|
273
|
+
explicitHidden: true,
|
|
274
|
+
role: '',
|
|
275
|
+
kind: 'standard',
|
|
276
|
+
visibility: '',
|
|
277
|
+
transcriptVisibility: '',
|
|
278
|
+
audience: '',
|
|
279
|
+
source: '',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const meta = readMessageMeta(message);
|
|
284
|
+
const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
|
|
285
|
+
const kind = resolveChatMessageKind(message);
|
|
286
|
+
const visibility = readVisibilityField(message, meta);
|
|
287
|
+
const transcriptVisibility = readTranscriptVisibilityField(message, meta);
|
|
288
|
+
const audience = readStringField(readRecordField(message, meta, 'audience'));
|
|
289
|
+
const source = readStringField(readRecordField(message, meta, 'source'));
|
|
290
|
+
const explicitHidden = EXPLICIT_HIDDEN_VISIBILITIES.has(visibility)
|
|
291
|
+
|| EXPLICIT_HIDDEN_VISIBILITIES.has(transcriptVisibility)
|
|
292
|
+
|| HIDDEN_AUDIENCES.has(audience)
|
|
293
|
+
|| hasBooleanMarker(message, meta, ['internal', 'isInternal', 'debug', 'statusOnly', 'controlOnly']);
|
|
294
|
+
const explicitUserFacing = EXPLICIT_VISIBLE_VISIBILITIES.has(visibility)
|
|
295
|
+
|| EXPLICIT_VISIBLE_VISIBILITIES.has(transcriptVisibility)
|
|
296
|
+
|| audience === 'chat'
|
|
297
|
+
|| hasBooleanMarker(message, meta, ['userFacing']);
|
|
298
|
+
|
|
299
|
+
if (explicitHidden) {
|
|
300
|
+
const activityLike = isActivityKind(kind) || ACTIVITY_SOURCE_SET.has(source);
|
|
301
|
+
return {
|
|
302
|
+
surface: activityLike ? 'activity' : 'internal',
|
|
303
|
+
isUserFacing: false,
|
|
304
|
+
isActivityFacing: activityLike,
|
|
305
|
+
isInternal: !activityLike,
|
|
306
|
+
explicitUserFacing,
|
|
307
|
+
explicitHidden,
|
|
308
|
+
role,
|
|
309
|
+
kind,
|
|
310
|
+
visibility,
|
|
311
|
+
transcriptVisibility,
|
|
312
|
+
audience,
|
|
313
|
+
source,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (explicitUserFacing) {
|
|
318
|
+
return {
|
|
319
|
+
surface: 'chat',
|
|
320
|
+
isUserFacing: true,
|
|
321
|
+
isActivityFacing: false,
|
|
322
|
+
isInternal: false,
|
|
323
|
+
explicitUserFacing,
|
|
324
|
+
explicitHidden,
|
|
325
|
+
role,
|
|
326
|
+
kind,
|
|
327
|
+
visibility,
|
|
328
|
+
transcriptVisibility,
|
|
329
|
+
audience,
|
|
330
|
+
source,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (INTERNAL_SOURCE_SET.has(source) || role === 'system' || kind === 'system') {
|
|
335
|
+
return {
|
|
336
|
+
surface: 'internal',
|
|
337
|
+
isUserFacing: false,
|
|
338
|
+
isActivityFacing: false,
|
|
339
|
+
isInternal: true,
|
|
340
|
+
explicitUserFacing,
|
|
341
|
+
explicitHidden,
|
|
342
|
+
role,
|
|
343
|
+
kind,
|
|
344
|
+
visibility,
|
|
345
|
+
transcriptVisibility,
|
|
346
|
+
audience,
|
|
347
|
+
source,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (ACTIVITY_SOURCE_SET.has(source) || isActivityKind(kind)) {
|
|
352
|
+
return {
|
|
353
|
+
surface: 'activity',
|
|
354
|
+
isUserFacing: false,
|
|
355
|
+
isActivityFacing: true,
|
|
356
|
+
isInternal: false,
|
|
357
|
+
explicitUserFacing,
|
|
358
|
+
explicitHidden,
|
|
359
|
+
role,
|
|
360
|
+
kind,
|
|
361
|
+
visibility,
|
|
362
|
+
transcriptVisibility,
|
|
363
|
+
audience,
|
|
364
|
+
source,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const isUserFacing = isOrdinaryVisibleTurn(message, role, kind);
|
|
369
|
+
return {
|
|
370
|
+
surface: isUserFacing ? 'chat' : 'internal',
|
|
371
|
+
isUserFacing,
|
|
372
|
+
isActivityFacing: false,
|
|
373
|
+
isInternal: !isUserFacing,
|
|
374
|
+
explicitUserFacing,
|
|
375
|
+
explicitHidden,
|
|
376
|
+
role,
|
|
377
|
+
kind,
|
|
378
|
+
visibility,
|
|
379
|
+
transcriptVisibility,
|
|
380
|
+
audience,
|
|
381
|
+
source,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function isUserFacingChatMessage(message: ChatMessage | null | undefined): boolean {
|
|
386
|
+
return classifyChatMessageVisibility(message).isUserFacing;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function isActivityChatMessage(message: ChatMessage | null | undefined): boolean {
|
|
390
|
+
return classifyChatMessageVisibility(message).isActivityFacing;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function isInternalChatMessage(message: ChatMessage | null | undefined): boolean {
|
|
394
|
+
return classifyChatMessageVisibility(message).isInternal;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function filterUserFacingChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
|
|
398
|
+
return (Array.isArray(messages) ? messages : []).filter((message) => isUserFacingChatMessage(message));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function filterActivityChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
|
|
402
|
+
return (Array.isArray(messages) ? messages : []).filter((message) => isActivityChatMessage(message));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function filterInternalChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
|
|
406
|
+
return (Array.isArray(messages) ? messages : []).filter((message) => isInternalChatMessage(message));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function filterChatMessagesByVisibility<T extends ChatMessage>(
|
|
410
|
+
messages: T[] | null | undefined,
|
|
411
|
+
surface: ChatMessageTranscriptSurface,
|
|
412
|
+
): T[] {
|
|
413
|
+
return (Array.isArray(messages) ? messages : []).filter((message) => classifyChatMessageVisibility(message).surface === surface);
|
|
414
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { ProviderModule } from './contracts.js';
|
|
8
8
|
import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
|
|
9
|
+
import type { ChatMessage } from '../types.js';
|
|
9
10
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
10
11
|
import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
11
12
|
export declare class CliProviderInstance implements ProviderInstance {
|
|
@@ -77,6 +78,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
77
78
|
private formatMarkerTimestamp;
|
|
78
79
|
private maybeAppendRuntimeRecoveryMessage;
|
|
79
80
|
private appendRuntimeSystemMessage;
|
|
81
|
+
mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[];
|
|
80
82
|
private mergeConversationMessages;
|
|
81
83
|
private formatApprovalRequestMessage;
|
|
82
84
|
private promoteProviderSessionId;
|
|
@@ -10,8 +10,8 @@ import * as path from 'path';
|
|
|
10
10
|
import * as crypto from 'crypto';
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
|
-
import { normalizeInputEnvelope, type ProviderModule, flattenContent } from './contracts.js';
|
|
14
|
-
import {
|
|
13
|
+
import { normalizeInputEnvelope, type ProviderModule, flattenContent, type InputEnvelope, type InputPart } from './contracts.js';
|
|
14
|
+
import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport } from './provider-input-support.js';
|
|
15
15
|
import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
|
|
16
16
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
17
17
|
import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
|
|
@@ -25,7 +25,7 @@ import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.
|
|
|
25
25
|
import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
|
|
26
26
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
27
27
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
28
|
-
import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages } from './chat-message-normalization.js';
|
|
28
|
+
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind } from './chat-message-normalization.js';
|
|
29
29
|
|
|
30
30
|
type PersistableCliHistoryMessage = {
|
|
31
31
|
role: string;
|
|
@@ -35,6 +35,125 @@ type PersistableCliHistoryMessage = {
|
|
|
35
35
|
receivedAt?: number;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
|
|
39
|
+
'image/png': '.png',
|
|
40
|
+
'image/jpeg': '.jpg',
|
|
41
|
+
'image/jpg': '.jpg',
|
|
42
|
+
'image/gif': '.gif',
|
|
43
|
+
'image/webp': '.webp',
|
|
44
|
+
'image/bmp': '.bmp',
|
|
45
|
+
'image/tiff': '.tiff',
|
|
46
|
+
'image/svg+xml': '.svg',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function filePathFromUri(uri: string): string | null {
|
|
50
|
+
if (!uri) return null;
|
|
51
|
+
if (uri.startsWith('file://')) {
|
|
52
|
+
try {
|
|
53
|
+
return decodeURIComponent(new URL(uri).pathname);
|
|
54
|
+
} catch {
|
|
55
|
+
return uri.slice('file://'.length);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (path.isAbsolute(uri)) return uri;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extensionForImageMime(mimeType: string): string {
|
|
63
|
+
return IMAGE_MIME_EXTENSIONS[mimeType.toLowerCase()] || '.img';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function safeInputImageBasename(index: number, mimeType: string): string {
|
|
67
|
+
const extension = extensionForImageMime(mimeType);
|
|
68
|
+
const suffix = crypto.randomBytes(6).toString('hex');
|
|
69
|
+
return `adhdev-input-image-${Date.now()}-${index}-${suffix}${extension}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function materializeImageDataPart(part: Extract<InputPart, { type: 'image' }>, index: number, dir: string): string | null {
|
|
73
|
+
if (!part.data) return null;
|
|
74
|
+
const rawData = part.data.includes(',') ? part.data.split(',').pop() || '' : part.data;
|
|
75
|
+
if (!rawData) return null;
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
const filePath = path.join(dir, safeInputImageBasename(index, part.mimeType));
|
|
78
|
+
fs.writeFileSync(filePath, Buffer.from(rawData, 'base64'));
|
|
79
|
+
cleanupStaleMaterializedImages(dir);
|
|
80
|
+
return filePath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const MATERIALIZED_IMAGE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
84
|
+
const MATERIALIZED_IMAGE_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
85
|
+
let lastMaterializedImageCleanupAt = 0;
|
|
86
|
+
|
|
87
|
+
function cleanupStaleMaterializedImages(dir: string): void {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
if (now - lastMaterializedImageCleanupAt < MATERIALIZED_IMAGE_CLEANUP_INTERVAL_MS) return;
|
|
90
|
+
lastMaterializedImageCleanupAt = now;
|
|
91
|
+
try {
|
|
92
|
+
const entries = fs.readdirSync(dir);
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (!entry.startsWith('adhdev-input-image-')) continue;
|
|
95
|
+
const fullPath = path.join(dir, entry);
|
|
96
|
+
try {
|
|
97
|
+
const stat = fs.statSync(fullPath);
|
|
98
|
+
if (now - stat.mtimeMs > MATERIALIZED_IMAGE_MAX_AGE_MS) {
|
|
99
|
+
fs.unlinkSync(fullPath);
|
|
100
|
+
}
|
|
101
|
+
} catch { /* file may have been removed concurrently */ }
|
|
102
|
+
}
|
|
103
|
+
} catch { /* dir may not exist or be inaccessible */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildCliStructuredInputPrompt(
|
|
107
|
+
input: InputEnvelope,
|
|
108
|
+
options: { materializeDir?: string } = {},
|
|
109
|
+
): string {
|
|
110
|
+
const promptParts: string[] = [];
|
|
111
|
+
const imageRefs: string[] = [];
|
|
112
|
+
const resourceRefs: string[] = [];
|
|
113
|
+
const materializeDir = options.materializeDir || path.join(os.tmpdir(), 'adhdev-input-media');
|
|
114
|
+
|
|
115
|
+
input.parts.forEach((part, index) => {
|
|
116
|
+
if (part.type === 'text' && part.text.trim()) {
|
|
117
|
+
promptParts.push(part.text.trim());
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (part.type === 'image') {
|
|
122
|
+
const localPath = typeof part.uri === 'string' ? filePathFromUri(part.uri) : null;
|
|
123
|
+
const materializedPath = !localPath && part.data ? materializeImageDataPart(part, index, materializeDir) : null;
|
|
124
|
+
const ref = localPath || materializedPath || part.uri || '';
|
|
125
|
+
if (ref) imageRefs.push(ref);
|
|
126
|
+
if (part.alt?.trim()) promptParts.push(part.alt.trim());
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (part.type === 'resource_link') {
|
|
131
|
+
resourceRefs.push([part.title, part.name, part.description, part.uri].filter(Boolean).join('\n'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (part.type === 'resource') {
|
|
136
|
+
resourceRefs.push([part.name, part.text, part.uri].filter(Boolean).join('\n'));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Only use textFallback when no explicit text parts were collected — it is
|
|
141
|
+
// the flattened version of the same parts, so appending it alongside them
|
|
142
|
+
// would duplicate the content for multipart inputs.
|
|
143
|
+
const hasExplicitTextParts = input.parts.some((part) => part.type === 'text' && part.text.trim());
|
|
144
|
+
if (!hasExplicitTextParts && input.textFallback.trim()) {
|
|
145
|
+
promptParts.push(input.textFallback.trim());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ordered = [
|
|
149
|
+
...imageRefs,
|
|
150
|
+
...promptParts,
|
|
151
|
+
...resourceRefs,
|
|
152
|
+
].filter((value, index, values) => value.trim().length > 0 && values.indexOf(value) === index);
|
|
153
|
+
|
|
154
|
+
return ordered.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
38
157
|
function normalizePersistableCliHistoryContent(content: unknown): string {
|
|
39
158
|
return flattenContent(content as any).replace(/\s+/g, ' ').trim();
|
|
40
159
|
}
|
|
@@ -214,6 +333,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
214
333
|
options?: {
|
|
215
334
|
providerSessionId?: string;
|
|
216
335
|
launchMode?: 'new' | 'resume' | 'manual';
|
|
336
|
+
extraEnv?: Record<string, string>;
|
|
217
337
|
onProviderSessionResolved?: (info: {
|
|
218
338
|
instanceId: string;
|
|
219
339
|
providerType: string;
|
|
@@ -230,7 +350,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
230
350
|
this.providerSessionId = options?.providerSessionId;
|
|
231
351
|
this.launchMode = options?.launchMode || 'new';
|
|
232
352
|
this.onProviderSessionResolved = options?.onProviderSessionResolved;
|
|
233
|
-
this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, transportFactory);
|
|
353
|
+
this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
|
|
234
354
|
this.monitor = new StatusMonitor();
|
|
235
355
|
this.historyWriter = new ChatHistoryWriter();
|
|
236
356
|
}
|
|
@@ -475,6 +595,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
475
595
|
resume: this.provider.resume,
|
|
476
596
|
controlValues: surface.controlValues,
|
|
477
597
|
providerControls: this.provider.controls,
|
|
598
|
+
messageInput: getEffectiveMessageInputSupport(this.provider),
|
|
478
599
|
summaryMetadata: surface.summaryMetadata as any,
|
|
479
600
|
errorMessage: this.errorMessage,
|
|
480
601
|
errorReason: this.errorReason,
|
|
@@ -531,9 +652,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
531
652
|
onEvent(event: string, data?: any): void {
|
|
532
653
|
if (event === 'send_message') {
|
|
533
654
|
const input = normalizeInputEnvelope(data);
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
655
|
+
assertProviderSupportsDeclaredInput(this.provider, input);
|
|
656
|
+
const promptText = buildCliStructuredInputPrompt(input);
|
|
657
|
+
if (promptText) {
|
|
658
|
+
void this.adapter.sendMessage(promptText).catch((e: any) => {
|
|
537
659
|
LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
|
|
538
660
|
});
|
|
539
661
|
}
|
|
@@ -737,7 +859,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
737
859
|
}
|
|
738
860
|
|
|
739
861
|
private pushEvent(event: ProviderEvent): void {
|
|
740
|
-
|
|
862
|
+
const enrichedEvent: ProviderEvent = {
|
|
863
|
+
...event,
|
|
864
|
+
instanceId: typeof event.instanceId === 'string' && event.instanceId.trim()
|
|
865
|
+
? event.instanceId
|
|
866
|
+
: this.instanceId,
|
|
867
|
+
targetSessionId: typeof event.targetSessionId === 'string' && event.targetSessionId.trim()
|
|
868
|
+
? event.targetSessionId
|
|
869
|
+
: this.instanceId,
|
|
870
|
+
providerType: typeof event.providerType === 'string' && event.providerType.trim()
|
|
871
|
+
? event.providerType
|
|
872
|
+
: this.type,
|
|
873
|
+
workspaceName: typeof event.workspaceName === 'string' && event.workspaceName.trim()
|
|
874
|
+
? event.workspaceName
|
|
875
|
+
: this.workingDir,
|
|
876
|
+
providerSessionId: typeof event.providerSessionId === 'string' && event.providerSessionId.trim()
|
|
877
|
+
? event.providerSessionId
|
|
878
|
+
: this.providerSessionId,
|
|
879
|
+
};
|
|
880
|
+
if (this.context?.emitProviderEvent) {
|
|
881
|
+
this.context.emitProviderEvent(enrichedEvent);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
this.events.push(enrichedEvent);
|
|
741
885
|
}
|
|
742
886
|
|
|
743
887
|
private flushEvents(): ProviderEvent[] {
|
|
@@ -977,15 +1121,77 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
977
1121
|
}
|
|
978
1122
|
}
|
|
979
1123
|
|
|
1124
|
+
mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[] {
|
|
1125
|
+
return this.mergeConversationMessages(parsedMessages);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
980
1128
|
private mergeConversationMessages(parsedMessages: any[]): ChatMessage[] {
|
|
981
1129
|
if (this.runtimeMessages.length === 0) return normalizeChatMessages(parsedMessages);
|
|
982
1130
|
|
|
983
|
-
|
|
984
|
-
|
|
1131
|
+
type MergeEntry = { message: ChatMessage; index: number; source: 'parsed' | 'runtime'; runtimeKey?: string };
|
|
1132
|
+
const parsedEntries: MergeEntry[] = parsedMessages.map((message, index) => ({
|
|
1133
|
+
message,
|
|
1134
|
+
index,
|
|
1135
|
+
source: 'parsed',
|
|
1136
|
+
}));
|
|
1137
|
+
const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
|
|
1138
|
+
message: entry.message,
|
|
1139
|
+
index: parsedMessages.length + index,
|
|
1140
|
+
source: 'runtime',
|
|
1141
|
+
runtimeKey: entry.key,
|
|
1142
|
+
}));
|
|
1143
|
+
const getTime = (message: ChatMessage): number => {
|
|
1144
|
+
const value = typeof message.receivedAt === 'number'
|
|
1145
|
+
? message.receivedAt
|
|
1146
|
+
: typeof message.timestamp === 'number'
|
|
1147
|
+
? message.timestamp
|
|
1148
|
+
: 0;
|
|
1149
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1153
|
+
? message.role.trim().toLowerCase()
|
|
1154
|
+
: '';
|
|
1155
|
+
const isRuntimeOverlay = (entry: MergeEntry): boolean => {
|
|
1156
|
+
if (entry.source !== 'runtime') return false;
|
|
1157
|
+
const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
|
|
1158
|
+
if (key.startsWith('auto_approval:')) return true;
|
|
1159
|
+
return !isUserFacingChatMessage(entry.message);
|
|
1160
|
+
};
|
|
1161
|
+
const shouldKeepParsedBeforeUntimedRuntime = (message: ChatMessage): boolean => {
|
|
1162
|
+
const role = getRole(message);
|
|
1163
|
+
return role === 'user' || role === 'human';
|
|
1164
|
+
};
|
|
1165
|
+
const shouldKeepParsedAfterUntimedRuntime = (message: ChatMessage): boolean => {
|
|
1166
|
+
const role = getRole(message);
|
|
1167
|
+
if (role !== 'assistant') return false;
|
|
1168
|
+
const kind = resolveChatMessageKind(message);
|
|
1169
|
+
return kind === 'standard' || kind === 'terminal';
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
return normalizeChatMessages([...parsedEntries, ...runtimeEntries]
|
|
985
1173
|
.sort((a, b) => {
|
|
986
|
-
const aTime = a.message
|
|
987
|
-
const bTime = b.message
|
|
988
|
-
if (aTime !== bTime) return aTime - bTime;
|
|
1174
|
+
const aTime = getTime(a.message);
|
|
1175
|
+
const bTime = getTime(b.message);
|
|
1176
|
+
if (aTime && bTime && aTime !== bTime) return aTime - bTime;
|
|
1177
|
+
if (a.source !== b.source && aTime !== bTime) {
|
|
1178
|
+
const parsedEntry = a.source === 'parsed' ? a : b.source === 'parsed' ? b : null;
|
|
1179
|
+
const runtimeEntry = a.source === 'runtime' ? a : b.source === 'runtime' ? b : null;
|
|
1180
|
+
if (parsedEntry && runtimeEntry && isRuntimeOverlay(runtimeEntry) && getTime(parsedEntry.message) === 0 && getTime(runtimeEntry.message) > 0) {
|
|
1181
|
+
if (shouldKeepParsedBeforeUntimedRuntime(parsedEntry.message)) {
|
|
1182
|
+
return a.source === 'parsed' ? -1 : 1;
|
|
1183
|
+
}
|
|
1184
|
+
if (shouldKeepParsedAfterUntimedRuntime(parsedEntry.message)) {
|
|
1185
|
+
return a.source === 'parsed' ? 1 : -1;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
// Many provider-owned CLI transcripts (including Hermes CLI in debug bundles)
|
|
1190
|
+
// do not carry timestamps on parsed messages. In that case there is no safe
|
|
1191
|
+
// clock basis for interleaving timestamped runtime/system messages into the
|
|
1192
|
+
// provider transcript. Keep user prompts before runtime overlays, but do not
|
|
1193
|
+
// let timed runtime/system/tool/internal overlays become the final chat turns
|
|
1194
|
+
// after an untimed parsed assistant transcript.
|
|
989
1195
|
return a.index - b.index;
|
|
990
1196
|
})
|
|
991
1197
|
.map((entry) => entry.message));
|
|
@@ -128,6 +128,7 @@ export type ContentBlock =
|
|
|
128
128
|
| TextBlock
|
|
129
129
|
| ImageBlock
|
|
130
130
|
| AudioBlock
|
|
131
|
+
| VideoBlock
|
|
131
132
|
| ResourceLinkBlock
|
|
132
133
|
| ResourceBlock;
|
|
133
134
|
|
|
@@ -144,6 +145,7 @@ export interface ImageBlock {
|
|
|
144
145
|
data: string; // base64-encoded
|
|
145
146
|
mimeType: string; // 'image/png', 'image/jpeg', etc.
|
|
146
147
|
uri?: string; // optional URL reference
|
|
148
|
+
alt?: string;
|
|
147
149
|
annotations?: ContentAnnotations;
|
|
148
150
|
}
|
|
149
151
|
|
|
@@ -152,6 +154,19 @@ export interface AudioBlock {
|
|
|
152
154
|
type: 'audio';
|
|
153
155
|
data: string; // base64-encoded
|
|
154
156
|
mimeType: string;
|
|
157
|
+
uri?: string;
|
|
158
|
+
transcript?: string;
|
|
159
|
+
annotations?: ContentAnnotations;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Video content — ADHDev canonical display block. ACP prompt input degrades video to resource_link/text. */
|
|
163
|
+
export interface VideoBlock {
|
|
164
|
+
type: 'video';
|
|
165
|
+
data?: string; // base64-encoded
|
|
166
|
+
mimeType: string;
|
|
167
|
+
uri?: string;
|
|
168
|
+
transcript?: string;
|
|
169
|
+
posterUri?: string;
|
|
155
170
|
annotations?: ContentAnnotations;
|
|
156
171
|
}
|
|
157
172
|
|
|
@@ -595,7 +610,16 @@ export interface ProviderModule {
|
|
|
595
610
|
// ─── Contract version / capability declaration ───
|
|
596
611
|
contractVersion?: number;
|
|
597
612
|
capabilities?: {
|
|
598
|
-
input?: {
|
|
613
|
+
input?: {
|
|
614
|
+
multipart?: boolean;
|
|
615
|
+
mediaTypes?: Array<'text' | 'image' | 'audio' | 'video' | 'resource'>;
|
|
616
|
+
strategies?: Array<{
|
|
617
|
+
mediaType: 'text' | 'image' | 'audio' | 'video' | 'resource';
|
|
618
|
+
strategies?: Array<'native' | 'native_acp' | 'resource_link' | 'text_fallback' | 'paste' | 'upload'>;
|
|
619
|
+
native?: boolean;
|
|
620
|
+
degradation?: Array<'native' | 'native_acp' | 'resource_link' | 'text_fallback' | 'paste' | 'upload'>;
|
|
621
|
+
}>;
|
|
622
|
+
};
|
|
599
623
|
output?: { richContent?: boolean; mediaTypes?: Array<'text' | 'image' | 'audio' | 'video' | 'resource'> };
|
|
600
624
|
controls?: { typedResults?: boolean };
|
|
601
625
|
};
|