@swarmclawai/swarmclaw 1.5.35 → 1.5.37
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/README.md +29 -1
- package/package.json +18 -1
- package/public/provider-logos/droid-cli.svg +7 -0
- package/src/app/api/setup/check-provider/route.ts +4 -2
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/components/agents/agent-sheet.tsx +3 -1
- package/src/components/agents/inspector-panel.tsx +1 -0
- package/src/components/auth/access-key-gate.tsx +0 -24
- package/src/components/chat/activity-moment.tsx +1 -0
- package/src/components/chat/chat-header.tsx +2 -1
- package/src/components/chat/tool-call-bubble.tsx +5 -0
- package/src/components/layout/sidebar-rail.tsx +0 -47
- package/src/lib/orchestrator-config.ts +1 -0
- package/src/lib/provider-sets.ts +3 -3
- package/src/lib/providers/cli-utils.test.ts +2 -0
- package/src/lib/providers/cli-utils.ts +28 -1
- package/src/lib/providers/droid-cli.ts +220 -0
- package/src/lib/providers/index.ts +11 -1
- package/src/lib/server/agents/agent-availability.test.ts +1 -1
- package/src/lib/server/agents/agent-thread-session.ts +1 -0
- package/src/lib/server/agents/task-session.ts +2 -0
- package/src/lib/server/capability-router.ts +3 -1
- package/src/lib/server/chat-execution/chat-execution-utils.ts +11 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +2 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +1 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +2 -0
- package/src/lib/server/chatrooms/chatroom-helpers.ts +3 -0
- package/src/lib/server/chats/chat-session-service.ts +4 -0
- package/src/lib/server/connectors/session.ts +2 -0
- package/src/lib/server/context-manager.ts +1 -0
- package/src/lib/server/provider-health.ts +4 -2
- package/src/lib/server/provider-model-discovery.test.ts +1 -1
- package/src/lib/server/provider-model-discovery.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +2 -2
- package/src/lib/server/session-reset-policy.ts +2 -0
- package/src/lib/server/session-tools/context.ts +2 -2
- package/src/lib/server/session-tools/delegate-droid.test.ts +24 -0
- package/src/lib/server/session-tools/delegate.ts +105 -12
- package/src/lib/server/session-tools/index.ts +3 -2
- package/src/lib/server/session-tools/session-info.ts +1 -0
- package/src/lib/server/storage-normalization.ts +3 -0
- package/src/lib/server/tool-aliases.ts +1 -1
- package/src/lib/server/tool-capability-policy.ts +2 -1
- package/src/lib/setup-defaults.ts +21 -0
- package/src/types/misc.ts +1 -1
- package/src/types/provider.ts +1 -1
- package/src/types/session.ts +3 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { spawn } from 'child_process'
|
|
5
|
+
import type { StreamChatOptions } from './index'
|
|
6
|
+
import { log } from '../server/logger'
|
|
7
|
+
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
8
|
+
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles, isStderrNoise } from './cli-utils'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Factory Droid CLI provider — spawns `droid exec <message> --output-format stream-json`.
|
|
12
|
+
* Tracks `session.droidSessionId` from streamed events to support multi-turn continuity.
|
|
13
|
+
*/
|
|
14
|
+
export function streamDroidCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
|
|
15
|
+
const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
|
|
16
|
+
const binary = resolveCliBinary('droid')
|
|
17
|
+
if (!binary) {
|
|
18
|
+
const msg = 'Factory Droid CLI not found. Install it (brew install --cask droid, npm i -g droid, or https://docs.factory.ai/cli/getting-started/quickstart) and ensure it is on your PATH.'
|
|
19
|
+
write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
|
|
20
|
+
return Promise.resolve('')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const env = buildCliEnv()
|
|
24
|
+
|
|
25
|
+
if (session.apiKey) {
|
|
26
|
+
env.FACTORY_API_KEY = session.apiKey
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!session.apiKey) {
|
|
30
|
+
const auth = probeCliAuth(binary, 'droid', env, session.cwd)
|
|
31
|
+
if (!auth.authenticated) {
|
|
32
|
+
log.error('droid-cli', auth.errorMessage || 'Auth failed')
|
|
33
|
+
write(`data: ${JSON.stringify({ t: 'err', text: auth.errorMessage || 'Factory Droid CLI is not authenticated.' })}\n\n`)
|
|
34
|
+
return Promise.resolve('')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const promptParts: string[] = []
|
|
39
|
+
if (imagePath) {
|
|
40
|
+
promptParts.push(`[The user has shared an image at: ${imagePath}]`)
|
|
41
|
+
}
|
|
42
|
+
promptParts.push(message)
|
|
43
|
+
const prompt = promptParts.join('\n\n')
|
|
44
|
+
|
|
45
|
+
const args = ['exec', prompt, '--output-format', 'stream-json']
|
|
46
|
+
if (session.droidSessionId) args.push('-s', session.droidSessionId)
|
|
47
|
+
if (session.model) args.push('-m', session.model)
|
|
48
|
+
|
|
49
|
+
let tempFactoryHome: string | null = null
|
|
50
|
+
if (systemPrompt && !session.droidSessionId) {
|
|
51
|
+
const realFactoryHome = process.env.FACTORY_HOME || path.join(os.homedir(), '.factory')
|
|
52
|
+
tempFactoryHome = path.join(os.tmpdir(), `swarmclaw-droid-${session.id}`)
|
|
53
|
+
fs.mkdirSync(tempFactoryHome, { recursive: true })
|
|
54
|
+
symlinkConfigFiles(realFactoryHome, tempFactoryHome)
|
|
55
|
+
fs.writeFileSync(path.join(tempFactoryHome, 'AGENTS.override.md'), systemPrompt)
|
|
56
|
+
env.FACTORY_HOME = tempFactoryHome
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
log.info('droid-cli', `Spawning: ${binary}`, {
|
|
60
|
+
args: args.map((a) => a.length > 100 ? a.slice(0, 100) + '...' : a),
|
|
61
|
+
cwd: session.cwd,
|
|
62
|
+
promptLen: prompt.length,
|
|
63
|
+
hasSystemPrompt: !!systemPrompt,
|
|
64
|
+
resumeSessionId: session.droidSessionId || null,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const proc = spawn(binary, args, {
|
|
68
|
+
cwd: session.cwd,
|
|
69
|
+
env,
|
|
70
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
71
|
+
timeout: processTimeoutMs,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
log.info('droid-cli', `Process spawned: pid=${proc.pid}`)
|
|
75
|
+
active.set(session.id, proc)
|
|
76
|
+
attachAbortHandler(proc, signal)
|
|
77
|
+
|
|
78
|
+
let fullResponse = ''
|
|
79
|
+
let buf = ''
|
|
80
|
+
let eventCount = 0
|
|
81
|
+
let stderrText = ''
|
|
82
|
+
|
|
83
|
+
proc.stdout!.on('data', (chunk: Buffer) => {
|
|
84
|
+
const raw = chunk.toString()
|
|
85
|
+
buf += raw
|
|
86
|
+
|
|
87
|
+
if (eventCount === 0) {
|
|
88
|
+
log.debug('droid-cli', `First stdout chunk (${raw.length} bytes)`, raw.slice(0, 500))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lines = buf.split('\n')
|
|
92
|
+
buf = lines.pop()!
|
|
93
|
+
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
if (!line.trim()) continue
|
|
96
|
+
try {
|
|
97
|
+
const ev = JSON.parse(line) as Record<string, unknown>
|
|
98
|
+
eventCount++
|
|
99
|
+
|
|
100
|
+
const data = ev.data as Record<string, unknown> | undefined
|
|
101
|
+
|
|
102
|
+
if (typeof ev.session_id === 'string') {
|
|
103
|
+
session.droidSessionId = ev.session_id
|
|
104
|
+
} else if (typeof ev.sessionId === 'string') {
|
|
105
|
+
session.droidSessionId = ev.sessionId
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (ev.type === 'assistant.message_delta' && typeof data?.deltaContent === 'string') {
|
|
109
|
+
fullResponse += data.deltaContent
|
|
110
|
+
write(`data: ${JSON.stringify({ t: 'd', text: data.deltaContent })}\n\n`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
else if (ev.type === 'assistant.message' && typeof data?.content === 'string') {
|
|
114
|
+
if (!fullResponse) {
|
|
115
|
+
fullResponse = data.content
|
|
116
|
+
write(`data: ${JSON.stringify({ t: 'r', text: data.content })}\n\n`)
|
|
117
|
+
}
|
|
118
|
+
log.debug('droid-cli', `Assistant message (${data.content.length} chars)`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
else if (ev.type === 'content_block_delta') {
|
|
122
|
+
const delta = ev.delta as Record<string, unknown> | undefined
|
|
123
|
+
if (typeof delta?.text === 'string') {
|
|
124
|
+
fullResponse += delta.text
|
|
125
|
+
write(`data: ${JSON.stringify({ t: 'd', text: delta.text })}\n\n`)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
else if (ev.type === 'agent_message_chunk' && typeof ev.text === 'string') {
|
|
130
|
+
fullResponse += ev.text
|
|
131
|
+
write(`data: ${JSON.stringify({ t: 'd', text: ev.text })}\n\n`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
else if (ev.type === 'message' && ev.role === 'assistant' && typeof ev.content === 'string') {
|
|
135
|
+
fullResponse += ev.content
|
|
136
|
+
write(`data: ${JSON.stringify({ t: 'd', text: ev.content })}\n\n`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
else if (ev.type === 'item.completed' && (ev.item as Record<string, unknown>)?.type === 'agent_message') {
|
|
140
|
+
const item = ev.item as Record<string, unknown>
|
|
141
|
+
if (typeof item.text === 'string') {
|
|
142
|
+
fullResponse = item.text
|
|
143
|
+
write(`data: ${JSON.stringify({ t: 'r', text: item.text })}\n\n`)
|
|
144
|
+
log.debug('droid-cli', `Agent message (${item.text.length} chars)`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
else if (ev.type === 'result' && typeof ev.result === 'string') {
|
|
149
|
+
fullResponse = ev.result
|
|
150
|
+
write(`data: ${JSON.stringify({ t: 'r', text: ev.result })}\n\n`)
|
|
151
|
+
log.debug('droid-cli', `Result event (${ev.result.length} chars)`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
else if (ev.type === 'result' && ev.status === 'error') {
|
|
155
|
+
const errMsg = typeof ev.error === 'string' ? ev.error : 'Droid error'
|
|
156
|
+
write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
|
|
157
|
+
log.warn('droid-cli', `Error result: ${errMsg}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
else if (ev.type === 'error') {
|
|
161
|
+
const errMsg = typeof ev.message === 'string'
|
|
162
|
+
? ev.message
|
|
163
|
+
: typeof ev.error === 'string'
|
|
164
|
+
? ev.error
|
|
165
|
+
: 'Unknown Droid error'
|
|
166
|
+
write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
|
|
167
|
+
log.warn('droid-cli', `Event error: ${errMsg}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
else if (eventCount <= 10) {
|
|
171
|
+
log.debug('droid-cli', `Event: ${String(ev.type)}`)
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
if (line.trim()) {
|
|
175
|
+
log.debug('droid-cli', `Non-JSON stdout line`, line.slice(0, 300))
|
|
176
|
+
fullResponse += line + '\n'
|
|
177
|
+
write(`data: ${JSON.stringify({ t: 'd', text: line + '\n' })}\n\n`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
proc.stderr!.on('data', (chunk: Buffer) => {
|
|
184
|
+
const text = chunk.toString()
|
|
185
|
+
stderrText += text
|
|
186
|
+
if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
|
|
187
|
+
if (isStderrNoise(text)) {
|
|
188
|
+
log.debug('droid-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
|
|
189
|
+
} else {
|
|
190
|
+
log.warn('droid-cli', `stderr [${session.id}]`, text.slice(0, 500))
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
proc.on('close', (code, sig) => {
|
|
196
|
+
log.info('droid-cli', `Process closed: code=${code} signal=${sig} events=${eventCount} response=${fullResponse.length}chars`)
|
|
197
|
+
active.delete(session.id)
|
|
198
|
+
if (tempFactoryHome) {
|
|
199
|
+
try { fs.rmSync(tempFactoryHome, { recursive: true }) } catch { /* ignore */ }
|
|
200
|
+
}
|
|
201
|
+
if ((code ?? 0) !== 0 && !fullResponse.trim()) {
|
|
202
|
+
const msg = stderrText.trim()
|
|
203
|
+
? `Factory Droid CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''}: ${stderrText.trim().slice(0, 1200)}`
|
|
204
|
+
: `Factory Droid CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''} and returned no output.`
|
|
205
|
+
write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
|
|
206
|
+
}
|
|
207
|
+
resolve(fullResponse)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
proc.on('error', (e) => {
|
|
211
|
+
log.error('droid-cli', `Process error: ${e.message}`)
|
|
212
|
+
active.delete(session.id)
|
|
213
|
+
if (tempFactoryHome) {
|
|
214
|
+
try { fs.rmSync(tempFactoryHome, { recursive: true }) } catch { /* ignore */ }
|
|
215
|
+
}
|
|
216
|
+
write(`data: ${JSON.stringify({ t: 'err', text: e.message })}\n\n`)
|
|
217
|
+
resolve(fullResponse)
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
}
|
|
@@ -3,6 +3,7 @@ import { streamCodexCliChat } from './codex-cli'
|
|
|
3
3
|
import { streamOpenCodeCliChat } from './opencode-cli'
|
|
4
4
|
import { streamGeminiCliChat } from './gemini-cli'
|
|
5
5
|
import { streamCopilotCliChat } from './copilot-cli'
|
|
6
|
+
import { streamDroidCliChat } from './droid-cli'
|
|
6
7
|
import { streamCursorCliChat } from './cursor-cli'
|
|
7
8
|
import { streamQwenCodeCliChat } from './qwen-code-cli'
|
|
8
9
|
import { streamGooseChat } from './goose'
|
|
@@ -151,6 +152,15 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
151
152
|
requiresEndpoint: false,
|
|
152
153
|
handler: { streamChat: streamCopilotCliChat },
|
|
153
154
|
},
|
|
155
|
+
'droid-cli': {
|
|
156
|
+
id: 'droid-cli',
|
|
157
|
+
name: 'Factory Droid CLI',
|
|
158
|
+
models: ['default'],
|
|
159
|
+
requiresApiKey: false,
|
|
160
|
+
optionalApiKey: true,
|
|
161
|
+
requiresEndpoint: false,
|
|
162
|
+
handler: { streamChat: streamDroidCliChat },
|
|
163
|
+
},
|
|
154
164
|
'cursor-cli': {
|
|
155
165
|
id: 'cursor-cli',
|
|
156
166
|
name: 'Cursor Agent CLI',
|
|
@@ -383,7 +393,7 @@ export function getProviderList(): ProviderInfo[] {
|
|
|
383
393
|
...info,
|
|
384
394
|
models: overrides[info.id] || info.models,
|
|
385
395
|
defaultModels: info.models,
|
|
386
|
-
supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'fireworks'].includes(info.id),
|
|
396
|
+
supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'fireworks'].includes(info.id),
|
|
387
397
|
}
|
|
388
398
|
})
|
|
389
399
|
|
|
@@ -4,7 +4,7 @@ import type { Agent, ProviderType } from '@/types'
|
|
|
4
4
|
import { isWorkerOnlyAgent, buildWorkerOnlyAgentMessage } from './agent-availability'
|
|
5
5
|
|
|
6
6
|
describe('isWorkerOnlyAgent', () => {
|
|
7
|
-
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw'] satisfies ProviderType[]
|
|
7
|
+
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'openclaw'] satisfies ProviderType[]
|
|
8
8
|
const NON_CLI_PROVIDERS = ['openai', 'anthropic', 'google', 'deepseek', 'groq', 'together'] satisfies ProviderType[]
|
|
9
9
|
|
|
10
10
|
function withProvider(provider: unknown): Pick<Agent, 'provider'> {
|
|
@@ -45,6 +45,7 @@ function buildThreadSession(agent: Agent, sessionId: string, user: string, creat
|
|
|
45
45
|
opencodeSessionId: existing?.opencodeSessionId || null,
|
|
46
46
|
geminiSessionId: existing?.geminiSessionId || null,
|
|
47
47
|
copilotSessionId: existing?.copilotSessionId || null,
|
|
48
|
+
droidSessionId: existing?.droidSessionId || null,
|
|
48
49
|
cursorSessionId: existing?.cursorSessionId || null,
|
|
49
50
|
qwenSessionId: existing?.qwenSessionId || null,
|
|
50
51
|
acpSessionId: existing?.acpSessionId || null,
|
|
@@ -42,6 +42,7 @@ export function createAgentTaskSession(
|
|
|
42
42
|
opencodeSessionId: null,
|
|
43
43
|
geminiSessionId: null,
|
|
44
44
|
copilotSessionId: null,
|
|
45
|
+
droidSessionId: null,
|
|
45
46
|
cursorSessionId: null,
|
|
46
47
|
qwenSessionId: null,
|
|
47
48
|
acpSessionId: null,
|
|
@@ -51,6 +52,7 @@ export function createAgentTaskSession(
|
|
|
51
52
|
opencode: null,
|
|
52
53
|
gemini: null,
|
|
53
54
|
copilot: null,
|
|
55
|
+
droid: null,
|
|
54
56
|
cursor: null,
|
|
55
57
|
qwen: null,
|
|
56
58
|
},
|
|
@@ -19,7 +19,7 @@ export interface CapabilityRoutingDecision {
|
|
|
19
19
|
primaryUrl?: string
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli' | 'delegate_to_copilot_cli' | 'delegate_to_cursor_cli' | 'delegate_to_qwen_code_cli'
|
|
22
|
+
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli' | 'delegate_to_copilot_cli' | 'delegate_to_droid_cli' | 'delegate_to_cursor_cli' | 'delegate_to_qwen_code_cli'
|
|
23
23
|
|
|
24
24
|
function findFirstUrl(text: string): string | undefined {
|
|
25
25
|
const m = text.match(/https?:\/\/[^\s<>"')]+/i)
|
|
@@ -42,6 +42,7 @@ function normalizeDelegateOrder(value: unknown): DelegateTool[] {
|
|
|
42
42
|
'delegate_to_opencode_cli',
|
|
43
43
|
'delegate_to_gemini_cli',
|
|
44
44
|
'delegate_to_copilot_cli',
|
|
45
|
+
'delegate_to_droid_cli',
|
|
45
46
|
'delegate_to_cursor_cli',
|
|
46
47
|
'delegate_to_qwen_code_cli',
|
|
47
48
|
]
|
|
@@ -54,6 +55,7 @@ function normalizeDelegateOrder(value: unknown): DelegateTool[] {
|
|
|
54
55
|
else if (raw === 'opencode') mapped.push('delegate_to_opencode_cli')
|
|
55
56
|
else if (raw === 'gemini') mapped.push('delegate_to_gemini_cli')
|
|
56
57
|
else if (raw === 'copilot') mapped.push('delegate_to_copilot_cli')
|
|
58
|
+
else if (raw === 'droid') mapped.push('delegate_to_droid_cli')
|
|
57
59
|
else if (raw === 'cursor') mapped.push('delegate_to_cursor_cli')
|
|
58
60
|
else if (raw === 'qwen') mapped.push('delegate_to_qwen_code_cli')
|
|
59
61
|
}
|
|
@@ -31,6 +31,7 @@ export type DelegateTool =
|
|
|
31
31
|
| 'delegate_to_opencode_cli'
|
|
32
32
|
| 'delegate_to_gemini_cli'
|
|
33
33
|
| 'delegate_to_copilot_cli'
|
|
34
|
+
| 'delegate_to_droid_cli'
|
|
34
35
|
| 'delegate_to_cursor_cli'
|
|
35
36
|
| 'delegate_to_qwen_code_cli'
|
|
36
37
|
|
|
@@ -138,6 +139,12 @@ export function translateRequestedToolInvocation(
|
|
|
138
139
|
if (requestedName === 'delegate_to_gemini_cli') {
|
|
139
140
|
return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
|
|
140
141
|
}
|
|
142
|
+
if (requestedName === 'delegate_to_copilot_cli') {
|
|
143
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'copilot' } }
|
|
144
|
+
}
|
|
145
|
+
if (requestedName === 'delegate_to_droid_cli') {
|
|
146
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'droid' } }
|
|
147
|
+
}
|
|
141
148
|
if (requestedName === 'delegate_to_cursor_cli') {
|
|
142
149
|
return { toolName: 'delegate', args: { ...rawArgs, backend: 'cursor' } }
|
|
143
150
|
}
|
|
@@ -306,6 +313,8 @@ export function requestedToolNamesFromMessage(message: string): string[] {
|
|
|
306
313
|
'delegate_to_codex_cli',
|
|
307
314
|
'delegate_to_opencode_cli',
|
|
308
315
|
'delegate_to_gemini_cli',
|
|
316
|
+
'delegate_to_copilot_cli',
|
|
317
|
+
'delegate_to_droid_cli',
|
|
309
318
|
'delegate_to_cursor_cli',
|
|
310
319
|
'delegate_to_qwen_code_cli',
|
|
311
320
|
'connector_message_tool',
|
|
@@ -372,6 +381,8 @@ export function enabledDelegationTools(session: SessionWithTools): DelegateTool[
|
|
|
372
381
|
if (hasToolEnabled(session, 'codex_cli')) tools.push('delegate_to_codex_cli')
|
|
373
382
|
if (hasToolEnabled(session, 'opencode_cli')) tools.push('delegate_to_opencode_cli')
|
|
374
383
|
if (hasToolEnabled(session, 'gemini_cli')) tools.push('delegate_to_gemini_cli')
|
|
384
|
+
if (hasToolEnabled(session, 'copilot_cli')) tools.push('delegate_to_copilot_cli')
|
|
385
|
+
if (hasToolEnabled(session, 'droid_cli')) tools.push('delegate_to_droid_cli')
|
|
375
386
|
if (hasToolEnabled(session, 'cursor_cli')) tools.push('delegate_to_cursor_cli')
|
|
376
387
|
if (hasToolEnabled(session, 'qwen_code_cli')) tools.push('delegate_to_qwen_code_cli')
|
|
377
388
|
return tools
|
|
@@ -398,6 +398,7 @@ export async function finalizeChatTurn(params: {
|
|
|
398
398
|
persistField('opencodeSessionId', session.opencodeSessionId)
|
|
399
399
|
persistField('geminiSessionId', session.geminiSessionId)
|
|
400
400
|
persistField('copilotSessionId', session.copilotSessionId)
|
|
401
|
+
persistField('droidSessionId', session.droidSessionId)
|
|
401
402
|
persistField('cursorSessionId', session.cursorSessionId)
|
|
402
403
|
persistField('qwenSessionId', session.qwenSessionId)
|
|
403
404
|
persistField('acpSessionId', session.acpSessionId)
|
|
@@ -415,6 +416,7 @@ export async function finalizeChatTurn(params: {
|
|
|
415
416
|
opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
|
|
416
417
|
gemini: normalizeResumeId(sr.gemini ?? cr.gemini),
|
|
417
418
|
copilot: normalizeResumeId(sr.copilot ?? cr.copilot),
|
|
419
|
+
droid: normalizeResumeId(sr.droid ?? cr.droid),
|
|
418
420
|
cursor: normalizeResumeId(sr.cursor ?? cr.cursor),
|
|
419
421
|
qwen: normalizeResumeId(sr.qwen ?? cr.qwen),
|
|
420
422
|
}
|
|
@@ -120,6 +120,8 @@ function normalizeRuntimeExtensionId(extensionId: string): string {
|
|
|
120
120
|
if (normalized === 'delegate_to_codex_cli' || normalized === 'codex_cli') return 'codex_cli'
|
|
121
121
|
if (normalized === 'delegate_to_opencode_cli' || normalized === 'opencode_cli') return 'opencode_cli'
|
|
122
122
|
if (normalized === 'delegate_to_gemini_cli' || normalized === 'gemini_cli') return 'gemini_cli'
|
|
123
|
+
if (normalized === 'delegate_to_copilot_cli' || normalized === 'copilot_cli') return 'copilot_cli'
|
|
124
|
+
if (normalized === 'delegate_to_droid_cli' || normalized === 'droid_cli') return 'droid_cli'
|
|
123
125
|
if (normalized === 'delegate_to_cursor_cli' || normalized === 'cursor_cli') return 'cursor_cli'
|
|
124
126
|
if (normalized === 'delegate_to_qwen_code_cli' || normalized === 'qwen_code_cli') return 'qwen_code_cli'
|
|
125
127
|
if (['session_info', 'sessions_tool', 'whoami_tool', 'search_history_tool'].includes(normalized)) return 'manage_sessions'
|
|
@@ -282,6 +282,7 @@ function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']
|
|
|
282
282
|
opencode: null,
|
|
283
283
|
gemini: null,
|
|
284
284
|
copilot: null,
|
|
285
|
+
droid: null,
|
|
285
286
|
cursor: null,
|
|
286
287
|
qwen: null,
|
|
287
288
|
}
|
|
@@ -307,6 +308,7 @@ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session
|
|
|
307
308
|
opencodeSessionId: null,
|
|
308
309
|
geminiSessionId: null,
|
|
309
310
|
copilotSessionId: null,
|
|
311
|
+
droidSessionId: null,
|
|
310
312
|
cursorSessionId: null,
|
|
311
313
|
qwenSessionId: null,
|
|
312
314
|
acpSessionId: null,
|
|
@@ -362,6 +364,7 @@ export function ensureSyntheticSession(agent: Agent, chatroomId: string): Sessio
|
|
|
362
364
|
if (session.opencodeSessionId === undefined) session.opencodeSessionId = null
|
|
363
365
|
if (session.geminiSessionId === undefined) session.geminiSessionId = null
|
|
364
366
|
if (session.copilotSessionId === undefined) session.copilotSessionId = null
|
|
367
|
+
if (session.droidSessionId === undefined) session.droidSessionId = null
|
|
365
368
|
if (session.cursorSessionId === undefined) session.cursorSessionId = null
|
|
366
369
|
if (session.qwenSessionId === undefined) session.qwenSessionId = null
|
|
367
370
|
if (session.acpSessionId === undefined) session.acpSessionId = null
|
|
@@ -47,6 +47,7 @@ function emptyDelegateResumeIds() {
|
|
|
47
47
|
opencode: null,
|
|
48
48
|
gemini: null,
|
|
49
49
|
copilot: null,
|
|
50
|
+
droid: null,
|
|
50
51
|
cursor: null,
|
|
51
52
|
qwen: null,
|
|
52
53
|
}
|
|
@@ -129,6 +130,7 @@ export function createChatSession(input: Record<string, unknown>): ServiceResult
|
|
|
129
130
|
opencodeSessionId: null,
|
|
130
131
|
geminiSessionId: null,
|
|
131
132
|
copilotSessionId: null,
|
|
133
|
+
droidSessionId: null,
|
|
132
134
|
cursorSessionId: null,
|
|
133
135
|
qwenSessionId: null,
|
|
134
136
|
acpSessionId: null,
|
|
@@ -289,6 +291,7 @@ export function updateChatSession(sessionId: string, updates: Record<string, unk
|
|
|
289
291
|
if (updates.opencodeSessionId !== undefined) session.opencodeSessionId = updates.opencodeSessionId
|
|
290
292
|
if (updates.geminiSessionId !== undefined) session.geminiSessionId = updates.geminiSessionId
|
|
291
293
|
if (updates.copilotSessionId !== undefined) session.copilotSessionId = updates.copilotSessionId
|
|
294
|
+
if (updates.droidSessionId !== undefined) session.droidSessionId = updates.droidSessionId
|
|
292
295
|
if (updates.cursorSessionId !== undefined) session.cursorSessionId = updates.cursorSessionId
|
|
293
296
|
if (updates.qwenSessionId !== undefined) session.qwenSessionId = updates.qwenSessionId
|
|
294
297
|
if (updates.acpSessionId !== undefined) session.acpSessionId = updates.acpSessionId
|
|
@@ -375,6 +378,7 @@ export function clearChatMessages(sessionId: string): boolean {
|
|
|
375
378
|
session.opencodeSessionId = null
|
|
376
379
|
session.geminiSessionId = null
|
|
377
380
|
session.copilotSessionId = null
|
|
381
|
+
session.droidSessionId = null
|
|
378
382
|
session.cursorSessionId = null
|
|
379
383
|
session.qwenSessionId = null
|
|
380
384
|
session.acpSessionId = null
|
|
@@ -343,6 +343,7 @@ export function resolveDirectSession(params: {
|
|
|
343
343
|
opencodeSessionId: null,
|
|
344
344
|
geminiSessionId: null,
|
|
345
345
|
copilotSessionId: null,
|
|
346
|
+
droidSessionId: null,
|
|
346
347
|
cursorSessionId: null,
|
|
347
348
|
qwenSessionId: null,
|
|
348
349
|
acpSessionId: null,
|
|
@@ -352,6 +353,7 @@ export function resolveDirectSession(params: {
|
|
|
352
353
|
opencode: null,
|
|
353
354
|
gemini: null,
|
|
354
355
|
copilot: null,
|
|
356
|
+
droid: null,
|
|
355
357
|
cursor: null,
|
|
356
358
|
qwen: null,
|
|
357
359
|
},
|
|
@@ -5,7 +5,7 @@ import { log } from './logger'
|
|
|
5
5
|
|
|
6
6
|
const TAG = 'provider-health'
|
|
7
7
|
|
|
8
|
-
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli' | 'delegate_to_copilot_cli' | 'delegate_to_cursor_cli' | 'delegate_to_qwen_code_cli'
|
|
8
|
+
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli' | 'delegate_to_copilot_cli' | 'delegate_to_droid_cli' | 'delegate_to_cursor_cli' | 'delegate_to_qwen_code_cli'
|
|
9
9
|
|
|
10
10
|
interface ProviderHealthState {
|
|
11
11
|
failures: number
|
|
@@ -120,6 +120,7 @@ function delegateBinary(delegateTool: DelegateTool): string {
|
|
|
120
120
|
if (delegateTool === 'delegate_to_codex_cli') return 'codex'
|
|
121
121
|
if (delegateTool === 'delegate_to_gemini_cli') return 'gemini'
|
|
122
122
|
if (delegateTool === 'delegate_to_copilot_cli') return 'copilot'
|
|
123
|
+
if (delegateTool === 'delegate_to_droid_cli') return 'droid'
|
|
123
124
|
if (delegateTool === 'delegate_to_cursor_cli') return 'cursor-agent'
|
|
124
125
|
if (delegateTool === 'delegate_to_qwen_code_cli') return 'qwen'
|
|
125
126
|
return 'opencode'
|
|
@@ -164,6 +165,7 @@ function delegateProviderId(delegateTool: DelegateTool): string {
|
|
|
164
165
|
if (delegateTool === 'delegate_to_codex_cli') return 'codex-cli'
|
|
165
166
|
if (delegateTool === 'delegate_to_gemini_cli') return 'gemini-cli'
|
|
166
167
|
if (delegateTool === 'delegate_to_copilot_cli') return 'copilot-cli'
|
|
168
|
+
if (delegateTool === 'delegate_to_droid_cli') return 'droid-cli'
|
|
167
169
|
if (delegateTool === 'delegate_to_cursor_cli') return 'cursor-cli'
|
|
168
170
|
if (delegateTool === 'delegate_to_qwen_code_cli') return 'qwen-code-cli'
|
|
169
171
|
return 'opencode-cli'
|
|
@@ -350,7 +352,7 @@ export async function pingProvider(
|
|
|
350
352
|
apiKey: string | undefined,
|
|
351
353
|
endpoint: string | undefined,
|
|
352
354
|
): Promise<{ ok: boolean; message: string }> {
|
|
353
|
-
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose']
|
|
355
|
+
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose']
|
|
354
356
|
const OPTIONAL_OPENAI_COMPATIBLE_KEY_PROVIDERS = new Set(['hermes'])
|
|
355
357
|
if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
|
|
356
358
|
|
|
@@ -229,7 +229,7 @@ test('resolveDescriptor uses Hermes as an OpenAI-compatible provider with option
|
|
|
229
229
|
})
|
|
230
230
|
|
|
231
231
|
test('resolveDescriptor disables model discovery for local CLI-backed providers without live model catalogs', () => {
|
|
232
|
-
for (const providerId of ['copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose']) {
|
|
232
|
+
for (const providerId of ['copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose']) {
|
|
233
233
|
const descriptor = resolveDescriptor({ providerId })
|
|
234
234
|
assert.equal(descriptor?.supportsDiscovery, false, `${providerId} should not support discovery`)
|
|
235
235
|
assert.equal(descriptor?.endpoint, undefined, `${providerId} should not expose a discovery endpoint`)
|
|
@@ -55,7 +55,7 @@ function toOpenAiCompatibleEndpoint(raw: string | null | undefined, fallback: st
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function supportsBuiltInModelDiscovery(providerId: string): boolean {
|
|
58
|
-
return !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose'].includes(providerId)
|
|
58
|
+
return !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose'].includes(providerId)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function normalizeGoogleModelsEndpoint(raw: string | null | undefined): string {
|
|
@@ -704,7 +704,7 @@ async function runProviderHealthChecks() {
|
|
|
704
704
|
if (!agent?.id || typeof agent.id !== 'string') continue
|
|
705
705
|
if (shouldSuppressSyntheticAgentHealthAlert(agent.id)) continue
|
|
706
706
|
const provider = typeof agent.provider === 'string' ? agent.provider : ''
|
|
707
|
-
if (!provider || ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose'].includes(provider)) continue
|
|
707
|
+
if (!provider || ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose'].includes(provider)) continue
|
|
708
708
|
|
|
709
709
|
const credentialId = typeof agent.credentialId === 'string' ? agent.credentialId : ''
|
|
710
710
|
const apiEndpoint = typeof agent.apiEndpoint === 'string' ? agent.apiEndpoint : ''
|
|
@@ -1508,7 +1508,7 @@ export function getDaemonHealthSummary(): {
|
|
|
1508
1508
|
for (const agent of agentEntries) {
|
|
1509
1509
|
if (!agent?.id || typeof agent.id !== 'string') continue
|
|
1510
1510
|
const provider = typeof agent.provider === 'string' ? agent.provider : ''
|
|
1511
|
-
if (!provider || ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose'].includes(provider)) continue
|
|
1511
|
+
if (!provider || ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose'].includes(provider)) continue
|
|
1512
1512
|
const credentialId = typeof agent.credentialId === 'string' ? agent.credentialId : ''
|
|
1513
1513
|
const apiEndpoint = typeof agent.apiEndpoint === 'string' ? agent.apiEndpoint : ''
|
|
1514
1514
|
providerKeys.add(`${provider}:${credentialId || 'no-cred'}:${apiEndpoint}`)
|
|
@@ -284,6 +284,7 @@ export function resetSessionRuntime(
|
|
|
284
284
|
session.opencodeSessionId = null
|
|
285
285
|
session.geminiSessionId = null
|
|
286
286
|
session.copilotSessionId = null
|
|
287
|
+
session.droidSessionId = null
|
|
287
288
|
session.cursorSessionId = null
|
|
288
289
|
session.qwenSessionId = null
|
|
289
290
|
session.acpSessionId = null
|
|
@@ -293,6 +294,7 @@ export function resetSessionRuntime(
|
|
|
293
294
|
opencode: null,
|
|
294
295
|
gemini: null,
|
|
295
296
|
copilot: null,
|
|
297
|
+
droid: null,
|
|
296
298
|
cursor: null,
|
|
297
299
|
qwen: null,
|
|
298
300
|
}
|
|
@@ -86,8 +86,8 @@ export interface ToolBuildContext {
|
|
|
86
86
|
commandTimeoutMs: number
|
|
87
87
|
claudeTimeoutMs: number
|
|
88
88
|
cliProcessTimeoutMs: number
|
|
89
|
-
persistDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen', id: string | null | undefined) => void
|
|
90
|
-
readStoredDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen') => string | null
|
|
89
|
+
persistDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'droid' | 'cursor' | 'qwen', id: string | null | undefined) => void
|
|
90
|
+
readStoredDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'droid' | 'cursor' | 'qwen') => string | null
|
|
91
91
|
resolveCurrentSession: () => any | null
|
|
92
92
|
activeExtensions: string[]
|
|
93
93
|
/** Agent's file access policy — passed to shell for command-level enforcement */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
describe('droid delegate backend wiring', () => {
|
|
5
|
+
it('coerces Factory Droid aliases to the droid backend', async () => {
|
|
6
|
+
const mod = await import('./delegate')
|
|
7
|
+
const anyMod = mod as unknown as Record<string, unknown>
|
|
8
|
+
const coerceDelegateBackend = anyMod.coerceDelegateBackend as ((value: unknown) => string | null) | undefined
|
|
9
|
+
if (typeof coerceDelegateBackend !== 'function') return
|
|
10
|
+
|
|
11
|
+
for (const alias of ['droid', 'droid cli', 'droid-cli', 'droid_cli', 'factory', 'factory droid', 'factory-droid']) {
|
|
12
|
+
assert.equal(coerceDelegateBackend(alias), 'droid', `alias ${alias} should coerce to droid`)
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('includes droid in the delegation JSON-schema enum', async () => {
|
|
17
|
+
const { default: fs } = await import('node:fs')
|
|
18
|
+
const { default: path } = await import('node:path')
|
|
19
|
+
const delegatePath = path.resolve(process.cwd(), 'src/lib/server/session-tools/delegate.ts')
|
|
20
|
+
const source = fs.readFileSync(delegatePath, 'utf-8')
|
|
21
|
+
assert.match(source, /enum: \[[^\]]*'droid'[^\]]*\]/, 'droid must appear in the delegate backend enum')
|
|
22
|
+
assert.match(source, /DELEGATE_BACKEND_ORDER[\s\S]{0,200}'droid'/, 'droid must appear in DELEGATE_BACKEND_ORDER')
|
|
23
|
+
})
|
|
24
|
+
})
|