@302ai/media-studio-core 0.1.0-beta.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.
@@ -0,0 +1,91 @@
1
+ import { nanoid } from "nanoid";
2
+ export class ChatSession {
3
+ sessionId;
4
+ chat;
5
+ messages;
6
+ isTurnActive = false;
7
+ constructor(chat, options = {}) {
8
+ this.chat = chat;
9
+ this.sessionId = options.sessionId ?? nanoid();
10
+ this.messages = options.initialMessages ? [...options.initialMessages] : [];
11
+ }
12
+ /**
13
+ * Whether a message turn is currently in-flight.
14
+ */
15
+ get isBusy() {
16
+ return this.isTurnActive;
17
+ }
18
+ /**
19
+ * Returns a copy of the current in-memory conversation history.
20
+ */
21
+ getMessages() {
22
+ return [...this.messages];
23
+ }
24
+ /**
25
+ * Clears accumulated turn history while preserving the session ID.
26
+ */
27
+ clearHistory() {
28
+ if (this.isTurnActive) {
29
+ throw new Error("Cannot clear history while a turn is active.");
30
+ }
31
+ this.messages = [];
32
+ }
33
+ /**
34
+ * Sends a user turn within this session.
35
+ *
36
+ * Automatically appends the user message to history, dispatches the full
37
+ * history to the model, and asynchronously appends the assistant's reply
38
+ * to history once the stream completes.
39
+ *
40
+ * Throws an error if another turn is already active on this session.
41
+ */
42
+ async sendMessage(prompt, options) {
43
+ if (this.isTurnActive) {
44
+ throw new Error("Cannot send message: a turn is already active in this ChatSession.");
45
+ }
46
+ this.isTurnActive = true;
47
+ const userMessage = { role: "user", content: prompt };
48
+ const turnMessages = [...this.messages, userMessage];
49
+ this.messages.push(userMessage);
50
+ let stream;
51
+ try {
52
+ stream = await this.chat.stream({
53
+ sessionId: this.sessionId,
54
+ messages: turnMessages,
55
+ chatTaskId: options?.chatTaskId,
56
+ signal: options?.signal,
57
+ maxRetries: options?.maxRetries,
58
+ initialRetryDelayMs: options?.initialRetryDelayMs,
59
+ onFinalResponse: (finalResponse) => {
60
+ this.isTurnActive = false;
61
+ this.messages.push({
62
+ role: "assistant",
63
+ content: finalResponse.text,
64
+ });
65
+ options?.onFinalResponse?.(finalResponse);
66
+ },
67
+ onError: (err) => {
68
+ this.isTurnActive = false;
69
+ const idx = this.messages.lastIndexOf(userMessage);
70
+ if (idx !== -1 && this.messages[idx + 1]?.role !== "assistant") {
71
+ this.messages.splice(idx, 1);
72
+ }
73
+ options?.onError?.(err);
74
+ },
75
+ });
76
+ // Ensure turn is marked inactive once stream settles (e.g. clean completion, early break, or abort)
77
+ stream.settled.finally(() => {
78
+ this.isTurnActive = false;
79
+ });
80
+ }
81
+ catch (err) {
82
+ this.isTurnActive = false;
83
+ const idx = this.messages.lastIndexOf(userMessage);
84
+ if (idx !== -1) {
85
+ this.messages.splice(idx, 1);
86
+ }
87
+ throw err;
88
+ }
89
+ return stream;
90
+ }
91
+ }
@@ -0,0 +1,54 @@
1
+ import { type ListSessionsQuery, type ListSessionsResponse, type PrewarmSessionReady, type SessionPrewarmOutcome, type SessionPrewarmStage, type SessionSource } from "../sessions/types.js";
2
+ export type SessionPrewarmOptions = {
3
+ sessionId: string;
4
+ source: SessionSource;
5
+ note?: string;
6
+ installUserSkills?: boolean;
7
+ signal?: AbortSignal;
8
+ onStageChange?: (stage: SessionPrewarmStage) => void;
9
+ /**
10
+ * Fired exactly once, when the first `session_ready` event arrives. That
11
+ * event is the upstream commit point: the session is durably persisted and
12
+ * the id is safe to store from this moment on.
13
+ */
14
+ onSessionReady?: (session: PrewarmSessionReady) => void;
15
+ };
16
+ /**
17
+ * Session management against the media-studio gateway: list, remove, and
18
+ * provision (prewarm) sessions.
19
+ */
20
+ export declare class ClientSessions {
21
+ private readonly baseUrl;
22
+ private readonly apiKey;
23
+ private readonly customFetch;
24
+ constructor(baseUrl: string, apiKey: string, customFetch?: typeof fetch);
25
+ private authHeaders;
26
+ /**
27
+ * Lists sessions from the server (the single source of truth).
28
+ *
29
+ * @throws {ChatUpstreamError} on HTTP failure.
30
+ */
31
+ list(query?: ListSessionsQuery): Promise<ListSessionsResponse>;
32
+ /**
33
+ * Deletes a session server-side. Destroys the upstream conversation memory;
34
+ * this is irreversible.
35
+ *
36
+ * @throws {ChatUpstreamError} on HTTP failure.
37
+ */
38
+ remove(sessionId: string): Promise<void>;
39
+ /**
40
+ * Provisions a session via the prewarm SSE endpoint — the same flow the
41
+ * web app uses. Streams stage events through `onStageChange` and resolves
42
+ * to a three-state outcome bounded by the `session_ready` commit point:
43
+ *
44
+ * - `ready` — full provisioning completed;
45
+ * - `fallback` — the session is persisted but sandbox/skills provisioning
46
+ * failed or the stream ended early; plain-text chat still works;
47
+ * - `failed` — the session was never persisted upstream.
48
+ *
49
+ * Never call this again with a fresh id after `onSessionReady` fired for an
50
+ * id — that creates orphan sessions.
51
+ */
52
+ prewarm(options: SessionPrewarmOptions): Promise<SessionPrewarmOutcome>;
53
+ private consumePrewarmStream;
54
+ }
@@ -0,0 +1,213 @@
1
+ import { createParser } from "eventsource-parser";
2
+ import { DeleteSessionResponseSchema, ListSessionsResponseSchema, PrewarmDoneSchema, PrewarmErrorSchema, PrewarmSandboxReadySchema, PrewarmSessionReadySchema, PrewarmSkillsReadySchema, } from "../sessions/types.js";
3
+ import { ChatUpstreamError, extractUpstreamErrorMessage, } from "../transport/errors.js";
4
+ const PREWARM_TIMEOUT_MS = 60_000;
5
+ const MAX_NETWORK_RETRIES = 2;
6
+ const RETRY_BASE_DELAY_MS = 500;
7
+ function delay(durationMs) {
8
+ return new Promise((resolve) => {
9
+ setTimeout(resolve, durationMs);
10
+ });
11
+ }
12
+ /**
13
+ * Session management against the media-studio gateway: list, remove, and
14
+ * provision (prewarm) sessions.
15
+ */
16
+ export class ClientSessions {
17
+ baseUrl;
18
+ apiKey;
19
+ customFetch;
20
+ constructor(baseUrl, apiKey, customFetch) {
21
+ this.baseUrl = baseUrl;
22
+ this.apiKey = apiKey;
23
+ this.customFetch = customFetch ?? fetch;
24
+ }
25
+ authHeaders() {
26
+ return { Authorization: `Bearer ${this.apiKey}` };
27
+ }
28
+ /**
29
+ * Lists sessions from the server (the single source of truth).
30
+ *
31
+ * @throws {ChatUpstreamError} on HTTP failure.
32
+ */
33
+ async list(query) {
34
+ const params = new URLSearchParams();
35
+ if (query?.limit !== undefined)
36
+ params.set("limit", String(query.limit));
37
+ if (query?.offset !== undefined) {
38
+ params.set("offset", String(query.offset));
39
+ }
40
+ if (query?.note !== undefined)
41
+ params.set("note", query.note);
42
+ const search = params.toString();
43
+ const url = `${this.baseUrl}/api/sessions${search ? `?${search}` : ""}`;
44
+ const response = await this.customFetch(url, {
45
+ method: "GET",
46
+ headers: this.authHeaders(),
47
+ });
48
+ if (!response.ok) {
49
+ const rawBody = await response.text();
50
+ const message = extractUpstreamErrorMessage(rawBody) ||
51
+ `Failed to fetch sessions with status ${response.status}`;
52
+ throw new ChatUpstreamError(message, response.status, rawBody);
53
+ }
54
+ const rawJson = await response.json();
55
+ return ListSessionsResponseSchema.parse(rawJson);
56
+ }
57
+ /**
58
+ * Deletes a session server-side. Destroys the upstream conversation memory;
59
+ * this is irreversible.
60
+ *
61
+ * @throws {ChatUpstreamError} on HTTP failure.
62
+ */
63
+ async remove(sessionId) {
64
+ const url = `${this.baseUrl}/api/sessions?sessionId=${encodeURIComponent(sessionId)}`;
65
+ const response = await this.customFetch(url, {
66
+ method: "DELETE",
67
+ headers: this.authHeaders(),
68
+ });
69
+ if (!response.ok) {
70
+ const rawBody = await response.text();
71
+ const message = extractUpstreamErrorMessage(rawBody) ||
72
+ `Failed to delete session with status ${response.status}`;
73
+ throw new ChatUpstreamError(message, response.status, rawBody);
74
+ }
75
+ const rawJson = await response.json();
76
+ DeleteSessionResponseSchema.parse(rawJson);
77
+ }
78
+ /**
79
+ * Provisions a session via the prewarm SSE endpoint — the same flow the
80
+ * web app uses. Streams stage events through `onStageChange` and resolves
81
+ * to a three-state outcome bounded by the `session_ready` commit point:
82
+ *
83
+ * - `ready` — full provisioning completed;
84
+ * - `fallback` — the session is persisted but sandbox/skills provisioning
85
+ * failed or the stream ended early; plain-text chat still works;
86
+ * - `failed` — the session was never persisted upstream.
87
+ *
88
+ * Never call this again with a fresh id after `onSessionReady` fired for an
89
+ * id — that creates orphan sessions.
90
+ */
91
+ async prewarm(options) {
92
+ const { onStageChange, onSessionReady } = options;
93
+ const timeoutController = new AbortController();
94
+ const timeout = setTimeout(() => timeoutController.abort(), PREWARM_TIMEOUT_MS);
95
+ const signal = options.signal
96
+ ? AbortSignal.any([timeoutController.signal, options.signal])
97
+ : timeoutController.signal;
98
+ const state = { sessionReady: null, outcome: null };
99
+ onStageChange?.("creating");
100
+ try {
101
+ for (let attempt = 0; attempt <= MAX_NETWORK_RETRIES; attempt++) {
102
+ if (state.outcome || signal.aborted)
103
+ break;
104
+ if (attempt > 0)
105
+ await delay(attempt * RETRY_BASE_DELAY_MS);
106
+ try {
107
+ const response = await this.customFetch(`${this.baseUrl}/api/sessions/prewarm`, {
108
+ method: "POST",
109
+ headers: {
110
+ ...this.authHeaders(),
111
+ Accept: "text/event-stream",
112
+ "Content-Type": "application/json",
113
+ },
114
+ body: JSON.stringify({
115
+ sessionId: options.sessionId,
116
+ source: options.source,
117
+ ...(options.note !== undefined ? { note: options.note } : {}),
118
+ ...(options.installUserSkills !== undefined
119
+ ? { installUserSkills: options.installUserSkills }
120
+ : {}),
121
+ }),
122
+ signal,
123
+ });
124
+ if (!response.ok) {
125
+ // 4xx (except 429) is deterministic: no point retrying.
126
+ const retryable = response.status >= 500 || response.status === 429;
127
+ if (!retryable)
128
+ break;
129
+ continue;
130
+ }
131
+ await this.consumePrewarmStream(response, state, {
132
+ onStageChange,
133
+ onSessionReady,
134
+ });
135
+ }
136
+ catch {
137
+ if (signal.aborted)
138
+ break;
139
+ // Network failure or mid-stream read/parse error: retry until the
140
+ // budget is exhausted, then fall back on the session_ready boundary.
141
+ }
142
+ if (state.outcome)
143
+ break;
144
+ }
145
+ }
146
+ finally {
147
+ clearTimeout(timeout);
148
+ }
149
+ if (state.outcome)
150
+ return state.outcome;
151
+ return state.sessionReady
152
+ ? { status: "fallback", session: state.sessionReady }
153
+ : { status: "failed" };
154
+ }
155
+ async consumePrewarmStream(response, state, callbacks) {
156
+ if (!response.body)
157
+ return;
158
+ const parser = createParser({
159
+ onEvent(message) {
160
+ if (!message.data || state.outcome)
161
+ return;
162
+ const data = JSON.parse(message.data);
163
+ switch (message.event) {
164
+ case "session_ready": {
165
+ const session = PrewarmSessionReadySchema.parse(data);
166
+ if (!state.sessionReady)
167
+ callbacks.onSessionReady?.(session);
168
+ state.sessionReady = session;
169
+ callbacks.onStageChange?.("sandbox");
170
+ return;
171
+ }
172
+ case "sandbox_ready": {
173
+ PrewarmSandboxReadySchema.parse(data);
174
+ callbacks.onStageChange?.("skills");
175
+ return;
176
+ }
177
+ case "skills_ready": {
178
+ PrewarmSkillsReadySchema.parse(data);
179
+ callbacks.onStageChange?.("finishing");
180
+ return;
181
+ }
182
+ case "done": {
183
+ PrewarmDoneSchema.parse(data);
184
+ state.outcome = state.sessionReady
185
+ ? { status: "ready", session: state.sessionReady }
186
+ : { status: "failed" };
187
+ return;
188
+ }
189
+ case "error": {
190
+ PrewarmErrorSchema.parse(data);
191
+ state.outcome = state.sessionReady
192
+ ? { status: "fallback", session: state.sessionReady }
193
+ : { status: "failed" };
194
+ return;
195
+ }
196
+ default:
197
+ return;
198
+ }
199
+ },
200
+ });
201
+ const reader = response.body.getReader();
202
+ const decoder = new TextDecoder();
203
+ for (;;) {
204
+ const { done, value } = await reader.read();
205
+ if (done)
206
+ break;
207
+ parser.feed(decoder.decode(value, { stream: true }));
208
+ if (state.outcome)
209
+ break;
210
+ }
211
+ parser.feed(decoder.decode());
212
+ }
213
+ }
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import type { ChatFinalResponse } from "../stream/stream.js";
3
+ import type { ChatUpstreamMessage } from "../transport/types.js";
4
+ export interface MediaStudioClientOptions {
5
+ apiKey: string;
6
+ baseUrl?: string | undefined;
7
+ locale?: string | undefined;
8
+ fetch?: typeof fetch | undefined;
9
+ }
10
+ export declare const MeResponseSchema: z.ZodObject<{
11
+ uid: z.ZodOptional<z.ZodNullable<z.ZodString>>;
12
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
13
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
14
+ apiKeyMasked: z.ZodOptional<z.ZodNullable<z.ZodString>>;
15
+ exp: z.ZodOptional<z.ZodNumber>;
16
+ valid: z.ZodBoolean;
17
+ }, z.core.$strip>;
18
+ export type MeResponse = z.infer<typeof MeResponseSchema>;
19
+ export interface QuickPromptOptions {
20
+ sessionId: string;
21
+ chatTaskId?: string | undefined;
22
+ signal?: AbortSignal | undefined;
23
+ maxRetries?: number | undefined;
24
+ initialRetryDelayMs?: number | undefined;
25
+ onFinalResponse?: ((res: ChatFinalResponse) => void) | undefined;
26
+ onError?: ((err: unknown) => void) | undefined;
27
+ }
28
+ export interface ChatTurnOptions {
29
+ sessionId: string;
30
+ messages: ChatUpstreamMessage[];
31
+ chatTaskId?: string | undefined;
32
+ sinceSeq?: number | string | undefined;
33
+ modelParams?: unknown;
34
+ skillUrls?: string[] | undefined;
35
+ structuredOutput?: boolean | undefined;
36
+ signal?: AbortSignal | undefined;
37
+ maxRetries?: number | undefined;
38
+ initialRetryDelayMs?: number | undefined;
39
+ onFinalResponse?: ((res: ChatFinalResponse) => void) | undefined;
40
+ onError?: ((err: unknown) => void) | undefined;
41
+ }
42
+ export interface ChatSessionOptions {
43
+ sessionId?: string | undefined;
44
+ initialMessages?: ChatUpstreamMessage[] | undefined;
45
+ }
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ export const MeResponseSchema = z.object({
3
+ uid: z.string().nullable().optional(),
4
+ name: z.string().nullable().optional(),
5
+ email: z.string().nullable().optional(),
6
+ apiKeyMasked: z.string().nullable().optional(),
7
+ exp: z.number().optional(),
8
+ valid: z.boolean(),
9
+ });
@@ -0,0 +1,90 @@
1
+ import { z } from "zod";
2
+ /** Text part delta in streaming turn. */
3
+ export declare const TextDeltaEventSchema: z.ZodObject<{
4
+ type: z.ZodLiteral<"text-delta">;
5
+ delta: z.ZodString;
6
+ id: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>;
8
+ /** Reasoning / thinking part delta in streaming turn. */
9
+ export declare const ReasoningDeltaEventSchema: z.ZodObject<{
10
+ type: z.ZodLiteral<"reasoning-delta">;
11
+ delta: z.ZodString;
12
+ id: z.ZodOptional<z.ZodString>;
13
+ }, z.core.$strip>;
14
+ /** Tool invocation event emitted when assistant calls an external tool. */
15
+ export declare const ToolCallEventSchema: z.ZodObject<{
16
+ type: z.ZodLiteral<"tool-call">;
17
+ toolCallId: z.ZodString;
18
+ toolName: z.ZodString;
19
+ args: z.ZodOptional<z.ZodUnknown>;
20
+ }, z.core.$strip>;
21
+ /** Tool result response emitted after tool execution finishes. */
22
+ export declare const ToolResultEventSchema: z.ZodObject<{
23
+ type: z.ZodLiteral<"tool-result">;
24
+ toolCallId: z.ZodString;
25
+ result: z.ZodOptional<z.ZodUnknown>;
26
+ }, z.core.$strip>;
27
+ /** High-level media result event emitted when image/video generation completes. */
28
+ export declare const MediaResultEventSchema: z.ZodObject<{
29
+ type: z.ZodLiteral<"media-result">;
30
+ result: z.ZodObject<{
31
+ status: z.ZodOptional<z.ZodString>;
32
+ domain: z.ZodOptional<z.ZodString>;
33
+ operation: z.ZodOptional<z.ZodString>;
34
+ modelUsed: z.ZodOptional<z.ZodString>;
35
+ taskId: z.ZodOptional<z.ZodString>;
36
+ resultUrl: z.ZodOptional<z.ZodString>;
37
+ prompt: z.ZodOptional<z.ZodString>;
38
+ }, z.core.$strip>;
39
+ }, z.core.$strip>;
40
+ /** Terminal finish event indicating turn completion. */
41
+ export declare const FinishEventSchema: z.ZodObject<{
42
+ type: z.ZodLiteral<"finish">;
43
+ finishReason: z.ZodOptional<z.ZodString>;
44
+ }, z.core.$strip>;
45
+ /** Stream error event. Supports either error or message string. */
46
+ export declare const StreamErrorEventSchema: z.ZodObject<{
47
+ type: z.ZodLiteral<"error">;
48
+ error: z.ZodOptional<z.ZodString>;
49
+ message: z.ZodOptional<z.ZodString>;
50
+ chatTaskId: z.ZodOptional<z.ZodString>;
51
+ }, z.core.$strip>;
52
+ /** Discriminated union of all possible streaming events yielded by ChatStream. */
53
+ export declare const ChatStreamEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
54
+ type: z.ZodLiteral<"text-delta">;
55
+ delta: z.ZodString;
56
+ id: z.ZodOptional<z.ZodString>;
57
+ }, z.core.$strip>, z.ZodObject<{
58
+ type: z.ZodLiteral<"reasoning-delta">;
59
+ delta: z.ZodString;
60
+ id: z.ZodOptional<z.ZodString>;
61
+ }, z.core.$strip>, z.ZodObject<{
62
+ type: z.ZodLiteral<"tool-call">;
63
+ toolCallId: z.ZodString;
64
+ toolName: z.ZodString;
65
+ args: z.ZodOptional<z.ZodUnknown>;
66
+ }, z.core.$strip>, z.ZodObject<{
67
+ type: z.ZodLiteral<"tool-result">;
68
+ toolCallId: z.ZodString;
69
+ result: z.ZodOptional<z.ZodUnknown>;
70
+ }, z.core.$strip>, z.ZodObject<{
71
+ type: z.ZodLiteral<"media-result">;
72
+ result: z.ZodObject<{
73
+ status: z.ZodOptional<z.ZodString>;
74
+ domain: z.ZodOptional<z.ZodString>;
75
+ operation: z.ZodOptional<z.ZodString>;
76
+ modelUsed: z.ZodOptional<z.ZodString>;
77
+ taskId: z.ZodOptional<z.ZodString>;
78
+ resultUrl: z.ZodOptional<z.ZodString>;
79
+ prompt: z.ZodOptional<z.ZodString>;
80
+ }, z.core.$strip>;
81
+ }, z.core.$strip>, z.ZodObject<{
82
+ type: z.ZodLiteral<"finish">;
83
+ finishReason: z.ZodOptional<z.ZodString>;
84
+ }, z.core.$strip>, z.ZodObject<{
85
+ type: z.ZodLiteral<"error">;
86
+ error: z.ZodOptional<z.ZodString>;
87
+ message: z.ZodOptional<z.ZodString>;
88
+ chatTaskId: z.ZodOptional<z.ZodString>;
89
+ }, z.core.$strip>], "type">;
90
+ export type ChatStreamEvent = z.infer<typeof ChatStreamEventSchema>;
@@ -0,0 +1,31 @@
1
+ export type ToolCategory = "agent" | "create" | "model" | "search" | "file" | "bash" | "skill" | "other";
2
+ export interface FormattedToolMilestone {
3
+ rawName: string;
4
+ normalizedName: string;
5
+ category: ToolCategory;
6
+ milestoneLine: string;
7
+ actionLabel: string;
8
+ details: {
9
+ model?: string;
10
+ aspectRatio?: string;
11
+ duration?: string;
12
+ resolution?: string;
13
+ voice?: string;
14
+ operation?: string;
15
+ subagentType?: string;
16
+ description?: string;
17
+ target?: string;
18
+ query?: string;
19
+ };
20
+ }
21
+ /**
22
+ * 1:1 normalization parity with main project's `normalizeMcpToolName`.
23
+ * Strips `tool-` prefix, lowercases, resolves MCP namespaces (`mcp__server__name`),
24
+ * and trims trailing `output` suffix.
25
+ */
26
+ export declare function normalizeMcpToolName(toolName: string): string;
27
+ /**
28
+ * Parses and formats a tool call into a structured milestone and dynamic action label.
29
+ * Fully aligned 1:1 with the main project's tool renderer hierarchy.
30
+ */
31
+ export declare function formatToolMilestone(toolName: string, args: unknown): FormattedToolMilestone;