@swarmclawai/swarmclaw 0.7.2 → 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 +81 -22
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +36 -7
- package/src/app/api/agents/route.ts +12 -1
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +18 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/route.ts +16 -0
- 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 +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- 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/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +4 -0
- package/src/cli/index.ts +3 -10
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +207 -16
- package/src/components/agents/inspector-panel.tsx +108 -48
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/chat/chat-area.tsx +29 -13
- package/src/components/chat/chat-card.tsx +4 -20
- package/src/components/chat/chat-header.tsx +255 -353
- package/src/components/chat/chat-list.tsx +7 -9
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +3 -1
- 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 +217 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/layout/app-layout.tsx +383 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -24
- 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 +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- 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 +245 -46
- 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 +74 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- 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/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- 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/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 +250 -61
- 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 +45 -5
- 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 +946 -110
- 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/daemon-state.ts +59 -1
- 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 +13 -39
- 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 +27 -967
- 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 +17 -6
- package/src/lib/server/orchestrator.ts +2 -2
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +822 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/queue.ts +3 -20
- 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 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +70 -32
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery.ts +22 -4
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- 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 +237 -24
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +56 -1
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +150 -7
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +86 -23
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +36 -3
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/subagent.ts +193 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +896 -100
- package/src/lib/server/storage.ts +226 -7
- package/src/lib/server/stream-agent-chat.ts +46 -21
- 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 +44 -7
- package/src/lib/server/tool-capability-policy.ts +6 -0
- 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/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +7 -0
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +0 -6
- package/src/stores/use-chat-store.ts +31 -2
- package/src/types/index.ts +287 -44
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -156,8 +156,13 @@ const COLLECTIONS = [
|
|
|
156
156
|
'souls',
|
|
157
157
|
'benchmarks',
|
|
158
158
|
'approvals',
|
|
159
|
+
'browser_sessions',
|
|
160
|
+
'watch_jobs',
|
|
161
|
+
'delegation_jobs',
|
|
159
162
|
] as const
|
|
160
163
|
|
|
164
|
+
export type StorageCollection = (typeof COLLECTIONS)[number]
|
|
165
|
+
|
|
161
166
|
for (const table of COLLECTIONS) {
|
|
162
167
|
db.exec(`CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)
|
|
163
168
|
}
|
|
@@ -185,12 +190,34 @@ function getCollectionRawCache(table: string): LRUMap<string, string> {
|
|
|
185
190
|
return loaded
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
function normalizeStoredRecord(table: string, value: any): any {
|
|
194
|
+
if (table !== 'sessions') return value
|
|
195
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return value
|
|
196
|
+
|
|
197
|
+
const session = value as Record<string, any>
|
|
198
|
+
if (session.sessionType !== 'human') session.sessionType = 'human'
|
|
199
|
+
const isLegacyShortcut = (
|
|
200
|
+
(typeof session.id === 'string' && session.id.startsWith('agent-thread-'))
|
|
201
|
+
|| (typeof session.name === 'string' && session.name.startsWith('agent-thread:'))
|
|
202
|
+
)
|
|
203
|
+
if (
|
|
204
|
+
isLegacyShortcut
|
|
205
|
+
&& typeof session.agentId === 'string'
|
|
206
|
+
&& session.agentId.trim()
|
|
207
|
+
&& (!session.shortcutForAgentId || session.shortcutForAgentId !== session.agentId)
|
|
208
|
+
) {
|
|
209
|
+
session.shortcutForAgentId = session.agentId
|
|
210
|
+
}
|
|
211
|
+
if ('mainLoopState' in session) delete session.mainLoopState
|
|
212
|
+
return session
|
|
213
|
+
}
|
|
214
|
+
|
|
188
215
|
function loadCollection(table: string): Record<string, any> {
|
|
189
216
|
const raw = getCollectionRawCache(table)
|
|
190
217
|
const result: Record<string, any> = {}
|
|
191
218
|
for (const [id, data] of raw.entries()) {
|
|
192
219
|
try {
|
|
193
|
-
result[id] = JSON.parse(data)
|
|
220
|
+
result[id] = normalizeStoredRecord(table, JSON.parse(data))
|
|
194
221
|
} catch {
|
|
195
222
|
// Ignore malformed records instead of crashing list endpoints.
|
|
196
223
|
}
|
|
@@ -205,7 +232,8 @@ function saveCollection(table: string, data: Record<string, any>) {
|
|
|
205
232
|
const toDelete: string[] = []
|
|
206
233
|
|
|
207
234
|
for (const [id, val] of Object.entries(data)) {
|
|
208
|
-
const
|
|
235
|
+
const normalized = normalizeStoredRecord(table, val)
|
|
236
|
+
const serialized = JSON.stringify(normalized)
|
|
209
237
|
if (typeof serialized !== 'string') continue
|
|
210
238
|
next.set(id, serialized)
|
|
211
239
|
if (current.get(id) !== serialized) {
|
|
@@ -251,7 +279,7 @@ function deleteCollectionItem(table: string, id: string) {
|
|
|
251
279
|
* concurrent processes are modifying different items.
|
|
252
280
|
*/
|
|
253
281
|
function upsertCollectionItem(table: string, id: string, value: any) {
|
|
254
|
-
const serialized = JSON.stringify(value)
|
|
282
|
+
const serialized = JSON.stringify(normalizeStoredRecord(table, value))
|
|
255
283
|
db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`).run(id, serialized)
|
|
256
284
|
// Update the in-memory cache
|
|
257
285
|
const cached = collectionCache.get(table)
|
|
@@ -260,6 +288,51 @@ function upsertCollectionItem(table: string, id: string, value: any) {
|
|
|
260
288
|
}
|
|
261
289
|
}
|
|
262
290
|
|
|
291
|
+
function loadCollectionItem(table: string, id: string): any | null {
|
|
292
|
+
const row = db.prepare(`SELECT data FROM ${table} WHERE id = ?`).get(id) as { data: string } | undefined
|
|
293
|
+
if (!row) return null
|
|
294
|
+
try {
|
|
295
|
+
return normalizeStoredRecord(table, JSON.parse(row.data))
|
|
296
|
+
} catch {
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function upsertCollectionItems(table: string, entries: Array<[string, any]>): void {
|
|
302
|
+
if (!entries.length) return
|
|
303
|
+
const prepared = entries
|
|
304
|
+
.map(([id, value]) => [id, JSON.stringify(normalizeStoredRecord(table, value))] as const)
|
|
305
|
+
.filter(([, serialized]) => typeof serialized === 'string')
|
|
306
|
+
if (!prepared.length) return
|
|
307
|
+
|
|
308
|
+
const transaction = db.transaction(() => {
|
|
309
|
+
const upsert = db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`)
|
|
310
|
+
for (const [id, serialized] of prepared) {
|
|
311
|
+
upsert.run(id, serialized)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
transaction()
|
|
315
|
+
|
|
316
|
+
const cached = collectionCache.get(table)
|
|
317
|
+
if (cached) {
|
|
318
|
+
for (const [id, serialized] of prepared) {
|
|
319
|
+
cached.set(id, serialized)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function loadStoredItem(table: StorageCollection, id: string): any | null {
|
|
325
|
+
return loadCollectionItem(table, id)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function upsertStoredItem(table: StorageCollection, id: string, value: any): void {
|
|
329
|
+
upsertCollectionItem(table, id, value)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function upsertStoredItems(table: StorageCollection, entries: Array<[string, any]>): void {
|
|
333
|
+
upsertCollectionItems(table, entries)
|
|
334
|
+
}
|
|
335
|
+
|
|
263
336
|
function loadSingleton(table: string, fallback: any): any {
|
|
264
337
|
const row = db.prepare(`SELECT data FROM ${table} WHERE id = 1`).get() as { data: string } | undefined
|
|
265
338
|
return row ? JSON.parse(row.data) : fallback
|
|
@@ -392,7 +465,7 @@ if (!IS_BUILD_BOOTSTRAP) {
|
|
|
392
465
|
|
|
393
466
|
## Platform
|
|
394
467
|
|
|
395
|
-
- **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description.
|
|
468
|
+
- **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Enable cross-agent delegation when an agent should assign work to others.
|
|
396
469
|
- **Providers** — Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, OpenClaw, or custom OpenAI-compatible endpoints.
|
|
397
470
|
- **Tasks** — The Task Board tracks work items. Assign agents and they'll execute autonomously.
|
|
398
471
|
- **Schedules** — Cron-based recurring jobs that run agents or tasks automatically.
|
|
@@ -419,7 +492,7 @@ Use your platform management tools proactively:
|
|
|
419
492
|
You have opinions about good agent design. You suggest creative approaches, warn about common pitfalls, and get excited when someone gets something cool working. You're not a manual — you're a collaborator.
|
|
420
493
|
|
|
421
494
|
Be concise but not curt. Warmth doesn't require verbosity. When someone asks "how do I...?", give them the direct steps. Offer to do things rather than just explaining — if someone wants an agent created, create it. Use your tools when actions speak louder than words. If you don't know something, say so honestly.`,
|
|
422
|
-
isOrchestrator:
|
|
495
|
+
isOrchestrator: true,
|
|
423
496
|
plugins: defaultStarterTools,
|
|
424
497
|
heartbeatEnabled: true,
|
|
425
498
|
platformAssignScope: 'all',
|
|
@@ -440,6 +513,15 @@ Be concise but not curt. Warmth doesn't require verbosity. When someone asks "ho
|
|
|
440
513
|
existing.plugins = mergedPlugins
|
|
441
514
|
delete existing.tools
|
|
442
515
|
existing.updatedAt = Date.now()
|
|
516
|
+
}
|
|
517
|
+
if (existing.platformAssignScope === 'all' || existing.platformAssignScope === 'self') {
|
|
518
|
+
const derivedIsOrchestrator = existing.platformAssignScope === 'all'
|
|
519
|
+
if (existing.isOrchestrator !== derivedIsOrchestrator) {
|
|
520
|
+
existing.isOrchestrator = derivedIsOrchestrator
|
|
521
|
+
existing.updatedAt = Date.now()
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (JSON.stringify(JSON.parse(row.data)) !== JSON.stringify(existing)) {
|
|
443
525
|
db.prepare('UPDATE agents SET data = ? WHERE id = ?').run(JSON.stringify(existing), 'default')
|
|
444
526
|
}
|
|
445
527
|
} catch {
|
|
@@ -685,12 +767,106 @@ export function saveQueue(q: string[]) {
|
|
|
685
767
|
}
|
|
686
768
|
|
|
687
769
|
// --- Settings ---
|
|
770
|
+
const APP_SETTINGS_SECRET_FIELDS = [
|
|
771
|
+
'elevenLabsApiKey',
|
|
772
|
+
'tavilyApiKey',
|
|
773
|
+
'braveApiKey',
|
|
774
|
+
] as const
|
|
775
|
+
|
|
776
|
+
const ENCRYPTED_APP_SETTINGS_KEY = '__encryptedAppSettings'
|
|
777
|
+
|
|
778
|
+
type PersistedSettingsRecord = Record<string, any> & {
|
|
779
|
+
[ENCRYPTED_APP_SETTINGS_KEY]?: Record<string, string>
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function cloneRecord<T extends Record<string, any>>(value: T): T {
|
|
783
|
+
return JSON.parse(JSON.stringify(value || {})) as T
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function isPlainRecord(value: unknown): value is Record<string, any> {
|
|
787
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function getEncryptedAppSettings(settings: PersistedSettingsRecord): Record<string, string> {
|
|
791
|
+
return isPlainRecord(settings[ENCRYPTED_APP_SETTINGS_KEY])
|
|
792
|
+
? { ...(settings[ENCRYPTED_APP_SETTINGS_KEY] as Record<string, string>) }
|
|
793
|
+
: {}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function isClearedSecretValue(value: unknown): boolean {
|
|
797
|
+
return value === null || (typeof value === 'string' && value.trim() === '')
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function isProvidedSecretValue(value: unknown): value is string {
|
|
801
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function buildPersistedSettings(input: Record<string, any>, existing?: PersistedSettingsRecord): PersistedSettingsRecord {
|
|
805
|
+
const next = cloneRecord(input) as PersistedSettingsRecord
|
|
806
|
+
const encrypted = {
|
|
807
|
+
...(existing ? getEncryptedAppSettings(existing) : {}),
|
|
808
|
+
...getEncryptedAppSettings(next),
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
delete next[ENCRYPTED_APP_SETTINGS_KEY]
|
|
812
|
+
|
|
813
|
+
for (const field of APP_SETTINGS_SECRET_FIELDS) {
|
|
814
|
+
const raw = next[field]
|
|
815
|
+
if (isClearedSecretValue(raw)) {
|
|
816
|
+
delete encrypted[field]
|
|
817
|
+
delete next[field]
|
|
818
|
+
continue
|
|
819
|
+
}
|
|
820
|
+
if (isProvidedSecretValue(raw)) {
|
|
821
|
+
encrypted[field] = encryptKey(raw)
|
|
822
|
+
delete next[field]
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (Object.keys(encrypted).length > 0) next[ENCRYPTED_APP_SETTINGS_KEY] = encrypted
|
|
827
|
+
return next
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function resolveSettingsSecrets(settings: PersistedSettingsRecord): Record<string, any> {
|
|
831
|
+
const resolved = cloneRecord(settings)
|
|
832
|
+
delete resolved[ENCRYPTED_APP_SETTINGS_KEY]
|
|
833
|
+
|
|
834
|
+
const encrypted = getEncryptedAppSettings(settings)
|
|
835
|
+
for (const field of APP_SETTINGS_SECRET_FIELDS) {
|
|
836
|
+
if (isProvidedSecretValue(resolved[field])) continue
|
|
837
|
+
const value = encrypted[field]
|
|
838
|
+
if (typeof value !== 'string' || !value) continue
|
|
839
|
+
try {
|
|
840
|
+
resolved[field] = decryptKey(value)
|
|
841
|
+
} catch {
|
|
842
|
+
// Ignore malformed encrypted settings instead of breaking all settings reads.
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return resolved
|
|
847
|
+
}
|
|
848
|
+
|
|
688
849
|
export function loadSettings(): Record<string, any> {
|
|
689
|
-
|
|
850
|
+
const persisted = loadSingleton('settings', {}) as PersistedSettingsRecord
|
|
851
|
+
const normalized = buildPersistedSettings(persisted, persisted)
|
|
852
|
+
if (JSON.stringify(persisted) !== JSON.stringify(normalized)) {
|
|
853
|
+
saveSingleton('settings', normalized)
|
|
854
|
+
}
|
|
855
|
+
return resolveSettingsSecrets(normalized)
|
|
690
856
|
}
|
|
691
857
|
|
|
692
858
|
export function saveSettings(s: Record<string, any>) {
|
|
693
|
-
|
|
859
|
+
const existing = loadSingleton('settings', {}) as PersistedSettingsRecord
|
|
860
|
+
saveSingleton('settings', buildPersistedSettings(s, existing))
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export function loadPublicSettings(): Record<string, any> {
|
|
864
|
+
const settings = cloneRecord(loadSettings())
|
|
865
|
+
for (const field of APP_SETTINGS_SECRET_FIELDS) {
|
|
866
|
+
settings[`${field}Configured`] = isProvidedSecretValue(settings[field])
|
|
867
|
+
settings[field] = null
|
|
868
|
+
}
|
|
869
|
+
return settings
|
|
694
870
|
}
|
|
695
871
|
|
|
696
872
|
// --- Secrets (service keys for orchestrators) ---
|
|
@@ -1026,6 +1202,49 @@ export function loadApprovals(): Record<string, unknown> {
|
|
|
1026
1202
|
return loadCollection('approvals')
|
|
1027
1203
|
}
|
|
1028
1204
|
|
|
1205
|
+
// --- Browser Sessions ---
|
|
1206
|
+
export function loadBrowserSessions(): Record<string, unknown> {
|
|
1207
|
+
return loadCollection('browser_sessions')
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
export function upsertBrowserSession(id: string, data: unknown) {
|
|
1211
|
+
upsertCollectionItem('browser_sessions', id, data)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export function deleteBrowserSession(id: string) {
|
|
1215
|
+
deleteCollectionItem('browser_sessions', id)
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// --- Watch Jobs ---
|
|
1219
|
+
export function loadWatchJobs(): Record<string, unknown> {
|
|
1220
|
+
return loadCollection('watch_jobs')
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
export function upsertWatchJob(id: string, data: unknown) {
|
|
1224
|
+
upsertCollectionItem('watch_jobs', id, data)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
export function upsertWatchJobs(entries: Array<[string, unknown]>) {
|
|
1228
|
+
upsertCollectionItems('watch_jobs', entries)
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
export function deleteWatchJob(id: string) {
|
|
1232
|
+
deleteCollectionItem('watch_jobs', id)
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// --- Delegation Jobs ---
|
|
1236
|
+
export function loadDelegationJobs(): Record<string, unknown> {
|
|
1237
|
+
return loadCollection('delegation_jobs')
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
export function upsertDelegationJob(id: string, data: unknown) {
|
|
1241
|
+
upsertCollectionItem('delegation_jobs', id, data)
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
export function deleteDelegationJob(id: string) {
|
|
1245
|
+
deleteCollectionItem('delegation_jobs', id)
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1029
1248
|
export function upsertApproval(id: string, approval: unknown) {
|
|
1030
1249
|
upsertCollectionItem('approvals', id, approval)
|
|
1031
1250
|
}
|
|
@@ -13,6 +13,7 @@ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
|
13
13
|
import { expandPluginIds } from './tool-aliases'
|
|
14
14
|
import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
|
|
15
15
|
import { extractSuggestions } from './suggestions'
|
|
16
|
+
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
16
17
|
|
|
17
18
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
18
19
|
interface StreamAgentChatOpts {
|
|
@@ -59,7 +60,7 @@ const GOAL_DECOMPOSITION_BLOCK = [
|
|
|
59
60
|
'When you receive a broad, open-ended goal:',
|
|
60
61
|
'1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
|
|
61
62
|
'2. If manage_tasks is available, create a task for each subtask to track progress.',
|
|
62
|
-
'3.
|
|
63
|
+
'3. Present the plan as a short checklist or numbered list in plain language.',
|
|
63
64
|
'4. Execute the first subtask immediately — do not stop after planning.',
|
|
64
65
|
'5. After each subtask, update progress and move to the next.',
|
|
65
66
|
].join('\n')
|
|
@@ -71,7 +72,6 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
71
72
|
heartbeatIntervalSec: number
|
|
72
73
|
platformAssignScope?: 'self' | 'all'
|
|
73
74
|
userMessage?: string
|
|
74
|
-
hasExistingPlan?: boolean
|
|
75
75
|
}) {
|
|
76
76
|
const hasTooling = opts.enabledPlugins.length > 0
|
|
77
77
|
const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
|
|
@@ -102,13 +102,15 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
102
102
|
'Always reply to: questions, tasks, emotional sharing, or when you have something useful to add.',
|
|
103
103
|
'Execute by default — only ask for confirmation on high-risk/irreversible actions. Do not end every response with a question.',
|
|
104
104
|
'Never repeat completed side effects. Verify state first.',
|
|
105
|
+
'If a tool returns an error or validation failure, do not claim the task succeeded. Retry with corrected arguments or explain the blocker plainly.',
|
|
106
|
+
'Delegation is optional, not a stopping condition. If one delegate backend is unavailable or unauthenticated, try another delegate backend or continue with your other tools.',
|
|
107
|
+
'Only mention files, screenshots, URLs, or download links that were actually returned by tools. Copy returned links exactly; do not rewrite them or prepend extra prefixes like "sandbox:".',
|
|
105
108
|
`Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
|
|
106
109
|
opts.heartbeatIntervalSec > 0 ? `Heartbeat cadence: ~${opts.heartbeatIntervalSec}s.` : '',
|
|
107
|
-
'For SWARM_MAIN_MISSION_TICK / SWARM_MAIN_AUTO_FOLLOWUP messages, follow the response contract and include [MAIN_LOOP_META] JSON.',
|
|
108
110
|
)
|
|
109
111
|
|
|
110
112
|
if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
|
|
111
|
-
if (opts.userMessage &&
|
|
113
|
+
if (opts.userMessage && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
|
|
112
114
|
|
|
113
115
|
return parts.filter(Boolean).join('\n')
|
|
114
116
|
}
|
|
@@ -136,7 +138,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
136
138
|
|
|
137
139
|
// Resolve agent's thinking level for provider-native params
|
|
138
140
|
let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
|
|
139
|
-
if (session.
|
|
141
|
+
if (session.thinkingLevel) {
|
|
142
|
+
agentThinkingLevel = session.thinkingLevel
|
|
143
|
+
} else if (session.agentId) {
|
|
140
144
|
const agentsForThinking = loadAgents()
|
|
141
145
|
agentThinkingLevel = agentsForThinking[session.agentId]?.thinkingLevel
|
|
142
146
|
}
|
|
@@ -192,6 +196,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
192
196
|
if (agent?.description) identityLines.push(agent.description)
|
|
193
197
|
identityLines.push('I should always refer to myself by this name. I am not "Assistant" — I have my own name and identity.')
|
|
194
198
|
stateModifierParts.push(identityLines.join(' '))
|
|
199
|
+
const continuityBlock = buildIdentityContinuityContext(session, agent)
|
|
200
|
+
if (continuityBlock) stateModifierParts.push(continuityBlock)
|
|
195
201
|
if (agent?.soul) stateModifierParts.push(agent.soul)
|
|
196
202
|
if (agent?.systemPrompt) stateModifierParts.push(agent.systemPrompt)
|
|
197
203
|
if (agent?.skillIds?.length) {
|
|
@@ -290,9 +296,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
290
296
|
)
|
|
291
297
|
}
|
|
292
298
|
|
|
293
|
-
// Check for existing plan in mainLoopState to skip decomposition injection
|
|
294
|
-
const hasExistingPlan = Array.isArray(session.mainLoopState?.planSteps) && session.mainLoopState.planSteps.length > 0
|
|
295
|
-
|
|
296
299
|
stateModifierParts.push(
|
|
297
300
|
buildAgenticExecutionPolicy({
|
|
298
301
|
enabledPlugins: sessionPlugins,
|
|
@@ -301,7 +304,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
301
304
|
heartbeatIntervalSec,
|
|
302
305
|
platformAssignScope: agentPlatformAssignScope,
|
|
303
306
|
userMessage: message,
|
|
304
|
-
hasExistingPlan,
|
|
305
307
|
}),
|
|
306
308
|
)
|
|
307
309
|
|
|
@@ -480,14 +482,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
480
482
|
let needsTextSeparator = false
|
|
481
483
|
let totalInputTokens = 0
|
|
482
484
|
let totalOutputTokens = 0
|
|
483
|
-
let lastToolInput: unknown = null
|
|
484
485
|
let accumulatedThinking = ''
|
|
485
486
|
const pluginInvocations: PluginInvocationRecord[] = []
|
|
486
487
|
let currentToolInputTokens = 0
|
|
487
488
|
|
|
488
489
|
// Plugin hooks: beforeAgentStart
|
|
489
490
|
const pluginMgr = getPluginManager()
|
|
490
|
-
await pluginMgr.runHook('beforeAgentStart', { session, message })
|
|
491
|
+
await pluginMgr.runHook('beforeAgentStart', { session, message }, { enabledIds: sessionPlugins })
|
|
491
492
|
|
|
492
493
|
const abortController = new AbortController()
|
|
493
494
|
const abortFromSignal = () => abortController.abort()
|
|
@@ -512,6 +513,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
512
513
|
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
|
|
513
514
|
for (let iteration = 0; iteration <= maxIterations; iteration++) {
|
|
514
515
|
let shouldContinue: 'recursion' | 'transient' | false = false
|
|
516
|
+
let waitingForToolResult = false
|
|
517
|
+
let idleTimedOut = false
|
|
518
|
+
let idleTimer: ReturnType<typeof setTimeout> | null = null
|
|
515
519
|
|
|
516
520
|
// Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
|
|
517
521
|
// Linked to the parent so client disconnect / timeout still propagates.
|
|
@@ -520,7 +524,24 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
520
524
|
if (abortController.signal.aborted) iterationController.abort()
|
|
521
525
|
else abortController.signal.addEventListener('abort', onParentAbort)
|
|
522
526
|
|
|
527
|
+
const clearIdleWatchdog = () => {
|
|
528
|
+
if (idleTimer) {
|
|
529
|
+
clearTimeout(idleTimer)
|
|
530
|
+
idleTimer = null
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const armIdleWatchdog = () => {
|
|
535
|
+
clearIdleWatchdog()
|
|
536
|
+
if (waitingForToolResult || iterationController.signal.aborted) return
|
|
537
|
+
idleTimer = setTimeout(() => {
|
|
538
|
+
idleTimedOut = true
|
|
539
|
+
iterationController.abort()
|
|
540
|
+
}, 90_000)
|
|
541
|
+
}
|
|
542
|
+
|
|
523
543
|
try {
|
|
544
|
+
armIdleWatchdog()
|
|
524
545
|
const eventStream = agent.streamEvents(
|
|
525
546
|
{ messages: langchainMessages },
|
|
526
547
|
{ version: 'v2', recursionLimit, signal: iterationController.signal },
|
|
@@ -530,6 +551,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
530
551
|
const kind = event.event
|
|
531
552
|
|
|
532
553
|
if (kind === 'on_chat_model_stream') {
|
|
554
|
+
armIdleWatchdog()
|
|
533
555
|
const chunk = event.data?.chunk
|
|
534
556
|
if (chunk?.content) {
|
|
535
557
|
// content can be string or array of content blocks
|
|
@@ -569,6 +591,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
569
591
|
}
|
|
570
592
|
}
|
|
571
593
|
} else if (kind === 'on_llm_end') {
|
|
594
|
+
armIdleWatchdog()
|
|
572
595
|
// Track token usage from LLM responses — check all known LangChain event shapes
|
|
573
596
|
const output = event.data?.output
|
|
574
597
|
const usage = output?.llmOutput?.tokenUsage
|
|
@@ -581,17 +604,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
581
604
|
totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
|
|
582
605
|
}
|
|
583
606
|
} else if (kind === 'on_tool_start') {
|
|
607
|
+
clearIdleWatchdog()
|
|
608
|
+
waitingForToolResult = true
|
|
584
609
|
hasToolCalls = true
|
|
585
610
|
needsTextSeparator = true
|
|
586
611
|
lastSegment = ''
|
|
587
612
|
const toolName = event.name || 'unknown'
|
|
588
613
|
const input = event.data?.input
|
|
589
|
-
lastToolInput = input
|
|
590
614
|
// Estimate input tokens for plugin invocation tracking
|
|
591
615
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
592
616
|
currentToolInputTokens = Math.ceil((inputStr?.length || 0) / 4)
|
|
593
|
-
// Plugin hooks: beforeToolExec
|
|
594
|
-
await pluginMgr.runHook('beforeToolExec', { toolName, input })
|
|
595
617
|
logExecution(session.id, 'tool_call', `${toolName} invoked`, {
|
|
596
618
|
agentId: session.agentId,
|
|
597
619
|
detail: { toolName, input: inputStr?.slice(0, 4000) },
|
|
@@ -602,6 +624,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
602
624
|
toolInput: inputStr,
|
|
603
625
|
})}\n\n`)
|
|
604
626
|
} else if (kind === 'on_tool_end') {
|
|
627
|
+
waitingForToolResult = false
|
|
628
|
+
armIdleWatchdog()
|
|
605
629
|
const toolName = event.name || 'unknown'
|
|
606
630
|
const output = event.data?.output
|
|
607
631
|
const outputStr = typeof output === 'string'
|
|
@@ -609,9 +633,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
609
633
|
: output?.content
|
|
610
634
|
? String(output.content)
|
|
611
635
|
: JSON.stringify(output)
|
|
612
|
-
// Plugin hooks: afterToolExec
|
|
613
|
-
await pluginMgr.runHook('afterToolExec', { session, toolName, input: lastToolInput as Record<string, unknown> | null, output: outputStr })
|
|
614
|
-
lastToolInput = null
|
|
615
636
|
logExecution(session.id, 'tool_result', `${toolName} returned`, {
|
|
616
637
|
agentId: session.agentId,
|
|
617
638
|
detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
|
|
@@ -654,7 +675,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
654
675
|
}
|
|
655
676
|
} catch (innerErr: unknown) {
|
|
656
677
|
const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
|
|
657
|
-
const errMsg =
|
|
678
|
+
const errMsg = idleTimedOut
|
|
679
|
+
? 'Model stream stalled without emitting text or tool results for 90 seconds.'
|
|
680
|
+
: innerErr instanceof Error ? innerErr.message : String(innerErr)
|
|
658
681
|
const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
|
|
659
682
|
|
|
660
683
|
// Classify the error:
|
|
@@ -662,9 +685,10 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
662
685
|
// 2. Transient abort/timeout — LLM API failure, not from client disconnect
|
|
663
686
|
const isRecursionError = errName === 'GraphRecursionError'
|
|
664
687
|
|| /recursion limit|maximum recursion/i.test(errMsg)
|
|
665
|
-
const isTransientAbort = !isRecursionError
|
|
688
|
+
const isTransientAbort = (!isRecursionError && idleTimedOut)
|
|
689
|
+
|| (!isRecursionError
|
|
666
690
|
&& /abort|timed?\s*out|ECONNRESET|ECONNREFUSED|socket hang up|network/i.test(errMsg)
|
|
667
|
-
&& !abortController.signal.aborted
|
|
691
|
+
&& !abortController.signal.aborted)
|
|
668
692
|
|
|
669
693
|
// Log diagnostic details for every error so we can trace root causes
|
|
670
694
|
console.error(`[stream-agent-chat] Error in streamEvents iteration=${iteration}`, {
|
|
@@ -695,6 +719,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
695
719
|
throw innerErr
|
|
696
720
|
}
|
|
697
721
|
} finally {
|
|
722
|
+
clearIdleWatchdog()
|
|
698
723
|
abortController.signal.removeEventListener('abort', onParentAbort)
|
|
699
724
|
}
|
|
700
725
|
|
|
@@ -775,7 +800,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
775
800
|
}
|
|
776
801
|
|
|
777
802
|
// Plugin hooks: afterAgentComplete
|
|
778
|
-
await pluginMgr.runHook('afterAgentComplete', { session, response: fullText })
|
|
803
|
+
await pluginMgr.runHook('afterAgentComplete', { session, response: fullText }, { enabledIds: sessionPlugins })
|
|
779
804
|
|
|
780
805
|
// OpenClaw auto-sync: push memory if enabled
|
|
781
806
|
try {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { afterEach, describe, it } from 'node:test'
|
|
3
|
+
import { PROVIDERS } from '../providers'
|
|
4
|
+
import { runStructuredExtraction } from './structured-extract'
|
|
5
|
+
|
|
6
|
+
const originalOllamaHandler = PROVIDERS.ollama.handler.streamChat
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
PROVIDERS.ollama.handler.streamChat = originalOllamaHandler
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('runStructuredExtraction', () => {
|
|
13
|
+
it('parses fenced JSON output from the current provider', async () => {
|
|
14
|
+
PROVIDERS.ollama.handler.streamChat = async () => '```json\n{"name":"Ada","score":10}\n```'
|
|
15
|
+
|
|
16
|
+
const result = await runStructuredExtraction({
|
|
17
|
+
session: {
|
|
18
|
+
id: 'session-1',
|
|
19
|
+
provider: 'ollama',
|
|
20
|
+
model: 'qwen3.5',
|
|
21
|
+
credentialId: null,
|
|
22
|
+
fallbackCredentialIds: [],
|
|
23
|
+
apiEndpoint: 'http://localhost:11434',
|
|
24
|
+
},
|
|
25
|
+
text: 'Ada scored 10.',
|
|
26
|
+
schema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
name: { type: 'string' },
|
|
30
|
+
score: { type: 'number' },
|
|
31
|
+
},
|
|
32
|
+
required: ['name', 'score'],
|
|
33
|
+
},
|
|
34
|
+
instruction: 'Extract the person and score.',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(result.object, { name: 'Ada', score: 10 })
|
|
38
|
+
assert.deepEqual(result.validationErrors, [])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('repairs invalid JSON with a second pass', async () => {
|
|
42
|
+
let callCount = 0
|
|
43
|
+
PROVIDERS.ollama.handler.streamChat = async () => {
|
|
44
|
+
callCount += 1
|
|
45
|
+
return callCount === 1 ? 'name: Ada' : '{"name":"Ada"}'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await runStructuredExtraction({
|
|
49
|
+
session: {
|
|
50
|
+
id: 'session-2',
|
|
51
|
+
provider: 'ollama',
|
|
52
|
+
model: 'qwen3.5',
|
|
53
|
+
credentialId: null,
|
|
54
|
+
fallbackCredentialIds: [],
|
|
55
|
+
apiEndpoint: 'http://localhost:11434',
|
|
56
|
+
},
|
|
57
|
+
text: 'Ada',
|
|
58
|
+
schema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
name: { type: 'string' },
|
|
62
|
+
},
|
|
63
|
+
required: ['name'],
|
|
64
|
+
},
|
|
65
|
+
instruction: 'Extract the name.',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
assert.equal(callCount, 2)
|
|
69
|
+
assert.deepEqual(result.object, { name: 'Ada' })
|
|
70
|
+
assert.deepEqual(result.validationErrors, [])
|
|
71
|
+
})
|
|
72
|
+
})
|