@swarmclawai/swarmclaw 1.3.6 → 1.4.2
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 +16 -52
- package/next.config.ts +9 -4
- package/package.json +18 -10
- package/scripts/build-bootstrap-env.mjs +24 -0
- package/scripts/run-next-build.mjs +74 -0
- package/scripts/run-next-typegen.mjs +61 -0
- package/src/app/api/.well-known/agent-card/route.ts +46 -0
- package/src/app/api/a2a/route.ts +56 -0
- package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
- package/src/app/api/approvals/route.test.ts +29 -3
- package/src/app/api/approvals/route.ts +13 -7
- package/src/app/api/chats/[id]/chat/route.test.ts +64 -0
- package/src/app/api/chats/[id]/chat/route.ts +24 -8
- package/src/app/api/chats/[id]/deploy/route.ts +2 -2
- package/src/app/api/chats/chat-route.test.ts +68 -0
- package/src/app/api/connectors/[id]/doctor/route.test.ts +97 -0
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -1
- package/src/app/api/connectors/connector-doctor-route.test.ts +1 -0
- package/src/app/api/logs/route.test.ts +61 -0
- package/src/app/api/logs/route.ts +35 -0
- package/src/app/api/openclaw/sync/route.ts +1 -1
- package/src/app/api/swarmfeed/channels/route.ts +14 -0
- package/src/app/api/swarmfeed/posts/route.ts +60 -0
- package/src/app/api/swarmfeed/route.ts +37 -0
- package/src/app/api/tts/route.test.ts +82 -0
- package/src/app/api/tts/route.ts +13 -6
- package/src/app/api/tts/stream/route.ts +12 -5
- package/src/app/error.tsx +32 -0
- package/src/app/global-error.tsx +33 -0
- package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
- package/src/app/protocols/page.tsx +16 -7
- package/src/app/swarmfeed/page.tsx +7 -0
- package/src/cli/index.js +22 -0
- package/src/cli/spec.js +9 -0
- package/src/components/agents/agent-avatar.tsx +2 -5
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/auth/access-key-gate.tsx +25 -0
- package/src/components/layout/error-boundary.tsx +12 -30
- package/src/components/layout/error-fallback.tsx +61 -0
- package/src/components/layout/sidebar-rail.tsx +52 -0
- package/src/components/protocols/builder/edge-editor.tsx +43 -0
- package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
- package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
- package/src/components/protocols/builder/edge-types/index.ts +3 -0
- package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
- package/src/components/protocols/builder/node-inspector.tsx +227 -0
- package/src/components/protocols/builder/node-palette.tsx +97 -0
- package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
- package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
- package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
- package/src/components/protocols/builder/node-types/index.ts +9 -0
- package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
- package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
- package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
- package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
- package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
- package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
- package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
- package/src/components/protocols/builder/run-overlay.tsx +29 -0
- package/src/components/protocols/builder/template-gallery.tsx +53 -0
- package/src/components/protocols/builder/validation-panel.tsx +57 -0
- package/src/components/skills/skills-workspace.tsx +1 -9
- package/src/features/protocols/builder/hooks/index.ts +2 -0
- package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
- package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
- package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
- package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
- package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
- package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
- package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
- package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
- package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
- package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
- package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
- package/src/features/swarmfeed/compose-post.tsx +139 -0
- package/src/features/swarmfeed/feed-page.tsx +136 -0
- package/src/features/swarmfeed/post-card.tsx +114 -0
- package/src/features/swarmfeed/queries.ts +28 -0
- package/src/lib/a2a/agent-card.ts +61 -0
- package/src/lib/a2a/auth.ts +54 -0
- package/src/lib/a2a/client.ts +133 -0
- package/src/lib/a2a/discovery.ts +116 -0
- package/src/lib/a2a/handlers.ts +176 -0
- package/src/lib/a2a/json-rpc-router.ts +38 -0
- package/src/lib/a2a/types.ts +95 -0
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/report-client-error.ts +52 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/providers/anthropic.ts +119 -107
- package/src/lib/providers/ollama.ts +34 -14
- package/src/lib/providers/openai.ts +154 -142
- package/src/lib/providers/openclaw.ts +3 -3
- package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
- package/src/lib/server/agents/main-agent-loop.ts +377 -41
- package/src/lib/server/chat-execution/chat-execution.ts +12 -7
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +19 -12
- package/src/lib/server/connectors/swarmdock.ts +1 -1
- package/src/lib/server/extensions.ts +11 -0
- package/src/lib/server/messages/message-repository.ts +31 -0
- package/src/lib/server/openclaw/sync.ts +4 -4
- package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
- package/src/lib/server/protocols/protocol-normalization.ts +1 -0
- package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
- package/src/lib/server/protocols/protocol-types.ts +1 -0
- package/src/lib/server/provider-health.ts +19 -3
- package/src/lib/server/safe-parse-body.test.ts +32 -0
- package/src/lib/server/safe-parse-body.ts +20 -3
- package/src/lib/server/session-tools/delegate.ts +151 -77
- package/src/lib/server/storage-auth.ts +10 -2
- package/src/lib/server/storage-normalization.ts +11 -0
- package/src/lib/server/storage.ts +113 -4
- package/src/lib/server/working-state/service.test.ts +2 -3
- package/src/lib/server/working-state/service.ts +37 -6
- package/src/lib/swarmfeed-client.ts +157 -0
- package/src/lib/validation/schemas.ts +1 -1
- package/src/stores/slices/data-slice.ts +3 -0
- package/src/stores/use-approval-store.ts +4 -1
- package/src/types/agent.ts +31 -1
- package/src/types/index.ts +1 -0
- package/src/types/protocol.ts +19 -0
- package/src/types/session.ts +1 -1
- package/src/types/swarmfeed.ts +30 -0
- package/tsconfig.json +1 -2
|
@@ -16,8 +16,10 @@ import { deleteSessionWorkingState, loadSessionWorkingState, syncWorkingStateFro
|
|
|
16
16
|
import { syncMainLoopToRunContext } from '@/lib/server/run-context'
|
|
17
17
|
import { buildExecutionBrief, buildExecutionBriefContextBlock } from '@/lib/server/execution-brief'
|
|
18
18
|
import { cleanText, cleanMultiline } from '@/lib/server/text-normalization'
|
|
19
|
+
import { getGoalById, resolveEffectiveGoal } from '@/lib/server/goals/goal-service'
|
|
19
20
|
|
|
20
|
-
const LEGACY_META_LINE_RE = /\[(?:MAIN_LOOP_META|MAIN_LOOP_PLAN|MAIN_LOOP_REVIEW|AGENT_HEARTBEAT_META)\]\s*(\{[^\n]*\})?/i
|
|
21
|
+
const LEGACY_META_LINE_RE = /\[(?:MAIN_LOOP_META|MAIN_LOOP_PLAN|MAIN_LOOP_REVIEW|AGENT_HEARTBEAT_META|AUTONOMY_TICK)\]\s*(\{[^\n]*\})?/i
|
|
22
|
+
const AUTONOMY_TICK_RE = /\[AUTONOMY_TICK\]\s*(\{[^\n]*\})/i
|
|
21
23
|
const HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
|
|
22
24
|
const MAX_PENDING_EVENTS = 16
|
|
23
25
|
const MAX_TIMELINE_ITEMS = 40
|
|
@@ -26,9 +28,12 @@ const DEFAULT_FOLLOWUP_DELAY_MS = 1500
|
|
|
26
28
|
const DEFAULT_MAX_FOLLOWUP_CHAIN = 3
|
|
27
29
|
const MAX_LIFETIME_ITERATIONS = 200
|
|
28
30
|
|
|
31
|
+
type MainLoopObjectiveSource = 'goal' | 'working_state' | 'run_context' | 'legacy_tag'
|
|
32
|
+
|
|
29
33
|
export interface MainLoopState {
|
|
30
34
|
goal: string | null
|
|
31
35
|
goalContract: GoalContract | null
|
|
36
|
+
objectiveSource: MainLoopObjectiveSource | null
|
|
32
37
|
summary: string | null
|
|
33
38
|
nextAction: string | null
|
|
34
39
|
planSteps: string[]
|
|
@@ -70,6 +75,8 @@ export interface MainLoopState {
|
|
|
70
75
|
lastPlannedAt: number | null
|
|
71
76
|
lastReviewedAt: number | null
|
|
72
77
|
lastTickAt: number | null
|
|
78
|
+
missionTokens: number
|
|
79
|
+
missionCostUsd: number
|
|
73
80
|
updatedAt: number
|
|
74
81
|
}
|
|
75
82
|
|
|
@@ -99,6 +106,28 @@ export interface HandleMainLoopRunResultInput {
|
|
|
99
106
|
estimatedCost?: number
|
|
100
107
|
}
|
|
101
108
|
|
|
109
|
+
interface DurableObjectiveSnapshot {
|
|
110
|
+
goal: string | null
|
|
111
|
+
goalContract: GoalContract | null
|
|
112
|
+
objectiveSource: Exclude<MainLoopObjectiveSource, 'legacy_tag'> | null
|
|
113
|
+
summary: string | null
|
|
114
|
+
nextAction: string | null
|
|
115
|
+
status: MainLoopState['status'] | null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface ParsedAutonomyTick {
|
|
119
|
+
goal?: string
|
|
120
|
+
status?: MainLoopState['status']
|
|
121
|
+
summary?: string
|
|
122
|
+
nextAction?: string
|
|
123
|
+
planSteps?: string[]
|
|
124
|
+
currentStep?: string
|
|
125
|
+
completedSteps?: string[]
|
|
126
|
+
reviewNote?: string
|
|
127
|
+
reviewConfidence?: number
|
|
128
|
+
needsReplan?: boolean
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
type MainSessionLike = Partial<Session> & Record<string, unknown>
|
|
103
132
|
|
|
104
133
|
const stateMap = hmrSingleton('__swarmclaw_main_loop_state__', () => new Map<string, MainLoopState>())
|
|
@@ -126,6 +155,7 @@ function defaultState(): MainLoopState {
|
|
|
126
155
|
return {
|
|
127
156
|
goal: null,
|
|
128
157
|
goalContract: null,
|
|
158
|
+
objectiveSource: null,
|
|
129
159
|
summary: null,
|
|
130
160
|
nextAction: null,
|
|
131
161
|
planSteps: [],
|
|
@@ -148,6 +178,8 @@ function defaultState(): MainLoopState {
|
|
|
148
178
|
lastPlannedAt: null,
|
|
149
179
|
lastReviewedAt: null,
|
|
150
180
|
lastTickAt: null,
|
|
181
|
+
missionTokens: 0,
|
|
182
|
+
missionCostUsd: 0,
|
|
151
183
|
updatedAt: now(),
|
|
152
184
|
}
|
|
153
185
|
}
|
|
@@ -162,6 +194,11 @@ function normalizeAutonomyMode(value: unknown, fallback: MainLoopState['autonomy
|
|
|
162
194
|
return value === 'autonomous' || value === 'assist' ? value : fallback
|
|
163
195
|
}
|
|
164
196
|
|
|
197
|
+
function cleanNullableText(value: unknown, max: number): string | null {
|
|
198
|
+
const cleaned = cleanText(value, max)
|
|
199
|
+
return cleaned || null
|
|
200
|
+
}
|
|
201
|
+
|
|
165
202
|
function uniqueStrings(values: string[], maxItems: number): string[] {
|
|
166
203
|
const seen = new Set<string>()
|
|
167
204
|
const out: string[] = []
|
|
@@ -282,21 +319,78 @@ function parseHeartbeatMeta(text: string): { goal?: string; status?: MainLoopSta
|
|
|
282
319
|
}
|
|
283
320
|
}
|
|
284
321
|
|
|
322
|
+
function parseAutonomyTick(text: string): ParsedAutonomyTick | null {
|
|
323
|
+
const match = (text || '').match(AUTONOMY_TICK_RE)
|
|
324
|
+
if (!match) return null
|
|
325
|
+
try {
|
|
326
|
+
const parsed = JSON.parse(match[1]) as Record<string, unknown>
|
|
327
|
+
const review = parsed.review && typeof parsed.review === 'object' && !Array.isArray(parsed.review)
|
|
328
|
+
? parsed.review as Record<string, unknown>
|
|
329
|
+
: null
|
|
330
|
+
const payload: ParsedAutonomyTick = {}
|
|
331
|
+
const goal = cleanText(parsed.goal, 400)
|
|
332
|
+
const summary = cleanText(parsed.summary, 500)
|
|
333
|
+
const nextAction = cleanText(parsed.next_action ?? parsed.nextAction, 240)
|
|
334
|
+
const currentStep = cleanText(parsed.current_step ?? parsed.currentStep, 240)
|
|
335
|
+
const planSteps = Array.isArray(parsed.plan_steps ?? parsed.planSteps)
|
|
336
|
+
? uniqueStrings(((parsed.plan_steps ?? parsed.planSteps) as unknown[]).filter((value): value is string => typeof value === 'string'), 8)
|
|
337
|
+
: []
|
|
338
|
+
const completedSteps = Array.isArray(parsed.completed_steps ?? parsed.completedSteps)
|
|
339
|
+
? uniqueStrings(((parsed.completed_steps ?? parsed.completedSteps) as unknown[]).filter((value): value is string => typeof value === 'string'), 16)
|
|
340
|
+
: []
|
|
341
|
+
const reviewNote = cleanText(review?.note ?? parsed.review_note ?? parsed.reviewNote, 320)
|
|
342
|
+
const reviewConfidence = normalizeConfidence(review?.confidence ?? parsed.review_confidence ?? parsed.reviewConfidence)
|
|
343
|
+
const needsReplan = review?.needs_replan === true
|
|
344
|
+
|| parsed.needs_replan === true
|
|
345
|
+
|| parsed.needsReplan === true
|
|
346
|
+
? true
|
|
347
|
+
: review?.needs_replan === false
|
|
348
|
+
|| parsed.needs_replan === false
|
|
349
|
+
|| parsed.needsReplan === false
|
|
350
|
+
? false
|
|
351
|
+
: undefined
|
|
352
|
+
|
|
353
|
+
if (goal) payload.goal = goal
|
|
354
|
+
if (summary) payload.summary = summary
|
|
355
|
+
if (nextAction) payload.nextAction = nextAction
|
|
356
|
+
if (currentStep) payload.currentStep = currentStep
|
|
357
|
+
if (planSteps.length > 0) payload.planSteps = planSteps
|
|
358
|
+
if (completedSteps.length > 0) payload.completedSteps = completedSteps
|
|
359
|
+
if (reviewNote) payload.reviewNote = reviewNote
|
|
360
|
+
if (typeof reviewConfidence === 'number') payload.reviewConfidence = reviewConfidence
|
|
361
|
+
if (typeof needsReplan === 'boolean') payload.needsReplan = needsReplan
|
|
362
|
+
if (parsed.status === 'idle' || parsed.status === 'progress' || parsed.status === 'blocked' || parsed.status === 'ok') {
|
|
363
|
+
payload.status = normalizeStatus(parsed.status, 'idle')
|
|
364
|
+
}
|
|
365
|
+
return Object.keys(payload).length > 0 ? payload : null
|
|
366
|
+
} catch {
|
|
367
|
+
return null
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
285
371
|
function clampState(state: MainLoopState): MainLoopState {
|
|
286
372
|
state.planSteps = uniqueStrings(state.planSteps || [], 8)
|
|
287
373
|
state.workingMemoryNotes = uniqueStrings(state.workingMemoryNotes || [], MAX_WORKING_MEMORY_NOTES)
|
|
288
374
|
state.pendingEvents = normalizePendingEvents(state.pendingEvents).slice(-MAX_PENDING_EVENTS)
|
|
289
375
|
state.timeline = normalizeTimeline(state.timeline).slice(-MAX_TIMELINE_ITEMS)
|
|
290
|
-
state.goal =
|
|
291
|
-
state.summary =
|
|
292
|
-
state.nextAction =
|
|
293
|
-
state.currentPlanStep =
|
|
294
|
-
state.reviewNote =
|
|
376
|
+
state.goal = cleanNullableText(state.goal, 500)
|
|
377
|
+
state.summary = cleanNullableText(state.summary, 1000)
|
|
378
|
+
state.nextAction = cleanNullableText(state.nextAction, 240)
|
|
379
|
+
state.currentPlanStep = cleanNullableText(state.currentPlanStep, 240)
|
|
380
|
+
state.reviewNote = cleanNullableText(state.reviewNote, 320)
|
|
295
381
|
state.reviewConfidence = normalizeConfidence(state.reviewConfidence)
|
|
382
|
+
state.objectiveSource = state.objectiveSource === 'goal'
|
|
383
|
+
|| state.objectiveSource === 'working_state'
|
|
384
|
+
|| state.objectiveSource === 'run_context'
|
|
385
|
+
|| state.objectiveSource === 'legacy_tag'
|
|
386
|
+
? state.objectiveSource
|
|
387
|
+
: null
|
|
296
388
|
state.momentumScore = Math.max(-10, Math.min(10, Math.trunc(state.momentumScore || 0)))
|
|
297
389
|
state.followupChainCount = Math.max(0, Math.min(10, Math.trunc(state.followupChainCount || 0)))
|
|
298
390
|
state.lifetimeIterations = Math.max(0, Math.trunc(state.lifetimeIterations || 0))
|
|
299
391
|
state.metaMissCount = Math.max(0, Math.min(100, Math.trunc(state.metaMissCount || 0)))
|
|
392
|
+
state.missionTokens = Math.max(0, Math.trunc(state.missionTokens || 0))
|
|
393
|
+
state.missionCostUsd = Number.isFinite(state.missionCostUsd) ? Math.max(0, state.missionCostUsd || 0) : 0
|
|
300
394
|
state.skillBlocker = normalizeSkillBlocker(state.skillBlocker)
|
|
301
395
|
state.updatedAt = typeof state.updatedAt === 'number' && Number.isFinite(state.updatedAt) ? Math.trunc(state.updatedAt) : now()
|
|
302
396
|
return state
|
|
@@ -307,6 +401,9 @@ function normalizeState(input?: Partial<MainLoopState> | null): MainLoopState {
|
|
|
307
401
|
if (input) {
|
|
308
402
|
if (input.goalContract) next.goalContract = input.goalContract
|
|
309
403
|
if (typeof input.goal === 'string' || input.goal === null) next.goal = input.goal
|
|
404
|
+
if (input.objectiveSource === 'goal' || input.objectiveSource === 'working_state' || input.objectiveSource === 'run_context' || input.objectiveSource === 'legacy_tag' || input.objectiveSource === null) {
|
|
405
|
+
next.objectiveSource = input.objectiveSource
|
|
406
|
+
}
|
|
310
407
|
if (typeof input.summary === 'string' || input.summary === null) next.summary = input.summary
|
|
311
408
|
if (typeof input.nextAction === 'string' || input.nextAction === null) next.nextAction = input.nextAction
|
|
312
409
|
if (Array.isArray(input.planSteps)) next.planSteps = [...input.planSteps]
|
|
@@ -331,6 +428,8 @@ function normalizeState(input?: Partial<MainLoopState> | null): MainLoopState {
|
|
|
331
428
|
if (typeof input.lastPlannedAt === 'number' || input.lastPlannedAt === null) next.lastPlannedAt = input.lastPlannedAt ?? null
|
|
332
429
|
if (typeof input.lastReviewedAt === 'number' || input.lastReviewedAt === null) next.lastReviewedAt = input.lastReviewedAt ?? null
|
|
333
430
|
if (typeof input.lastTickAt === 'number' || input.lastTickAt === null) next.lastTickAt = input.lastTickAt ?? null
|
|
431
|
+
if (typeof input.missionTokens === 'number') next.missionTokens = input.missionTokens
|
|
432
|
+
if (typeof input.missionCostUsd === 'number') next.missionCostUsd = input.missionCostUsd
|
|
334
433
|
if (typeof input.updatedAt === 'number') next.updatedAt = input.updatedAt
|
|
335
434
|
}
|
|
336
435
|
return clampState(next)
|
|
@@ -373,6 +472,155 @@ function extractLatestGoal(messages: Message[]): { goal: string | null; goalCont
|
|
|
373
472
|
return { goal, goalContract }
|
|
374
473
|
}
|
|
375
474
|
|
|
475
|
+
function mapWorkingStateStatusToMainLoopStatus(value: unknown): MainLoopState['status'] | null {
|
|
476
|
+
if (value === 'completed') return 'ok'
|
|
477
|
+
if (value === 'blocked' || value === 'waiting') return 'blocked'
|
|
478
|
+
if (value === 'progress') return 'progress'
|
|
479
|
+
if (value === 'idle') return 'idle'
|
|
480
|
+
return null
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function buildGoalContractFromGoal(goal: Record<string, unknown> | null): GoalContract | null {
|
|
484
|
+
if (!goal) return null
|
|
485
|
+
const objective = cleanMultiline(goal.objective, 900)
|
|
486
|
+
if (!objective) return null
|
|
487
|
+
const constraints = Array.isArray(goal.constraints)
|
|
488
|
+
? goal.constraints.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
489
|
+
: []
|
|
490
|
+
const budgetUsd = typeof goal.budgetUsd === 'number' && Number.isFinite(goal.budgetUsd) ? goal.budgetUsd : null
|
|
491
|
+
const deadlineAt = typeof goal.deadlineAt === 'number' && Number.isFinite(goal.deadlineAt) ? goal.deadlineAt : null
|
|
492
|
+
const successMetric = cleanText(goal.successMetric, 240)
|
|
493
|
+
return {
|
|
494
|
+
objective,
|
|
495
|
+
constraints: constraints.length ? constraints : undefined,
|
|
496
|
+
budgetUsd,
|
|
497
|
+
deadlineAt,
|
|
498
|
+
successMetric,
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function resolveSessionGoalRecord(session: Session | null | undefined): Record<string, unknown> | null {
|
|
503
|
+
if (!session) return null
|
|
504
|
+
const s = session as unknown as Record<string, unknown>
|
|
505
|
+
const directGoalId = typeof s.goalId === 'string'
|
|
506
|
+
? String(s.goalId).trim()
|
|
507
|
+
: ''
|
|
508
|
+
if (directGoalId) {
|
|
509
|
+
const directGoal = getGoalById(directGoalId)
|
|
510
|
+
if (directGoal) return directGoal as unknown as Record<string, unknown>
|
|
511
|
+
}
|
|
512
|
+
const legacyMissionId = typeof s.missionId === 'string'
|
|
513
|
+
? String(s.missionId).trim()
|
|
514
|
+
: ''
|
|
515
|
+
if (legacyMissionId) {
|
|
516
|
+
const missionGoal = getGoalById(legacyMissionId)
|
|
517
|
+
if (missionGoal) return missionGoal as unknown as Record<string, unknown>
|
|
518
|
+
}
|
|
519
|
+
const effectiveGoal = resolveEffectiveGoal({
|
|
520
|
+
agentId: session.agentId || null,
|
|
521
|
+
projectId: session.projectId || null,
|
|
522
|
+
})
|
|
523
|
+
return effectiveGoal ? effectiveGoal as unknown as Record<string, unknown> : null
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function resolveRunContextNextAction(session: Session | null | undefined): string | null {
|
|
527
|
+
const runContext = session?.runContext
|
|
528
|
+
if (!runContext || !Array.isArray(runContext.currentPlan)) return null
|
|
529
|
+
const completed = new Set(
|
|
530
|
+
Array.isArray(runContext.completedSteps)
|
|
531
|
+
? runContext.completedSteps
|
|
532
|
+
.map((entry) => cleanText(entry, 240)?.toLowerCase())
|
|
533
|
+
.filter((entry): entry is string => Boolean(entry))
|
|
534
|
+
: [],
|
|
535
|
+
)
|
|
536
|
+
for (const step of runContext.currentPlan) {
|
|
537
|
+
const cleaned = cleanText(step, 240)
|
|
538
|
+
if (!cleaned) continue
|
|
539
|
+
if (completed.has(cleaned.toLowerCase())) continue
|
|
540
|
+
return cleaned
|
|
541
|
+
}
|
|
542
|
+
return null
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function resolveDurableObjectiveSnapshot(
|
|
546
|
+
sessionId: string,
|
|
547
|
+
session: Session | null | undefined,
|
|
548
|
+
current: MainLoopState,
|
|
549
|
+
): DurableObjectiveSnapshot {
|
|
550
|
+
const workingState = loadSessionWorkingState(sessionId)
|
|
551
|
+
if (workingState && (workingState.objective || workingState.summary || workingState.nextAction || (workingState.planSteps?.length || 0) > 0)) {
|
|
552
|
+
const workingPlan = Array.isArray(workingState.planSteps) ? workingState.planSteps : []
|
|
553
|
+
const activeStep = workingPlan.find((step) => step.status === 'active')
|
|
554
|
+
const firstPlanStep = workingPlan.find((step) => step.status !== 'resolved' && step.status !== 'superseded')
|
|
555
|
+
return {
|
|
556
|
+
goal: cleanMultiline(workingState.objective, 900) || null,
|
|
557
|
+
goalContract: mergeGoalContracts(
|
|
558
|
+
buildGoalContractFromGoal(resolveSessionGoalRecord(session)),
|
|
559
|
+
workingState.objective ? parseGoalContractFromText(workingState.objective) : null,
|
|
560
|
+
),
|
|
561
|
+
objectiveSource: 'working_state',
|
|
562
|
+
summary: cleanText(workingState.summary, 1000) || null,
|
|
563
|
+
nextAction: cleanText(workingState.nextAction || activeStep?.text || firstPlanStep?.text, 240) || null,
|
|
564
|
+
status: mapWorkingStateStatusToMainLoopStatus(workingState.status),
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const goalRecord = resolveSessionGoalRecord(session)
|
|
569
|
+
const goalObjective = cleanMultiline(goalRecord?.objective, 900)
|
|
570
|
+
if (goalObjective) {
|
|
571
|
+
return {
|
|
572
|
+
goal: goalObjective,
|
|
573
|
+
goalContract: buildGoalContractFromGoal(goalRecord),
|
|
574
|
+
objectiveSource: 'goal',
|
|
575
|
+
summary: current.summary,
|
|
576
|
+
nextAction: current.nextAction,
|
|
577
|
+
status: null,
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const runContext = session?.runContext
|
|
582
|
+
const runContextObjective = cleanMultiline(runContext?.objective, 900)
|
|
583
|
+
if (runContextObjective) {
|
|
584
|
+
return {
|
|
585
|
+
goal: runContextObjective,
|
|
586
|
+
goalContract: parseGoalContractFromText(runContextObjective),
|
|
587
|
+
objectiveSource: 'run_context',
|
|
588
|
+
summary: current.summary,
|
|
589
|
+
nextAction: resolveRunContextNextAction(session),
|
|
590
|
+
status: null,
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
goal: null,
|
|
596
|
+
goalContract: null,
|
|
597
|
+
objectiveSource: null,
|
|
598
|
+
summary: null,
|
|
599
|
+
nextAction: null,
|
|
600
|
+
status: null,
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function applyDurableObjectiveOverlay(
|
|
605
|
+
sessionId: string,
|
|
606
|
+
state: MainLoopState,
|
|
607
|
+
session: Session | null | undefined,
|
|
608
|
+
): MainLoopState {
|
|
609
|
+
const durable = resolveDurableObjectiveSnapshot(sessionId, session, state)
|
|
610
|
+
if (!durable.objectiveSource) {
|
|
611
|
+
if (!state.objectiveSource && state.goal) state.objectiveSource = 'legacy_tag'
|
|
612
|
+
return clampState(state)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (durable.goal) state.goal = durable.goal
|
|
616
|
+
state.objectiveSource = durable.objectiveSource
|
|
617
|
+
state.goalContract = mergeGoalContracts(state.goalContract, durable.goalContract)
|
|
618
|
+
if (durable.summary) state.summary = durable.summary
|
|
619
|
+
if (durable.nextAction) state.nextAction = durable.nextAction
|
|
620
|
+
if (durable.status && state.status !== 'blocked') state.status = durable.status
|
|
621
|
+
return clampState(state)
|
|
622
|
+
}
|
|
623
|
+
|
|
376
624
|
function hydrateStateFromSession(sessionId: string): MainLoopState | null {
|
|
377
625
|
const sessions = loadSessions()
|
|
378
626
|
const session = sessions[sessionId]
|
|
@@ -386,11 +634,15 @@ function hydrateStateFromSession(sessionId: string): MainLoopState | null {
|
|
|
386
634
|
const initial = extractLatestGoal(messages)
|
|
387
635
|
hydrated.goal = initial.goal
|
|
388
636
|
hydrated.goalContract = initial.goalContract
|
|
637
|
+
hydrated.objectiveSource = initial.goal ? 'legacy_tag' : null
|
|
389
638
|
|
|
390
639
|
for (const message of messages) {
|
|
391
640
|
if (message.role !== 'assistant' || typeof message.text !== 'string') continue
|
|
392
641
|
const heartbeat = parseHeartbeatMeta(message.text)
|
|
393
|
-
if (heartbeat?.goal)
|
|
642
|
+
if (heartbeat?.goal) {
|
|
643
|
+
hydrated.goal = heartbeat.goal
|
|
644
|
+
hydrated.objectiveSource = 'legacy_tag'
|
|
645
|
+
}
|
|
394
646
|
if (heartbeat?.summary) hydrated.summary = heartbeat.summary
|
|
395
647
|
if (heartbeat?.nextAction) hydrated.nextAction = heartbeat.nextAction
|
|
396
648
|
if (heartbeat?.status) hydrated.status = heartbeat.status
|
|
@@ -415,7 +667,7 @@ function hydrateStateFromSession(sessionId: string): MainLoopState | null {
|
|
|
415
667
|
}
|
|
416
668
|
}
|
|
417
669
|
|
|
418
|
-
return mergeWorkingStateIntoMainLoopState(sessionId, normalizeState(hydrated))
|
|
670
|
+
return applyDurableObjectiveOverlay(sessionId, mergeWorkingStateIntoMainLoopState(sessionId, normalizeState(hydrated)), session)
|
|
419
671
|
}
|
|
420
672
|
|
|
421
673
|
function persistState(sessionId: string, state: MainLoopState): void {
|
|
@@ -423,6 +675,10 @@ function persistState(sessionId: string, state: MainLoopState): void {
|
|
|
423
675
|
upsertPersistedMainLoopState(sessionId, normalized as unknown as Record<string, unknown>)
|
|
424
676
|
const session = getSession(sessionId)
|
|
425
677
|
if (!session) return
|
|
678
|
+
const shouldSyncDurableWorkingState = normalized.objectiveSource === 'goal'
|
|
679
|
+
|| normalized.objectiveSource === 'working_state'
|
|
680
|
+
|| normalized.objectiveSource === 'run_context'
|
|
681
|
+
if (!shouldSyncDurableWorkingState) return
|
|
426
682
|
void syncWorkingStateFromMainLoopState({
|
|
427
683
|
sessionId,
|
|
428
684
|
goal: normalized.goal,
|
|
@@ -432,10 +688,11 @@ function persistState(sessionId: string, state: MainLoopState): void {
|
|
|
432
688
|
: normalized.status === 'blocked'
|
|
433
689
|
? 'blocked'
|
|
434
690
|
: normalized.status === 'progress'
|
|
435
|
-
|
|
691
|
+
? 'progress'
|
|
436
692
|
: 'idle',
|
|
437
693
|
nextAction: normalized.nextAction,
|
|
438
694
|
planSteps: normalized.planSteps,
|
|
695
|
+
completedPlanSteps: normalized.completedPlanSteps,
|
|
439
696
|
blockers: normalized.skillBlocker ? [{
|
|
440
697
|
summary: normalized.skillBlocker.summary,
|
|
441
698
|
kind: normalized.skillBlocker.status === 'approval_requested' ? 'approval' : 'other',
|
|
@@ -446,7 +703,12 @@ function persistState(sessionId: string, state: MainLoopState): void {
|
|
|
446
703
|
function getOrCreateState(sessionId: string): MainLoopState | null {
|
|
447
704
|
const existing = stateMap.get(sessionId)
|
|
448
705
|
if (existing) {
|
|
449
|
-
const
|
|
706
|
+
const sessions = loadSessions()
|
|
707
|
+
const merged = applyDurableObjectiveOverlay(
|
|
708
|
+
sessionId,
|
|
709
|
+
mergeWorkingStateIntoMainLoopState(sessionId, existing),
|
|
710
|
+
sessions[sessionId] as Session | undefined,
|
|
711
|
+
)
|
|
450
712
|
stateMap.set(sessionId, merged)
|
|
451
713
|
return merged
|
|
452
714
|
}
|
|
@@ -454,7 +716,12 @@ function getOrCreateState(sessionId: string): MainLoopState | null {
|
|
|
454
716
|
// Try disk (survives full restart)
|
|
455
717
|
const persisted = loadPersistedMainLoopState(sessionId) as Partial<MainLoopState> | null
|
|
456
718
|
if (persisted) {
|
|
457
|
-
const
|
|
719
|
+
const sessions = loadSessions()
|
|
720
|
+
const restored = applyDurableObjectiveOverlay(
|
|
721
|
+
sessionId,
|
|
722
|
+
mergeWorkingStateIntoMainLoopState(sessionId, normalizeState(persisted)),
|
|
723
|
+
sessions[sessionId] as Session | undefined,
|
|
724
|
+
)
|
|
458
725
|
stateMap.set(sessionId, restored)
|
|
459
726
|
return restored
|
|
460
727
|
}
|
|
@@ -789,6 +1056,14 @@ export function buildMainLoopHeartbeatPrompt(session: unknown, fallbackPrompt: s
|
|
|
789
1056
|
const effectiveGoalContract = latestExternalGoal.goalContract
|
|
790
1057
|
? mergeGoalContracts(state.goalContract, latestExternalGoal.goalContract)
|
|
791
1058
|
: state.goalContract
|
|
1059
|
+
const durableSnapshot = resolveDurableObjectiveSnapshot(
|
|
1060
|
+
String(candidate.id),
|
|
1061
|
+
(persistedSession || candidate as Session),
|
|
1062
|
+
state,
|
|
1063
|
+
)
|
|
1064
|
+
const usesDurableObjective = durableSnapshot.objectiveSource === 'goal'
|
|
1065
|
+
|| durableSnapshot.objectiveSource === 'working_state'
|
|
1066
|
+
|| durableSnapshot.objectiveSource === 'run_context'
|
|
792
1067
|
|
|
793
1068
|
const heartbeatSession = (persistedSession || candidate as Session)
|
|
794
1069
|
const executionBrief = buildExecutionBrief({
|
|
@@ -819,12 +1094,22 @@ export function buildMainLoopHeartbeatPrompt(session: unknown, fallbackPrompt: s
|
|
|
819
1094
|
'Use the execution brief and pending external events shown above as the authoritative state for this tick.',
|
|
820
1095
|
'Do not infer or repeat old tasks from prior heartbeats.',
|
|
821
1096
|
'Prefer taking the single highest-value next step over restating the plan. Do not repeat completed work.',
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1097
|
+
...(usesDurableObjective
|
|
1098
|
+
? [
|
|
1099
|
+
'If anything materially changed, emit exactly one line like:',
|
|
1100
|
+
'[AUTONOMY_TICK]{"status":"progress","summary":"what changed","next_action":"best next step","plan_steps":["step 1","step 2"],"current_step":"step 1","completed_steps":["done"],"review":{"note":"brief review","confidence":0.72,"needs_replan":false}}',
|
|
1101
|
+
'The durable objective, current execution brief, and working state are authoritative for this thread.',
|
|
1102
|
+
'If nothing materially changed, stop cleanly instead of replaying legacy heartbeat metadata.',
|
|
1103
|
+
'Reply HEARTBEAT_OK only when nothing needs action right now.',
|
|
1104
|
+
]
|
|
1105
|
+
: [
|
|
1106
|
+
'If you revise the plan, emit exactly one line like:',
|
|
1107
|
+
'[MAIN_LOOP_PLAN]{"steps":["step 1","step 2"],"current_step":"step 1","completed_steps":["step 0"]}',
|
|
1108
|
+
'After acting, emit exactly one review line like:',
|
|
1109
|
+
'[MAIN_LOOP_REVIEW]{"note":"what changed","confidence":0.72,"needs_replan":false}',
|
|
1110
|
+
'If you are actively progressing or you changed the plan, also emit [AGENT_HEARTBEAT_META] with goal/status/next_action.',
|
|
1111
|
+
'Reply HEARTBEAT_OK only when nothing needs action right now.',
|
|
1112
|
+
]),
|
|
828
1113
|
].filter(Boolean).join('\n')
|
|
829
1114
|
}
|
|
830
1115
|
|
|
@@ -881,6 +1166,7 @@ export function pruneMainLoopState(liveSessionIds: Set<string>): number {
|
|
|
881
1166
|
export function setMainLoopStateForSession(sessionId: string, patch: Partial<MainLoopState>): MainLoopState | null {
|
|
882
1167
|
const current = getOrCreateState(sessionId)
|
|
883
1168
|
if (!current) return null
|
|
1169
|
+
const sessions = loadSessions()
|
|
884
1170
|
const next = normalizeState({
|
|
885
1171
|
...current,
|
|
886
1172
|
...patch,
|
|
@@ -890,9 +1176,10 @@ export function setMainLoopStateForSession(sessionId: string, patch: Partial<Mai
|
|
|
890
1176
|
workingMemoryNotes: patch.workingMemoryNotes ?? current.workingMemoryNotes,
|
|
891
1177
|
updatedAt: now(),
|
|
892
1178
|
})
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1179
|
+
const overlaid = applyDurableObjectiveOverlay(sessionId, next, sessions[sessionId] as Session | undefined)
|
|
1180
|
+
stateMap.set(sessionId, overlaid)
|
|
1181
|
+
persistState(sessionId, overlaid)
|
|
1182
|
+
return normalizeState(overlaid)
|
|
896
1183
|
}
|
|
897
1184
|
|
|
898
1185
|
export function pushMainLoopEventToMainSessions(input: PushMainLoopEventInput): number {
|
|
@@ -937,36 +1224,69 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
937
1224
|
|
|
938
1225
|
const sessions = loadSessions()
|
|
939
1226
|
const session = sessions[input.sessionId] as unknown as Session | undefined
|
|
1227
|
+
const durableBefore = resolveDurableObjectiveSnapshot(input.sessionId, session || null, state)
|
|
1228
|
+
const hasDurableObjective = durableBefore.objectiveSource === 'goal'
|
|
1229
|
+
|| durableBefore.objectiveSource === 'working_state'
|
|
1230
|
+
|| durableBefore.objectiveSource === 'run_context'
|
|
940
1231
|
const isDirectUserChat = !input.internal && input.source === 'chat'
|
|
941
1232
|
const resultText = input.resultText || ''
|
|
942
1233
|
const persistedText = stripMainLoopMetaForPersistence(resultText)
|
|
943
1234
|
const toolEvents = Array.isArray(input.toolEvents) ? input.toolEvents : []
|
|
944
1235
|
const toolNames = uniqueStrings(toolEvents.map((event) => event.name || '').filter(Boolean), 8)
|
|
1236
|
+
const autonomyTick = parseAutonomyTick(resultText)
|
|
945
1237
|
const heartbeat = parseHeartbeatMeta(resultText)
|
|
946
1238
|
const plan = parseMainLoopPlan(resultText)
|
|
947
1239
|
const review = parseMainLoopReview(resultText)
|
|
1240
|
+
const shouldAcceptLegacyHeartbeatMeta = !input.internal || !hasDurableObjective
|
|
1241
|
+
const shouldAcceptStructuredAutonomyTick = Boolean(autonomyTick)
|
|
948
1242
|
const shouldCaptureMessageGoal = !input.internal
|
|
949
1243
|
const messageGoal = shouldCaptureMessageGoal ? parseGoalContractFromText(input.message || '') : null
|
|
950
1244
|
const nowTs = now()
|
|
951
1245
|
state.lifetimeIterations++
|
|
952
1246
|
if (messageGoal) state.goalContract = mergeGoalContracts(state.goalContract, messageGoal)
|
|
953
|
-
if (!state.goal && shouldCaptureMessageGoal)
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1247
|
+
if (!state.goal && shouldCaptureMessageGoal) {
|
|
1248
|
+
state.goal = cleanMultiline(input.message, 900)
|
|
1249
|
+
if (state.goal) state.objectiveSource = 'legacy_tag'
|
|
1250
|
+
}
|
|
1251
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick?.goal) state.goal = autonomyTick.goal
|
|
1252
|
+
if (shouldAcceptLegacyHeartbeatMeta && heartbeat?.goal) {
|
|
1253
|
+
state.goal = heartbeat.goal
|
|
1254
|
+
state.objectiveSource = 'legacy_tag'
|
|
1255
|
+
}
|
|
1256
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick?.summary) state.summary = autonomyTick.summary
|
|
1257
|
+
if (shouldAcceptLegacyHeartbeatMeta && heartbeat?.summary) state.summary = heartbeat.summary
|
|
1258
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick?.nextAction) state.nextAction = autonomyTick.nextAction
|
|
1259
|
+
if (shouldAcceptLegacyHeartbeatMeta && heartbeat?.nextAction) state.nextAction = heartbeat.nextAction
|
|
1260
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick?.status) state.status = autonomyTick.status
|
|
1261
|
+
if (shouldAcceptLegacyHeartbeatMeta && heartbeat?.status) state.status = heartbeat.status
|
|
1262
|
+
|
|
1263
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick?.planSteps?.length) state.planSteps = autonomyTick.planSteps
|
|
1264
|
+
if (shouldAcceptLegacyHeartbeatMeta && plan?.steps?.length) state.planSteps = plan.steps
|
|
1265
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick?.currentStep) state.currentPlanStep = autonomyTick.currentStep
|
|
1266
|
+
if (shouldAcceptLegacyHeartbeatMeta && plan?.current_step) state.currentPlanStep = plan.current_step
|
|
1267
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick?.completedSteps?.length) {
|
|
1268
|
+
const merged = new Set([...state.completedPlanSteps, ...autonomyTick.completedSteps])
|
|
1269
|
+
state.completedPlanSteps = [...merged].slice(0, 16)
|
|
1270
|
+
}
|
|
1271
|
+
if (shouldAcceptLegacyHeartbeatMeta && plan?.completed_steps?.length) {
|
|
962
1272
|
const merged = new Set([...state.completedPlanSteps, ...plan.completed_steps])
|
|
963
1273
|
state.completedPlanSteps = [...merged].slice(0, 16)
|
|
964
1274
|
}
|
|
965
|
-
if (
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (
|
|
969
|
-
if (review) state.
|
|
1275
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick) state.lastPlannedAt = nowTs
|
|
1276
|
+
if (shouldAcceptLegacyHeartbeatMeta && plan) state.lastPlannedAt = nowTs
|
|
1277
|
+
|
|
1278
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick?.reviewNote) state.reviewNote = autonomyTick.reviewNote
|
|
1279
|
+
if (shouldAcceptLegacyHeartbeatMeta && review?.note) state.reviewNote = review.note
|
|
1280
|
+
if (shouldAcceptStructuredAutonomyTick && typeof autonomyTick?.reviewConfidence === 'number') state.reviewConfidence = autonomyTick.reviewConfidence
|
|
1281
|
+
if (shouldAcceptLegacyHeartbeatMeta && typeof review?.confidence === 'number') state.reviewConfidence = review.confidence
|
|
1282
|
+
if (shouldAcceptStructuredAutonomyTick && autonomyTick) state.lastReviewedAt = nowTs
|
|
1283
|
+
if (shouldAcceptLegacyHeartbeatMeta && review) state.lastReviewedAt = nowTs
|
|
1284
|
+
|
|
1285
|
+
const turnTokens = (input.inputTokens || 0) + (input.outputTokens || 0)
|
|
1286
|
+
if (turnTokens > 0) state.missionTokens += turnTokens
|
|
1287
|
+
if (typeof input.estimatedCost === 'number' && Number.isFinite(input.estimatedCost) && input.estimatedCost > 0) {
|
|
1288
|
+
state.missionCostUsd += input.estimatedCost
|
|
1289
|
+
}
|
|
970
1290
|
|
|
971
1291
|
if (toolNames.length > 0) {
|
|
972
1292
|
appendWorkingMemory(state, `Used tools: ${toolNames.join(', ')}`)
|
|
@@ -992,10 +1312,16 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
992
1312
|
const cleanedResult = persistedText.trim()
|
|
993
1313
|
const waitingForExternal = extractWaitSignal(resultText)
|
|
994
1314
|
const gotTerminalAck = /^HEARTBEAT_OK$/i.test(cleanedResult) || /^NO_MESSAGE$/i.test(cleanedResult)
|
|
1315
|
+
const ignoredLegacyCompatibilityPulse = input.internal
|
|
1316
|
+
&& hasDurableObjective
|
|
1317
|
+
&& !input.error
|
|
1318
|
+
&& !toolEvents.length
|
|
1319
|
+
&& !autonomyTick
|
|
1320
|
+
&& Boolean(heartbeat || plan || review)
|
|
995
1321
|
const successfulChatDelivery = isDirectUserChat && !input.error && hasSuccessfulChatDelivery(toolEvents)
|
|
996
1322
|
const selectedSkillNote = summarizeUseSkillToolEvent(toolEvents)
|
|
997
1323
|
if (selectedSkillNote) appendWorkingMemory(state, selectedSkillNote)
|
|
998
|
-
state.metaMissCount = heartbeat || plan || review || gotTerminalAck ? 0 : state.metaMissCount + 1
|
|
1324
|
+
state.metaMissCount = autonomyTick || heartbeat || plan || review || gotTerminalAck ? 0 : state.metaMissCount + 1
|
|
999
1325
|
const skillQuery = cleanText(state.nextAction || input.message || state.goal, 240)
|
|
1000
1326
|
let skillBlocker = deriveSkillBlockerFromToolEvents({
|
|
1001
1327
|
toolEvents,
|
|
@@ -1011,6 +1337,9 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1011
1337
|
skillBlocker = null
|
|
1012
1338
|
}
|
|
1013
1339
|
state.skillBlocker = skillBlocker
|
|
1340
|
+
if (!autonomyTick) {
|
|
1341
|
+
applyDurableObjectiveOverlay(input.sessionId, state, session || null)
|
|
1342
|
+
}
|
|
1014
1343
|
|
|
1015
1344
|
if (input.internal) {
|
|
1016
1345
|
state.pendingEvents = []
|
|
@@ -1046,7 +1375,9 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1046
1375
|
state.paused = false
|
|
1047
1376
|
}
|
|
1048
1377
|
|
|
1049
|
-
const needsReplan =
|
|
1378
|
+
const needsReplan = autonomyTick?.needsReplan === true
|
|
1379
|
+
|| review?.needs_replan === true
|
|
1380
|
+
|| ((autonomyTick?.reviewConfidence ?? review?.confidence ?? 1) < 0.45)
|
|
1050
1381
|
const limit = followupLimit(input.sessionId)
|
|
1051
1382
|
|
|
1052
1383
|
let followup: MainLoopFollowupRequest | null = null
|
|
@@ -1063,7 +1394,7 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1063
1394
|
}
|
|
1064
1395
|
} else if (!input.internal) {
|
|
1065
1396
|
state.followupChainCount = 0
|
|
1066
|
-
} else if (input.error || waitingForExternal || gotTerminalAck) {
|
|
1397
|
+
} else if (input.error || waitingForExternal || gotTerminalAck || ignoredLegacyCompatibilityPulse) {
|
|
1067
1398
|
state.followupChainCount = 0
|
|
1068
1399
|
if (gotTerminalAck && state.status !== 'blocked') state.status = 'ok'
|
|
1069
1400
|
} else {
|
|
@@ -1101,10 +1432,15 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1101
1432
|
stateMap.set(input.sessionId, finalClamped)
|
|
1102
1433
|
persistState(input.sessionId, finalClamped)
|
|
1103
1434
|
|
|
1104
|
-
// Project orchestrator state into
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1435
|
+
// Project orchestrator state into RunContext only when the objective source is already durable.
|
|
1436
|
+
const shouldSyncDurableRunContext = finalClamped.objectiveSource === 'goal'
|
|
1437
|
+
|| finalClamped.objectiveSource === 'working_state'
|
|
1438
|
+
|| finalClamped.objectiveSource === 'run_context'
|
|
1439
|
+
if (shouldSyncDurableRunContext) {
|
|
1440
|
+
try {
|
|
1441
|
+
syncMainLoopToRunContext(input.sessionId, finalClamped)
|
|
1442
|
+
} catch { /* non-critical — main loop continues even if sync fails */ }
|
|
1443
|
+
}
|
|
1108
1444
|
|
|
1109
1445
|
return followup
|
|
1110
1446
|
}
|
|
@@ -89,14 +89,19 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
89
89
|
return preflight.terminalResult
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
let streamResult: Awaited<ReturnType<typeof executePreparedChatTurn>>
|
|
93
|
+
try {
|
|
94
|
+
streamResult = await executePreparedChatTurn({
|
|
95
|
+
input,
|
|
96
|
+
prepared: preparedTurn,
|
|
97
|
+
partialPersistence,
|
|
98
|
+
preflightToolRoutingResult: preflight?.directMemoryResult || null,
|
|
99
|
+
})
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
await partialPersistence.awaitIdle()
|
|
102
|
+
} finally {
|
|
103
|
+
partialPersistence.stop()
|
|
104
|
+
}
|
|
100
105
|
|
|
101
106
|
if (!streamResult.errorMessage) {
|
|
102
107
|
markProviderSuccess(preparedTurn.providerType, preparedTurn.sessionForRun.credentialId)
|