@swarmclawai/swarmclaw 1.2.4 → 1.2.6

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.
Files changed (262) hide show
  1. package/README.md +14 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +23 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/providers/index.test.ts +108 -0
  109. package/src/lib/providers/index.ts +38 -15
  110. package/src/lib/query/client.ts +17 -0
  111. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  112. package/src/lib/server/agents/agent-service.ts +429 -0
  113. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  114. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  115. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  116. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  117. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  118. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  119. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  120. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  121. package/src/lib/server/build-llm.ts +7 -15
  122. package/src/lib/server/capability-router.test.ts +70 -1
  123. package/src/lib/server/capability-router.ts +24 -99
  124. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  125. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  126. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  127. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  128. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  129. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  130. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  131. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  132. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  133. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  134. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  135. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  136. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  137. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  138. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  139. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  140. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  141. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  142. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  143. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  144. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  145. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  146. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  147. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  148. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  149. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  150. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  151. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  152. package/src/lib/server/chats/chat-session-service.ts +410 -0
  153. package/src/lib/server/connectors/access.ts +1 -1
  154. package/src/lib/server/connectors/commands.ts +7 -6
  155. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  156. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  157. package/src/lib/server/connectors/connector-service.ts +453 -0
  158. package/src/lib/server/connectors/delivery.ts +17 -12
  159. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  160. package/src/lib/server/connectors/media.ts +1 -1
  161. package/src/lib/server/connectors/response-media.ts +1 -1
  162. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  163. package/src/lib/server/connectors/session.ts +9 -7
  164. package/src/lib/server/connectors/voice-note.ts +2 -1
  165. package/src/lib/server/context-manager.ts +20 -1
  166. package/src/lib/server/cost.ts +2 -3
  167. package/src/lib/server/credentials/credential-repository.ts +43 -4
  168. package/src/lib/server/credentials/credential-service.ts +112 -0
  169. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  170. package/src/lib/server/daemon/controller.ts +577 -0
  171. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  172. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  173. package/src/lib/server/daemon/types.ts +101 -0
  174. package/src/lib/server/embeddings.ts +3 -9
  175. package/src/lib/server/eval/agent-regression.ts +3 -2
  176. package/src/lib/server/eval/runner.ts +2 -2
  177. package/src/lib/server/execution-brief.test.ts +167 -0
  178. package/src/lib/server/execution-brief.ts +295 -0
  179. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  180. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  181. package/src/lib/server/execution-engine/index.ts +35 -0
  182. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  183. package/src/lib/server/execution-engine/types.ts +33 -0
  184. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  185. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  186. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  187. package/src/lib/server/messages/message-repository.ts +330 -0
  188. package/src/lib/server/missions/mission-service/core.ts +8 -6
  189. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  190. package/src/lib/server/openclaw/doctor.ts +1 -1
  191. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  192. package/src/lib/server/openclaw/gateway.ts +5 -14
  193. package/src/lib/server/openclaw/health.ts +3 -11
  194. package/src/lib/server/openclaw/sync.ts +8 -6
  195. package/src/lib/server/persistence/storage-context.ts +3 -0
  196. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  197. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  198. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  199. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  200. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  201. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  202. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  203. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  204. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  205. package/src/lib/server/protocols/protocol-types.ts +10 -7
  206. package/src/lib/server/provider-endpoint.ts +7 -12
  207. package/src/lib/server/provider-model-discovery.ts +2 -11
  208. package/src/lib/server/query-expansion.ts +5 -6
  209. package/src/lib/server/run-context.test.ts +365 -0
  210. package/src/lib/server/run-context.ts +367 -0
  211. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  212. package/src/lib/server/runtime/queue/core.ts +61 -190
  213. package/src/lib/server/runtime/run-ledger.ts +8 -0
  214. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  215. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  216. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  217. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  218. package/src/lib/server/service-result.ts +16 -0
  219. package/src/lib/server/session-note.ts +2 -3
  220. package/src/lib/server/session-reset-policy.ts +4 -3
  221. package/src/lib/server/session-tools/connector.ts +9 -6
  222. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  223. package/src/lib/server/session-tools/crud.ts +162 -10
  224. package/src/lib/server/session-tools/delegate.ts +1 -1
  225. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  226. package/src/lib/server/session-tools/memory.ts +6 -4
  227. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  228. package/src/lib/server/session-tools/session-info.ts +119 -12
  229. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  230. package/src/lib/server/session-tools/skills.ts +15 -15
  231. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  232. package/src/lib/server/session-tools/subagent.ts +125 -7
  233. package/src/lib/server/session-tools/team-context.ts +4 -3
  234. package/src/lib/server/session-tools/wallet.ts +0 -58
  235. package/src/lib/server/sessions/session-lineage.ts +55 -0
  236. package/src/lib/server/sessions/session-repository.ts +2 -2
  237. package/src/lib/server/skills/learned-skills.ts +24 -23
  238. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  239. package/src/lib/server/skills/skill-repository.ts +136 -13
  240. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  241. package/src/lib/server/storage-normalization.test.ts +44 -267
  242. package/src/lib/server/storage-normalization.ts +75 -0
  243. package/src/lib/server/storage.ts +19 -0
  244. package/src/lib/server/structured-extract.ts +3 -14
  245. package/src/lib/server/tasks/task-followups.ts +16 -11
  246. package/src/lib/server/tasks/task-result.test.ts +25 -29
  247. package/src/lib/server/tasks/task-result.ts +5 -9
  248. package/src/lib/server/tasks/task-route-service.ts +449 -0
  249. package/src/lib/server/text-normalization.ts +41 -0
  250. package/src/lib/server/tool-planning.ts +6 -42
  251. package/src/lib/server/upload-path.ts +5 -0
  252. package/src/lib/server/working-state/extraction.ts +614 -0
  253. package/src/lib/server/working-state/normalization.ts +866 -0
  254. package/src/lib/server/working-state/prompt.ts +60 -0
  255. package/src/lib/server/working-state/repository.ts +38 -0
  256. package/src/lib/server/working-state/service.test.ts +253 -0
  257. package/src/lib/server/working-state/service.ts +293 -0
  258. package/src/lib/validation/schemas.ts +1 -0
  259. package/src/lib/ws-client.ts +3 -3
  260. package/src/stores/slices/task-slice.ts +1 -4
  261. package/src/stores/use-chatroom-store.ts +2 -2
  262. package/src/types/index.ts +277 -12
@@ -4,7 +4,7 @@ import type { MessageToolEvent } from '@/types'
4
4
  import { buildLLM } from '@/lib/server/build-llm'
5
5
 
6
6
  const DirectMemoryIntentResponseSchema = z.object({
7
- action: z.enum(['none', 'store', 'update', 'recall']),
7
+ action: z.enum(['none', 'store', 'update', 'recall', 'list']),
8
8
  confidence: z.number().min(0).max(1).optional(),
9
9
  title: z.string().optional().nullable(),
10
10
  value: z.string().optional().nullable(),
@@ -18,6 +18,7 @@ export type DirectMemoryIntent =
18
18
  | { action: 'none'; confidence: number }
19
19
  | { action: 'store'; confidence: number; title?: string; value: string; acknowledgement: string; exclusiveCompletion: boolean }
20
20
  | { action: 'update'; confidence: number; title?: string; value: string; acknowledgement: string; exclusiveCompletion: boolean }
21
+ | { action: 'list'; confidence: number }
21
22
  | { action: 'recall'; confidence: number; query: string; missResponse: string }
22
23
 
23
24
  export interface DirectMemoryIntentClassifierInput {
@@ -122,6 +123,13 @@ export function parseDirectMemoryIntentResponse(text: string): DirectMemoryInten
122
123
  }
123
124
  }
124
125
 
126
+ if (parsed.data.action === 'list') {
127
+ return {
128
+ action: 'list',
129
+ confidence,
130
+ }
131
+ }
132
+
125
133
  const query = normalizeText(parsed.data.query)
126
134
  if (!query) return null
127
135
  return {
@@ -153,15 +161,17 @@ function buildDirectMemoryIntentPrompt(input: DirectMemoryIntentClassifierInput)
153
161
  'Rules:',
154
162
  '- Choose "store" when the user wants a new durable fact, preference, decision, or profile detail remembered.',
155
163
  '- Choose "update" when the user is correcting or replacing previously remembered information.',
164
+ '- Choose "list" when the user wants a broad inventory of stored memories or asks what the assistant remembers in general.',
156
165
  '- Choose "recall" when the user is asking what the assistant remembers from earlier interactions.',
157
166
  '- Choose "none" for ordinary conversation, current-thread-only questions, file/code/document work, and anything that should not touch durable memory.',
158
167
  '- Be conservative. If unsure, return {"action":"none","confidence":0}.',
159
168
  '- For "store" and "update", return the durable fact in "value" and a short natural user-facing acknowledgement in "acknowledgement". Do not mention tools, memory ids, storage, creation, or updating.',
160
169
  '- Set "exclusiveCompletion" to true only when a successful memory write fully satisfies the user turn and the assistant should stop after the acknowledgement. Set it to false when the user also asked for other work in the same turn.',
170
+ '- Choose "recall" for targeted lookups about a specific remembered fact. Choose "list" for broad inventory requests like listing memories or asking what is remembered overall.',
161
171
  '- For "recall", return a concise search query in "query" and a short natural "missResponse". Do not mention tools.',
162
172
  '',
163
173
  'Output shape:',
164
- '{"action":"none|store|update|recall","confidence":0-1,"title":"optional short title","value":"for store/update","query":"for recall","acknowledgement":"for store/update","missResponse":"for recall","exclusiveCompletion":true}',
174
+ '{"action":"none|store|update|recall|list","confidence":0-1,"title":"optional short title","value":"for store/update","query":"for recall","acknowledgement":"for store/update","missResponse":"for recall","exclusiveCompletion":true}',
165
175
  '',
166
176
  `user_message: ${JSON.stringify(message)}`,
167
177
  `assistant_response: ${JSON.stringify(currentResponse)}`,
@@ -23,12 +23,14 @@ after(() => {
23
23
 
24
24
  describe('parseClassificationResponse', () => {
25
25
  const validJson = JSON.stringify({
26
+ taskIntent: 'general',
26
27
  isDeliverableTask: true,
27
28
  isBroadGoal: false,
28
29
  walletIntent: 'none',
29
30
  hasHumanSignals: false,
30
31
  hasSignificantEvent: false,
31
32
  isResearchSynthesis: false,
33
+ workType: 'general',
32
34
  explicitToolRequests: [],
33
35
  confidence: 0.9,
34
36
  })
@@ -39,6 +41,8 @@ describe('parseClassificationResponse', () => {
39
41
  assert.equal(result!.isDeliverableTask, true)
40
42
  assert.equal(result!.isBroadGoal, false)
41
43
  assert.equal(result!.walletIntent, 'none')
44
+ assert.equal(result!.taskIntent, 'general')
45
+ assert.equal(result!.workType, 'general')
42
46
  assert.equal(result!.confidence, 0.9)
43
47
  assert.deepEqual(result!.explicitToolRequests, [])
44
48
  })
@@ -55,12 +59,14 @@ describe('parseClassificationResponse', () => {
55
59
 
56
60
  it('tolerates extra keys in JSON', () => {
57
61
  const withExtra = JSON.stringify({
62
+ taskIntent: 'general',
58
63
  isDeliverableTask: true,
59
64
  isBroadGoal: false,
60
65
  walletIntent: 'none',
61
66
  hasHumanSignals: false,
62
67
  hasSignificantEvent: false,
63
68
  isResearchSynthesis: false,
69
+ workType: 'general',
64
70
  explicitToolRequests: ['shell'],
65
71
  confidence: 0.85,
66
72
  extraKey: 'should be ignored',
@@ -97,9 +103,10 @@ describe('isDeliverableTask', () => {
97
103
  })
98
104
 
99
105
  it('falls back to regex when classification is null', () => {
100
- // A message that looks like a deliverable task
101
- const result = mod.isDeliverableTask(null, 'Create a detailed marketing report with competitor analysis and market sizing. Include charts and recommendations for Q3 strategy across all regions.')
102
- assert.equal(typeof result, 'boolean')
106
+ assert.equal(
107
+ mod.isDeliverableTask(null, 'Create a detailed marketing report with competitor analysis and market sizing.'),
108
+ false,
109
+ )
103
110
  })
104
111
  })
105
112
 
@@ -114,8 +121,10 @@ describe('isBroadGoal', () => {
114
121
  })
115
122
 
116
123
  it('falls back to regex when classification is null', () => {
117
- const result = mod.isBroadGoal(null, 'I want to build a complete e-commerce platform with user authentication, product catalog, shopping cart, and payment processing')
118
- assert.equal(typeof result, 'boolean')
124
+ assert.equal(
125
+ mod.isBroadGoal(null, 'I want to build a complete e-commerce platform with user authentication, product catalog, shopping cart, and payment processing'),
126
+ false,
127
+ )
119
128
  })
120
129
  })
121
130
 
@@ -137,8 +146,7 @@ describe('hasWalletIntent', () => {
137
146
  })
138
147
 
139
148
  it('falls back to regex when classification is null', () => {
140
- const result = mod.hasWalletIntent(null, 'check my wallet balance')
141
- assert.equal(typeof result, 'boolean')
149
+ assert.equal(mod.hasWalletIntent(null, 'check my wallet balance'), false)
142
150
  })
143
151
  })
144
152
 
@@ -154,8 +162,7 @@ describe('hasTransactionalWalletIntent', () => {
154
162
  })
155
163
 
156
164
  it('falls back to regex when classification is null', () => {
157
- const result = mod.hasTransactionalWalletIntent(null, 'swap 1 ETH for USDC')
158
- assert.equal(typeof result, 'boolean')
165
+ assert.equal(mod.hasTransactionalWalletIntent(null, 'swap 1 ETH for USDC'), false)
159
166
  })
160
167
  })
161
168
 
@@ -169,11 +176,8 @@ describe('hasHumanSignals', () => {
169
176
  assert.equal(mod.hasHumanSignals(makeClassification({ hasHumanSignals: false }), ''), false)
170
177
  })
171
178
 
172
- it('regex detects personal text', () => {
173
- assert.equal(mod.hasHumanSignals(null, 'my birthday is next week'), true)
174
- })
175
-
176
- it('regex returns false for task-only text', () => {
179
+ it('returns false when classification is null', () => {
180
+ assert.equal(mod.hasHumanSignals(null, 'my birthday is next week'), false)
177
181
  assert.equal(mod.hasHumanSignals(null, 'deploy the app'), false)
178
182
  })
179
183
  })
@@ -188,12 +192,9 @@ describe('hasSignificantEvent', () => {
188
192
  assert.equal(mod.hasSignificantEvent(makeClassification({ hasSignificantEvent: false }), ''), false)
189
193
  })
190
194
 
191
- it('regex detects significant events', () => {
192
- assert.equal(mod.hasSignificantEvent(null, 'I just got promoted at work'), true)
193
- assert.equal(mod.hasSignificantEvent(null, 'my graduation ceremony is on Friday'), true)
194
- })
195
-
196
- it('regex returns false for non-event text', () => {
195
+ it('returns false when classification is null', () => {
196
+ assert.equal(mod.hasSignificantEvent(null, 'I just got promoted at work'), false)
197
+ assert.equal(mod.hasSignificantEvent(null, 'my graduation ceremony is on Friday'), false)
197
198
  assert.equal(mod.hasSignificantEvent(null, 'fix the login bug'), false)
198
199
  })
199
200
  })
@@ -208,9 +209,9 @@ describe('isResearchSynthesis', () => {
208
209
  assert.equal(mod.isResearchSynthesis(makeClassification({ isResearchSynthesis: false }), null), false)
209
210
  })
210
211
 
211
- it('falls back to routingIntent when classification is null', () => {
212
- assert.equal(mod.isResearchSynthesis(null, 'research'), true)
213
- assert.equal(mod.isResearchSynthesis(null, 'browsing'), true)
212
+ it('returns false when classification is null', () => {
213
+ assert.equal(mod.isResearchSynthesis(null, 'research'), false)
214
+ assert.equal(mod.isResearchSynthesis(null, 'browsing'), false)
214
215
  assert.equal(mod.isResearchSynthesis(null, 'coding'), false)
215
216
  assert.equal(mod.isResearchSynthesis(null, null), false)
216
217
  })
@@ -223,12 +224,14 @@ describe('isResearchSynthesis', () => {
223
224
  describe('classifyMessage', () => {
224
225
  it('returns valid classification from mock generateText', async () => {
225
226
  const mockResponse = JSON.stringify({
227
+ taskIntent: 'coding',
226
228
  isDeliverableTask: true,
227
229
  isBroadGoal: false,
228
230
  walletIntent: 'none',
229
231
  hasHumanSignals: false,
230
232
  hasSignificantEvent: false,
231
233
  isResearchSynthesis: false,
234
+ workType: 'coding',
232
235
  explicitToolRequests: ['shell'],
233
236
  confidence: 0.95,
234
237
  })
@@ -240,7 +243,9 @@ describe('classifyMessage', () => {
240
243
 
241
244
  assert.ok(result)
242
245
  assert.equal(result!.isDeliverableTask, true)
246
+ assert.equal(result!.taskIntent, 'coding')
243
247
  assert.equal(result!.walletIntent, 'none')
248
+ assert.equal(result!.workType, 'coding')
244
249
  assert.deepEqual(result!.explicitToolRequests, ['shell'])
245
250
  })
246
251
 
@@ -276,12 +281,14 @@ describe('classifyMessage', () => {
276
281
  it('caches results for the same message', async () => {
277
282
  let callCount = 0
278
283
  const mockResponse = JSON.stringify({
284
+ taskIntent: 'general',
279
285
  isDeliverableTask: false,
280
286
  isBroadGoal: false,
281
287
  walletIntent: 'none',
282
288
  hasHumanSignals: false,
283
289
  hasSignificantEvent: false,
284
290
  isResearchSynthesis: false,
291
+ workType: 'general',
285
292
  explicitToolRequests: [],
286
293
  confidence: 0.8,
287
294
  })
@@ -316,12 +323,17 @@ describe('classifyMessage', () => {
316
323
 
317
324
  function makeClassification(overrides: Partial<import('@/lib/server/chat-execution/message-classifier').MessageClassification>): import('@/lib/server/chat-execution/message-classifier').MessageClassification {
318
325
  return {
326
+ taskIntent: 'general',
319
327
  isDeliverableTask: false,
320
328
  isBroadGoal: false,
321
329
  walletIntent: 'none',
322
330
  hasHumanSignals: false,
323
331
  hasSignificantEvent: false,
324
332
  isResearchSynthesis: false,
333
+ workType: 'general',
334
+ wantsScreenshots: false,
335
+ wantsOutboundDelivery: false,
336
+ wantsVoiceDelivery: false,
325
337
  explicitToolRequests: [],
326
338
  confidence: 0.9,
327
339
  ...overrides,
@@ -15,7 +15,8 @@ import { z } from 'zod'
15
15
  import { buildLLM } from '@/lib/server/build-llm'
16
16
  import { log } from '@/lib/server/logger'
17
17
  import { hmrSingleton } from '@/lib/shared-utils'
18
- import type { Message } from '@/types'
18
+ import type { Message, MessageSemanticsSummary, MessageTaskIntent } from '@/types'
19
+ import type { DelegationWorkType } from '@/lib/server/agents/delegation-advisory'
19
20
 
20
21
  const TAG = 'message-classifier'
21
22
 
@@ -23,18 +24,40 @@ const TAG = 'message-classifier'
23
24
  // Schema
24
25
  // ---------------------------------------------------------------------------
25
26
 
27
+ const WorkTypeSchema = z.enum(['coding', 'research', 'writing', 'review', 'operations', 'general']).optional().default('general')
28
+ const TaskIntentSchema = z.enum(['coding', 'research', 'browsing', 'outreach', 'scheduling', 'general']).optional().default('general')
29
+
26
30
  export const MessageClassificationSchema = z.object({
31
+ taskIntent: TaskIntentSchema,
27
32
  isDeliverableTask: z.boolean(),
28
33
  isBroadGoal: z.boolean(),
29
34
  walletIntent: z.enum(['none', 'read_only', 'transactional']),
30
35
  hasHumanSignals: z.boolean(),
31
36
  hasSignificantEvent: z.boolean(),
32
37
  isResearchSynthesis: z.boolean(),
38
+ workType: WorkTypeSchema,
39
+ wantsScreenshots: z.boolean().optional().default(false),
40
+ wantsOutboundDelivery: z.boolean().optional().default(false),
41
+ wantsVoiceDelivery: z.boolean().optional().default(false),
33
42
  explicitToolRequests: z.array(z.string()),
34
43
  confidence: z.number().min(0).max(1),
35
44
  })
36
45
 
37
- export type MessageClassification = z.infer<typeof MessageClassificationSchema>
46
+ export interface MessageClassification {
47
+ taskIntent: MessageTaskIntent
48
+ isDeliverableTask: boolean
49
+ isBroadGoal: boolean
50
+ walletIntent: 'none' | 'read_only' | 'transactional'
51
+ hasHumanSignals: boolean
52
+ hasSignificantEvent: boolean
53
+ isResearchSynthesis: boolean
54
+ workType?: DelegationWorkType
55
+ wantsScreenshots?: boolean
56
+ wantsOutboundDelivery?: boolean
57
+ wantsVoiceDelivery?: boolean
58
+ explicitToolRequests: string[]
59
+ confidence: number
60
+ }
38
61
 
39
62
  // ---------------------------------------------------------------------------
40
63
  // LRU Cache (module-level, keyed on sha256 of message)
@@ -76,12 +99,17 @@ function buildClassificationPrompt(message: string, recentHistory: string): stri
76
99
  'Classify the user message below across multiple dimensions. Return JSON only.',
77
100
  '',
78
101
  'Dimensions:',
102
+ '- taskIntent: The primary execution intent. Use exactly one of: "coding", "research", "browsing", "outreach", "scheduling", or "general". Choose "coding" for repo/code/build/debug/edit tasks. Choose "research" for gathering current info or synthesizing sources. Choose "browsing" for page navigation, rendered-page inspection, form work, or literal browser workflows. Choose "outreach" for sending/sharing/delivering updates to an external channel. Choose "scheduling" for reminders, recurring work, monitoring, or follow-up scheduling. Choose "general" when none of the above clearly fits.',
79
103
  '- isDeliverableTask (bool): The user wants a concrete artifact produced — a document, report, plan, proposal, landing page, dashboard, HTML file, markdown file, brief, copy, screenshots, or similar deliverable. NOT simple Q&A, code fixes, or single-command tasks.',
80
104
  '- isBroadGoal (bool): The message describes a broad, multi-step goal (50+ chars, no code blocks, no file paths, no numbered lists). Short questions ending with "?" are NOT broad goals.',
81
105
  '- walletIntent: "none" if no crypto/wallet/trading context. "read_only" if mentioning wallet/crypto but only for checking balances, viewing transactions, or research. "transactional" if the user wants to swap, trade, buy, sell, mint, claim, deposit, withdraw, bridge, or execute a transaction.',
82
106
  '- hasHumanSignals (bool): The message contains personal signals — preferences ("I prefer", "call me"), relationships ("my wife", "my partner", "my kid"), life events ("birthday", "wedding", "promotion", "moving", "graduation", "hospital"), or personal disclosures.',
83
107
  '- hasSignificantEvent (bool): The message mentions a notable life/work event or milestone (birthday, anniversary, wedding, graduation, promotion, new job, relocation, illness, funeral, travel, house, deadline, launch).',
84
108
  '- isResearchSynthesis (bool): The task requires gathering information from multiple sources and synthesizing it — research reports, competitive analysis, market overviews, literature reviews, multi-source comparisons. NOT simple factual lookups.',
109
+ '- workType: The primary work domain. Use exactly one of: "coding", "research", "writing", "review", "operations", or "general". Choose "general" when nothing else clearly fits.',
110
+ '- wantsScreenshots (bool): The user explicitly wants screenshots, visual capture, rendered proof, or page snapshots.',
111
+ '- wantsOutboundDelivery (bool): The user explicitly wants the result sent, shared, delivered, posted, or messaged to an external destination/channel.',
112
+ '- wantsVoiceDelivery (bool): The user explicitly wants a voice note, voice memo, audio note, or voice message.',
85
113
  '- explicitToolRequests (string[]): Tool names the user explicitly asks to use. E.g. "use the shell", "run curl", "send an email", "ask the human", "use the browser". Return canonical tool names: "shell", "email", "ask_human", "browser", "files", "web". Empty array if none.',
86
114
  '- confidence (0-1): How confident are you in this classification overall.',
87
115
  '',
@@ -90,9 +118,10 @@ function buildClassificationPrompt(message: string, recentHistory: string): stri
90
118
  '- A message can be both a deliverable task AND a broad goal.',
91
119
  '- "walletIntent" should be "transactional" only if the user wants to execute a state-changing action, not just discuss crypto.',
92
120
  '- For "explicitToolRequests", only include tools the user explicitly mentions by name or clear synonym. Do not infer tool needs from the task type.',
121
+ '- Prefer the most execution-relevant taskIntent. Example: "research this and send me a voice note" is "research", not "outreach".',
93
122
  '',
94
123
  'Output shape:',
95
- '{"isDeliverableTask":bool,"isBroadGoal":bool,"walletIntent":"none|read_only|transactional","hasHumanSignals":bool,"hasSignificantEvent":bool,"isResearchSynthesis":bool,"explicitToolRequests":[],"confidence":0.0-1.0}',
124
+ '{"taskIntent":"coding|research|browsing|outreach|scheduling|general","isDeliverableTask":bool,"isBroadGoal":bool,"walletIntent":"none|read_only|transactional","hasHumanSignals":bool,"hasSignificantEvent":bool,"isResearchSynthesis":bool,"workType":"coding|research|writing|review|operations|general","wantsScreenshots":bool,"wantsOutboundDelivery":bool,"wantsVoiceDelivery":bool,"explicitToolRequests":[],"confidence":0.0-1.0}',
96
125
  '',
97
126
  recentHistory ? `Recent context:\n${recentHistory}\n` : '',
98
127
  `User message: ${JSON.stringify(message)}`,
@@ -181,7 +210,7 @@ const CLASSIFIER_TIMEOUT_MS = 2_000
181
210
 
182
211
  /**
183
212
  * Classify a user message using a single LLM call.
184
- * Returns null on failure/timeout callers should fall back to regex.
213
+ * Returns null on failure/timeout so callers can fail open to neutral behavior.
185
214
  */
186
215
  export async function classifyMessage(
187
216
  input: ClassifyMessageInput,
@@ -238,47 +267,60 @@ export async function classifyMessage(
238
267
  }
239
268
  }
240
269
 
270
+ export function toMessageSemanticsSummary(classification: MessageClassification | null | undefined): MessageSemanticsSummary | undefined {
271
+ if (!classification) return undefined
272
+ return {
273
+ taskIntent: classification.taskIntent,
274
+ workType: classification.workType || 'general',
275
+ walletIntent: classification.walletIntent,
276
+ isDeliverableTask: classification.isDeliverableTask,
277
+ isBroadGoal: classification.isBroadGoal,
278
+ isResearchSynthesis: classification.isResearchSynthesis,
279
+ hasHumanSignals: classification.hasHumanSignals,
280
+ hasSignificantEvent: classification.hasSignificantEvent,
281
+ wantsScreenshots: classification.wantsScreenshots === true,
282
+ wantsOutboundDelivery: classification.wantsOutboundDelivery === true,
283
+ wantsVoiceDelivery: classification.wantsVoiceDelivery === true,
284
+ explicitToolRequests: [...classification.explicitToolRequests],
285
+ confidence: classification.confidence,
286
+ }
287
+ }
288
+
241
289
  // ---------------------------------------------------------------------------
242
- // Adapter functions — fall back to regex when classification is null
290
+ // Adapter functions — neutral defaults when classification is unavailable
243
291
  // ---------------------------------------------------------------------------
244
292
 
245
- import {
246
- isBroadGoal as regexIsBroadGoal,
247
- looksLikeExternalWalletTask as regexLooksLikeExternalWalletTask,
248
- looksLikeBoundedExternalExecutionTask as regexLooksLikeBoundedExternalExecutionTask,
249
- looksLikeOpenEndedDeliverableTask as regexLooksLikeOpenEndedDeliverableTask,
250
- } from '@/lib/server/chat-execution/stream-continuation'
251
-
252
- export function isDeliverableTask(classification: MessageClassification | null, message: string): boolean {
253
- return classification?.isDeliverableTask ?? regexLooksLikeOpenEndedDeliverableTask(message)
293
+ export function isDeliverableTask(classification: MessageClassification | null, message?: string): boolean {
294
+ void message
295
+ return classification?.isDeliverableTask === true
254
296
  }
255
297
 
256
- export function isBroadGoal(classification: MessageClassification | null, message: string): boolean {
257
- return classification?.isBroadGoal ?? regexIsBroadGoal(message)
298
+ export function isBroadGoal(classification: MessageClassification | null, message?: string): boolean {
299
+ void message
300
+ return classification?.isBroadGoal === true
258
301
  }
259
302
 
260
- export function hasWalletIntent(classification: MessageClassification | null, message: string): boolean {
261
- if (classification) return classification.walletIntent !== 'none'
262
- return regexLooksLikeExternalWalletTask(message)
303
+ export function hasWalletIntent(classification: MessageClassification | null, message?: string): boolean {
304
+ void message
305
+ return classification?.walletIntent !== undefined && classification.walletIntent !== 'none'
263
306
  }
264
307
 
265
- export function hasTransactionalWalletIntent(classification: MessageClassification | null, message: string): boolean {
266
- if (classification) return classification.walletIntent === 'transactional'
267
- return regexLooksLikeBoundedExternalExecutionTask(message)
308
+ export function hasTransactionalWalletIntent(classification: MessageClassification | null, message?: string): boolean {
309
+ void message
310
+ return classification?.walletIntent === 'transactional'
268
311
  }
269
312
 
270
- export function hasHumanSignals(classification: MessageClassification | null, transcript: string): boolean {
271
- if (classification) return classification.hasHumanSignals
272
- // Fallback to regex
273
- return /\b(?:prefer|please|call me|don't call me|do not call me|i like|i dislike|i hate|i love|my pronouns|my partner|my wife|my husband|my kid|my child|my mom|my dad|my sister|my brother|birthday|anniversary|wedding|married|divorc|pregnan|baby|moved|moving|relocat|promotion|promoted|laid off|new job|job change|graduat|hospital|sick|illness|diagnos|passed away|funeral|grief|bereave|deadline|launch|fundraising|closing|house|home|travel)\b/i.test(transcript)
313
+ export function hasHumanSignals(classification: MessageClassification | null, transcript?: string): boolean {
314
+ void transcript
315
+ return classification?.hasHumanSignals === true
274
316
  }
275
317
 
276
- export function hasSignificantEvent(classification: MessageClassification | null, text: string): boolean {
277
- if (classification) return classification.hasSignificantEvent
278
- return /\b(?:birthday|anniversary|wedding|married|divorc|pregnan|baby|moved|moving|relocat|promotion|promoted|laid off|new job|job change|graduat|hospital|sick|illness|diagnos|passed away|funeral|grief|bereave|deadline|launch|fundraising|closing|house|home|travel)\b/i.test(text)
318
+ export function hasSignificantEvent(classification: MessageClassification | null, text?: string): boolean {
319
+ void text
320
+ return classification?.hasSignificantEvent === true
279
321
  }
280
322
 
281
- export function isResearchSynthesis(classification: MessageClassification | null, routingIntent: string | null): boolean {
282
- if (classification) return classification.isResearchSynthesis
283
- return routingIntent === 'research' || routingIntent === 'browsing'
323
+ export function isResearchSynthesis(classification: MessageClassification | null, routingIntent?: string | null): boolean {
324
+ void routingIntent
325
+ return classification?.isResearchSynthesis === true
284
326
  }
@@ -0,0 +1,29 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { buildAgenticExecutionPolicy } from '@/lib/server/chat-execution/prompt-builder'
5
+
6
+ describe('buildAgenticExecutionPolicy', () => {
7
+ it('adds a routing matrix that teaches session introspection, durable tracking, and direct routing', () => {
8
+ const prompt = buildAgenticExecutionPolicy({
9
+ enabledExtensions: ['memory', 'manage_sessions', 'manage_tasks', 'manage_skills', 'spawn_subagent'],
10
+ loopMode: 'bounded',
11
+ heartbeatPrompt: 'HEARTBEAT',
12
+ heartbeatIntervalSec: 120,
13
+ userMessage: 'Figure out what tools you have, then continue the task.',
14
+ history: [],
15
+ mode: 'minimal',
16
+ })
17
+
18
+ assert.ok(prompt.includes('## Routing Matrix'))
19
+ assert.ok(prompt.includes('Current-thread facts already visible in this chat'))
20
+ assert.ok(prompt.includes('`memory_search`'))
21
+ assert.ok(prompt.includes('`sessions_tool` action `identity`'))
22
+ assert.ok(prompt.includes('`sessions_tool` action `history`'))
23
+ assert.ok(prompt.includes('`manage_tasks`'))
24
+ assert.ok(prompt.includes('`manage_skills`'))
25
+ assert.ok(prompt.includes('delegate or spawn a subagent'))
26
+ assert.ok(prompt.includes('use the concrete tool now'))
27
+ assert.ok(prompt.includes('prefer the direct `manage_*` tool'))
28
+ })
29
+ })
@@ -202,10 +202,11 @@ export function shouldForceAttachmentFollowthrough(params: {
202
202
  enabledExtensions: string[]
203
203
  hasToolCalls: boolean
204
204
  hasAttachmentContext: boolean
205
+ classification?: MessageClassification | null
205
206
  }): boolean {
206
207
  if (!params.hasAttachmentContext) return false
207
208
  if (params.hasToolCalls) return false
208
- const decision = routeTaskIntent(params.userMessage, params.enabledExtensions, null)
209
+ const decision = routeTaskIntent(params.userMessage, params.enabledExtensions, null, params.classification ?? null)
209
210
  if (decision.intent !== 'research' && decision.intent !== 'browsing') return false
210
211
  return decision.preferredTools.some((toolName) => extensionIdMatches(params.enabledExtensions, toolName))
211
212
  }
@@ -326,6 +327,13 @@ export function buildAgenticExecutionPolicy(opts: {
326
327
  const extensionLines = isMinimal ? [] : buildExtensionCapabilityLines(opts.enabledExtensions, { delegationEnabled: opts.delegationEnabled, agentId: opts.agentId })
327
328
  const toolDisciplineLines = buildToolSection(opts.enabledExtensions)
328
329
  const hasMemoryTools = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'memory')
330
+ const hasManageSessions = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_sessions')
331
+ const hasManageTasks = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_tasks')
332
+ const hasManageSkills = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_skills')
333
+ const hasDelegationTools = opts.enabledExtensions.some((toolId) => {
334
+ const canonical = canonicalizeExtensionId(toolId) || toolId
335
+ return canonical === 'delegate' || canonical === 'spawn_subagent'
336
+ })
329
337
 
330
338
  const parts: string[] = []
331
339
 
@@ -351,6 +359,33 @@ export function buildAgenticExecutionPolicy(opts: {
351
359
  : 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
352
360
  )
353
361
 
362
+ if (hasTooling) {
363
+ parts.push(
364
+ '## Routing Matrix',
365
+ 'Current-thread facts already visible in this chat: answer directly from the thread before using tools.',
366
+ hasMemoryTools
367
+ ? 'Facts from previous conversations: start with `memory_search`, then `memory_get` only for a targeted follow-up read.'
368
+ : 'Facts from previous conversations: rely on the visible thread only and state when memory tools are unavailable.',
369
+ hasManageSessions
370
+ ? 'Harness/session context, lineage, project attachment, or enabled-tool questions: use `sessions_tool` action `identity`.'
371
+ : 'Harness/session introspection is limited here; rely on the runtime orientation block and visible context.',
372
+ hasManageSessions
373
+ ? 'Earlier messages from this same session that are not already visible in the thread: use `sessions_tool` action `history`.'
374
+ : 'Do not claim hidden session history is checked when `sessions_tool` is unavailable.',
375
+ hasManageTasks
376
+ ? 'Durable backlog or resumable progress tracking: use `manage_tasks` for multi-turn work, delegation, or explicit task-board requests.'
377
+ : 'Do not create pseudo-task workflows in prose when task tooling is unavailable.',
378
+ hasManageSkills
379
+ ? 'Missing capability, workflow, or environment setup blocker: use `manage_skills` before repeating generic exploration.'
380
+ : 'If a capability is genuinely missing, say so plainly instead of pretending a skill install happened.',
381
+ hasDelegationTools
382
+ ? 'Multi-step specialist work: delegate or spawn a subagent instead of doing the whole chain yourself.'
383
+ : 'If delegation tools are unavailable, execute directly with the tools you do have.',
384
+ 'For direct reversible execution, use the concrete tool now instead of creating a task or stopping at advice.',
385
+ 'When both `manage_platform` and a direct `manage_*` tool are available, prefer the direct `manage_*` tool.',
386
+ )
387
+ }
388
+
354
389
  // Sections skipped in minimal mode
355
390
  if (!isMinimal) {
356
391
  if (hasMemoryTools) {
@@ -374,7 +409,7 @@ export function buildAgenticExecutionPolicy(opts: {
374
409
  'Prefer `use_skill` action `run` for executable skills and `use_skill` action `load` only when the skill is guidance-only.',
375
410
  )
376
411
  }
377
- if (opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_skills')) {
412
+ if (hasManageSkills) {
378
413
  parts.push(
379
414
  '## Skill Resolution',
380
415
  'When you are blocked on a missing capability, binary, or environment setup, call `manage_skills` before repeating generic exploration.',
@@ -1,5 +1,8 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { after, before, describe, it } from 'node:test'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
3
6
 
4
7
  let mod: typeof import('@/lib/server/chat-execution/prompt-sections')
5
8
 
@@ -86,6 +89,59 @@ describe('prompt-sections', () => {
86
89
  })
87
90
  })
88
91
 
92
+ describe('buildRuntimeOrientationSection', () => {
93
+ it('includes delegated lineage, workspace markers, project context, and routing guidance', () => {
94
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-runtime-orientation-'))
95
+ try {
96
+ fs.writeFileSync(path.join(cwd, 'AGENTS.md'), '# Agent notes')
97
+ const result = mod.buildRuntimeOrientationSection({
98
+ session: {
99
+ id: 'child-session',
100
+ cwd,
101
+ provider: 'openai',
102
+ model: 'gpt-5',
103
+ parentSessionId: 'parent-session',
104
+ agentId: 'agent-1',
105
+ } as never,
106
+ promptMode: 'minimal',
107
+ sessionExtensions: ['files', 'manage_sessions', 'codex_cli'],
108
+ toolPolicy: {
109
+ mode: 'balanced',
110
+ requestedExtensions: ['files', 'manage_sessions', 'codex_cli', 'manage_secrets'],
111
+ enabledExtensions: ['files', 'manage_sessions', 'codex_cli'],
112
+ blockedExtensions: [{ tool: 'manage_secrets', reason: 'blocked by policy', source: 'policy' }],
113
+ },
114
+ agent: {
115
+ id: 'agent-1',
116
+ name: 'Builder',
117
+ delegationTargetMode: 'selected',
118
+ delegationTargetAgentIds: ['qa-1', 'ops-1'],
119
+ } as never,
120
+ activeProjectContext: {
121
+ projectId: 'project-1',
122
+ project: { name: 'Northstar' },
123
+ projectRoot: '/workspace/projects/project-1',
124
+ } as never,
125
+ rootSessionId: 'root-session',
126
+ })
127
+
128
+ assert.ok(result.includes('## Runtime Orientation'))
129
+ assert.ok(result.includes('delegated_child'))
130
+ assert.ok(result.includes('prompt=minimal'))
131
+ assert.ok(result.includes('root=root-session'))
132
+ assert.ok(result.includes('Workspace markers: AGENTS.md'))
133
+ assert.ok(result.includes('Active project: Northstar'))
134
+ assert.ok(result.includes('`manage_sessions`'))
135
+ assert.ok(result.includes('`codex_cli`'))
136
+ assert.ok(result.includes('Policy blocked:'))
137
+ assert.ok(result.includes('sessions_tool'))
138
+ assert.ok(result.includes('use `manage_platform` only as fallback'))
139
+ } finally {
140
+ fs.rmSync(cwd, { recursive: true, force: true })
141
+ }
142
+ })
143
+ })
144
+
89
145
  // ---- buildProjectSection ----
90
146
  describe('buildProjectSection', () => {
91
147
  it('returns null for minimal mode', () => {