@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- 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]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- 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/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- 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/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- 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/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- 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 +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- 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 +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- 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 +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -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 +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- 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/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -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 +44 -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 +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- 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 +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- 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 +994 -130
- 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 +189 -10
- 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 +62 -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/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- 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 +31 -964
- 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 +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- 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 +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- 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 +128 -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 +96 -34
- 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-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- 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 +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- 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 +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- 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 +95 -25
- 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/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -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 +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- 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 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- 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/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import { createReactAgent } from '@langchain/langgraph/prebuilt'
|
|
3
3
|
import { HumanMessage, AIMessage } from '@langchain/core/messages'
|
|
4
|
+
import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
|
|
4
5
|
import { buildSessionTools } from './session-tools'
|
|
5
6
|
import { buildChatModel } from './build-llm'
|
|
6
7
|
import { loadSettings, loadAgents, loadSkills, appendUsage } from './storage'
|
|
@@ -13,6 +14,8 @@ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
|
13
14
|
import { expandPluginIds } from './tool-aliases'
|
|
14
15
|
import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
|
|
15
16
|
import { extractSuggestions } from './suggestions'
|
|
17
|
+
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
18
|
+
import { enqueueSystemEvent } from './system-events'
|
|
16
19
|
|
|
17
20
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
18
21
|
interface StreamAgentChatOpts {
|
|
@@ -42,6 +45,85 @@ function buildPluginCapabilityLines(enabledPlugins: string[], opts?: { platformA
|
|
|
42
45
|
return lines
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
49
|
+
const uniqueTools = Array.from(new Set(enabledPlugins.filter(Boolean))).sort()
|
|
50
|
+
if (uniqueTools.length === 0) return []
|
|
51
|
+
|
|
52
|
+
const lines = [
|
|
53
|
+
`Enabled tools in this session: ${uniqueTools.map((toolId) => `\`${toolId}\``).join(', ')}.`,
|
|
54
|
+
'Only call tools from this enabled list or tools explicitly returned by the runtime.',
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const directPlatformTools = uniqueTools.filter((toolId) => toolId.startsWith('manage_') && toolId !== 'manage_platform')
|
|
58
|
+
if (directPlatformTools.length > 0 && !uniqueTools.includes('manage_platform')) {
|
|
59
|
+
lines.push(`Use direct platform tools exactly as named (${directPlatformTools.map((toolId) => `\`${toolId}\``).join(', ')}). Do not substitute \`manage_platform\` unless it is explicitly enabled.`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (uniqueTools.includes('files')) {
|
|
63
|
+
lines.push('For `files`, include an explicit action whenever possible. Common patterns: `{"action":"list","dirPath":"."}`, `{"action":"read","filePath":"path/to/file.md"}`, and `{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}`.')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (uniqueTools.includes('shell')) {
|
|
67
|
+
lines.push('For `shell`, use `{"action":"execute","command":"..."}` for commands and `{"action":"status","processId":"..."}` or `{"action":"log","processId":"..."}` for long-lived processes.')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (uniqueTools.includes('web')) {
|
|
71
|
+
lines.push('For `web`, use `{"action":"search","query":"..."}` to research and `{"action":"fetch","url":"https://..."}` to read a specific page.')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (uniqueTools.includes('browser')) {
|
|
75
|
+
lines.push('For `browser`, when the task includes a literal URL, pass that exact URL string to `{"action":"navigate","url":"..."}`. Do not invent placeholder URLs like `[Your URL]`, `Example_URL`, or `MockMailPage_URL`.')
|
|
76
|
+
lines.push('For `browser` form work, prefer `{"action":"fill_form","fields":[{"element":"#email","value":"user@example.com"},{"element":"#password","value":"..."}]}`. A shorthand `form` object keyed by input id/name also works for simple forms.')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (uniqueTools.includes('http_request')) {
|
|
80
|
+
lines.push('For `http_request`, send exact literal URLs from the task or from prior tool results. Keep JSON request bodies as raw JSON strings.')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (uniqueTools.includes('email')) {
|
|
84
|
+
lines.push('For `email`, send mail with `{"action":"send","to":"user@example.com","subject":"...","body":"..."}`. If delivery depends on SMTP setup, check `{"action":"status"}` before claiming success.')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (uniqueTools.includes('ask_human')) {
|
|
88
|
+
lines.push('For `ask_human`, when a workflow needs a code, approval, or out-of-band value from a person, do not guess or keep re-submitting blank forms. Use `{"action":"request_input","question":"..."}` and, for durable pauses, `{"action":"wait_for_reply","correlationId":"..."}`.')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lines
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function looksLikeOpenEndedDeliverableTask(text: string): boolean {
|
|
95
|
+
const normalized = text.toLowerCase()
|
|
96
|
+
if (!normalized.trim()) return false
|
|
97
|
+
if (/```|package\.json|tsconfig|tsx?\b|jsx?\b|pytest|vitest|npm run|src\/|components\/|api\//.test(normalized)) return false
|
|
98
|
+
if (/\b(revise|revision|iterate|iteration|draft|deliverable|deliverables|offer|brief|copy|proposal|landing|outreach|plan|strategy|report|memo|document|docs?)\b/.test(normalized)) return true
|
|
99
|
+
return isBroadGoal(text) && /(\.md\b|\.txt\b|copy|brief|proposal|plan|report|draft|document)/.test(normalized)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
|
|
103
|
+
const normalized = userMessage.toLowerCase()
|
|
104
|
+
const required: string[] = []
|
|
105
|
+
|
|
106
|
+
if (enabledPlugins.includes('ask_human')
|
|
107
|
+
&& (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
|
|
108
|
+
required.push('ask_human')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (enabledPlugins.includes('email')
|
|
112
|
+
&& (/\bemail\b/.test(normalized) || /send a welcome email/.test(normalized) || /send an email/.test(normalized))) {
|
|
113
|
+
required.push('email')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return required
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const OPEN_ENDED_REVISION_BLOCK = [
|
|
120
|
+
'## Revision Loop',
|
|
121
|
+
'For open-ended deliverable work, do a real two-pass loop before declaring success: create the draft artifacts, critique them against the objective, then modify at least one artifact based on that critique.',
|
|
122
|
+
'A critique by itself does not count as iteration. Iteration requires an actual changed artifact.',
|
|
123
|
+
'When resuming in an existing workspace, inspect the current files first, then update them. Do not assume you lost access to the workspace without an explicit tool attempt.',
|
|
124
|
+
'If `files` is available, use it with explicit actions and paths to inspect and revise the artifacts.',
|
|
125
|
+
].join('\n')
|
|
126
|
+
|
|
45
127
|
/** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
|
|
46
128
|
function isBroadGoal(text: string): boolean {
|
|
47
129
|
if (text.length < 50) return false
|
|
@@ -59,7 +141,7 @@ const GOAL_DECOMPOSITION_BLOCK = [
|
|
|
59
141
|
'When you receive a broad, open-ended goal:',
|
|
60
142
|
'1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
|
|
61
143
|
'2. If manage_tasks is available, create a task for each subtask to track progress.',
|
|
62
|
-
'3.
|
|
144
|
+
'3. Present the plan as a short checklist or numbered list in plain language.',
|
|
63
145
|
'4. Execute the first subtask immediately — do not stop after planning.',
|
|
64
146
|
'5. After each subtask, update progress and move to the next.',
|
|
65
147
|
].join('\n')
|
|
@@ -71,10 +153,10 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
71
153
|
heartbeatIntervalSec: number
|
|
72
154
|
platformAssignScope?: 'self' | 'all'
|
|
73
155
|
userMessage?: string
|
|
74
|
-
hasExistingPlan?: boolean
|
|
75
156
|
}) {
|
|
76
157
|
const hasTooling = opts.enabledPlugins.length > 0
|
|
77
158
|
const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
|
|
159
|
+
const toolDisciplineLines = buildToolDisciplineLines(opts.enabledPlugins)
|
|
78
160
|
|
|
79
161
|
const parts: string[] = []
|
|
80
162
|
|
|
@@ -85,7 +167,8 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
85
167
|
? 'I take initiative — plan briefly, execute tools, evaluate, iterate until done. Never stop at advice when action is implied.'
|
|
86
168
|
: 'No tools enabled. Be explicit about what tool access is needed.',
|
|
87
169
|
'Follow through on stated intentions with tool calls. Never claim results without tool evidence.',
|
|
88
|
-
'If a
|
|
170
|
+
'If a task explicitly names an enabled tool, use that tool before declaring success. A prose request is not a substitute for `ask_human`, and browser work is not a substitute for `email` delivery.',
|
|
171
|
+
'When `ask_human` is enabled, collect required human input through the tool instead of asking for it only in plain assistant text.',
|
|
89
172
|
opts.loopMode === 'ongoing'
|
|
90
173
|
? 'Loop: ONGOING — keep iterating until done, blocked, or limits reached.'
|
|
91
174
|
: 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
|
|
@@ -102,13 +185,22 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
102
185
|
'Always reply to: questions, tasks, emotional sharing, or when you have something useful to add.',
|
|
103
186
|
'Execute by default — only ask for confirmation on high-risk/irreversible actions. Do not end every response with a question.',
|
|
104
187
|
'Never repeat completed side effects. Verify state first.',
|
|
188
|
+
'If a tool returns an error or validation failure, do not claim the task succeeded. Retry with corrected arguments or explain the blocker plainly.',
|
|
189
|
+
'Prefer the most specific tool you already have. Example: use `manage_schedules` for schedules and `manage_tasks` for tasks; treat `manage_platform` as a fallback umbrella only when a specific `manage_*` tool is unavailable.',
|
|
190
|
+
'For recurring, cron, interval, or follow-up automation requests, use `manage_schedules` directly when it is available.',
|
|
191
|
+
'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.',
|
|
192
|
+
'If a required tool is missing, request access by name with `manage_capabilities` action `request_access` (for example `shell` or `manage_schedules`).',
|
|
193
|
+
'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
194
|
`Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
|
|
106
195
|
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
196
|
)
|
|
109
197
|
|
|
198
|
+
if (toolDisciplineLines.length) parts.push('## Tool Discipline', ...toolDisciplineLines)
|
|
110
199
|
if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
|
|
111
|
-
if (opts.userMessage &&
|
|
200
|
+
if (opts.userMessage && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
|
|
201
|
+
if (opts.userMessage && looksLikeOpenEndedDeliverableTask(opts.userMessage) && opts.enabledPlugins.some((toolId) => toolId === 'files' || toolId === 'edit_file')) {
|
|
202
|
+
parts.push(OPEN_ENDED_REVISION_BLOCK)
|
|
203
|
+
}
|
|
112
204
|
|
|
113
205
|
return parts.filter(Boolean).join('\n')
|
|
114
206
|
}
|
|
@@ -136,7 +228,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
136
228
|
|
|
137
229
|
// Resolve agent's thinking level for provider-native params
|
|
138
230
|
let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
|
|
139
|
-
if (session.
|
|
231
|
+
if (session.thinkingLevel) {
|
|
232
|
+
agentThinkingLevel = session.thinkingLevel
|
|
233
|
+
} else if (session.agentId) {
|
|
140
234
|
const agentsForThinking = loadAgents()
|
|
141
235
|
agentThinkingLevel = agentsForThinking[session.agentId]?.thinkingLevel
|
|
142
236
|
}
|
|
@@ -162,7 +256,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
162
256
|
: typeof raw === 'string'
|
|
163
257
|
? Number.parseInt(raw, 10)
|
|
164
258
|
: Number.NaN
|
|
165
|
-
if (!Number.isFinite(parsed)) return
|
|
259
|
+
if (!Number.isFinite(parsed)) return DEFAULT_HEARTBEAT_INTERVAL_SEC
|
|
166
260
|
return Math.max(0, Math.min(3600, Math.trunc(parsed)))
|
|
167
261
|
})()
|
|
168
262
|
|
|
@@ -180,18 +274,22 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
180
274
|
let agentPlatformAssignScope: 'self' | 'all' = 'self'
|
|
181
275
|
let agentMcpServerIds: string[] | undefined
|
|
182
276
|
let agentMcpDisabledTools: string[] | undefined
|
|
277
|
+
let agentHeartbeatEnabled = false
|
|
183
278
|
if (session.agentId) {
|
|
184
279
|
const agents = loadAgents()
|
|
185
280
|
const agent = agents[session.agentId]
|
|
186
281
|
agentPlatformAssignScope = agent?.platformAssignScope || 'self'
|
|
187
282
|
agentMcpServerIds = agent?.mcpServerIds
|
|
188
283
|
agentMcpDisabledTools = agent?.mcpDisabledTools
|
|
284
|
+
agentHeartbeatEnabled = agent?.heartbeatEnabled === true
|
|
189
285
|
if (!hasProvidedSystemPrompt) {
|
|
190
286
|
// Identity block — make sure the agent knows who it is
|
|
191
287
|
const identityLines = [`## My Identity`, `My name is ${agent?.name || 'Agent'}.`]
|
|
192
288
|
if (agent?.description) identityLines.push(agent.description)
|
|
193
289
|
identityLines.push('I should always refer to myself by this name. I am not "Assistant" — I have my own name and identity.')
|
|
194
290
|
stateModifierParts.push(identityLines.join(' '))
|
|
291
|
+
const continuityBlock = buildIdentityContinuityContext(session, agent)
|
|
292
|
+
if (continuityBlock) stateModifierParts.push(continuityBlock)
|
|
195
293
|
if (agent?.soul) stateModifierParts.push(agent.soul)
|
|
196
294
|
if (agent?.systemPrompt) stateModifierParts.push(agent.systemPrompt)
|
|
197
295
|
if (agent?.skillIds?.length) {
|
|
@@ -290,9 +388,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
290
388
|
)
|
|
291
389
|
}
|
|
292
390
|
|
|
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
391
|
stateModifierParts.push(
|
|
297
392
|
buildAgenticExecutionPolicy({
|
|
298
393
|
enabledPlugins: sessionPlugins,
|
|
@@ -301,7 +396,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
301
396
|
heartbeatIntervalSec,
|
|
302
397
|
platformAssignScope: agentPlatformAssignScope,
|
|
303
398
|
userMessage: message,
|
|
304
|
-
hasExistingPlan,
|
|
305
399
|
}),
|
|
306
400
|
)
|
|
307
401
|
|
|
@@ -480,14 +574,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
480
574
|
let needsTextSeparator = false
|
|
481
575
|
let totalInputTokens = 0
|
|
482
576
|
let totalOutputTokens = 0
|
|
483
|
-
let lastToolInput: unknown = null
|
|
484
577
|
let accumulatedThinking = ''
|
|
485
578
|
const pluginInvocations: PluginInvocationRecord[] = []
|
|
486
579
|
let currentToolInputTokens = 0
|
|
487
580
|
|
|
488
581
|
// Plugin hooks: beforeAgentStart
|
|
489
582
|
const pluginMgr = getPluginManager()
|
|
490
|
-
await pluginMgr.runHook('beforeAgentStart', { session, message })
|
|
583
|
+
await pluginMgr.runHook('beforeAgentStart', { session, message }, { enabledIds: sessionPlugins })
|
|
491
584
|
|
|
492
585
|
const abortController = new AbortController()
|
|
493
586
|
const abortFromSignal = () => abortController.abort()
|
|
@@ -505,13 +598,21 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
505
598
|
|
|
506
599
|
const MAX_AUTO_CONTINUES = 3
|
|
507
600
|
const MAX_TRANSIENT_RETRIES = 2
|
|
601
|
+
const MAX_REQUIRED_TOOL_CONTINUES = 2
|
|
508
602
|
let autoContinueCount = 0
|
|
509
603
|
let transientRetryCount = 0
|
|
604
|
+
let requiredToolContinueCount = 0
|
|
605
|
+
const explicitRequiredToolNames = getExplicitRequiredToolNames(message, sessionPlugins)
|
|
606
|
+
const usedToolNames = new Set<string>()
|
|
510
607
|
|
|
511
608
|
try {
|
|
512
|
-
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
|
|
609
|
+
const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES + MAX_REQUIRED_TOOL_CONTINUES
|
|
513
610
|
for (let iteration = 0; iteration <= maxIterations; iteration++) {
|
|
514
|
-
let shouldContinue: 'recursion' | 'transient' | false = false
|
|
611
|
+
let shouldContinue: 'recursion' | 'transient' | 'required_tool' | false = false
|
|
612
|
+
let requiredToolReminderNames: string[] = []
|
|
613
|
+
let waitingForToolResult = false
|
|
614
|
+
let idleTimedOut = false
|
|
615
|
+
let idleTimer: ReturnType<typeof setTimeout> | null = null
|
|
515
616
|
|
|
516
617
|
// Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
|
|
517
618
|
// Linked to the parent so client disconnect / timeout still propagates.
|
|
@@ -520,7 +621,24 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
520
621
|
if (abortController.signal.aborted) iterationController.abort()
|
|
521
622
|
else abortController.signal.addEventListener('abort', onParentAbort)
|
|
522
623
|
|
|
624
|
+
const clearIdleWatchdog = () => {
|
|
625
|
+
if (idleTimer) {
|
|
626
|
+
clearTimeout(idleTimer)
|
|
627
|
+
idleTimer = null
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const armIdleWatchdog = () => {
|
|
632
|
+
clearIdleWatchdog()
|
|
633
|
+
if (waitingForToolResult || iterationController.signal.aborted) return
|
|
634
|
+
idleTimer = setTimeout(() => {
|
|
635
|
+
idleTimedOut = true
|
|
636
|
+
iterationController.abort()
|
|
637
|
+
}, 90_000)
|
|
638
|
+
}
|
|
639
|
+
|
|
523
640
|
try {
|
|
641
|
+
armIdleWatchdog()
|
|
524
642
|
const eventStream = agent.streamEvents(
|
|
525
643
|
{ messages: langchainMessages },
|
|
526
644
|
{ version: 'v2', recursionLimit, signal: iterationController.signal },
|
|
@@ -530,6 +648,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
530
648
|
const kind = event.event
|
|
531
649
|
|
|
532
650
|
if (kind === 'on_chat_model_stream') {
|
|
651
|
+
armIdleWatchdog()
|
|
533
652
|
const chunk = event.data?.chunk
|
|
534
653
|
if (chunk?.content) {
|
|
535
654
|
// content can be string or array of content blocks
|
|
@@ -569,6 +688,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
569
688
|
}
|
|
570
689
|
}
|
|
571
690
|
} else if (kind === 'on_llm_end') {
|
|
691
|
+
armIdleWatchdog()
|
|
572
692
|
// Track token usage from LLM responses — check all known LangChain event shapes
|
|
573
693
|
const output = event.data?.output
|
|
574
694
|
const usage = output?.llmOutput?.tokenUsage
|
|
@@ -581,17 +701,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
581
701
|
totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
|
|
582
702
|
}
|
|
583
703
|
} else if (kind === 'on_tool_start') {
|
|
704
|
+
clearIdleWatchdog()
|
|
705
|
+
waitingForToolResult = true
|
|
584
706
|
hasToolCalls = true
|
|
585
707
|
needsTextSeparator = true
|
|
586
708
|
lastSegment = ''
|
|
587
709
|
const toolName = event.name || 'unknown'
|
|
710
|
+
usedToolNames.add(toolName)
|
|
588
711
|
const input = event.data?.input
|
|
589
|
-
lastToolInput = input
|
|
590
712
|
// Estimate input tokens for plugin invocation tracking
|
|
591
713
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
592
714
|
currentToolInputTokens = Math.ceil((inputStr?.length || 0) / 4)
|
|
593
|
-
// Plugin hooks: beforeToolExec
|
|
594
|
-
await pluginMgr.runHook('beforeToolExec', { toolName, input })
|
|
595
715
|
logExecution(session.id, 'tool_call', `${toolName} invoked`, {
|
|
596
716
|
agentId: session.agentId,
|
|
597
717
|
detail: { toolName, input: inputStr?.slice(0, 4000) },
|
|
@@ -602,6 +722,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
602
722
|
toolInput: inputStr,
|
|
603
723
|
})}\n\n`)
|
|
604
724
|
} else if (kind === 'on_tool_end') {
|
|
725
|
+
waitingForToolResult = false
|
|
726
|
+
armIdleWatchdog()
|
|
605
727
|
const toolName = event.name || 'unknown'
|
|
606
728
|
const output = event.data?.output
|
|
607
729
|
const outputStr = typeof output === 'string'
|
|
@@ -609,9 +731,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
609
731
|
: output?.content
|
|
610
732
|
? String(output.content)
|
|
611
733
|
: 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
734
|
logExecution(session.id, 'tool_result', `${toolName} returned`, {
|
|
616
735
|
agentId: session.agentId,
|
|
617
736
|
detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
|
|
@@ -654,7 +773,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
654
773
|
}
|
|
655
774
|
} catch (innerErr: unknown) {
|
|
656
775
|
const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
|
|
657
|
-
const errMsg =
|
|
776
|
+
const errMsg = idleTimedOut
|
|
777
|
+
? 'Model stream stalled without emitting text or tool results for 90 seconds.'
|
|
778
|
+
: innerErr instanceof Error ? innerErr.message : String(innerErr)
|
|
658
779
|
const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
|
|
659
780
|
|
|
660
781
|
// Classify the error:
|
|
@@ -662,9 +783,10 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
662
783
|
// 2. Transient abort/timeout — LLM API failure, not from client disconnect
|
|
663
784
|
const isRecursionError = errName === 'GraphRecursionError'
|
|
664
785
|
|| /recursion limit|maximum recursion/i.test(errMsg)
|
|
665
|
-
const isTransientAbort = !isRecursionError
|
|
786
|
+
const isTransientAbort = (!isRecursionError && idleTimedOut)
|
|
787
|
+
|| (!isRecursionError
|
|
666
788
|
&& /abort|timed?\s*out|ECONNRESET|ECONNREFUSED|socket hang up|network/i.test(errMsg)
|
|
667
|
-
&& !abortController.signal.aborted
|
|
789
|
+
&& !abortController.signal.aborted)
|
|
668
790
|
|
|
669
791
|
// Log diagnostic details for every error so we can trace root causes
|
|
670
792
|
console.error(`[stream-agent-chat] Error in streamEvents iteration=${iteration}`, {
|
|
@@ -695,9 +817,26 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
695
817
|
throw innerErr
|
|
696
818
|
}
|
|
697
819
|
} finally {
|
|
820
|
+
clearIdleWatchdog()
|
|
698
821
|
abortController.signal.removeEventListener('abort', onParentAbort)
|
|
699
822
|
}
|
|
700
823
|
|
|
824
|
+
if (!shouldContinue && explicitRequiredToolNames.length > 0 && requiredToolContinueCount < MAX_REQUIRED_TOOL_CONTINUES) {
|
|
825
|
+
requiredToolReminderNames = explicitRequiredToolNames.filter((toolName) => !usedToolNames.has(toolName))
|
|
826
|
+
if (requiredToolReminderNames.length > 0) {
|
|
827
|
+
shouldContinue = 'required_tool'
|
|
828
|
+
requiredToolContinueCount++
|
|
829
|
+
write(`data: ${JSON.stringify({
|
|
830
|
+
t: 'status',
|
|
831
|
+
text: JSON.stringify({
|
|
832
|
+
requiredToolsPending: requiredToolReminderNames,
|
|
833
|
+
reminderCount: requiredToolContinueCount,
|
|
834
|
+
maxReminders: MAX_REQUIRED_TOOL_CONTINUES,
|
|
835
|
+
}),
|
|
836
|
+
})}\n\n`)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
701
840
|
if (!shouldContinue) break
|
|
702
841
|
|
|
703
842
|
if (shouldContinue === 'recursion') {
|
|
@@ -707,6 +846,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
707
846
|
}
|
|
708
847
|
langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
|
|
709
848
|
lastSegment = ''
|
|
849
|
+
} else if (shouldContinue === 'required_tool') {
|
|
850
|
+
if (fullText.trim()) {
|
|
851
|
+
langchainMessages.push(new AIMessage({ content: fullText }))
|
|
852
|
+
}
|
|
853
|
+
langchainMessages.push(new HumanMessage({
|
|
854
|
+
content: `You have not yet completed the required explicit tool step(s): ${requiredToolReminderNames.join(', ')}. Use those enabled tools now before declaring success. Do not replace ask_human with a plain-text request, and do not replace email delivery with browser work or prose.`,
|
|
855
|
+
}))
|
|
856
|
+
lastSegment = ''
|
|
710
857
|
} else if (shouldContinue === 'transient') {
|
|
711
858
|
// Short delay before retrying transient errors (API timeout, rate limit, etc.)
|
|
712
859
|
await new Promise((r) => setTimeout(r, 2000 * transientRetryCount))
|
|
@@ -716,6 +863,19 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
716
863
|
const errMsg = timedOut
|
|
717
864
|
? 'Ongoing loop stopped after reaching the configured runtime limit.'
|
|
718
865
|
: err instanceof Error ? err.message : String(err)
|
|
866
|
+
const heartbeatEligible = runtime.loopMode === 'ongoing' || session.heartbeatEnabled === true || agentHeartbeatEnabled
|
|
867
|
+
const budgetLimited = timedOut || /recursion limit|maximum recursion/i.test(errMsg)
|
|
868
|
+
if (heartbeatEligible && budgetLimited) {
|
|
869
|
+
enqueueSystemEvent(
|
|
870
|
+
session.id,
|
|
871
|
+
'[Loop Budget Reached] The previous autonomous run stopped after hitting its loop budget. On the next heartbeat, resume carefully from the current state, verify completed work before repeating it, and focus only on the remaining objective.',
|
|
872
|
+
'loop_budget_reached',
|
|
873
|
+
)
|
|
874
|
+
logExecution(session.id, 'decision', 'Queued a deferred resume cue for the next heartbeat after loop budget exhaustion.', {
|
|
875
|
+
agentId: session.agentId,
|
|
876
|
+
detail: { timedOut, heartbeatEligible },
|
|
877
|
+
})
|
|
878
|
+
}
|
|
719
879
|
logExecution(session.id, 'error', errMsg, { agentId: session.agentId, detail: { timedOut } })
|
|
720
880
|
write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
|
|
721
881
|
} finally {
|
|
@@ -775,7 +935,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
775
935
|
}
|
|
776
936
|
|
|
777
937
|
// Plugin hooks: afterAgentComplete
|
|
778
|
-
await pluginMgr.runHook('afterAgentComplete', { session, response: fullText })
|
|
938
|
+
await pluginMgr.runHook('afterAgentComplete', { session, response: fullText }, { enabledIds: sessionPlugins })
|
|
779
939
|
|
|
780
940
|
// OpenClaw auto-sync: push memory if enabled
|
|
781
941
|
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
|
+
})
|