@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,78 +1,6 @@
|
|
|
1
|
-
import { genId } from '@/lib/id'
|
|
2
|
-
import { z } from 'zod'
|
|
3
1
|
import type { GoalContract, MessageToolEvent } from '@/types'
|
|
4
|
-
import { loadSessions, saveSessions, loadAgents, saveAgents, loadSettings } from './storage'
|
|
5
|
-
import { log } from './logger'
|
|
6
|
-
import { getMemoryDb } from './memory-db'
|
|
7
|
-
import { isMainLoopSession } from './main-session'
|
|
8
|
-
import { logExecution } from './execution-log'
|
|
9
|
-
import {
|
|
10
|
-
mergeGoalContracts,
|
|
11
|
-
parseGoalContractFromText,
|
|
12
|
-
parseMainLoopPlan,
|
|
13
|
-
parseMainLoopReview,
|
|
14
|
-
} from './autonomy-contract'
|
|
15
|
-
import { buildIdentityContext } from './heartbeat-service'
|
|
16
2
|
|
|
17
|
-
const
|
|
18
|
-
const MAX_TIMELINE_EVENTS = 120
|
|
19
|
-
const EVENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
20
|
-
const MEMORY_NOTE_MIN_INTERVAL_MS = 30 * 60 * 1000
|
|
21
|
-
const DEFAULT_FOLLOWUP_DELAY_SEC = 45
|
|
22
|
-
const DEFAULT_MAX_FOLLOWUP_CHAIN = 20
|
|
23
|
-
function getMaxFollowupChain(agentId: string | undefined): number {
|
|
24
|
-
if (agentId) {
|
|
25
|
-
const agents = loadAgents()
|
|
26
|
-
const agent = agents[agentId]
|
|
27
|
-
if (typeof agent?.maxFollowupChain === 'number' && agent.maxFollowupChain > 0) {
|
|
28
|
-
return Math.min(agent.maxFollowupChain, 100)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const settings = loadSettings()
|
|
32
|
-
if (typeof settings?.maxFollowupChain === 'number' && settings.maxFollowupChain > 0) {
|
|
33
|
-
return Math.min(settings.maxFollowupChain, 100)
|
|
34
|
-
}
|
|
35
|
-
return DEFAULT_MAX_FOLLOWUP_CHAIN
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const META_LINE_RE = /\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i
|
|
39
|
-
const AGENT_HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
|
|
40
|
-
const SCREENSHOT_GOAL_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
|
|
41
|
-
const DELIVERY_GOAL_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i
|
|
42
|
-
const SCHEDULE_GOAL_HINT = /\b(schedule|scheduled|every\s+\w+|interval|cron|recurr)\b/i
|
|
43
|
-
const UPLOAD_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i
|
|
44
|
-
const SENT_ARTIFACT_HINT = /\b(sent|shared|uploaded|returned)\b[^.]*\b(screenshot|snapshot|image|file)\b/i
|
|
45
|
-
|
|
46
|
-
const COMPANION_GOAL_PROMPT = `
|
|
47
|
-
## Identity & Vibe
|
|
48
|
-
You are a persistent companion.
|
|
49
|
-
1. **Identity**: Embody your creature, theme, and vibe. Your emoji is your signature.
|
|
50
|
-
2. **Workspace Context**: Respect the current workspace. Read IDENTITY.md and HEARTBEAT.md if they exist.
|
|
51
|
-
3. **Continuity**: Maintain awareness of the user's long-term journey. Proactively help with open-ended goals without being asked for every step.
|
|
52
|
-
`.trim()
|
|
53
|
-
|
|
54
|
-
interface MainLoopSessionMessageLike {
|
|
55
|
-
text?: string
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface MainLoopSessionEvidenceLike {
|
|
59
|
-
messages?: MainLoopSessionMessageLike[]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface MainLoopEvent {
|
|
63
|
-
id: string
|
|
64
|
-
type: string
|
|
65
|
-
text: string
|
|
66
|
-
createdAt: number
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface MainLoopTimelineEntry {
|
|
70
|
-
id: string
|
|
71
|
-
at: number
|
|
72
|
-
source: string
|
|
73
|
-
note: string
|
|
74
|
-
status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection'
|
|
75
|
-
}
|
|
3
|
+
const LEGACY_META_LINE_RE = /\[(?:MAIN_LOOP_META|MAIN_LOOP_PLAN|MAIN_LOOP_REVIEW|AGENT_HEARTBEAT_META)\]\s*(\{[^\n]*\})?/i
|
|
76
4
|
|
|
77
5
|
export interface MainLoopState {
|
|
78
6
|
goal: string | null
|
|
@@ -88,8 +16,19 @@ export interface MainLoopState {
|
|
|
88
16
|
paused: boolean
|
|
89
17
|
status: 'idle' | 'progress' | 'blocked' | 'ok'
|
|
90
18
|
autonomyMode: 'assist' | 'autonomous'
|
|
91
|
-
pendingEvents:
|
|
92
|
-
|
|
19
|
+
pendingEvents: Array<{
|
|
20
|
+
id: string
|
|
21
|
+
type: string
|
|
22
|
+
text: string
|
|
23
|
+
createdAt: number
|
|
24
|
+
}>
|
|
25
|
+
timeline: Array<{
|
|
26
|
+
id: string
|
|
27
|
+
at: number
|
|
28
|
+
source: string
|
|
29
|
+
note: string
|
|
30
|
+
status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection'
|
|
31
|
+
}>
|
|
93
32
|
missionTokens: number
|
|
94
33
|
missionCostUsd: number
|
|
95
34
|
followupChainCount: number
|
|
@@ -102,16 +41,6 @@ export interface MainLoopState {
|
|
|
102
41
|
updatedAt: number
|
|
103
42
|
}
|
|
104
43
|
|
|
105
|
-
interface MainLoopMeta {
|
|
106
|
-
status?: 'idle' | 'progress' | 'blocked' | 'ok'
|
|
107
|
-
summary?: string
|
|
108
|
-
next_action?: string
|
|
109
|
-
follow_up?: boolean
|
|
110
|
-
delay_sec?: number
|
|
111
|
-
goal?: string
|
|
112
|
-
consume_event_ids?: string[]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
44
|
export interface MainLoopFollowupRequest {
|
|
116
45
|
message: string
|
|
117
46
|
delayMs: number
|
|
@@ -137,903 +66,41 @@ export interface HandleMainLoopRunResultInput {
|
|
|
137
66
|
estimatedCost?: number
|
|
138
67
|
}
|
|
139
68
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
function normalizeMemoryText(value: string): string {
|
|
145
|
-
return (value || '').replace(/\s+/g, ' ').trim()
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function clampInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
149
|
-
const parsed = typeof value === 'number'
|
|
150
|
-
? value
|
|
151
|
-
: typeof value === 'string'
|
|
152
|
-
? Number.parseInt(value, 10)
|
|
153
|
-
: Number.NaN
|
|
154
|
-
if (!Number.isFinite(parsed)) return fallback
|
|
155
|
-
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function pruneEvents(events: MainLoopEvent[], now = Date.now()): MainLoopEvent[] {
|
|
159
|
-
const minTs = now - EVENT_TTL_MS
|
|
160
|
-
const fresh = events.filter((e) => e && typeof e.createdAt === 'number' && e.createdAt >= minTs)
|
|
161
|
-
if (fresh.length <= MAX_PENDING_EVENTS) return fresh
|
|
162
|
-
return fresh.slice(fresh.length - MAX_PENDING_EVENTS)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function pruneTimeline(entries: MainLoopTimelineEntry[], now = Date.now()): MainLoopTimelineEntry[] {
|
|
166
|
-
const minTs = now - EVENT_TTL_MS
|
|
167
|
-
const fresh = entries.filter((e) => e && typeof e.at === 'number' && e.at >= minTs && typeof e.note === 'string' && e.note.trim())
|
|
168
|
-
if (fresh.length <= MAX_TIMELINE_EVENTS) return fresh
|
|
169
|
-
return fresh.slice(fresh.length - MAX_TIMELINE_EVENTS)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function appendTimeline(
|
|
173
|
-
state: MainLoopState,
|
|
174
|
-
source: string,
|
|
175
|
-
note: string,
|
|
176
|
-
now = Date.now(),
|
|
177
|
-
status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection',
|
|
178
|
-
) {
|
|
179
|
-
const normalizedNote = toOneLine(note, 400)
|
|
180
|
-
if (!normalizedNote) return
|
|
181
|
-
const recent = state.timeline.at(-1)
|
|
182
|
-
if (recent && recent.source === source && recent.note === normalizedNote && now - recent.at < 45_000) return
|
|
183
|
-
state.timeline.push({
|
|
184
|
-
id: `tl_${genId()}`,
|
|
185
|
-
at: now,
|
|
186
|
-
source,
|
|
187
|
-
note: normalizedNote,
|
|
188
|
-
status,
|
|
189
|
-
})
|
|
190
|
-
state.timeline = pruneTimeline(state.timeline, now)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function computeMomentumScore(state: MainLoopState): number {
|
|
194
|
-
const baseByStatus = {
|
|
195
|
-
idle: 40,
|
|
196
|
-
progress: 72,
|
|
197
|
-
blocked: 20,
|
|
198
|
-
ok: 94,
|
|
199
|
-
} as const
|
|
200
|
-
let score: number = baseByStatus[state.status]
|
|
201
|
-
score -= Math.min(20, state.metaMissCount * 3)
|
|
202
|
-
score -= Math.min(12, Math.max(0, state.pendingEvents.length - 4) * 2)
|
|
203
|
-
if (state.paused) score = Math.min(score, 35)
|
|
204
|
-
return clampInt(score, 0, 0, 100)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function normalizeStringList(input: unknown, maxItems: number, maxChars: number): string[] {
|
|
208
|
-
if (!Array.isArray(input)) return []
|
|
209
|
-
const seen = new Set<string>()
|
|
210
|
-
const out: string[] = []
|
|
211
|
-
for (const raw of input) {
|
|
212
|
-
if (typeof raw !== 'string') continue
|
|
213
|
-
const value = raw.replace(/\s+/g, ' ').trim().slice(0, maxChars)
|
|
214
|
-
if (!value) continue
|
|
215
|
-
const key = value.toLowerCase()
|
|
216
|
-
if (seen.has(key)) continue
|
|
217
|
-
seen.add(key)
|
|
218
|
-
out.push(value)
|
|
219
|
-
if (out.length >= maxItems) break
|
|
220
|
-
}
|
|
221
|
-
return out
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function normalizeGoalContract(raw: any): GoalContract | null {
|
|
225
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
226
|
-
const objective = typeof raw.objective === 'string' ? raw.objective.trim().slice(0, 300) : ''
|
|
227
|
-
if (!objective) return null
|
|
228
|
-
const constraints = normalizeStringList(raw.constraints, 10, 220)
|
|
229
|
-
const budgetUsd = typeof raw.budgetUsd === 'number'
|
|
230
|
-
? Math.max(0, Math.min(1_000_000, raw.budgetUsd))
|
|
231
|
-
: null
|
|
232
|
-
const deadlineAt = typeof raw.deadlineAt === 'number' && Number.isFinite(raw.deadlineAt)
|
|
233
|
-
? Math.trunc(raw.deadlineAt)
|
|
234
|
-
: null
|
|
235
|
-
const successMetric = typeof raw.successMetric === 'string'
|
|
236
|
-
? raw.successMetric.trim().slice(0, 220) || null
|
|
237
|
-
: null
|
|
238
|
-
return {
|
|
239
|
-
objective,
|
|
240
|
-
constraints: constraints.length ? constraints : undefined,
|
|
241
|
-
budgetUsd,
|
|
242
|
-
deadlineAt,
|
|
243
|
-
successMetric,
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function normalizeState(raw: any, now = Date.now()): MainLoopState {
|
|
248
|
-
const status = raw?.status === 'blocked' || raw?.status === 'ok' || raw?.status === 'progress' || raw?.status === 'idle'
|
|
249
|
-
? raw.status
|
|
250
|
-
: 'idle'
|
|
251
|
-
|
|
252
|
-
const pendingRaw = Array.isArray(raw?.pendingEvents) ? raw.pendingEvents : []
|
|
253
|
-
const pendingEvents = pruneEvents(
|
|
254
|
-
pendingRaw
|
|
255
|
-
.map((e: any) => {
|
|
256
|
-
const text = toOneLine(typeof e?.text === 'string' ? e.text : '')
|
|
257
|
-
if (!text) return null
|
|
258
|
-
return {
|
|
259
|
-
id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${genId(3)}`,
|
|
260
|
-
type: typeof e?.type === 'string' && e.type.trim() ? e.type.trim() : 'event',
|
|
261
|
-
text,
|
|
262
|
-
createdAt: typeof e?.createdAt === 'number' ? e.createdAt : now,
|
|
263
|
-
} as MainLoopEvent
|
|
264
|
-
})
|
|
265
|
-
.filter(Boolean) as MainLoopEvent[],
|
|
266
|
-
now,
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
const timelineRaw = Array.isArray(raw?.timeline) ? raw.timeline : []
|
|
270
|
-
const timeline = pruneTimeline(
|
|
271
|
-
timelineRaw
|
|
272
|
-
.map((entry: any) => {
|
|
273
|
-
const note = toOneLine(typeof entry?.note === 'string' ? entry.note : '', 400)
|
|
274
|
-
if (!note) return null
|
|
275
|
-
const status = entry?.status === 'blocked' || entry?.status === 'ok' || entry?.status === 'progress' || entry?.status === 'idle'
|
|
276
|
-
? entry.status
|
|
277
|
-
: undefined
|
|
278
|
-
return {
|
|
279
|
-
id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${genId(3)}`,
|
|
280
|
-
at: typeof entry?.at === 'number' ? entry.at : now,
|
|
281
|
-
source: typeof entry?.source === 'string' && entry.source.trim() ? entry.source.trim() : 'event',
|
|
282
|
-
note,
|
|
283
|
-
status,
|
|
284
|
-
} as MainLoopTimelineEntry
|
|
285
|
-
})
|
|
286
|
-
.filter(Boolean) as MainLoopTimelineEntry[],
|
|
287
|
-
now,
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
const normalized: MainLoopState = {
|
|
291
|
-
goal: typeof raw?.goal === 'string' && raw.goal.trim() ? raw.goal.trim().slice(0, 600) : null,
|
|
292
|
-
goalContract: normalizeGoalContract(raw?.goalContract),
|
|
293
|
-
status,
|
|
294
|
-
summary: typeof raw?.summary === 'string' && raw.summary.trim() ? raw.summary.trim().slice(0, 800) : null,
|
|
295
|
-
nextAction: typeof raw?.nextAction === 'string' && raw.nextAction.trim() ? raw.nextAction.trim().slice(0, 600) : null,
|
|
296
|
-
planSteps: normalizeStringList(raw?.planSteps, 10, 220),
|
|
297
|
-
currentPlanStep: typeof raw?.currentPlanStep === 'string' && raw.currentPlanStep.trim()
|
|
298
|
-
? raw.currentPlanStep.trim().slice(0, 220)
|
|
299
|
-
: null,
|
|
300
|
-
reviewNote: typeof raw?.reviewNote === 'string' && raw.reviewNote.trim()
|
|
301
|
-
? raw.reviewNote.trim().slice(0, 320)
|
|
302
|
-
: null,
|
|
303
|
-
reviewConfidence: typeof raw?.reviewConfidence === 'number' && Number.isFinite(raw.reviewConfidence)
|
|
304
|
-
? Math.max(0, Math.min(1, raw.reviewConfidence))
|
|
305
|
-
: null,
|
|
306
|
-
missionTaskId: typeof raw?.missionTaskId === 'string' && raw.missionTaskId.trim() ? raw.missionTaskId.trim() : null,
|
|
307
|
-
momentumScore: clampInt(raw?.momentumScore, 40, 0, 100),
|
|
308
|
-
paused: raw?.paused === true,
|
|
309
|
-
autonomyMode: raw?.autonomyMode === 'assist' ? 'assist' : 'autonomous',
|
|
310
|
-
pendingEvents,
|
|
311
|
-
timeline,
|
|
312
|
-
missionTokens: typeof raw?.missionTokens === 'number' && Number.isFinite(raw.missionTokens) ? raw.missionTokens : 0,
|
|
313
|
-
missionCostUsd: typeof raw?.missionCostUsd === 'number' && Number.isFinite(raw.missionCostUsd) ? raw.missionCostUsd : 0,
|
|
314
|
-
followupChainCount: clampInt(raw?.followupChainCount, 0, 0, 100),
|
|
315
|
-
metaMissCount: clampInt(raw?.metaMissCount, 0, 0, 100),
|
|
316
|
-
workingMemoryNotes: normalizeStringList(raw?.workingMemoryNotes, 24, 260),
|
|
317
|
-
lastMemoryNoteAt: typeof raw?.lastMemoryNoteAt === 'number' ? raw.lastMemoryNoteAt : null,
|
|
318
|
-
lastPlannedAt: typeof raw?.lastPlannedAt === 'number' ? raw.lastPlannedAt : null,
|
|
319
|
-
lastReviewedAt: typeof raw?.lastReviewedAt === 'number' ? raw.lastReviewedAt : null,
|
|
320
|
-
lastTickAt: typeof raw?.lastTickAt === 'number' ? raw.lastTickAt : null,
|
|
321
|
-
updatedAt: typeof raw?.updatedAt === 'number' ? raw.updatedAt : now,
|
|
322
|
-
}
|
|
323
|
-
if (!normalized.goal && normalized.goalContract?.objective) {
|
|
324
|
-
normalized.goal = normalized.goalContract.objective
|
|
325
|
-
}
|
|
326
|
-
normalized.momentumScore = computeMomentumScore(normalized)
|
|
327
|
-
return normalized
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function appendEvent(state: MainLoopState, type: string, text: string, now = Date.now()): boolean {
|
|
331
|
-
const normalizedText = toOneLine(text)
|
|
332
|
-
if (!normalizedText) return false
|
|
333
|
-
const recent = state.pendingEvents.at(-1)
|
|
334
|
-
if (recent && recent.type === type && recent.text === normalizedText && now - recent.createdAt < 60_000) {
|
|
335
|
-
return false
|
|
336
|
-
}
|
|
337
|
-
state.pendingEvents.push({
|
|
338
|
-
id: `evt_${genId()}`,
|
|
339
|
-
type,
|
|
340
|
-
text: normalizedText,
|
|
341
|
-
createdAt: now,
|
|
342
|
-
})
|
|
343
|
-
state.pendingEvents = pruneEvents(state.pendingEvents, now)
|
|
344
|
-
return true
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function appendWorkingMemoryNote(state: MainLoopState, note: string) {
|
|
348
|
-
const value = toOneLine(note, 260)
|
|
349
|
-
if (!value) return
|
|
350
|
-
const existing = state.workingMemoryNotes || []
|
|
351
|
-
if (existing.length && existing[existing.length - 1] === value) return
|
|
352
|
-
state.workingMemoryNotes = [...existing.slice(-23), value]
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
function parseMainLoopMeta(text: string): MainLoopMeta | null {
|
|
357
|
-
const raw = (text || '').trim()
|
|
358
|
-
if (!raw) return null
|
|
359
|
-
|
|
360
|
-
const markerMatch = raw.match(META_LINE_RE)
|
|
361
|
-
const parseCandidate = markerMatch?.[1]
|
|
362
|
-
if (parseCandidate) {
|
|
363
|
-
try {
|
|
364
|
-
const parsed = JSON.parse(parseCandidate)
|
|
365
|
-
return normalizeMeta(parsed)
|
|
366
|
-
} catch {
|
|
367
|
-
// fall through
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Fallback: parse any one-line JSON that appears to be the meta payload.
|
|
372
|
-
for (const line of raw.split('\n')) {
|
|
373
|
-
const trimmed = line.trim()
|
|
374
|
-
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue
|
|
375
|
-
if (!trimmed.includes('follow_up') && !trimmed.includes('next_action') && !trimmed.includes('consume_event_ids')) continue
|
|
376
|
-
try {
|
|
377
|
-
const parsed = JSON.parse(trimmed)
|
|
378
|
-
return normalizeMeta(parsed)
|
|
379
|
-
} catch {
|
|
380
|
-
// skip malformed candidate lines
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return null
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function normalizeMeta(raw: any): MainLoopMeta {
|
|
388
|
-
const status = raw?.status === 'blocked' || raw?.status === 'ok' || raw?.status === 'progress' || raw?.status === 'idle'
|
|
389
|
-
? raw.status
|
|
390
|
-
: undefined
|
|
391
|
-
|
|
392
|
-
const consumeIds = Array.isArray(raw?.consume_event_ids)
|
|
393
|
-
? raw.consume_event_ids
|
|
394
|
-
.map((v: unknown) => (typeof v === 'string' ? v.trim() : ''))
|
|
395
|
-
.filter(Boolean)
|
|
396
|
-
: undefined
|
|
397
|
-
|
|
398
|
-
const followUp = typeof raw?.follow_up === 'boolean'
|
|
399
|
-
? raw.follow_up
|
|
400
|
-
: typeof raw?.follow_up === 'string'
|
|
401
|
-
? raw.follow_up.trim().toLowerCase() === 'true'
|
|
402
|
-
: undefined
|
|
403
|
-
|
|
404
|
-
return {
|
|
405
|
-
status,
|
|
406
|
-
summary: typeof raw?.summary === 'string' ? raw.summary.trim().slice(0, 800) : undefined,
|
|
407
|
-
next_action: typeof raw?.next_action === 'string' ? raw.next_action.trim().slice(0, 600) : undefined,
|
|
408
|
-
follow_up: followUp,
|
|
409
|
-
delay_sec: clampInt(raw?.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900),
|
|
410
|
-
goal: typeof raw?.goal === 'string' ? raw.goal.trim().slice(0, 600) : undefined,
|
|
411
|
-
consume_event_ids: consumeIds,
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function consumeEvents(state: MainLoopState, ids: string[] | undefined) {
|
|
416
|
-
if (!ids?.length) return
|
|
417
|
-
const remove = new Set(ids)
|
|
418
|
-
state.pendingEvents = state.pendingEvents.filter((event) => !remove.has(event.id))
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function buildPendingEventLines(state: MainLoopState): string {
|
|
422
|
-
if (!state.pendingEvents.length) return 'Pending events:\n- none'
|
|
423
|
-
const lines = state.pendingEvents
|
|
424
|
-
.slice(-10)
|
|
425
|
-
.map((event) => `- ${event.id} | ${event.type} | ${event.text}`)
|
|
426
|
-
.join('\n')
|
|
427
|
-
return `Pending events (oldest → newest):\n${lines}`
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function buildTimelineLines(state: MainLoopState): string {
|
|
431
|
-
if (!state.timeline.length) return 'Recent mission timeline:\n- none'
|
|
432
|
-
const lines = state.timeline
|
|
433
|
-
.slice(-5)
|
|
434
|
-
.map((entry) => {
|
|
435
|
-
const ts = new Date(entry.at).toISOString().slice(11, 19)
|
|
436
|
-
const status = entry.status ? ` [${entry.status}]` : ''
|
|
437
|
-
return `- ${ts} ${entry.source}${status}: ${entry.note}`
|
|
438
|
-
})
|
|
439
|
-
.join('\n')
|
|
440
|
-
return `Recent mission timeline:\n${lines}`
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function buildGoalContractLines(state: MainLoopState): string[] {
|
|
444
|
-
const contract = state.goalContract
|
|
445
|
-
if (!contract?.objective) return []
|
|
446
|
-
const lines = [
|
|
447
|
-
`contract_objective: ${contract.objective}`,
|
|
448
|
-
]
|
|
449
|
-
if (contract.constraints?.length) lines.push(`contract_constraints: ${contract.constraints.join(' | ')}`)
|
|
450
|
-
if (typeof contract.budgetUsd === 'number') lines.push(`contract_budget_usd: ${contract.budgetUsd}`)
|
|
451
|
-
if (typeof contract.deadlineAt === 'number') lines.push(`contract_deadline_iso: ${new Date(contract.deadlineAt).toISOString()}`)
|
|
452
|
-
if (contract.successMetric) lines.push(`contract_success_metric: ${contract.successMetric}`)
|
|
453
|
-
return lines
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function missionNeedsScreenshotArtifactEvidence(state: MainLoopState): boolean {
|
|
457
|
-
const haystack = [
|
|
458
|
-
state.goal || '',
|
|
459
|
-
state.goalContract?.objective || '',
|
|
460
|
-
state.goalContract?.successMetric || '',
|
|
461
|
-
state.nextAction || '',
|
|
462
|
-
...(state.planSteps || []),
|
|
463
|
-
state.currentPlanStep || '',
|
|
464
|
-
].join(' ')
|
|
465
|
-
if (!SCREENSHOT_GOAL_HINT.test(haystack)) return false
|
|
466
|
-
return DELIVERY_GOAL_HINT.test(haystack) || SCHEDULE_GOAL_HINT.test(haystack)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function missionHasScreenshotArtifactEvidence(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): boolean {
|
|
470
|
-
const candidates: string[] = [
|
|
471
|
-
state.summary || '',
|
|
472
|
-
additionalText || '',
|
|
473
|
-
]
|
|
474
|
-
if (Array.isArray(session?.messages)) {
|
|
475
|
-
for (let i = session.messages.length - 1; i >= 0 && candidates.length < 16; i--) {
|
|
476
|
-
const text = typeof session.messages[i]?.text === 'string' ? session.messages[i].text! : ''
|
|
477
|
-
if (text && text.trim()) candidates.push(text)
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return candidates.some((value) => UPLOAD_ARTIFACT_HINT.test(value) || SENT_ARTIFACT_HINT.test(value))
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function getMissionCompletionGateReason(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): string | null {
|
|
484
|
-
if (!missionNeedsScreenshotArtifactEvidence(state)) return null
|
|
485
|
-
if (missionHasScreenshotArtifactEvidence(session, state, additionalText)) return null
|
|
486
|
-
return 'Mission requires screenshot artifact evidence (upload link or explicit sent screenshot confirmation) before completion.'
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
function maybeStoreMissionMemoryNote(
|
|
492
|
-
session: any,
|
|
493
|
-
state: MainLoopState,
|
|
494
|
-
now: number,
|
|
495
|
-
source: string,
|
|
496
|
-
force = false,
|
|
497
|
-
) {
|
|
498
|
-
if (!session?.agentId || !state.goal) return
|
|
499
|
-
if (!force && state.lastMemoryNoteAt && (now - state.lastMemoryNoteAt) < MEMORY_NOTE_MIN_INTERVAL_MS) return
|
|
500
|
-
|
|
501
|
-
const summary = state.summary || 'No summary'
|
|
502
|
-
const next = state.nextAction || 'No next action'
|
|
503
|
-
const title = `Mission ${state.status}: ${state.goal.slice(0, 72)}`
|
|
504
|
-
const content = [
|
|
505
|
-
`source: ${source}`,
|
|
506
|
-
`status: ${state.status}`,
|
|
507
|
-
`momentum: ${state.momentumScore}/100`,
|
|
508
|
-
`goal: ${state.goal}`,
|
|
509
|
-
...buildGoalContractLines(state),
|
|
510
|
-
state.planSteps.length ? `plan_steps: ${state.planSteps.join(' -> ')}` : '',
|
|
511
|
-
state.currentPlanStep ? `current_plan_step: ${state.currentPlanStep}` : '',
|
|
512
|
-
`summary: ${summary}`,
|
|
513
|
-
`next_action: ${next}`,
|
|
514
|
-
state.reviewNote ? `review: ${state.reviewNote}` : '',
|
|
515
|
-
typeof state.reviewConfidence === 'number' ? `review_confidence: ${state.reviewConfidence}` : '',
|
|
516
|
-
state.missionTaskId ? `mission_task_id: ${state.missionTaskId}` : '',
|
|
517
|
-
typeof state.missionTokens === 'number' ? `mission_tokens: ${state.missionTokens}` : '',
|
|
518
|
-
typeof state.missionCostUsd === 'number' ? `mission_cost_usd: $${state.missionCostUsd.toFixed(4)}` : '',
|
|
519
|
-
state.workingMemoryNotes?.length ? `working_notes: ${state.workingMemoryNotes.slice(-5).join('; ')}` : '',
|
|
520
|
-
].filter(Boolean).join('\n')
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
const memDb = getMemoryDb()
|
|
524
|
-
const latest = memDb.getLatestBySessionCategory?.(session.id, 'mission')
|
|
525
|
-
if (latest) {
|
|
526
|
-
const sameTitle = normalizeMemoryText(latest.title) === normalizeMemoryText(title)
|
|
527
|
-
const sameContent = normalizeMemoryText(latest.content) === normalizeMemoryText(content)
|
|
528
|
-
if (sameTitle && sameContent) {
|
|
529
|
-
state.lastMemoryNoteAt = now
|
|
530
|
-
return
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
memDb.add({
|
|
534
|
-
agentId: session.agentId || null,
|
|
535
|
-
sessionId: session.id,
|
|
536
|
-
category: 'mission',
|
|
537
|
-
title,
|
|
538
|
-
content,
|
|
539
|
-
})
|
|
540
|
-
state.lastMemoryNoteAt = now
|
|
541
|
-
logExecution(session.id, 'mission_checkpoint', `Checkpoint: ${toOneLine(state.goal || '', 120)}`, {
|
|
542
|
-
agentId: session.agentId,
|
|
543
|
-
detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
|
|
544
|
-
})
|
|
545
|
-
} catch (err: unknown) {
|
|
546
|
-
appendEvent(state, 'memory_note_error', `Failed to store mission memory note: ${toOneLine(err instanceof Error ? err.message : String(err), 240)}`, now)
|
|
547
|
-
}
|
|
69
|
+
export function isMainSession(session: unknown): boolean {
|
|
70
|
+
void session
|
|
71
|
+
return false
|
|
548
72
|
}
|
|
549
73
|
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const nextAction = state.nextAction || 'Determine the next highest-impact action and execute it.'
|
|
554
|
-
const contractLines = buildGoalContractLines(state)
|
|
555
|
-
return [
|
|
556
|
-
'SWARM_MAIN_AUTO_FOLLOWUP',
|
|
557
|
-
identityContext,
|
|
558
|
-
COMPANION_GOAL_PROMPT,
|
|
559
|
-
`Mission goal: ${goal}`,
|
|
560
|
-
`Next action to execute now: ${nextAction}`,
|
|
561
|
-
`Current status: ${state.status}`,
|
|
562
|
-
`Mission task id: ${state.missionTaskId || 'none'}`,
|
|
563
|
-
`Momentum score: ${state.momentumScore}/100`,
|
|
564
|
-
...contractLines,
|
|
565
|
-
state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
|
|
566
|
-
state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
|
|
567
|
-
state.reviewNote ? `Last review: ${state.reviewNote}` : '',
|
|
568
|
-
buildPendingEventLines(state),
|
|
569
|
-
buildTimelineLines(state),
|
|
570
|
-
state.planSteps.length === 0 && state.followupChainCount === 0
|
|
571
|
-
? 'Before executing, break the mission goal into 3-7 concrete subtasks. Output a [MAIN_LOOP_PLAN] JSON line with your plan, then execute the first step immediately.'
|
|
572
|
-
: '',
|
|
573
|
-
'Act autonomously. Use available tools to execute work, verify results, and keep momentum.',
|
|
574
|
-
state.autonomyMode === 'assist'
|
|
575
|
-
? 'Assist mode: execute safe internal analysis by default, and ask before irreversible external side effects (sending messages, purchases, account mutations).'
|
|
576
|
-
: 'Autonomous mode: execute safe next actions without waiting for confirmation; ask only when blocked by permissions, credentials, or policy.',
|
|
577
|
-
'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
|
|
578
|
-
'Use any available tools actively to maintain state across turns.',
|
|
579
|
-
'If you are blocked by missing credentials, permissions, or policy limits, say exactly what is blocked and the smallest unblock needed.',
|
|
580
|
-
'For screenshot/image delivery goals (including scheduled captures), do not report status "ok" until a real artifact exists (upload link or explicit sent-file confirmation).',
|
|
581
|
-
'When the mission goal is fully completed, set status to "ok" with follow_up:false and include a clear summary of what was accomplished. The loop will auto-pause.',
|
|
582
|
-
'If no meaningful action remains right now, reply exactly HEARTBEAT_OK.',
|
|
583
|
-
'Otherwise include a concise human update, then append exactly one [MAIN_LOOP_META] JSON line.',
|
|
584
|
-
'Optionally append one [MAIN_LOOP_PLAN] JSON line when you create/revise a plan.',
|
|
585
|
-
'Optionally append one [MAIN_LOOP_REVIEW] JSON line when you review recent execution results.',
|
|
586
|
-
'[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
|
|
587
|
-
'[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
|
|
588
|
-
'[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
|
|
589
|
-
].join('\n')
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
593
|
-
export function isMainSession(session: any): boolean {
|
|
594
|
-
return isMainLoopSession(session)
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
598
|
-
export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
|
|
599
|
-
const now = Date.now()
|
|
600
|
-
const agents = loadAgents()
|
|
601
|
-
const agent = session.agentId ? agents[session.agentId] : null
|
|
602
|
-
const identityContext = buildIdentityContext(session, agent)
|
|
603
|
-
const state = normalizeState(session?.mainLoopState, now)
|
|
604
|
-
const goal = state.goal || null
|
|
605
|
-
const promptGoal = goal || 'No explicit mission captured yet. Infer the mission from recent user instructions and continue proactively.'
|
|
606
|
-
const promptSummary = state.summary || 'No prior mission summary yet.'
|
|
607
|
-
const promptNextAction = state.nextAction || 'No queued action. Determine one.'
|
|
608
|
-
const contractLines = buildGoalContractLines(state)
|
|
609
|
-
|
|
610
|
-
return [
|
|
611
|
-
'SWARM_MAIN_MISSION_TICK',
|
|
612
|
-
identityContext,
|
|
613
|
-
COMPANION_GOAL_PROMPT,
|
|
614
|
-
`Time: ${new Date(now).toISOString()}`,
|
|
615
|
-
`Mission goal: ${promptGoal}`,
|
|
616
|
-
`Current status: ${state.status}`,
|
|
617
|
-
`Mission paused: ${state.paused ? 'yes' : 'no'}`,
|
|
618
|
-
`Autonomy mode: ${state.autonomyMode}`,
|
|
619
|
-
`Mission task id: ${state.missionTaskId || 'none'}`,
|
|
620
|
-
`Momentum score: ${state.momentumScore}/100`,
|
|
621
|
-
...contractLines,
|
|
622
|
-
state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
|
|
623
|
-
state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
|
|
624
|
-
state.reviewNote ? `Last review: ${state.reviewNote}` : '',
|
|
625
|
-
`Last summary: ${toOneLine(promptSummary, 500)}`,
|
|
626
|
-
`Last next action: ${toOneLine(promptNextAction, 500)}`,
|
|
627
|
-
buildPendingEventLines(state),
|
|
628
|
-
buildTimelineLines(state),
|
|
629
|
-
'You are running the main autonomous mission loop. Continue executing toward the goal with initiative.',
|
|
630
|
-
state.autonomyMode === 'assist'
|
|
631
|
-
? 'Assist mode is active: execute safe internal work and ask before irreversible external side effects.'
|
|
632
|
-
: 'Autonomous mode is active: execute safe next actions without waiting for confirmation; only ask when blocked.',
|
|
633
|
-
'Use tools where needed, verify outcomes, and avoid vague status-only replies.',
|
|
634
|
-
'Do not ask broad exploratory questions when a safe next action exists. Pick a reasonable assumption, execute, and adapt from evidence.',
|
|
635
|
-
'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
|
|
636
|
-
'Use any available tools actively to maintain state and recall context across turns.',
|
|
637
|
-
'Use a planner-executor-review loop: keep a concrete step plan, execute one meaningful step, then self-review and either continue or re-plan.',
|
|
638
|
-
'For screenshot/image delivery goals (including scheduled captures), do not report status "ok" until a real artifact exists (upload link or explicit sent-file confirmation).',
|
|
639
|
-
'When the mission goal is fully completed, set status to "ok" with follow_up:false and include a clear summary of what was accomplished. The loop will auto-pause.',
|
|
640
|
-
'If nothing important changed and no action is needed now, reply exactly HEARTBEAT_OK.',
|
|
641
|
-
'Otherwise: provide a concise human-readable update, then append exactly one [MAIN_LOOP_META] JSON line.',
|
|
642
|
-
'Optionally append one [MAIN_LOOP_PLAN] JSON line when creating/updating plan steps.',
|
|
643
|
-
'Optionally append one [MAIN_LOOP_REVIEW] JSON line after execution review.',
|
|
644
|
-
'[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
|
|
645
|
-
'[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
|
|
646
|
-
'[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
|
|
647
|
-
'The [MAIN_LOOP_META] JSON must be valid, on one line, and only appear once.',
|
|
648
|
-
`Fallback prompt context: ${fallbackPrompt || 'SWARM_HEARTBEAT_CHECK'}`,
|
|
649
|
-
].join('\n')
|
|
74
|
+
export function buildMainLoopHeartbeatPrompt(session: unknown, fallbackPrompt: string): string {
|
|
75
|
+
void session
|
|
76
|
+
return fallbackPrompt
|
|
650
77
|
}
|
|
651
78
|
|
|
652
79
|
export function stripMainLoopMetaForPersistence(text: string): string {
|
|
653
|
-
|
|
654
|
-
return text
|
|
80
|
+
return (text || '')
|
|
655
81
|
.split('\n')
|
|
656
|
-
.filter((line) => !
|
|
82
|
+
.filter((line) => !LEGACY_META_LINE_RE.test(line))
|
|
657
83
|
.join('\n')
|
|
658
84
|
.trim()
|
|
659
85
|
}
|
|
660
86
|
|
|
661
87
|
export function getMainLoopStateForSession(sessionId: string): MainLoopState | null {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (!session || !isMainSession(session)) return null
|
|
665
|
-
return normalizeState(session.mainLoopState)
|
|
88
|
+
void sessionId
|
|
89
|
+
return null
|
|
666
90
|
}
|
|
667
91
|
|
|
668
92
|
export function setMainLoopStateForSession(sessionId: string, patch: Partial<MainLoopState>): MainLoopState | null {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const now = Date.now()
|
|
673
|
-
const state = normalizeState(session.mainLoopState, now)
|
|
674
|
-
|
|
675
|
-
if (typeof patch.goal === 'string') state.goal = patch.goal.trim().slice(0, 600) || null
|
|
676
|
-
if (patch.goal === null) state.goal = null
|
|
677
|
-
if (patch.goalContract !== undefined) state.goalContract = normalizeGoalContract(patch.goalContract)
|
|
678
|
-
if (patch.status === 'idle' || patch.status === 'progress' || patch.status === 'blocked' || patch.status === 'ok') state.status = patch.status
|
|
679
|
-
if (typeof patch.summary === 'string') state.summary = patch.summary.trim().slice(0, 800) || null
|
|
680
|
-
if (patch.summary === null) state.summary = null
|
|
681
|
-
if (typeof patch.nextAction === 'string') state.nextAction = patch.nextAction.trim().slice(0, 600) || null
|
|
682
|
-
if (patch.nextAction === null) state.nextAction = null
|
|
683
|
-
if (Array.isArray(patch.planSteps)) state.planSteps = normalizeStringList(patch.planSteps, 10, 220)
|
|
684
|
-
if (typeof patch.currentPlanStep === 'string') state.currentPlanStep = patch.currentPlanStep.trim().slice(0, 220) || null
|
|
685
|
-
if (patch.currentPlanStep === null) state.currentPlanStep = null
|
|
686
|
-
if (typeof patch.reviewNote === 'string') state.reviewNote = patch.reviewNote.trim().slice(0, 320) || null
|
|
687
|
-
if (patch.reviewNote === null) state.reviewNote = null
|
|
688
|
-
if (typeof patch.reviewConfidence === 'number' && Number.isFinite(patch.reviewConfidence)) {
|
|
689
|
-
state.reviewConfidence = Math.max(0, Math.min(1, patch.reviewConfidence))
|
|
690
|
-
}
|
|
691
|
-
if (patch.reviewConfidence === null) state.reviewConfidence = null
|
|
692
|
-
if (typeof patch.missionTaskId === 'string') state.missionTaskId = patch.missionTaskId.trim() || null
|
|
693
|
-
if (patch.missionTaskId === null) state.missionTaskId = null
|
|
694
|
-
if (typeof patch.momentumScore === 'number') state.momentumScore = clampInt(patch.momentumScore, state.momentumScore, 0, 100)
|
|
695
|
-
if (typeof patch.paused === 'boolean') state.paused = patch.paused
|
|
696
|
-
if (patch.autonomyMode === 'assist' || patch.autonomyMode === 'autonomous') state.autonomyMode = patch.autonomyMode
|
|
697
|
-
if (Array.isArray(patch.pendingEvents)) state.pendingEvents = pruneEvents(patch.pendingEvents, now)
|
|
698
|
-
if (Array.isArray(patch.timeline)) state.timeline = pruneTimeline(patch.timeline, now)
|
|
699
|
-
if (typeof patch.followupChainCount === 'number') state.followupChainCount = clampInt(patch.followupChainCount, state.followupChainCount, 0, 100)
|
|
700
|
-
if (typeof patch.metaMissCount === 'number') state.metaMissCount = clampInt(patch.metaMissCount, state.metaMissCount, 0, 100)
|
|
701
|
-
if (Array.isArray(patch.workingMemoryNotes)) state.workingMemoryNotes = normalizeStringList(patch.workingMemoryNotes, 24, 260)
|
|
702
|
-
if (typeof patch.lastMemoryNoteAt === 'number') state.lastMemoryNoteAt = patch.lastMemoryNoteAt
|
|
703
|
-
if (patch.lastMemoryNoteAt === null) state.lastMemoryNoteAt = null
|
|
704
|
-
if (typeof patch.lastPlannedAt === 'number') state.lastPlannedAt = patch.lastPlannedAt
|
|
705
|
-
if (patch.lastPlannedAt === null) state.lastPlannedAt = null
|
|
706
|
-
if (typeof patch.lastReviewedAt === 'number') state.lastReviewedAt = patch.lastReviewedAt
|
|
707
|
-
if (patch.lastReviewedAt === null) state.lastReviewedAt = null
|
|
708
|
-
|
|
709
|
-
state.momentumScore = computeMomentumScore(state)
|
|
710
|
-
state.updatedAt = now
|
|
711
|
-
session.mainLoopState = state
|
|
712
|
-
sessions[sessionId] = session
|
|
713
|
-
saveSessions(sessions)
|
|
714
|
-
return state
|
|
93
|
+
void sessionId
|
|
94
|
+
void patch
|
|
95
|
+
return null
|
|
715
96
|
}
|
|
716
97
|
|
|
717
98
|
export function pushMainLoopEventToMainSessions(input: PushMainLoopEventInput): number {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const sessions = loadSessions()
|
|
722
|
-
const now = Date.now()
|
|
723
|
-
let changed = 0
|
|
724
|
-
|
|
725
|
-
for (const session of Object.values(sessions) as any[]) {
|
|
726
|
-
if (!isMainSession(session)) continue
|
|
727
|
-
if (input.user && session.user && session.user !== input.user) continue
|
|
728
|
-
|
|
729
|
-
const state = normalizeState(session.mainLoopState, now)
|
|
730
|
-
const appended = appendEvent(state, input.type || 'event', text, now)
|
|
731
|
-
if (!appended) continue
|
|
732
|
-
appendTimeline(state, input.type || 'event', text, now, state.status)
|
|
733
|
-
state.momentumScore = computeMomentumScore(state)
|
|
734
|
-
state.updatedAt = now
|
|
735
|
-
session.mainLoopState = state
|
|
736
|
-
changed += 1
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (changed > 0) {
|
|
740
|
-
saveSessions(sessions)
|
|
741
|
-
log.info('main-loop', `Queued event for ${changed} main session(s)`, {
|
|
742
|
-
type: input.type,
|
|
743
|
-
text,
|
|
744
|
-
user: input.user || null,
|
|
745
|
-
})
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
return changed
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const AgentHeartbeatMetaSchema = z.object({
|
|
752
|
-
goal: z.string().trim().optional(),
|
|
753
|
-
status: z.enum(['progress', 'ok', 'idle', 'blocked']).optional(),
|
|
754
|
-
next_action: z.string().trim().optional(),
|
|
755
|
-
}).passthrough()
|
|
756
|
-
|
|
757
|
-
type AgentHeartbeatMeta = z.infer<typeof AgentHeartbeatMetaSchema>
|
|
758
|
-
|
|
759
|
-
function parseAgentHeartbeatMeta(text: string): AgentHeartbeatMeta | null {
|
|
760
|
-
const raw = (text || '').trim()
|
|
761
|
-
if (!raw) return null
|
|
762
|
-
const match = raw.match(AGENT_HEARTBEAT_META_RE)
|
|
763
|
-
if (!match?.[1]) return null
|
|
764
|
-
try {
|
|
765
|
-
const parsed = JSON.parse(match[1])
|
|
766
|
-
return AgentHeartbeatMetaSchema.parse(parsed)
|
|
767
|
-
} catch {
|
|
768
|
-
return null
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function handleAgentHeartbeatResult(session: any, input: HandleMainLoopRunResultInput): null {
|
|
773
|
-
if (!input.internal || input.source !== 'heartbeat') return null
|
|
774
|
-
if (!session.agentId) return null
|
|
775
|
-
const text = input.resultText || ''
|
|
776
|
-
if (!text.trim()) return null
|
|
777
|
-
|
|
778
|
-
const meta = parseAgentHeartbeatMeta(text)
|
|
779
|
-
if (!meta) return null
|
|
780
|
-
|
|
781
|
-
const agents = loadAgents()
|
|
782
|
-
const agent = agents[session.agentId]
|
|
783
|
-
if (!agent) return null
|
|
784
|
-
|
|
785
|
-
let changed = false
|
|
786
|
-
if (meta.goal && meta.goal !== agent.heartbeatGoal) {
|
|
787
|
-
agent.heartbeatGoal = meta.goal
|
|
788
|
-
changed = true
|
|
789
|
-
log.info('agent-heartbeat', `Goal updated for agent ${agent.name}: ${meta.goal.slice(0, 120)}`)
|
|
790
|
-
}
|
|
791
|
-
if (meta.next_action) {
|
|
792
|
-
agent.heartbeatNextAction = meta.next_action
|
|
793
|
-
changed = true
|
|
794
|
-
}
|
|
795
|
-
if (meta.status) {
|
|
796
|
-
agent.heartbeatStatus = meta.status
|
|
797
|
-
changed = true
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
if (changed) {
|
|
801
|
-
agents[session.agentId] = agent
|
|
802
|
-
saveAgents(agents)
|
|
803
|
-
}
|
|
804
|
-
return null
|
|
99
|
+
void input
|
|
100
|
+
return 0
|
|
805
101
|
}
|
|
806
102
|
|
|
807
103
|
export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
if (!session) return null
|
|
811
|
-
if (!isMainLoopSession(session)) return handleAgentHeartbeatResult(session, input)
|
|
812
|
-
|
|
813
|
-
const now = Date.now()
|
|
814
|
-
const state = normalizeState(session.mainLoopState, now)
|
|
815
|
-
state.pendingEvents = pruneEvents(state.pendingEvents, now)
|
|
816
|
-
let forceMemoryNote = false
|
|
817
|
-
|
|
818
|
-
const userGoalContract = parseGoalContractFromText(input.message)
|
|
819
|
-
if (!input.internal) {
|
|
820
|
-
if (userGoalContract?.objective) {
|
|
821
|
-
state.goal = userGoalContract.objective
|
|
822
|
-
state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
|
|
823
|
-
state.status = 'progress'
|
|
824
|
-
appendTimeline(state, 'user_goal_contract', `Goal contract updated: ${userGoalContract.objective}`, now, state.status)
|
|
825
|
-
appendWorkingMemoryNote(state, `contract:${userGoalContract.objective}`)
|
|
826
|
-
forceMemoryNote = true
|
|
827
|
-
logExecution(input.sessionId, 'mission_start', `New goal contract: ${toOneLine(userGoalContract.objective, 200)}`, {
|
|
828
|
-
agentId: session.agentId,
|
|
829
|
-
detail: { goal: userGoalContract.objective, contract: userGoalContract, planSteps: state.planSteps },
|
|
830
|
-
})
|
|
831
|
-
}
|
|
832
|
-
state.followupChainCount = 0
|
|
833
|
-
state.missionTokens = 0
|
|
834
|
-
state.missionCostUsd = 0
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Accumulate per-mission token/cost tracking
|
|
838
|
-
if (typeof input.inputTokens === 'number') {
|
|
839
|
-
state.missionTokens = (state.missionTokens || 0) + input.inputTokens + (input.outputTokens || 0)
|
|
840
|
-
}
|
|
841
|
-
if (typeof input.estimatedCost === 'number') {
|
|
842
|
-
state.missionCostUsd = (state.missionCostUsd || 0) + input.estimatedCost
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (state.paused && input.internal) {
|
|
846
|
-
appendTimeline(state, 'paused_skip', `Skipped internal tick from ${input.source} because mission is paused.`, now, state.status)
|
|
847
|
-
state.momentumScore = computeMomentumScore(state)
|
|
848
|
-
state.updatedAt = now
|
|
849
|
-
session.mainLoopState = state
|
|
850
|
-
sessions[input.sessionId] = session
|
|
851
|
-
saveSessions(sessions)
|
|
852
|
-
return null
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if (input.error) {
|
|
856
|
-
appendEvent(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 400)}`, now)
|
|
857
|
-
appendTimeline(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 220)}`, now, 'blocked')
|
|
858
|
-
state.status = 'blocked'
|
|
859
|
-
appendWorkingMemoryNote(state, `blocked:${toOneLine(input.error, 120)}`)
|
|
860
|
-
forceMemoryNote = true
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
for (const event of input.toolEvents || []) {
|
|
864
|
-
if (!event?.error) continue
|
|
865
|
-
appendEvent(
|
|
866
|
-
state,
|
|
867
|
-
'tool_error',
|
|
868
|
-
`Tool ${event.name || 'unknown'} error: ${toOneLine(event.output || event.input || 'unknown error', 400)}`,
|
|
869
|
-
now,
|
|
870
|
-
)
|
|
871
|
-
appendTimeline(
|
|
872
|
-
state,
|
|
873
|
-
'tool_error',
|
|
874
|
-
`Tool ${event.name || 'unknown'} error encountered.`,
|
|
875
|
-
now,
|
|
876
|
-
'blocked',
|
|
877
|
-
)
|
|
878
|
-
forceMemoryNote = true
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
let followup: MainLoopFollowupRequest | null = null
|
|
882
|
-
const shouldAutoKickFromUserGoal = !input.internal
|
|
883
|
-
&& !input.error
|
|
884
|
-
&& !!userGoalContract?.objective
|
|
885
|
-
&& !state.paused
|
|
886
|
-
&& state.autonomyMode === 'autonomous'
|
|
887
|
-
|
|
888
|
-
if (shouldAutoKickFromUserGoal) {
|
|
889
|
-
followup = {
|
|
890
|
-
message: buildFollowupPrompt(state, { agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
|
|
891
|
-
delayMs: 1500,
|
|
892
|
-
dedupeKey: `main-loop-user-kickoff:${input.sessionId}`,
|
|
893
|
-
}
|
|
894
|
-
appendTimeline(state, 'followup', 'Queued autonomous kickoff follow-up from new user goal.', now, state.status)
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
if (input.internal) {
|
|
898
|
-
state.lastTickAt = now
|
|
899
|
-
const trimmedText = (input.resultText || '').trim()
|
|
900
|
-
const isHeartbeatOk = /^HEARTBEAT_OK$/i.test(trimmedText)
|
|
901
|
-
const meta = parseMainLoopMeta(trimmedText)
|
|
902
|
-
const planMeta = parseMainLoopPlan(trimmedText)
|
|
903
|
-
const reviewMeta = parseMainLoopReview(trimmedText)
|
|
904
|
-
|
|
905
|
-
if (planMeta) {
|
|
906
|
-
if (planMeta.steps?.length) {
|
|
907
|
-
state.planSteps = planMeta.steps
|
|
908
|
-
state.lastPlannedAt = now
|
|
909
|
-
appendWorkingMemoryNote(state, `plan:${planMeta.steps.join(' -> ')}`)
|
|
910
|
-
}
|
|
911
|
-
if (planMeta.current_step) {
|
|
912
|
-
state.currentPlanStep = planMeta.current_step
|
|
913
|
-
state.lastPlannedAt = now
|
|
914
|
-
}
|
|
915
|
-
appendTimeline(state, 'plan', `Plan updated${planMeta.current_step ? ` at step: ${planMeta.current_step}` : ''}.`, now, state.status)
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
if (reviewMeta) {
|
|
919
|
-
if (reviewMeta.note) {
|
|
920
|
-
state.reviewNote = reviewMeta.note
|
|
921
|
-
appendWorkingMemoryNote(state, `review:${reviewMeta.note}`)
|
|
922
|
-
}
|
|
923
|
-
if (typeof reviewMeta.confidence === 'number') state.reviewConfidence = reviewMeta.confidence
|
|
924
|
-
state.lastReviewedAt = now
|
|
925
|
-
if (reviewMeta.needs_replan === true && state.planSteps.length > 0) {
|
|
926
|
-
appendEvent(state, 'review_replan', 'Execution review requested replanning.', now)
|
|
927
|
-
}
|
|
928
|
-
appendTimeline(state, 'review', reviewMeta.note || 'Execution review updated.', now, state.status)
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
if (meta) {
|
|
932
|
-
state.metaMissCount = 0
|
|
933
|
-
if (meta.goal) {
|
|
934
|
-
state.goal = meta.goal
|
|
935
|
-
const metaGoalContract = parseGoalContractFromText(meta.goal)
|
|
936
|
-
if (metaGoalContract) state.goalContract = mergeGoalContracts(state.goalContract, metaGoalContract)
|
|
937
|
-
}
|
|
938
|
-
if (meta.status) state.status = meta.status
|
|
939
|
-
if (meta.summary) state.summary = meta.summary
|
|
940
|
-
if (meta.next_action) state.nextAction = meta.next_action
|
|
941
|
-
if (meta.summary) appendWorkingMemoryNote(state, `summary:${toOneLine(meta.summary, 180)}`)
|
|
942
|
-
if (meta.next_action) appendWorkingMemoryNote(state, `next:${toOneLine(meta.next_action, 180)}`)
|
|
943
|
-
appendTimeline(
|
|
944
|
-
state,
|
|
945
|
-
'meta',
|
|
946
|
-
`Meta update: status=${meta.status || state.status}; summary=${toOneLine(meta.summary || state.summary || 'none', 140)}`,
|
|
947
|
-
now,
|
|
948
|
-
meta.status || state.status,
|
|
949
|
-
)
|
|
950
|
-
consumeEvents(state, meta.consume_event_ids)
|
|
951
|
-
|
|
952
|
-
// Budget enforcement: check mission cost against goalContract.budgetUsd
|
|
953
|
-
const budgetUsd = state.goalContract?.budgetUsd
|
|
954
|
-
if (typeof budgetUsd === 'number' && budgetUsd > 0 && typeof state.missionCostUsd === 'number') {
|
|
955
|
-
const usageRatio = state.missionCostUsd / budgetUsd
|
|
956
|
-
if (usageRatio >= 1.0 && !state.paused) {
|
|
957
|
-
state.paused = true
|
|
958
|
-
state.status = 'blocked'
|
|
959
|
-
appendTimeline(state, 'budget_exceeded', `Mission budget exceeded ($${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}). Mission paused.`, now, 'blocked')
|
|
960
|
-
appendEvent(state, 'budget_exceeded', `Budget limit reached: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)}`, now)
|
|
961
|
-
logExecution(input.sessionId, 'budget_warning', `Budget exceeded: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
|
|
962
|
-
agentId: session.agentId,
|
|
963
|
-
detail: { missionCostUsd: state.missionCostUsd, budgetUsd, missionTokens: state.missionTokens },
|
|
964
|
-
})
|
|
965
|
-
} else if (usageRatio >= 0.8) {
|
|
966
|
-
appendEvent(state, 'budget_warning', `Mission approaching budget limit: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)} (${Math.round(usageRatio * 100)}%)`, now)
|
|
967
|
-
logExecution(input.sessionId, 'budget_warning', `Budget at ${Math.round(usageRatio * 100)}%: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
|
|
968
|
-
agentId: session.agentId,
|
|
969
|
-
detail: { missionCostUsd: state.missionCostUsd, budgetUsd, usagePercent: Math.round(usageRatio * 100) },
|
|
970
|
-
})
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
if (meta.follow_up === true && !input.error && !isHeartbeatOk && !state.paused && state.followupChainCount < getMaxFollowupChain(session.agentId)) {
|
|
975
|
-
state.followupChainCount += 1
|
|
976
|
-
const delaySec = clampInt(meta.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900)
|
|
977
|
-
followup = {
|
|
978
|
-
message: buildFollowupPrompt(state, { agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
|
|
979
|
-
delayMs: delaySec * 1000,
|
|
980
|
-
dedupeKey: `main-loop-followup:${input.sessionId}`,
|
|
981
|
-
}
|
|
982
|
-
appendTimeline(state, 'followup', `Queued chained follow-up in ${delaySec}s.`, now, state.status)
|
|
983
|
-
} else if (meta.follow_up === false || isHeartbeatOk) {
|
|
984
|
-
state.followupChainCount = 0
|
|
985
|
-
}
|
|
986
|
-
if (state.status === 'ok' || state.status === 'blocked') {
|
|
987
|
-
forceMemoryNote = true
|
|
988
|
-
if (state.status === 'ok') {
|
|
989
|
-
// Auto-pause the mission loop — the goal is complete
|
|
990
|
-
state.paused = true
|
|
991
|
-
state.followupChainCount = 0
|
|
992
|
-
followup = null
|
|
993
|
-
logExecution(input.sessionId, 'mission_complete', `Mission completed: ${toOneLine(state.goal || 'unknown', 200)}`, {
|
|
994
|
-
agentId: session.agentId,
|
|
995
|
-
detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
|
|
996
|
-
})
|
|
997
|
-
appendTimeline(state, 'auto_pause', 'Mission goal completed — auto-paused.', now, state.status)
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
} else if (!isHeartbeatOk && trimmedText) {
|
|
1001
|
-
state.metaMissCount = Math.min(100, state.metaMissCount + 1)
|
|
1002
|
-
state.summary = toOneLine(trimmedText, 700)
|
|
1003
|
-
appendWorkingMemoryNote(state, `inferred:${toOneLine(trimmedText, 160)}`)
|
|
1004
|
-
if (state.status === 'idle') state.status = 'progress'
|
|
1005
|
-
appendEvent(state, 'meta_missing', 'Main-loop reply missing [MAIN_LOOP_META] contract; state inferred from text.', now)
|
|
1006
|
-
appendTimeline(state, 'meta_missing', 'Missing [MAIN_LOOP_META]; inferred state from plain text.', now, state.status)
|
|
1007
|
-
} else if (isHeartbeatOk) {
|
|
1008
|
-
state.metaMissCount = 0
|
|
1009
|
-
appendTimeline(state, 'heartbeat_ok', 'Heartbeat returned HEARTBEAT_OK.', now, state.status)
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
if (input.internal && state.status === 'ok') {
|
|
1014
|
-
const completionGateReason = getMissionCompletionGateReason(session, state, input.resultText || '')
|
|
1015
|
-
if (completionGateReason) {
|
|
1016
|
-
state.status = 'progress'
|
|
1017
|
-
if (!state.nextAction || /^no queued action/i.test(state.nextAction)) {
|
|
1018
|
-
state.nextAction = 'Wait for the next schedule run and verify a screenshot artifact link is delivered.'
|
|
1019
|
-
}
|
|
1020
|
-
appendEvent(state, 'completion_gate', completionGateReason, now)
|
|
1021
|
-
appendTimeline(state, 'completion_gate', 'Holding completion until screenshot artifact evidence is observed.', now, state.status)
|
|
1022
|
-
appendWorkingMemoryNote(state, `gate:${toOneLine(completionGateReason, 180)}`)
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// Agents don't auto-create tasks for themselves — they just do the work.
|
|
1027
|
-
// Tasks are created explicitly by the user or when delegating to another agent.
|
|
1028
|
-
const shouldWritePeriodicMemory = !!state.summary && state.status === 'progress'
|
|
1029
|
-
maybeStoreMissionMemoryNote(session, state, now, input.source, forceMemoryNote || shouldWritePeriodicMemory)
|
|
1030
|
-
|
|
1031
|
-
state.momentumScore = computeMomentumScore(state)
|
|
1032
|
-
|
|
1033
|
-
state.updatedAt = now
|
|
1034
|
-
session.mainLoopState = state
|
|
1035
|
-
sessions[input.sessionId] = session
|
|
1036
|
-
saveSessions(sessions)
|
|
1037
|
-
|
|
1038
|
-
return followup
|
|
104
|
+
void input
|
|
105
|
+
return null
|
|
1039
106
|
}
|