@swarmclawai/swarmclaw 0.7.1 → 0.7.3
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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { afterEach, describe, it } from 'node:test'
|
|
3
|
+
import { PROVIDERS } from '../providers'
|
|
4
|
+
import { runStructuredExtraction } from './structured-extract'
|
|
5
|
+
|
|
6
|
+
const originalOllamaHandler = PROVIDERS.ollama.handler.streamChat
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
PROVIDERS.ollama.handler.streamChat = originalOllamaHandler
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('runStructuredExtraction', () => {
|
|
13
|
+
it('parses fenced JSON output from the current provider', async () => {
|
|
14
|
+
PROVIDERS.ollama.handler.streamChat = async () => '```json\n{"name":"Ada","score":10}\n```'
|
|
15
|
+
|
|
16
|
+
const result = await runStructuredExtraction({
|
|
17
|
+
session: {
|
|
18
|
+
id: 'session-1',
|
|
19
|
+
provider: 'ollama',
|
|
20
|
+
model: 'qwen3.5',
|
|
21
|
+
credentialId: null,
|
|
22
|
+
fallbackCredentialIds: [],
|
|
23
|
+
apiEndpoint: 'http://localhost:11434',
|
|
24
|
+
},
|
|
25
|
+
text: 'Ada scored 10.',
|
|
26
|
+
schema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
name: { type: 'string' },
|
|
30
|
+
score: { type: 'number' },
|
|
31
|
+
},
|
|
32
|
+
required: ['name', 'score'],
|
|
33
|
+
},
|
|
34
|
+
instruction: 'Extract the person and score.',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(result.object, { name: 'Ada', score: 10 })
|
|
38
|
+
assert.deepEqual(result.validationErrors, [])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('repairs invalid JSON with a second pass', async () => {
|
|
42
|
+
let callCount = 0
|
|
43
|
+
PROVIDERS.ollama.handler.streamChat = async () => {
|
|
44
|
+
callCount += 1
|
|
45
|
+
return callCount === 1 ? 'name: Ada' : '{"name":"Ada"}'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await runStructuredExtraction({
|
|
49
|
+
session: {
|
|
50
|
+
id: 'session-2',
|
|
51
|
+
provider: 'ollama',
|
|
52
|
+
model: 'qwen3.5',
|
|
53
|
+
credentialId: null,
|
|
54
|
+
fallbackCredentialIds: [],
|
|
55
|
+
apiEndpoint: 'http://localhost:11434',
|
|
56
|
+
},
|
|
57
|
+
text: 'Ada',
|
|
58
|
+
schema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
name: { type: 'string' },
|
|
62
|
+
},
|
|
63
|
+
required: ['name'],
|
|
64
|
+
},
|
|
65
|
+
instruction: 'Extract the name.',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
assert.equal(callCount, 2)
|
|
69
|
+
assert.deepEqual(result.object, { name: 'Ada' })
|
|
70
|
+
assert.deepEqual(result.validationErrors, [])
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import type { Session } from '@/types'
|
|
2
|
+
import { getProvider, streamChatWithFailover } from '@/lib/providers'
|
|
3
|
+
import { decryptKey, loadCredentials } from './storage'
|
|
4
|
+
import { extractDocumentArtifact, type DocumentArtifact } from './document-utils'
|
|
5
|
+
|
|
6
|
+
type JsonSchemaLike = Record<string, unknown>
|
|
7
|
+
|
|
8
|
+
interface ExtractionSession extends Pick<Session, 'id' | 'provider' | 'model' | 'credentialId' | 'fallbackCredentialIds' | 'apiEndpoint' | 'thinkingLevel'> {
|
|
9
|
+
name?: string
|
|
10
|
+
cwd?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StructuredExtractionSource {
|
|
14
|
+
kind: 'text' | 'file' | 'mixed'
|
|
15
|
+
text: string
|
|
16
|
+
filePath?: string | null
|
|
17
|
+
artifact?: DocumentArtifact | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface StructuredExtractionResult {
|
|
21
|
+
object: unknown
|
|
22
|
+
raw: string
|
|
23
|
+
validationErrors: string[]
|
|
24
|
+
provider: string
|
|
25
|
+
model: string
|
|
26
|
+
source: StructuredExtractionSource
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveApiKey(session: ExtractionSession): string | null {
|
|
30
|
+
const provider = getProvider(session.provider)
|
|
31
|
+
if (!provider) throw new Error(`Unknown provider: ${session.provider}`)
|
|
32
|
+
if (provider.requiresApiKey) {
|
|
33
|
+
if (!session.credentialId) throw new Error('No API key configured for this session')
|
|
34
|
+
const creds = loadCredentials()
|
|
35
|
+
const cred = creds[session.credentialId]
|
|
36
|
+
if (!cred?.encryptedKey) throw new Error('API key not found. Please add one in Settings.')
|
|
37
|
+
return decryptKey(cred.encryptedKey)
|
|
38
|
+
}
|
|
39
|
+
if (provider.optionalApiKey && session.credentialId) {
|
|
40
|
+
const creds = loadCredentials()
|
|
41
|
+
const cred = creds[session.credentialId]
|
|
42
|
+
if (cred?.encryptedKey) {
|
|
43
|
+
try {
|
|
44
|
+
return decryptKey(cred.encryptedKey)
|
|
45
|
+
} catch {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeSchemaInput(schema: unknown): JsonSchemaLike {
|
|
54
|
+
if (schema && typeof schema === 'object' && !Array.isArray(schema)) return schema as JsonSchemaLike
|
|
55
|
+
if (typeof schema === 'string' && schema.trim()) {
|
|
56
|
+
const parsed = JSON.parse(schema)
|
|
57
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as JsonSchemaLike
|
|
58
|
+
}
|
|
59
|
+
throw new Error('schema must be a JSON object or a JSON string representing an object.')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function defaultSummarySchema(): JsonSchemaLike {
|
|
63
|
+
return {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
summary: { type: 'string' },
|
|
67
|
+
keyPoints: { type: 'array', items: { type: 'string' } },
|
|
68
|
+
entities: {
|
|
69
|
+
type: 'array',
|
|
70
|
+
items: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
name: { type: 'string' },
|
|
74
|
+
type: { type: 'string' },
|
|
75
|
+
value: {},
|
|
76
|
+
},
|
|
77
|
+
required: ['name'],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ['summary', 'keyPoints'],
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeText(value: string, maxChars = 120_000): string {
|
|
86
|
+
const cleaned = value.replace(/\r\n/g, '\n').replace(/\u0000/g, '').trim()
|
|
87
|
+
if (cleaned.length <= maxChars) return cleaned
|
|
88
|
+
return `${cleaned.slice(0, maxChars)}\n\n[... truncated ...]`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractJsonBlock(text: string): string | null {
|
|
92
|
+
const raw = (text || '').trim()
|
|
93
|
+
if (!raw) return null
|
|
94
|
+
|
|
95
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim()
|
|
96
|
+
if (fenced) return fenced
|
|
97
|
+
|
|
98
|
+
if ((raw.startsWith('{') && raw.endsWith('}')) || (raw.startsWith('[') && raw.endsWith(']'))) {
|
|
99
|
+
return raw
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let inString = false
|
|
103
|
+
let escaped = false
|
|
104
|
+
let start = -1
|
|
105
|
+
const stack: string[] = []
|
|
106
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
107
|
+
const char = raw[index]
|
|
108
|
+
if (inString) {
|
|
109
|
+
if (escaped) {
|
|
110
|
+
escaped = false
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (char === '\\') {
|
|
114
|
+
escaped = true
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (char === '"') inString = false
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
if (char === '"') {
|
|
121
|
+
inString = true
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (char === '{' || char === '[') {
|
|
125
|
+
if (stack.length === 0) start = index
|
|
126
|
+
stack.push(char)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (char === '}' || char === ']') {
|
|
130
|
+
const last = stack.at(-1)
|
|
131
|
+
if ((char === '}' && last === '{') || (char === ']' && last === '[')) {
|
|
132
|
+
stack.pop()
|
|
133
|
+
if (stack.length === 0 && start >= 0) {
|
|
134
|
+
return raw.slice(start, index + 1)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseModelJson(text: string): unknown {
|
|
144
|
+
const candidate = extractJsonBlock(text)
|
|
145
|
+
if (!candidate) throw new Error('Model did not return JSON.')
|
|
146
|
+
return JSON.parse(candidate)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function typeMatches(value: unknown, expected: string): boolean {
|
|
150
|
+
if (expected === 'array') return Array.isArray(value)
|
|
151
|
+
if (expected === 'object') return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
152
|
+
if (expected === 'string') return typeof value === 'string'
|
|
153
|
+
if (expected === 'number') return typeof value === 'number' && Number.isFinite(value)
|
|
154
|
+
if (expected === 'integer') return typeof value === 'number' && Number.isInteger(value)
|
|
155
|
+
if (expected === 'boolean') return typeof value === 'boolean'
|
|
156
|
+
if (expected === 'null') return value === null
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateJsonLikeSchema(
|
|
161
|
+
value: unknown,
|
|
162
|
+
schema: JsonSchemaLike,
|
|
163
|
+
path = '$',
|
|
164
|
+
errors: string[] = [],
|
|
165
|
+
): string[] {
|
|
166
|
+
const expected = schema.type
|
|
167
|
+
if (typeof expected === 'string' && !typeMatches(value, expected)) {
|
|
168
|
+
errors.push(`${path} should be ${expected}`)
|
|
169
|
+
return errors
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((entry) => JSON.stringify(entry) === JSON.stringify(value))) {
|
|
173
|
+
errors.push(`${path} must be one of the allowed enum values`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (expected === 'object' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
177
|
+
const asRecord = value as Record<string, unknown>
|
|
178
|
+
const properties = (schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties))
|
|
179
|
+
? schema.properties as Record<string, JsonSchemaLike>
|
|
180
|
+
: {}
|
|
181
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((entry): entry is string => typeof entry === 'string') : []
|
|
182
|
+
for (const key of required) {
|
|
183
|
+
if (!(key in asRecord)) errors.push(`${path}.${key} is required`)
|
|
184
|
+
}
|
|
185
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
186
|
+
if (!(key in asRecord)) continue
|
|
187
|
+
validateJsonLikeSchema(asRecord[key], childSchema, `${path}.${key}`, errors)
|
|
188
|
+
}
|
|
189
|
+
if (schema.additionalProperties === false) {
|
|
190
|
+
for (const key of Object.keys(asRecord)) {
|
|
191
|
+
if (!(key in properties)) errors.push(`${path}.${key} is not allowed`)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (expected === 'array' && Array.isArray(value)) {
|
|
197
|
+
const itemSchema = (schema.items && typeof schema.items === 'object' && !Array.isArray(schema.items))
|
|
198
|
+
? schema.items as JsonSchemaLike
|
|
199
|
+
: null
|
|
200
|
+
if (typeof schema.minItems === 'number' && value.length < schema.minItems) {
|
|
201
|
+
errors.push(`${path} must contain at least ${schema.minItems} items`)
|
|
202
|
+
}
|
|
203
|
+
if (typeof schema.maxItems === 'number' && value.length > schema.maxItems) {
|
|
204
|
+
errors.push(`${path} must contain at most ${schema.maxItems} items`)
|
|
205
|
+
}
|
|
206
|
+
if (itemSchema) {
|
|
207
|
+
value.slice(0, 100).forEach((entry, index) => validateJsonLikeSchema(entry, itemSchema, `${path}[${index}]`, errors))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return errors
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function callExtractionModel(params: {
|
|
215
|
+
session: ExtractionSession
|
|
216
|
+
prompt: string
|
|
217
|
+
}): Promise<string> {
|
|
218
|
+
const provider = getProvider(params.session.provider)
|
|
219
|
+
if (!provider) throw new Error(`Unknown provider: ${params.session.provider}`)
|
|
220
|
+
|
|
221
|
+
const apiKey = resolveApiKey(params.session)
|
|
222
|
+
const streamedText: string[] = []
|
|
223
|
+
const streamedErrors: string[] = []
|
|
224
|
+
|
|
225
|
+
const raw = await streamChatWithFailover({
|
|
226
|
+
session: {
|
|
227
|
+
id: `${params.session.id}:extract:${Date.now()}`,
|
|
228
|
+
provider: params.session.provider,
|
|
229
|
+
model: params.session.model,
|
|
230
|
+
credentialId: params.session.credentialId ?? null,
|
|
231
|
+
fallbackCredentialIds: params.session.fallbackCredentialIds || [],
|
|
232
|
+
apiEndpoint: params.session.apiEndpoint || undefined,
|
|
233
|
+
thinkingLevel: params.session.thinkingLevel,
|
|
234
|
+
},
|
|
235
|
+
message: params.prompt,
|
|
236
|
+
apiKey,
|
|
237
|
+
active: new Map(),
|
|
238
|
+
loadHistory: () => [],
|
|
239
|
+
write: (chunk) => {
|
|
240
|
+
for (const line of chunk.split('\n')) {
|
|
241
|
+
if (!line.startsWith('data: ')) continue
|
|
242
|
+
try {
|
|
243
|
+
const event = JSON.parse(line.slice(6).trim()) as Record<string, unknown>
|
|
244
|
+
if (event.t === 'd' && typeof event.text === 'string') streamedText.push(event.text)
|
|
245
|
+
if (event.t === 'err' && typeof event.text === 'string') streamedErrors.push(event.text)
|
|
246
|
+
} catch {
|
|
247
|
+
// ignore malformed SSE fragments
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const text = (raw || streamedText.join('')).trim()
|
|
254
|
+
if (!text) {
|
|
255
|
+
throw new Error(streamedErrors[0] || `Provider "${provider.name}" returned no content.`)
|
|
256
|
+
}
|
|
257
|
+
return text
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildExtractionPrompt(params: {
|
|
261
|
+
instruction?: string | null
|
|
262
|
+
schema: JsonSchemaLike
|
|
263
|
+
source: StructuredExtractionSource
|
|
264
|
+
}): string {
|
|
265
|
+
const parts = [
|
|
266
|
+
'Extract structured data from the provided source.',
|
|
267
|
+
'Return only valid JSON. Do not include markdown fences, commentary, or explanatory text.',
|
|
268
|
+
'If a field cannot be determined, use null, an empty string, or an empty array based on the schema.',
|
|
269
|
+
]
|
|
270
|
+
if (params.instruction?.trim()) {
|
|
271
|
+
parts.push(`Task:\n${params.instruction.trim()}`)
|
|
272
|
+
}
|
|
273
|
+
parts.push(`JSON Schema:\n${JSON.stringify(params.schema, null, 2)}`)
|
|
274
|
+
if (params.source.artifact) {
|
|
275
|
+
const artifact = params.source.artifact
|
|
276
|
+
parts.push(`Source metadata:\n${JSON.stringify({
|
|
277
|
+
filePath: artifact.filePath,
|
|
278
|
+
fileName: artifact.fileName,
|
|
279
|
+
ext: artifact.ext,
|
|
280
|
+
method: artifact.method,
|
|
281
|
+
metadata: artifact.metadata,
|
|
282
|
+
tableCount: artifact.tables.length,
|
|
283
|
+
}, null, 2)}`)
|
|
284
|
+
}
|
|
285
|
+
parts.push(`Source text:\n${params.source.text}`)
|
|
286
|
+
return parts.join('\n\n')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function prepareSource(params: {
|
|
290
|
+
text?: string | null
|
|
291
|
+
filePath?: string | null
|
|
292
|
+
preferOcr?: boolean
|
|
293
|
+
maxChars?: number
|
|
294
|
+
}): Promise<StructuredExtractionSource> {
|
|
295
|
+
const chunks: string[] = []
|
|
296
|
+
let artifact: DocumentArtifact | null = null
|
|
297
|
+
|
|
298
|
+
if (params.filePath) {
|
|
299
|
+
artifact = await extractDocumentArtifact(params.filePath, {
|
|
300
|
+
preferOcr: params.preferOcr,
|
|
301
|
+
maxChars: params.maxChars,
|
|
302
|
+
})
|
|
303
|
+
if (artifact.text.trim()) chunks.push(artifact.text)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (params.text?.trim()) chunks.push(params.text.trim())
|
|
307
|
+
if (chunks.length === 0) throw new Error('text or filePath is required.')
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
kind: params.filePath && params.text ? 'mixed' : params.filePath ? 'file' : 'text',
|
|
311
|
+
filePath: params.filePath || null,
|
|
312
|
+
artifact,
|
|
313
|
+
text: normalizeText(chunks.join('\n\n'), params.maxChars || 120_000),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function runStructuredExtraction(params: {
|
|
318
|
+
session: ExtractionSession
|
|
319
|
+
text?: string | null
|
|
320
|
+
filePath?: string | null
|
|
321
|
+
instruction?: string | null
|
|
322
|
+
schema?: unknown
|
|
323
|
+
preferOcr?: boolean
|
|
324
|
+
maxChars?: number
|
|
325
|
+
}): Promise<StructuredExtractionResult> {
|
|
326
|
+
if (!params.session.provider || !params.session.model) {
|
|
327
|
+
throw new Error('Current session is missing provider/model configuration.')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const source = await prepareSource({
|
|
331
|
+
text: params.text,
|
|
332
|
+
filePath: params.filePath,
|
|
333
|
+
preferOcr: params.preferOcr,
|
|
334
|
+
maxChars: params.maxChars,
|
|
335
|
+
})
|
|
336
|
+
const schema = params.schema === undefined ? defaultSummarySchema() : normalizeSchemaInput(params.schema)
|
|
337
|
+
const prompt = buildExtractionPrompt({
|
|
338
|
+
instruction: params.instruction,
|
|
339
|
+
schema,
|
|
340
|
+
source,
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
let raw = await callExtractionModel({
|
|
344
|
+
session: params.session,
|
|
345
|
+
prompt,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
let parsed: unknown
|
|
349
|
+
try {
|
|
350
|
+
parsed = parseModelJson(raw)
|
|
351
|
+
} catch (error) {
|
|
352
|
+
raw = await callExtractionModel({
|
|
353
|
+
session: params.session,
|
|
354
|
+
prompt: [
|
|
355
|
+
'Repair the invalid JSON below so it becomes valid JSON that matches the provided schema.',
|
|
356
|
+
'Return only JSON.',
|
|
357
|
+
`JSON Schema:\n${JSON.stringify(schema, null, 2)}`,
|
|
358
|
+
`Invalid output:\n${raw}`,
|
|
359
|
+
].join('\n\n'),
|
|
360
|
+
})
|
|
361
|
+
parsed = parseModelJson(raw)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const validationErrors = validateJsonLikeSchema(parsed, schema).slice(0, 50)
|
|
365
|
+
return {
|
|
366
|
+
object: parsed,
|
|
367
|
+
raw,
|
|
368
|
+
validationErrors,
|
|
369
|
+
provider: params.session.provider,
|
|
370
|
+
model: params.session.model,
|
|
371
|
+
source,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import type { Agent } from '@/types'
|
|
4
|
-
import { parseMentionedAgentId, resolveTaskAgentFromDescription } from './task-mention'
|
|
4
|
+
import { parseAssignedAgentId, parseMentionedAgentId, resolveAgentReference, resolveTaskAgentFromDescription } from './task-mention'
|
|
5
5
|
|
|
6
6
|
const now = Date.now()
|
|
7
7
|
const agents: Record<string, Agent> = {
|
|
@@ -37,5 +37,19 @@ describe('task-mention', () => {
|
|
|
37
37
|
const resolved = resolveTaskAgentFromDescription('No mention here', 'default', agents)
|
|
38
38
|
assert.equal(resolved, 'default')
|
|
39
39
|
})
|
|
40
|
-
})
|
|
41
40
|
|
|
41
|
+
it('resolves agent ids directly', () => {
|
|
42
|
+
const resolved = resolveAgentReference('coder', agents)
|
|
43
|
+
assert.equal(resolved, 'coder')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('parses plain-language assignment phrases', () => {
|
|
47
|
+
const assigned = parseAssignedAgentId('Create this task and assign it to agent "default".', agents)
|
|
48
|
+
assert.equal(assigned, 'default')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('resolves task assignment without @mentions', () => {
|
|
52
|
+
const resolved = resolveTaskAgentFromDescription('Please delegate this to CodeBot.', 'default', agents)
|
|
53
|
+
assert.equal(resolved, 'coder')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import type { Agent } from '@/types'
|
|
2
2
|
|
|
3
|
+
function normalizeReference(reference: string): string {
|
|
4
|
+
return reference
|
|
5
|
+
.trim()
|
|
6
|
+
.replace(/^@/, '')
|
|
7
|
+
.replace(/^agent\s+/i, '')
|
|
8
|
+
.replace(/^["'`]+|["'`]+$/g, '')
|
|
9
|
+
.replace(/[.,!?;:]+$/g, '')
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveAgentReference(
|
|
15
|
+
reference: string,
|
|
16
|
+
agents: Record<string, Agent>,
|
|
17
|
+
): string | null {
|
|
18
|
+
const normalized = normalizeReference(reference)
|
|
19
|
+
if (!normalized) return null
|
|
20
|
+
|
|
21
|
+
const agentList = Object.values(agents)
|
|
22
|
+
const exactId = agentList.find((agent) => agent.id.toLowerCase() === normalized)
|
|
23
|
+
if (exactId) return exactId.id
|
|
24
|
+
|
|
25
|
+
const exactName = agentList.find((agent) => agent.name.toLowerCase() === normalized)
|
|
26
|
+
if (exactName) return exactName.id
|
|
27
|
+
|
|
28
|
+
const startsWithId = agentList.find((agent) => agent.id.toLowerCase().startsWith(normalized))
|
|
29
|
+
if (startsWithId) return startsWithId.id
|
|
30
|
+
|
|
31
|
+
const startsWithName = agentList.find((agent) => agent.name.toLowerCase().startsWith(normalized))
|
|
32
|
+
if (startsWithName) return startsWithName.id
|
|
33
|
+
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
3
37
|
/**
|
|
4
38
|
* Parse @AgentName mentions from text and resolve to an agent ID.
|
|
5
39
|
* Uses case-insensitive exact match, then falls back to starts-with.
|
|
@@ -13,16 +47,31 @@ export function parseMentionedAgentId(
|
|
|
13
47
|
let match: RegExpExecArray | null
|
|
14
48
|
|
|
15
49
|
while ((match = mentionRegex.exec(description)) !== null) {
|
|
16
|
-
const mention =
|
|
17
|
-
|
|
50
|
+
const mention = match[1] || ''
|
|
51
|
+
const resolved = resolveAgentReference(mention, agents)
|
|
52
|
+
if (resolved) return resolved
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
18
57
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
58
|
+
export function parseAssignedAgentId(
|
|
59
|
+
description: string,
|
|
60
|
+
agents: Record<string, Agent>,
|
|
61
|
+
): string | null {
|
|
62
|
+
const patterns = [
|
|
63
|
+
/(?:assign(?:ed)?|delegate(?:d)?|route(?:d)?|hand(?:ed)?)(?:\s+\w+){0,4}\s+to\s+(?:agent\s+)?["'`]?([^"'`\n]+?)["'`]?(?=$|[\s.,;:])/gi,
|
|
64
|
+
/(?:assignee|assigned[_\s-]?to|agent(?:\s+id)?)\s*[:=]\s*["'`]?([^"'`\n]+?)["'`]?(?=$|[\s.,;:])/gi,
|
|
65
|
+
/for\s+agent\s+["'`]?([^"'`\n]+?)["'`]?(?=$|[\s.,;:])/gi,
|
|
66
|
+
]
|
|
22
67
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
68
|
+
for (const pattern of patterns) {
|
|
69
|
+
let match: RegExpExecArray | null
|
|
70
|
+
while ((match = pattern.exec(description)) !== null) {
|
|
71
|
+
const candidate = (match[1] || '').trim()
|
|
72
|
+
const resolved = resolveAgentReference(candidate, agents)
|
|
73
|
+
if (resolved) return resolved
|
|
74
|
+
}
|
|
26
75
|
}
|
|
27
76
|
|
|
28
77
|
return null
|
|
@@ -30,7 +79,7 @@ export function parseMentionedAgentId(
|
|
|
30
79
|
|
|
31
80
|
/**
|
|
32
81
|
* Resolve task agent: if description has an @mention, use that agent.
|
|
33
|
-
* Otherwise fall back to currentAgentId.
|
|
82
|
+
* Otherwise fall back to an explicit assignment phrase, then currentAgentId.
|
|
34
83
|
*/
|
|
35
84
|
export function resolveTaskAgentFromDescription(
|
|
36
85
|
description: string,
|
|
@@ -38,5 +87,7 @@ export function resolveTaskAgentFromDescription(
|
|
|
38
87
|
agents: Record<string, Agent>,
|
|
39
88
|
): string {
|
|
40
89
|
const mentioned = parseMentionedAgentId(description, agents)
|
|
41
|
-
|
|
90
|
+
if (mentioned) return mentioned
|
|
91
|
+
const assigned = parseAssignedAgentId(description, agents)
|
|
92
|
+
return assigned || currentAgentId
|
|
42
93
|
}
|