agent-accelerator 0.5.4 → 0.5.6

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.
@@ -17,6 +17,7 @@ import { telemetryBus } from "../telemetry.ts";
17
17
  import { resolveHome } from "../utils/home.ts";
18
18
  export { resolveHome };
19
19
  import { ValidationError } from "../types/errors.ts";
20
+ import type { CostSource } from "../session/usage.ts";
20
21
  import type { ScheduledActionRecord, ScheduledRunStatus } from "../types/scheduler.ts";
21
22
  import { durationMs, runFileName } from "./time.ts";
22
23
 
@@ -33,6 +34,8 @@ export interface PendingDelivery {
33
34
  status: ScheduledRunStatus;
34
35
  readyAt: string;
35
36
  deliverAt: string;
37
+ /** Set when the run replaced a missed occurrence: it is late whatever the clock says (a `run_now` on a `missed` action, or `onMissed: "run"`). */
38
+ late?: boolean;
36
39
  }
37
40
 
38
41
  /** Header written at the top of every run file. */
@@ -46,9 +49,26 @@ export interface RunHeader {
46
49
  /** The run finished after its delivery time. */
47
50
  late: boolean;
48
51
  model?: string;
52
+ /** Model calls the run made (all attempts), and what they spent: the same numbers `usageReport()` keeps for workers. */
53
+ turns?: number;
54
+ /** Input + output tokens. */
49
55
  tokens?: number;
50
- /** `unknown` when the model has no known pricing (never a made-up 0). */
56
+ /** Everything sent; `freshInputTokens + cacheReadTokens + cacheWriteTokens`. */
57
+ inputTokens?: number;
58
+ freshInputTokens?: number;
59
+ cacheReadTokens?: number;
60
+ cacheWriteTokens?: number;
61
+ /** Everything generated, thinking included. */
62
+ outputTokens?: number;
63
+ thinkingTokens?: number;
64
+ /** `cacheReadTokens / inputTokens`, 0 to 1; absent when nothing was sent. */
65
+ cacheHitRate?: number;
66
+ /** `unknown` when no turn had a known price (never a made-up 0). */
51
67
  costUsd?: number | "unknown";
68
+ /** Where the price came from: the `provider`'s reported cost, the `catalog`, `mixed`, or `unknown`. */
69
+ costSource?: CostSource;
70
+ /** Turns with no known price; when present, `costUsd` is a lower bound. */
71
+ unpricedTurns?: number;
52
72
  error?: string;
53
73
  }
54
74
 
@@ -163,6 +183,7 @@ const recordSchema = z
163
183
  startCookingInAdvance: z.string(),
164
184
  repeatEvery: z.string().optional(),
165
185
  retries: z.number().int().min(0).optional(),
186
+ onMissed: z.enum(["skip", "run"]).optional(),
166
187
  }),
167
188
  state: z.enum(["waiting", "cooking", "missed", "done"]),
168
189
  nextCookAt: iso.nullable(),
@@ -195,6 +216,7 @@ const deliverySchema = z.object({
195
216
  status: z.enum(["ok", "failed"]),
196
217
  readyAt: iso,
197
218
  deliverAt: iso,
219
+ late: z.boolean().optional(),
198
220
  });
199
221
 
200
222
  /** `undefined` when a `deliveries.json` entry is usable, else a one-line reason. Never throws. */
@@ -14,7 +14,7 @@ import type { ScheduledActionInput, ScheduledActionPatch } from "../types/schedu
14
14
  import {
15
15
  createScheduledAction,
16
16
  getScheduler,
17
- killScheduledAction,
17
+ deleteScheduledAction,
18
18
  listScheduledActions,
19
19
  runScheduledAction,
20
20
  skipScheduledAction,
@@ -24,7 +24,7 @@ import {
24
24
  import { readRun } from "./store.ts";
25
25
  import { formatDatetime } from "./time.ts";
26
26
 
27
- const COMMANDS = ["create", "list", "get", "update", "kill", "run_now", "skip"] as const;
27
+ const COMMANDS = ["create", "list", "get", "update", "delete", "run_now", "skip"] as const;
28
28
 
29
29
  let manual: string | undefined;
30
30
  /** Read on first use (not at import), so a program that never enables scheduling never touches the file. */
@@ -50,9 +50,10 @@ const scheduledActionSchema = z.object({
50
50
  startCookingInAdvance: z.string().optional().describe('Duration "DD:HH:MM" before delivery when the run starts.'),
51
51
  repeatEvery: z.string().optional().describe('Duration "DD:HH:MM" between deliveries. Omit to run once.'),
52
52
  retries: z.number().optional().describe("Extra attempts if a run fails."),
53
+ onMissed: z.enum(["skip", "run"]).optional().describe('If the host program was not running at delivery time: "skip" = move on to the next time, "run" = run when it is back. Omit to decide later.'),
53
54
  });
54
55
 
55
- const describedId = "Full action id including its 6-character suffix (get, update, kill, run_now, skip).";
56
+ const describedId = "Full action id including its 6-character suffix (get, update, delete, run_now, skip).";
56
57
 
57
58
  /** Builds the `scheduled_actions` tool for `agent`; its description is the manual with the worker pool filled in. */
58
59
  export function createScheduledActionsTool(agent: Agent): ToolDefinition {
@@ -85,10 +86,10 @@ export function createScheduledActionsTool(agent: Agent): ToolDefinition {
85
86
  instructions: z.string().optional().describe("The worker's system prompt (create, update)."),
86
87
  task: z.string().optional().describe("The concrete assignment for every run (create, update)."),
87
88
  tools: z.array(z.string()).optional().describe(`Worker tools to grant, from: ${poolNames.join(", ") || "none"}.`),
88
- timeoutMs: z.number().optional().describe("Per-run time limit in ms (> 0)."),
89
+ timeoutMs: z.number().optional().describe("Stop a run that takes longer than this many ms (> 0). Omit it for no limit of your own; the host program may cap it."),
89
90
  scheduled_action: scheduledActionSchema.optional(),
90
- force: z.boolean().optional().describe("kill: abort a run that is cooking. Ask the user first."),
91
- purge: z.boolean().optional().describe("kill: also delete past run files."),
91
+ force: z.boolean().optional().describe("delete: abort a run that is cooking. Ask the user first."),
92
+ purge: z.boolean().optional().describe("delete: also delete past run files."),
92
93
  }),
93
94
  execute: async (a) => {
94
95
  const sa = a.scheduled_action ?? {};
@@ -137,8 +138,8 @@ export function createScheduledActionsTool(agent: Agent): ToolDefinition {
137
138
  }
138
139
  return updateScheduledAction(id, patch, { home });
139
140
  }
140
- case "kill":
141
- return killScheduledAction(needId(), { home, force: a.force, purge: a.purge });
141
+ case "delete":
142
+ return deleteScheduledAction(needId(), { home, force: a.force, purge: a.purge });
142
143
  case "run_now":
143
144
  return runScheduledAction(needId(), { home });
144
145
  case "skip":
@@ -1391,7 +1391,7 @@ function resolveEffectivePricing(
1391
1391
  /**
1392
1392
  * Prices one usage snapshot with integer micro-USD accumulation (Q-36).
1393
1393
  *
1394
- * - Provider-reported `usage.cost.totalCost > 0` wins (source `provider`).
1394
+ * - A provider-reported `usage.cost.totalCost` (a number, 0 included: free or cached) wins (source `provider`).
1395
1395
  * - Else catalog pricing via `resolveEffectivePricing` (source `catalog`).
1396
1396
  * - Else returns `undefined` (source `unknown` is implicit) — unknown stays
1397
1397
  * unknown, never a fabricated 0 object.
@@ -1408,7 +1408,7 @@ export function priceUsage(
1408
1408
  opts?: { serviceTier?: ServiceTier | string; modelId?: string }
1409
1409
  ): PricedCost | undefined {
1410
1410
  const reported = usage?.cost;
1411
- if (reported && typeof reported.totalCost === "number" && reported.totalCost > 0) {
1411
+ if (reported && typeof reported.totalCost === "number" && reported.totalCost >= 0) {
1412
1412
  const micro = (d?: number): number => (typeof d === "number" && d > 0 ? Math.round(d * 1_000_000) : 0);
1413
1413
  const inputMicroUSD = micro(reported.inputCost);
1414
1414
  const outputMicroUSD = micro(reported.outputCost);
@@ -1492,7 +1492,7 @@ export function addCostToUsage(
1492
1492
  spec?: ModelSpec,
1493
1493
  opts?: { serviceTier?: ServiceTier | string }
1494
1494
  ): TokenUsage["cost"] | undefined {
1495
- if (usage?.cost && typeof usage.cost.totalCost === "number" && usage.cost.totalCost > 0) {
1495
+ if (usage?.cost && typeof usage.cost.totalCost === "number" && usage.cost.totalCost >= 0) {
1496
1496
  return usage.cost;
1497
1497
  }
1498
1498
  const priced = priceUsage(usage, spec, opts);
@@ -14,6 +14,8 @@ export interface EventStreamOptions {
14
14
  includePartial?: boolean;
15
15
  /** Max buffered events when no iterator is attached. Default 1000. Oldest dropped. */
16
16
  maxBufferedEvents?: number;
17
+ /** `false`: events go to listeners and waiting iterators only, never into the buffer (for a caller that only wants `result()`). Default true. */
18
+ buffer?: boolean;
17
19
  /** Max approx bytes for a single delta event. Default 1MB. Oversized dropped. */
18
20
  maxEventBytes?: number;
19
21
  }
@@ -61,6 +63,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
61
63
  private resolveFinalResult!: (result: AgentResponse) => void;
62
64
  private rejectFinalResult!: (err: Error) => void;
63
65
  private readonly includePartial: boolean;
66
+ private readonly buffered: boolean;
64
67
  private readonly maxBufferedEvents: number;
65
68
  private readonly maxEventBytes: number;
66
69
  /** First listener exception captured for inspection (Q-29/Q-41). */
@@ -80,6 +83,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
80
83
  */
81
84
  constructor(opts?: EventStreamOptions) {
82
85
  this.includePartial = opts?.includePartial !== false;
86
+ this.buffered = opts?.buffer !== false;
83
87
  this.maxBufferedEvents =
84
88
  typeof opts?.maxBufferedEvents === "number" && Number.isFinite(opts.maxBufferedEvents) && opts.maxBufferedEvents > 0
85
89
  ? Math.floor(opts.maxBufferedEvents)
@@ -151,8 +155,9 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
151
155
  } else {
152
156
  // No waiter right now (typical between `next()` polls of a burst):
153
157
  // buffer for the iterator with a hard cap (Q-29). Overflow drops the
154
- // oldest with an anomaly report — see `enqueueBounded`.
155
- this.enqueueBounded(out);
158
+ // oldest with an anomaly report — see `enqueueBounded`. A stream built
159
+ // with `buffer: false` has no iterator to feed and keeps nothing.
160
+ if (this.buffered) this.enqueueBounded(out);
156
161
  }
157
162
  }
158
163
 
@@ -422,6 +422,21 @@ function waitForRetry(ms: number, signal?: AbortSignal): Promise<void> {
422
422
  });
423
423
  }
424
424
 
425
+ // Settles with `work`, or rejects "Tool execution aborted" the moment `signal` aborts, without waiting for a tool that never looks at its signal (it finishes in the background, its result is dropped); in: executeAttempt, out: the attempt's result or error.
426
+ function raceAbort<T>(work: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
427
+ if (!signal) return work;
428
+ return new Promise<T>((resolve, reject) => {
429
+ const onAbort = () => reject(new Error("Tool execution aborted"));
430
+ if (signal.aborted) {
431
+ work.catch(() => {}); // the abandoned tool may still fail later: nobody listens
432
+ onAbort();
433
+ return;
434
+ }
435
+ signal.addEventListener("abort", onAbort, { once: true });
436
+ work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
437
+ });
438
+ }
439
+
425
440
  async function executeAttempt(
426
441
  toolDef: ToolDefinition,
427
442
  parsedInput: unknown,
@@ -429,7 +444,7 @@ async function executeAttempt(
429
444
  timeoutMs: number | undefined
430
445
  ): Promise<unknown> {
431
446
  if (!timeoutMs || timeoutMs <= 0) {
432
- return toolDef.execute(parsedInput as any, context);
447
+ return raceAbort(Promise.resolve(toolDef.execute(parsedInput as any, context)), context.signal);
433
448
  }
434
449
 
435
450
  const controller = new AbortController();
@@ -453,7 +468,7 @@ async function executeAttempt(
453
468
  }, timeoutMs);
454
469
  });
455
470
  const result = await Promise.race([
456
- Promise.resolve(toolDef.execute(parsedInput as any, executionContext)),
471
+ raceAbort(Promise.resolve(toolDef.execute(parsedInput as any, executionContext)), context.signal),
457
472
  timeout,
458
473
  ]);
459
474
  // A resolved promise can beat a timer callback even when the event loop
package/src/tools/tool.ts CHANGED
@@ -61,22 +61,6 @@ export function resolveWhenBusy(
61
61
  return v === "steer" || v === "queue" ? v : "auto";
62
62
  }
63
63
 
64
- /**
65
- * Maps the Q-52 canonical worker-timeout knobs to the legacy `timeout` value
66
- * (`0` = no limit, `-1` = model sets per-task `timeoutMs`, `>0` = fixed ms).
67
- * An explicit `timeout`/`workerTimeoutMs` wins over `modelMaySetTimeout`.
68
- *
69
- * @example `normalizeWorkerTimeout({ modelMaySetTimeout: true }); // -1`
70
- */
71
- export function normalizeWorkerTimeout(
72
- config?: { timeout?: number; workerTimeoutMs?: number; modelMaySetTimeout?: boolean }
73
- ): number {
74
- const explicit = config?.workerTimeoutMs ?? config?.timeout;
75
- if (Number.isFinite(explicit as number)) return Math.floor(explicit as number);
76
- if (config?.modelMaySetTimeout === true) return -1;
77
- return 0;
78
- }
79
-
80
64
  /**
81
65
  * Normalizes the Q-52 `maxAttempts` alias to a `maxTries` value.
82
66
  * `maxTries` wins when both are set.
@@ -121,7 +121,8 @@ export interface AgentConfig {
121
121
  * - `{ mode: "steer" }`: only steer (inject into current turn) is allowed.
122
122
  * - `{ mode: "queue" }`: only queue (next-turn follow-up) is allowed.
123
123
  * - `{ mode: "auto" }` (default): caller picks per message via `enqueue` or `steer()`/`queue()`.
124
- * `interrupt` (cancel stream now) is always available via `interrupt()`/`cancel()`/`AbortSignal`.
124
+ * `interrupt` (stop the active run now, optionally start a new message) is always available, whatever the mode:
125
+ * `interrupt()`, `interrupt(message)`, `enqueue: "interrupt"`, `cancel()` and `AbortSignal`.
125
126
  */
126
127
  midSession?: MidSessionConfig;
127
128
  /**
@@ -227,6 +228,25 @@ export interface AgentConfig {
227
228
  budget?: Budget;
228
229
  /** Run + network limits (Q-22). Turn/budget caps unlimited by default; network timeouts finite. */
229
230
  limits?: AgentRunLimits;
231
+ /**
232
+ * The only models this agent may use, as `provider/model` strings compared as written. Unset (default) = any model.
233
+ * Checked when the Agent is built (also for a config an agent edited through {@link Agent.fromConfig}); a model
234
+ * outside the list throws a `ConfigError`. Workers and `subagentModel` are not affected.
235
+ */
236
+ allowedModels?: string[];
237
+ }
238
+
239
+ /**
240
+ * The part of an agent that an agent may edit (self-improvement): see {@link Agent.toConfig} and {@link Agent.fromConfig}.
241
+ * Plain JSON. Every other setting (limits, budget, cache, thinking, keys, ...) stays with the developer.
242
+ */
243
+ export interface EditableAgentConfig {
244
+ /** `provider/model`; must be in `allowedModels` when the developer set it. */
245
+ model?: string;
246
+ /** System instructions. */
247
+ instructions?: string;
248
+ /** Names of tools, picked from the developer's tool pool (`base.tools` of `fromConfig`). */
249
+ tools?: string[];
230
250
  }
231
251
 
232
252
  /** A run that a crash cut off (see {@link Agent.interrupted}). */
@@ -246,6 +266,7 @@ export interface AgentRunLimits {
246
266
  deadlineMs?: number;
247
267
  maxTotalTokens?: number;
248
268
  maxToolCallsPerRun?: number;
269
+ /** Network timeouts in ms. Defaults 300 000 / 120 000 / 300 000. `0` or `Infinity` turns one off. */
249
270
  requestTimeoutMs?: number;
250
271
  firstByteTimeoutMs?: number;
251
272
  streamIdleTimeoutMs?: number;
@@ -288,7 +309,7 @@ export interface DynamicSubagentsConfig {
288
309
  allowScheduledActions?: boolean;
289
310
  /** Developer-only worker model override. Never choosable by the Main Agent LLM. Falls back to top-level `subagentModel`, then `SUB_AGENT_MODEL` env, then the parent model. */
290
311
  model?: string | ModelSpec | ModelProviderInstance;
291
- /** Max workers that may be open at once on this agent (the model must close one before spawning another). Defaults to 4. */
312
+ /** Max workers that may be open at once on this agent (the model must close one before spawning another). Default: no limit; set a whole number >= 1 to cap it. */
292
313
  maxSpawn?: number;
293
314
  /** Fixed reasoning level for all dynamic workers. The Main Agent cannot override it. Falls back to the parent level when omitted. */
294
315
  thinkingLevel?: ThinkingLevel;
@@ -300,27 +321,19 @@ export interface DynamicSubagentsConfig {
300
321
  */
301
322
  workerBudget?: Pick<Budget, "maxCostUsd" | "maxTotalTokens">;
302
323
  /**
303
- * How many levels of workers may exist below this agent (default 3). `1` = workers cannot start workers of their own;
304
- * `3` = a worker may start workers that start workers. A worker with levels left gets its own `subagents` tool, and
305
- * its children can only be granted the tools the worker itself was granted. Whole number >= 1.
324
+ * How many levels of workers may exist below this agent. Default: no limit. `1` = workers cannot start workers of their
325
+ * own; `3` = a worker may start workers that start workers. A worker with levels left gets its own `subagents` tool, and
326
+ * its children can only be granted the tools the worker itself was granted. A whole number >= 1, or omit it for no limit.
306
327
  */
307
328
  maxDepth?: number;
308
329
  /** Tool pool available to dynamic workers. Workers receive zero tools unless the Main Agent grants a per-task `tools` subset by name. */
309
330
  tools?: Record<string, ToolDefinition> | ToolDefinition[];
310
- /** Per-run timeout in ms of a SCHEDULED ACTION's worker (the `subagents` tool and `agent.workers` have no timeouts). `0` (default) = no limit. `-1` = the action sets its own `timeoutMs`. `>0` = fixed. */
311
- timeout?: number;
312
331
  /**
313
- * Canonical timeout in ms for scheduled-action workers (Q-52, preferred over `timeout`).
314
- * Maps to `timeout`: `workerTimeoutMs ?? timeout ?? 0`. See
315
- * `normalizeWorkerTimeout` (re-exported from the root entry).
332
+ * Longest a SCHEDULED ACTION's run may take, in ms (the `subagents` tool and `agent.workers` have no timeouts). An
333
+ * action can set its own shorter `timeoutMs`; this is the ceiling for it, and the limit for actions that set none.
334
+ * Default: no limit (`0` and `Infinity` mean the same).
316
335
  */
317
336
  workerTimeoutMs?: number;
318
- /**
319
- * Scheduled actions only: when true, each action sets its own `timeoutMs` (maps to
320
- * `timeout: -1`). When both are set, an explicit `timeout`/`workerTimeoutMs`
321
- * wins over `modelMaySetTimeout`.
322
- */
323
- modelMaySetTimeout?: boolean;
324
337
  }
325
338
 
326
339
  /** Per-run overrides for {@link Agent.run}, {@link Agent.ask}, and {@link Agent.stream}. */
@@ -346,8 +359,10 @@ export interface AgentRunOptions {
346
359
  * enforces `"steer"` or `"queue"`, that policy wins and this field is ignored.
347
360
  * When idle, this field is ignored and the call runs immediately.
348
361
  * Defaults to `"queue"` (finish current turn, run as next turn).
362
+ * `"interrupt"` is honored in every mode: it stops the active run (provider request, tools, workers) and starts this
363
+ * prompt as the next run, ahead of queued follow-ups.
349
364
  */
350
- enqueue?: "steer" | "queue";
365
+ enqueue?: "steer" | "queue" | "interrupt";
351
366
  /** Headers merged for this run only. */
352
367
  headers?: Record<string, string>;
353
368
  /** Called for each text delta as it streams (only when stream:true). */
@@ -9,7 +9,7 @@ export interface TextPart {
9
9
  thoughtSignature?: string;
10
10
  }
11
11
 
12
- /** Detail level a provider spends on one image or video (more detail = more input tokens). Google only; other providers ignore it with a warning. */
12
+ /** Detail level a provider spends on one image or video (more detail = more input tokens). Google (images and videos) and OpenRouter (images, as `image_url.detail`) honour it; other providers ignore it with a warning. */
13
13
  export type MediaResolution = "low" | "medium" | "high" | "ultra_high";
14
14
 
15
15
  /** Image content accepted as a URL, data, path, or binary value. */
@@ -15,6 +15,12 @@ export interface ScheduleSpec {
15
15
  repeatEvery?: string;
16
16
  /** Extra attempts if a run fails, only while time remains before delivery. Default 0. */
17
17
  retries?: number;
18
+ /**
19
+ * What happens when the host program was not running at a delivery time. `"skip"`: move on to the next time (a one-shot
20
+ * is done). `"run"`: run it as soon as the program is back. Omitted: the action is marked `missed` and waits for
21
+ * `runScheduledAction` or `skipScheduledAction`.
22
+ */
23
+ onMissed?: "skip" | "run";
18
24
  }
19
25
 
20
26
  /** What `createScheduledAction` receives: dynamic sub-agent fields plus the schedule. */
@@ -24,6 +30,7 @@ export interface ScheduledActionInput {
24
30
  task: string;
25
31
  /** Names from the developer's `dynamicSubagents.tools` pool. */
26
32
  tools?: string[];
33
+ /** Longest one run may take, in ms. Never above `dynamicSubagents.workerTimeoutMs` when the developer set one. */
27
34
  timeoutMs?: number;
28
35
  scheduled_action: ScheduleSpec & {
29
36
  /** Human label. */
@@ -74,8 +81,8 @@ export interface ScheduledDelivery {
74
81
  late: boolean;
75
82
  }
76
83
 
77
- /** Result of `killScheduledAction`. */
78
- export type KillResult =
84
+ /** Result of `deleteScheduledAction`. */
85
+ export type DeleteResult =
79
86
  | { ok: true }
80
87
  | { ok: false; reason: "not_found" }
81
88
  | { ok: false; reason: "running"; message: string };
@@ -113,7 +113,8 @@ const PART_TO_MODALITY: Record<string, "image" | "audio" | "video" | "pdf"> = {
113
113
 
114
114
  /**
115
115
  * Fails fast — before any network call — when the catalog knows the model
116
- * lacks a requested input modality (image/audio/video/document). Unknown
116
+ * lacks a requested input modality (image/audio/video/document; files are
117
+ * not checked on OpenRouter, which parses them for any model). Unknown
117
118
  * models (custom providers, dynamic routers) are skipped: the provider
118
119
  * verdict stands and surfaces via toConciseProviderError instead.
119
120
  */
@@ -130,6 +131,8 @@ export function assertModalitiesSupported(
130
131
  if (mapped) needed.add(mapped);
131
132
  }
132
133
  }
134
+ // OpenRouter reads a file with the model's native support or its own parser, so the catalog's "pdf" flag never gates a file there.
135
+ if (providerId === "openrouter") needed.delete("pdf");
133
136
  if (needed.size === 0) return;
134
137
  let supported: readonly string[] | undefined;
135
138
  try {
@@ -23,7 +23,7 @@ export function getPackageVersion(): string {
23
23
  const v = (globalThis as any).process?.env?.npm_package_version;
24
24
  if (typeof v === "string" && v) return v;
25
25
  } catch {}
26
- return "0.5.4";
26
+ return "0.5.6";
27
27
  }
28
28
 
29
29
  function getAgentAccelUserAgent(): string {
@@ -26,6 +26,11 @@ export function inferMimeType(input: string, fallback = "application/octet-strea
26
26
  if (clean.endsWith(".mp3")) return "audio/mp3";
27
27
  if (clean.endsWith(".wav")) return "audio/wav";
28
28
  if (clean.endsWith(".ogg")) return "audio/ogg";
29
+ if (clean.endsWith(".flac")) return "audio/flac";
30
+ if (clean.endsWith(".m4a")) return "audio/m4a";
31
+ if (clean.endsWith(".aac")) return "audio/aac";
32
+ if (clean.endsWith(".aiff") || clean.endsWith(".aif")) return "audio/aiff";
33
+ if (clean.endsWith(".mpeg") || clean.endsWith(".mpg")) return "video/mpeg";
29
34
  if (clean.endsWith(".mp4")) return "video/mp4";
30
35
  if (clean.endsWith(".webm")) return "video/webm";
31
36
  if (clean.endsWith(".mov")) return "video/quicktime";
@@ -53,8 +58,12 @@ export function sniffMimeType(bytes: Uint8Array): string | undefined {
53
58
  if (ascii(bytes, 0, "RIFF") && ascii(bytes, 8, "WAVE")) return "audio/wav";
54
59
  if (ascii(bytes, 0, "%PDF")) return "application/pdf";
55
60
  if (ascii(bytes, 0, "OggS")) return "audio/ogg";
61
+ if (ascii(bytes, 0, "fLaC")) return "audio/flac";
62
+ if (ascii(bytes, 0, "FORM") && ascii(bytes, 8, "AIF")) return "audio/aiff";
63
+ if (bytes[0] === 0xff && ((bytes[1] ?? 0) & 0xf6) === 0xf0) return "audio/aac"; // ADTS: sync bits plus layer 00 (MP3 frames have a non-zero layer)
56
64
  if (ascii(bytes, 0, "ID3") || (bytes[0] === 0xff && ((bytes[1] ?? 0) & 0xe0) === 0xe0)) return "audio/mp3";
57
- if (ascii(bytes, 4, "ftyp")) return ascii(bytes, 8, "qt") ? "video/quicktime" : "video/mp4";
65
+ if (ascii(bytes, 4, "ftyp")) return ascii(bytes, 8, "M4A") || ascii(bytes, 8, "M4B") ? "audio/m4a" : ascii(bytes, 8, "qt") ? "video/quicktime" : "video/mp4";
66
+ if (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 1 && bytes[3] === 0xba) return "video/mpeg"; // MPEG program stream
58
67
  if (bytes[0] === 0x1a && bytes[1] === 0x45 && bytes[2] === 0xdf && bytes[3] === 0xa3) return "video/webm";
59
68
  return undefined;
60
69
  }