@swarmclawai/swarmclaw 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import type { GoalContract, MessageToolEvent } from '@/types'
|
|
3
|
+
import { loadSessions, saveSessions, loadTasks, saveTasks } from './storage'
|
|
4
|
+
import { log } from './logger'
|
|
5
|
+
import { getMemoryDb } from './memory-db'
|
|
6
|
+
import {
|
|
7
|
+
mergeGoalContracts,
|
|
8
|
+
parseGoalContractFromText,
|
|
9
|
+
parseMainLoopPlan,
|
|
10
|
+
parseMainLoopReview,
|
|
11
|
+
} from './autonomy-contract'
|
|
12
|
+
|
|
13
|
+
const MAIN_SESSION_NAME = '__main__'
|
|
14
|
+
const MAX_PENDING_EVENTS = 40
|
|
15
|
+
const MAX_TIMELINE_EVENTS = 80
|
|
16
|
+
const EVENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
17
|
+
const MEMORY_NOTE_MIN_INTERVAL_MS = 90 * 60 * 1000
|
|
18
|
+
const DEFAULT_FOLLOWUP_DELAY_SEC = 45
|
|
19
|
+
const MAX_FOLLOWUP_CHAIN = 6
|
|
20
|
+
const META_LINE_RE = /\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i
|
|
21
|
+
const SCREENSHOT_GOAL_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
|
|
22
|
+
const DELIVERY_GOAL_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i
|
|
23
|
+
const SCHEDULE_GOAL_HINT = /\b(schedule|scheduled|every\s+\w+|interval|cron|recurr)\b/i
|
|
24
|
+
const UPLOAD_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i
|
|
25
|
+
const SENT_ARTIFACT_HINT = /\b(sent|shared|uploaded|returned)\b[^.]*\b(screenshot|snapshot|image|file)\b/i
|
|
26
|
+
|
|
27
|
+
interface MainLoopSessionMessageLike {
|
|
28
|
+
text?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface MainLoopSessionEvidenceLike {
|
|
32
|
+
messages?: MainLoopSessionMessageLike[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MainLoopEvent {
|
|
36
|
+
id: string
|
|
37
|
+
type: string
|
|
38
|
+
text: string
|
|
39
|
+
createdAt: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MainLoopTimelineEntry {
|
|
43
|
+
id: string
|
|
44
|
+
at: number
|
|
45
|
+
source: string
|
|
46
|
+
note: string
|
|
47
|
+
status?: 'idle' | 'progress' | 'blocked' | 'ok'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MainLoopState {
|
|
51
|
+
goal: string | null
|
|
52
|
+
goalContract: GoalContract | null
|
|
53
|
+
status: 'idle' | 'progress' | 'blocked' | 'ok'
|
|
54
|
+
summary: string | null
|
|
55
|
+
nextAction: string | null
|
|
56
|
+
planSteps: string[]
|
|
57
|
+
currentPlanStep: string | null
|
|
58
|
+
reviewNote: string | null
|
|
59
|
+
reviewConfidence: number | null
|
|
60
|
+
missionTaskId: string | null
|
|
61
|
+
momentumScore: number
|
|
62
|
+
paused: boolean
|
|
63
|
+
autonomyMode: 'assist' | 'autonomous'
|
|
64
|
+
pendingEvents: MainLoopEvent[]
|
|
65
|
+
timeline: MainLoopTimelineEntry[]
|
|
66
|
+
followupChainCount: number
|
|
67
|
+
metaMissCount: number
|
|
68
|
+
workingMemoryNotes: string[]
|
|
69
|
+
lastMemoryNoteAt: number | null
|
|
70
|
+
lastPlannedAt: number | null
|
|
71
|
+
lastReviewedAt: number | null
|
|
72
|
+
lastTickAt: number | null
|
|
73
|
+
updatedAt: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface MainLoopMeta {
|
|
77
|
+
status?: 'idle' | 'progress' | 'blocked' | 'ok'
|
|
78
|
+
summary?: string
|
|
79
|
+
next_action?: string
|
|
80
|
+
follow_up?: boolean
|
|
81
|
+
delay_sec?: number
|
|
82
|
+
goal?: string
|
|
83
|
+
consume_event_ids?: string[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface MainLoopFollowupRequest {
|
|
87
|
+
message: string
|
|
88
|
+
delayMs: number
|
|
89
|
+
dedupeKey: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface PushMainLoopEventInput {
|
|
93
|
+
type: string
|
|
94
|
+
text: string
|
|
95
|
+
user?: string | null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface HandleMainLoopRunResultInput {
|
|
99
|
+
sessionId: string
|
|
100
|
+
message: string
|
|
101
|
+
internal: boolean
|
|
102
|
+
source: string
|
|
103
|
+
resultText: string
|
|
104
|
+
error?: string
|
|
105
|
+
toolEvents?: MessageToolEvent[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toOneLine(value: string, max = 240): string {
|
|
109
|
+
return (value || '').replace(/\s+/g, ' ').trim().slice(0, max)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeMemoryText(value: string): string {
|
|
113
|
+
return (value || '').replace(/\s+/g, ' ').trim()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function clampInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
117
|
+
const parsed = typeof value === 'number'
|
|
118
|
+
? value
|
|
119
|
+
: typeof value === 'string'
|
|
120
|
+
? Number.parseInt(value, 10)
|
|
121
|
+
: Number.NaN
|
|
122
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
123
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pruneEvents(events: MainLoopEvent[], now = Date.now()): MainLoopEvent[] {
|
|
127
|
+
const minTs = now - EVENT_TTL_MS
|
|
128
|
+
const fresh = events.filter((e) => e && typeof e.createdAt === 'number' && e.createdAt >= minTs)
|
|
129
|
+
if (fresh.length <= MAX_PENDING_EVENTS) return fresh
|
|
130
|
+
return fresh.slice(fresh.length - MAX_PENDING_EVENTS)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function pruneTimeline(entries: MainLoopTimelineEntry[], now = Date.now()): MainLoopTimelineEntry[] {
|
|
134
|
+
const minTs = now - EVENT_TTL_MS
|
|
135
|
+
const fresh = entries.filter((e) => e && typeof e.at === 'number' && e.at >= minTs && typeof e.note === 'string' && e.note.trim())
|
|
136
|
+
if (fresh.length <= MAX_TIMELINE_EVENTS) return fresh
|
|
137
|
+
return fresh.slice(fresh.length - MAX_TIMELINE_EVENTS)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function appendTimeline(
|
|
141
|
+
state: MainLoopState,
|
|
142
|
+
source: string,
|
|
143
|
+
note: string,
|
|
144
|
+
now = Date.now(),
|
|
145
|
+
status?: 'idle' | 'progress' | 'blocked' | 'ok',
|
|
146
|
+
) {
|
|
147
|
+
const normalizedNote = toOneLine(note, 400)
|
|
148
|
+
if (!normalizedNote) return
|
|
149
|
+
const recent = state.timeline.at(-1)
|
|
150
|
+
if (recent && recent.source === source && recent.note === normalizedNote && now - recent.at < 45_000) return
|
|
151
|
+
state.timeline.push({
|
|
152
|
+
id: `tl_${crypto.randomBytes(4).toString('hex')}`,
|
|
153
|
+
at: now,
|
|
154
|
+
source,
|
|
155
|
+
note: normalizedNote,
|
|
156
|
+
status,
|
|
157
|
+
})
|
|
158
|
+
state.timeline = pruneTimeline(state.timeline, now)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function computeMomentumScore(state: MainLoopState): number {
|
|
162
|
+
const baseByStatus = {
|
|
163
|
+
idle: 40,
|
|
164
|
+
progress: 72,
|
|
165
|
+
blocked: 20,
|
|
166
|
+
ok: 94,
|
|
167
|
+
} as const
|
|
168
|
+
let score: number = baseByStatus[state.status]
|
|
169
|
+
score -= Math.min(20, state.metaMissCount * 3)
|
|
170
|
+
score -= Math.min(12, Math.max(0, state.pendingEvents.length - 4) * 2)
|
|
171
|
+
if (state.paused) score = Math.min(score, 35)
|
|
172
|
+
return clampInt(score, 0, 0, 100)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeStringList(input: unknown, maxItems: number, maxChars: number): string[] {
|
|
176
|
+
if (!Array.isArray(input)) return []
|
|
177
|
+
const seen = new Set<string>()
|
|
178
|
+
const out: string[] = []
|
|
179
|
+
for (const raw of input) {
|
|
180
|
+
if (typeof raw !== 'string') continue
|
|
181
|
+
const value = raw.replace(/\s+/g, ' ').trim().slice(0, maxChars)
|
|
182
|
+
if (!value) continue
|
|
183
|
+
const key = value.toLowerCase()
|
|
184
|
+
if (seen.has(key)) continue
|
|
185
|
+
seen.add(key)
|
|
186
|
+
out.push(value)
|
|
187
|
+
if (out.length >= maxItems) break
|
|
188
|
+
}
|
|
189
|
+
return out
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeGoalContract(raw: any): GoalContract | null {
|
|
193
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
194
|
+
const objective = typeof raw.objective === 'string' ? raw.objective.trim().slice(0, 300) : ''
|
|
195
|
+
if (!objective) return null
|
|
196
|
+
const constraints = normalizeStringList(raw.constraints, 10, 220)
|
|
197
|
+
const budgetUsd = typeof raw.budgetUsd === 'number'
|
|
198
|
+
? Math.max(0, Math.min(1_000_000, raw.budgetUsd))
|
|
199
|
+
: null
|
|
200
|
+
const deadlineAt = typeof raw.deadlineAt === 'number' && Number.isFinite(raw.deadlineAt)
|
|
201
|
+
? Math.trunc(raw.deadlineAt)
|
|
202
|
+
: null
|
|
203
|
+
const successMetric = typeof raw.successMetric === 'string'
|
|
204
|
+
? raw.successMetric.trim().slice(0, 220) || null
|
|
205
|
+
: null
|
|
206
|
+
return {
|
|
207
|
+
objective,
|
|
208
|
+
constraints: constraints.length ? constraints : undefined,
|
|
209
|
+
budgetUsd,
|
|
210
|
+
deadlineAt,
|
|
211
|
+
successMetric,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizeState(raw: any, now = Date.now()): MainLoopState {
|
|
216
|
+
const status = raw?.status === 'blocked' || raw?.status === 'ok' || raw?.status === 'progress' || raw?.status === 'idle'
|
|
217
|
+
? raw.status
|
|
218
|
+
: 'idle'
|
|
219
|
+
|
|
220
|
+
const pendingRaw = Array.isArray(raw?.pendingEvents) ? raw.pendingEvents : []
|
|
221
|
+
const pendingEvents = pruneEvents(
|
|
222
|
+
pendingRaw
|
|
223
|
+
.map((e: any) => {
|
|
224
|
+
const text = toOneLine(typeof e?.text === 'string' ? e.text : '')
|
|
225
|
+
if (!text) return null
|
|
226
|
+
return {
|
|
227
|
+
id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${crypto.randomBytes(3).toString('hex')}`,
|
|
228
|
+
type: typeof e?.type === 'string' && e.type.trim() ? e.type.trim() : 'event',
|
|
229
|
+
text,
|
|
230
|
+
createdAt: typeof e?.createdAt === 'number' ? e.createdAt : now,
|
|
231
|
+
} as MainLoopEvent
|
|
232
|
+
})
|
|
233
|
+
.filter(Boolean) as MainLoopEvent[],
|
|
234
|
+
now,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
const timelineRaw = Array.isArray(raw?.timeline) ? raw.timeline : []
|
|
238
|
+
const timeline = pruneTimeline(
|
|
239
|
+
timelineRaw
|
|
240
|
+
.map((entry: any) => {
|
|
241
|
+
const note = toOneLine(typeof entry?.note === 'string' ? entry.note : '', 400)
|
|
242
|
+
if (!note) return null
|
|
243
|
+
const status = entry?.status === 'blocked' || entry?.status === 'ok' || entry?.status === 'progress' || entry?.status === 'idle'
|
|
244
|
+
? entry.status
|
|
245
|
+
: undefined
|
|
246
|
+
return {
|
|
247
|
+
id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${crypto.randomBytes(3).toString('hex')}`,
|
|
248
|
+
at: typeof entry?.at === 'number' ? entry.at : now,
|
|
249
|
+
source: typeof entry?.source === 'string' && entry.source.trim() ? entry.source.trim() : 'event',
|
|
250
|
+
note,
|
|
251
|
+
status,
|
|
252
|
+
} as MainLoopTimelineEntry
|
|
253
|
+
})
|
|
254
|
+
.filter(Boolean) as MainLoopTimelineEntry[],
|
|
255
|
+
now,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const normalized: MainLoopState = {
|
|
259
|
+
goal: typeof raw?.goal === 'string' && raw.goal.trim() ? raw.goal.trim().slice(0, 600) : null,
|
|
260
|
+
goalContract: normalizeGoalContract(raw?.goalContract),
|
|
261
|
+
status,
|
|
262
|
+
summary: typeof raw?.summary === 'string' && raw.summary.trim() ? raw.summary.trim().slice(0, 800) : null,
|
|
263
|
+
nextAction: typeof raw?.nextAction === 'string' && raw.nextAction.trim() ? raw.nextAction.trim().slice(0, 600) : null,
|
|
264
|
+
planSteps: normalizeStringList(raw?.planSteps, 10, 220),
|
|
265
|
+
currentPlanStep: typeof raw?.currentPlanStep === 'string' && raw.currentPlanStep.trim()
|
|
266
|
+
? raw.currentPlanStep.trim().slice(0, 220)
|
|
267
|
+
: null,
|
|
268
|
+
reviewNote: typeof raw?.reviewNote === 'string' && raw.reviewNote.trim()
|
|
269
|
+
? raw.reviewNote.trim().slice(0, 320)
|
|
270
|
+
: null,
|
|
271
|
+
reviewConfidence: typeof raw?.reviewConfidence === 'number' && Number.isFinite(raw.reviewConfidence)
|
|
272
|
+
? Math.max(0, Math.min(1, raw.reviewConfidence))
|
|
273
|
+
: null,
|
|
274
|
+
missionTaskId: typeof raw?.missionTaskId === 'string' && raw.missionTaskId.trim() ? raw.missionTaskId.trim() : null,
|
|
275
|
+
momentumScore: clampInt(raw?.momentumScore, 40, 0, 100),
|
|
276
|
+
paused: raw?.paused === true,
|
|
277
|
+
autonomyMode: raw?.autonomyMode === 'assist' ? 'assist' : 'autonomous',
|
|
278
|
+
pendingEvents,
|
|
279
|
+
timeline,
|
|
280
|
+
followupChainCount: clampInt(raw?.followupChainCount, 0, 0, 100),
|
|
281
|
+
metaMissCount: clampInt(raw?.metaMissCount, 0, 0, 100),
|
|
282
|
+
workingMemoryNotes: normalizeStringList(raw?.workingMemoryNotes, 24, 260),
|
|
283
|
+
lastMemoryNoteAt: typeof raw?.lastMemoryNoteAt === 'number' ? raw.lastMemoryNoteAt : null,
|
|
284
|
+
lastPlannedAt: typeof raw?.lastPlannedAt === 'number' ? raw.lastPlannedAt : null,
|
|
285
|
+
lastReviewedAt: typeof raw?.lastReviewedAt === 'number' ? raw.lastReviewedAt : null,
|
|
286
|
+
lastTickAt: typeof raw?.lastTickAt === 'number' ? raw.lastTickAt : null,
|
|
287
|
+
updatedAt: typeof raw?.updatedAt === 'number' ? raw.updatedAt : now,
|
|
288
|
+
}
|
|
289
|
+
if (!normalized.goal && normalized.goalContract?.objective) {
|
|
290
|
+
normalized.goal = normalized.goalContract.objective
|
|
291
|
+
}
|
|
292
|
+
normalized.momentumScore = computeMomentumScore(normalized)
|
|
293
|
+
return normalized
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function appendEvent(state: MainLoopState, type: string, text: string, now = Date.now()): boolean {
|
|
297
|
+
const normalizedText = toOneLine(text)
|
|
298
|
+
if (!normalizedText) return false
|
|
299
|
+
const recent = state.pendingEvents.at(-1)
|
|
300
|
+
if (recent && recent.type === type && recent.text === normalizedText && now - recent.createdAt < 60_000) {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
state.pendingEvents.push({
|
|
304
|
+
id: `evt_${crypto.randomBytes(4).toString('hex')}`,
|
|
305
|
+
type,
|
|
306
|
+
text: normalizedText,
|
|
307
|
+
createdAt: now,
|
|
308
|
+
})
|
|
309
|
+
state.pendingEvents = pruneEvents(state.pendingEvents, now)
|
|
310
|
+
return true
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function appendWorkingMemoryNote(state: MainLoopState, note: string) {
|
|
314
|
+
const value = toOneLine(note, 260)
|
|
315
|
+
if (!value) return
|
|
316
|
+
const existing = state.workingMemoryNotes || []
|
|
317
|
+
if (existing.length && existing[existing.length - 1] === value) return
|
|
318
|
+
state.workingMemoryNotes = [...existing.slice(-23), value]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function inferGoalFromUserMessage(message: string): string | null {
|
|
322
|
+
const text = (message || '').trim()
|
|
323
|
+
if (!text) return null
|
|
324
|
+
if (/^SWARM_MAIN_(MISSION_TICK|AUTO_FOLLOWUP)\b/i.test(text)) return null
|
|
325
|
+
if (/^SWARM_HEARTBEAT_CHECK\b/i.test(text)) return null
|
|
326
|
+
if (/^(ok|okay|cool|thanks|thx|got it|nice|yep|yeah|nope|nah)[.! ]*$/i.test(text)) return null
|
|
327
|
+
return text.slice(0, 600)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function inferGoalFromSessionMessages(session: any): string | null {
|
|
331
|
+
const msgs = Array.isArray(session?.messages) ? session.messages : []
|
|
332
|
+
for (let i = msgs.length - 1; i >= 0; i -= 1) {
|
|
333
|
+
const msg = msgs[i]
|
|
334
|
+
if (msg?.role !== 'user') continue
|
|
335
|
+
const inferred = inferGoalFromUserMessage(typeof msg?.text === 'string' ? msg.text : '')
|
|
336
|
+
if (inferred) return inferred
|
|
337
|
+
}
|
|
338
|
+
return null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function parseMainLoopMeta(text: string): MainLoopMeta | null {
|
|
342
|
+
const raw = (text || '').trim()
|
|
343
|
+
if (!raw) return null
|
|
344
|
+
|
|
345
|
+
const markerMatch = raw.match(META_LINE_RE)
|
|
346
|
+
const parseCandidate = markerMatch?.[1]
|
|
347
|
+
if (parseCandidate) {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = JSON.parse(parseCandidate)
|
|
350
|
+
return normalizeMeta(parsed)
|
|
351
|
+
} catch {
|
|
352
|
+
// fall through
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Fallback: parse any one-line JSON that appears to be the meta payload.
|
|
357
|
+
for (const line of raw.split('\n')) {
|
|
358
|
+
const trimmed = line.trim()
|
|
359
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue
|
|
360
|
+
if (!trimmed.includes('follow_up') && !trimmed.includes('next_action') && !trimmed.includes('consume_event_ids')) continue
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(trimmed)
|
|
363
|
+
return normalizeMeta(parsed)
|
|
364
|
+
} catch {
|
|
365
|
+
// skip malformed candidate lines
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return null
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function normalizeMeta(raw: any): MainLoopMeta {
|
|
373
|
+
const status = raw?.status === 'blocked' || raw?.status === 'ok' || raw?.status === 'progress' || raw?.status === 'idle'
|
|
374
|
+
? raw.status
|
|
375
|
+
: undefined
|
|
376
|
+
|
|
377
|
+
const consumeIds = Array.isArray(raw?.consume_event_ids)
|
|
378
|
+
? raw.consume_event_ids
|
|
379
|
+
.map((v: unknown) => (typeof v === 'string' ? v.trim() : ''))
|
|
380
|
+
.filter(Boolean)
|
|
381
|
+
: undefined
|
|
382
|
+
|
|
383
|
+
const followUp = typeof raw?.follow_up === 'boolean'
|
|
384
|
+
? raw.follow_up
|
|
385
|
+
: typeof raw?.follow_up === 'string'
|
|
386
|
+
? raw.follow_up.trim().toLowerCase() === 'true'
|
|
387
|
+
: undefined
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
status,
|
|
391
|
+
summary: typeof raw?.summary === 'string' ? raw.summary.trim().slice(0, 800) : undefined,
|
|
392
|
+
next_action: typeof raw?.next_action === 'string' ? raw.next_action.trim().slice(0, 600) : undefined,
|
|
393
|
+
follow_up: followUp,
|
|
394
|
+
delay_sec: clampInt(raw?.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900),
|
|
395
|
+
goal: typeof raw?.goal === 'string' ? raw.goal.trim().slice(0, 600) : undefined,
|
|
396
|
+
consume_event_ids: consumeIds,
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function consumeEvents(state: MainLoopState, ids: string[] | undefined) {
|
|
401
|
+
if (!ids?.length) return
|
|
402
|
+
const remove = new Set(ids)
|
|
403
|
+
state.pendingEvents = state.pendingEvents.filter((event) => !remove.has(event.id))
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildPendingEventLines(state: MainLoopState): string {
|
|
407
|
+
if (!state.pendingEvents.length) return 'Pending events:\n- none'
|
|
408
|
+
const lines = state.pendingEvents
|
|
409
|
+
.slice(-10)
|
|
410
|
+
.map((event) => `- ${event.id} | ${event.type} | ${event.text}`)
|
|
411
|
+
.join('\n')
|
|
412
|
+
return `Pending events (oldest → newest):\n${lines}`
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function buildTimelineLines(state: MainLoopState): string {
|
|
416
|
+
if (!state.timeline.length) return 'Recent mission timeline:\n- none'
|
|
417
|
+
const lines = state.timeline
|
|
418
|
+
.slice(-5)
|
|
419
|
+
.map((entry) => {
|
|
420
|
+
const ts = new Date(entry.at).toISOString().slice(11, 19)
|
|
421
|
+
const status = entry.status ? ` [${entry.status}]` : ''
|
|
422
|
+
return `- ${ts} ${entry.source}${status}: ${entry.note}`
|
|
423
|
+
})
|
|
424
|
+
.join('\n')
|
|
425
|
+
return `Recent mission timeline:\n${lines}`
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function buildGoalContractLines(state: MainLoopState): string[] {
|
|
429
|
+
const contract = state.goalContract
|
|
430
|
+
if (!contract?.objective) return []
|
|
431
|
+
const lines = [
|
|
432
|
+
`contract_objective: ${contract.objective}`,
|
|
433
|
+
]
|
|
434
|
+
if (contract.constraints?.length) lines.push(`contract_constraints: ${contract.constraints.join(' | ')}`)
|
|
435
|
+
if (typeof contract.budgetUsd === 'number') lines.push(`contract_budget_usd: ${contract.budgetUsd}`)
|
|
436
|
+
if (typeof contract.deadlineAt === 'number') lines.push(`contract_deadline_iso: ${new Date(contract.deadlineAt).toISOString()}`)
|
|
437
|
+
if (contract.successMetric) lines.push(`contract_success_metric: ${contract.successMetric}`)
|
|
438
|
+
return lines
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function missionNeedsScreenshotArtifactEvidence(state: MainLoopState): boolean {
|
|
442
|
+
const haystack = [
|
|
443
|
+
state.goal || '',
|
|
444
|
+
state.goalContract?.objective || '',
|
|
445
|
+
state.goalContract?.successMetric || '',
|
|
446
|
+
state.nextAction || '',
|
|
447
|
+
...(state.planSteps || []),
|
|
448
|
+
state.currentPlanStep || '',
|
|
449
|
+
].join(' ')
|
|
450
|
+
if (!SCREENSHOT_GOAL_HINT.test(haystack)) return false
|
|
451
|
+
return DELIVERY_GOAL_HINT.test(haystack) || SCHEDULE_GOAL_HINT.test(haystack)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function missionHasScreenshotArtifactEvidence(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): boolean {
|
|
455
|
+
const candidates: string[] = [
|
|
456
|
+
state.summary || '',
|
|
457
|
+
additionalText || '',
|
|
458
|
+
]
|
|
459
|
+
if (Array.isArray(session?.messages)) {
|
|
460
|
+
for (let i = session.messages.length - 1; i >= 0 && candidates.length < 16; i--) {
|
|
461
|
+
const text = typeof session.messages[i]?.text === 'string' ? session.messages[i].text! : ''
|
|
462
|
+
if (text && text.trim()) candidates.push(text)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return candidates.some((value) => UPLOAD_ARTIFACT_HINT.test(value) || SENT_ARTIFACT_HINT.test(value))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function getMissionCompletionGateReason(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): string | null {
|
|
469
|
+
if (!missionNeedsScreenshotArtifactEvidence(state)) return null
|
|
470
|
+
if (missionHasScreenshotArtifactEvidence(session, state, additionalText)) return null
|
|
471
|
+
return 'Mission requires screenshot artifact evidence (upload link or explicit sent screenshot confirmation) before completion.'
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function upsertMissionTask(session: any, state: MainLoopState, now: number): string | null {
|
|
475
|
+
if (!state.goal) return state.missionTaskId || null
|
|
476
|
+
|
|
477
|
+
const tasks = loadTasks()
|
|
478
|
+
let task = state.missionTaskId ? tasks[state.missionTaskId] : null
|
|
479
|
+
if (!task) {
|
|
480
|
+
task = Object.values(tasks).find((t: any) =>
|
|
481
|
+
t?.sessionId === session.id
|
|
482
|
+
&& t?.title?.startsWith('Mission:')
|
|
483
|
+
&& t?.status !== 'archived'
|
|
484
|
+
) as any || null
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const title = `Mission: ${state.goal.slice(0, 140)}`
|
|
488
|
+
const statusMap = {
|
|
489
|
+
idle: 'backlog',
|
|
490
|
+
progress: 'running',
|
|
491
|
+
blocked: 'failed',
|
|
492
|
+
ok: 'completed',
|
|
493
|
+
} as const
|
|
494
|
+
let mappedStatus = statusMap[state.status]
|
|
495
|
+
const completionGateReason = mappedStatus === 'completed'
|
|
496
|
+
? getMissionCompletionGateReason(session, state)
|
|
497
|
+
: null
|
|
498
|
+
if (completionGateReason) mappedStatus = 'running'
|
|
499
|
+
|
|
500
|
+
let changed = false
|
|
501
|
+
const contractLines = buildGoalContractLines(state)
|
|
502
|
+
const planLines = state.planSteps.length
|
|
503
|
+
? [`plan_steps: ${state.planSteps.join(' -> ')}`]
|
|
504
|
+
: []
|
|
505
|
+
if (state.currentPlanStep) planLines.push(`current_plan_step: ${state.currentPlanStep}`)
|
|
506
|
+
if (state.reviewNote) planLines.push(`latest_review: ${state.reviewNote}`)
|
|
507
|
+
|
|
508
|
+
const baseDescription = [
|
|
509
|
+
'Autonomous mission goal tracked from main loop.',
|
|
510
|
+
`Goal: ${state.goal}`,
|
|
511
|
+
state.nextAction ? `Next action: ${state.nextAction}` : '',
|
|
512
|
+
completionGateReason ? `Completion gate: ${completionGateReason}` : '',
|
|
513
|
+
...contractLines,
|
|
514
|
+
...planLines,
|
|
515
|
+
].filter(Boolean).join('\n')
|
|
516
|
+
|
|
517
|
+
if (!task) {
|
|
518
|
+
const id = crypto.randomBytes(4).toString('hex')
|
|
519
|
+
task = {
|
|
520
|
+
id,
|
|
521
|
+
title,
|
|
522
|
+
description: baseDescription,
|
|
523
|
+
status: mappedStatus,
|
|
524
|
+
agentId: session.agentId || 'default',
|
|
525
|
+
sessionId: session.id,
|
|
526
|
+
result: state.summary || null,
|
|
527
|
+
error: state.status === 'blocked' ? (state.summary || 'Blocked') : null,
|
|
528
|
+
createdAt: now,
|
|
529
|
+
updatedAt: now,
|
|
530
|
+
startedAt: mappedStatus === 'running' ? now : null,
|
|
531
|
+
completedAt: mappedStatus === 'completed' ? now : null,
|
|
532
|
+
queuedAt: null,
|
|
533
|
+
archivedAt: null,
|
|
534
|
+
comments: [],
|
|
535
|
+
images: [],
|
|
536
|
+
validation: null,
|
|
537
|
+
}
|
|
538
|
+
tasks[id] = task
|
|
539
|
+
changed = true
|
|
540
|
+
} else {
|
|
541
|
+
if (task.title !== title) {
|
|
542
|
+
task.title = title
|
|
543
|
+
changed = true
|
|
544
|
+
}
|
|
545
|
+
const nextDescription = baseDescription
|
|
546
|
+
if (task.description !== nextDescription) {
|
|
547
|
+
task.description = nextDescription
|
|
548
|
+
changed = true
|
|
549
|
+
}
|
|
550
|
+
if (task.status !== mappedStatus) {
|
|
551
|
+
task.status = mappedStatus
|
|
552
|
+
changed = true
|
|
553
|
+
if (mappedStatus === 'running' && !task.startedAt) task.startedAt = now
|
|
554
|
+
if (mappedStatus === 'completed') task.completedAt = now
|
|
555
|
+
}
|
|
556
|
+
const nextResult = state.summary || task.result || null
|
|
557
|
+
if (task.result !== nextResult) {
|
|
558
|
+
task.result = nextResult
|
|
559
|
+
changed = true
|
|
560
|
+
}
|
|
561
|
+
const nextError = mappedStatus === 'failed'
|
|
562
|
+
? (state.summary || state.nextAction || 'Blocked')
|
|
563
|
+
: null
|
|
564
|
+
if (task.error !== nextError) {
|
|
565
|
+
task.error = nextError
|
|
566
|
+
changed = true
|
|
567
|
+
}
|
|
568
|
+
if (changed) task.updatedAt = now
|
|
569
|
+
tasks[task.id] = task
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (changed) {
|
|
573
|
+
saveTasks(tasks)
|
|
574
|
+
}
|
|
575
|
+
return task?.id || null
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function maybeStoreMissionMemoryNote(
|
|
579
|
+
session: any,
|
|
580
|
+
state: MainLoopState,
|
|
581
|
+
now: number,
|
|
582
|
+
source: string,
|
|
583
|
+
force = false,
|
|
584
|
+
) {
|
|
585
|
+
if (!Array.isArray(session?.tools) || !session.tools.includes('memory')) return
|
|
586
|
+
if (!state.goal) return
|
|
587
|
+
if (!force && state.lastMemoryNoteAt && (now - state.lastMemoryNoteAt) < MEMORY_NOTE_MIN_INTERVAL_MS) return
|
|
588
|
+
|
|
589
|
+
const summary = state.summary || 'No summary'
|
|
590
|
+
const next = state.nextAction || 'No next action'
|
|
591
|
+
const title = `Mission ${state.status}: ${state.goal.slice(0, 72)}`
|
|
592
|
+
const content = [
|
|
593
|
+
`source: ${source}`,
|
|
594
|
+
`status: ${state.status}`,
|
|
595
|
+
`momentum: ${state.momentumScore}/100`,
|
|
596
|
+
`goal: ${state.goal}`,
|
|
597
|
+
...buildGoalContractLines(state),
|
|
598
|
+
state.planSteps.length ? `plan_steps: ${state.planSteps.join(' -> ')}` : '',
|
|
599
|
+
state.currentPlanStep ? `current_plan_step: ${state.currentPlanStep}` : '',
|
|
600
|
+
`summary: ${summary}`,
|
|
601
|
+
`next_action: ${next}`,
|
|
602
|
+
state.reviewNote ? `review: ${state.reviewNote}` : '',
|
|
603
|
+
typeof state.reviewConfidence === 'number' ? `review_confidence: ${state.reviewConfidence}` : '',
|
|
604
|
+
state.missionTaskId ? `mission_task_id: ${state.missionTaskId}` : '',
|
|
605
|
+
].filter(Boolean).join('\n')
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
const memDb = getMemoryDb()
|
|
609
|
+
const latest = memDb.getLatestBySessionCategory?.(session.id, 'mission')
|
|
610
|
+
if (latest) {
|
|
611
|
+
const sameTitle = normalizeMemoryText(latest.title) === normalizeMemoryText(title)
|
|
612
|
+
const sameContent = normalizeMemoryText(latest.content) === normalizeMemoryText(content)
|
|
613
|
+
if (sameTitle && sameContent) {
|
|
614
|
+
state.lastMemoryNoteAt = now
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
memDb.add({
|
|
619
|
+
agentId: session.agentId || null,
|
|
620
|
+
sessionId: session.id,
|
|
621
|
+
category: 'mission',
|
|
622
|
+
title,
|
|
623
|
+
content,
|
|
624
|
+
} as any)
|
|
625
|
+
state.lastMemoryNoteAt = now
|
|
626
|
+
} catch (err: any) {
|
|
627
|
+
appendEvent(state, 'memory_note_error', `Failed to store mission memory note: ${toOneLine(err?.message || String(err), 240)}`, now)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: boolean }): string {
|
|
632
|
+
const hasMemoryTool = opts?.hasMemoryTool === true
|
|
633
|
+
const goal = state.goal || 'No explicit goal yet. Continue with the strongest actionable objective from recent context.'
|
|
634
|
+
const nextAction = state.nextAction || 'Determine the next highest-impact action and execute it.'
|
|
635
|
+
const contractLines = buildGoalContractLines(state)
|
|
636
|
+
return [
|
|
637
|
+
'SWARM_MAIN_AUTO_FOLLOWUP',
|
|
638
|
+
`Mission goal: ${goal}`,
|
|
639
|
+
`Next action to execute now: ${nextAction}`,
|
|
640
|
+
`Current status: ${state.status}`,
|
|
641
|
+
`Mission task id: ${state.missionTaskId || 'none'}`,
|
|
642
|
+
`Momentum score: ${state.momentumScore}/100`,
|
|
643
|
+
...contractLines,
|
|
644
|
+
state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
|
|
645
|
+
state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
|
|
646
|
+
state.reviewNote ? `Last review: ${state.reviewNote}` : '',
|
|
647
|
+
buildPendingEventLines(state),
|
|
648
|
+
buildTimelineLines(state),
|
|
649
|
+
'Act autonomously. Use available tools to execute work, verify results, and keep momentum.',
|
|
650
|
+
state.autonomyMode === 'assist'
|
|
651
|
+
? 'Assist mode: execute safe internal analysis by default, and ask before irreversible external side effects (sending messages, purchases, account mutations).'
|
|
652
|
+
: 'Autonomous mode: execute safe next actions without waiting for confirmation; ask only when blocked by permissions, credentials, or policy.',
|
|
653
|
+
'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
|
|
654
|
+
hasMemoryTool
|
|
655
|
+
? 'Use memory_tool actively: recall relevant prior notes before acting, and store a concise note after each meaningful step.'
|
|
656
|
+
: 'memory_tool is unavailable in this session. Keep concise progress summaries in your status/meta output.',
|
|
657
|
+
'If you are blocked by missing credentials, permissions, or policy limits, say exactly what is blocked and the smallest unblock needed.',
|
|
658
|
+
'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).',
|
|
659
|
+
'If no meaningful action remains right now, reply exactly HEARTBEAT_OK.',
|
|
660
|
+
'Otherwise include a concise human update, then append exactly one [MAIN_LOOP_META] JSON line.',
|
|
661
|
+
'Optionally append one [MAIN_LOOP_PLAN] JSON line when you create/revise a plan.',
|
|
662
|
+
'Optionally append one [MAIN_LOOP_REVIEW] JSON line when you review recent execution results.',
|
|
663
|
+
'[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
|
|
664
|
+
'[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
|
|
665
|
+
'[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
|
|
666
|
+
].join('\n')
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export function isMainSession(session: any): boolean {
|
|
670
|
+
return session?.name === MAIN_SESSION_NAME
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
|
|
674
|
+
const now = Date.now()
|
|
675
|
+
const state = normalizeState(session?.mainLoopState, now)
|
|
676
|
+
const goal = state.goal || inferGoalFromSessionMessages(session) || null
|
|
677
|
+
const hasMemoryTool = Array.isArray(session?.tools) && session.tools.includes('memory')
|
|
678
|
+
|
|
679
|
+
const promptGoal = goal || 'No explicit mission captured yet. Infer the mission from recent user instructions and continue proactively.'
|
|
680
|
+
const promptSummary = state.summary || 'No prior mission summary yet.'
|
|
681
|
+
const promptNextAction = state.nextAction || 'No queued action. Determine one.'
|
|
682
|
+
const contractLines = buildGoalContractLines(state)
|
|
683
|
+
|
|
684
|
+
return [
|
|
685
|
+
'SWARM_MAIN_MISSION_TICK',
|
|
686
|
+
`Time: ${new Date(now).toISOString()}`,
|
|
687
|
+
`Mission goal: ${promptGoal}`,
|
|
688
|
+
`Current status: ${state.status}`,
|
|
689
|
+
`Mission paused: ${state.paused ? 'yes' : 'no'}`,
|
|
690
|
+
`Autonomy mode: ${state.autonomyMode}`,
|
|
691
|
+
`Mission task id: ${state.missionTaskId || 'none'}`,
|
|
692
|
+
`Momentum score: ${state.momentumScore}/100`,
|
|
693
|
+
...contractLines,
|
|
694
|
+
state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
|
|
695
|
+
state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
|
|
696
|
+
state.reviewNote ? `Last review: ${state.reviewNote}` : '',
|
|
697
|
+
`Last summary: ${toOneLine(promptSummary, 500)}`,
|
|
698
|
+
`Last next action: ${toOneLine(promptNextAction, 500)}`,
|
|
699
|
+
buildPendingEventLines(state),
|
|
700
|
+
buildTimelineLines(state),
|
|
701
|
+
'You are running the main autonomous mission loop. Continue executing toward the goal with initiative.',
|
|
702
|
+
state.autonomyMode === 'assist'
|
|
703
|
+
? 'Assist mode is active: execute safe internal work and ask before irreversible external side effects.'
|
|
704
|
+
: 'Autonomous mode is active: execute safe next actions without waiting for confirmation; only ask when blocked.',
|
|
705
|
+
'Use tools where needed, verify outcomes, and avoid vague status-only replies.',
|
|
706
|
+
'Do not ask broad exploratory questions when a safe next action exists. Pick a reasonable assumption, execute, and adapt from evidence.',
|
|
707
|
+
'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
|
|
708
|
+
hasMemoryTool
|
|
709
|
+
? 'Use memory_tool actively: recall relevant prior notes before acting, and store concise notes about progress, constraints, and next step after each meaningful action.'
|
|
710
|
+
: 'If memory_tool is unavailable, keep concise state in summary/next_action and continue execution.',
|
|
711
|
+
'Use a planner-executor-review loop: keep a concrete step plan, execute one meaningful step, then self-review and either continue or re-plan.',
|
|
712
|
+
'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).',
|
|
713
|
+
'If nothing important changed and no action is needed now, reply exactly HEARTBEAT_OK.',
|
|
714
|
+
'Otherwise: provide a concise human-readable update, then append exactly one [MAIN_LOOP_META] JSON line.',
|
|
715
|
+
'Optionally append one [MAIN_LOOP_PLAN] JSON line when creating/updating plan steps.',
|
|
716
|
+
'Optionally append one [MAIN_LOOP_REVIEW] JSON line after execution review.',
|
|
717
|
+
'[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
|
|
718
|
+
'[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
|
|
719
|
+
'[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
|
|
720
|
+
'The [MAIN_LOOP_META] JSON must be valid, on one line, and only appear once.',
|
|
721
|
+
`Fallback prompt context: ${fallbackPrompt || 'SWARM_HEARTBEAT_CHECK'}`,
|
|
722
|
+
].join('\n')
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export function stripMainLoopMetaForPersistence(text: string, internal: boolean): string {
|
|
726
|
+
if (!internal) return text
|
|
727
|
+
if (!text) return ''
|
|
728
|
+
return text
|
|
729
|
+
.split('\n')
|
|
730
|
+
.filter((line) => !line.includes('[MAIN_LOOP_META]') && !line.includes('[MAIN_LOOP_PLAN]') && !line.includes('[MAIN_LOOP_REVIEW]'))
|
|
731
|
+
.join('\n')
|
|
732
|
+
.trim()
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export function getMainLoopStateForSession(sessionId: string): MainLoopState | null {
|
|
736
|
+
const sessions = loadSessions()
|
|
737
|
+
const session = sessions[sessionId]
|
|
738
|
+
if (!session || !isMainSession(session)) return null
|
|
739
|
+
return normalizeState(session.mainLoopState)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export function setMainLoopStateForSession(sessionId: string, patch: Partial<MainLoopState>): MainLoopState | null {
|
|
743
|
+
const sessions = loadSessions()
|
|
744
|
+
const session = sessions[sessionId]
|
|
745
|
+
if (!session || !isMainSession(session)) return null
|
|
746
|
+
const now = Date.now()
|
|
747
|
+
const state = normalizeState(session.mainLoopState, now)
|
|
748
|
+
|
|
749
|
+
if (typeof patch.goal === 'string') state.goal = patch.goal.trim().slice(0, 600) || null
|
|
750
|
+
if (patch.goal === null) state.goal = null
|
|
751
|
+
if (patch.goalContract !== undefined) state.goalContract = normalizeGoalContract(patch.goalContract)
|
|
752
|
+
if (patch.status === 'idle' || patch.status === 'progress' || patch.status === 'blocked' || patch.status === 'ok') state.status = patch.status
|
|
753
|
+
if (typeof patch.summary === 'string') state.summary = patch.summary.trim().slice(0, 800) || null
|
|
754
|
+
if (patch.summary === null) state.summary = null
|
|
755
|
+
if (typeof patch.nextAction === 'string') state.nextAction = patch.nextAction.trim().slice(0, 600) || null
|
|
756
|
+
if (patch.nextAction === null) state.nextAction = null
|
|
757
|
+
if (Array.isArray(patch.planSteps)) state.planSteps = normalizeStringList(patch.planSteps, 10, 220)
|
|
758
|
+
if (typeof patch.currentPlanStep === 'string') state.currentPlanStep = patch.currentPlanStep.trim().slice(0, 220) || null
|
|
759
|
+
if (patch.currentPlanStep === null) state.currentPlanStep = null
|
|
760
|
+
if (typeof patch.reviewNote === 'string') state.reviewNote = patch.reviewNote.trim().slice(0, 320) || null
|
|
761
|
+
if (patch.reviewNote === null) state.reviewNote = null
|
|
762
|
+
if (typeof patch.reviewConfidence === 'number' && Number.isFinite(patch.reviewConfidence)) {
|
|
763
|
+
state.reviewConfidence = Math.max(0, Math.min(1, patch.reviewConfidence))
|
|
764
|
+
}
|
|
765
|
+
if (patch.reviewConfidence === null) state.reviewConfidence = null
|
|
766
|
+
if (typeof patch.missionTaskId === 'string') state.missionTaskId = patch.missionTaskId.trim() || null
|
|
767
|
+
if (patch.missionTaskId === null) state.missionTaskId = null
|
|
768
|
+
if (typeof patch.momentumScore === 'number') state.momentumScore = clampInt(patch.momentumScore, state.momentumScore, 0, 100)
|
|
769
|
+
if (typeof patch.paused === 'boolean') state.paused = patch.paused
|
|
770
|
+
if (patch.autonomyMode === 'assist' || patch.autonomyMode === 'autonomous') state.autonomyMode = patch.autonomyMode
|
|
771
|
+
if (Array.isArray(patch.pendingEvents)) state.pendingEvents = pruneEvents(patch.pendingEvents, now)
|
|
772
|
+
if (Array.isArray(patch.timeline)) state.timeline = pruneTimeline(patch.timeline, now)
|
|
773
|
+
if (typeof patch.followupChainCount === 'number') state.followupChainCount = clampInt(patch.followupChainCount, state.followupChainCount, 0, 100)
|
|
774
|
+
if (typeof patch.metaMissCount === 'number') state.metaMissCount = clampInt(patch.metaMissCount, state.metaMissCount, 0, 100)
|
|
775
|
+
if (Array.isArray(patch.workingMemoryNotes)) state.workingMemoryNotes = normalizeStringList(patch.workingMemoryNotes, 24, 260)
|
|
776
|
+
if (typeof patch.lastMemoryNoteAt === 'number') state.lastMemoryNoteAt = patch.lastMemoryNoteAt
|
|
777
|
+
if (patch.lastMemoryNoteAt === null) state.lastMemoryNoteAt = null
|
|
778
|
+
if (typeof patch.lastPlannedAt === 'number') state.lastPlannedAt = patch.lastPlannedAt
|
|
779
|
+
if (patch.lastPlannedAt === null) state.lastPlannedAt = null
|
|
780
|
+
if (typeof patch.lastReviewedAt === 'number') state.lastReviewedAt = patch.lastReviewedAt
|
|
781
|
+
if (patch.lastReviewedAt === null) state.lastReviewedAt = null
|
|
782
|
+
|
|
783
|
+
state.momentumScore = computeMomentumScore(state)
|
|
784
|
+
state.updatedAt = now
|
|
785
|
+
session.mainLoopState = state
|
|
786
|
+
sessions[sessionId] = session
|
|
787
|
+
saveSessions(sessions)
|
|
788
|
+
return state
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function pushMainLoopEventToMainSessions(input: PushMainLoopEventInput): number {
|
|
792
|
+
const text = toOneLine(input.text)
|
|
793
|
+
if (!text) return 0
|
|
794
|
+
|
|
795
|
+
const sessions = loadSessions()
|
|
796
|
+
const now = Date.now()
|
|
797
|
+
let changed = 0
|
|
798
|
+
|
|
799
|
+
for (const session of Object.values(sessions) as any[]) {
|
|
800
|
+
if (!isMainSession(session)) continue
|
|
801
|
+
if (input.user && session.user && session.user !== input.user) continue
|
|
802
|
+
|
|
803
|
+
const state = normalizeState(session.mainLoopState, now)
|
|
804
|
+
const appended = appendEvent(state, input.type || 'event', text, now)
|
|
805
|
+
if (!appended) continue
|
|
806
|
+
appendTimeline(state, input.type || 'event', text, now, state.status)
|
|
807
|
+
state.momentumScore = computeMomentumScore(state)
|
|
808
|
+
state.updatedAt = now
|
|
809
|
+
session.mainLoopState = state
|
|
810
|
+
changed += 1
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (changed > 0) {
|
|
814
|
+
saveSessions(sessions)
|
|
815
|
+
log.info('main-loop', `Queued event for ${changed} main session(s)`, {
|
|
816
|
+
type: input.type,
|
|
817
|
+
text,
|
|
818
|
+
user: input.user || null,
|
|
819
|
+
})
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return changed
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
|
|
826
|
+
const sessions = loadSessions()
|
|
827
|
+
const session = sessions[input.sessionId]
|
|
828
|
+
if (!session || !isMainSession(session)) return null
|
|
829
|
+
|
|
830
|
+
const now = Date.now()
|
|
831
|
+
const state = normalizeState(session.mainLoopState, now)
|
|
832
|
+
const hasMemoryTool = Array.isArray(session.tools) && session.tools.includes('memory')
|
|
833
|
+
state.pendingEvents = pruneEvents(state.pendingEvents, now)
|
|
834
|
+
let forceMemoryNote = false
|
|
835
|
+
|
|
836
|
+
const userGoal = inferGoalFromUserMessage(input.message)
|
|
837
|
+
const userGoalContract = parseGoalContractFromText(input.message)
|
|
838
|
+
if (!input.internal) {
|
|
839
|
+
if (userGoal) {
|
|
840
|
+
state.goal = userGoal
|
|
841
|
+
if (userGoalContract) state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
|
|
842
|
+
state.status = 'progress'
|
|
843
|
+
appendEvent(state, 'user_instruction', `User goal updated: ${userGoal}`, now)
|
|
844
|
+
appendTimeline(state, 'user_goal', `Goal updated: ${userGoal}`, now, state.status)
|
|
845
|
+
appendWorkingMemoryNote(state, `goal:${userGoal}`)
|
|
846
|
+
forceMemoryNote = true
|
|
847
|
+
} else if (userGoalContract?.objective) {
|
|
848
|
+
state.goal = userGoalContract.objective
|
|
849
|
+
state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
|
|
850
|
+
state.status = 'progress'
|
|
851
|
+
appendTimeline(state, 'user_goal_contract', `Goal contract updated: ${userGoalContract.objective}`, now, state.status)
|
|
852
|
+
appendWorkingMemoryNote(state, `contract:${userGoalContract.objective}`)
|
|
853
|
+
forceMemoryNote = true
|
|
854
|
+
}
|
|
855
|
+
state.followupChainCount = 0
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (state.paused && input.internal) {
|
|
859
|
+
appendTimeline(state, 'paused_skip', `Skipped internal tick from ${input.source} because mission is paused.`, now, state.status)
|
|
860
|
+
state.momentumScore = computeMomentumScore(state)
|
|
861
|
+
state.updatedAt = now
|
|
862
|
+
session.mainLoopState = state
|
|
863
|
+
sessions[input.sessionId] = session
|
|
864
|
+
saveSessions(sessions)
|
|
865
|
+
return null
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (input.error) {
|
|
869
|
+
appendEvent(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 400)}`, now)
|
|
870
|
+
appendTimeline(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 220)}`, now, 'blocked')
|
|
871
|
+
state.status = 'blocked'
|
|
872
|
+
appendWorkingMemoryNote(state, `blocked:${toOneLine(input.error, 120)}`)
|
|
873
|
+
forceMemoryNote = true
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
for (const event of input.toolEvents || []) {
|
|
877
|
+
if (!event?.error) continue
|
|
878
|
+
appendEvent(
|
|
879
|
+
state,
|
|
880
|
+
'tool_error',
|
|
881
|
+
`Tool ${event.name || 'unknown'} error: ${toOneLine(event.output || event.input || 'unknown error', 400)}`,
|
|
882
|
+
now,
|
|
883
|
+
)
|
|
884
|
+
appendTimeline(
|
|
885
|
+
state,
|
|
886
|
+
'tool_error',
|
|
887
|
+
`Tool ${event.name || 'unknown'} error encountered.`,
|
|
888
|
+
now,
|
|
889
|
+
'blocked',
|
|
890
|
+
)
|
|
891
|
+
forceMemoryNote = true
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
let followup: MainLoopFollowupRequest | null = null
|
|
895
|
+
const shouldAutoKickFromUserGoal = !input.internal
|
|
896
|
+
&& !input.error
|
|
897
|
+
&& (!!userGoal || !!userGoalContract?.objective)
|
|
898
|
+
&& !state.paused
|
|
899
|
+
&& state.autonomyMode === 'autonomous'
|
|
900
|
+
|
|
901
|
+
if (shouldAutoKickFromUserGoal) {
|
|
902
|
+
followup = {
|
|
903
|
+
message: buildFollowupPrompt(state, { hasMemoryTool }),
|
|
904
|
+
delayMs: 1500,
|
|
905
|
+
dedupeKey: `main-loop-user-kickoff:${input.sessionId}`,
|
|
906
|
+
}
|
|
907
|
+
appendTimeline(state, 'followup', 'Queued autonomous kickoff follow-up from new user goal.', now, state.status)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (input.internal) {
|
|
911
|
+
state.lastTickAt = now
|
|
912
|
+
const trimmedText = (input.resultText || '').trim()
|
|
913
|
+
const isHeartbeatOk = /^HEARTBEAT_OK$/i.test(trimmedText)
|
|
914
|
+
const meta = parseMainLoopMeta(trimmedText)
|
|
915
|
+
const planMeta = parseMainLoopPlan(trimmedText)
|
|
916
|
+
const reviewMeta = parseMainLoopReview(trimmedText)
|
|
917
|
+
|
|
918
|
+
if (planMeta) {
|
|
919
|
+
if (planMeta.steps?.length) {
|
|
920
|
+
state.planSteps = planMeta.steps
|
|
921
|
+
state.lastPlannedAt = now
|
|
922
|
+
appendWorkingMemoryNote(state, `plan:${planMeta.steps.join(' -> ')}`)
|
|
923
|
+
}
|
|
924
|
+
if (planMeta.current_step) {
|
|
925
|
+
state.currentPlanStep = planMeta.current_step
|
|
926
|
+
state.lastPlannedAt = now
|
|
927
|
+
}
|
|
928
|
+
appendTimeline(state, 'plan', `Plan updated${planMeta.current_step ? ` at step: ${planMeta.current_step}` : ''}.`, now, state.status)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (reviewMeta) {
|
|
932
|
+
if (reviewMeta.note) {
|
|
933
|
+
state.reviewNote = reviewMeta.note
|
|
934
|
+
appendWorkingMemoryNote(state, `review:${reviewMeta.note}`)
|
|
935
|
+
}
|
|
936
|
+
if (typeof reviewMeta.confidence === 'number') state.reviewConfidence = reviewMeta.confidence
|
|
937
|
+
state.lastReviewedAt = now
|
|
938
|
+
if (reviewMeta.needs_replan === true && state.planSteps.length > 0) {
|
|
939
|
+
appendEvent(state, 'review_replan', 'Execution review requested replanning.', now)
|
|
940
|
+
}
|
|
941
|
+
appendTimeline(state, 'review', reviewMeta.note || 'Execution review updated.', now, state.status)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (meta) {
|
|
945
|
+
state.metaMissCount = 0
|
|
946
|
+
if (meta.goal) {
|
|
947
|
+
state.goal = meta.goal
|
|
948
|
+
const metaGoalContract = parseGoalContractFromText(meta.goal)
|
|
949
|
+
if (metaGoalContract) state.goalContract = mergeGoalContracts(state.goalContract, metaGoalContract)
|
|
950
|
+
}
|
|
951
|
+
if (meta.status) state.status = meta.status
|
|
952
|
+
if (meta.summary) state.summary = meta.summary
|
|
953
|
+
if (meta.next_action) state.nextAction = meta.next_action
|
|
954
|
+
if (meta.summary) appendWorkingMemoryNote(state, `summary:${toOneLine(meta.summary, 180)}`)
|
|
955
|
+
if (meta.next_action) appendWorkingMemoryNote(state, `next:${toOneLine(meta.next_action, 180)}`)
|
|
956
|
+
appendTimeline(
|
|
957
|
+
state,
|
|
958
|
+
'meta',
|
|
959
|
+
`Meta update: status=${meta.status || state.status}; summary=${toOneLine(meta.summary || state.summary || 'none', 140)}`,
|
|
960
|
+
now,
|
|
961
|
+
meta.status || state.status,
|
|
962
|
+
)
|
|
963
|
+
consumeEvents(state, meta.consume_event_ids)
|
|
964
|
+
|
|
965
|
+
if (meta.follow_up === true && !input.error && !isHeartbeatOk && !state.paused && state.followupChainCount < MAX_FOLLOWUP_CHAIN) {
|
|
966
|
+
state.followupChainCount += 1
|
|
967
|
+
const delaySec = clampInt(meta.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900)
|
|
968
|
+
followup = {
|
|
969
|
+
message: buildFollowupPrompt(state, { hasMemoryTool }),
|
|
970
|
+
delayMs: delaySec * 1000,
|
|
971
|
+
dedupeKey: `main-loop-followup:${input.sessionId}`,
|
|
972
|
+
}
|
|
973
|
+
appendTimeline(state, 'followup', `Queued chained follow-up in ${delaySec}s.`, now, state.status)
|
|
974
|
+
} else if (meta.follow_up === false || isHeartbeatOk) {
|
|
975
|
+
state.followupChainCount = 0
|
|
976
|
+
}
|
|
977
|
+
if (state.status === 'ok' || state.status === 'blocked') {
|
|
978
|
+
forceMemoryNote = true
|
|
979
|
+
}
|
|
980
|
+
} else if (!isHeartbeatOk && trimmedText) {
|
|
981
|
+
state.metaMissCount = Math.min(100, state.metaMissCount + 1)
|
|
982
|
+
state.summary = toOneLine(trimmedText, 700)
|
|
983
|
+
appendWorkingMemoryNote(state, `inferred:${toOneLine(trimmedText, 160)}`)
|
|
984
|
+
if (state.status === 'idle') state.status = 'progress'
|
|
985
|
+
appendEvent(state, 'meta_missing', 'Main-loop reply missing [MAIN_LOOP_META] contract; state inferred from text.', now)
|
|
986
|
+
appendTimeline(state, 'meta_missing', 'Missing [MAIN_LOOP_META]; inferred state from plain text.', now, state.status)
|
|
987
|
+
} else if (isHeartbeatOk) {
|
|
988
|
+
state.metaMissCount = 0
|
|
989
|
+
appendTimeline(state, 'heartbeat_ok', 'Heartbeat returned HEARTBEAT_OK.', now, state.status)
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (input.internal && state.status === 'ok') {
|
|
994
|
+
const completionGateReason = getMissionCompletionGateReason(session, state, input.resultText || '')
|
|
995
|
+
if (completionGateReason) {
|
|
996
|
+
state.status = 'progress'
|
|
997
|
+
if (!state.nextAction || /^no queued action/i.test(state.nextAction)) {
|
|
998
|
+
state.nextAction = 'Wait for the next schedule run and verify a screenshot artifact link is delivered.'
|
|
999
|
+
}
|
|
1000
|
+
appendEvent(state, 'completion_gate', completionGateReason, now)
|
|
1001
|
+
appendTimeline(state, 'completion_gate', 'Holding completion until screenshot artifact evidence is observed.', now, state.status)
|
|
1002
|
+
appendWorkingMemoryNote(state, `gate:${toOneLine(completionGateReason, 180)}`)
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
state.missionTaskId = upsertMissionTask(session, state, now)
|
|
1007
|
+
const shouldWritePeriodicMemory = !!state.summary && state.status === 'progress'
|
|
1008
|
+
maybeStoreMissionMemoryNote(session, state, now, input.source, forceMemoryNote || shouldWritePeriodicMemory)
|
|
1009
|
+
state.momentumScore = computeMomentumScore(state)
|
|
1010
|
+
|
|
1011
|
+
state.updatedAt = now
|
|
1012
|
+
session.mainLoopState = state
|
|
1013
|
+
sessions[input.sessionId] = session
|
|
1014
|
+
saveSessions(sessions)
|
|
1015
|
+
|
|
1016
|
+
return followup
|
|
1017
|
+
}
|