@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,7 @@ export type InputPart =
|
|
|
5
5
|
| ImageInputPart
|
|
6
6
|
| AudioInputPart
|
|
7
7
|
| VideoInputPart
|
|
8
|
+
| ResourceLinkInputPart
|
|
8
9
|
| ResourceInputPart
|
|
9
10
|
|
|
10
11
|
export interface TextInputPart {
|
|
@@ -33,6 +34,7 @@ export interface VideoInputPart {
|
|
|
33
34
|
mimeType: string
|
|
34
35
|
uri?: string
|
|
35
36
|
data?: string
|
|
37
|
+
transcript?: string
|
|
36
38
|
posterUri?: string
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -45,6 +47,17 @@ export interface ResourceInputPart {
|
|
|
45
47
|
data?: string
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
export interface ResourceLinkInputPart {
|
|
51
|
+
type: 'resource_link'
|
|
52
|
+
uri: string
|
|
53
|
+
name: string
|
|
54
|
+
title?: string
|
|
55
|
+
description?: string
|
|
56
|
+
mimeType?: string
|
|
57
|
+
size?: number
|
|
58
|
+
annotations?: ContentAnnotations
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
export interface InputEnvelope {
|
|
49
62
|
parts: InputPart[]
|
|
50
63
|
textFallback: string
|
|
@@ -73,6 +86,7 @@ export interface ImageMessagePart {
|
|
|
73
86
|
mimeType: string
|
|
74
87
|
uri?: string
|
|
75
88
|
data?: string
|
|
89
|
+
alt?: string
|
|
76
90
|
annotations?: ContentAnnotations
|
|
77
91
|
}
|
|
78
92
|
|
|
@@ -90,6 +104,7 @@ export interface VideoMessagePart {
|
|
|
90
104
|
mimeType: string
|
|
91
105
|
uri?: string
|
|
92
106
|
data?: string
|
|
107
|
+
transcript?: string
|
|
93
108
|
posterUri?: string
|
|
94
109
|
annotations?: ContentAnnotations
|
|
95
110
|
}
|
|
@@ -98,8 +113,11 @@ export interface ResourceLinkMessagePart {
|
|
|
98
113
|
type: 'resource_link'
|
|
99
114
|
uri: string
|
|
100
115
|
name: string
|
|
116
|
+
title?: string
|
|
117
|
+
description?: string
|
|
101
118
|
mimeType?: string
|
|
102
119
|
size?: number
|
|
120
|
+
annotations?: ContentAnnotations
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
export interface ResourceMessagePart {
|
|
@@ -149,6 +167,10 @@ export function flattenMessageParts(parts: MessagePart[]): string {
|
|
|
149
167
|
.map((part) => {
|
|
150
168
|
if (part.type === 'text') return part.text
|
|
151
169
|
if (part.type === 'resource') return part.resource.text || ''
|
|
170
|
+
if (part.type === 'image') return part.alt || (part.data ? `[image: ${part.mimeType}]` : '')
|
|
171
|
+
if (part.type === 'audio') return part.transcript || (part.data ? `[audio: ${part.mimeType}]` : '')
|
|
172
|
+
if (part.type === 'video') return part.transcript || (part.data ? `[video: ${part.mimeType}]` : '')
|
|
173
|
+
if (part.type === 'resource_link') return [part.name, part.description].filter(Boolean).join('\n')
|
|
152
174
|
return ''
|
|
153
175
|
})
|
|
154
176
|
.filter((value) => value.length > 0)
|
|
@@ -247,6 +269,7 @@ function normalizeInputPartObject(raw: Record<string, unknown>): InputPart | nul
|
|
|
247
269
|
mimeType: raw.mimeType,
|
|
248
270
|
...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
|
|
249
271
|
...(typeof raw.data === 'string' ? { data: raw.data } : {}),
|
|
272
|
+
...(typeof raw.transcript === 'string' ? { transcript: raw.transcript } : {}),
|
|
250
273
|
...(typeof raw.posterUri === 'string' ? { posterUri: raw.posterUri } : {}),
|
|
251
274
|
}
|
|
252
275
|
}
|
|
@@ -262,10 +285,14 @@ function normalizeInputPartObject(raw: Record<string, unknown>): InputPart | nul
|
|
|
262
285
|
}
|
|
263
286
|
if (type === 'resource_link' && typeof raw.uri === 'string') {
|
|
264
287
|
return {
|
|
265
|
-
type
|
|
288
|
+
type,
|
|
266
289
|
uri: raw.uri,
|
|
290
|
+
name: typeof raw.name === 'string' ? raw.name : getUriDisplayName(raw.uri, 'resource'),
|
|
291
|
+
...(typeof raw.title === 'string' ? { title: raw.title } : {}),
|
|
292
|
+
...(typeof raw.description === 'string' ? { description: raw.description } : {}),
|
|
267
293
|
...(typeof raw.mimeType === 'string' ? { mimeType: raw.mimeType } : {}),
|
|
268
|
-
...(typeof raw.
|
|
294
|
+
...(typeof raw.size === 'number' && Number.isFinite(raw.size) ? { size: raw.size } : {}),
|
|
295
|
+
...normalizeAnnotationsProperty(raw.annotations),
|
|
269
296
|
}
|
|
270
297
|
}
|
|
271
298
|
return null
|
|
@@ -282,6 +309,7 @@ function normalizeMessagePartObject(raw: Record<string, unknown>): MessagePart |
|
|
|
282
309
|
mimeType: raw.mimeType,
|
|
283
310
|
...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
|
|
284
311
|
...(typeof raw.data === 'string' ? { data: raw.data } : {}),
|
|
312
|
+
...(typeof raw.alt === 'string' ? { alt: raw.alt } : {}),
|
|
285
313
|
}
|
|
286
314
|
}
|
|
287
315
|
if (type === 'audio' && typeof raw.mimeType === 'string') {
|
|
@@ -299,6 +327,7 @@ function normalizeMessagePartObject(raw: Record<string, unknown>): MessagePart |
|
|
|
299
327
|
mimeType: raw.mimeType,
|
|
300
328
|
...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
|
|
301
329
|
...(typeof raw.data === 'string' ? { data: raw.data } : {}),
|
|
330
|
+
...(typeof raw.transcript === 'string' ? { transcript: raw.transcript } : {}),
|
|
302
331
|
...(typeof raw.posterUri === 'string' ? { posterUri: raw.posterUri } : {}),
|
|
303
332
|
}
|
|
304
333
|
}
|
|
@@ -307,8 +336,11 @@ function normalizeMessagePartObject(raw: Record<string, unknown>): MessagePart |
|
|
|
307
336
|
type,
|
|
308
337
|
uri: raw.uri,
|
|
309
338
|
name: raw.name,
|
|
339
|
+
...(typeof raw.title === 'string' ? { title: raw.title } : {}),
|
|
340
|
+
...(typeof raw.description === 'string' ? { description: raw.description } : {}),
|
|
310
341
|
...(typeof raw.mimeType === 'string' ? { mimeType: raw.mimeType } : {}),
|
|
311
|
-
...(typeof raw.size === 'number' ? { size: raw.size } : {}),
|
|
342
|
+
...(typeof raw.size === 'number' && Number.isFinite(raw.size) ? { size: raw.size } : {}),
|
|
343
|
+
...normalizeAnnotationsProperty(raw.annotations),
|
|
312
344
|
}
|
|
313
345
|
}
|
|
314
346
|
if (type === 'resource' && raw.resource && typeof raw.resource === 'object') {
|
|
@@ -331,10 +363,36 @@ function flattenInputParts(parts: InputPart[]): string {
|
|
|
331
363
|
return parts
|
|
332
364
|
.map((part) => {
|
|
333
365
|
if (part.type === 'text') return part.text
|
|
334
|
-
if (part.type === '
|
|
335
|
-
if (part.type === '
|
|
366
|
+
if (part.type === 'image') return part.alt || (part.data ? `[image: ${part.mimeType}]` : '')
|
|
367
|
+
if (part.type === 'audio') return part.transcript || (part.data ? `[audio: ${part.mimeType}]` : '')
|
|
368
|
+
if (part.type === 'video') return part.transcript || (part.data ? `[video: ${part.mimeType}]` : '')
|
|
369
|
+
if (part.type === 'resource_link') return [part.title, part.name, part.description, part.uri].filter(Boolean).join('\n')
|
|
370
|
+
if (part.type === 'resource') return part.text || part.name || part.uri
|
|
336
371
|
return ''
|
|
337
372
|
})
|
|
338
373
|
.filter((value) => value.length > 0)
|
|
339
374
|
.join('\n')
|
|
340
375
|
}
|
|
376
|
+
|
|
377
|
+
function getUriDisplayName(uri: string, fallback: string): string {
|
|
378
|
+
try {
|
|
379
|
+
const pathname = uri.startsWith('file://') ? new URL(uri).pathname : uri
|
|
380
|
+
return pathname.split(/[\\/]/).filter(Boolean).pop() || fallback
|
|
381
|
+
} catch {
|
|
382
|
+
return uri.split(/[\\/]/).filter(Boolean).pop() || fallback
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function normalizeAnnotationsProperty(value: unknown): { annotations?: ContentAnnotations } {
|
|
387
|
+
if (!value || typeof value !== 'object') return {}
|
|
388
|
+
const record = value as Record<string, unknown>
|
|
389
|
+
const annotations: ContentAnnotations = {}
|
|
390
|
+
if (Array.isArray(record.audience)) {
|
|
391
|
+
const audience = record.audience.filter((item): item is 'user' | 'assistant' => item === 'user' || item === 'assistant')
|
|
392
|
+
if (audience.length > 0) annotations.audience = audience
|
|
393
|
+
}
|
|
394
|
+
if (typeof record.priority === 'number' && Number.isFinite(record.priority)) {
|
|
395
|
+
annotations.priority = record.priority
|
|
396
|
+
}
|
|
397
|
+
return Object.keys(annotations).length > 0 ? { annotations } : {}
|
|
398
|
+
}
|
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import type { InputEnvelope, ProviderModule } from './contracts.js'
|
|
2
2
|
|
|
3
|
-
type InputMediaType = 'text' | 'image' | 'audio' | 'video' | 'resource'
|
|
3
|
+
export type InputMediaType = 'text' | 'image' | 'audio' | 'video' | 'resource'
|
|
4
|
+
|
|
5
|
+
export type InputAttachmentStrategy =
|
|
6
|
+
| 'native'
|
|
7
|
+
| 'native_acp'
|
|
8
|
+
| 'resource_link'
|
|
9
|
+
| 'text_fallback'
|
|
10
|
+
| 'paste'
|
|
11
|
+
| 'upload'
|
|
12
|
+
|
|
13
|
+
export interface InputMediaStrategyDescriptor {
|
|
14
|
+
mediaType: InputMediaType
|
|
15
|
+
strategies: InputAttachmentStrategy[]
|
|
16
|
+
native?: boolean
|
|
17
|
+
degradation?: InputAttachmentStrategy[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MessageInputSupport {
|
|
21
|
+
text: boolean
|
|
22
|
+
multipart: boolean
|
|
23
|
+
mediaTypes: InputMediaType[]
|
|
24
|
+
strategies: InputMediaStrategyDescriptor[]
|
|
25
|
+
}
|
|
4
26
|
|
|
5
27
|
const VALID_INPUT_MEDIA_TYPES = new Set<InputMediaType>(['text', 'image', 'audio', 'video', 'resource'])
|
|
28
|
+
const VALID_INPUT_STRATEGIES = new Set<InputAttachmentStrategy>(['native', 'native_acp', 'resource_link', 'text_fallback', 'paste', 'upload'])
|
|
29
|
+
|
|
30
|
+
export const TEXT_ONLY_MESSAGE_INPUT_SUPPORT: MessageInputSupport = Object.freeze<MessageInputSupport>({
|
|
31
|
+
text: true,
|
|
32
|
+
multipart: false,
|
|
33
|
+
mediaTypes: ['text'],
|
|
34
|
+
strategies: [],
|
|
35
|
+
})
|
|
6
36
|
|
|
7
37
|
function getProviderLabel(provider?: Pick<ProviderModule, 'name' | 'type'> | null): string {
|
|
8
38
|
return provider?.name || provider?.type || 'This provider'
|
|
@@ -44,14 +74,108 @@ export function assertTextOnlyInput(provider: Pick<ProviderModule, 'name' | 'typ
|
|
|
44
74
|
export function getDeclaredProviderInputSupport(provider?: Pick<ProviderModule, 'capabilities'> | null): {
|
|
45
75
|
multipart: boolean
|
|
46
76
|
mediaTypes: Set<InputMediaType>
|
|
77
|
+
strategies: InputMediaStrategyDescriptor[]
|
|
47
78
|
} {
|
|
48
79
|
const rawMediaTypes = Array.isArray(provider?.capabilities?.input?.mediaTypes)
|
|
49
80
|
? provider?.capabilities?.input?.mediaTypes.filter((type): type is InputMediaType => VALID_INPUT_MEDIA_TYPES.has(type as InputMediaType))
|
|
50
81
|
: []
|
|
82
|
+
const strategies = normalizeInputStrategyDescriptors(provider?.capabilities?.input?.strategies)
|
|
51
83
|
|
|
52
84
|
return {
|
|
53
85
|
multipart: provider?.capabilities?.input?.multipart === true,
|
|
54
86
|
mediaTypes: new Set<InputMediaType>(rawMediaTypes.length > 0 ? rawMediaTypes : ['text']),
|
|
87
|
+
strategies,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function normalizeInputStrategyDescriptors(raw: unknown): InputMediaStrategyDescriptor[] {
|
|
92
|
+
if (!Array.isArray(raw)) return []
|
|
93
|
+
const result: InputMediaStrategyDescriptor[] = []
|
|
94
|
+
for (const entry of raw) {
|
|
95
|
+
if (!entry || typeof entry !== 'object') continue
|
|
96
|
+
const record = entry as Record<string, unknown>
|
|
97
|
+
const mediaType = record.mediaType
|
|
98
|
+
if (typeof mediaType !== 'string' || !VALID_INPUT_MEDIA_TYPES.has(mediaType as InputMediaType)) continue
|
|
99
|
+
const strategies = Array.isArray(record.strategies)
|
|
100
|
+
? record.strategies.filter((value): value is InputAttachmentStrategy => typeof value === 'string' && VALID_INPUT_STRATEGIES.has(value as InputAttachmentStrategy))
|
|
101
|
+
: []
|
|
102
|
+
const degradation = Array.isArray(record.degradation)
|
|
103
|
+
? record.degradation.filter((value): value is InputAttachmentStrategy => typeof value === 'string' && VALID_INPUT_STRATEGIES.has(value as InputAttachmentStrategy))
|
|
104
|
+
: []
|
|
105
|
+
if (strategies.length === 0 && degradation.length === 0) continue
|
|
106
|
+
result.push({
|
|
107
|
+
mediaType: mediaType as InputMediaType,
|
|
108
|
+
strategies,
|
|
109
|
+
...(typeof record.native === 'boolean' ? { native: record.native } : {}),
|
|
110
|
+
...(degradation.length > 0 ? { degradation } : {}),
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function promptCapabilityFlags(runtimeCapabilities?: Record<string, any> | null): { image: boolean; audio: boolean; embeddedContext: boolean } {
|
|
117
|
+
const prompt = runtimeCapabilities?.promptCapabilities || {}
|
|
118
|
+
return {
|
|
119
|
+
image: prompt.image === true,
|
|
120
|
+
audio: prompt.audio === true,
|
|
121
|
+
embeddedContext: prompt.embeddedContext === true,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function supportFromDeclared(provider?: Pick<ProviderModule, 'capabilities'> | null): MessageInputSupport {
|
|
126
|
+
const declared = getDeclaredProviderInputSupport(provider)
|
|
127
|
+
return {
|
|
128
|
+
text: true,
|
|
129
|
+
multipart: declared.multipart,
|
|
130
|
+
mediaTypes: Array.from(declared.mediaTypes),
|
|
131
|
+
strategies: declared.strategies,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getEffectiveMessageInputSupport(
|
|
136
|
+
provider?: Pick<ProviderModule, 'category' | 'capabilities'> | null,
|
|
137
|
+
runtimeCapabilities?: Record<string, any> | null,
|
|
138
|
+
): MessageInputSupport {
|
|
139
|
+
if (provider?.category !== 'acp') {
|
|
140
|
+
const declared = supportFromDeclared(provider)
|
|
141
|
+
return {
|
|
142
|
+
...declared,
|
|
143
|
+
mediaTypes: [...declared.mediaTypes],
|
|
144
|
+
strategies: declared.strategies.map((strategy) => ({
|
|
145
|
+
...strategy,
|
|
146
|
+
strategies: [...strategy.strategies],
|
|
147
|
+
...(strategy.degradation ? { degradation: [...strategy.degradation] } : {}),
|
|
148
|
+
})),
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const declared = supportFromDeclared(provider)
|
|
153
|
+
const caps = promptCapabilityFlags(runtimeCapabilities)
|
|
154
|
+
const mediaTypes = new Set<InputMediaType>(['text'])
|
|
155
|
+
const strategies: InputMediaStrategyDescriptor[] = []
|
|
156
|
+
|
|
157
|
+
if (declared.mediaTypes.includes('resource')) {
|
|
158
|
+
mediaTypes.add('resource')
|
|
159
|
+
strategies.push({ mediaType: 'resource', strategies: caps.embeddedContext ? ['native_acp', 'resource_link', 'text_fallback'] : ['resource_link', 'text_fallback'], native: caps.embeddedContext, degradation: ['resource_link', 'text_fallback'] })
|
|
160
|
+
}
|
|
161
|
+
if (declared.mediaTypes.includes('video')) {
|
|
162
|
+
mediaTypes.add('video')
|
|
163
|
+
strategies.push({ mediaType: 'video', strategies: ['resource_link', 'text_fallback'], native: false, degradation: ['resource_link', 'text_fallback'] })
|
|
164
|
+
}
|
|
165
|
+
if (declared.mediaTypes.includes('image')) {
|
|
166
|
+
mediaTypes.add('image')
|
|
167
|
+
strategies.push({ mediaType: 'image', strategies: caps.image ? ['native_acp', 'resource_link', 'text_fallback'] : ['resource_link', 'text_fallback'], native: caps.image, degradation: ['resource_link', 'text_fallback'] })
|
|
168
|
+
}
|
|
169
|
+
if (declared.mediaTypes.includes('audio')) {
|
|
170
|
+
mediaTypes.add('audio')
|
|
171
|
+
strategies.push({ mediaType: 'audio', strategies: caps.audio ? ['native_acp', 'resource_link', 'text_fallback'] : ['resource_link', 'text_fallback'], native: caps.audio, degradation: ['resource_link', 'text_fallback'] })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
text: true,
|
|
176
|
+
multipart: declared.multipart && mediaTypes.size > 1,
|
|
177
|
+
mediaTypes: Array.from(mediaTypes),
|
|
178
|
+
strategies,
|
|
55
179
|
}
|
|
56
180
|
}
|
|
57
181
|
|
|
@@ -47,7 +47,10 @@ export class ProviderInstanceManager {
|
|
|
47
47
|
this.instances.get(id)!.dispose();
|
|
48
48
|
}
|
|
49
49
|
this.instances.set(id, instance);
|
|
50
|
-
await instance.init(
|
|
50
|
+
await instance.init({
|
|
51
|
+
...context,
|
|
52
|
+
emitProviderEvent: (event) => this.emitProviderEvent(instance.type, id, event),
|
|
53
|
+
});
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
/**
|
|
@@ -237,6 +240,22 @@ export class ProviderInstanceManager {
|
|
|
237
240
|
this.eventListeners.push(listener);
|
|
238
241
|
}
|
|
239
242
|
|
|
243
|
+
emitProviderEvent(providerType: string, instanceId: string, event: ProviderEvent): void {
|
|
244
|
+
const payload = {
|
|
245
|
+
...event,
|
|
246
|
+
providerType,
|
|
247
|
+
instanceId: typeof event.instanceId === 'string' && event.instanceId.trim()
|
|
248
|
+
? event.instanceId
|
|
249
|
+
: instanceId,
|
|
250
|
+
targetSessionId: typeof event.targetSessionId === 'string' && event.targetSessionId.trim()
|
|
251
|
+
? event.targetSessionId
|
|
252
|
+
: instanceId,
|
|
253
|
+
} as ProviderEvent & { providerType: string };
|
|
254
|
+
for (const listener of this.eventListeners) {
|
|
255
|
+
listener(payload);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
240
259
|
private emitPendingEvents(
|
|
241
260
|
providerType: string,
|
|
242
261
|
state: ProviderState,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ProviderModule, ProviderSettingDef, ProviderResumeCapability } from './contracts.js';
|
|
12
12
|
import type { AcpConfigOption, AcpMode, ProviderControlSchema, ProviderSummaryMetadata, SessionCapability } from '../shared-types.js';
|
|
13
|
+
import type { MessageInputSupport } from './provider-input-support.js';
|
|
13
14
|
import type { ChatMessage } from '../types.js';
|
|
14
15
|
|
|
15
16
|
// ─── ProviderState — Discriminated union by category ─────────────
|
|
@@ -87,6 +88,7 @@ interface ProviderStateBase {
|
|
|
87
88
|
runtime?: ProviderRuntimeInfo;
|
|
88
89
|
resume?: ProviderResumeCapability;
|
|
89
90
|
sessionCapabilities?: SessionCapability[];
|
|
91
|
+
messageInput?: MessageInputSupport;
|
|
90
92
|
/** Dynamic control current values */
|
|
91
93
|
controlValues?: Record<string, string | number | boolean>;
|
|
92
94
|
/** Provider-declared controls schema (from provider.controls) */
|
|
@@ -174,6 +176,8 @@ export interface InstanceContext {
|
|
|
174
176
|
onPtyData?: (data: string) => void;
|
|
175
177
|
/** Provider configvalue (resolved) */
|
|
176
178
|
settings: Record<string, any>;
|
|
179
|
+
/** Immediate provider-originated status/event emission. Used to avoid waiting for status polling. */
|
|
180
|
+
emitProviderEvent?: (event: ProviderEvent) => void;
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
export interface ProviderInstance {
|
|
@@ -2,6 +2,7 @@ import type { ProviderControlDef, ProviderControlType, ProviderModule } from './
|
|
|
2
2
|
import { providerHasOpenPanelSupport } from './open-panel-support.js'
|
|
3
3
|
|
|
4
4
|
const VALID_CAPABILITY_MEDIA_TYPES = new Set(['text', 'image', 'audio', 'video', 'resource'])
|
|
5
|
+
const VALID_INPUT_STRATEGIES = new Set(['native', 'native_acp', 'resource_link', 'text_fallback', 'paste', 'upload'])
|
|
5
6
|
|
|
6
7
|
const KNOWN_PROVIDER_FIELDS = new Set<string>([
|
|
7
8
|
'type',
|
|
@@ -158,16 +159,45 @@ function validateCapabilities(provider: ProviderModule, controls: ProviderContro
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
const input = capabilities.input
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (typeof input.multipart !== 'boolean') {
|
|
162
|
+
if (input !== undefined) {
|
|
163
|
+
if (!input || typeof input !== 'object') {
|
|
164
|
+
errors.push('capabilities.input must be an object when provided')
|
|
165
|
+
} else if (typeof input.multipart !== 'boolean') {
|
|
165
166
|
errors.push('capabilities.input.multipart must be boolean')
|
|
166
167
|
}
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
if (input && typeof input === 'object') {
|
|
169
|
+
const mediaTypes = Array.isArray(input.mediaTypes) ? input.mediaTypes : undefined
|
|
170
|
+
if (!mediaTypes || mediaTypes.length === 0) {
|
|
171
|
+
errors.push('capabilities.input.mediaTypes must be a non-empty array')
|
|
172
|
+
} else if (mediaTypes.some((type) => typeof type !== 'string' || !VALID_CAPABILITY_MEDIA_TYPES.has(type))) {
|
|
173
|
+
errors.push(`capabilities.input.mediaTypes must only include: ${Array.from(VALID_CAPABILITY_MEDIA_TYPES).join(', ')}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (input && typeof input === 'object' && input.strategies !== undefined) {
|
|
177
|
+
if (!Array.isArray(input.strategies)) {
|
|
178
|
+
errors.push('capabilities.input.strategies must be an array when provided')
|
|
179
|
+
} else {
|
|
180
|
+
for (const strategy of input.strategies) {
|
|
181
|
+
if (!strategy || typeof strategy !== 'object' || Array.isArray(strategy)) {
|
|
182
|
+
errors.push('capabilities.input.strategies entries must be objects')
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
const entry = strategy as Record<string, unknown>
|
|
186
|
+
if (typeof entry.mediaType !== 'string' || !VALID_CAPABILITY_MEDIA_TYPES.has(entry.mediaType)) {
|
|
187
|
+
errors.push(`capabilities.input.strategies.mediaType must only include: ${Array.from(VALID_CAPABILITY_MEDIA_TYPES).join(', ')}`)
|
|
188
|
+
}
|
|
189
|
+
for (const field of ['strategies', 'degradation'] as const) {
|
|
190
|
+
const values = entry[field]
|
|
191
|
+
if (values === undefined) continue
|
|
192
|
+
if (!Array.isArray(values) || values.some((value) => typeof value !== 'string' || !VALID_INPUT_STRATEGIES.has(value))) {
|
|
193
|
+
errors.push(`capabilities.input.strategies.${field} must only include: ${Array.from(VALID_INPUT_STRATEGIES).join(', ')}`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (entry.native !== undefined && typeof entry.native !== 'boolean') {
|
|
197
|
+
errors.push('capabilities.input.strategies.native must be boolean when provided')
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
171
201
|
}
|
|
172
202
|
}
|
|
173
203
|
|
|
@@ -77,6 +77,14 @@ function validateMessage(message: unknown, source: string, index: number): ChatM
|
|
|
77
77
|
if (typeof message.senderName === 'string') normalized.senderName = message.senderName
|
|
78
78
|
if (typeof (message as any)._type === 'string') normalized._type = (message as any)._type
|
|
79
79
|
if (typeof (message as any)._sub === 'string') normalized._sub = (message as any)._sub
|
|
80
|
+
if (typeof (message as any).visibility === 'string') normalized.visibility = (message as any).visibility
|
|
81
|
+
if (typeof (message as any).transcriptVisibility === 'string') normalized.transcriptVisibility = (message as any).transcriptVisibility
|
|
82
|
+
if (typeof (message as any).audience === 'string') normalized.audience = (message as any).audience
|
|
83
|
+
if (typeof (message as any).source === 'string') normalized.source = (message as any).source
|
|
84
|
+
if (typeof (message as any).userFacing === 'boolean') normalized.userFacing = (message as any).userFacing
|
|
85
|
+
if (typeof (message as any).internal === 'boolean') normalized.internal = (message as any).internal
|
|
86
|
+
if (typeof (message as any).isInternal === 'boolean') normalized.isInternal = (message as any).isInternal
|
|
87
|
+
if (typeof (message as any).debug === 'boolean') normalized.debug = (message as any).debug
|
|
80
88
|
|
|
81
89
|
return normalized
|
|
82
90
|
}
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -55,6 +55,9 @@ export type RepoMeshNodeHealth =
|
|
|
55
55
|
|
|
56
56
|
// ─── Policy Types ───────────────────────────────
|
|
57
57
|
|
|
58
|
+
export type RepoMeshSessionCleanupMode = 'preserve' | 'stop' | 'delete_stopped' | 'stop_and_delete';
|
|
59
|
+
export type RepoMeshSpawnedSessionVisibility = 'visible' | 'hidden';
|
|
60
|
+
|
|
58
61
|
export interface RepoMeshPolicy {
|
|
59
62
|
requirePreTaskCheckpoint: boolean;
|
|
60
63
|
requirePostTaskCheckpoint: boolean;
|
|
@@ -63,12 +66,39 @@ export interface RepoMeshPolicy {
|
|
|
63
66
|
dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
|
|
64
67
|
maxParallelTasks: number;
|
|
65
68
|
allowedProviders?: string[];
|
|
69
|
+
/**
|
|
70
|
+
* Whether sessions spawned by mesh/coordinator policy should auto-open as visible
|
|
71
|
+
* dashboard tabs or start hidden. Defaults to 'visible' to preserve existing
|
|
72
|
+
* watch-the-agents behavior; hidden sessions remain discoverable and manually openable.
|
|
73
|
+
*/
|
|
74
|
+
spawnedSessionVisibility?: RepoMeshSpawnedSessionVisibility;
|
|
75
|
+
/**
|
|
76
|
+
* What to do with delegated session-host records for a node when it is removed.
|
|
77
|
+
* Defaults to 'preserve' so completed work can be reviewed later and live
|
|
78
|
+
* runtimes are never stopped/deleted unless the mesh owner opts in.
|
|
79
|
+
*/
|
|
80
|
+
sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RepoMeshRelatedRepo {
|
|
84
|
+
/** Stable display label for an explicitly configured associated checkout. */
|
|
85
|
+
label: string;
|
|
86
|
+
/** Absolute checkout/workspace path for git freshness probes. */
|
|
87
|
+
workspace: string;
|
|
66
88
|
}
|
|
67
89
|
|
|
68
90
|
export interface RepoMeshNodePolicy {
|
|
69
91
|
readOnly?: boolean;
|
|
70
92
|
canPush?: boolean;
|
|
71
93
|
maxConcurrentSessions?: number;
|
|
94
|
+
/** Ordered provider preference used when mesh_launch_session omits an explicit type. */
|
|
95
|
+
providerPriority?: string[];
|
|
96
|
+
/**
|
|
97
|
+
* Optional associated/external repos that must be checked alongside this node.
|
|
98
|
+
* These are explicit policy/config entries only; Repo Mesh does not auto-discover
|
|
99
|
+
* sibling paths so freshness checks stay fail-closed and non-surprising.
|
|
100
|
+
*/
|
|
101
|
+
relatedRepos?: RepoMeshRelatedRepo[];
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
|
|
@@ -78,6 +108,8 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
|
|
|
78
108
|
requireApprovalForDestructiveGit: true,
|
|
79
109
|
dirtyWorkspaceBehavior: 'warn',
|
|
80
110
|
maxParallelTasks: 2,
|
|
111
|
+
spawnedSessionVisibility: 'visible',
|
|
112
|
+
sessionCleanupOnNodeRemove: 'preserve',
|
|
81
113
|
};
|
|
82
114
|
|
|
83
115
|
// ─── Capabilities ───────────────────────────────
|
|
@@ -189,6 +221,12 @@ export interface LocalMeshNodeEntry {
|
|
|
189
221
|
policy: RepoMeshNodePolicy;
|
|
190
222
|
/** For single-machine mesh: same daemon, different worktree */
|
|
191
223
|
isLocalWorktree?: boolean;
|
|
224
|
+
/** Branch this worktree tracks (set when created via clone_mesh_node) */
|
|
225
|
+
worktreeBranch?: string;
|
|
226
|
+
/** Node ID this worktree was cloned from */
|
|
227
|
+
clonedFromNodeId?: string;
|
|
228
|
+
/** Optional associated/external repos configured as node metadata. */
|
|
229
|
+
relatedRepos?: RepoMeshRelatedRepo[];
|
|
192
230
|
}
|
|
193
231
|
|
|
194
232
|
// ─── Mesh Status (runtime, not persisted) ───────
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SessionHostClient,
|
|
3
3
|
getDefaultSessionHostEndpoint,
|
|
4
|
+
type SessionHostDiagnostics,
|
|
4
5
|
type SessionHostEndpoint,
|
|
5
6
|
type SessionHostRecord,
|
|
7
|
+
type SessionHostRequestType,
|
|
6
8
|
} from '@adhdev/session-host-core';
|
|
7
9
|
import type { HostedCliRuntimeDescriptor } from '../commands/cli-manager.js';
|
|
8
10
|
import { DEFAULT_SESSION_HOST_READY_TIMEOUT_MS } from '../runtime-defaults.js';
|
|
@@ -10,21 +12,65 @@ import { DEFAULT_SESSION_HOST_READY_TIMEOUT_MS } from '../runtime-defaults.js';
|
|
|
10
12
|
const STARTUP_TIMEOUT_MS = DEFAULT_SESSION_HOST_READY_TIMEOUT_MS;
|
|
11
13
|
const STARTUP_POLL_MS = 200;
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
class SessionHostCompatibilityError extends Error {
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'SessionHostCompatibilityError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getMissingRequestTypes(
|
|
23
|
+
diagnostics: SessionHostDiagnostics | undefined,
|
|
24
|
+
requiredRequestTypes: readonly SessionHostRequestType[],
|
|
25
|
+
): SessionHostRequestType[] {
|
|
26
|
+
const supported = new Set(diagnostics?.supportedRequestTypes || []);
|
|
27
|
+
return requiredRequestTypes.filter((requestType) => !supported.has(requestType));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function assertRequiredRequestTypes(
|
|
31
|
+
client: SessionHostClient,
|
|
32
|
+
requiredRequestTypes: readonly SessionHostRequestType[],
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (requiredRequestTypes.length === 0) return;
|
|
35
|
+
|
|
36
|
+
const response = await client.request<SessionHostDiagnostics>({
|
|
37
|
+
type: 'get_host_diagnostics',
|
|
38
|
+
payload: { includeSessions: false },
|
|
39
|
+
});
|
|
40
|
+
const missing = getMissingRequestTypes(response.success ? response.result : undefined, requiredRequestTypes);
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
const detail = response.success ? '' : ` (${response.error || 'capability probe failed'})`;
|
|
43
|
+
throw new SessionHostCompatibilityError(
|
|
44
|
+
`Session host does not support required request types: ${missing.join(', ')}${detail}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function canConnect(
|
|
50
|
+
endpoint: SessionHostEndpoint,
|
|
51
|
+
requiredRequestTypes: readonly SessionHostRequestType[] = [],
|
|
52
|
+
): Promise<boolean> {
|
|
14
53
|
const client = new SessionHostClient({ endpoint });
|
|
15
54
|
try {
|
|
16
55
|
await client.connect();
|
|
17
|
-
await client
|
|
56
|
+
await assertRequiredRequestTypes(client, requiredRequestTypes);
|
|
18
57
|
return true;
|
|
19
|
-
} catch {
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof SessionHostCompatibilityError) throw error;
|
|
20
60
|
return false;
|
|
61
|
+
} finally {
|
|
62
|
+
await client.close().catch(() => {});
|
|
21
63
|
}
|
|
22
64
|
}
|
|
23
65
|
|
|
24
|
-
async function waitForReady(
|
|
66
|
+
async function waitForReady(
|
|
67
|
+
endpoint: SessionHostEndpoint,
|
|
68
|
+
timeoutMs = STARTUP_TIMEOUT_MS,
|
|
69
|
+
requiredRequestTypes: readonly SessionHostRequestType[] = [],
|
|
70
|
+
): Promise<void> {
|
|
25
71
|
const deadline = Date.now() + timeoutMs;
|
|
26
72
|
while (Date.now() < deadline) {
|
|
27
|
-
if (await canConnect(endpoint)) return;
|
|
73
|
+
if (await canConnect(endpoint, requiredRequestTypes)) return;
|
|
28
74
|
await new Promise((resolve) => setTimeout(resolve, STARTUP_POLL_MS));
|
|
29
75
|
}
|
|
30
76
|
throw new Error(`Session host did not become ready within ${timeoutMs}ms`);
|
|
@@ -34,11 +80,13 @@ export async function ensureSessionHostReady(options: {
|
|
|
34
80
|
appName?: string;
|
|
35
81
|
spawnHost: () => void;
|
|
36
82
|
timeoutMs?: number;
|
|
83
|
+
requiredRequestTypes?: readonly SessionHostRequestType[];
|
|
37
84
|
}): Promise<SessionHostEndpoint> {
|
|
38
85
|
const endpoint = getDefaultSessionHostEndpoint(options.appName || 'adhdev');
|
|
39
|
-
|
|
86
|
+
const requiredRequestTypes = options.requiredRequestTypes || [];
|
|
87
|
+
if (await canConnect(endpoint, requiredRequestTypes)) return endpoint;
|
|
40
88
|
options.spawnHost();
|
|
41
|
-
await waitForReady(endpoint, options.timeoutMs);
|
|
89
|
+
await waitForReady(endpoint, options.timeoutMs, requiredRequestTypes);
|
|
42
90
|
return endpoint;
|
|
43
91
|
}
|
|
44
92
|
|
package/src/shared-types.ts
CHANGED
|
@@ -336,7 +336,9 @@ export type SessionCapability =
|
|
|
336
336
|
| 'mark_notification_unread';
|
|
337
337
|
|
|
338
338
|
import type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
|
|
339
|
+
import type { MessageInputSupport } from './providers/provider-input-support.js';
|
|
339
340
|
export type { RuntimeWriteOwner, RuntimeAttachedClient, SessionStatus } from './shared-types-extra.js';
|
|
341
|
+
export type { MessageInputSupport, InputMediaStrategyDescriptor, InputAttachmentStrategy, InputMediaType } from './providers/provider-input-support.js';
|
|
340
342
|
|
|
341
343
|
export interface SessionEntry {
|
|
342
344
|
id: string;
|
|
@@ -364,6 +366,8 @@ export interface SessionEntry {
|
|
|
364
366
|
resume?: ProviderResumeCapability;
|
|
365
367
|
activeChat: SessionActiveChatData | null;
|
|
366
368
|
capabilities?: SessionCapability[];
|
|
369
|
+
/** Effective message input/media support for this session. Defaults fail-closed to text-only. */
|
|
370
|
+
messageInput?: MessageInputSupport;
|
|
367
371
|
cdpConnected?: boolean;
|
|
368
372
|
/** Dynamic control current values (generic key-value) */
|
|
369
373
|
controlValues?: Record<string, string | number | boolean>;
|
|
@@ -641,6 +645,8 @@ export interface DaemonStatusEventPayload {
|
|
|
641
645
|
timestamp: number;
|
|
642
646
|
targetSessionId?: string;
|
|
643
647
|
providerType?: string;
|
|
648
|
+
providerSessionId?: string;
|
|
649
|
+
workspaceName?: string;
|
|
644
650
|
duration?: number;
|
|
645
651
|
elapsedSec?: number;
|
|
646
652
|
modalMessage?: string;
|
|
@@ -662,6 +668,8 @@ export interface DashboardStatusEventPayload {
|
|
|
662
668
|
daemonId?: string;
|
|
663
669
|
providerType?: string;
|
|
664
670
|
targetSessionId?: string;
|
|
671
|
+
providerSessionId?: string;
|
|
672
|
+
workspaceName?: string;
|
|
665
673
|
duration?: number;
|
|
666
674
|
elapsedSec?: number;
|
|
667
675
|
modalMessage?: string;
|