@swarmclawai/swarmclaw 0.7.1 → 0.7.3
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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
-
import { spawn, spawnSync } from 'child_process'
|
|
3
|
+
import { spawn, spawnSync, type ChildProcess } from 'child_process'
|
|
4
4
|
import type { ToolBuildContext } from './context'
|
|
5
5
|
import { truncate, findBinaryOnPath, MAX_OUTPUT } from './context'
|
|
6
6
|
import type { Plugin, PluginHooks } from '@/types'
|
|
7
7
|
import { getPluginManager } from '../plugins'
|
|
8
8
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
9
|
+
import {
|
|
10
|
+
appendDelegationCheckpoint,
|
|
11
|
+
cancelDelegationJob,
|
|
12
|
+
completeDelegationJob,
|
|
13
|
+
createDelegationJob,
|
|
14
|
+
failDelegationJob,
|
|
15
|
+
getDelegationJob,
|
|
16
|
+
listDelegationJobs,
|
|
17
|
+
recoverStaleDelegationJobs,
|
|
18
|
+
registerDelegationRuntime,
|
|
19
|
+
startDelegationJob,
|
|
20
|
+
} from '../delegation-jobs'
|
|
21
|
+
import { markProviderFailure, markProviderSuccess } from '../provider-health'
|
|
9
22
|
|
|
10
23
|
const MAX_DELEGATION_CHAIN_HOPS = 128
|
|
24
|
+
const DELEGATE_BACKEND_ORDER: DelegateBackend[] = ['claude', 'codex', 'opencode', 'gemini']
|
|
11
25
|
|
|
12
26
|
interface DelegateContext {
|
|
27
|
+
id?: string
|
|
28
|
+
sessionId?: string | null
|
|
29
|
+
agentId?: string | null
|
|
30
|
+
jobId?: string | null
|
|
13
31
|
cwd?: string
|
|
14
32
|
claudeTimeoutMs?: number
|
|
15
|
-
readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode') => string | null
|
|
16
|
-
persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode', id: string) => void
|
|
17
|
-
ctx?: { platformAssignScope?: string; agentId?: string | null }
|
|
33
|
+
readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null
|
|
34
|
+
persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string | null | undefined) => void
|
|
35
|
+
ctx?: { platformAssignScope?: string; agentId?: string | null; sessionId?: string | null }
|
|
36
|
+
hasPlugin?: (name: string) => boolean
|
|
37
|
+
/** @deprecated Use hasPlugin */
|
|
18
38
|
hasTool?: (name: string) => boolean
|
|
19
39
|
}
|
|
20
40
|
|
|
21
|
-
type DelegateBackend = 'claude' | 'codex' | 'opencode'
|
|
41
|
+
type DelegateBackend = 'claude' | 'codex' | 'opencode' | 'gemini'
|
|
42
|
+
|
|
43
|
+
interface DelegateRuntimeState {
|
|
44
|
+
child?: ChildProcess | null
|
|
45
|
+
cancel?: () => void
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
function asTaskRecord(value: unknown): Record<string, unknown> | null {
|
|
24
49
|
return value && typeof value === 'object' ? value as Record<string, unknown> : null
|
|
@@ -61,11 +86,169 @@ function _computeDelegationDepth(
|
|
|
61
86
|
return depth
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
function sleep(ms: number) {
|
|
90
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildDelegateContextFromSessionish(session: unknown): DelegateContext {
|
|
94
|
+
const record = session && typeof session === 'object' ? session as Record<string, unknown> : {}
|
|
95
|
+
const sessionId = typeof record.id === 'string'
|
|
96
|
+
? record.id
|
|
97
|
+
: typeof record.sessionId === 'string'
|
|
98
|
+
? record.sessionId
|
|
99
|
+
: null
|
|
100
|
+
const agentId = typeof record.agentId === 'string' ? record.agentId : null
|
|
101
|
+
const platformAssignScope = typeof record.platformAssignScope === 'string' ? record.platformAssignScope : undefined
|
|
102
|
+
const storedResumeIds = record.delegateResumeIds && typeof record.delegateResumeIds === 'object'
|
|
103
|
+
? record.delegateResumeIds as Record<string, unknown>
|
|
104
|
+
: null
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
cwd: typeof record.cwd === 'string' ? record.cwd : process.cwd(),
|
|
108
|
+
claudeTimeoutMs: typeof record.claudeTimeoutMs === 'number' ? record.claudeTimeoutMs : undefined,
|
|
109
|
+
readStoredDelegateResumeId: typeof record.readStoredDelegateResumeId === 'function'
|
|
110
|
+
? record.readStoredDelegateResumeId as DelegateContext['readStoredDelegateResumeId']
|
|
111
|
+
: (key) => {
|
|
112
|
+
const raw = storedResumeIds?.[key]
|
|
113
|
+
return typeof raw === 'string' && raw.trim() ? raw.trim() : null
|
|
114
|
+
},
|
|
115
|
+
persistDelegateResumeId: typeof record.persistDelegateResumeId === 'function'
|
|
116
|
+
? record.persistDelegateResumeId as DelegateContext['persistDelegateResumeId']
|
|
117
|
+
: undefined,
|
|
118
|
+
id: typeof record.id === 'string' ? record.id : undefined,
|
|
119
|
+
sessionId,
|
|
120
|
+
agentId,
|
|
121
|
+
ctx: {
|
|
122
|
+
sessionId,
|
|
123
|
+
agentId,
|
|
124
|
+
platformAssignScope,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildDelegateResumePatch(bctx: DelegateContext) {
|
|
130
|
+
const resumeIds = {
|
|
131
|
+
claudeCode: bctx.readStoredDelegateResumeId?.('claudeCode') || null,
|
|
132
|
+
codex: bctx.readStoredDelegateResumeId?.('codex') || null,
|
|
133
|
+
opencode: bctx.readStoredDelegateResumeId?.('opencode') || null,
|
|
134
|
+
gemini: bctx.readStoredDelegateResumeId?.('gemini') || null,
|
|
135
|
+
}
|
|
136
|
+
const resumeId = resumeIds.claudeCode || resumeIds.codex || resumeIds.opencode || resumeIds.gemini || null
|
|
137
|
+
return { resumeIds, resumeId }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function coerceDelegateBackend(value: unknown): DelegateBackend | null {
|
|
141
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
142
|
+
if (!normalized) return null
|
|
143
|
+
if (['claude', 'claude code', 'claude-code', 'claude_code'].includes(normalized)) return 'claude'
|
|
144
|
+
if (['codex', 'codex cli', 'codex-cli', 'codex_cli'].includes(normalized)) return 'codex'
|
|
145
|
+
if (['opencode', 'open code', 'open-code', 'open_code'].includes(normalized)) return 'opencode'
|
|
146
|
+
if (['gemini', 'gemini cli', 'gemini-cli', 'gemini_cli'].includes(normalized)) return 'gemini'
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): string | null {
|
|
151
|
+
const action = String(normalized.action || '').trim().toLowerCase()
|
|
152
|
+
const target = [
|
|
153
|
+
normalized.target,
|
|
154
|
+
normalized.path,
|
|
155
|
+
normalized.filePath,
|
|
156
|
+
normalized.filename,
|
|
157
|
+
normalized.name,
|
|
158
|
+
].find((value) => typeof value === 'string' && value.trim()) as string | undefined
|
|
159
|
+
const content = typeof normalized.content === 'string' ? normalized.content.trim() : ''
|
|
160
|
+
const taskName = typeof normalized.name === 'string' ? normalized.name.trim() : ''
|
|
161
|
+
const files = Array.isArray(normalized.files) ? normalized.files : []
|
|
162
|
+
const fileInstructions = files
|
|
163
|
+
.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
|
|
164
|
+
.map((entry) => {
|
|
165
|
+
const filePath = typeof entry.path === 'string'
|
|
166
|
+
? entry.path.trim()
|
|
167
|
+
: typeof entry.filePath === 'string'
|
|
168
|
+
? entry.filePath.trim()
|
|
169
|
+
: typeof entry.filename === 'string'
|
|
170
|
+
? entry.filename.trim()
|
|
171
|
+
: ''
|
|
172
|
+
const fileContent = typeof entry.content === 'string' ? entry.content.trim() : ''
|
|
173
|
+
if (!filePath && !fileContent) return ''
|
|
174
|
+
if (filePath && fileContent) {
|
|
175
|
+
return `Create or update "${filePath}" with this content:\n\n${fileContent}`
|
|
176
|
+
}
|
|
177
|
+
if (filePath) return `Create or update "${filePath}".`
|
|
178
|
+
return `Create or update a file with this content:\n\n${fileContent}`
|
|
179
|
+
})
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
|
|
182
|
+
if (['write', 'create', 'create_file', 'create-file', 'createfile'].includes(action)) {
|
|
183
|
+
if (target && content) return `Create or overwrite the file "${target}" with this content:\n\n${content}`
|
|
184
|
+
if (target) return `Create the file "${target}".`
|
|
185
|
+
}
|
|
186
|
+
if (['edit', 'update', 'modify'].includes(action)) {
|
|
187
|
+
if (target && content) return `Update the file "${target}" with this content:\n\n${content}`
|
|
188
|
+
if (target) return `Update the file "${target}".`
|
|
189
|
+
}
|
|
190
|
+
if (target && content) return `Perform the "${action || 'requested'}" task against "${target}" using this content:\n\n${content}`
|
|
191
|
+
if (target) return `Perform the "${action || 'requested'}" task against "${target}".`
|
|
192
|
+
if (fileInstructions.length > 0) {
|
|
193
|
+
const intro = taskName || 'Perform the delegated file task.'
|
|
194
|
+
return `${intro}\n\n${fileInstructions.join('\n\n')}`
|
|
195
|
+
}
|
|
196
|
+
if (content) return `Perform the delegated task with this content:\n\n${content}`
|
|
197
|
+
if (taskName) return taskName
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeDelegateArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
|
|
202
|
+
const normalized = normalizeToolInputArgs(rawArgs)
|
|
203
|
+
const backend = coerceDelegateBackend(
|
|
204
|
+
normalized.backend
|
|
205
|
+
?? normalized.tool_name
|
|
206
|
+
?? normalized.toolName
|
|
207
|
+
?? normalized.delegate
|
|
208
|
+
?? normalized.provider,
|
|
209
|
+
)
|
|
210
|
+
if (backend && !normalized.backend) normalized.backend = backend
|
|
211
|
+
if (typeof normalized.task !== 'string' && typeof normalized.prompt === 'string') normalized.task = normalized.prompt
|
|
212
|
+
const action = String(normalized.action || '').trim().toLowerCase()
|
|
213
|
+
const isLifecycleAction = ['status', 'list', 'wait', 'cancel'].includes(action)
|
|
214
|
+
if (!isLifecycleAction) {
|
|
215
|
+
if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
|
|
216
|
+
const synthesized = buildDelegateTaskFromPayload(normalized)
|
|
217
|
+
if (synthesized) normalized.task = synthesized
|
|
218
|
+
}
|
|
219
|
+
normalized.action = 'start'
|
|
220
|
+
}
|
|
221
|
+
return normalized
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveDelegateSessionId(bctx: DelegateContext): string | null {
|
|
225
|
+
const nested = typeof bctx.ctx?.sessionId === 'string' ? bctx.ctx.sessionId.trim() : ''
|
|
226
|
+
if (nested) return nested
|
|
227
|
+
const direct = typeof bctx.sessionId === 'string' ? bctx.sessionId.trim() : ''
|
|
228
|
+
if (direct) return direct
|
|
229
|
+
const legacy = typeof bctx.id === 'string' ? bctx.id.trim() : ''
|
|
230
|
+
return legacy || null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function bindDelegateRuntime(runtime: DelegateRuntimeState | undefined, child: ChildProcess) {
|
|
234
|
+
if (!runtime) return
|
|
235
|
+
runtime.child = child
|
|
236
|
+
runtime.cancel = () => {
|
|
237
|
+
try {
|
|
238
|
+
child.kill('SIGTERM')
|
|
239
|
+
} catch {
|
|
240
|
+
// best-effort cancel
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const clear = () => {
|
|
244
|
+
if (runtime.child === child) runtime.child = null
|
|
245
|
+
}
|
|
246
|
+
child.once('close', clear)
|
|
247
|
+
child.once('error', clear)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
251
|
+
const normalized = normalizeDelegateArgs(args)
|
|
69
252
|
const task = normalized.task as string
|
|
70
253
|
const backend = ((normalized.backend as string) || 'claude') as DelegateBackend
|
|
71
254
|
const resume = normalized.resume as boolean
|
|
@@ -74,16 +257,214 @@ async function executeDelegateAction(args: Record<string, unknown>, bctx: Delega
|
|
|
74
257
|
claude: findBinaryOnPath('claude'),
|
|
75
258
|
codex: findBinaryOnPath('codex'),
|
|
76
259
|
opencode: findBinaryOnPath('opencode'),
|
|
260
|
+
gemini: findBinaryOnPath('gemini'),
|
|
77
261
|
}
|
|
78
262
|
const binary = backends[backend as keyof typeof backends]
|
|
79
263
|
if (!binary) return `Error: Backend "${backend}" unavailable.`
|
|
80
264
|
|
|
81
|
-
if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx)
|
|
82
|
-
if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx)
|
|
83
|
-
if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx)
|
|
265
|
+
if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx, runtime)
|
|
266
|
+
if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx, runtime)
|
|
267
|
+
if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx, runtime)
|
|
268
|
+
if (backend === 'gemini') return runGeminiDelegate(binary, task, resume, resumeId, bctx, runtime)
|
|
84
269
|
return `Error: Unsupported backend "${backend}".`
|
|
85
270
|
}
|
|
86
271
|
|
|
272
|
+
function providerIdForBackend(backend: DelegateBackend): string {
|
|
273
|
+
if (backend === 'claude') return 'claude-cli'
|
|
274
|
+
if (backend === 'codex') return 'codex-cli'
|
|
275
|
+
if (backend === 'opencode') return 'opencode-cli'
|
|
276
|
+
return 'gemini-cli'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function fallbackOrderForBackend(requested: DelegateBackend): DelegateBackend[] {
|
|
280
|
+
return [requested, ...DELEGATE_BACKEND_ORDER.filter((backend) => backend !== requested)]
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isRecoverableDelegateFailure(result: string): boolean {
|
|
284
|
+
const normalized = String(result || '').trim().toLowerCase()
|
|
285
|
+
if (!normalized.startsWith('error:')) return false
|
|
286
|
+
return [
|
|
287
|
+
'not authenticated',
|
|
288
|
+
'backend "',
|
|
289
|
+
'unavailable',
|
|
290
|
+
'enoent',
|
|
291
|
+
'not found',
|
|
292
|
+
'command not found',
|
|
293
|
+
'spawn ',
|
|
294
|
+
'eacces',
|
|
295
|
+
'permission denied',
|
|
296
|
+
].some((needle) => normalized.includes(needle))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function summarizeDelegateAttempts(
|
|
300
|
+
requested: DelegateBackend,
|
|
301
|
+
attempts: Array<{ backend: DelegateBackend; result: string }>,
|
|
302
|
+
): string {
|
|
303
|
+
const summary = attempts
|
|
304
|
+
.map(({ backend, result }) => `${backend}: ${result.replace(/^Error:\s*/i, '').trim() || result.trim()}`)
|
|
305
|
+
.join(' | ')
|
|
306
|
+
return `Error: Delegate backend "${requested}" could not complete the task. ${summary}. Continue with another available tool instead of stopping.`
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function runDelegateBackendWithFallback(
|
|
310
|
+
args: Record<string, unknown>,
|
|
311
|
+
bctx: DelegateContext,
|
|
312
|
+
runtime?: DelegateRuntimeState,
|
|
313
|
+
opts?: { onAttempt?: (backend: DelegateBackend, attemptIndex: number) => void; onFallback?: (from: DelegateBackend, to: DelegateBackend, reason: string) => void },
|
|
314
|
+
): Promise<{ backend: DelegateBackend; result: string; attempts: Array<{ backend: DelegateBackend; result: string }> }> {
|
|
315
|
+
const normalized = normalizeDelegateArgs(args)
|
|
316
|
+
const requested = ((normalized.backend as string) || 'claude') as DelegateBackend
|
|
317
|
+
const orderedBackends = fallbackOrderForBackend(requested)
|
|
318
|
+
const attempts: Array<{ backend: DelegateBackend; result: string }> = []
|
|
319
|
+
|
|
320
|
+
for (const [index, backend] of orderedBackends.entries()) {
|
|
321
|
+
opts?.onAttempt?.(backend, index)
|
|
322
|
+
const result = await runDelegateBackend({ ...normalized, backend }, bctx, runtime)
|
|
323
|
+
attempts.push({ backend, result })
|
|
324
|
+
if (/^Error:/i.test(result.trim())) {
|
|
325
|
+
markProviderFailure(providerIdForBackend(backend), result)
|
|
326
|
+
} else {
|
|
327
|
+
markProviderSuccess(providerIdForBackend(backend))
|
|
328
|
+
return { backend, result, attempts }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const nextBackend = orderedBackends[index + 1]
|
|
332
|
+
if (nextBackend && isRecoverableDelegateFailure(result)) {
|
|
333
|
+
opts?.onFallback?.(backend, nextBackend, result)
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
backend,
|
|
338
|
+
result: attempts.length > 1 ? summarizeDelegateAttempts(requested, attempts) : result,
|
|
339
|
+
attempts,
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
backend: requested,
|
|
345
|
+
result: summarizeDelegateAttempts(requested, attempts),
|
|
346
|
+
attempts,
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function waitForDelegateJob(jobId: string, timeoutSec = 30): Promise<string> {
|
|
351
|
+
const timeoutAt = Date.now() + Math.max(1, timeoutSec) * 1000
|
|
352
|
+
while (Date.now() < timeoutAt) {
|
|
353
|
+
const job = getDelegationJob(jobId)
|
|
354
|
+
if (!job) return `Error: delegation job "${jobId}" not found.`
|
|
355
|
+
if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
|
|
356
|
+
return JSON.stringify(job)
|
|
357
|
+
}
|
|
358
|
+
await sleep(1000)
|
|
359
|
+
}
|
|
360
|
+
const latest = getDelegationJob(jobId)
|
|
361
|
+
return latest ? JSON.stringify(latest) : `Error: delegation job "${jobId}" not found.`
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Core Delegate Execution Logic
|
|
366
|
+
*/
|
|
367
|
+
async function executeDelegateAction(args: Record<string, unknown>, bctx: DelegateContext) {
|
|
368
|
+
const normalized = normalizeDelegateArgs(args)
|
|
369
|
+
const action = String(normalized.action || '').trim().toLowerCase()
|
|
370
|
+
const task = normalized.task as string
|
|
371
|
+
const requestedBackend = ((normalized.backend as string) || 'claude') as DelegateBackend
|
|
372
|
+
const jobId = typeof normalized.jobId === 'string' ? normalized.jobId.trim() : ''
|
|
373
|
+
const waitForCompletion = normalized.waitForCompletion !== false && normalized.background !== true
|
|
374
|
+
const parentSessionId = resolveDelegateSessionId(bctx)
|
|
375
|
+
|
|
376
|
+
recoverStaleDelegationJobs()
|
|
377
|
+
|
|
378
|
+
if (action === 'status') {
|
|
379
|
+
if (!jobId) return 'Error: jobId is required.'
|
|
380
|
+
const job = getDelegationJob(jobId)
|
|
381
|
+
return job ? JSON.stringify(job) : `Error: delegation job "${jobId}" not found.`
|
|
382
|
+
}
|
|
383
|
+
if (action === 'list') {
|
|
384
|
+
const jobs = listDelegationJobs({ parentSessionId: parentSessionId || null })
|
|
385
|
+
.filter((job) => job.kind === 'delegate')
|
|
386
|
+
return JSON.stringify(jobs)
|
|
387
|
+
}
|
|
388
|
+
if (action === 'cancel') {
|
|
389
|
+
if (!jobId) return 'Error: jobId is required.'
|
|
390
|
+
const job = cancelDelegationJob(jobId)
|
|
391
|
+
return job ? JSON.stringify(job) : `Error: delegation job "${jobId}" not found.`
|
|
392
|
+
}
|
|
393
|
+
if (action === 'wait') {
|
|
394
|
+
if (!jobId) return 'Error: jobId is required.'
|
|
395
|
+
const timeoutSec = typeof normalized.timeoutSec === 'number' ? normalized.timeoutSec : 30
|
|
396
|
+
return waitForDelegateJob(jobId, timeoutSec)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!task) return 'Error: task is required.'
|
|
400
|
+
|
|
401
|
+
const job = createDelegationJob({
|
|
402
|
+
kind: 'delegate',
|
|
403
|
+
parentSessionId,
|
|
404
|
+
backend: requestedBackend,
|
|
405
|
+
task,
|
|
406
|
+
cwd: bctx.cwd || null,
|
|
407
|
+
})
|
|
408
|
+
appendDelegationCheckpoint(job.id, `Dispatching to ${requestedBackend}`, 'queued')
|
|
409
|
+
startDelegationJob(job.id, { backend: requestedBackend, cwd: bctx.cwd || null })
|
|
410
|
+
const runtimeHandle: DelegateRuntimeState = {}
|
|
411
|
+
registerDelegationRuntime(job.id, runtimeHandle)
|
|
412
|
+
|
|
413
|
+
const runner = runDelegateBackendWithFallback(args, bctx, runtimeHandle, {
|
|
414
|
+
onAttempt: (backend, index) => {
|
|
415
|
+
if (index === 0) return
|
|
416
|
+
appendDelegationCheckpoint(job.id, `Retrying delegate with ${backend}`, 'running')
|
|
417
|
+
startDelegationJob(job.id, { backend, cwd: bctx.cwd || null })
|
|
418
|
+
},
|
|
419
|
+
onFallback: (from, to, reason) => {
|
|
420
|
+
appendDelegationCheckpoint(
|
|
421
|
+
job.id,
|
|
422
|
+
`Delegate ${from} failed: ${reason.replace(/^Error:\s*/i, '').trim()}. Falling back to ${to}.`,
|
|
423
|
+
'running',
|
|
424
|
+
)
|
|
425
|
+
},
|
|
426
|
+
})
|
|
427
|
+
.then(({ backend, result }) => {
|
|
428
|
+
const latest = getDelegationJob(job.id)
|
|
429
|
+
if (latest?.status === 'cancelled') return { backend, result }
|
|
430
|
+
const resumePatch = buildDelegateResumePatch(bctx)
|
|
431
|
+
if (/^Error:/i.test(result.trim())) {
|
|
432
|
+
appendDelegationCheckpoint(job.id, `Delegate failed on ${backend}`, 'failed')
|
|
433
|
+
failDelegationJob(job.id, result.replace(/^Error:\s*/i, '').trim() || result, { ...resumePatch, backend })
|
|
434
|
+
} else {
|
|
435
|
+
appendDelegationCheckpoint(job.id, `Delegate completed on ${backend}`, 'completed')
|
|
436
|
+
completeDelegationJob(job.id, result, { ...resumePatch, backend })
|
|
437
|
+
}
|
|
438
|
+
return { backend, result }
|
|
439
|
+
})
|
|
440
|
+
.catch((err: unknown) => {
|
|
441
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
442
|
+
const latest = getDelegationJob(job.id)
|
|
443
|
+
if (latest?.status === 'cancelled') return { backend: requestedBackend, result: `Error: ${message}` }
|
|
444
|
+
appendDelegationCheckpoint(job.id, `Delegate crashed on ${requestedBackend}: ${message}`, 'failed')
|
|
445
|
+
failDelegationJob(job.id, message, { ...buildDelegateResumePatch(bctx), backend: requestedBackend })
|
|
446
|
+
return { backend: requestedBackend, result: `Error: ${message}` }
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
if (!waitForCompletion) {
|
|
450
|
+
void runner
|
|
451
|
+
return JSON.stringify({
|
|
452
|
+
jobId: job.id,
|
|
453
|
+
status: 'running',
|
|
454
|
+
backend: requestedBackend,
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const { backend, result } = await runner
|
|
459
|
+
const latest = getDelegationJob(job.id)
|
|
460
|
+
return JSON.stringify({
|
|
461
|
+
jobId: job.id,
|
|
462
|
+
status: latest?.status || (/^Error:/i.test(result.trim()) ? 'failed' : 'completed'),
|
|
463
|
+
backend: latest?.backend || backend,
|
|
464
|
+
response: result,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
87
468
|
function stripEnvPrefixes(input: NodeJS.ProcessEnv, prefixes: string[]): NodeJS.ProcessEnv {
|
|
88
469
|
const out: NodeJS.ProcessEnv = { ...input }
|
|
89
470
|
for (const key of Object.keys(out)) {
|
|
@@ -116,7 +497,7 @@ function parseCodexOutputText(ev: Record<string, unknown>): string | null {
|
|
|
116
497
|
return null
|
|
117
498
|
}
|
|
118
499
|
|
|
119
|
-
async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
|
|
500
|
+
async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
120
501
|
try {
|
|
121
502
|
const env = stripEnvPrefixes({ ...process.env, TERM: 'dumb', NO_COLOR: '1' }, ['CODEX'])
|
|
122
503
|
const authProbe = spawnSync(binary, ['login', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
|
|
@@ -135,6 +516,7 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
|
|
|
135
516
|
args.push('--json', '--full-auto', '--skip-git-repo-check', '-')
|
|
136
517
|
|
|
137
518
|
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
519
|
+
bindDelegateRuntime(runtime, child)
|
|
138
520
|
let stdoutBuf = ''
|
|
139
521
|
let stderrBuf = ''
|
|
140
522
|
let responseText = ''
|
|
@@ -197,7 +579,7 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
|
|
|
197
579
|
}
|
|
198
580
|
}
|
|
199
581
|
|
|
200
|
-
async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
|
|
582
|
+
async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
201
583
|
try {
|
|
202
584
|
const env = { ...process.env, TERM: 'dumb', NO_COLOR: '1' } as NodeJS.ProcessEnv
|
|
203
585
|
const storedResumeId = bctx.readStoredDelegateResumeId?.('opencode')
|
|
@@ -208,6 +590,7 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
|
|
|
208
590
|
if (resumeIdToUse) args.push('--session', resumeIdToUse)
|
|
209
591
|
|
|
210
592
|
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
593
|
+
bindDelegateRuntime(runtime, child)
|
|
211
594
|
let stdoutBuf = ''
|
|
212
595
|
let stderrBuf = ''
|
|
213
596
|
let responseText = ''
|
|
@@ -273,7 +656,88 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
|
|
|
273
656
|
}
|
|
274
657
|
}
|
|
275
658
|
|
|
276
|
-
async function
|
|
659
|
+
async function runGeminiDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
660
|
+
try {
|
|
661
|
+
const env = { ...process.env, TERM: 'dumb', NO_COLOR: '1' } as NodeJS.ProcessEnv
|
|
662
|
+
const storedResumeId = bctx.readStoredDelegateResumeId?.('gemini')
|
|
663
|
+
const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
|
|
664
|
+
|
|
665
|
+
return await new Promise<string>((resolve) => {
|
|
666
|
+
const args = ['--prompt', task, '--output-format', 'stream-json', '--yolo']
|
|
667
|
+
if (resumeIdToUse) args.push('--resume', resumeIdToUse)
|
|
668
|
+
|
|
669
|
+
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
670
|
+
bindDelegateRuntime(runtime, child)
|
|
671
|
+
let stdoutBuf = ''
|
|
672
|
+
let stderrBuf = ''
|
|
673
|
+
let responseText = ''
|
|
674
|
+
let discoveredId: string | null = null
|
|
675
|
+
let settled = false
|
|
676
|
+
|
|
677
|
+
const finish = (text: string) => {
|
|
678
|
+
if (settled) return
|
|
679
|
+
settled = true
|
|
680
|
+
resolve(truncate(text, MAX_OUTPUT))
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const timeoutHandle = setTimeout(() => {
|
|
684
|
+
try { child.kill('SIGTERM') } catch { /* ignore */ }
|
|
685
|
+
}, bctx.claudeTimeoutMs || 300000)
|
|
686
|
+
|
|
687
|
+
child.stdout?.on('data', (chunk) => {
|
|
688
|
+
stdoutBuf += chunk.toString()
|
|
689
|
+
const lines = stdoutBuf.split('\n')
|
|
690
|
+
stdoutBuf = lines.pop() || ''
|
|
691
|
+
for (const line of lines) {
|
|
692
|
+
const trimmed = line.trim()
|
|
693
|
+
if (!trimmed) continue
|
|
694
|
+
try {
|
|
695
|
+
const ev = JSON.parse(trimmed) as Record<string, unknown>
|
|
696
|
+
// Capture session ID from init event
|
|
697
|
+
if (ev.type === 'init' && typeof ev.session_id === 'string') {
|
|
698
|
+
discoveredId = ev.session_id
|
|
699
|
+
}
|
|
700
|
+
// Capture assistant text from message events
|
|
701
|
+
if (ev.type === 'message' && ev.role === 'assistant' && typeof ev.content === 'string') {
|
|
702
|
+
responseText += ev.content
|
|
703
|
+
}
|
|
704
|
+
// Capture final result
|
|
705
|
+
if (ev.type === 'result' && ev.status === 'error') {
|
|
706
|
+
const errMsg = typeof ev.error === 'string' ? ev.error : 'Gemini error'
|
|
707
|
+
stderrBuf += `${errMsg}\n`
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
responseText += `${line}\n`
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
child.stderr?.on('data', (chunk) => {
|
|
716
|
+
stderrBuf += chunk.toString()
|
|
717
|
+
if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
child.on('close', (code, signal) => {
|
|
721
|
+
clearTimeout(timeoutHandle)
|
|
722
|
+
if (discoveredId) bctx.persistDelegateResumeId?.('gemini', discoveredId)
|
|
723
|
+
const output = responseText.trim()
|
|
724
|
+
if (output) return finish(output)
|
|
725
|
+
const stderr = stderrBuf.trim()
|
|
726
|
+
if (stderr) return finish(`Error: ${stderr}`)
|
|
727
|
+
return finish(`Error: Gemini exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`)
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
child.on('error', (err) => {
|
|
731
|
+
clearTimeout(timeoutHandle)
|
|
732
|
+
finish(`Error: ${err.message}`)
|
|
733
|
+
})
|
|
734
|
+
})
|
|
735
|
+
} catch (err: unknown) {
|
|
736
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
277
741
|
try {
|
|
278
742
|
const env: NodeJS.ProcessEnv = stripEnvPrefixes({ ...process.env }, ['CLAUDE'])
|
|
279
743
|
const authProbe = spawnSync(binary, ['auth', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
|
|
@@ -286,6 +750,7 @@ async function runClaudeDelegate(binary: string, task: string, resume: boolean,
|
|
|
286
750
|
const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
|
|
287
751
|
if (resumeIdToUse) args.push('--resume', resumeIdToUse)
|
|
288
752
|
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
753
|
+
bindDelegateRuntime(runtime, child)
|
|
289
754
|
let stderr = ''
|
|
290
755
|
let assistantText = ''
|
|
291
756
|
let discoveredId: string | null = null
|
|
@@ -335,22 +800,30 @@ async function runClaudeDelegate(binary: string, task: string, resume: boolean,
|
|
|
335
800
|
const DelegatePlugin: Plugin = {
|
|
336
801
|
name: 'Core Delegate',
|
|
337
802
|
description: 'Delegate complex multi-file tasks to specialized CLI backends or other agents.',
|
|
338
|
-
hooks: {
|
|
803
|
+
hooks: {
|
|
804
|
+
getCapabilityDescription: () => 'I can hand off deep coding work to Claude Code, Codex, or Gemini CLI (`delegate`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.',
|
|
805
|
+
getOperatingGuidance: () => ['CRITICAL: `execute_command` (not delegation) for running servers, installs, scripts. Delegation sessions end and kill processes.', 'Delegate only for deep multi-file code work: refactors, debugging, generation, test suites.'],
|
|
806
|
+
} as PluginHooks,
|
|
339
807
|
tools: [
|
|
340
808
|
{
|
|
341
809
|
name: 'delegate',
|
|
342
|
-
description: 'Delegate to a specialized backend (Claude, Codex, OpenCode).',
|
|
810
|
+
description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini). Supports background jobs with action=status|list|wait|cancel.',
|
|
343
811
|
parameters: {
|
|
344
812
|
type: 'object',
|
|
345
813
|
properties: {
|
|
814
|
+
action: { type: 'string', enum: ['start', 'status', 'list', 'wait', 'cancel'] },
|
|
346
815
|
task: { type: 'string' },
|
|
347
|
-
backend: { type: 'string', enum: ['claude', 'codex', 'opencode'] },
|
|
816
|
+
backend: { type: 'string', enum: ['claude', 'codex', 'opencode', 'gemini'] },
|
|
348
817
|
resume: { type: 'boolean' },
|
|
349
|
-
resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' }
|
|
818
|
+
resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' },
|
|
819
|
+
jobId: { type: 'string' },
|
|
820
|
+
waitForCompletion: { type: 'boolean' },
|
|
821
|
+
background: { type: 'boolean' },
|
|
822
|
+
timeoutSec: { type: 'number' },
|
|
350
823
|
},
|
|
351
|
-
required: [
|
|
824
|
+
required: []
|
|
352
825
|
},
|
|
353
|
-
execute: async (args, context) => executeDelegateAction(args,
|
|
826
|
+
execute: async (args, context) => executeDelegateAction(args, buildDelegateContextFromSessionish(context.session))
|
|
354
827
|
}
|
|
355
828
|
]
|
|
356
829
|
}
|
|
@@ -362,9 +835,9 @@ getPluginManager().registerBuiltin('delegate', DelegatePlugin)
|
|
|
362
835
|
*/
|
|
363
836
|
export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
364
837
|
const tools: StructuredToolInterface[] = []
|
|
365
|
-
const {
|
|
838
|
+
const { hasPlugin } = bctx
|
|
366
839
|
|
|
367
|
-
if (
|
|
840
|
+
if (hasPlugin('delegate')) {
|
|
368
841
|
tools.push(
|
|
369
842
|
tool(
|
|
370
843
|
async (args) => executeDelegateAction(args, bctx),
|