@swarmclawai/swarmclaw 1.2.4 → 1.2.5
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 +14 -0
- package/bin/daemon-cmd.js +169 -0
- package/bin/server-cmd.js +3 -0
- package/bin/swarmclaw.js +11 -0
- package/package.json +17 -16
- package/src/app/api/agents/[id]/clone/route.ts +3 -32
- package/src/app/api/agents/[id]/route.ts +6 -158
- package/src/app/api/agents/[id]/status/route.ts +2 -3
- package/src/app/api/agents/[id]/thread/route.ts +4 -17
- package/src/app/api/agents/bulk/route.ts +5 -47
- package/src/app/api/agents/route.ts +5 -119
- package/src/app/api/agents/trash/route.ts +13 -24
- package/src/app/api/auth/route.ts +3 -9
- package/src/app/api/autonomy/estop/route.ts +5 -5
- package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
- package/src/app/api/chatrooms/[id]/route.ts +23 -2
- package/src/app/api/chatrooms/route.ts +13 -2
- package/src/app/api/chats/[id]/clear/route.ts +2 -13
- package/src/app/api/chats/[id]/deploy/route.ts +2 -3
- package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
- package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
- package/src/app/api/chats/[id]/queue/route.ts +17 -64
- package/src/app/api/chats/[id]/retry/route.ts +4 -22
- package/src/app/api/chats/[id]/route.ts +10 -138
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/migrate-messages/route.ts +7 -0
- package/src/app/api/chats/route.ts +13 -134
- package/src/app/api/connectors/[id]/access/route.ts +12 -229
- package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
- package/src/app/api/connectors/[id]/health/route.ts +12 -39
- package/src/app/api/connectors/[id]/route.ts +14 -122
- package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
- package/src/app/api/connectors/doctor/route.ts +1 -1
- package/src/app/api/connectors/route.ts +12 -70
- package/src/app/api/credentials/[id]/route.ts +2 -4
- package/src/app/api/credentials/route.ts +10 -19
- package/src/app/api/daemon/health-check/route.ts +3 -4
- package/src/app/api/daemon/route.ts +10 -8
- package/src/app/api/documents/route.ts +11 -10
- package/src/app/api/external-agents/route.ts +3 -3
- package/src/app/api/gateways/[id]/health/route.ts +2 -3
- package/src/app/api/gateways/[id]/route.ts +7 -122
- package/src/app/api/gateways/route.ts +3 -103
- package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
- package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
- package/src/app/api/openclaw/directory/route.ts +2 -2
- package/src/app/api/openclaw/history/route.ts +3 -5
- package/src/app/api/providers/[id]/route.test.ts +49 -0
- package/src/app/api/providers/ollama/route.ts +6 -5
- package/src/app/api/schedules/[id]/route.ts +14 -108
- package/src/app/api/schedules/[id]/run/route.ts +6 -67
- package/src/app/api/schedules/route.ts +9 -51
- package/src/app/api/settings/route.ts +4 -3
- package/src/app/api/setup/check-provider/route.ts +15 -1
- package/src/app/api/setup/openclaw-device/route.ts +2 -2
- package/src/app/api/system/status/route.ts +2 -2
- package/src/app/api/tasks/[id]/route.ts +16 -202
- package/src/app/api/tasks/bulk/route.ts +5 -86
- package/src/app/api/tasks/metrics/route.ts +2 -1
- package/src/app/api/tasks/route.ts +11 -171
- package/src/app/api/upload/route.ts +1 -1
- package/src/app/api/uploads/[filename]/route.ts +1 -1
- package/src/app/api/uploads/route.ts +1 -1
- package/src/app/api/webhooks/[id]/history/route.ts +2 -2
- package/src/app/layout.tsx +9 -6
- package/src/app/protocols/page.tsx +71 -89
- package/src/app/tasks/page.tsx +32 -32
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/components/auth/setup-wizard/index.tsx +4 -4
- package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
- package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
- package/src/components/auth/setup-wizard/utils.ts +1 -1
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
- package/src/components/connectors/connector-list.tsx +26 -40
- package/src/components/connectors/connector-sheet.tsx +95 -149
- package/src/components/gateways/gateway-sheet.tsx +61 -110
- package/src/components/layout/live-query-sync.tsx +121 -0
- package/src/components/protocols/structured-session-launcher.tsx +24 -45
- package/src/components/providers/app-query-provider.tsx +17 -0
- package/src/components/providers/provider-list.tsx +60 -61
- package/src/components/providers/provider-sheet.tsx +74 -56
- package/src/components/skills/skill-list.tsx +5 -18
- package/src/components/skills/skill-sheet.tsx +21 -20
- package/src/components/skills/skills-workspace.tsx +48 -87
- package/src/components/tasks/task-card.tsx +20 -13
- package/src/components/tasks/task-column.tsx +22 -7
- package/src/components/tasks/task-list.tsx +8 -11
- package/src/components/tasks/task-sheet.tsx +111 -103
- package/src/features/agents/queries.ts +20 -0
- package/src/features/chatrooms/queries.ts +20 -0
- package/src/features/chats/queries.ts +27 -0
- package/src/features/connectors/queries.ts +145 -0
- package/src/features/credentials/queries.ts +37 -0
- package/src/features/extensions/queries.ts +26 -0
- package/src/features/external-agents/queries.ts +36 -0
- package/src/features/gateways/queries.ts +274 -0
- package/src/features/missions/queries.ts +23 -0
- package/src/features/projects/queries.ts +20 -0
- package/src/features/protocols/queries.ts +149 -0
- package/src/features/providers/queries.ts +142 -0
- package/src/features/settings/queries.ts +20 -0
- package/src/features/skills/queries.ts +182 -0
- package/src/features/tasks/queries.ts +189 -0
- package/src/hooks/use-ws.ts +3 -2
- package/src/lib/app/api-client.ts +2 -2
- package/src/lib/query/client.ts +17 -0
- package/src/lib/server/agents/agent-runtime-config.ts +1 -1
- package/src/lib/server/agents/agent-service.ts +429 -0
- package/src/lib/server/agents/agent-thread-session.ts +6 -5
- package/src/lib/server/agents/autonomy-contract.ts +1 -4
- package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
- package/src/lib/server/agents/delegation-advisory.ts +251 -0
- package/src/lib/server/agents/main-agent-loop.ts +98 -40
- package/src/lib/server/agents/subagent-runtime.ts +12 -0
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
- package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
- package/src/lib/server/build-llm.ts +7 -15
- package/src/lib/server/capability-router.test.ts +70 -1
- package/src/lib/server/capability-router.ts +24 -99
- package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
- package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
- package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
- package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
- package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
- package/src/lib/server/chat-execution/message-classifier.ts +74 -32
- package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
- package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
- package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
- package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
- package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
- package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
- package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
- package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
- package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
- package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
- package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
- package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
- package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
- package/src/lib/server/chats/chat-session-service.ts +410 -0
- package/src/lib/server/connectors/access.ts +1 -1
- package/src/lib/server/connectors/commands.ts +7 -6
- package/src/lib/server/connectors/connector-inbound.ts +14 -7
- package/src/lib/server/connectors/connector-outbound.ts +16 -11
- package/src/lib/server/connectors/connector-service.ts +453 -0
- package/src/lib/server/connectors/delivery.ts +17 -12
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
- package/src/lib/server/connectors/media.ts +1 -1
- package/src/lib/server/connectors/response-media.ts +1 -1
- package/src/lib/server/connectors/session-consolidation.ts +11 -7
- package/src/lib/server/connectors/session.ts +9 -7
- package/src/lib/server/connectors/voice-note.ts +2 -1
- package/src/lib/server/context-manager.ts +20 -1
- package/src/lib/server/cost.ts +2 -3
- package/src/lib/server/credentials/credential-repository.ts +43 -4
- package/src/lib/server/credentials/credential-service.ts +112 -0
- package/src/lib/server/daemon/admin-metadata.ts +64 -0
- package/src/lib/server/daemon/controller.ts +577 -0
- package/src/lib/server/daemon/daemon-runtime.ts +352 -0
- package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
- package/src/lib/server/daemon/types.ts +101 -0
- package/src/lib/server/embeddings.ts +3 -9
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -2
- package/src/lib/server/execution-brief.test.ts +167 -0
- package/src/lib/server/execution-brief.ts +295 -0
- package/src/lib/server/execution-engine/chat-turn.ts +9 -0
- package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
- package/src/lib/server/execution-engine/index.ts +35 -0
- package/src/lib/server/execution-engine/task-attempt.ts +303 -0
- package/src/lib/server/execution-engine/types.ts +33 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
- package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
- package/src/lib/server/memory/session-archive-memory.ts +12 -10
- package/src/lib/server/messages/message-repository.ts +330 -0
- package/src/lib/server/missions/mission-service/core.ts +8 -6
- package/src/lib/server/openclaw/agent-resolver.ts +2 -3
- package/src/lib/server/openclaw/doctor.ts +1 -1
- package/src/lib/server/openclaw/gateway.test.ts +10 -1
- package/src/lib/server/openclaw/gateway.ts +5 -14
- package/src/lib/server/openclaw/health.ts +3 -11
- package/src/lib/server/openclaw/sync.ts +8 -6
- package/src/lib/server/persistence/storage-context.ts +3 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
- package/src/lib/server/protocols/protocol-normalization.ts +1 -1
- package/src/lib/server/protocols/protocol-queries.ts +13 -7
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
- package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
- package/src/lib/server/protocols/protocol-swarm.ts +8 -8
- package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
- package/src/lib/server/protocols/protocol-templates.ts +4 -2
- package/src/lib/server/protocols/protocol-types.ts +10 -7
- package/src/lib/server/provider-endpoint.ts +7 -12
- package/src/lib/server/provider-model-discovery.ts +2 -11
- package/src/lib/server/query-expansion.ts +5 -6
- package/src/lib/server/run-context.test.ts +365 -0
- package/src/lib/server/run-context.ts +367 -0
- package/src/lib/server/runtime/heartbeat-service.ts +7 -5
- package/src/lib/server/runtime/queue/core.ts +61 -190
- package/src/lib/server/runtime/run-ledger.ts +8 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
- package/src/lib/server/schedules/schedule-route-service.ts +230 -0
- package/src/lib/server/service-result.ts +16 -0
- package/src/lib/server/session-note.ts +2 -3
- package/src/lib/server/session-reset-policy.ts +4 -3
- package/src/lib/server/session-tools/connector.ts +9 -6
- package/src/lib/server/session-tools/context-mgmt.ts +58 -9
- package/src/lib/server/session-tools/crud.ts +162 -10
- package/src/lib/server/session-tools/delegate.ts +1 -1
- package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
- package/src/lib/server/session-tools/memory.ts +6 -4
- package/src/lib/server/session-tools/session-info.test.ts +56 -0
- package/src/lib/server/session-tools/session-info.ts +119 -12
- package/src/lib/server/session-tools/skill-runtime.ts +3 -1
- package/src/lib/server/session-tools/skills.ts +15 -15
- package/src/lib/server/session-tools/subagent.test.ts +115 -1
- package/src/lib/server/session-tools/subagent.ts +125 -7
- package/src/lib/server/session-tools/team-context.ts +4 -3
- package/src/lib/server/session-tools/wallet.ts +0 -58
- package/src/lib/server/sessions/session-lineage.ts +55 -0
- package/src/lib/server/sessions/session-repository.ts +2 -2
- package/src/lib/server/skills/learned-skills.ts +24 -23
- package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
- package/src/lib/server/skills/skill-repository.ts +136 -13
- package/src/lib/server/skills/skill-suggestions.ts +25 -28
- package/src/lib/server/storage-normalization.test.ts +44 -267
- package/src/lib/server/storage-normalization.ts +75 -0
- package/src/lib/server/storage.ts +19 -0
- package/src/lib/server/structured-extract.ts +3 -14
- package/src/lib/server/tasks/task-followups.ts +16 -11
- package/src/lib/server/tasks/task-result.test.ts +25 -29
- package/src/lib/server/tasks/task-result.ts +5 -9
- package/src/lib/server/tasks/task-route-service.ts +449 -0
- package/src/lib/server/text-normalization.ts +41 -0
- package/src/lib/server/tool-planning.ts +6 -42
- package/src/lib/server/upload-path.ts +5 -0
- package/src/lib/server/working-state/extraction.ts +614 -0
- package/src/lib/server/working-state/normalization.ts +866 -0
- package/src/lib/server/working-state/prompt.ts +60 -0
- package/src/lib/server/working-state/repository.ts +38 -0
- package/src/lib/server/working-state/service.test.ts +253 -0
- package/src/lib/server/working-state/service.ts +293 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/ws-client.ts +3 -3
- package/src/stores/slices/task-slice.ts +1 -4
- package/src/stores/use-chatroom-store.ts +2 -2
- package/src/types/index.ts +277 -12
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { after, before, describe, it } from 'node:test'
|
|
6
|
+
import type { Agent } from '@/types'
|
|
7
|
+
import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
|
|
8
|
+
|
|
9
|
+
const originalEnv = {
|
|
10
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
11
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
12
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let tempDir = ''
|
|
16
|
+
let advisory: typeof import('@/lib/server/agents/delegation-advisory')
|
|
17
|
+
let storage: typeof import('@/lib/server/storage')
|
|
18
|
+
|
|
19
|
+
before(async () => {
|
|
20
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-delegation-advisory-'))
|
|
21
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
22
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
23
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
24
|
+
fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
|
|
25
|
+
fs.mkdirSync(process.env.WORKSPACE_DIR, { recursive: true })
|
|
26
|
+
|
|
27
|
+
advisory = await import('@/lib/server/agents/delegation-advisory')
|
|
28
|
+
storage = await import('@/lib/server/storage')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
after(() => {
|
|
32
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
33
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
34
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
35
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
36
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
37
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
38
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
function makeAgent(params: Partial<Agent> & Pick<Agent, 'id' | 'name'>): Agent {
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
return {
|
|
44
|
+
id: params.id,
|
|
45
|
+
name: params.name,
|
|
46
|
+
role: params.role || 'worker',
|
|
47
|
+
description: params.description || '',
|
|
48
|
+
systemPrompt: params.systemPrompt || '',
|
|
49
|
+
provider: params.provider || 'openai',
|
|
50
|
+
model: params.model || 'gpt-test',
|
|
51
|
+
capabilities: params.capabilities || [],
|
|
52
|
+
delegationEnabled: params.delegationEnabled ?? false,
|
|
53
|
+
delegationTargetMode: params.delegationTargetMode || 'all',
|
|
54
|
+
delegationTargetAgentIds: params.delegationTargetAgentIds || [],
|
|
55
|
+
createdAt: params.createdAt || now,
|
|
56
|
+
updatedAt: params.updatedAt || now,
|
|
57
|
+
} as Agent
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeClassification(overrides: Partial<MessageClassification>): MessageClassification {
|
|
61
|
+
return {
|
|
62
|
+
taskIntent: 'general',
|
|
63
|
+
isDeliverableTask: false,
|
|
64
|
+
isBroadGoal: false,
|
|
65
|
+
walletIntent: 'none',
|
|
66
|
+
hasHumanSignals: false,
|
|
67
|
+
hasSignificantEvent: false,
|
|
68
|
+
isResearchSynthesis: false,
|
|
69
|
+
workType: 'general',
|
|
70
|
+
wantsScreenshots: false,
|
|
71
|
+
wantsOutboundDelivery: false,
|
|
72
|
+
wantsVoiceDelivery: false,
|
|
73
|
+
explicitToolRequests: [],
|
|
74
|
+
confidence: 0.9,
|
|
75
|
+
...overrides,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function saveAgents(agents: Agent[]): Record<string, Agent> {
|
|
80
|
+
const record = Object.fromEntries(agents.map((agent) => [agent.id, agent]))
|
|
81
|
+
storage.saveAgents(record)
|
|
82
|
+
storage.saveTasks({})
|
|
83
|
+
storage.saveSessions({})
|
|
84
|
+
return record
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('delegation-advisory', () => {
|
|
88
|
+
it('prefers a builder over a coordinator for coding work', () => {
|
|
89
|
+
const agents = saveAgents([
|
|
90
|
+
makeAgent({
|
|
91
|
+
id: 'ceo',
|
|
92
|
+
name: 'CEO',
|
|
93
|
+
role: 'coordinator',
|
|
94
|
+
capabilities: ['coordination', 'delegation', 'operations'],
|
|
95
|
+
delegationEnabled: true,
|
|
96
|
+
}),
|
|
97
|
+
makeAgent({
|
|
98
|
+
id: 'builder',
|
|
99
|
+
name: 'Builder',
|
|
100
|
+
role: 'worker',
|
|
101
|
+
capabilities: ['coding', 'implementation', 'debugging'],
|
|
102
|
+
}),
|
|
103
|
+
makeAgent({
|
|
104
|
+
id: 'writer',
|
|
105
|
+
name: 'Writer',
|
|
106
|
+
role: 'worker',
|
|
107
|
+
capabilities: ['writing', 'editing'],
|
|
108
|
+
}),
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
const profile = advisory.buildDelegationTaskProfile({
|
|
112
|
+
classification: makeClassification({
|
|
113
|
+
isDeliverableTask: true,
|
|
114
|
+
workType: 'coding',
|
|
115
|
+
}),
|
|
116
|
+
})
|
|
117
|
+
const result = advisory.resolveDelegationAdvisory({
|
|
118
|
+
currentAgent: agents.ceo,
|
|
119
|
+
agents,
|
|
120
|
+
profile,
|
|
121
|
+
delegationTargetMode: 'all',
|
|
122
|
+
delegationTargetAgentIds: [],
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
assert.equal(result.shouldDelegate, true)
|
|
126
|
+
assert.equal(result.style, 'managerial')
|
|
127
|
+
assert.equal(result.recommended?.agentId, 'builder')
|
|
128
|
+
assert.match(advisory.formatDelegationRationale(result.recommended), /coding/i)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('prefers a researcher for research work', () => {
|
|
132
|
+
const agents = saveAgents([
|
|
133
|
+
makeAgent({
|
|
134
|
+
id: 'ceo',
|
|
135
|
+
name: 'CEO',
|
|
136
|
+
role: 'coordinator',
|
|
137
|
+
capabilities: ['coordination', 'delegation', 'operations'],
|
|
138
|
+
delegationEnabled: true,
|
|
139
|
+
}),
|
|
140
|
+
makeAgent({
|
|
141
|
+
id: 'builder',
|
|
142
|
+
name: 'Builder',
|
|
143
|
+
role: 'worker',
|
|
144
|
+
capabilities: ['coding', 'implementation', 'debugging'],
|
|
145
|
+
}),
|
|
146
|
+
makeAgent({
|
|
147
|
+
id: 'researcher',
|
|
148
|
+
name: 'Researcher',
|
|
149
|
+
role: 'worker',
|
|
150
|
+
capabilities: ['research', 'analysis', 'summarization'],
|
|
151
|
+
}),
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
const profile = advisory.buildDelegationTaskProfile({
|
|
155
|
+
classification: makeClassification({
|
|
156
|
+
isResearchSynthesis: true,
|
|
157
|
+
workType: 'research',
|
|
158
|
+
}),
|
|
159
|
+
})
|
|
160
|
+
const result = advisory.resolveDelegationAdvisory({
|
|
161
|
+
currentAgent: agents.ceo,
|
|
162
|
+
agents,
|
|
163
|
+
profile,
|
|
164
|
+
delegationTargetMode: 'all',
|
|
165
|
+
delegationTargetAgentIds: [],
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
assert.equal(result.shouldDelegate, true)
|
|
169
|
+
assert.equal(result.recommended?.agentId, 'researcher')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('does not advise delegation when self is already as capable as peers', () => {
|
|
173
|
+
const agents = saveAgents([
|
|
174
|
+
makeAgent({
|
|
175
|
+
id: 'builder-a',
|
|
176
|
+
name: 'Builder A',
|
|
177
|
+
role: 'worker',
|
|
178
|
+
capabilities: ['coding', 'implementation', 'debugging'],
|
|
179
|
+
delegationEnabled: true,
|
|
180
|
+
}),
|
|
181
|
+
makeAgent({
|
|
182
|
+
id: 'builder-b',
|
|
183
|
+
name: 'Builder B',
|
|
184
|
+
role: 'worker',
|
|
185
|
+
capabilities: ['coding', 'implementation', 'debugging'],
|
|
186
|
+
}),
|
|
187
|
+
])
|
|
188
|
+
|
|
189
|
+
const profile = advisory.buildDelegationTaskProfile({
|
|
190
|
+
classification: makeClassification({
|
|
191
|
+
isDeliverableTask: true,
|
|
192
|
+
workType: 'coding',
|
|
193
|
+
}),
|
|
194
|
+
})
|
|
195
|
+
const result = advisory.resolveDelegationAdvisory({
|
|
196
|
+
currentAgent: agents['builder-a'],
|
|
197
|
+
agents,
|
|
198
|
+
profile,
|
|
199
|
+
delegationTargetMode: 'all',
|
|
200
|
+
delegationTargetAgentIds: [],
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
assert.equal(result.recommended?.agentId, 'builder-b')
|
|
204
|
+
assert.equal(result.shouldDelegate, false)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Agent } from '@/types'
|
|
2
|
+
import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
|
|
3
|
+
import { capabilityMatchScore } from '@/lib/server/agents/capability-match'
|
|
4
|
+
import { getAgentDirectory, type AgentDirectoryEntry } from '@/lib/server/agents/agent-registry'
|
|
5
|
+
|
|
6
|
+
export type DelegationWorkType =
|
|
7
|
+
| 'coding'
|
|
8
|
+
| 'research'
|
|
9
|
+
| 'writing'
|
|
10
|
+
| 'review'
|
|
11
|
+
| 'operations'
|
|
12
|
+
| 'general'
|
|
13
|
+
|
|
14
|
+
export interface DelegationTaskProfile {
|
|
15
|
+
workType: DelegationWorkType
|
|
16
|
+
requiredCapabilities: string[]
|
|
17
|
+
substantial: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DelegationCandidateFit {
|
|
21
|
+
agentId: string
|
|
22
|
+
agentName: string
|
|
23
|
+
score: number
|
|
24
|
+
availability: 'idle' | 'working' | 'unknown'
|
|
25
|
+
matchedCapabilities: string[]
|
|
26
|
+
reasons: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DelegationAdvisory {
|
|
30
|
+
profile: DelegationTaskProfile
|
|
31
|
+
current: DelegationCandidateFit | null
|
|
32
|
+
recommended: DelegationCandidateFit | null
|
|
33
|
+
shouldDelegate: boolean
|
|
34
|
+
style: 'managerial' | 'advisory'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const WORK_TYPE_CAPABILITIES: Record<DelegationWorkType, string[]> = {
|
|
38
|
+
coding: ['coding', 'implementation', 'debugging'],
|
|
39
|
+
research: ['research', 'analysis', 'summarization'],
|
|
40
|
+
writing: ['writing', 'messaging', 'structuring', 'editing'],
|
|
41
|
+
review: ['review', 'testing', 'risk assessment'],
|
|
42
|
+
operations: ['coordination', 'delegation', 'operations'],
|
|
43
|
+
general: [],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeCapabilityList(value: string[] | undefined | null): string[] {
|
|
47
|
+
if (!Array.isArray(value)) return []
|
|
48
|
+
const seen = new Set<string>()
|
|
49
|
+
const out: string[] = []
|
|
50
|
+
for (const entry of value) {
|
|
51
|
+
const trimmed = typeof entry === 'string' ? entry.trim() : ''
|
|
52
|
+
const key = trimmed.toLowerCase()
|
|
53
|
+
if (!trimmed || seen.has(key)) continue
|
|
54
|
+
seen.add(key)
|
|
55
|
+
out.push(trimmed)
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeWorkType(value: unknown): DelegationWorkType {
|
|
61
|
+
if (
|
|
62
|
+
value === 'coding'
|
|
63
|
+
|| value === 'research'
|
|
64
|
+
|| value === 'writing'
|
|
65
|
+
|| value === 'review'
|
|
66
|
+
|| value === 'operations'
|
|
67
|
+
) {
|
|
68
|
+
return value
|
|
69
|
+
}
|
|
70
|
+
return 'general'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function matchedCapabilities(agentCapabilities: string[] | undefined, requiredCapabilities: string[]): string[] {
|
|
74
|
+
if (!requiredCapabilities.length || !Array.isArray(agentCapabilities) || !agentCapabilities.length) return []
|
|
75
|
+
const agentSet = new Set(agentCapabilities.map((entry) => entry.toLowerCase()))
|
|
76
|
+
return requiredCapabilities.filter((entry) => agentSet.has(entry.toLowerCase()))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function roleAdjustment(agent: Agent, profile: DelegationTaskProfile): number {
|
|
80
|
+
const role = agent.role === 'coordinator' ? 'coordinator' : 'worker'
|
|
81
|
+
if (profile.workType === 'operations') {
|
|
82
|
+
return role === 'coordinator' ? 0.28 : -0.04
|
|
83
|
+
}
|
|
84
|
+
if (profile.workType === 'general') {
|
|
85
|
+
return role === 'coordinator' ? -0.03 : 0
|
|
86
|
+
}
|
|
87
|
+
return role === 'coordinator' ? -0.18 : 0.16
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function selfExecutionPenalty(agent: Agent, profile: DelegationTaskProfile, isSelf: boolean): number {
|
|
91
|
+
if (!isSelf) return 0
|
|
92
|
+
if (agent.role !== 'coordinator') return 0
|
|
93
|
+
if (!profile.substantial) return 0
|
|
94
|
+
if (profile.workType === 'operations') return 0
|
|
95
|
+
return -0.42
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function availabilityAdjustment(
|
|
99
|
+
availability: DelegationCandidateFit['availability'],
|
|
100
|
+
isSelf: boolean,
|
|
101
|
+
directoryEntry?: AgentDirectoryEntry,
|
|
102
|
+
): number {
|
|
103
|
+
if (availability === 'idle') return 0.08
|
|
104
|
+
if (availability === 'working') {
|
|
105
|
+
// The current live chat session should not count as a self-load penalty.
|
|
106
|
+
if (isSelf && !directoryEntry?.statusDetail) return 0.08
|
|
107
|
+
return -0.08
|
|
108
|
+
}
|
|
109
|
+
return 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildAvailabilityMap(): Map<string, AgentDirectoryEntry> {
|
|
113
|
+
return new Map(getAgentDirectory().map((entry) => [entry.id, entry]))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildCandidateFit(
|
|
117
|
+
agent: Agent,
|
|
118
|
+
profile: DelegationTaskProfile,
|
|
119
|
+
directory: Map<string, AgentDirectoryEntry>,
|
|
120
|
+
isSelf = false,
|
|
121
|
+
): DelegationCandidateFit {
|
|
122
|
+
const directoryEntry = directory.get(agent.id)
|
|
123
|
+
const availability = directoryEntry?.status || 'unknown'
|
|
124
|
+
const matched = matchedCapabilities(agent.capabilities, profile.requiredCapabilities)
|
|
125
|
+
const capabilityScore = profile.requiredCapabilities.length > 0
|
|
126
|
+
? capabilityMatchScore(agent.capabilities, profile.requiredCapabilities) * 1.45
|
|
127
|
+
: 0
|
|
128
|
+
const score = capabilityScore
|
|
129
|
+
+ roleAdjustment(agent, profile)
|
|
130
|
+
+ availabilityAdjustment(availability, isSelf, directoryEntry)
|
|
131
|
+
+ selfExecutionPenalty(agent, profile, isSelf)
|
|
132
|
+
|
|
133
|
+
const reasons: string[] = []
|
|
134
|
+
if (matched.length > 0) reasons.push(`capability match: ${matched.join(', ')}`)
|
|
135
|
+
if (profile.workType === 'operations' && agent.role === 'coordinator') reasons.push('coordinator role fits operations work')
|
|
136
|
+
if (profile.workType !== 'operations' && profile.workType !== 'general' && agent.role !== 'coordinator') reasons.push('worker role fits execution-heavy work')
|
|
137
|
+
if (availability === 'idle') reasons.push('currently idle')
|
|
138
|
+
if (availability === 'working' && directoryEntry?.statusDetail) reasons.push(directoryEntry.statusDetail)
|
|
139
|
+
if (isSelf && selfExecutionPenalty(agent, profile, true) < 0) reasons.push('coordinator should prefer orchestration over direct execution')
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
agentId: agent.id,
|
|
143
|
+
agentName: agent.name,
|
|
144
|
+
score,
|
|
145
|
+
availability,
|
|
146
|
+
matchedCapabilities: matched,
|
|
147
|
+
reasons,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isAllowedDelegateTarget(
|
|
152
|
+
agentId: string,
|
|
153
|
+
opts?: { delegationTargetMode?: 'all' | 'selected'; delegationTargetAgentIds?: string[] },
|
|
154
|
+
): boolean {
|
|
155
|
+
if (opts?.delegationTargetMode !== 'selected') return true
|
|
156
|
+
const allowed = new Set(normalizeCapabilityList(opts.delegationTargetAgentIds))
|
|
157
|
+
return allowed.size === 0 || allowed.has(agentId)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resolveDelegationWorkType(
|
|
161
|
+
classification: MessageClassification | null | undefined,
|
|
162
|
+
): DelegationWorkType {
|
|
163
|
+
return normalizeWorkType(classification?.workType)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function buildDelegationTaskProfile(params: {
|
|
167
|
+
classification?: MessageClassification | null
|
|
168
|
+
workType?: DelegationWorkType | null
|
|
169
|
+
requiredCapabilities?: string[] | null
|
|
170
|
+
}): DelegationTaskProfile {
|
|
171
|
+
const workType = params.workType
|
|
172
|
+
? normalizeWorkType(params.workType)
|
|
173
|
+
: resolveDelegationWorkType(params.classification)
|
|
174
|
+
const explicitRequirements = normalizeCapabilityList(params.requiredCapabilities)
|
|
175
|
+
const requiredCapabilities = explicitRequirements.length > 0
|
|
176
|
+
? explicitRequirements
|
|
177
|
+
: WORK_TYPE_CAPABILITIES[workType]
|
|
178
|
+
const substantial = explicitRequirements.length > 0
|
|
179
|
+
|| Boolean(params.classification?.isBroadGoal)
|
|
180
|
+
|| Boolean(params.classification?.isDeliverableTask)
|
|
181
|
+
|| Boolean(params.classification?.isResearchSynthesis)
|
|
182
|
+
|| workType !== 'general'
|
|
183
|
+
return {
|
|
184
|
+
workType,
|
|
185
|
+
requiredCapabilities,
|
|
186
|
+
substantial,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function resolveBestDelegateTarget(params: {
|
|
191
|
+
currentAgentId?: string | null
|
|
192
|
+
agents: Record<string, Agent>
|
|
193
|
+
profile: DelegationTaskProfile
|
|
194
|
+
delegationTargetMode?: 'all' | 'selected'
|
|
195
|
+
delegationTargetAgentIds?: string[]
|
|
196
|
+
}): DelegationCandidateFit | null {
|
|
197
|
+
const directory = buildAvailabilityMap()
|
|
198
|
+
const candidates = Object.values(params.agents)
|
|
199
|
+
.filter((agent) => agent.id !== params.currentAgentId)
|
|
200
|
+
.filter((agent) => !agent.disabled && !agent.trashedAt)
|
|
201
|
+
.filter((agent) => isAllowedDelegateTarget(agent.id, params))
|
|
202
|
+
.map((agent) => buildCandidateFit(agent, params.profile, directory))
|
|
203
|
+
.sort((left, right) => {
|
|
204
|
+
if (right.score !== left.score) return right.score - left.score
|
|
205
|
+
return left.agentName.localeCompare(right.agentName)
|
|
206
|
+
})
|
|
207
|
+
return candidates[0] || null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function resolveDelegationAdvisory(params: {
|
|
211
|
+
currentAgent: Agent | null | undefined
|
|
212
|
+
agents: Record<string, Agent>
|
|
213
|
+
profile: DelegationTaskProfile
|
|
214
|
+
delegationTargetMode?: 'all' | 'selected'
|
|
215
|
+
delegationTargetAgentIds?: string[]
|
|
216
|
+
}): DelegationAdvisory {
|
|
217
|
+
const directory = buildAvailabilityMap()
|
|
218
|
+
const current = params.currentAgent && !params.currentAgent.disabled && !params.currentAgent.trashedAt
|
|
219
|
+
? buildCandidateFit(params.currentAgent, params.profile, directory, true)
|
|
220
|
+
: null
|
|
221
|
+
const recommended = resolveBestDelegateTarget({
|
|
222
|
+
currentAgentId: params.currentAgent?.id || null,
|
|
223
|
+
agents: params.agents,
|
|
224
|
+
profile: params.profile,
|
|
225
|
+
delegationTargetMode: params.delegationTargetMode,
|
|
226
|
+
delegationTargetAgentIds: params.delegationTargetAgentIds,
|
|
227
|
+
})
|
|
228
|
+
const currentScore = current?.score ?? 0
|
|
229
|
+
const recommendedScore = recommended?.score ?? Number.NEGATIVE_INFINITY
|
|
230
|
+
const shouldDelegate = Boolean(
|
|
231
|
+
params.profile.substantial
|
|
232
|
+
&& recommended
|
|
233
|
+
&& recommendedScore >= currentScore + 0.3
|
|
234
|
+
&& recommendedScore >= 0.25,
|
|
235
|
+
)
|
|
236
|
+
const style = params.currentAgent?.role === 'coordinator' && params.profile.workType !== 'operations'
|
|
237
|
+
? 'managerial'
|
|
238
|
+
: 'advisory'
|
|
239
|
+
return {
|
|
240
|
+
profile: params.profile,
|
|
241
|
+
current,
|
|
242
|
+
recommended,
|
|
243
|
+
shouldDelegate,
|
|
244
|
+
style,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function formatDelegationRationale(candidate: DelegationCandidateFit | null | undefined): string {
|
|
249
|
+
if (!candidate || candidate.reasons.length === 0) return 'better fit for this work'
|
|
250
|
+
return candidate.reasons.slice(0, 2).join('; ')
|
|
251
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hmrSingleton } from '@/lib/shared-utils'
|
|
2
|
+
import { getMessages } from '@/lib/server/messages/message-repository'
|
|
2
3
|
import type { GoalContract, Message, MessageToolEvent, Session } from '@/types'
|
|
3
4
|
import { mergeGoalContracts, parseGoalContractFromText, parseMainLoopPlan, parseMainLoopReview } from '@/lib/server/agents/autonomy-contract'
|
|
4
5
|
import {
|
|
@@ -12,6 +13,10 @@ import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
|
|
|
12
13
|
import { buildMissionHeartbeatPrompt as buildMissionHeartbeatPromptFromMission, getMissionForSession } from '@/lib/server/missions/mission-service'
|
|
13
14
|
import { loadSettings } from '@/lib/server/settings/settings-repository'
|
|
14
15
|
import { getSession, loadSessions } from '@/lib/server/sessions/session-repository'
|
|
16
|
+
import { deleteSessionWorkingState, loadSessionWorkingState, syncWorkingStateFromMainLoopState } from '@/lib/server/working-state/service'
|
|
17
|
+
import { syncMainLoopToRunContext } from '@/lib/server/run-context'
|
|
18
|
+
import { buildExecutionBrief, buildExecutionBriefContextBlock } from '@/lib/server/execution-brief'
|
|
19
|
+
import { cleanText, cleanMultiline } from '@/lib/server/text-normalization'
|
|
15
20
|
|
|
16
21
|
const LEGACY_META_LINE_RE = /\[(?:MAIN_LOOP_META|MAIN_LOOP_PLAN|MAIN_LOOP_REVIEW|AGENT_HEARTBEAT_META)\]\s*(\{[^\n]*\})?/i
|
|
17
22
|
const HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
|
|
@@ -19,7 +24,7 @@ const MAX_PENDING_EVENTS = 16
|
|
|
19
24
|
const MAX_TIMELINE_ITEMS = 40
|
|
20
25
|
const MAX_WORKING_MEMORY_NOTES = 12
|
|
21
26
|
const DEFAULT_FOLLOWUP_DELAY_MS = 1500
|
|
22
|
-
const DEFAULT_MAX_FOLLOWUP_CHAIN =
|
|
27
|
+
const DEFAULT_MAX_FOLLOWUP_CHAIN = 3
|
|
23
28
|
const MAX_LIFETIME_ITERATIONS = 200
|
|
24
29
|
|
|
25
30
|
export interface MainLoopState {
|
|
@@ -111,24 +116,6 @@ function asSession(session: unknown): MainSessionLike | null {
|
|
|
111
116
|
return session as MainSessionLike
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
function cleanText(value: unknown, maxChars = 320): string | null {
|
|
115
|
-
if (typeof value !== 'string') return null
|
|
116
|
-
const normalized = value.replace(/\s+/g, ' ').trim()
|
|
117
|
-
return normalized ? normalized.slice(0, maxChars) : null
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function cleanMultiline(value: unknown, maxChars = 1400): string | null {
|
|
121
|
-
if (typeof value !== 'string') return null
|
|
122
|
-
const normalized = value
|
|
123
|
-
.split('\n')
|
|
124
|
-
.map((line) => line.trim())
|
|
125
|
-
.filter(Boolean)
|
|
126
|
-
.join('\n')
|
|
127
|
-
.slice(0, maxChars)
|
|
128
|
-
.trim()
|
|
129
|
-
return normalized || null
|
|
130
|
-
}
|
|
131
|
-
|
|
132
119
|
function normalizeConfidence(value: unknown): number | null {
|
|
133
120
|
const raw = typeof value === 'number'
|
|
134
121
|
? value
|
|
@@ -403,7 +390,7 @@ function hydrateStateFromSession(sessionId: string): MainLoopState | null {
|
|
|
403
390
|
const session = sessions[sessionId]
|
|
404
391
|
if (!session || !isMainSession(session)) return null
|
|
405
392
|
|
|
406
|
-
const messages =
|
|
393
|
+
const messages = getMessages(sessionId)
|
|
407
394
|
const hydrated = defaultState()
|
|
408
395
|
hydrated.autonomyMode = session.heartbeatEnabled === true ? 'autonomous' : 'assist'
|
|
409
396
|
hydrated.updatedAt = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : now()
|
|
@@ -440,21 +427,48 @@ function hydrateStateFromSession(sessionId: string): MainLoopState | null {
|
|
|
440
427
|
}
|
|
441
428
|
}
|
|
442
429
|
|
|
443
|
-
return normalizeState(hydrated)
|
|
430
|
+
return mergeWorkingStateIntoMainLoopState(sessionId, normalizeState(hydrated))
|
|
444
431
|
}
|
|
445
432
|
|
|
446
433
|
function persistState(sessionId: string, state: MainLoopState): void {
|
|
447
|
-
|
|
434
|
+
const normalized = clampState(state)
|
|
435
|
+
upsertPersistedMainLoopState(sessionId, normalized as unknown as Record<string, unknown>)
|
|
436
|
+
const session = getSession(sessionId)
|
|
437
|
+
if (!session) return
|
|
438
|
+
const mission = getMissionForSession(session)
|
|
439
|
+
void syncWorkingStateFromMainLoopState({
|
|
440
|
+
sessionId,
|
|
441
|
+
mission,
|
|
442
|
+
goal: normalized.goal,
|
|
443
|
+
summary: normalized.summary,
|
|
444
|
+
status: normalized.status === 'ok'
|
|
445
|
+
? 'completed'
|
|
446
|
+
: normalized.status === 'blocked'
|
|
447
|
+
? 'blocked'
|
|
448
|
+
: normalized.status === 'progress'
|
|
449
|
+
? 'progress'
|
|
450
|
+
: 'idle',
|
|
451
|
+
nextAction: normalized.nextAction,
|
|
452
|
+
planSteps: normalized.planSteps,
|
|
453
|
+
blockers: normalized.skillBlocker ? [{
|
|
454
|
+
summary: normalized.skillBlocker.summary,
|
|
455
|
+
kind: normalized.skillBlocker.status === 'approval_requested' ? 'approval' : 'other',
|
|
456
|
+
}] : undefined,
|
|
457
|
+
})
|
|
448
458
|
}
|
|
449
459
|
|
|
450
460
|
function getOrCreateState(sessionId: string): MainLoopState | null {
|
|
451
461
|
const existing = stateMap.get(sessionId)
|
|
452
|
-
if (existing)
|
|
462
|
+
if (existing) {
|
|
463
|
+
const merged = mergeWorkingStateIntoMainLoopState(sessionId, existing)
|
|
464
|
+
stateMap.set(sessionId, merged)
|
|
465
|
+
return merged
|
|
466
|
+
}
|
|
453
467
|
|
|
454
468
|
// Try disk (survives full restart)
|
|
455
469
|
const persisted = loadPersistedMainLoopState(sessionId) as Partial<MainLoopState> | null
|
|
456
470
|
if (persisted) {
|
|
457
|
-
const restored = normalizeState(persisted)
|
|
471
|
+
const restored = mergeWorkingStateIntoMainLoopState(sessionId, normalizeState(persisted))
|
|
458
472
|
stateMap.set(sessionId, restored)
|
|
459
473
|
return restored
|
|
460
474
|
}
|
|
@@ -467,6 +481,43 @@ function getOrCreateState(sessionId: string): MainLoopState | null {
|
|
|
467
481
|
return hydrated
|
|
468
482
|
}
|
|
469
483
|
|
|
484
|
+
function mergeWorkingStateIntoMainLoopState(sessionId: string, current: MainLoopState): MainLoopState {
|
|
485
|
+
const workingState = loadSessionWorkingState(sessionId)
|
|
486
|
+
if (!workingState) return clampState(current)
|
|
487
|
+
const next = normalizeState(current)
|
|
488
|
+
if (workingState.objective) next.goal = cleanMultiline(workingState.objective, 900)
|
|
489
|
+
if (workingState.summary) next.summary = cleanText(workingState.summary, 1000)
|
|
490
|
+
if (workingState.nextAction) next.nextAction = cleanText(workingState.nextAction, 240)
|
|
491
|
+
if (workingState.status === 'completed') next.status = 'ok'
|
|
492
|
+
else if (workingState.status === 'blocked' || workingState.status === 'waiting') next.status = 'blocked'
|
|
493
|
+
else if (workingState.status === 'progress') next.status = 'progress'
|
|
494
|
+
|
|
495
|
+
const planSteps = (workingState.planSteps || [])
|
|
496
|
+
.map((step) => cleanText(step.text, 240))
|
|
497
|
+
.filter((step): step is string => Boolean(step))
|
|
498
|
+
if (planSteps.length > 0) {
|
|
499
|
+
next.planSteps = uniqueStrings(planSteps, 8)
|
|
500
|
+
next.completedPlanSteps = uniqueStrings(
|
|
501
|
+
(workingState.planSteps || [])
|
|
502
|
+
.filter((step) => step.status === 'resolved')
|
|
503
|
+
.map((step) => cleanText(step.text, 240))
|
|
504
|
+
.filter((step): step is string => Boolean(step)),
|
|
505
|
+
16,
|
|
506
|
+
)
|
|
507
|
+
const activeStep = (workingState.planSteps || []).find((step) => step.status === 'active')
|
|
508
|
+
if (activeStep?.text) next.currentPlanStep = cleanText(activeStep.text, 240)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const noteCandidates = [
|
|
512
|
+
...(workingState.confirmedFacts || []).filter((item) => item.status === 'active').map((item) => `Fact: ${item.statement}`),
|
|
513
|
+
...(workingState.blockers || []).filter((item) => item.status === 'active').map((item) => `Blocker: ${item.summary}`),
|
|
514
|
+
]
|
|
515
|
+
if (noteCandidates.length > 0) {
|
|
516
|
+
next.workingMemoryNotes = uniqueStrings([...(next.workingMemoryNotes || []), ...noteCandidates], MAX_WORKING_MEMORY_NOTES)
|
|
517
|
+
}
|
|
518
|
+
return clampState(next)
|
|
519
|
+
}
|
|
520
|
+
|
|
470
521
|
function summarizePendingEvents(events: MainLoopState['pendingEvents']): string {
|
|
471
522
|
if (!events.length) return ''
|
|
472
523
|
return events
|
|
@@ -754,39 +805,38 @@ export function buildMainLoopHeartbeatPrompt(session: unknown, fallbackPrompt: s
|
|
|
754
805
|
const state = getOrCreateState(String(candidate.id))
|
|
755
806
|
if (!state) return fallbackPrompt
|
|
756
807
|
const latestExternalGoal = extractLatestGoal(Array.isArray(candidate.messages) ? candidate.messages as Message[] : [])
|
|
757
|
-
const effectiveGoal = state.goal || latestExternalGoal.goal
|
|
758
808
|
const effectiveGoalContract = latestExternalGoal.goalContract
|
|
759
809
|
? mergeGoalContracts(state.goalContract, latestExternalGoal.goalContract)
|
|
760
810
|
: state.goalContract
|
|
761
811
|
|
|
762
|
-
const
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
: ''
|
|
812
|
+
const heartbeatSession = (persistedSession || candidate as Session)
|
|
813
|
+
const executionBrief = buildExecutionBrief({
|
|
814
|
+
session: heartbeatSession,
|
|
815
|
+
mission: getMissionForSession(heartbeatSession),
|
|
816
|
+
})
|
|
817
|
+
const executionBriefBlock = buildExecutionBriefContextBlock(executionBrief)
|
|
769
818
|
const boundedFallbackPrompt = cleanMultiline(fallbackPrompt, 500)
|
|
770
|
-
const
|
|
819
|
+
const workingState = loadSessionWorkingState(String(candidate.id))
|
|
820
|
+
const activeWorkingBlockers = (workingState?.blockers || [])
|
|
821
|
+
.filter((item) => item.status === 'active')
|
|
822
|
+
.map((item) => item.nextAction ? `${item.summary} | next: ${item.nextAction}` : item.summary)
|
|
823
|
+
.slice(0, 4)
|
|
824
|
+
.join('\n')
|
|
771
825
|
|
|
772
826
|
return [
|
|
773
827
|
'MAIN_AGENT_HEARTBEAT_TICK',
|
|
774
828
|
`Time: ${new Date().toISOString()}`,
|
|
775
|
-
|
|
829
|
+
executionBriefBlock,
|
|
776
830
|
formatGoalContract(effectiveGoalContract),
|
|
777
|
-
`Current status: ${state.status}`,
|
|
778
|
-
state.nextAction ? `Planned next action: ${state.nextAction}` : '',
|
|
779
|
-
state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
|
|
780
|
-
planLines ? `Plan:\n${planLines}` : '',
|
|
781
831
|
state.pendingEvents.length > 0 ? `Pending external events:\n${summarizePendingEvents(state.pendingEvents)}` : '',
|
|
832
|
+
activeWorkingBlockers ? `Active blockers:\n${activeWorkingBlockers}` : '',
|
|
782
833
|
state.skillBlocker ? `Active skill blocker:\n${summarizeSkillBlocker(state.skillBlocker)}` : '',
|
|
783
834
|
summarizeSelectedSkillRuntime(candidate),
|
|
784
|
-
boundedSummary ? `Latest summary:\n${boundedSummary}` : '',
|
|
785
835
|
boundedFallbackPrompt ? `Base heartbeat instructions:\n${boundedFallbackPrompt}` : '',
|
|
786
836
|
'',
|
|
787
837
|
'You are checking the durable main mission thread for this agent.',
|
|
788
838
|
'Keep this status check brief — 5-10 tool calls maximum. Read key state, summarize progress, and report. Do not attempt fixes or deep investigation during heartbeats.',
|
|
789
|
-
'Use
|
|
839
|
+
'Use the execution brief and pending external events shown above as the authoritative state for this tick.',
|
|
790
840
|
'Do not infer or repeat old tasks from prior heartbeats.',
|
|
791
841
|
'Prefer taking the single highest-value next step over restating the plan. Do not repeat completed work.',
|
|
792
842
|
'If you revise the plan, emit exactly one line like:',
|
|
@@ -827,6 +877,7 @@ export function getMainLoopStateForSession(sessionId: string): MainLoopState | n
|
|
|
827
877
|
|
|
828
878
|
export function clearMainLoopStateForSession(sessionId: string): boolean {
|
|
829
879
|
deletePersistedMainLoopState(sessionId)
|
|
880
|
+
deleteSessionWorkingState(sessionId)
|
|
830
881
|
return stateMap.delete(sessionId)
|
|
831
882
|
}
|
|
832
883
|
|
|
@@ -840,6 +891,7 @@ export function pruneMainLoopState(liveSessionIds: Set<string>): number {
|
|
|
840
891
|
if (!liveSessionIds.has(sessionId)) {
|
|
841
892
|
stateMap.delete(sessionId)
|
|
842
893
|
deletePersistedMainLoopState(sessionId)
|
|
894
|
+
deleteSessionWorkingState(sessionId)
|
|
843
895
|
removed++
|
|
844
896
|
}
|
|
845
897
|
}
|
|
@@ -1096,5 +1148,11 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1096
1148
|
const finalClamped = clampState(state)
|
|
1097
1149
|
stateMap.set(input.sessionId, finalClamped)
|
|
1098
1150
|
persistState(input.sessionId, finalClamped)
|
|
1151
|
+
|
|
1152
|
+
// Project orchestrator state into session RunContext (non-critical)
|
|
1153
|
+
try {
|
|
1154
|
+
syncMainLoopToRunContext(input.sessionId, finalClamped)
|
|
1155
|
+
} catch { /* non-critical — main loop continues even if sync fails */ }
|
|
1156
|
+
|
|
1099
1157
|
return followup
|
|
1100
1158
|
}
|