@swarmclawai/swarmclaw 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -1,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, loadTasks, saveTasks, loadSettings } from './storage'
|
|
5
|
-
import { log } from './logger'
|
|
6
|
-
import { getMemoryDb } from './memory-db'
|
|
7
|
-
import { isProtectedMainSession } 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,1038 +66,34 @@ export interface HandleMainLoopRunResultInput {
|
|
|
137
66
|
estimatedCost?: number
|
|
138
67
|
}
|
|
139
68
|
|
|
140
|
-
function
|
|
141
|
-
return
|
|
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
|
-
function inferGoalFromUserMessage(message: string): string | null {
|
|
356
|
-
const text = (message || '').trim()
|
|
357
|
-
if (!text) return null
|
|
358
|
-
if (/^SWARM_MAIN_(MISSION_TICK|AUTO_FOLLOWUP)\b/i.test(text)) return null
|
|
359
|
-
if (/^SWARM_HEARTBEAT_CHECK\b/i.test(text)) return null
|
|
360
|
-
if (/^(ok|okay|cool|thanks|thx|got it|nice|yep|yeah|nope|nah)[.! ]*$/i.test(text)) return null
|
|
361
|
-
return text.slice(0, 600)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function inferGoalFromSessionMessages(session: any): string | null {
|
|
365
|
-
const msgs = Array.isArray(session?.messages) ? session.messages : []
|
|
366
|
-
for (let i = msgs.length - 1; i >= 0; i -= 1) {
|
|
367
|
-
const msg = msgs[i]
|
|
368
|
-
if (msg?.role !== 'user') continue
|
|
369
|
-
const inferred = inferGoalFromUserMessage(typeof msg?.text === 'string' ? msg.text : '')
|
|
370
|
-
if (inferred) return inferred
|
|
371
|
-
}
|
|
372
|
-
return null
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function parseMainLoopMeta(text: string): MainLoopMeta | null {
|
|
376
|
-
const raw = (text || '').trim()
|
|
377
|
-
if (!raw) return null
|
|
378
|
-
|
|
379
|
-
const markerMatch = raw.match(META_LINE_RE)
|
|
380
|
-
const parseCandidate = markerMatch?.[1]
|
|
381
|
-
if (parseCandidate) {
|
|
382
|
-
try {
|
|
383
|
-
const parsed = JSON.parse(parseCandidate)
|
|
384
|
-
return normalizeMeta(parsed)
|
|
385
|
-
} catch {
|
|
386
|
-
// fall through
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Fallback: parse any one-line JSON that appears to be the meta payload.
|
|
391
|
-
for (const line of raw.split('\n')) {
|
|
392
|
-
const trimmed = line.trim()
|
|
393
|
-
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue
|
|
394
|
-
if (!trimmed.includes('follow_up') && !trimmed.includes('next_action') && !trimmed.includes('consume_event_ids')) continue
|
|
395
|
-
try {
|
|
396
|
-
const parsed = JSON.parse(trimmed)
|
|
397
|
-
return normalizeMeta(parsed)
|
|
398
|
-
} catch {
|
|
399
|
-
// skip malformed candidate lines
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return null
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function normalizeMeta(raw: any): MainLoopMeta {
|
|
407
|
-
const status = raw?.status === 'blocked' || raw?.status === 'ok' || raw?.status === 'progress' || raw?.status === 'idle'
|
|
408
|
-
? raw.status
|
|
409
|
-
: undefined
|
|
410
|
-
|
|
411
|
-
const consumeIds = Array.isArray(raw?.consume_event_ids)
|
|
412
|
-
? raw.consume_event_ids
|
|
413
|
-
.map((v: unknown) => (typeof v === 'string' ? v.trim() : ''))
|
|
414
|
-
.filter(Boolean)
|
|
415
|
-
: undefined
|
|
416
|
-
|
|
417
|
-
const followUp = typeof raw?.follow_up === 'boolean'
|
|
418
|
-
? raw.follow_up
|
|
419
|
-
: typeof raw?.follow_up === 'string'
|
|
420
|
-
? raw.follow_up.trim().toLowerCase() === 'true'
|
|
421
|
-
: undefined
|
|
422
|
-
|
|
423
|
-
return {
|
|
424
|
-
status,
|
|
425
|
-
summary: typeof raw?.summary === 'string' ? raw.summary.trim().slice(0, 800) : undefined,
|
|
426
|
-
next_action: typeof raw?.next_action === 'string' ? raw.next_action.trim().slice(0, 600) : undefined,
|
|
427
|
-
follow_up: followUp,
|
|
428
|
-
delay_sec: clampInt(raw?.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900),
|
|
429
|
-
goal: typeof raw?.goal === 'string' ? raw.goal.trim().slice(0, 600) : undefined,
|
|
430
|
-
consume_event_ids: consumeIds,
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function consumeEvents(state: MainLoopState, ids: string[] | undefined) {
|
|
435
|
-
if (!ids?.length) return
|
|
436
|
-
const remove = new Set(ids)
|
|
437
|
-
state.pendingEvents = state.pendingEvents.filter((event) => !remove.has(event.id))
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function buildPendingEventLines(state: MainLoopState): string {
|
|
441
|
-
if (!state.pendingEvents.length) return 'Pending events:\n- none'
|
|
442
|
-
const lines = state.pendingEvents
|
|
443
|
-
.slice(-10)
|
|
444
|
-
.map((event) => `- ${event.id} | ${event.type} | ${event.text}`)
|
|
445
|
-
.join('\n')
|
|
446
|
-
return `Pending events (oldest → newest):\n${lines}`
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function buildTimelineLines(state: MainLoopState): string {
|
|
450
|
-
if (!state.timeline.length) return 'Recent mission timeline:\n- none'
|
|
451
|
-
const lines = state.timeline
|
|
452
|
-
.slice(-5)
|
|
453
|
-
.map((entry) => {
|
|
454
|
-
const ts = new Date(entry.at).toISOString().slice(11, 19)
|
|
455
|
-
const status = entry.status ? ` [${entry.status}]` : ''
|
|
456
|
-
return `- ${ts} ${entry.source}${status}: ${entry.note}`
|
|
457
|
-
})
|
|
458
|
-
.join('\n')
|
|
459
|
-
return `Recent mission timeline:\n${lines}`
|
|
69
|
+
export function isMainSession(_session: unknown): boolean {
|
|
70
|
+
return false
|
|
460
71
|
}
|
|
461
72
|
|
|
462
|
-
function
|
|
463
|
-
|
|
464
|
-
if (!contract?.objective) return []
|
|
465
|
-
const lines = [
|
|
466
|
-
`contract_objective: ${contract.objective}`,
|
|
467
|
-
]
|
|
468
|
-
if (contract.constraints?.length) lines.push(`contract_constraints: ${contract.constraints.join(' | ')}`)
|
|
469
|
-
if (typeof contract.budgetUsd === 'number') lines.push(`contract_budget_usd: ${contract.budgetUsd}`)
|
|
470
|
-
if (typeof contract.deadlineAt === 'number') lines.push(`contract_deadline_iso: ${new Date(contract.deadlineAt).toISOString()}`)
|
|
471
|
-
if (contract.successMetric) lines.push(`contract_success_metric: ${contract.successMetric}`)
|
|
472
|
-
return lines
|
|
73
|
+
export function buildMainLoopHeartbeatPrompt(_session: unknown, fallbackPrompt: string): string {
|
|
74
|
+
return fallbackPrompt
|
|
473
75
|
}
|
|
474
76
|
|
|
475
|
-
function
|
|
476
|
-
|
|
477
|
-
state.goal || '',
|
|
478
|
-
state.goalContract?.objective || '',
|
|
479
|
-
state.goalContract?.successMetric || '',
|
|
480
|
-
state.nextAction || '',
|
|
481
|
-
...(state.planSteps || []),
|
|
482
|
-
state.currentPlanStep || '',
|
|
483
|
-
].join(' ')
|
|
484
|
-
if (!SCREENSHOT_GOAL_HINT.test(haystack)) return false
|
|
485
|
-
return DELIVERY_GOAL_HINT.test(haystack) || SCHEDULE_GOAL_HINT.test(haystack)
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function missionHasScreenshotArtifactEvidence(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): boolean {
|
|
489
|
-
const candidates: string[] = [
|
|
490
|
-
state.summary || '',
|
|
491
|
-
additionalText || '',
|
|
492
|
-
]
|
|
493
|
-
if (Array.isArray(session?.messages)) {
|
|
494
|
-
for (let i = session.messages.length - 1; i >= 0 && candidates.length < 16; i--) {
|
|
495
|
-
const text = typeof session.messages[i]?.text === 'string' ? session.messages[i].text! : ''
|
|
496
|
-
if (text && text.trim()) candidates.push(text)
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
return candidates.some((value) => UPLOAD_ARTIFACT_HINT.test(value) || SENT_ARTIFACT_HINT.test(value))
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function getMissionCompletionGateReason(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): string | null {
|
|
503
|
-
if (!missionNeedsScreenshotArtifactEvidence(state)) return null
|
|
504
|
-
if (missionHasScreenshotArtifactEvidence(session, state, additionalText)) return null
|
|
505
|
-
return 'Mission requires screenshot artifact evidence (upload link or explicit sent screenshot confirmation) before completion.'
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function upsertMissionTask(session: any, state: MainLoopState, now: number): string | null {
|
|
509
|
-
if (!state.goal) return state.missionTaskId || null
|
|
510
|
-
|
|
511
|
-
const tasks = loadTasks()
|
|
512
|
-
let task = state.missionTaskId ? tasks[state.missionTaskId] : null
|
|
513
|
-
if (!task) {
|
|
514
|
-
task = Object.values(tasks).find((t: any) =>
|
|
515
|
-
t?.sessionId === session.id
|
|
516
|
-
&& t?.title?.startsWith('Mission:')
|
|
517
|
-
&& t?.status !== 'archived'
|
|
518
|
-
) as any || null
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const title = `Mission: ${state.goal.slice(0, 140)}`
|
|
522
|
-
const statusMap = {
|
|
523
|
-
idle: 'backlog',
|
|
524
|
-
progress: 'running',
|
|
525
|
-
reflection: 'running',
|
|
526
|
-
blocked: 'failed',
|
|
527
|
-
ok: 'completed',
|
|
528
|
-
} as const
|
|
529
|
-
let mappedStatus = statusMap[state.status]
|
|
530
|
-
const completionGateReason = mappedStatus === 'completed'
|
|
531
|
-
? getMissionCompletionGateReason(session, state)
|
|
532
|
-
: null
|
|
533
|
-
if (completionGateReason) mappedStatus = 'running'
|
|
534
|
-
|
|
535
|
-
let changed = false
|
|
536
|
-
const contractLines = buildGoalContractLines(state)
|
|
537
|
-
const planLines = state.planSteps.length
|
|
538
|
-
? [`plan_steps: ${state.planSteps.join(' -> ')}`]
|
|
539
|
-
: []
|
|
540
|
-
if (state.currentPlanStep) planLines.push(`current_plan_step: ${state.currentPlanStep}`)
|
|
541
|
-
if (state.reviewNote) planLines.push(`latest_review: ${state.reviewNote}`)
|
|
542
|
-
|
|
543
|
-
const baseDescription = [
|
|
544
|
-
'Autonomous mission goal tracked from main loop.',
|
|
545
|
-
`Goal: ${state.goal}`,
|
|
546
|
-
state.nextAction ? `Next action: ${state.nextAction}` : '',
|
|
547
|
-
completionGateReason ? `Completion gate: ${completionGateReason}` : '',
|
|
548
|
-
...contractLines,
|
|
549
|
-
...planLines,
|
|
550
|
-
].filter(Boolean).join('\n')
|
|
551
|
-
|
|
552
|
-
if (!task) {
|
|
553
|
-
const id = genId()
|
|
554
|
-
task = {
|
|
555
|
-
id,
|
|
556
|
-
title,
|
|
557
|
-
description: baseDescription,
|
|
558
|
-
status: mappedStatus,
|
|
559
|
-
agentId: session.agentId || 'default',
|
|
560
|
-
sessionId: session.id,
|
|
561
|
-
result: state.summary || null,
|
|
562
|
-
error: state.status === 'blocked' ? (state.summary || 'Blocked') : null,
|
|
563
|
-
createdAt: now,
|
|
564
|
-
updatedAt: now,
|
|
565
|
-
startedAt: mappedStatus === 'running' ? now : null,
|
|
566
|
-
completedAt: mappedStatus === 'completed' ? now : null,
|
|
567
|
-
queuedAt: null,
|
|
568
|
-
archivedAt: null,
|
|
569
|
-
comments: [],
|
|
570
|
-
images: [],
|
|
571
|
-
validation: null,
|
|
572
|
-
}
|
|
573
|
-
tasks[id] = task
|
|
574
|
-
changed = true
|
|
575
|
-
} else {
|
|
576
|
-
if (task.title !== title) {
|
|
577
|
-
task.title = title
|
|
578
|
-
changed = true
|
|
579
|
-
}
|
|
580
|
-
const nextDescription = baseDescription
|
|
581
|
-
if (task.description !== nextDescription) {
|
|
582
|
-
task.description = nextDescription
|
|
583
|
-
changed = true
|
|
584
|
-
}
|
|
585
|
-
if (task.status !== mappedStatus) {
|
|
586
|
-
task.status = mappedStatus
|
|
587
|
-
changed = true
|
|
588
|
-
if (mappedStatus === 'running' && !task.startedAt) task.startedAt = now
|
|
589
|
-
if (mappedStatus === 'completed') task.completedAt = now
|
|
590
|
-
}
|
|
591
|
-
const nextResult = state.summary || task.result || null
|
|
592
|
-
if (task.result !== nextResult) {
|
|
593
|
-
task.result = nextResult
|
|
594
|
-
changed = true
|
|
595
|
-
}
|
|
596
|
-
const nextError = mappedStatus === 'failed'
|
|
597
|
-
? (state.summary || state.nextAction || 'Blocked')
|
|
598
|
-
: null
|
|
599
|
-
if (task.error !== nextError) {
|
|
600
|
-
task.error = nextError
|
|
601
|
-
changed = true
|
|
602
|
-
}
|
|
603
|
-
if (changed) task.updatedAt = now
|
|
604
|
-
tasks[task.id] = task
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (changed) {
|
|
608
|
-
saveTasks(tasks)
|
|
609
|
-
}
|
|
610
|
-
return task?.id || null
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function maybeStoreMissionMemoryNote(
|
|
614
|
-
session: any,
|
|
615
|
-
state: MainLoopState,
|
|
616
|
-
now: number,
|
|
617
|
-
source: string,
|
|
618
|
-
force = false,
|
|
619
|
-
) {
|
|
620
|
-
if (!Array.isArray(session?.tools) || !session.tools.includes('memory')) return
|
|
621
|
-
if (!state.goal) return
|
|
622
|
-
if (!force && state.lastMemoryNoteAt && (now - state.lastMemoryNoteAt) < MEMORY_NOTE_MIN_INTERVAL_MS) return
|
|
623
|
-
|
|
624
|
-
const summary = state.summary || 'No summary'
|
|
625
|
-
const next = state.nextAction || 'No next action'
|
|
626
|
-
const title = `Mission ${state.status}: ${state.goal.slice(0, 72)}`
|
|
627
|
-
const content = [
|
|
628
|
-
`source: ${source}`,
|
|
629
|
-
`status: ${state.status}`,
|
|
630
|
-
`momentum: ${state.momentumScore}/100`,
|
|
631
|
-
`goal: ${state.goal}`,
|
|
632
|
-
...buildGoalContractLines(state),
|
|
633
|
-
state.planSteps.length ? `plan_steps: ${state.planSteps.join(' -> ')}` : '',
|
|
634
|
-
state.currentPlanStep ? `current_plan_step: ${state.currentPlanStep}` : '',
|
|
635
|
-
`summary: ${summary}`,
|
|
636
|
-
`next_action: ${next}`,
|
|
637
|
-
state.reviewNote ? `review: ${state.reviewNote}` : '',
|
|
638
|
-
typeof state.reviewConfidence === 'number' ? `review_confidence: ${state.reviewConfidence}` : '',
|
|
639
|
-
state.missionTaskId ? `mission_task_id: ${state.missionTaskId}` : '',
|
|
640
|
-
typeof state.missionTokens === 'number' ? `mission_tokens: ${state.missionTokens}` : '',
|
|
641
|
-
typeof state.missionCostUsd === 'number' ? `mission_cost_usd: $${state.missionCostUsd.toFixed(4)}` : '',
|
|
642
|
-
state.workingMemoryNotes?.length ? `working_notes: ${state.workingMemoryNotes.slice(-5).join('; ')}` : '',
|
|
643
|
-
].filter(Boolean).join('\n')
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
const memDb = getMemoryDb()
|
|
647
|
-
const latest = memDb.getLatestBySessionCategory?.(session.id, 'mission')
|
|
648
|
-
if (latest) {
|
|
649
|
-
const sameTitle = normalizeMemoryText(latest.title) === normalizeMemoryText(title)
|
|
650
|
-
const sameContent = normalizeMemoryText(latest.content) === normalizeMemoryText(content)
|
|
651
|
-
if (sameTitle && sameContent) {
|
|
652
|
-
state.lastMemoryNoteAt = now
|
|
653
|
-
return
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
memDb.add({
|
|
657
|
-
agentId: session.agentId || null,
|
|
658
|
-
sessionId: session.id,
|
|
659
|
-
category: 'mission',
|
|
660
|
-
title,
|
|
661
|
-
content,
|
|
662
|
-
})
|
|
663
|
-
state.lastMemoryNoteAt = now
|
|
664
|
-
logExecution(session.id, 'mission_checkpoint', `Checkpoint: ${toOneLine(state.goal || '', 120)}`, {
|
|
665
|
-
agentId: session.agentId,
|
|
666
|
-
detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
|
|
667
|
-
})
|
|
668
|
-
} catch (err: unknown) {
|
|
669
|
-
appendEvent(state, 'memory_note_error', `Failed to store mission memory note: ${toOneLine(err instanceof Error ? err.message : String(err), 240)}`, now)
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: boolean; agent?: Record<string, unknown> | null; session?: Record<string, unknown> | null }): string {
|
|
674
|
-
const hasMemoryTool = opts?.hasMemoryTool === true
|
|
675
|
-
const identityContext = buildIdentityContext(opts?.session, opts?.agent)
|
|
676
|
-
const goal = state.goal || 'No explicit goal yet. Continue with the strongest actionable objective from recent context.'
|
|
677
|
-
const nextAction = state.nextAction || 'Determine the next highest-impact action and execute it.'
|
|
678
|
-
const contractLines = buildGoalContractLines(state)
|
|
679
|
-
return [
|
|
680
|
-
'SWARM_MAIN_AUTO_FOLLOWUP',
|
|
681
|
-
identityContext,
|
|
682
|
-
COMPANION_GOAL_PROMPT,
|
|
683
|
-
`Mission goal: ${goal}`,
|
|
684
|
-
`Next action to execute now: ${nextAction}`,
|
|
685
|
-
`Current status: ${state.status}`,
|
|
686
|
-
`Mission task id: ${state.missionTaskId || 'none'}`,
|
|
687
|
-
`Momentum score: ${state.momentumScore}/100`,
|
|
688
|
-
...contractLines,
|
|
689
|
-
state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
|
|
690
|
-
state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
|
|
691
|
-
state.reviewNote ? `Last review: ${state.reviewNote}` : '',
|
|
692
|
-
buildPendingEventLines(state),
|
|
693
|
-
buildTimelineLines(state),
|
|
694
|
-
state.planSteps.length === 0 && state.followupChainCount === 0
|
|
695
|
-
? '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.'
|
|
696
|
-
: '',
|
|
697
|
-
'Act autonomously. Use available tools to execute work, verify results, and keep momentum.',
|
|
698
|
-
state.autonomyMode === 'assist'
|
|
699
|
-
? 'Assist mode: execute safe internal analysis by default, and ask before irreversible external side effects (sending messages, purchases, account mutations).'
|
|
700
|
-
: 'Autonomous mode: execute safe next actions without waiting for confirmation; ask only when blocked by permissions, credentials, or policy.',
|
|
701
|
-
'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
|
|
702
|
-
hasMemoryTool
|
|
703
|
-
? 'Use memory_tool actively: recall relevant prior notes before acting, and store a concise note after each meaningful step.'
|
|
704
|
-
: 'memory_tool is unavailable in this session. Keep concise progress summaries in your status/meta output.',
|
|
705
|
-
'If you are blocked by missing credentials, permissions, or policy limits, say exactly what is blocked and the smallest unblock needed.',
|
|
706
|
-
'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).',
|
|
707
|
-
'If no meaningful action remains right now, reply exactly HEARTBEAT_OK.',
|
|
708
|
-
'Otherwise include a concise human update, then append exactly one [MAIN_LOOP_META] JSON line.',
|
|
709
|
-
'Optionally append one [MAIN_LOOP_PLAN] JSON line when you create/revise a plan.',
|
|
710
|
-
'Optionally append one [MAIN_LOOP_REVIEW] JSON line when you review recent execution results.',
|
|
711
|
-
'[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
|
|
712
|
-
'[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
|
|
713
|
-
'[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
|
|
714
|
-
].join('\n')
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
export function isMainSession(session: any): boolean {
|
|
718
|
-
return isProtectedMainSession(session)
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
|
|
722
|
-
const now = Date.now()
|
|
723
|
-
const agents = loadAgents()
|
|
724
|
-
const agent = session.agentId ? agents[session.agentId] : null
|
|
725
|
-
const identityContext = buildIdentityContext(session, agent)
|
|
726
|
-
const state = normalizeState(session?.mainLoopState, now)
|
|
727
|
-
const goal = state.goal || inferGoalFromSessionMessages(session) || null
|
|
728
|
-
const hasMemoryTool = Array.isArray(session?.tools) && session.tools.includes('memory')
|
|
729
|
-
|
|
730
|
-
const promptGoal = goal || 'No explicit mission captured yet. Infer the mission from recent user instructions and continue proactively.'
|
|
731
|
-
const promptSummary = state.summary || 'No prior mission summary yet.'
|
|
732
|
-
const promptNextAction = state.nextAction || 'No queued action. Determine one.'
|
|
733
|
-
const contractLines = buildGoalContractLines(state)
|
|
734
|
-
|
|
735
|
-
return [
|
|
736
|
-
'SWARM_MAIN_MISSION_TICK',
|
|
737
|
-
identityContext,
|
|
738
|
-
COMPANION_GOAL_PROMPT,
|
|
739
|
-
`Time: ${new Date(now).toISOString()}`,
|
|
740
|
-
`Mission goal: ${promptGoal}`,
|
|
741
|
-
`Current status: ${state.status}`,
|
|
742
|
-
`Mission paused: ${state.paused ? 'yes' : 'no'}`,
|
|
743
|
-
`Autonomy mode: ${state.autonomyMode}`,
|
|
744
|
-
`Mission task id: ${state.missionTaskId || 'none'}`,
|
|
745
|
-
`Momentum score: ${state.momentumScore}/100`,
|
|
746
|
-
...contractLines,
|
|
747
|
-
state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
|
|
748
|
-
state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
|
|
749
|
-
state.reviewNote ? `Last review: ${state.reviewNote}` : '',
|
|
750
|
-
`Last summary: ${toOneLine(promptSummary, 500)}`,
|
|
751
|
-
`Last next action: ${toOneLine(promptNextAction, 500)}`,
|
|
752
|
-
buildPendingEventLines(state),
|
|
753
|
-
buildTimelineLines(state),
|
|
754
|
-
'You are running the main autonomous mission loop. Continue executing toward the goal with initiative.',
|
|
755
|
-
state.autonomyMode === 'assist'
|
|
756
|
-
? 'Assist mode is active: execute safe internal work and ask before irreversible external side effects.'
|
|
757
|
-
: 'Autonomous mode is active: execute safe next actions without waiting for confirmation; only ask when blocked.',
|
|
758
|
-
'Use tools where needed, verify outcomes, and avoid vague status-only replies.',
|
|
759
|
-
'Do not ask broad exploratory questions when a safe next action exists. Pick a reasonable assumption, execute, and adapt from evidence.',
|
|
760
|
-
'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
|
|
761
|
-
hasMemoryTool
|
|
762
|
-
? 'Use memory_tool actively: recall relevant prior notes before acting, and store concise notes about progress, constraints, and next step after each meaningful action.'
|
|
763
|
-
: 'If memory_tool is unavailable, keep concise state in summary/next_action and continue execution.',
|
|
764
|
-
'Use a planner-executor-review loop: keep a concrete step plan, execute one meaningful step, then self-review and either continue or re-plan.',
|
|
765
|
-
'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).',
|
|
766
|
-
'If nothing important changed and no action is needed now, reply exactly HEARTBEAT_OK.',
|
|
767
|
-
'Otherwise: provide a concise human-readable update, then append exactly one [MAIN_LOOP_META] JSON line.',
|
|
768
|
-
'Optionally append one [MAIN_LOOP_PLAN] JSON line when creating/updating plan steps.',
|
|
769
|
-
'Optionally append one [MAIN_LOOP_REVIEW] JSON line after execution review.',
|
|
770
|
-
'[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
|
|
771
|
-
'[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
|
|
772
|
-
'[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
|
|
773
|
-
'The [MAIN_LOOP_META] JSON must be valid, on one line, and only appear once.',
|
|
774
|
-
`Fallback prompt context: ${fallbackPrompt || 'SWARM_HEARTBEAT_CHECK'}`,
|
|
775
|
-
].join('\n')
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
export function stripMainLoopMetaForPersistence(text: string, internal: boolean): string {
|
|
779
|
-
if (!internal) return text
|
|
780
|
-
if (!text) return ''
|
|
781
|
-
return text
|
|
77
|
+
export function stripMainLoopMetaForPersistence(text: string): string {
|
|
78
|
+
return (text || '')
|
|
782
79
|
.split('\n')
|
|
783
|
-
.filter((line) => !
|
|
80
|
+
.filter((line) => !LEGACY_META_LINE_RE.test(line))
|
|
784
81
|
.join('\n')
|
|
785
82
|
.trim()
|
|
786
83
|
}
|
|
787
84
|
|
|
788
|
-
export function getMainLoopStateForSession(
|
|
789
|
-
|
|
790
|
-
const session = sessions[sessionId]
|
|
791
|
-
if (!session || !isMainSession(session)) return null
|
|
792
|
-
return normalizeState(session.mainLoopState)
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
export function setMainLoopStateForSession(sessionId: string, patch: Partial<MainLoopState>): MainLoopState | null {
|
|
796
|
-
const sessions = loadSessions()
|
|
797
|
-
const session = sessions[sessionId]
|
|
798
|
-
if (!session || !isMainSession(session)) return null
|
|
799
|
-
const now = Date.now()
|
|
800
|
-
const state = normalizeState(session.mainLoopState, now)
|
|
801
|
-
|
|
802
|
-
if (typeof patch.goal === 'string') state.goal = patch.goal.trim().slice(0, 600) || null
|
|
803
|
-
if (patch.goal === null) state.goal = null
|
|
804
|
-
if (patch.goalContract !== undefined) state.goalContract = normalizeGoalContract(patch.goalContract)
|
|
805
|
-
if (patch.status === 'idle' || patch.status === 'progress' || patch.status === 'blocked' || patch.status === 'ok') state.status = patch.status
|
|
806
|
-
if (typeof patch.summary === 'string') state.summary = patch.summary.trim().slice(0, 800) || null
|
|
807
|
-
if (patch.summary === null) state.summary = null
|
|
808
|
-
if (typeof patch.nextAction === 'string') state.nextAction = patch.nextAction.trim().slice(0, 600) || null
|
|
809
|
-
if (patch.nextAction === null) state.nextAction = null
|
|
810
|
-
if (Array.isArray(patch.planSteps)) state.planSteps = normalizeStringList(patch.planSteps, 10, 220)
|
|
811
|
-
if (typeof patch.currentPlanStep === 'string') state.currentPlanStep = patch.currentPlanStep.trim().slice(0, 220) || null
|
|
812
|
-
if (patch.currentPlanStep === null) state.currentPlanStep = null
|
|
813
|
-
if (typeof patch.reviewNote === 'string') state.reviewNote = patch.reviewNote.trim().slice(0, 320) || null
|
|
814
|
-
if (patch.reviewNote === null) state.reviewNote = null
|
|
815
|
-
if (typeof patch.reviewConfidence === 'number' && Number.isFinite(patch.reviewConfidence)) {
|
|
816
|
-
state.reviewConfidence = Math.max(0, Math.min(1, patch.reviewConfidence))
|
|
817
|
-
}
|
|
818
|
-
if (patch.reviewConfidence === null) state.reviewConfidence = null
|
|
819
|
-
if (typeof patch.missionTaskId === 'string') state.missionTaskId = patch.missionTaskId.trim() || null
|
|
820
|
-
if (patch.missionTaskId === null) state.missionTaskId = null
|
|
821
|
-
if (typeof patch.momentumScore === 'number') state.momentumScore = clampInt(patch.momentumScore, state.momentumScore, 0, 100)
|
|
822
|
-
if (typeof patch.paused === 'boolean') state.paused = patch.paused
|
|
823
|
-
if (patch.autonomyMode === 'assist' || patch.autonomyMode === 'autonomous') state.autonomyMode = patch.autonomyMode
|
|
824
|
-
if (Array.isArray(patch.pendingEvents)) state.pendingEvents = pruneEvents(patch.pendingEvents, now)
|
|
825
|
-
if (Array.isArray(patch.timeline)) state.timeline = pruneTimeline(patch.timeline, now)
|
|
826
|
-
if (typeof patch.followupChainCount === 'number') state.followupChainCount = clampInt(patch.followupChainCount, state.followupChainCount, 0, 100)
|
|
827
|
-
if (typeof patch.metaMissCount === 'number') state.metaMissCount = clampInt(patch.metaMissCount, state.metaMissCount, 0, 100)
|
|
828
|
-
if (Array.isArray(patch.workingMemoryNotes)) state.workingMemoryNotes = normalizeStringList(patch.workingMemoryNotes, 24, 260)
|
|
829
|
-
if (typeof patch.lastMemoryNoteAt === 'number') state.lastMemoryNoteAt = patch.lastMemoryNoteAt
|
|
830
|
-
if (patch.lastMemoryNoteAt === null) state.lastMemoryNoteAt = null
|
|
831
|
-
if (typeof patch.lastPlannedAt === 'number') state.lastPlannedAt = patch.lastPlannedAt
|
|
832
|
-
if (patch.lastPlannedAt === null) state.lastPlannedAt = null
|
|
833
|
-
if (typeof patch.lastReviewedAt === 'number') state.lastReviewedAt = patch.lastReviewedAt
|
|
834
|
-
if (patch.lastReviewedAt === null) state.lastReviewedAt = null
|
|
835
|
-
|
|
836
|
-
state.momentumScore = computeMomentumScore(state)
|
|
837
|
-
state.updatedAt = now
|
|
838
|
-
session.mainLoopState = state
|
|
839
|
-
sessions[sessionId] = session
|
|
840
|
-
saveSessions(sessions)
|
|
841
|
-
return state
|
|
85
|
+
export function getMainLoopStateForSession(_sessionId: string): MainLoopState | null {
|
|
86
|
+
return null
|
|
842
87
|
}
|
|
843
88
|
|
|
844
|
-
export function
|
|
845
|
-
|
|
846
|
-
if (!text) return 0
|
|
847
|
-
|
|
848
|
-
const sessions = loadSessions()
|
|
849
|
-
const now = Date.now()
|
|
850
|
-
let changed = 0
|
|
851
|
-
|
|
852
|
-
for (const session of Object.values(sessions) as any[]) {
|
|
853
|
-
if (!isMainSession(session)) continue
|
|
854
|
-
if (input.user && session.user && session.user !== input.user) continue
|
|
855
|
-
|
|
856
|
-
const state = normalizeState(session.mainLoopState, now)
|
|
857
|
-
const appended = appendEvent(state, input.type || 'event', text, now)
|
|
858
|
-
if (!appended) continue
|
|
859
|
-
appendTimeline(state, input.type || 'event', text, now, state.status)
|
|
860
|
-
state.momentumScore = computeMomentumScore(state)
|
|
861
|
-
state.updatedAt = now
|
|
862
|
-
session.mainLoopState = state
|
|
863
|
-
changed += 1
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
if (changed > 0) {
|
|
867
|
-
saveSessions(sessions)
|
|
868
|
-
log.info('main-loop', `Queued event for ${changed} main session(s)`, {
|
|
869
|
-
type: input.type,
|
|
870
|
-
text,
|
|
871
|
-
user: input.user || null,
|
|
872
|
-
})
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
return changed
|
|
89
|
+
export function setMainLoopStateForSession(_sessionId: string, _patch: Partial<MainLoopState>): MainLoopState | null {
|
|
90
|
+
return null
|
|
876
91
|
}
|
|
877
92
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
status: z.enum(['progress', 'ok', 'idle', 'blocked']).optional(),
|
|
881
|
-
next_action: z.string().trim().optional(),
|
|
882
|
-
}).passthrough()
|
|
883
|
-
|
|
884
|
-
type AgentHeartbeatMeta = z.infer<typeof AgentHeartbeatMetaSchema>
|
|
885
|
-
|
|
886
|
-
function parseAgentHeartbeatMeta(text: string): AgentHeartbeatMeta | null {
|
|
887
|
-
const raw = (text || '').trim()
|
|
888
|
-
if (!raw) return null
|
|
889
|
-
const match = raw.match(AGENT_HEARTBEAT_META_RE)
|
|
890
|
-
if (!match?.[1]) return null
|
|
891
|
-
try {
|
|
892
|
-
const parsed = JSON.parse(match[1])
|
|
893
|
-
return AgentHeartbeatMetaSchema.parse(parsed)
|
|
894
|
-
} catch {
|
|
895
|
-
return null
|
|
896
|
-
}
|
|
93
|
+
export function pushMainLoopEventToMainSessions(_input: PushMainLoopEventInput): number {
|
|
94
|
+
return 0
|
|
897
95
|
}
|
|
898
96
|
|
|
899
|
-
function
|
|
900
|
-
if (!input.internal || input.source !== 'heartbeat') return null
|
|
901
|
-
if (!session.agentId) return null
|
|
902
|
-
const text = input.resultText || ''
|
|
903
|
-
if (!text.trim()) return null
|
|
904
|
-
|
|
905
|
-
const meta = parseAgentHeartbeatMeta(text)
|
|
906
|
-
if (!meta) return null
|
|
907
|
-
|
|
908
|
-
const agents = loadAgents()
|
|
909
|
-
const agent = agents[session.agentId]
|
|
910
|
-
if (!agent) return null
|
|
911
|
-
|
|
912
|
-
let changed = false
|
|
913
|
-
if (meta.goal && meta.goal !== agent.heartbeatGoal) {
|
|
914
|
-
agent.heartbeatGoal = meta.goal
|
|
915
|
-
changed = true
|
|
916
|
-
log.info('agent-heartbeat', `Goal updated for agent ${agent.name}: ${meta.goal.slice(0, 120)}`)
|
|
917
|
-
}
|
|
918
|
-
if (meta.next_action) {
|
|
919
|
-
agent.heartbeatNextAction = meta.next_action
|
|
920
|
-
changed = true
|
|
921
|
-
}
|
|
922
|
-
if (meta.status) {
|
|
923
|
-
agent.heartbeatStatus = meta.status
|
|
924
|
-
changed = true
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
if (changed) {
|
|
928
|
-
agents[session.agentId] = agent
|
|
929
|
-
saveAgents(agents)
|
|
930
|
-
}
|
|
97
|
+
export function handleMainLoopRunResult(_input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
|
|
931
98
|
return null
|
|
932
99
|
}
|
|
933
|
-
|
|
934
|
-
export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
|
|
935
|
-
const sessions = loadSessions()
|
|
936
|
-
const session = sessions[input.sessionId]
|
|
937
|
-
if (!session) return null
|
|
938
|
-
if (!isProtectedMainSession(session)) return handleAgentHeartbeatResult(session, input)
|
|
939
|
-
|
|
940
|
-
const now = Date.now()
|
|
941
|
-
const state = normalizeState(session.mainLoopState, now)
|
|
942
|
-
const hasMemoryTool = Array.isArray(session.tools) && session.tools.includes('memory')
|
|
943
|
-
state.pendingEvents = pruneEvents(state.pendingEvents, now)
|
|
944
|
-
let forceMemoryNote = false
|
|
945
|
-
|
|
946
|
-
const userGoal = inferGoalFromUserMessage(input.message)
|
|
947
|
-
const userGoalContract = parseGoalContractFromText(input.message)
|
|
948
|
-
if (!input.internal) {
|
|
949
|
-
if (userGoal) {
|
|
950
|
-
state.goal = userGoal
|
|
951
|
-
if (userGoalContract) state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
|
|
952
|
-
state.status = 'progress'
|
|
953
|
-
appendEvent(state, 'user_instruction', `User goal updated: ${userGoal}`, now)
|
|
954
|
-
appendTimeline(state, 'user_goal', `Goal updated: ${userGoal}`, now, state.status)
|
|
955
|
-
appendWorkingMemoryNote(state, `goal:${userGoal}`)
|
|
956
|
-
forceMemoryNote = true
|
|
957
|
-
logExecution(input.sessionId, 'mission_start', `New goal: ${toOneLine(userGoal, 200)}`, {
|
|
958
|
-
agentId: session.agentId,
|
|
959
|
-
detail: { goal: userGoal, planSteps: state.planSteps },
|
|
960
|
-
})
|
|
961
|
-
} else if (userGoalContract?.objective) {
|
|
962
|
-
state.goal = userGoalContract.objective
|
|
963
|
-
state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
|
|
964
|
-
state.status = 'progress'
|
|
965
|
-
appendTimeline(state, 'user_goal_contract', `Goal contract updated: ${userGoalContract.objective}`, now, state.status)
|
|
966
|
-
appendWorkingMemoryNote(state, `contract:${userGoalContract.objective}`)
|
|
967
|
-
forceMemoryNote = true
|
|
968
|
-
logExecution(input.sessionId, 'mission_start', `New goal contract: ${toOneLine(userGoalContract.objective, 200)}`, {
|
|
969
|
-
agentId: session.agentId,
|
|
970
|
-
detail: { goal: userGoalContract.objective, contract: userGoalContract, planSteps: state.planSteps },
|
|
971
|
-
})
|
|
972
|
-
}
|
|
973
|
-
state.followupChainCount = 0
|
|
974
|
-
state.missionTokens = 0
|
|
975
|
-
state.missionCostUsd = 0
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// Accumulate per-mission token/cost tracking
|
|
979
|
-
if (typeof input.inputTokens === 'number') {
|
|
980
|
-
state.missionTokens = (state.missionTokens || 0) + input.inputTokens + (input.outputTokens || 0)
|
|
981
|
-
}
|
|
982
|
-
if (typeof input.estimatedCost === 'number') {
|
|
983
|
-
state.missionCostUsd = (state.missionCostUsd || 0) + input.estimatedCost
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
if (state.paused && input.internal) {
|
|
987
|
-
appendTimeline(state, 'paused_skip', `Skipped internal tick from ${input.source} because mission is paused.`, now, state.status)
|
|
988
|
-
state.momentumScore = computeMomentumScore(state)
|
|
989
|
-
state.updatedAt = now
|
|
990
|
-
session.mainLoopState = state
|
|
991
|
-
sessions[input.sessionId] = session
|
|
992
|
-
saveSessions(sessions)
|
|
993
|
-
return null
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (input.error) {
|
|
997
|
-
appendEvent(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 400)}`, now)
|
|
998
|
-
appendTimeline(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 220)}`, now, 'blocked')
|
|
999
|
-
state.status = 'blocked'
|
|
1000
|
-
appendWorkingMemoryNote(state, `blocked:${toOneLine(input.error, 120)}`)
|
|
1001
|
-
forceMemoryNote = true
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
for (const event of input.toolEvents || []) {
|
|
1005
|
-
if (!event?.error) continue
|
|
1006
|
-
appendEvent(
|
|
1007
|
-
state,
|
|
1008
|
-
'tool_error',
|
|
1009
|
-
`Tool ${event.name || 'unknown'} error: ${toOneLine(event.output || event.input || 'unknown error', 400)}`,
|
|
1010
|
-
now,
|
|
1011
|
-
)
|
|
1012
|
-
appendTimeline(
|
|
1013
|
-
state,
|
|
1014
|
-
'tool_error',
|
|
1015
|
-
`Tool ${event.name || 'unknown'} error encountered.`,
|
|
1016
|
-
now,
|
|
1017
|
-
'blocked',
|
|
1018
|
-
)
|
|
1019
|
-
forceMemoryNote = true
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
let followup: MainLoopFollowupRequest | null = null
|
|
1023
|
-
const shouldAutoKickFromUserGoal = !input.internal
|
|
1024
|
-
&& !input.error
|
|
1025
|
-
&& (!!userGoal || !!userGoalContract?.objective)
|
|
1026
|
-
&& !state.paused
|
|
1027
|
-
&& state.autonomyMode === 'autonomous'
|
|
1028
|
-
|
|
1029
|
-
if (shouldAutoKickFromUserGoal) {
|
|
1030
|
-
followup = {
|
|
1031
|
-
message: buildFollowupPrompt(state, { hasMemoryTool, agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
|
|
1032
|
-
delayMs: 1500,
|
|
1033
|
-
dedupeKey: `main-loop-user-kickoff:${input.sessionId}`,
|
|
1034
|
-
}
|
|
1035
|
-
appendTimeline(state, 'followup', 'Queued autonomous kickoff follow-up from new user goal.', now, state.status)
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
if (input.internal) {
|
|
1039
|
-
state.lastTickAt = now
|
|
1040
|
-
const trimmedText = (input.resultText || '').trim()
|
|
1041
|
-
const isHeartbeatOk = /^HEARTBEAT_OK$/i.test(trimmedText)
|
|
1042
|
-
const meta = parseMainLoopMeta(trimmedText)
|
|
1043
|
-
const planMeta = parseMainLoopPlan(trimmedText)
|
|
1044
|
-
const reviewMeta = parseMainLoopReview(trimmedText)
|
|
1045
|
-
|
|
1046
|
-
if (planMeta) {
|
|
1047
|
-
if (planMeta.steps?.length) {
|
|
1048
|
-
state.planSteps = planMeta.steps
|
|
1049
|
-
state.lastPlannedAt = now
|
|
1050
|
-
appendWorkingMemoryNote(state, `plan:${planMeta.steps.join(' -> ')}`)
|
|
1051
|
-
}
|
|
1052
|
-
if (planMeta.current_step) {
|
|
1053
|
-
state.currentPlanStep = planMeta.current_step
|
|
1054
|
-
state.lastPlannedAt = now
|
|
1055
|
-
}
|
|
1056
|
-
appendTimeline(state, 'plan', `Plan updated${planMeta.current_step ? ` at step: ${planMeta.current_step}` : ''}.`, now, state.status)
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
if (reviewMeta) {
|
|
1060
|
-
if (reviewMeta.note) {
|
|
1061
|
-
state.reviewNote = reviewMeta.note
|
|
1062
|
-
appendWorkingMemoryNote(state, `review:${reviewMeta.note}`)
|
|
1063
|
-
}
|
|
1064
|
-
if (typeof reviewMeta.confidence === 'number') state.reviewConfidence = reviewMeta.confidence
|
|
1065
|
-
state.lastReviewedAt = now
|
|
1066
|
-
if (reviewMeta.needs_replan === true && state.planSteps.length > 0) {
|
|
1067
|
-
appendEvent(state, 'review_replan', 'Execution review requested replanning.', now)
|
|
1068
|
-
}
|
|
1069
|
-
appendTimeline(state, 'review', reviewMeta.note || 'Execution review updated.', now, state.status)
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
if (meta) {
|
|
1073
|
-
state.metaMissCount = 0
|
|
1074
|
-
if (meta.goal) {
|
|
1075
|
-
state.goal = meta.goal
|
|
1076
|
-
const metaGoalContract = parseGoalContractFromText(meta.goal)
|
|
1077
|
-
if (metaGoalContract) state.goalContract = mergeGoalContracts(state.goalContract, metaGoalContract)
|
|
1078
|
-
}
|
|
1079
|
-
if (meta.status) state.status = meta.status
|
|
1080
|
-
if (meta.summary) state.summary = meta.summary
|
|
1081
|
-
if (meta.next_action) state.nextAction = meta.next_action
|
|
1082
|
-
if (meta.summary) appendWorkingMemoryNote(state, `summary:${toOneLine(meta.summary, 180)}`)
|
|
1083
|
-
if (meta.next_action) appendWorkingMemoryNote(state, `next:${toOneLine(meta.next_action, 180)}`)
|
|
1084
|
-
appendTimeline(
|
|
1085
|
-
state,
|
|
1086
|
-
'meta',
|
|
1087
|
-
`Meta update: status=${meta.status || state.status}; summary=${toOneLine(meta.summary || state.summary || 'none', 140)}`,
|
|
1088
|
-
now,
|
|
1089
|
-
meta.status || state.status,
|
|
1090
|
-
)
|
|
1091
|
-
consumeEvents(state, meta.consume_event_ids)
|
|
1092
|
-
|
|
1093
|
-
// Budget enforcement: check mission cost against goalContract.budgetUsd
|
|
1094
|
-
const budgetUsd = state.goalContract?.budgetUsd
|
|
1095
|
-
if (typeof budgetUsd === 'number' && budgetUsd > 0 && typeof state.missionCostUsd === 'number') {
|
|
1096
|
-
const usageRatio = state.missionCostUsd / budgetUsd
|
|
1097
|
-
if (usageRatio >= 1.0 && !state.paused) {
|
|
1098
|
-
state.paused = true
|
|
1099
|
-
state.status = 'blocked'
|
|
1100
|
-
appendTimeline(state, 'budget_exceeded', `Mission budget exceeded ($${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}). Mission paused.`, now, 'blocked')
|
|
1101
|
-
appendEvent(state, 'budget_exceeded', `Budget limit reached: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)}`, now)
|
|
1102
|
-
logExecution(input.sessionId, 'budget_warning', `Budget exceeded: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
|
|
1103
|
-
agentId: session.agentId,
|
|
1104
|
-
detail: { missionCostUsd: state.missionCostUsd, budgetUsd, missionTokens: state.missionTokens },
|
|
1105
|
-
})
|
|
1106
|
-
} else if (usageRatio >= 0.8) {
|
|
1107
|
-
appendEvent(state, 'budget_warning', `Mission approaching budget limit: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)} (${Math.round(usageRatio * 100)}%)`, now)
|
|
1108
|
-
logExecution(input.sessionId, 'budget_warning', `Budget at ${Math.round(usageRatio * 100)}%: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
|
|
1109
|
-
agentId: session.agentId,
|
|
1110
|
-
detail: { missionCostUsd: state.missionCostUsd, budgetUsd, usagePercent: Math.round(usageRatio * 100) },
|
|
1111
|
-
})
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
if (meta.follow_up === true && !input.error && !isHeartbeatOk && !state.paused && state.followupChainCount < getMaxFollowupChain(session.agentId)) {
|
|
1116
|
-
state.followupChainCount += 1
|
|
1117
|
-
const delaySec = clampInt(meta.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900)
|
|
1118
|
-
followup = {
|
|
1119
|
-
message: buildFollowupPrompt(state, { hasMemoryTool, agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
|
|
1120
|
-
delayMs: delaySec * 1000,
|
|
1121
|
-
dedupeKey: `main-loop-followup:${input.sessionId}`,
|
|
1122
|
-
}
|
|
1123
|
-
appendTimeline(state, 'followup', `Queued chained follow-up in ${delaySec}s.`, now, state.status)
|
|
1124
|
-
} else if (meta.follow_up === false || isHeartbeatOk) {
|
|
1125
|
-
state.followupChainCount = 0
|
|
1126
|
-
}
|
|
1127
|
-
if (state.status === 'ok' || state.status === 'blocked') {
|
|
1128
|
-
forceMemoryNote = true
|
|
1129
|
-
if (state.status === 'ok') {
|
|
1130
|
-
logExecution(input.sessionId, 'mission_complete', `Mission completed: ${toOneLine(state.goal || 'unknown', 200)}`, {
|
|
1131
|
-
agentId: session.agentId,
|
|
1132
|
-
detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
|
|
1133
|
-
})
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
} else if (!isHeartbeatOk && trimmedText) {
|
|
1137
|
-
state.metaMissCount = Math.min(100, state.metaMissCount + 1)
|
|
1138
|
-
state.summary = toOneLine(trimmedText, 700)
|
|
1139
|
-
appendWorkingMemoryNote(state, `inferred:${toOneLine(trimmedText, 160)}`)
|
|
1140
|
-
if (state.status === 'idle') state.status = 'progress'
|
|
1141
|
-
appendEvent(state, 'meta_missing', 'Main-loop reply missing [MAIN_LOOP_META] contract; state inferred from text.', now)
|
|
1142
|
-
appendTimeline(state, 'meta_missing', 'Missing [MAIN_LOOP_META]; inferred state from plain text.', now, state.status)
|
|
1143
|
-
} else if (isHeartbeatOk) {
|
|
1144
|
-
state.metaMissCount = 0
|
|
1145
|
-
appendTimeline(state, 'heartbeat_ok', 'Heartbeat returned HEARTBEAT_OK.', now, state.status)
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
if (input.internal && state.status === 'ok') {
|
|
1150
|
-
const completionGateReason = getMissionCompletionGateReason(session, state, input.resultText || '')
|
|
1151
|
-
if (completionGateReason) {
|
|
1152
|
-
state.status = 'progress'
|
|
1153
|
-
if (!state.nextAction || /^no queued action/i.test(state.nextAction)) {
|
|
1154
|
-
state.nextAction = 'Wait for the next schedule run and verify a screenshot artifact link is delivered.'
|
|
1155
|
-
}
|
|
1156
|
-
appendEvent(state, 'completion_gate', completionGateReason, now)
|
|
1157
|
-
appendTimeline(state, 'completion_gate', 'Holding completion until screenshot artifact evidence is observed.', now, state.status)
|
|
1158
|
-
appendWorkingMemoryNote(state, `gate:${toOneLine(completionGateReason, 180)}`)
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
state.missionTaskId = upsertMissionTask(session, state, now)
|
|
1163
|
-
const shouldWritePeriodicMemory = !!state.summary && state.status === 'progress'
|
|
1164
|
-
maybeStoreMissionMemoryNote(session, state, now, input.source, forceMemoryNote || shouldWritePeriodicMemory)
|
|
1165
|
-
|
|
1166
|
-
state.momentumScore = computeMomentumScore(state)
|
|
1167
|
-
|
|
1168
|
-
state.updatedAt = now
|
|
1169
|
-
session.mainLoopState = state
|
|
1170
|
-
sessions[input.sessionId] = session
|
|
1171
|
-
saveSessions(sessions)
|
|
1172
|
-
|
|
1173
|
-
return followup
|
|
1174
|
-
}
|