@vellumai/assistant 0.3.4 → 0.3.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/Dockerfile +2 -0
- package/README.md +37 -2
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +70 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -17
- package/src/__tests__/channel-approvals.test.ts +48 -1
- package/src/__tests__/channel-guardian.test.ts +74 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +13 -12
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/handlers-twilio-config.test.ts +407 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +22 -11
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +21 -6
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/system-prompt.ts +24 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
- package/src/daemon/handlers/config.ts +783 -9
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +108 -4
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/ride-shotgun-handler.ts +1 -1
- package/src/daemon/server.ts +6 -2
- package/src/daemon/session-agent-loop.ts +5 -1
- package/src/daemon/session-runtime-assembly.ts +55 -0
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +11 -1
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-init.ts +144 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/media-store.ts +759 -0
- package/src/memory/retriever.ts +6 -1
- package/src/memory/schema.ts +98 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +24 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +12 -4
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/http-server.ts +53 -27
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +67 -21
- package/src/runtime/run-orchestrator.ts +35 -2
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +35 -0
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
stripInjectedContext,
|
|
35
35
|
} from './session-runtime-assembly.js';
|
|
36
36
|
import { buildTemporalContext } from './date-context.js';
|
|
37
|
-
import type { ActiveSurfaceContext, ChannelCapabilities } from './session-runtime-assembly.js';
|
|
37
|
+
import type { ActiveSurfaceContext, ChannelCapabilities, GuardianRuntimeContext } from './session-runtime-assembly.js';
|
|
38
38
|
import {
|
|
39
39
|
cleanAssistantContent,
|
|
40
40
|
drainDirectiveDisplayBuffer,
|
|
@@ -95,6 +95,7 @@ export interface AgentLoopSessionContext {
|
|
|
95
95
|
workspaceTopLevelContext: string | null;
|
|
96
96
|
workspaceTopLevelDirty: boolean;
|
|
97
97
|
channelCapabilities?: ChannelCapabilities;
|
|
98
|
+
guardianContext?: GuardianRuntimeContext;
|
|
98
99
|
|
|
99
100
|
readonly coreToolNames: Set<string>;
|
|
100
101
|
allowedToolNames?: Set<string>;
|
|
@@ -296,6 +297,7 @@ export async function runAgentLoopImpl(
|
|
|
296
297
|
activeSurface,
|
|
297
298
|
workspaceTopLevelContext: ctx.workspaceTopLevelContext,
|
|
298
299
|
channelCapabilities: ctx.channelCapabilities ?? null,
|
|
300
|
+
guardianContext: ctx.guardianContext ?? null,
|
|
299
301
|
temporalContext,
|
|
300
302
|
});
|
|
301
303
|
|
|
@@ -617,6 +619,7 @@ export async function runAgentLoopImpl(
|
|
|
617
619
|
activeSurface,
|
|
618
620
|
workspaceTopLevelContext: ctx.workspaceTopLevelContext,
|
|
619
621
|
channelCapabilities: ctx.channelCapabilities ?? null,
|
|
622
|
+
guardianContext: ctx.guardianContext ?? null,
|
|
620
623
|
temporalContext,
|
|
621
624
|
});
|
|
622
625
|
preRepairMessages = runMessages;
|
|
@@ -649,6 +652,7 @@ export async function runAgentLoopImpl(
|
|
|
649
652
|
activeSurface,
|
|
650
653
|
workspaceTopLevelContext: ctx.workspaceTopLevelContext,
|
|
651
654
|
channelCapabilities: ctx.channelCapabilities ?? null,
|
|
655
|
+
guardianContext: ctx.guardianContext ?? null,
|
|
652
656
|
temporalContext,
|
|
653
657
|
});
|
|
654
658
|
preRepairMessages = runMessages;
|
|
@@ -25,6 +25,18 @@ export interface ChannelCapabilities {
|
|
|
25
25
|
supportsVoiceInput: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Guardian identity/trust context for external chat channels. */
|
|
29
|
+
export interface GuardianRuntimeContext {
|
|
30
|
+
sourceChannel: string;
|
|
31
|
+
actorRole: 'guardian' | 'non-guardian' | 'unverified_channel';
|
|
32
|
+
guardianChatId?: string;
|
|
33
|
+
guardianExternalUserId?: string;
|
|
34
|
+
requesterIdentifier?: string;
|
|
35
|
+
requesterExternalUserId?: string;
|
|
36
|
+
requesterChatId?: string;
|
|
37
|
+
denialReason?: 'no_binding' | 'no_identity';
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
/** Derive channel capabilities from a raw source channel identifier. */
|
|
29
41
|
export function resolveChannelCapabilities(sourceChannel?: string | null): ChannelCapabilities {
|
|
30
42
|
const channel = sourceChannel ?? 'dashboard';
|
|
@@ -264,6 +276,32 @@ export function injectChannelCapabilityContext(message: Message, caps: ChannelCa
|
|
|
264
276
|
};
|
|
265
277
|
}
|
|
266
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Prepend guardian trust/identity facts to the last user message so the
|
|
281
|
+
* model can reason about guardian status from deterministic runtime facts.
|
|
282
|
+
*/
|
|
283
|
+
export function injectGuardianContext(message: Message, ctx: GuardianRuntimeContext): Message {
|
|
284
|
+
const lines: string[] = ['<guardian_context>'];
|
|
285
|
+
lines.push(`source_channel: ${ctx.sourceChannel}`);
|
|
286
|
+
lines.push(`actor_role: ${ctx.actorRole}`);
|
|
287
|
+
lines.push(`guardian_external_user_id: ${ctx.guardianExternalUserId ?? 'unknown'}`);
|
|
288
|
+
lines.push(`guardian_chat_id: ${ctx.guardianChatId ?? 'unknown'}`);
|
|
289
|
+
lines.push(`requester_identifier: ${ctx.requesterIdentifier ?? 'unknown'}`);
|
|
290
|
+
lines.push(`requester_external_user_id: ${ctx.requesterExternalUserId ?? 'unknown'}`);
|
|
291
|
+
lines.push(`requester_chat_id: ${ctx.requesterChatId ?? 'unknown'}`);
|
|
292
|
+
lines.push(`denial_reason: ${ctx.denialReason ?? 'none'}`);
|
|
293
|
+
lines.push('</guardian_context>');
|
|
294
|
+
|
|
295
|
+
const block = lines.join('\n');
|
|
296
|
+
return {
|
|
297
|
+
...message,
|
|
298
|
+
content: [
|
|
299
|
+
{ type: 'text', text: block },
|
|
300
|
+
...message.content,
|
|
301
|
+
],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
267
305
|
// ---------------------------------------------------------------------------
|
|
268
306
|
// Prefix-based stripping primitive
|
|
269
307
|
// ---------------------------------------------------------------------------
|
|
@@ -298,6 +336,11 @@ export function stripChannelCapabilityContext(messages: Message[]): Message[] {
|
|
|
298
336
|
return stripUserTextBlocksByPrefix(messages, ['<channel_capabilities>']);
|
|
299
337
|
}
|
|
300
338
|
|
|
339
|
+
/** Strip `<guardian_context>` blocks injected by `injectGuardianContext`. */
|
|
340
|
+
export function stripGuardianContext(messages: Message[]): Message[] {
|
|
341
|
+
return stripUserTextBlocksByPrefix(messages, ['<guardian_context>']);
|
|
342
|
+
}
|
|
343
|
+
|
|
301
344
|
/**
|
|
302
345
|
* Prepend workspace top-level directory context to a user message.
|
|
303
346
|
*/
|
|
@@ -358,6 +401,7 @@ export function stripActiveSurfaceContext(messages: Message[]): Message[] {
|
|
|
358
401
|
/** Prefixes stripped by the pipeline (order doesn't matter — single pass). */
|
|
359
402
|
const RUNTIME_INJECTION_PREFIXES = [
|
|
360
403
|
'<channel_capabilities>',
|
|
404
|
+
'<guardian_context>',
|
|
361
405
|
'<workspace_top_level>',
|
|
362
406
|
TEMPORAL_INJECTED_PREFIX,
|
|
363
407
|
'<active_workspace>',
|
|
@@ -398,6 +442,7 @@ export function applyRuntimeInjections(
|
|
|
398
442
|
activeSurface?: ActiveSurfaceContext | null;
|
|
399
443
|
workspaceTopLevelContext?: string | null;
|
|
400
444
|
channelCapabilities?: ChannelCapabilities | null;
|
|
445
|
+
guardianContext?: GuardianRuntimeContext | null;
|
|
401
446
|
temporalContext?: string | null;
|
|
402
447
|
},
|
|
403
448
|
): Message[] {
|
|
@@ -433,6 +478,16 @@ export function applyRuntimeInjections(
|
|
|
433
478
|
}
|
|
434
479
|
}
|
|
435
480
|
|
|
481
|
+
if (options.guardianContext) {
|
|
482
|
+
const userTail = result[result.length - 1];
|
|
483
|
+
if (userTail && userTail.role === 'user') {
|
|
484
|
+
result = [
|
|
485
|
+
...result.slice(0, -1),
|
|
486
|
+
injectGuardianContext(userTail, options.guardianContext),
|
|
487
|
+
];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
436
491
|
// Temporal context is injected before workspace top-level so it
|
|
437
492
|
// appears after workspace context in the final message content
|
|
438
493
|
// (both are prepended, so later injections appear first).
|
|
@@ -41,6 +41,7 @@ import { projectSkillTools, type SkillProjectionCache } from './session-skill-to
|
|
|
41
41
|
*/
|
|
42
42
|
export interface ToolSetupContext extends SurfaceSessionContext {
|
|
43
43
|
readonly conversationId: string;
|
|
44
|
+
assistantId?: string;
|
|
44
45
|
currentRequestId?: string;
|
|
45
46
|
workingDir: string;
|
|
46
47
|
sandboxOverride?: boolean;
|
|
@@ -186,6 +187,7 @@ export function createToolExecutor(
|
|
|
186
187
|
workingDir: ctx.workingDir,
|
|
187
188
|
sessionId: ctx.conversationId,
|
|
188
189
|
conversationId: ctx.conversationId,
|
|
190
|
+
assistantId: ctx.assistantId,
|
|
189
191
|
requestId: ctx.currentRequestId,
|
|
190
192
|
taskRunId: ctx.taskRunId,
|
|
191
193
|
onOutput,
|
package/src/daemon/session.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { getHookManager } from '../hooks/manager.js';
|
|
|
38
38
|
import { ConflictGate } from './session-conflict-gate.js';
|
|
39
39
|
import { MessageQueue } from './session-queue-manager.js';
|
|
40
40
|
import type { QueueDrainReason } from './session-queue-manager.js';
|
|
41
|
-
import type { ChannelCapabilities } from './session-runtime-assembly.js';
|
|
41
|
+
import type { ChannelCapabilities, GuardianRuntimeContext } from './session-runtime-assembly.js';
|
|
42
42
|
import type { AssistantAttachmentDraft } from './assistant-attachments.js';
|
|
43
43
|
import {
|
|
44
44
|
handleSurfaceAction as handleSurfaceActionImpl,
|
|
@@ -127,6 +127,8 @@ export class Session {
|
|
|
127
127
|
/** @internal */ currentActiveSurfaceId?: string;
|
|
128
128
|
/** @internal */ currentPage?: string;
|
|
129
129
|
/** @internal */ channelCapabilities?: ChannelCapabilities;
|
|
130
|
+
/** @internal */ guardianContext?: GuardianRuntimeContext;
|
|
131
|
+
/** @internal */ assistantId?: string;
|
|
130
132
|
/** @internal */ pendingSurfaceActions = new Map<string, { surfaceType: SurfaceType }>();
|
|
131
133
|
/** @internal */ lastSurfaceAction = new Map<string, { actionId: string; data?: Record<string, unknown> }>();
|
|
132
134
|
/** @internal */ surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>();
|
|
@@ -327,6 +329,14 @@ export class Session {
|
|
|
327
329
|
this.channelCapabilities = caps ?? undefined;
|
|
328
330
|
}
|
|
329
331
|
|
|
332
|
+
setGuardianContext(ctx: GuardianRuntimeContext | null): void {
|
|
333
|
+
this.guardianContext = ctx ?? undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
setAssistantId(assistantId: string | null): void {
|
|
337
|
+
this.assistantId = assistantId ?? undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
330
340
|
persistUserMessage(
|
|
331
341
|
content: string,
|
|
332
342
|
attachments: UserMessageAttachment[],
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The canonical public base URL is resolved through a two-level chain:
|
|
7
7
|
*
|
|
8
|
-
* 1. **User Settings** (`config.ingress.publicBaseUrl`) — set via
|
|
9
|
-
* Settings UI or `config set ingress.publicBaseUrl`. This is the
|
|
8
|
+
* 1. **User Settings** (`config.ingress.publicBaseUrl`) — set via
|
|
9
|
+
* the in-chat config flow, the Settings UI, or `config set ingress.publicBaseUrl`. This is the
|
|
10
10
|
* primary source of truth. When the assistant spawns or restarts
|
|
11
11
|
* the gateway, this value is forwarded as the `INGRESS_PUBLIC_BASE_URL`
|
|
12
12
|
* environment variable so both processes agree on the same URL.
|
|
@@ -51,7 +51,7 @@ function normalizeUrl(url: string): string {
|
|
|
51
51
|
export function getPublicBaseUrl(config: IngressConfig): string {
|
|
52
52
|
if (config.ingress?.enabled === false) {
|
|
53
53
|
throw new Error(
|
|
54
|
-
'Public ingress is disabled.
|
|
54
|
+
'Public ingress is disabled. Ask the assistant to enable it, or update it from the Settings page.',
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -142,6 +142,7 @@ export function createBinding(params: {
|
|
|
142
142
|
guardianExternalUserId: string;
|
|
143
143
|
guardianDeliveryChatId: string;
|
|
144
144
|
verifiedVia?: string;
|
|
145
|
+
metadataJson?: string | null;
|
|
145
146
|
}): GuardianBinding {
|
|
146
147
|
const db = getDb();
|
|
147
148
|
const now = Date.now();
|
|
@@ -156,7 +157,7 @@ export function createBinding(params: {
|
|
|
156
157
|
status: 'active' as const,
|
|
157
158
|
verifiedAt: now,
|
|
158
159
|
verifiedVia: params.verifiedVia ?? 'challenge',
|
|
159
|
-
metadataJson: null,
|
|
160
|
+
metadataJson: params.metadataJson ?? null,
|
|
160
161
|
createdAt: now,
|
|
161
162
|
updatedAt: now,
|
|
162
163
|
};
|
package/src/memory/db-init.ts
CHANGED
|
@@ -1015,5 +1015,149 @@ export function initializeDb(): void {
|
|
|
1015
1015
|
|
|
1016
1016
|
database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_guardian_rate_limits_actor ON channel_guardian_rate_limits(assistant_id, channel, actor_external_user_id, actor_chat_id)`);
|
|
1017
1017
|
|
|
1018
|
+
// ── Media Assets ───────────────────────────────────────────────────
|
|
1019
|
+
|
|
1020
|
+
database.run(/*sql*/ `
|
|
1021
|
+
CREATE TABLE IF NOT EXISTS media_assets (
|
|
1022
|
+
id TEXT PRIMARY KEY,
|
|
1023
|
+
title TEXT NOT NULL,
|
|
1024
|
+
file_path TEXT NOT NULL,
|
|
1025
|
+
mime_type TEXT NOT NULL,
|
|
1026
|
+
duration_seconds REAL,
|
|
1027
|
+
file_hash TEXT NOT NULL,
|
|
1028
|
+
status TEXT NOT NULL DEFAULT 'registered',
|
|
1029
|
+
media_type TEXT NOT NULL,
|
|
1030
|
+
metadata TEXT,
|
|
1031
|
+
created_at INTEGER NOT NULL,
|
|
1032
|
+
updated_at INTEGER NOT NULL
|
|
1033
|
+
)
|
|
1034
|
+
`);
|
|
1035
|
+
|
|
1036
|
+
// Drop the old non-unique index so it can be recreated as UNIQUE (migration for existing databases)
|
|
1037
|
+
database.run(/*sql*/ `DROP INDEX IF EXISTS idx_media_assets_file_hash`);
|
|
1038
|
+
database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_media_assets_file_hash ON media_assets(file_hash)`);
|
|
1039
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_assets_status ON media_assets(status)`);
|
|
1040
|
+
|
|
1041
|
+
database.run(/*sql*/ `
|
|
1042
|
+
CREATE TABLE IF NOT EXISTS processing_stages (
|
|
1043
|
+
id TEXT PRIMARY KEY,
|
|
1044
|
+
asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
|
|
1045
|
+
stage TEXT NOT NULL,
|
|
1046
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1047
|
+
progress INTEGER NOT NULL DEFAULT 0,
|
|
1048
|
+
last_error TEXT,
|
|
1049
|
+
started_at INTEGER,
|
|
1050
|
+
completed_at INTEGER
|
|
1051
|
+
)
|
|
1052
|
+
`);
|
|
1053
|
+
|
|
1054
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_processing_stages_asset_id ON processing_stages(asset_id)`);
|
|
1055
|
+
|
|
1056
|
+
// ── Media Keyframes ─────────────────────────────────────────────────
|
|
1057
|
+
|
|
1058
|
+
database.run(/*sql*/ `
|
|
1059
|
+
CREATE TABLE IF NOT EXISTS media_keyframes (
|
|
1060
|
+
id TEXT PRIMARY KEY,
|
|
1061
|
+
asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
|
|
1062
|
+
timestamp REAL NOT NULL,
|
|
1063
|
+
file_path TEXT NOT NULL,
|
|
1064
|
+
metadata TEXT,
|
|
1065
|
+
created_at INTEGER NOT NULL
|
|
1066
|
+
)
|
|
1067
|
+
`);
|
|
1068
|
+
|
|
1069
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_keyframes_asset_id ON media_keyframes(asset_id)`);
|
|
1070
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_keyframes_asset_timestamp ON media_keyframes(asset_id, timestamp)`);
|
|
1071
|
+
|
|
1072
|
+
// ── Media Vision Outputs ────────────────────────────────────────────
|
|
1073
|
+
|
|
1074
|
+
database.run(/*sql*/ `
|
|
1075
|
+
CREATE TABLE IF NOT EXISTS media_vision_outputs (
|
|
1076
|
+
id TEXT PRIMARY KEY,
|
|
1077
|
+
asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
|
|
1078
|
+
keyframe_id TEXT NOT NULL REFERENCES media_keyframes(id) ON DELETE CASCADE,
|
|
1079
|
+
analysis_type TEXT NOT NULL,
|
|
1080
|
+
output TEXT NOT NULL,
|
|
1081
|
+
confidence REAL,
|
|
1082
|
+
created_at INTEGER NOT NULL
|
|
1083
|
+
)
|
|
1084
|
+
`);
|
|
1085
|
+
|
|
1086
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_vision_outputs_asset_id ON media_vision_outputs(asset_id)`);
|
|
1087
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_vision_outputs_keyframe_id ON media_vision_outputs(keyframe_id)`);
|
|
1088
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_vision_outputs_asset_type ON media_vision_outputs(asset_id, analysis_type)`);
|
|
1089
|
+
|
|
1090
|
+
// ── Media Timelines ─────────────────────────────────────────────────
|
|
1091
|
+
|
|
1092
|
+
database.run(/*sql*/ `
|
|
1093
|
+
CREATE TABLE IF NOT EXISTS media_timelines (
|
|
1094
|
+
id TEXT PRIMARY KEY,
|
|
1095
|
+
asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
|
|
1096
|
+
start_time REAL NOT NULL,
|
|
1097
|
+
end_time REAL NOT NULL,
|
|
1098
|
+
segment_type TEXT NOT NULL,
|
|
1099
|
+
attributes TEXT,
|
|
1100
|
+
confidence REAL,
|
|
1101
|
+
created_at INTEGER NOT NULL
|
|
1102
|
+
)
|
|
1103
|
+
`);
|
|
1104
|
+
|
|
1105
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_timelines_asset_id ON media_timelines(asset_id)`);
|
|
1106
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_timelines_asset_time ON media_timelines(asset_id, start_time)`);
|
|
1107
|
+
|
|
1108
|
+
// ── Media Events ──────────────────────────────────────────────────
|
|
1109
|
+
|
|
1110
|
+
database.run(/*sql*/ `
|
|
1111
|
+
CREATE TABLE IF NOT EXISTS media_events (
|
|
1112
|
+
id TEXT PRIMARY KEY,
|
|
1113
|
+
asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
|
|
1114
|
+
event_type TEXT NOT NULL,
|
|
1115
|
+
start_time REAL NOT NULL,
|
|
1116
|
+
end_time REAL NOT NULL,
|
|
1117
|
+
confidence REAL NOT NULL,
|
|
1118
|
+
reasons TEXT NOT NULL,
|
|
1119
|
+
metadata TEXT,
|
|
1120
|
+
created_at INTEGER NOT NULL
|
|
1121
|
+
)
|
|
1122
|
+
`);
|
|
1123
|
+
|
|
1124
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_events_asset_id ON media_events(asset_id)`);
|
|
1125
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_events_asset_type ON media_events(asset_id, event_type)`);
|
|
1126
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_events_confidence ON media_events(confidence DESC)`);
|
|
1127
|
+
|
|
1128
|
+
// ── Media Tracking Profiles ─────────────────────────────────────────
|
|
1129
|
+
|
|
1130
|
+
database.run(/*sql*/ `
|
|
1131
|
+
CREATE TABLE IF NOT EXISTS media_tracking_profiles (
|
|
1132
|
+
id TEXT PRIMARY KEY,
|
|
1133
|
+
asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
|
|
1134
|
+
capabilities TEXT NOT NULL,
|
|
1135
|
+
created_at INTEGER NOT NULL
|
|
1136
|
+
)
|
|
1137
|
+
`);
|
|
1138
|
+
|
|
1139
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_tracking_profiles_asset_id ON media_tracking_profiles(asset_id)`);
|
|
1140
|
+
|
|
1141
|
+
// ── Media Event Feedback ──────────────────────────────────────────
|
|
1142
|
+
|
|
1143
|
+
database.run(/*sql*/ `
|
|
1144
|
+
CREATE TABLE IF NOT EXISTS media_event_feedback (
|
|
1145
|
+
id TEXT PRIMARY KEY,
|
|
1146
|
+
asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
|
|
1147
|
+
event_id TEXT NOT NULL REFERENCES media_events(id) ON DELETE CASCADE,
|
|
1148
|
+
feedback_type TEXT NOT NULL,
|
|
1149
|
+
original_start_time REAL,
|
|
1150
|
+
original_end_time REAL,
|
|
1151
|
+
corrected_start_time REAL,
|
|
1152
|
+
corrected_end_time REAL,
|
|
1153
|
+
notes TEXT,
|
|
1154
|
+
created_at INTEGER NOT NULL
|
|
1155
|
+
)
|
|
1156
|
+
`);
|
|
1157
|
+
|
|
1158
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_event_feedback_asset_id ON media_event_feedback(asset_id)`);
|
|
1159
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_event_feedback_event_id ON media_event_feedback(event_id)`);
|
|
1160
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_event_feedback_type ON media_event_feedback(asset_id, feedback_type)`);
|
|
1161
|
+
|
|
1018
1162
|
migrateMemoryFtsBackfill(database);
|
|
1019
1163
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { getLogger } from '../../util/logger.js';
|
|
2
|
+
import { asString } from '../job-utils.js';
|
|
3
|
+
import { getMediaAssetById, updateMediaAssetStatus } from '../media-store.js';
|
|
4
|
+
import type { MemoryJob } from '../jobs-store.js';
|
|
5
|
+
import {
|
|
6
|
+
runPipeline,
|
|
7
|
+
type PipelineStageName,
|
|
8
|
+
type StageHandler,
|
|
9
|
+
} from '../../config/bundled-skills/media-processing/services/processing-pipeline.js';
|
|
10
|
+
import { extractKeyframesForAsset } from '../../config/bundled-skills/media-processing/tools/extract-keyframes.js';
|
|
11
|
+
import { analyzeKeyframesForAsset } from '../../config/bundled-skills/media-processing/tools/analyze-keyframes.js';
|
|
12
|
+
import { generateTimeline } from '../../config/bundled-skills/media-processing/services/timeline-service.js';
|
|
13
|
+
import {
|
|
14
|
+
detectEvents,
|
|
15
|
+
type DetectionConfig,
|
|
16
|
+
} from '../../config/bundled-skills/media-processing/services/event-detection-service.js';
|
|
17
|
+
|
|
18
|
+
const log = getLogger('media-processing-job');
|
|
19
|
+
|
|
20
|
+
const defaultDetectionConfig: DetectionConfig = {
|
|
21
|
+
eventType: 'scene_change',
|
|
22
|
+
rules: [
|
|
23
|
+
{
|
|
24
|
+
ruleType: 'segment_transition',
|
|
25
|
+
params: { field: 'segmentType' },
|
|
26
|
+
weight: 1.0,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function mediaProcessingJob(job: MemoryJob): Promise<void> {
|
|
32
|
+
const mediaAssetId = asString(job.payload.mediaAssetId);
|
|
33
|
+
if (!mediaAssetId) {
|
|
34
|
+
log.warn({ jobId: job.id }, 'Missing mediaAssetId in job payload');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const asset = getMediaAssetById(mediaAssetId);
|
|
39
|
+
if (!asset) {
|
|
40
|
+
log.warn({ jobId: job.id, mediaAssetId }, 'Media asset not found');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (asset.mediaType !== 'video') {
|
|
45
|
+
log.info(
|
|
46
|
+
{ assetId: mediaAssetId, mediaType: asset.mediaType },
|
|
47
|
+
'Skipping media processing pipeline — only video assets are supported',
|
|
48
|
+
);
|
|
49
|
+
updateMediaAssetStatus(mediaAssetId, 'indexed');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Build detection config, allowing optional eventType override from payload
|
|
54
|
+
const eventType = asString(job.payload.eventType);
|
|
55
|
+
const detectionConfig: DetectionConfig = eventType
|
|
56
|
+
? { ...defaultDetectionConfig, eventType }
|
|
57
|
+
: defaultDetectionConfig;
|
|
58
|
+
|
|
59
|
+
const handlers: Record<PipelineStageName, StageHandler> = {
|
|
60
|
+
keyframe_extraction: {
|
|
61
|
+
execute: (assetId, onProgress) =>
|
|
62
|
+
extractKeyframesForAsset(assetId, 1, onProgress),
|
|
63
|
+
},
|
|
64
|
+
vision_analysis: {
|
|
65
|
+
execute: (assetId, onProgress) =>
|
|
66
|
+
analyzeKeyframesForAsset(assetId, undefined, undefined, onProgress),
|
|
67
|
+
},
|
|
68
|
+
timeline_generation: {
|
|
69
|
+
execute: async (assetId, onProgress) => {
|
|
70
|
+
generateTimeline(assetId, { onProgress });
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
event_detection: {
|
|
74
|
+
execute: async (assetId, onProgress) => {
|
|
75
|
+
detectEvents(assetId, detectionConfig, { onProgress });
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const result = await runPipeline(mediaAssetId, handlers, {
|
|
81
|
+
onProgress: (msg) => log.info({ mediaAssetId }, msg),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
log.info(
|
|
85
|
+
{
|
|
86
|
+
mediaAssetId,
|
|
87
|
+
completedStages: result.completedStages,
|
|
88
|
+
failedStage: result.failedStage,
|
|
89
|
+
cancelled: result.cancelled,
|
|
90
|
+
},
|
|
91
|
+
'Media processing pipeline finished',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (result.failedStage) {
|
|
95
|
+
throw new Error(`Media processing failed at stage ${result.failedStage}: ${result.failureReason}`);
|
|
96
|
+
}
|
|
97
|
+
if (result.cancelled) {
|
|
98
|
+
throw new Error(`Media processing cancelled for asset ${mediaAssetId}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/memory/jobs-store.ts
CHANGED
|
@@ -20,7 +20,8 @@ export type MemoryJobType =
|
|
|
20
20
|
| 'build_conversation_summary'
|
|
21
21
|
| 'backfill'
|
|
22
22
|
| 'rebuild_index'
|
|
23
|
-
| 'delete_qdrant_vectors'
|
|
23
|
+
| 'delete_qdrant_vectors'
|
|
24
|
+
| 'media_processing';
|
|
24
25
|
|
|
25
26
|
const EMBED_JOB_TYPES: MemoryJobType[] = ['embed_segment', 'embed_item', 'embed_summary'];
|
|
26
27
|
|
|
@@ -29,6 +29,7 @@ import { checkContradictionsJob, cleanupStaleSupersededItemsJob } from './job-ha
|
|
|
29
29
|
import { buildConversationSummaryJob, buildGlobalSummaryJob } from './job-handlers/summarization.js';
|
|
30
30
|
import { backfillJob, backfillEntityRelationsJob } from './job-handlers/backfill.js';
|
|
31
31
|
import { rebuildIndexJob, deleteQdrantVectorsJob } from './job-handlers/index-maintenance.js';
|
|
32
|
+
import { mediaProcessingJob } from './job-handlers/media-processing.js';
|
|
32
33
|
|
|
33
34
|
// Re-export public utilities consumed by tests and other modules
|
|
34
35
|
export { currentWeekWindow } from './job-utils.js';
|
|
@@ -229,6 +230,9 @@ async function processJob(job: MemoryJob, config: AssistantConfig): Promise<void
|
|
|
229
230
|
case 'delete_qdrant_vectors':
|
|
230
231
|
await deleteQdrantVectorsJob(job);
|
|
231
232
|
return;
|
|
233
|
+
case 'media_processing':
|
|
234
|
+
await mediaProcessingJob(job);
|
|
235
|
+
return;
|
|
232
236
|
default:
|
|
233
237
|
throw new Error(`Unknown memory job type: ${(job as { type: string }).type}`);
|
|
234
238
|
}
|