@jini-ai/daemon 0.3.7 → 0.5.0
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/CHANGELOG.md +119 -0
- package/README.md +50 -8
- package/dist/agent-executor/contracts.d.ts +736 -0
- package/dist/agent-executor/contracts.js +28 -0
- package/dist/agent-executor/event-translation.d.ts +87 -0
- package/dist/agent-executor/event-translation.js +230 -0
- package/dist/agent-executor/events.d.ts +190 -0
- package/dist/agent-executor/events.js +848 -0
- package/dist/agent-executor/handoff.d.ts +18 -0
- package/dist/agent-executor/handoff.js +16 -0
- package/dist/agent-executor/index.d.ts +98 -0
- package/dist/agent-executor/index.js +98 -0
- package/dist/agent-executor/launch.d.ts +375 -0
- package/dist/agent-executor/launch.js +1001 -0
- package/dist/agent-executor/mcp-bridge.d.ts +338 -0
- package/dist/agent-executor/mcp-bridge.js +835 -0
- package/dist/agent-executor/values.d.ts +5 -0
- package/dist/agent-executor/values.js +13 -0
- package/dist/agent-executor.d.ts +382 -177
- package/dist/agent-executor.js +408 -344
- package/dist/close-status.d.ts +11 -8
- package/dist/close-status.js +31 -19
- package/dist/continuation/continuation-transport.d.ts +3 -1
- package/dist/continuation/continuation-transport.js +1 -1
- package/dist/continuation/journal.d.ts +12 -5
- package/dist/continuation/journal.js +4 -4
- package/dist/continuation/run-scoped-context-store.d.ts +13 -4
- package/dist/continuation/run-scoped-context-store.js +6 -6
- package/dist/continuation/run-start-handler.d.ts +1 -1
- package/dist/continuation/run-start-handler.js +3 -16
- package/dist/delegated-tool-bridge.d.ts +7 -3
- package/dist/delegated-tool-bridge.js +32 -31
- package/dist/event-log.d.ts +7 -5
- package/dist/event-log.js +12 -11
- package/dist/frontend-capability-tools.d.ts +1 -1
- package/dist/frontend-capability-tools.js +3 -2
- package/dist/frontend-session-registry.d.ts +32 -9
- package/dist/frontend-session-registry.js +12 -11
- package/dist/http/active-context.d.ts +105 -0
- package/dist/http/active-context.js +67 -0
- package/dist/http/agents.d.ts +68 -0
- package/dist/http/agents.js +12 -0
- package/dist/http/attachments.d.ts +618 -0
- package/dist/http/attachments.js +1034 -0
- package/dist/http/cancel-owned-runs.d.ts +49 -0
- package/dist/http/cancel-owned-runs.js +35 -0
- package/dist/http/component-catalog.d.ts +84 -0
- package/dist/http/component-catalog.js +64 -0
- package/dist/http/connectors.d.ts +209 -0
- package/dist/http/connectors.js +409 -0
- package/dist/http/daemon-status.d.ts +76 -0
- package/dist/http/daemon-status.js +55 -0
- package/dist/http/db-ops.d.ts +123 -0
- package/dist/http/db-ops.js +189 -0
- package/dist/http/delegated-tools.d.ts +113 -0
- package/dist/http/delegated-tools.js +230 -0
- package/dist/http/frontend-control.d.ts +136 -0
- package/dist/http/frontend-control.js +79 -0
- package/dist/http/frontend-sessions.d.ts +110 -0
- package/dist/http/frontend-sessions.js +180 -0
- package/dist/http/health.d.ts +95 -0
- package/dist/http/health.js +40 -0
- package/dist/http/host-tools.d.ts +214 -0
- package/dist/http/host-tools.js +329 -0
- package/dist/http/index.d.ts +52 -0
- package/dist/http/index.js +27 -0
- package/dist/http/media.d.ts +146 -0
- package/dist/http/media.js +346 -0
- package/dist/http/memory.d.ts +265 -0
- package/dist/http/memory.js +328 -0
- package/dist/http/model-proxy.d.ts +49 -0
- package/dist/http/model-proxy.js +642 -0
- package/dist/http/remote-run-events.d.ts +70 -0
- package/dist/http/remote-run-events.js +224 -0
- package/dist/http/research.d.ts +48 -0
- package/dist/http/research.js +315 -0
- package/dist/http/route-manifest.d.ts +35 -0
- package/dist/http/route-manifest.js +103 -0
- package/dist/http/routines.d.ts +120 -0
- package/dist/http/routines.js +298 -0
- package/dist/http/run-credentials.d.ts +73 -0
- package/dist/http/run-credentials.js +71 -0
- package/dist/http/runs.d.ts +91 -0
- package/dist/http/runs.js +221 -0
- package/dist/http/terminals.d.ts +138 -0
- package/dist/http/terminals.js +325 -0
- package/dist/http/tool-catalog.d.ts +78 -0
- package/dist/http/tool-catalog.js +66 -0
- package/dist/http/workspace-root.d.ts +77 -0
- package/dist/http/workspace-root.js +63 -0
- package/dist/http/xai.d.ts +90 -0
- package/dist/http/xai.js +655 -0
- package/dist/image-prompt-delivery.d.ts +13 -3
- package/dist/image-prompt-delivery.js +5 -5
- package/dist/index.d.ts +556 -4
- package/dist/index.js +556 -4
- package/dist/legacy-data-migration.d.ts +59 -8
- package/dist/legacy-data-migration.js +38 -33
- package/dist/read-only-tools.d.ts +130 -0
- package/dist/read-only-tools.js +115 -0
- package/dist/remote-tool-bridge.d.ts +11 -5
- package/dist/remote-tool-bridge.js +15 -15
- package/dist/routines/index.d.ts +1 -1
- package/dist/routines/index.js +1 -1
- package/dist/routines/routine-store.d.ts +34 -12
- package/dist/routines/routine-store.js +21 -20
- package/dist/routines/schedule.d.ts +48 -10
- package/dist/routines/schedule.js +21 -21
- package/dist/routines/scheduler.d.ts +32 -11
- package/dist/routines/scheduler.js +61 -57
- package/dist/routines/types.d.ts +10 -3
- package/dist/run/core/result.d.ts +6 -2
- package/dist/run/core/result.js +3 -3
- package/dist/run/core/retry.d.ts +17 -4
- package/dist/run/core/retry.js +7 -11
- package/dist/run/diagnostics/diagnostics.d.ts +14 -4
- package/dist/run/diagnostics/diagnostics.js +7 -7
- package/dist/run/index.d.ts +1 -1
- package/dist/run/index.js +1 -1
- package/dist/run-credentials.d.ts +138 -0
- package/dist/run-credentials.js +152 -0
- package/dist/run-lifecycle.d.ts +92 -33
- package/dist/run-lifecycle.js +86 -73
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.js +2 -0
- package/dist/session-coordination.d.ts +115 -0
- package/dist/session-coordination.js +111 -0
- package/dist/store/agent-sessions/errors.d.ts +8 -0
- package/dist/store/agent-sessions/errors.js +10 -0
- package/dist/store/agent-sessions/index.d.ts +12 -0
- package/dist/store/agent-sessions/index.js +19 -0
- package/dist/store/agent-sessions/pglite.d.ts +12 -0
- package/dist/store/agent-sessions/pglite.js +7 -0
- package/dist/store/agent-sessions/ports.d.ts +24 -0
- package/dist/store/agent-sessions/ports.js +2 -0
- package/dist/store/agent-sessions/postgres.d.ts +12 -0
- package/dist/store/agent-sessions/postgres.js +7 -0
- package/dist/store/agent-sessions/schema.d.ts +3 -0
- package/dist/store/agent-sessions/schema.js +23 -0
- package/dist/store/agent-sessions/sql.d.ts +35 -0
- package/dist/store/agent-sessions/sql.js +59 -0
- package/dist/store/agent-sessions/sqlite.d.ts +117 -0
- package/dist/store/agent-sessions/sqlite.js +128 -0
- package/dist/store/event-log/sqlite.d.ts +75 -0
- package/dist/store/event-log/sqlite.js +230 -0
- package/dist/surface-exchanges.d.ts +97 -0
- package/dist/surface-exchanges.js +153 -0
- package/dist/terminal-session.d.ts +43 -11
- package/dist/terminal-session.js +22 -20
- package/dist/tokens.js +4 -4
- package/dist/tool-audit.d.ts +114 -0
- package/dist/tool-audit.js +95 -0
- package/dist/tool-executor.d.ts +22 -7
- package/dist/tool-executor.js +16 -16
- package/dist/tool-result-media.d.ts +3 -1
- package/dist/tool-result-media.js +1 -1
- package/dist/tool-result-surfaces.d.ts +3 -1
- package/dist/tool-result-surfaces.js +1 -1
- package/package.json +127 -9
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import { type AdapterContext } from '@jini-ai/http-kit';
|
|
3
|
+
type MediaSurface = 'image' | 'video' | 'audio';
|
|
4
|
+
type AudioKind = 'music' | 'speech' | 'sfx';
|
|
5
|
+
type MediaSpeechFormat = 'mp3' | 'wav' | 'flac' | 'aac' | 'opus';
|
|
6
|
+
interface MediaImageReference {
|
|
7
|
+
readonly dataUrl: string;
|
|
8
|
+
}
|
|
9
|
+
interface MediaGenerationRequest {
|
|
10
|
+
readonly surface: MediaSurface;
|
|
11
|
+
readonly model: string;
|
|
12
|
+
readonly prompt?: string;
|
|
13
|
+
readonly aspect?: string;
|
|
14
|
+
readonly length?: number;
|
|
15
|
+
readonly duration?: number;
|
|
16
|
+
readonly voice?: string;
|
|
17
|
+
readonly audioKind?: AudioKind;
|
|
18
|
+
readonly language?: string;
|
|
19
|
+
readonly loop?: boolean;
|
|
20
|
+
readonly promptInfluence?: number;
|
|
21
|
+
readonly imageRef?: MediaImageReference;
|
|
22
|
+
readonly imageRefs?: readonly MediaImageReference[];
|
|
23
|
+
readonly wireModel?: string;
|
|
24
|
+
readonly speechFormat?: MediaSpeechFormat;
|
|
25
|
+
}
|
|
26
|
+
interface MediaGenerationResult {
|
|
27
|
+
readonly bytes: Buffer;
|
|
28
|
+
readonly providerNote: string;
|
|
29
|
+
readonly suggestedExt?: string;
|
|
30
|
+
readonly providerId: string;
|
|
31
|
+
readonly usedStubFallback: boolean;
|
|
32
|
+
readonly warnings: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
interface MediaDispatchEngine {
|
|
35
|
+
generate(requiredArgs: Pick<MediaGenerationRequest, 'surface' | 'model'>, optionalArgs?: Omit<MediaGenerationRequest, 'surface' | 'model'>): Promise<MediaGenerationResult>;
|
|
36
|
+
}
|
|
37
|
+
type MediaTaskStatus = 'queued' | 'running' | 'done' | 'failed' | 'interrupted';
|
|
38
|
+
interface MediaTaskError {
|
|
39
|
+
readonly message: string;
|
|
40
|
+
readonly status?: number;
|
|
41
|
+
readonly code?: string;
|
|
42
|
+
}
|
|
43
|
+
interface MediaTask {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly ownerRef: string;
|
|
46
|
+
readonly status: MediaTaskStatus;
|
|
47
|
+
readonly surface?: string;
|
|
48
|
+
readonly model?: string;
|
|
49
|
+
readonly progress: readonly string[];
|
|
50
|
+
readonly file: unknown | null;
|
|
51
|
+
readonly error: MediaTaskError | null;
|
|
52
|
+
readonly startedAt: number;
|
|
53
|
+
readonly endedAt: number | null;
|
|
54
|
+
readonly createdAt: number;
|
|
55
|
+
readonly updatedAt: number;
|
|
56
|
+
}
|
|
57
|
+
interface MediaTaskCreateInput {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly ownerRef: string;
|
|
60
|
+
readonly status?: MediaTaskStatus;
|
|
61
|
+
readonly surface?: string;
|
|
62
|
+
readonly model?: string;
|
|
63
|
+
readonly progress?: readonly string[];
|
|
64
|
+
readonly file?: unknown | null;
|
|
65
|
+
readonly error?: MediaTaskError | null;
|
|
66
|
+
readonly startedAt?: number;
|
|
67
|
+
}
|
|
68
|
+
interface MediaTaskPatch {
|
|
69
|
+
readonly status?: MediaTaskStatus;
|
|
70
|
+
readonly surface?: string | null;
|
|
71
|
+
readonly model?: string | null;
|
|
72
|
+
readonly progress?: readonly string[];
|
|
73
|
+
readonly file?: unknown | null;
|
|
74
|
+
readonly error?: MediaTaskError | null;
|
|
75
|
+
readonly endedAt?: number | null;
|
|
76
|
+
}
|
|
77
|
+
interface MediaTaskListOptions {
|
|
78
|
+
readonly includeTerminal?: boolean;
|
|
79
|
+
}
|
|
80
|
+
interface MediaTaskStore {
|
|
81
|
+
create(requiredArgs: {
|
|
82
|
+
readonly input: MediaTaskCreateInput;
|
|
83
|
+
}): Promise<MediaTask>;
|
|
84
|
+
get({ id }: {
|
|
85
|
+
readonly id: string;
|
|
86
|
+
}, _optional?: Record<string, never>): Promise<MediaTask | null>;
|
|
87
|
+
update({ id, patch }: {
|
|
88
|
+
readonly id: string;
|
|
89
|
+
readonly patch: MediaTaskPatch;
|
|
90
|
+
}, _optional?: Record<string, never>): Promise<MediaTask | null>;
|
|
91
|
+
listByOwner({ ownerRef }: {
|
|
92
|
+
readonly ownerRef: string;
|
|
93
|
+
}, options?: MediaTaskListOptions): Promise<MediaTask[]>;
|
|
94
|
+
delete({ id }: {
|
|
95
|
+
readonly id: string;
|
|
96
|
+
}, _optional?: Record<string, never>): Promise<void>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Diagnostic detail for an internal-error response the public API deliberately does not disclose
|
|
100
|
+
* (SEC-005), matching `delegated-tools.ts`'s `DelegatedToolsInternalErrorContext` precedent.
|
|
101
|
+
*/
|
|
102
|
+
export interface MediaInternalErrorContext {
|
|
103
|
+
readonly source: 'media-generate-dispatch' | 'media-task-store';
|
|
104
|
+
readonly taskId: string | null;
|
|
105
|
+
readonly ownerRef: string | null;
|
|
106
|
+
readonly correlationId: string;
|
|
107
|
+
readonly error: unknown;
|
|
108
|
+
}
|
|
109
|
+
export interface MediaHttpDeps {
|
|
110
|
+
readonly engine: MediaDispatchEngine;
|
|
111
|
+
readonly taskStore: MediaTaskStore;
|
|
112
|
+
/** Host-owned sink for the real exception behind a generic `INTERNAL_ERROR` response (SEC-005). Defaults to `console.error`. */
|
|
113
|
+
readonly onInternalError?: (context: MediaInternalErrorContext) => void;
|
|
114
|
+
}
|
|
115
|
+
interface MediaGenerateRequest {
|
|
116
|
+
readonly ownerRef: string;
|
|
117
|
+
readonly request: MediaGenerationRequest;
|
|
118
|
+
}
|
|
119
|
+
interface MediaTaskListQuery {
|
|
120
|
+
readonly ownerRef: string;
|
|
121
|
+
readonly options: MediaTaskListOptions;
|
|
122
|
+
}
|
|
123
|
+
export interface MediaGenerateResponse {
|
|
124
|
+
readonly task: MediaTask;
|
|
125
|
+
}
|
|
126
|
+
export declare const mediaGenerateRoute: import("@jini-ai/http-kit").JsonRouteSpec<MediaGenerateRequest, MediaGenerateResponse, MediaHttpDeps>;
|
|
127
|
+
export interface MediaTaskResponse {
|
|
128
|
+
readonly task: MediaTask;
|
|
129
|
+
}
|
|
130
|
+
export declare const mediaTaskGetRoute: import("@jini-ai/http-kit").JsonRouteSpec<string, MediaTaskResponse, MediaHttpDeps>;
|
|
131
|
+
export interface MediaTaskDeleteResponse {
|
|
132
|
+
readonly ok: true;
|
|
133
|
+
}
|
|
134
|
+
export declare const mediaTaskDeleteRoute: import("@jini-ai/http-kit").JsonRouteSpec<string, MediaTaskDeleteResponse, MediaHttpDeps>;
|
|
135
|
+
export interface MediaTaskListResponse {
|
|
136
|
+
readonly tasks: readonly MediaTask[];
|
|
137
|
+
}
|
|
138
|
+
export declare const mediaTaskListRoute: import("@jini-ai/http-kit").JsonRouteSpec<MediaTaskListQuery, MediaTaskListResponse, MediaHttpDeps>;
|
|
139
|
+
/** Mounts every media route on `app`. A pack's `http(app, services)` calls this directly. */
|
|
140
|
+
export declare function registerMediaRoutes({ app, deps, adapter }: {
|
|
141
|
+
readonly app: Express;
|
|
142
|
+
readonly deps: MediaHttpDeps;
|
|
143
|
+
readonly adapter: AdapterContext;
|
|
144
|
+
}, _optional?: Record<string, never>): void;
|
|
145
|
+
export {};
|
|
146
|
+
//# sourceMappingURL=media.d.ts.map
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module media
|
|
3
|
+
*
|
|
4
|
+
* `POST /api/media/generate`, `GET /api/media/tasks`, `GET /api/media/tasks/:id`, `DELETE
|
|
5
|
+
* /api/media/tasks/:id` — an HTTP route pack over narrow, transport-owned engine/task-store ports.
|
|
6
|
+
* A product composition root may satisfy those ports with `@jini-ai/media`; the daemon route pack
|
|
7
|
+
* deliberately does not point down into that incubating implementation package.
|
|
8
|
+
*
|
|
9
|
+
* Follows the "request-now, poll-later" async-task shape `@jini-ai/media`'s own `MediaTaskStore` doc
|
|
10
|
+
* describes: `POST /api/media/generate` creates a task, kicks off `MediaDispatchEngine.generate()`
|
|
11
|
+
* in the background (never awaited by the response — a real provider call can take tens of
|
|
12
|
+
* seconds), and returns the freshly-`'queued'` task immediately. A caller polls
|
|
13
|
+
* `GET /api/media/tasks/:id` until the task reaches a terminal status (`'done'`/`'failed'`/
|
|
14
|
+
* `'interrupted'`), mirroring `routines.ts`'s `POST /api/routines/:id/run` (`202`, poll separately)
|
|
15
|
+
* rather than `runs.ts`'s SSE stream — media generation has no meaningful intermediate event
|
|
16
|
+
* stream the way a run does, just one eventual result.
|
|
17
|
+
*
|
|
18
|
+
* **Where generated bytes land** (per `@jini-ai/media`'s own boundary doc on `MediaGenerationRequest`/
|
|
19
|
+
* `MediaGenerationResult`: "this engine performs no filesystem I/O... a host pairs this with its
|
|
20
|
+
* own wiring to decide where generated bytes land"): this route pack encodes the result as a
|
|
21
|
+
* base64 `data:` URL and stores it directly in `MediaTask.file` (a deliberately untyped `unknown`
|
|
22
|
+
* field — see `task-store.ts`'s own doc). This is the simplest host-owned answer that needs no new
|
|
23
|
+
* storage port of its own; a host that wants generated bytes written to a real asset store instead
|
|
24
|
+
* can layer that on top by reading `MediaTask.file` off a terminal task and re-persisting it
|
|
25
|
+
* elsewhere — this route pack does not preclude that, it just doesn't invent a new persistence
|
|
26
|
+
* abstraction to do it for a caller who hasn't asked for one (matching `packages/deploy/source-
|
|
27
|
+
* map.md`'s "no speculative port" precedent).
|
|
28
|
+
*
|
|
29
|
+
* **`ownerRef`** is the same opaque, host-defined scoping key `MediaTaskStore` itself already
|
|
30
|
+
* uses (e.g. a run id) — this route pack has no `Project`/`Workspace` noun of its own, matching
|
|
31
|
+
* `active-context.ts`/`delegated-tools.ts`'s precedent for the same category of coupling.
|
|
32
|
+
*
|
|
33
|
+
* **SEC-005 redaction**: `MediaDispatchEngine.generate()` can throw a raw vendor-SDK/network
|
|
34
|
+
* error (API keys, response bodies) that must never reach an HTTP caller verbatim — matching
|
|
35
|
+
* `delegated-tools.ts`'s `reportInternalError` pattern exactly (a correlation-id-bearing generic
|
|
36
|
+
* `INTERNAL_ERROR`, the real error only reaching a host-owned sink). Applied to the background
|
|
37
|
+
* generation failure path (recorded on the task's own `error` field, `message` already redacted
|
|
38
|
+
* the same way) as well as the sync per-request validation-throw path (`POST /api/media/generate`
|
|
39
|
+
* itself can throw before ever creating a task — an unregistered `toolId`-shaped programming
|
|
40
|
+
* error, per `MediaDispatchEngine.generate`'s own contract).
|
|
41
|
+
*/
|
|
42
|
+
import { randomUUID } from 'node:crypto';
|
|
43
|
+
import { createApiError } from '@jini-ai/protocol';
|
|
44
|
+
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
45
|
+
import { validationError } from '@jini-ai/http-kit';
|
|
46
|
+
import { err, ok } from '@jini-ai/http-kit';
|
|
47
|
+
const SURFACES = new Set(['image', 'video', 'audio']);
|
|
48
|
+
const AUDIO_KINDS = new Set(['music', 'speech', 'sfx']);
|
|
49
|
+
const SPEECH_FORMATS = new Set(['mp3', 'wav', 'flac', 'aac', 'opus']);
|
|
50
|
+
function defaultInternalErrorSink(context) {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
53
|
+
}
|
|
54
|
+
function reportInternalError(deps, source, error, taskId, ownerRef) {
|
|
55
|
+
const correlationId = randomUUID();
|
|
56
|
+
const sink = deps.onInternalError ?? defaultInternalErrorSink;
|
|
57
|
+
sink({ source, taskId, ownerRef, correlationId, error });
|
|
58
|
+
return createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId });
|
|
59
|
+
}
|
|
60
|
+
function isRecord(value) {
|
|
61
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
function requireNonEmptyString(body, key) {
|
|
64
|
+
const value = body[key];
|
|
65
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
function parseOptionalString(body, key) {
|
|
68
|
+
const value = body[key];
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
return ok({ value: undefined });
|
|
71
|
+
if (typeof value !== 'string')
|
|
72
|
+
return err({ error: validationError({ message: `${key} must be a string when provided` }) });
|
|
73
|
+
return ok({ value });
|
|
74
|
+
}
|
|
75
|
+
function parseOptionalNumber(body, key) {
|
|
76
|
+
const value = body[key];
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
return ok({ value: undefined });
|
|
79
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
80
|
+
return err({ error: validationError({ message: `${key} must be a finite number when provided` }) });
|
|
81
|
+
return ok({ value });
|
|
82
|
+
}
|
|
83
|
+
function parseOptionalBoolean(body, key) {
|
|
84
|
+
const value = body[key];
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
return ok({ value: undefined });
|
|
87
|
+
if (typeof value !== 'boolean')
|
|
88
|
+
return err({ error: validationError({ message: `${key} must be a boolean when provided` }) });
|
|
89
|
+
return ok({ value });
|
|
90
|
+
}
|
|
91
|
+
function parseImageReference(value, field) {
|
|
92
|
+
if (!isRecord(value) || typeof value.dataUrl !== 'string' || value.dataUrl.trim().length === 0) {
|
|
93
|
+
return err({ error: validationError({ message: `${field} must be an object with a non-empty dataUrl string` }) });
|
|
94
|
+
}
|
|
95
|
+
return ok({ value: { dataUrl: value.dataUrl } });
|
|
96
|
+
}
|
|
97
|
+
function parseOptionalImageRef(body) {
|
|
98
|
+
const value = body.imageRef;
|
|
99
|
+
if (value === undefined)
|
|
100
|
+
return ok({ value: undefined });
|
|
101
|
+
return parseImageReference(value, 'imageRef');
|
|
102
|
+
}
|
|
103
|
+
function parseOptionalImageRefs(body) {
|
|
104
|
+
const value = body.imageRefs;
|
|
105
|
+
if (value === undefined)
|
|
106
|
+
return ok({ value: undefined });
|
|
107
|
+
if (!Array.isArray(value))
|
|
108
|
+
return err({ error: validationError({ message: 'imageRefs must be an array when provided' }) });
|
|
109
|
+
const refs = [];
|
|
110
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
111
|
+
const parsed = parseImageReference(value[i], `imageRefs[${i}]`);
|
|
112
|
+
if (!parsed.ok)
|
|
113
|
+
return parsed;
|
|
114
|
+
refs.push(parsed.value);
|
|
115
|
+
}
|
|
116
|
+
return ok({ value: refs });
|
|
117
|
+
}
|
|
118
|
+
function parseMediaGenerate(input) {
|
|
119
|
+
if (!isRecord(input.body))
|
|
120
|
+
return err({ error: validationError({ message: 'body must be a JSON object' }) });
|
|
121
|
+
const body = input.body;
|
|
122
|
+
const ownerRef = requireNonEmptyString(body, 'ownerRef');
|
|
123
|
+
if (ownerRef === undefined) {
|
|
124
|
+
return err({ error: validationError({ message: 'ownerRef must be a non-empty string' }, { issues: [{ path: 'ownerRef', message: 'required non-empty string' }] }) });
|
|
125
|
+
}
|
|
126
|
+
const surface = body.surface;
|
|
127
|
+
if (typeof surface !== 'string' || !SURFACES.has(surface)) {
|
|
128
|
+
return err({ error: validationError({ message: 'surface must be one of: image, video, audio' }) });
|
|
129
|
+
}
|
|
130
|
+
const model = requireNonEmptyString(body, 'model');
|
|
131
|
+
if (model === undefined) {
|
|
132
|
+
return err({ error: validationError({ message: 'model must be a non-empty string' }, { issues: [{ path: 'model', message: 'required non-empty string' }] }) });
|
|
133
|
+
}
|
|
134
|
+
const audioKind = body.audioKind;
|
|
135
|
+
if (audioKind !== undefined && (typeof audioKind !== 'string' || !AUDIO_KINDS.has(audioKind))) {
|
|
136
|
+
return err({ error: validationError({ message: 'audioKind must be one of: music, speech, sfx when provided' }) });
|
|
137
|
+
}
|
|
138
|
+
const speechFormat = body.speechFormat;
|
|
139
|
+
if (speechFormat !== undefined && (typeof speechFormat !== 'string' || !SPEECH_FORMATS.has(speechFormat))) {
|
|
140
|
+
return err({ error: validationError({ message: 'speechFormat must be one of: mp3, wav, flac, aac, opus when provided' }) });
|
|
141
|
+
}
|
|
142
|
+
const prompt = parseOptionalString(body, 'prompt');
|
|
143
|
+
if (!prompt.ok)
|
|
144
|
+
return prompt;
|
|
145
|
+
const aspect = parseOptionalString(body, 'aspect');
|
|
146
|
+
if (!aspect.ok)
|
|
147
|
+
return aspect;
|
|
148
|
+
const voice = parseOptionalString(body, 'voice');
|
|
149
|
+
if (!voice.ok)
|
|
150
|
+
return voice;
|
|
151
|
+
const language = parseOptionalString(body, 'language');
|
|
152
|
+
if (!language.ok)
|
|
153
|
+
return language;
|
|
154
|
+
const wireModel = parseOptionalString(body, 'wireModel');
|
|
155
|
+
if (!wireModel.ok)
|
|
156
|
+
return wireModel;
|
|
157
|
+
const length = parseOptionalNumber(body, 'length');
|
|
158
|
+
if (!length.ok)
|
|
159
|
+
return length;
|
|
160
|
+
const duration = parseOptionalNumber(body, 'duration');
|
|
161
|
+
if (!duration.ok)
|
|
162
|
+
return duration;
|
|
163
|
+
const promptInfluence = parseOptionalNumber(body, 'promptInfluence');
|
|
164
|
+
if (!promptInfluence.ok)
|
|
165
|
+
return promptInfluence;
|
|
166
|
+
const loop = parseOptionalBoolean(body, 'loop');
|
|
167
|
+
if (!loop.ok)
|
|
168
|
+
return loop;
|
|
169
|
+
const imageRef = parseOptionalImageRef(body);
|
|
170
|
+
if (!imageRef.ok)
|
|
171
|
+
return imageRef;
|
|
172
|
+
const imageRefs = parseOptionalImageRefs(body);
|
|
173
|
+
if (!imageRefs.ok)
|
|
174
|
+
return imageRefs;
|
|
175
|
+
return ok({
|
|
176
|
+
value: {
|
|
177
|
+
ownerRef,
|
|
178
|
+
request: {
|
|
179
|
+
surface: surface,
|
|
180
|
+
model,
|
|
181
|
+
...(prompt.value !== undefined ? { prompt: prompt.value } : {}),
|
|
182
|
+
...(aspect.value !== undefined ? { aspect: aspect.value } : {}),
|
|
183
|
+
...(length.value !== undefined ? { length: length.value } : {}),
|
|
184
|
+
...(duration.value !== undefined ? { duration: duration.value } : {}),
|
|
185
|
+
...(voice.value !== undefined ? { voice: voice.value } : {}),
|
|
186
|
+
...(audioKind !== undefined ? { audioKind: audioKind } : {}),
|
|
187
|
+
...(language.value !== undefined ? { language: language.value } : {}),
|
|
188
|
+
...(loop.value !== undefined ? { loop: loop.value } : {}),
|
|
189
|
+
...(promptInfluence.value !== undefined ? { promptInfluence: promptInfluence.value } : {}),
|
|
190
|
+
...(imageRef.value !== undefined ? { imageRef: imageRef.value } : {}),
|
|
191
|
+
...(imageRefs.value !== undefined ? { imageRefs: imageRefs.value } : {}),
|
|
192
|
+
...(wireModel.value !== undefined ? { wireModel: wireModel.value } : {}),
|
|
193
|
+
...(speechFormat !== undefined ? { speechFormat: speechFormat } : {}),
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function parseTaskId(input) {
|
|
199
|
+
const id = input.params.id;
|
|
200
|
+
return typeof id === 'string' && id.length > 0 ? ok({ value: id }) : err({ error: validationError({ message: 'id must be a non-empty path parameter' }) });
|
|
201
|
+
}
|
|
202
|
+
function parseMediaTaskListQuery(input) {
|
|
203
|
+
const raw = input.query.ownerRef;
|
|
204
|
+
const ownerRef = Array.isArray(raw) ? raw[0] : raw;
|
|
205
|
+
if (typeof ownerRef !== 'string' || ownerRef.trim().length === 0) {
|
|
206
|
+
return err({ error: validationError({ message: 'ownerRef query parameter is required' }, { issues: [{ path: 'ownerRef', message: 'required non-empty string' }] }) });
|
|
207
|
+
}
|
|
208
|
+
const includeTerminalRaw = input.query.includeTerminal;
|
|
209
|
+
const includeTerminal = (Array.isArray(includeTerminalRaw) ? includeTerminalRaw[0] : includeTerminalRaw) === 'true';
|
|
210
|
+
return ok({ value: { ownerRef, options: { includeTerminal } } });
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Runs the real generation in the background and patches the task to its terminal state.
|
|
214
|
+
* Deliberately fire-and-forget from `mediaGenerateRoute.handle`'s own perspective — the HTTP
|
|
215
|
+
* response has already returned the `'queued'` task by the time this settles.
|
|
216
|
+
*/
|
|
217
|
+
async function runMediaGenerationInBackground(deps, taskId, ownerRef, request) {
|
|
218
|
+
try {
|
|
219
|
+
await deps.taskStore.update({ id: taskId, patch: { status: 'running' } });
|
|
220
|
+
const { surface, model, ...options } = request;
|
|
221
|
+
const result = await deps.engine.generate({ surface, model }, options);
|
|
222
|
+
const base64 = result.bytes.toString('base64');
|
|
223
|
+
const mime = mimeTypeForSurface(request.surface, request.speechFormat);
|
|
224
|
+
await deps.taskStore.update({
|
|
225
|
+
id: taskId, patch: {
|
|
226
|
+
status: 'done',
|
|
227
|
+
file: {
|
|
228
|
+
dataUrl: `data:${mime};base64,${base64}`,
|
|
229
|
+
providerId: result.providerId,
|
|
230
|
+
providerNote: result.providerNote,
|
|
231
|
+
...(result.suggestedExt !== undefined ? { suggestedExt: result.suggestedExt } : {}),
|
|
232
|
+
usedStubFallback: result.usedStubFallback,
|
|
233
|
+
},
|
|
234
|
+
progress: result.warnings,
|
|
235
|
+
endedAt: Date.now(),
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const apiError = reportInternalError(deps, 'media-generate-dispatch', error, taskId, ownerRef);
|
|
241
|
+
try {
|
|
242
|
+
await deps.taskStore.update({
|
|
243
|
+
id: taskId, patch: {
|
|
244
|
+
status: 'failed',
|
|
245
|
+
error: { message: apiError.message, code: apiError.code },
|
|
246
|
+
endedAt: Date.now(),
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (writeError) {
|
|
251
|
+
// The task store can legitimately already be closed by the time a slow in-flight
|
|
252
|
+
// generation's failure settles (e.g. daemon shutdown mid-generation — a real vendor call
|
|
253
|
+
// can take "tens of seconds" per this module's own doc). This function is invoked
|
|
254
|
+
// fire-and-forget (`void runMediaGenerationInBackground(...)` in `mediaGenerateRoute.handle`
|
|
255
|
+
// below), so letting this secondary write failure propagate would reject an unobserved
|
|
256
|
+
// promise — an unhandled rejection that crashes a default-configured Node process over a
|
|
257
|
+
// condition that isn't actually fatal (the real generation failure was already reported
|
|
258
|
+
// above; this is only "the store couldn't also persist that fact"). Reported through the
|
|
259
|
+
// same sink, never rethrown.
|
|
260
|
+
reportInternalError(deps, 'media-generate-dispatch', writeError, taskId, ownerRef);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** A minimal, real content-type per surface — enough for a caller to render/play the resulting data URL without needing to know each vendor's own response shape. */
|
|
265
|
+
function mimeTypeForSurface(surface, speechFormat) {
|
|
266
|
+
if (surface === 'image')
|
|
267
|
+
return 'image/png';
|
|
268
|
+
if (surface === 'video')
|
|
269
|
+
return 'video/mp4';
|
|
270
|
+
const format = speechFormat ?? 'mp3';
|
|
271
|
+
const audioMimeByFormat = {
|
|
272
|
+
mp3: 'audio/mpeg',
|
|
273
|
+
wav: 'audio/wav',
|
|
274
|
+
flac: 'audio/flac',
|
|
275
|
+
aac: 'audio/aac',
|
|
276
|
+
opus: 'audio/opus',
|
|
277
|
+
};
|
|
278
|
+
return audioMimeByFormat[format];
|
|
279
|
+
}
|
|
280
|
+
export const mediaGenerateRoute = defineJsonRoute({
|
|
281
|
+
method: 'post', path: '/api/media/generate', parse: parseMediaGenerate, handle: async ({ input: { ownerRef, request }, deps }) => {
|
|
282
|
+
let task;
|
|
283
|
+
try {
|
|
284
|
+
task = await deps.taskStore.create({
|
|
285
|
+
input: {
|
|
286
|
+
id: randomUUID(),
|
|
287
|
+
ownerRef,
|
|
288
|
+
status: 'queued',
|
|
289
|
+
surface: request.surface,
|
|
290
|
+
model: request.model,
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
return err({ error: reportInternalError(deps, 'media-task-store', error, null, ownerRef) });
|
|
296
|
+
}
|
|
297
|
+
void runMediaGenerationInBackground(deps, task.id, ownerRef, request);
|
|
298
|
+
return ok({ value: { task } });
|
|
299
|
+
}
|
|
300
|
+
}, { requireSameOrigin: true, successStatus: 202 });
|
|
301
|
+
export const mediaTaskGetRoute = defineJsonRoute({
|
|
302
|
+
method: 'get', path: '/api/media/tasks/:id', parse: parseTaskId, handle: async ({ input: id, deps }) => {
|
|
303
|
+
try {
|
|
304
|
+
const task = await deps.taskStore.get({ id });
|
|
305
|
+
return task === null ? err({ error: createApiError({ code: 'NOT_FOUND', message: 'media task not found' }) }) : ok({ value: { task } });
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
return err({ error: reportInternalError(deps, 'media-task-store', error, id, null) });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
export const mediaTaskDeleteRoute = defineJsonRoute({
|
|
313
|
+
method: 'delete', path: '/api/media/tasks/:id', parse: parseTaskId, handle: async ({ input: id, deps }) => {
|
|
314
|
+
let ownerRef = null;
|
|
315
|
+
try {
|
|
316
|
+
const existing = await deps.taskStore.get({ id });
|
|
317
|
+
if (existing === null)
|
|
318
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: 'media task not found' }) });
|
|
319
|
+
ownerRef = existing.ownerRef;
|
|
320
|
+
await deps.taskStore.delete({ id });
|
|
321
|
+
return ok({ value: { ok: true } });
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return err({ error: reportInternalError(deps, 'media-task-store', error, id, ownerRef) });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}, { requireSameOrigin: true });
|
|
328
|
+
export const mediaTaskListRoute = defineJsonRoute({
|
|
329
|
+
method: 'get', path: '/api/media/tasks', parse: parseMediaTaskListQuery, handle: async ({ input: { ownerRef, options }, deps }) => {
|
|
330
|
+
try {
|
|
331
|
+
const tasks = await deps.taskStore.listByOwner({ ownerRef }, options);
|
|
332
|
+
return ok({ value: { tasks } });
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
return err({ error: reportInternalError(deps, 'media-task-store', error, null, ownerRef) });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
/** Mounts every media route on `app`. A pack's `http(app, services)` calls this directly. */
|
|
340
|
+
export function registerMediaRoutes({ app, deps, adapter }, _optional = {}) {
|
|
341
|
+
mountJsonRoute({ app, spec: mediaGenerateRoute, deps, adapter });
|
|
342
|
+
mountJsonRoute({ app, spec: mediaTaskGetRoute, deps, adapter });
|
|
343
|
+
mountJsonRoute({ app, spec: mediaTaskDeleteRoute, deps, adapter });
|
|
344
|
+
mountJsonRoute({ app, spec: mediaTaskListRoute, deps, adapter });
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=media.js.map
|