@swarmclawai/swarmclaw 0.7.8 → 0.8.0
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 +12 -15
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +7 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +1 -1
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +189 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +15 -10
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.ts +4 -2
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +210 -14
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
decryptKey,
|
|
22
22
|
} from '../storage'
|
|
23
23
|
import { resolveScheduleName } from '@/lib/schedule-name'
|
|
24
|
-
import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
|
|
24
|
+
import { findDuplicateSchedule, findEquivalentSchedules, type ScheduleLike } from '@/lib/schedule-dedupe'
|
|
25
25
|
import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
|
|
26
26
|
import {
|
|
27
27
|
hasManagedAgentAssignmentInput,
|
|
@@ -118,6 +118,95 @@ function deriveTaskTitle(input: { title?: unknown; description?: unknown }): str
|
|
|
118
118
|
return compact.slice(0, 120)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
const VALID_CONNECTOR_PLATFORMS = new Set([
|
|
122
|
+
'discord',
|
|
123
|
+
'telegram',
|
|
124
|
+
'slack',
|
|
125
|
+
'whatsapp',
|
|
126
|
+
'openclaw',
|
|
127
|
+
'bluebubbles',
|
|
128
|
+
'signal',
|
|
129
|
+
'teams',
|
|
130
|
+
'googlechat',
|
|
131
|
+
'matrix',
|
|
132
|
+
'email',
|
|
133
|
+
'webchat',
|
|
134
|
+
'mockmail',
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
const VALID_CONNECTOR_STATUSES = new Set(['stopped', 'running', 'error'])
|
|
138
|
+
|
|
139
|
+
function normalizeConnectorConfig(raw: unknown): Record<string, string> {
|
|
140
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
|
|
141
|
+
const normalized: Record<string, string> = {}
|
|
142
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
143
|
+
const normalizedKey = typeof key === 'string' ? key.trim() : ''
|
|
144
|
+
if (!normalizedKey) continue
|
|
145
|
+
if (typeof value === 'string') {
|
|
146
|
+
normalized[normalizedKey] = value
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
150
|
+
normalized[normalizedKey] = String(value)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return normalized
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sanitizeConnectorCrudPayload(
|
|
157
|
+
raw: Record<string, unknown>,
|
|
158
|
+
options: { forUpdate?: boolean } = {},
|
|
159
|
+
): Record<string, unknown> {
|
|
160
|
+
const { forUpdate = false } = options
|
|
161
|
+
const out: Record<string, unknown> = {}
|
|
162
|
+
const setString = (key: 'name' | 'platform' | 'status') => {
|
|
163
|
+
if (!Object.prototype.hasOwnProperty.call(raw, key)) return
|
|
164
|
+
const value = typeof raw[key] === 'string' ? raw[key].trim() : ''
|
|
165
|
+
if (!value) return
|
|
166
|
+
if (key === 'platform' && !VALID_CONNECTOR_PLATFORMS.has(value)) return
|
|
167
|
+
if (key === 'status' && !VALID_CONNECTOR_STATUSES.has(value)) return
|
|
168
|
+
out[key] = value
|
|
169
|
+
}
|
|
170
|
+
const setNullableId = (key: 'agentId' | 'chatroomId' | 'credentialId') => {
|
|
171
|
+
if (!Object.prototype.hasOwnProperty.call(raw, key)) return
|
|
172
|
+
const value = typeof raw[key] === 'string' ? raw[key].trim() : ''
|
|
173
|
+
out[key] = value || null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setString('name')
|
|
177
|
+
setString('platform')
|
|
178
|
+
setString('status')
|
|
179
|
+
setNullableId('agentId')
|
|
180
|
+
setNullableId('chatroomId')
|
|
181
|
+
setNullableId('credentialId')
|
|
182
|
+
|
|
183
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'config')) {
|
|
184
|
+
out.config = normalizeConnectorConfig(raw.config)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'isEnabled')) {
|
|
188
|
+
out.isEnabled = raw.isEnabled === true
|
|
189
|
+
} else if (Object.prototype.hasOwnProperty.call(raw, 'enabled')) {
|
|
190
|
+
out.isEnabled = raw.enabled === true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!forUpdate) {
|
|
194
|
+
const platform = typeof out.platform === 'string' ? out.platform : 'discord'
|
|
195
|
+
return {
|
|
196
|
+
name: typeof out.name === 'string' && out.name ? out.name : 'Unnamed Connector',
|
|
197
|
+
platform,
|
|
198
|
+
agentId: Object.prototype.hasOwnProperty.call(out, 'agentId') ? out.agentId : null,
|
|
199
|
+
chatroomId: Object.prototype.hasOwnProperty.call(out, 'chatroomId') ? out.chatroomId : null,
|
|
200
|
+
credentialId: Object.prototype.hasOwnProperty.call(out, 'credentialId') ? out.credentialId : null,
|
|
201
|
+
config: Object.prototype.hasOwnProperty.call(out, 'config') ? out.config : {},
|
|
202
|
+
isEnabled: Object.prototype.hasOwnProperty.call(out, 'isEnabled') ? out.isEnabled : false,
|
|
203
|
+
...(typeof out.status === 'string' ? { status: out.status } : {}),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return out
|
|
208
|
+
}
|
|
209
|
+
|
|
121
210
|
const TASK_STATUS_VALUES = new Set([
|
|
122
211
|
'backlog',
|
|
123
212
|
'queued',
|
|
@@ -157,6 +246,95 @@ function pickFirstTaskId(value: unknown): string | null {
|
|
|
157
246
|
return ids[0] || null
|
|
158
247
|
}
|
|
159
248
|
|
|
249
|
+
function buildScheduleCreatorScope(schedule: Record<string, unknown> | null | undefined): {
|
|
250
|
+
agentId?: string | null
|
|
251
|
+
sessionId?: string | null
|
|
252
|
+
} | null {
|
|
253
|
+
if (!schedule || typeof schedule !== 'object') return null
|
|
254
|
+
const agentId = typeof schedule.createdByAgentId === 'string' && schedule.createdByAgentId.trim()
|
|
255
|
+
? schedule.createdByAgentId.trim()
|
|
256
|
+
: null
|
|
257
|
+
const sessionId = typeof schedule.createdInSessionId === 'string' && schedule.createdInSessionId.trim()
|
|
258
|
+
? schedule.createdInSessionId.trim()
|
|
259
|
+
: null
|
|
260
|
+
if (!agentId && !sessionId) return null
|
|
261
|
+
return { agentId, sessionId }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function deriveScheduleFollowupTarget(sessionId: string | null | undefined): {
|
|
265
|
+
followupConnectorId?: string | null
|
|
266
|
+
followupChannelId?: string | null
|
|
267
|
+
followupThreadId?: string | null
|
|
268
|
+
followupSenderId?: string | null
|
|
269
|
+
followupSenderName?: string | null
|
|
270
|
+
} {
|
|
271
|
+
const normalizedSessionId = typeof sessionId === 'string' ? sessionId.trim() : ''
|
|
272
|
+
if (!normalizedSessionId) return {}
|
|
273
|
+
|
|
274
|
+
const session = loadSessions()[normalizedSessionId] as {
|
|
275
|
+
connectorContext?: Record<string, unknown>
|
|
276
|
+
messages?: Array<Record<string, unknown>>
|
|
277
|
+
} | undefined
|
|
278
|
+
if (!session) return {}
|
|
279
|
+
|
|
280
|
+
const pickSourceFields = (source: Record<string, unknown> | null | undefined) => {
|
|
281
|
+
const connectorId = typeof source?.connectorId === 'string' ? source.connectorId.trim() : ''
|
|
282
|
+
const channelId = typeof source?.channelId === 'string' ? source.channelId.trim() : ''
|
|
283
|
+
if (!connectorId || !channelId) return {}
|
|
284
|
+
const threadId = typeof source?.threadId === 'string' ? source.threadId.trim() : ''
|
|
285
|
+
const senderId = typeof source?.senderId === 'string' ? source.senderId.trim() : ''
|
|
286
|
+
const senderName = typeof source?.senderName === 'string' ? source.senderName.trim() : ''
|
|
287
|
+
return {
|
|
288
|
+
followupConnectorId: connectorId,
|
|
289
|
+
followupChannelId: channelId,
|
|
290
|
+
followupThreadId: threadId || null,
|
|
291
|
+
followupSenderId: senderId || null,
|
|
292
|
+
followupSenderName: senderName || null,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const contextTarget = pickSourceFields(session.connectorContext || undefined)
|
|
297
|
+
if (contextTarget.followupConnectorId && contextTarget.followupChannelId) return contextTarget
|
|
298
|
+
|
|
299
|
+
const messages = Array.isArray(session.messages) ? session.messages : []
|
|
300
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
301
|
+
const message = messages[i]
|
|
302
|
+
if ((typeof message?.role === 'string' ? message.role : '') !== 'user') continue
|
|
303
|
+
if (message?.historyExcluded === true) continue
|
|
304
|
+
const messageTarget = pickSourceFields(message?.source as Record<string, unknown> | undefined)
|
|
305
|
+
if (messageTarget.followupConnectorId && messageTarget.followupChannelId) return messageTarget
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function findRelatedScheduleIds(
|
|
312
|
+
schedules: Record<string, ScheduleLike>,
|
|
313
|
+
schedule: Record<string, unknown> | null | undefined,
|
|
314
|
+
opts: { ignoreId?: string | null } = {},
|
|
315
|
+
): string[] {
|
|
316
|
+
if (!schedule || typeof schedule !== 'object') return []
|
|
317
|
+
const scope = buildScheduleCreatorScope(schedule)
|
|
318
|
+
if (!scope?.sessionId) return []
|
|
319
|
+
const matches = findEquivalentSchedules(schedules, {
|
|
320
|
+
id: typeof schedule.id === 'string' ? schedule.id : null,
|
|
321
|
+
agentId: typeof schedule.agentId === 'string' ? schedule.agentId : null,
|
|
322
|
+
taskPrompt: typeof schedule.taskPrompt === 'string' ? schedule.taskPrompt : null,
|
|
323
|
+
scheduleType: typeof schedule.scheduleType === 'string' ? schedule.scheduleType : null,
|
|
324
|
+
cron: typeof schedule.cron === 'string' ? schedule.cron : null,
|
|
325
|
+
intervalMs: typeof schedule.intervalMs === 'number' ? schedule.intervalMs : null,
|
|
326
|
+
runAt: typeof schedule.runAt === 'number' ? schedule.runAt : null,
|
|
327
|
+
createdByAgentId: scope.agentId,
|
|
328
|
+
createdInSessionId: scope.sessionId,
|
|
329
|
+
}, {
|
|
330
|
+
ignoreId: opts.ignoreId || (typeof schedule.id === 'string' ? schedule.id : null),
|
|
331
|
+
creatorScope: scope,
|
|
332
|
+
})
|
|
333
|
+
return Array.from(new Set(matches
|
|
334
|
+
.map((entry) => (typeof entry.id === 'string' ? entry.id : ''))
|
|
335
|
+
.filter(Boolean)))
|
|
336
|
+
}
|
|
337
|
+
|
|
160
338
|
function applyTaskContinuationDefaults(
|
|
161
339
|
parsed: Record<string, unknown>,
|
|
162
340
|
tasks: Record<string, BoardTask>,
|
|
@@ -285,11 +463,7 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
|
|
|
285
463
|
...p,
|
|
286
464
|
}),
|
|
287
465
|
manage_connectors: (p) => ({
|
|
288
|
-
|
|
289
|
-
platform: p.platform || 'discord',
|
|
290
|
-
agentId: p.agentId || null,
|
|
291
|
-
enabled: p.enabled ?? false,
|
|
292
|
-
...p,
|
|
466
|
+
...sanitizeConnectorCrudPayload(p as Record<string, unknown>),
|
|
293
467
|
}),
|
|
294
468
|
manage_webhooks: (p) => ({
|
|
295
469
|
name: p.name || 'Unnamed Webhook',
|
|
@@ -391,9 +565,9 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
391
565
|
description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field. Set "platformAssignScope":"all" to let an agent delegate work across the fleet; use "self" for solo execution.`
|
|
392
566
|
} else if (toolKey === 'manage_schedules') {
|
|
393
567
|
if (assignScope === 'self') {
|
|
394
|
-
description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set it explicitly to yourself. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
|
|
568
|
+
description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set it explicitly to yourself. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. Reuse or update an existing schedule you already created in this chat instead of making a near-duplicate. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true). For one-off reminders, prefer "once"; agent-created one-off schedules are cleaned up automatically after they finish. When the user says stop/pause/cancel a reminder, pause or delete every matching schedule you created in this chat, not just one row.`
|
|
395
569
|
} else {
|
|
396
|
-
description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).` + agentSummary
|
|
570
|
+
description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. Reuse or update an existing schedule you already created in this chat instead of making a near-duplicate. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true). For one-off reminders, prefer "once"; agent-created one-off schedules are cleaned up automatically after they finish. When the user says stop/pause/cancel a reminder, pause or delete every matching schedule you created in this chat, not just one row.` + agentSummary
|
|
397
571
|
}
|
|
398
572
|
if (ctx?.projectId) {
|
|
399
573
|
description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). Omit "projectId" to use this active project by default.`
|
|
@@ -593,11 +767,15 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
593
767
|
}
|
|
594
768
|
}
|
|
595
769
|
const newId = genId()
|
|
770
|
+
const scheduleFollowupTarget = toolKey === 'manage_schedules'
|
|
771
|
+
? deriveScheduleFollowupTarget(ctx?.sessionId || null)
|
|
772
|
+
: {}
|
|
596
773
|
const entry = {
|
|
597
774
|
id: newId,
|
|
598
775
|
...parsed,
|
|
599
776
|
createdByAgentId: ctx?.agentId || null,
|
|
600
777
|
createdInSessionId: ctx?.sessionId || null,
|
|
778
|
+
...scheduleFollowupTarget,
|
|
601
779
|
createdAt: now,
|
|
602
780
|
updatedAt: now,
|
|
603
781
|
}
|
|
@@ -665,8 +843,12 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
665
843
|
if (!effectiveId) return 'Error: "id" is required for update action.'
|
|
666
844
|
const all = res.load()
|
|
667
845
|
if (!all[effectiveId]) return `Not found: ${res.label} "${effectiveId}"`
|
|
846
|
+
const previousEntry = all[effectiveId]
|
|
847
|
+
let affectedScheduleIds: string[] | null = null
|
|
668
848
|
const parsed = toolKey === 'manage_projects'
|
|
669
849
|
? normalizeProjectPatchInput(buildCrudPayload(normalized, action, data))
|
|
850
|
+
: toolKey === 'manage_connectors'
|
|
851
|
+
? sanitizeConnectorCrudPayload(buildCrudPayload(normalized, action, data), { forUpdate: true })
|
|
670
852
|
: buildCrudPayload(normalized, action, data)
|
|
671
853
|
const parsedRecord = parsed as Record<string, unknown>
|
|
672
854
|
if (toolKey === 'manage_tasks') {
|
|
@@ -734,6 +916,21 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
734
916
|
...normalizedSchedule.value,
|
|
735
917
|
updatedAt: Date.now(),
|
|
736
918
|
}
|
|
919
|
+
const nextStatus = typeof all[effectiveId].status === 'string' ? all[effectiveId].status.trim().toLowerCase() : ''
|
|
920
|
+
if (nextStatus === 'paused' || nextStatus === 'completed' || nextStatus === 'failed') {
|
|
921
|
+
const relatedIds = findRelatedScheduleIds(all as Record<string, ScheduleLike>, previousEntry, {
|
|
922
|
+
ignoreId: effectiveId,
|
|
923
|
+
})
|
|
924
|
+
for (const relatedId of relatedIds) {
|
|
925
|
+
if (!all[relatedId]) continue
|
|
926
|
+
all[relatedId] = {
|
|
927
|
+
...all[relatedId],
|
|
928
|
+
status: nextStatus,
|
|
929
|
+
updatedAt: Date.now(),
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
affectedScheduleIds = [effectiveId, ...relatedIds]
|
|
933
|
+
}
|
|
737
934
|
}
|
|
738
935
|
if (toolKey === 'manage_secrets') {
|
|
739
936
|
if (!canAccessSecret(all[effectiveId])) return 'Error: you do not have access to this secret.'
|
|
@@ -807,6 +1004,12 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
807
1004
|
if (toolKey === 'manage_projects') {
|
|
808
1005
|
return JSON.stringify(buildProjectSnapshot(all[effectiveId]))
|
|
809
1006
|
}
|
|
1007
|
+
if (toolKey === 'manage_schedules' && affectedScheduleIds?.length) {
|
|
1008
|
+
return JSON.stringify({
|
|
1009
|
+
...all[effectiveId],
|
|
1010
|
+
affectedScheduleIds,
|
|
1011
|
+
})
|
|
1012
|
+
}
|
|
810
1013
|
return JSON.stringify(all[effectiveId])
|
|
811
1014
|
}
|
|
812
1015
|
if (action === 'delete') {
|
|
@@ -817,7 +1020,12 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
817
1020
|
if (toolKey === 'manage_secrets' && !canAccessSecret(all[effectiveId])) {
|
|
818
1021
|
return 'Error: you do not have access to this secret.'
|
|
819
1022
|
}
|
|
820
|
-
|
|
1023
|
+
const deletedIds = toolKey === 'manage_schedules'
|
|
1024
|
+
? [effectiveId, ...findRelatedScheduleIds(all as Record<string, ScheduleLike>, all[effectiveId], { ignoreId: effectiveId })]
|
|
1025
|
+
: [effectiveId]
|
|
1026
|
+
for (const deleteId of deletedIds) {
|
|
1027
|
+
delete all[deleteId]
|
|
1028
|
+
}
|
|
821
1029
|
res.save(all)
|
|
822
1030
|
if (toolKey === 'manage_projects') {
|
|
823
1031
|
const clearProjectId = (load: () => Record<string, Record<string, unknown>>, save: (d: Record<string, Record<string, unknown>>) => void) => {
|
|
@@ -837,7 +1045,10 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
837
1045
|
clearProjectId(loadSkills, saveSkills)
|
|
838
1046
|
clearProjectId(loadSecrets, saveSecrets)
|
|
839
1047
|
}
|
|
840
|
-
return JSON.stringify({
|
|
1048
|
+
return JSON.stringify({
|
|
1049
|
+
deleted: effectiveId,
|
|
1050
|
+
deletedIds,
|
|
1051
|
+
})
|
|
841
1052
|
}
|
|
842
1053
|
return `Unknown action "${action}". Valid: list, get, create, update, delete`
|
|
843
1054
|
} catch (err: any) {
|
|
@@ -206,6 +206,109 @@ describe('delegate fallback', () => {
|
|
|
206
206
|
assert.match(String(output.response || ''), /codex fallback ok/i)
|
|
207
207
|
})
|
|
208
208
|
|
|
209
|
+
it('uses nested data.task payloads from recent tool-call wrappers', () => {
|
|
210
|
+
const output = runWithFakeDelegates(`
|
|
211
|
+
const mod = await import('./src/lib/server/session-tools/delegate.ts')
|
|
212
|
+
const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
|
|
213
|
+
|
|
214
|
+
const tools = buildDelegateTools({
|
|
215
|
+
cwd: process.cwd(),
|
|
216
|
+
ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
|
|
217
|
+
hasPlugin: (name) => name === 'delegate',
|
|
218
|
+
hasTool: (name) => name === 'delegate',
|
|
219
|
+
cleanupFns: [],
|
|
220
|
+
commandTimeoutMs: 5000,
|
|
221
|
+
claudeTimeoutMs: 5000,
|
|
222
|
+
cliProcessTimeoutMs: 5000,
|
|
223
|
+
persistDelegateResumeId: () => {},
|
|
224
|
+
readStoredDelegateResumeId: () => null,
|
|
225
|
+
resolveCurrentSession: () => null,
|
|
226
|
+
activePlugins: ['delegate'],
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const delegateTool = tools.find((tool) => tool.name === 'delegate')
|
|
230
|
+
const raw = await delegateTool.invoke({
|
|
231
|
+
input: JSON.stringify({
|
|
232
|
+
data: {
|
|
233
|
+
task: 'Create a simple to-do list application.',
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
})
|
|
237
|
+
console.log(raw)
|
|
238
|
+
`)
|
|
239
|
+
|
|
240
|
+
assert.equal(output.backend, 'codex')
|
|
241
|
+
assert.equal(output.status, 'completed')
|
|
242
|
+
assert.match(String(output.response || ''), /codex fallback ok/i)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('falls back to reason text when malformed delegate wrappers omit task', () => {
|
|
246
|
+
const output = runWithFakeDelegates(`
|
|
247
|
+
const mod = await import('./src/lib/server/session-tools/delegate.ts')
|
|
248
|
+
const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
|
|
249
|
+
|
|
250
|
+
const tools = buildDelegateTools({
|
|
251
|
+
cwd: process.cwd(),
|
|
252
|
+
ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
|
|
253
|
+
hasPlugin: (name) => name === 'delegate',
|
|
254
|
+
hasTool: (name) => name === 'delegate',
|
|
255
|
+
cleanupFns: [],
|
|
256
|
+
commandTimeoutMs: 5000,
|
|
257
|
+
claudeTimeoutMs: 5000,
|
|
258
|
+
cliProcessTimeoutMs: 5000,
|
|
259
|
+
persistDelegateResumeId: () => {},
|
|
260
|
+
readStoredDelegateResumeId: () => null,
|
|
261
|
+
resolveCurrentSession: () => null,
|
|
262
|
+
activePlugins: ['delegate'],
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const delegateTool = tools.find((tool) => tool.name === 'delegate')
|
|
266
|
+
const raw = await delegateTool.invoke({
|
|
267
|
+
input: JSON.stringify({
|
|
268
|
+
parameters: {
|
|
269
|
+
tool_id: 'delegate',
|
|
270
|
+
reason: 'Building a simple front-end to-do list app is well-suited for a delegated agent.',
|
|
271
|
+
subagent_tool_id: 'agent_coder',
|
|
272
|
+
subagent_name: 'Coder',
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
})
|
|
276
|
+
console.log(raw)
|
|
277
|
+
`)
|
|
278
|
+
|
|
279
|
+
assert.equal(output.backend, 'codex')
|
|
280
|
+
assert.equal(output.status, 'completed')
|
|
281
|
+
assert.match(String(output.response || ''), /codex fallback ok/i)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('accepts legacy id fields for lifecycle delegate actions', () => {
|
|
285
|
+
const output = runWithFakeDelegates(`
|
|
286
|
+
const mod = await import('./src/lib/server/session-tools/delegate.ts')
|
|
287
|
+
const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
|
|
288
|
+
|
|
289
|
+
const tools = buildDelegateTools({
|
|
290
|
+
cwd: process.cwd(),
|
|
291
|
+
ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
|
|
292
|
+
hasPlugin: (name) => name === 'delegate',
|
|
293
|
+
hasTool: (name) => name === 'delegate',
|
|
294
|
+
cleanupFns: [],
|
|
295
|
+
commandTimeoutMs: 5000,
|
|
296
|
+
claudeTimeoutMs: 5000,
|
|
297
|
+
cliProcessTimeoutMs: 5000,
|
|
298
|
+
persistDelegateResumeId: () => {},
|
|
299
|
+
readStoredDelegateResumeId: () => null,
|
|
300
|
+
resolveCurrentSession: () => null,
|
|
301
|
+
activePlugins: ['delegate'],
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const delegateTool = tools.find((tool) => tool.name === 'delegate')
|
|
305
|
+
const raw = await delegateTool.invoke({ action: 'status', id: 'job-123' })
|
|
306
|
+
console.log(JSON.stringify({ raw }))
|
|
307
|
+
`)
|
|
308
|
+
|
|
309
|
+
assert.match(String(output.raw || ''), /delegation job "job-123" not found/i)
|
|
310
|
+
})
|
|
311
|
+
|
|
209
312
|
it('ranks authenticated delegate backends ahead of unauthenticated ones', () => {
|
|
210
313
|
const output = runWithFakeDelegates(`
|
|
211
314
|
const mod = await import('./src/lib/server/provider-health.ts')
|
|
@@ -147,6 +147,41 @@ function coerceDelegateBackend(value: unknown): DelegateBackend | null {
|
|
|
147
147
|
return null
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
function asDelegateRecord(value: unknown): Record<string, unknown> | null {
|
|
151
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
152
|
+
? value as Record<string, unknown>
|
|
153
|
+
: null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function pickNonEmptyDelegateString(...values: unknown[]): string | null {
|
|
157
|
+
for (const value of values) {
|
|
158
|
+
if (typeof value !== 'string') continue
|
|
159
|
+
const trimmed = value.trim()
|
|
160
|
+
if (!trimmed) continue
|
|
161
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
return trimmed
|
|
165
|
+
}
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pickDelegateTaskText(record: Record<string, unknown> | null): string | null {
|
|
170
|
+
if (!record) return null
|
|
171
|
+
return pickNonEmptyDelegateString(
|
|
172
|
+
record.task,
|
|
173
|
+
record.prompt,
|
|
174
|
+
record.request,
|
|
175
|
+
record.instructions,
|
|
176
|
+
record.instruction,
|
|
177
|
+
record.description,
|
|
178
|
+
record.input,
|
|
179
|
+
record.reason,
|
|
180
|
+
record.goal,
|
|
181
|
+
record.objective,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
150
185
|
function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): string | null {
|
|
151
186
|
const action = String(normalized.action || '').trim().toLowerCase()
|
|
152
187
|
const target = [
|
|
@@ -200,20 +235,41 @@ function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): stri
|
|
|
200
235
|
|
|
201
236
|
function normalizeDelegateArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
|
|
202
237
|
const normalized = normalizeToolInputArgs(rawArgs)
|
|
238
|
+
const nestedData = asDelegateRecord(normalized.data)
|
|
239
|
+
const delegatePayload = {
|
|
240
|
+
...(nestedData || {}),
|
|
241
|
+
...normalized,
|
|
242
|
+
}
|
|
203
243
|
const backend = coerceDelegateBackend(
|
|
204
|
-
|
|
205
|
-
??
|
|
206
|
-
??
|
|
207
|
-
??
|
|
208
|
-
??
|
|
244
|
+
delegatePayload.backend
|
|
245
|
+
?? delegatePayload.tool_name
|
|
246
|
+
?? delegatePayload.toolName
|
|
247
|
+
?? delegatePayload.tool
|
|
248
|
+
?? delegatePayload.delegate
|
|
249
|
+
?? delegatePayload.provider
|
|
250
|
+
?? delegatePayload.subagent_tool_id
|
|
251
|
+
?? delegatePayload.subagent_name,
|
|
209
252
|
)
|
|
210
253
|
if (backend && !normalized.backend) normalized.backend = backend
|
|
211
|
-
if (typeof normalized.task !== 'string'
|
|
212
|
-
|
|
254
|
+
if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
|
|
255
|
+
const directTask = pickDelegateTaskText(delegatePayload) || pickDelegateTaskText(nestedData)
|
|
256
|
+
if (directTask) normalized.task = directTask
|
|
257
|
+
}
|
|
258
|
+
const lifecycleJobId = pickNonEmptyDelegateString(
|
|
259
|
+
normalized.jobId,
|
|
260
|
+
normalized.id,
|
|
261
|
+
nestedData?.jobId,
|
|
262
|
+
nestedData?.id,
|
|
263
|
+
)
|
|
264
|
+
if (lifecycleJobId && (!normalized.jobId || typeof normalized.jobId !== 'string')) {
|
|
265
|
+
normalized.jobId = lifecycleJobId
|
|
266
|
+
}
|
|
267
|
+
const action = String(normalized.action ?? nestedData?.action ?? '').trim().toLowerCase()
|
|
213
268
|
const isLifecycleAction = ['status', 'list', 'wait', 'cancel'].includes(action)
|
|
269
|
+
if (action) normalized.action = action
|
|
214
270
|
if (!isLifecycleAction) {
|
|
215
271
|
if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
|
|
216
|
-
const synthesized = buildDelegateTaskFromPayload(
|
|
272
|
+
const synthesized = buildDelegateTaskFromPayload(delegatePayload)
|
|
217
273
|
if (synthesized) normalized.task = synthesized
|
|
218
274
|
}
|
|
219
275
|
normalized.action = 'start'
|
|
@@ -167,4 +167,146 @@ describe('discovery approval flows', () => {
|
|
|
167
167
|
assert.equal(output.toolNames.includes('manage_schedules'), true)
|
|
168
168
|
assert.equal(output.toolNames.includes('manage_platform'), false)
|
|
169
169
|
})
|
|
170
|
+
|
|
171
|
+
it('session-granted builtins disabled by default still appear in the next turn tool list', () => {
|
|
172
|
+
const output = runWithTempDataDir(`
|
|
173
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
174
|
+
const toolsMod = await import('./src/lib/server/session-tools/index.ts')
|
|
175
|
+
const storage = storageMod.default || storageMod
|
|
176
|
+
const toolsApi = toolsMod.default || toolsMod
|
|
177
|
+
|
|
178
|
+
const now = Date.now()
|
|
179
|
+
storage.saveSessions({
|
|
180
|
+
session_email: {
|
|
181
|
+
id: 'session_email',
|
|
182
|
+
name: 'Email Tool Test',
|
|
183
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
184
|
+
user: 'tester',
|
|
185
|
+
provider: 'openai',
|
|
186
|
+
model: 'gpt-test',
|
|
187
|
+
claudeSessionId: null,
|
|
188
|
+
messages: [],
|
|
189
|
+
createdAt: now,
|
|
190
|
+
lastActiveAt: now,
|
|
191
|
+
sessionType: 'human',
|
|
192
|
+
agentId: 'default',
|
|
193
|
+
plugins: ['email'],
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['email'], {
|
|
198
|
+
sessionId: 'session_email',
|
|
199
|
+
agentId: 'default',
|
|
200
|
+
platformAssignScope: 'self',
|
|
201
|
+
})
|
|
202
|
+
console.log(JSON.stringify({
|
|
203
|
+
toolNames: built.tools.map((entry) => entry.name).sort(),
|
|
204
|
+
}))
|
|
205
|
+
`)
|
|
206
|
+
|
|
207
|
+
assert.equal(output.toolNames.includes('email'), true)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('discover reports session-granted builtin tools as available now', () => {
|
|
211
|
+
const output = runWithTempDataDir(`
|
|
212
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
213
|
+
const toolsMod = await import('./src/lib/server/session-tools/index.ts')
|
|
214
|
+
const storage = storageMod.default || storageMod
|
|
215
|
+
const toolsApi = toolsMod.default || toolsMod
|
|
216
|
+
|
|
217
|
+
const now = Date.now()
|
|
218
|
+
storage.saveSessions({
|
|
219
|
+
session_discover_email: {
|
|
220
|
+
id: 'session_discover_email',
|
|
221
|
+
name: 'Discovery Email Test',
|
|
222
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
223
|
+
user: 'tester',
|
|
224
|
+
provider: 'openai',
|
|
225
|
+
model: 'gpt-test',
|
|
226
|
+
claudeSessionId: null,
|
|
227
|
+
messages: [],
|
|
228
|
+
createdAt: now,
|
|
229
|
+
lastActiveAt: now,
|
|
230
|
+
sessionType: 'human',
|
|
231
|
+
agentId: 'default',
|
|
232
|
+
plugins: ['email'],
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['email'], {
|
|
237
|
+
sessionId: 'session_discover_email',
|
|
238
|
+
agentId: 'default',
|
|
239
|
+
platformAssignScope: 'self',
|
|
240
|
+
})
|
|
241
|
+
const tool = built.tools.find((entry) => entry.name === 'manage_capabilities')
|
|
242
|
+
const raw = await tool.invoke({ action: 'discover', reason: 'Check runtime tool availability.' })
|
|
243
|
+
const plugins = JSON.parse(raw)
|
|
244
|
+
const email = plugins.find((entry) => entry.id === 'email')
|
|
245
|
+
console.log(JSON.stringify({
|
|
246
|
+
email,
|
|
247
|
+
}))
|
|
248
|
+
`)
|
|
249
|
+
|
|
250
|
+
assert.equal(output.email.granted, true)
|
|
251
|
+
assert.equal(output.email.availableNow, true)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('hydrates agent-approved tools into stale connector sessions on the next turn', () => {
|
|
255
|
+
const output = runWithTempDataDir(`
|
|
256
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
257
|
+
const toolsMod = await import('./src/lib/server/session-tools/index.ts')
|
|
258
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
259
|
+
const storage = storageMod.default || storageMod
|
|
260
|
+
const toolsApi = toolsMod.default || toolsMod
|
|
261
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
262
|
+
|
|
263
|
+
const now = Date.now()
|
|
264
|
+
storage.saveSettings({ approvalsEnabled: true })
|
|
265
|
+
storage.saveSessions({
|
|
266
|
+
connector_session: {
|
|
267
|
+
id: 'connector_session',
|
|
268
|
+
name: 'Connector Session',
|
|
269
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
270
|
+
user: 'connector',
|
|
271
|
+
provider: 'openai',
|
|
272
|
+
model: 'gpt-test',
|
|
273
|
+
claudeSessionId: null,
|
|
274
|
+
messages: [],
|
|
275
|
+
createdAt: now,
|
|
276
|
+
lastActiveAt: now,
|
|
277
|
+
sessionType: 'human',
|
|
278
|
+
agentId: 'agent_1',
|
|
279
|
+
plugins: ['browser'],
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const approval = approvals.requestApproval({
|
|
284
|
+
category: 'tool_access',
|
|
285
|
+
title: 'Enable connector tool',
|
|
286
|
+
description: 'Grant connector messaging',
|
|
287
|
+
data: { toolId: 'connector_message_tool', pluginId: 'connector_message_tool' },
|
|
288
|
+
agentId: 'agent_1',
|
|
289
|
+
sessionId: null,
|
|
290
|
+
})
|
|
291
|
+
await approvals.submitDecision(approval.id, true)
|
|
292
|
+
|
|
293
|
+
const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['browser'], {
|
|
294
|
+
sessionId: 'connector_session',
|
|
295
|
+
agentId: 'agent_1',
|
|
296
|
+
platformAssignScope: 'self',
|
|
297
|
+
})
|
|
298
|
+
try {
|
|
299
|
+
const session = storage.loadSessions().connector_session
|
|
300
|
+
console.log(JSON.stringify({
|
|
301
|
+
toolNames: built.tools.map((entry) => entry.name).sort(),
|
|
302
|
+
plugins: session.plugins || [],
|
|
303
|
+
}))
|
|
304
|
+
} finally {
|
|
305
|
+
await built.cleanup()
|
|
306
|
+
}
|
|
307
|
+
`)
|
|
308
|
+
|
|
309
|
+
assert.equal(output.toolNames.includes('connector_message_tool'), true)
|
|
310
|
+
assert.equal(output.plugins.includes('connector_message_tool'), true)
|
|
311
|
+
})
|
|
170
312
|
})
|