@swarmclawai/swarmclaw 1.2.3 → 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 +20 -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]/models/route.test.ts +60 -0
- package/src/app/api/providers/[id]/models/route.ts +33 -1
- package/src/app/api/providers/[id]/route.test.ts +49 -0
- package/src/app/api/providers/[id]/route.ts +30 -1
- 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 +51 -25
- package/src/components/agents/inspector-panel.tsx +15 -4
- package/src/components/auth/setup-wizard/index.tsx +27 -18
- package/src/components/auth/setup-wizard/shared.tsx +2 -2
- package/src/components/auth/setup-wizard/step-agents.tsx +51 -38
- package/src/components/auth/setup-wizard/step-connect.tsx +48 -17
- package/src/components/auth/setup-wizard/types.ts +6 -4
- package/src/components/auth/setup-wizard/utils.test.ts +38 -8
- package/src/components/auth/setup-wizard/utils.ts +14 -8
- 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 +150 -77
- package/src/components/providers/provider-sheet.tsx +102 -77
- package/src/components/shared/model-combobox.tsx +5 -4
- 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/agent-provider-options.test.ts +152 -0
- package/src/lib/agent-provider-options.ts +84 -0
- package/src/lib/app/api-client.ts +2 -2
- package/src/lib/providers/index.test.ts +78 -0
- package/src/lib/providers/index.ts +13 -10
- package/src/lib/query/client.ts +17 -0
- package/src/lib/server/agents/agent-runtime-config.ts +6 -6
- 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 +42 -215
- package/src/lib/server/storage-normalization.ts +98 -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 +288 -22
- package/src/views/settings/section-providers.tsx +2 -2
|
@@ -439,6 +439,51 @@ describe('chat-turn-tool-routing', () => {
|
|
|
439
439
|
assert.equal(result.calledNames.has('memory_search'), true)
|
|
440
440
|
})
|
|
441
441
|
|
|
442
|
+
it('uses classifier-backed memory list fallback for broad memory inventory requests', async () => {
|
|
443
|
+
const invocations: Array<{ toolName: string; args: Record<string, unknown> }> = []
|
|
444
|
+
const result = await runPostLlmToolRouting({
|
|
445
|
+
session: {
|
|
446
|
+
cwd: process.cwd(),
|
|
447
|
+
tools: ['memory'],
|
|
448
|
+
},
|
|
449
|
+
sessionId: 'session-memory-list',
|
|
450
|
+
message: 'List everything you remember about me.',
|
|
451
|
+
effectiveMessage: 'List everything you remember about me.',
|
|
452
|
+
enabledExtensions: ['memory'],
|
|
453
|
+
toolPolicy: resolveSessionToolPolicy(['memory'], {}),
|
|
454
|
+
appSettings: {},
|
|
455
|
+
internal: false,
|
|
456
|
+
source: 'chat',
|
|
457
|
+
toolEvents: [],
|
|
458
|
+
emit: () => {},
|
|
459
|
+
}, '', undefined, {
|
|
460
|
+
classifyDirectMemoryIntent: async () => ({
|
|
461
|
+
action: 'list',
|
|
462
|
+
confidence: 0.91,
|
|
463
|
+
}),
|
|
464
|
+
invokeTool: async (_ctx, toolName, args, _failurePrefix, calledNames) => {
|
|
465
|
+
invocations.push({ toolName, args })
|
|
466
|
+
calledNames.add(toolName)
|
|
467
|
+
return {
|
|
468
|
+
invoked: true,
|
|
469
|
+
responseOverride: null,
|
|
470
|
+
toolOutputText: '[mem_1] favorite editor: Neovim\n[mem_2] timezone: Europe/Isle_of_Man',
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
assert.equal(invocations.length, 1)
|
|
476
|
+
assert.equal(invocations[0].toolName, 'memory_tool')
|
|
477
|
+
assert.deepEqual(invocations[0].args, {
|
|
478
|
+
action: 'list',
|
|
479
|
+
key: '',
|
|
480
|
+
scope: 'auto',
|
|
481
|
+
})
|
|
482
|
+
assert.equal(result.fullResponse, '[mem_1] favorite editor: Neovim\n[mem_2] timezone: Europe/Isle_of_Man')
|
|
483
|
+
assert.equal(result.errorMessage, undefined)
|
|
484
|
+
assert.equal(result.calledNames.has('memory_tool'), true)
|
|
485
|
+
})
|
|
486
|
+
|
|
442
487
|
it('fails open when post-LLM memory classification times out', async () => {
|
|
443
488
|
let invoked = false
|
|
444
489
|
const started = Date.now()
|
|
@@ -22,6 +22,7 @@ import { log } from '@/lib/server/logger'
|
|
|
22
22
|
import { rankDelegatesByHealth } from '@/lib/server/provider-health'
|
|
23
23
|
import { routeTaskIntent, type CapabilityRoutingDecision } from '@/lib/server/capability-router'
|
|
24
24
|
import { canonicalizeExtensionId, extensionIdMatches } from '@/lib/server/tool-aliases'
|
|
25
|
+
import { classifyMessage, type MessageClassification } from '@/lib/server/chat-execution/message-classifier'
|
|
25
26
|
import {
|
|
26
27
|
buildDirectMemoryRecallResponse,
|
|
27
28
|
classifyDirectMemoryIntent,
|
|
@@ -37,7 +38,6 @@ import {
|
|
|
37
38
|
findFirstUrl,
|
|
38
39
|
hasToolEnabled,
|
|
39
40
|
hasDirectLocalCodingTools,
|
|
40
|
-
isMemoryListIntent,
|
|
41
41
|
requestedToolNamesFromMessage,
|
|
42
42
|
translateRequestedToolInvocation,
|
|
43
43
|
} from '@/lib/server/chat-execution/chat-execution-utils'
|
|
@@ -61,6 +61,7 @@ export interface ToolRoutingContext {
|
|
|
61
61
|
source: string
|
|
62
62
|
toolEvents: MessageToolEvent[]
|
|
63
63
|
emit: (ev: SSEEvent) => void
|
|
64
|
+
classification?: MessageClassification | null
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export interface ToolRoutingResult {
|
|
@@ -126,6 +127,16 @@ async function resolveDirectMemoryIntentWithTimeout(
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
async function resolveTurnClassification(ctx: ToolRoutingContext): Promise<MessageClassification | null> {
|
|
131
|
+
if (ctx.classification !== undefined) return ctx.classification ?? null
|
|
132
|
+
if (ctx.internal || ctx.source !== 'chat') return null
|
|
133
|
+
return classifyMessage({
|
|
134
|
+
sessionId: ctx.sessionId,
|
|
135
|
+
agentId: ctx.session.agentId || null,
|
|
136
|
+
message: ctx.message,
|
|
137
|
+
}).catch(() => null)
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
export async function runExclusiveDirectMemoryPreflight(
|
|
130
141
|
ctx: ToolRoutingContext,
|
|
131
142
|
hooks?: ToolRoutingHooks,
|
|
@@ -148,14 +159,18 @@ export async function runExclusiveDirectMemoryPreflight(
|
|
|
148
159
|
? 'memory_store'
|
|
149
160
|
: directMemoryIntent.action === 'update'
|
|
150
161
|
? 'memory_update'
|
|
151
|
-
: '
|
|
162
|
+
: directMemoryIntent.action === 'list'
|
|
163
|
+
? 'memory_tool'
|
|
164
|
+
: 'memory_search'
|
|
152
165
|
|
|
153
166
|
const args: Record<string, unknown> = directMemoryIntent.action === 'recall'
|
|
154
167
|
? { query: directMemoryIntent.query, scope: 'auto' }
|
|
155
|
-
:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
: directMemoryIntent.action === 'list'
|
|
169
|
+
? { action: 'list', key: '', scope: 'auto' }
|
|
170
|
+
: {
|
|
171
|
+
value: directMemoryIntent.value,
|
|
172
|
+
...(directMemoryIntent.title ? { title: directMemoryIntent.title } : {}),
|
|
173
|
+
}
|
|
159
174
|
|
|
160
175
|
const result = await invokeTool(
|
|
161
176
|
ctx,
|
|
@@ -192,6 +207,15 @@ export async function runExclusiveDirectMemoryPreflight(
|
|
|
192
207
|
}
|
|
193
208
|
}
|
|
194
209
|
|
|
210
|
+
if (directMemoryIntent.action === 'list') {
|
|
211
|
+
return {
|
|
212
|
+
calledNames,
|
|
213
|
+
fullResponse: String(result.toolOutputText || '').trim() || 'No memories found.',
|
|
214
|
+
errorMessage: undefined,
|
|
215
|
+
missedRequestedTools: [],
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
195
219
|
if (directMemoryIntent.action === 'recall') {
|
|
196
220
|
const recallResponse = result.toolOutputText
|
|
197
221
|
? buildDirectMemoryRecallResponse(directMemoryIntent, result.toolOutputText)
|
|
@@ -494,12 +518,13 @@ export async function runPostLlmToolRouting(
|
|
|
494
518
|
const unavailableRequestedTools = new Map<string, string>()
|
|
495
519
|
let fullResponse = currentResponse
|
|
496
520
|
let errorMessage = currentError
|
|
521
|
+
const classification = await resolveTurnClassification(ctx)
|
|
497
522
|
|
|
498
523
|
const requestedToolNames = (!ctx.internal && ctx.source === 'chat')
|
|
499
524
|
? requestedToolNamesFromMessage(ctx.message)
|
|
500
525
|
: []
|
|
501
526
|
const routingDecision: CapabilityRoutingDecision | null = (!ctx.internal && ctx.source === 'chat')
|
|
502
|
-
? routeTaskIntent(ctx.message, ctx.enabledExtensions, ctx.appSettings)
|
|
527
|
+
? routeTaskIntent(ctx.message, ctx.enabledExtensions, ctx.appSettings, classification)
|
|
503
528
|
: null
|
|
504
529
|
|
|
505
530
|
// --- Forced connector_message_tool ---
|
|
@@ -609,6 +634,22 @@ export async function runPostLlmToolRouting(
|
|
|
609
634
|
}
|
|
610
635
|
}
|
|
611
636
|
|
|
637
|
+
if (directMemoryIntent?.action === 'list') {
|
|
638
|
+
const result = await invokeTool(
|
|
639
|
+
ctx,
|
|
640
|
+
'memory_tool',
|
|
641
|
+
{ action: 'list', key: '', scope: 'auto' },
|
|
642
|
+
'Forced memory list invocation failed',
|
|
643
|
+
calledNames,
|
|
644
|
+
)
|
|
645
|
+
if (result.blockedReason) policyBlockedTools.set('memory_tool', result.blockedReason)
|
|
646
|
+
if (result.unavailableReason) unavailableRequestedTools.set('memory_tool', result.unavailableReason)
|
|
647
|
+
if (result.invoked) {
|
|
648
|
+
fullResponse = String(result.toolOutputText || '').trim() || 'No memories found.'
|
|
649
|
+
errorMessage = undefined
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
612
653
|
// --- Auto-delegation for coding intent ---
|
|
613
654
|
const hasDelegationCall = FORCED_DELEGATION_TOOLS.some((t) => calledNames.has(t))
|
|
614
655
|
const enabledDelegates = enabledDelegationTools(ctx.session)
|
|
@@ -690,16 +731,6 @@ export async function runPostLlmToolRouting(
|
|
|
690
731
|
}
|
|
691
732
|
}
|
|
692
733
|
|
|
693
|
-
if (canAutoRoute && calledNames.size === 0 && hasToolEnabled(ctx.session, 'memory') && isMemoryListIntent(ctx.message)) {
|
|
694
|
-
const result = await invokeTool(
|
|
695
|
-
ctx, 'memory_tool',
|
|
696
|
-
{ action: 'list', key: '', scope: 'auto' },
|
|
697
|
-
'Auto memory listing failed',
|
|
698
|
-
calledNames,
|
|
699
|
-
)
|
|
700
|
-
if (result.responseOverride) fullResponse = result.responseOverride
|
|
701
|
-
}
|
|
702
|
-
|
|
703
734
|
const explicitArtifactTarget = extractExplicitArtifactTarget(ctx.message)
|
|
704
735
|
const canAutoSaveArtifact = (!ctx.internal && ctx.source === 'chat')
|
|
705
736
|
&& !!explicitArtifactTarget
|
|
@@ -42,6 +42,8 @@ export interface ContinuationContext {
|
|
|
42
42
|
sessionExtensions: string[]
|
|
43
43
|
isConnectorSession: boolean
|
|
44
44
|
isCoordinatorAgent: boolean
|
|
45
|
+
delegationEnabled: boolean
|
|
46
|
+
delegationPreferenceActive: boolean
|
|
45
47
|
history: Message[]
|
|
46
48
|
session: { cwd: string }
|
|
47
49
|
write: (data: string) => void
|
|
@@ -279,6 +281,7 @@ function checkAttachmentFollowthrough(ctx: ContinuationContext): ContinuationDec
|
|
|
279
281
|
enabledExtensions: ctx.sessionExtensions,
|
|
280
282
|
hasToolCalls: ctx.state.hasToolCalls,
|
|
281
283
|
hasAttachmentContext: ctx.hasAttachmentContext,
|
|
284
|
+
classification: ctx.classification,
|
|
282
285
|
})) return null
|
|
283
286
|
const count = ctx.limits.increment('attachment_followthrough')
|
|
284
287
|
const { max } = ctx.limits.getStatus('attachment_followthrough')
|
|
@@ -335,7 +338,7 @@ function checkToolErrorFollowthrough(ctx: ContinuationContext): ContinuationDeci
|
|
|
335
338
|
}
|
|
336
339
|
|
|
337
340
|
function checkCoordinatorDelegation(ctx: ContinuationContext): ContinuationDecision | null {
|
|
338
|
-
if (!ctx.
|
|
341
|
+
if (!ctx.delegationEnabled || !ctx.delegationPreferenceActive) return null
|
|
339
342
|
if (!ctx.limits.canContinue('coordinator_delegation_nudge')) return null
|
|
340
343
|
// Skip if already delegated
|
|
341
344
|
const delegationTools = ['spawn_subagent', 'manage_protocols']
|
|
@@ -46,6 +46,15 @@ describe('direct-memory-intent', () => {
|
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
+
it('parses a list classification payload', () => {
|
|
50
|
+
const parsed = parseDirectMemoryIntentResponse('{"action":"list","confidence":0.84}')
|
|
51
|
+
|
|
52
|
+
assert.deepEqual(parsed, {
|
|
53
|
+
action: 'list',
|
|
54
|
+
confidence: 0.84,
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
49
58
|
it('parses exclusive completion for pure memory turns', () => {
|
|
50
59
|
const parsed = parseDirectMemoryIntentResponse('{"action":"store","confidence":0.98,"title":"Launch marker","value":"My launch marker is ALPHA-9","acknowledgement":"I\\u2019ll remember that.","exclusiveCompletion":true}')
|
|
51
60
|
|
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
173
|
-
assert.equal(mod.hasHumanSignals(null, 'my birthday is next week'),
|
|
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('
|
|
192
|
-
assert.equal(mod.hasSignificantEvent(null, 'I just got promoted at work'),
|
|
193
|
-
assert.equal(mod.hasSignificantEvent(null, 'my graduation ceremony is on Friday'),
|
|
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('
|
|
212
|
-
assert.equal(mod.isResearchSynthesis(null, 'research'),
|
|
213
|
-
assert.equal(mod.isResearchSynthesis(null, 'browsing'),
|
|
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
|
|
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
|
|
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 —
|
|
290
|
+
// Adapter functions — neutral defaults when classification is unavailable
|
|
243
291
|
// ---------------------------------------------------------------------------
|
|
244
292
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
257
|
-
|
|
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
|
|
261
|
-
|
|
262
|
-
return
|
|
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
|
|
266
|
-
|
|
267
|
-
return
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
277
|
-
|
|
278
|
-
return
|
|
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
|
|
282
|
-
|
|
283
|
-
return
|
|
323
|
+
export function isResearchSynthesis(classification: MessageClassification | null, routingIntent?: string | null): boolean {
|
|
324
|
+
void routingIntent
|
|
325
|
+
return classification?.isResearchSynthesis === true
|
|
284
326
|
}
|