@okrlinkhub/agent-factory 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { v } from "convex/values";
2
- import { internal } from "./_generated/api.js";
3
- import { internalMutation, internalQuery, mutation, query, } from "./_generated/server.js";
2
+ import { api, internal } from "./_generated/api.js";
3
+ import { action, internalMutation, internalQuery, mutation, query, } from "./_generated/server.js";
4
4
  import { computeRetryDelayMs, DEFAULT_CONFIG, providerConfigValidator } from "./config.js";
5
5
  import { canTransitionWorkerStatus, isWorkerClaimable, isWorkerRunning, isWorkerTerminal, workerStatusValidator, } from "./workerLifecycle.js";
6
6
  const queueStatusValidator = v.union(v.literal("queued"), v.literal("processing"), v.literal("done"), v.literal("failed"), v.literal("dead_letter"));
@@ -15,6 +15,14 @@ const telegramAttachmentValidator = v.object({
15
15
  mimeType: v.optional(v.string()),
16
16
  sizeBytes: v.optional(v.number()),
17
17
  expiresAt: v.number(),
18
+ downloadUrl: v.optional(v.string()),
19
+ });
20
+ const telegramAttachmentCandidateValidator = v.object({
21
+ kind: telegramAttachmentKindValidator,
22
+ telegramFileId: v.string(),
23
+ fileName: v.optional(v.string()),
24
+ mimeType: v.optional(v.string()),
25
+ sizeBytes: v.optional(v.number()),
18
26
  });
19
27
  const queuePayloadValidator = v.object({
20
28
  provider: v.string(),
@@ -474,6 +482,23 @@ export const getTelegramIngressRuntimeConfig = internalQuery({
474
482
  };
475
483
  },
476
484
  });
485
+ export const prepareTelegramAttachmentsForEnqueue = action({
486
+ args: {
487
+ agentKey: v.string(),
488
+ attachments: v.array(telegramAttachmentCandidateValidator),
489
+ },
490
+ returns: v.array(telegramAttachmentValidator),
491
+ handler: async (ctx, args) => {
492
+ const ingressConfig = await ctx.runQuery(internal.queue.getTelegramIngressRuntimeConfig, {
493
+ agentKey: args.agentKey,
494
+ });
495
+ if (!ingressConfig.botToken) {
496
+ throw new Error(`missing active telegram bot token for agent '${args.agentKey}'`);
497
+ }
498
+ const expiresAt = Date.now() + ingressConfig.attachmentRetentionMs;
499
+ return await Promise.all(args.attachments.map((attachment) => persistTelegramAttachmentFromCandidate(ctx, ingressConfig.botToken, attachment, expiresAt)));
500
+ },
501
+ });
477
502
  export const upsertMessageRuntimeConfig = internalMutation({
478
503
  args: {
479
504
  messageConfig: messageRuntimeConfigValidator,
@@ -1451,11 +1476,12 @@ export const getHydrationBundleForClaimedJob = query({
1451
1476
  ? conversationCache.deltaContext
1452
1477
  : conversation.contextHistory;
1453
1478
  const bridgeRuntimeConfig = await resolveBridgeRuntimeConfig(ctx, profile);
1479
+ const hydratedPayload = await hydrateQueuePayloadAttachments(ctx, message.payload);
1454
1480
  return {
1455
1481
  messageId: message._id,
1456
1482
  conversationId: message.conversationId,
1457
1483
  agentKey: message.agentKey,
1458
- payload: message.payload,
1484
+ payload: hydratedPayload,
1459
1485
  conversationState: {
1460
1486
  contextHistory,
1461
1487
  pendingToolCalls: conversation.pendingToolCalls,
@@ -2310,6 +2336,21 @@ async function enqueueMessageRecord(ctx, args) {
2310
2336
  }
2311
2337
  return messageId;
2312
2338
  }
2339
+ async function hydrateQueuePayloadAttachments(ctx, payload) {
2340
+ if (!payload.attachments?.length) {
2341
+ return payload;
2342
+ }
2343
+ const attachments = await Promise.all(payload.attachments.map(async (attachment) => ({
2344
+ ...attachment,
2345
+ downloadUrl: attachment.status === "ready"
2346
+ ? ((await ctx.storage.getUrl(attachment.storageId)) ?? undefined)
2347
+ : undefined,
2348
+ })));
2349
+ return {
2350
+ ...payload,
2351
+ attachments,
2352
+ };
2353
+ }
2313
2354
  async function resolveConversationTargetForUserAgent(ctx, consumerUserId, agentKey, requireActive = false) {
2314
2355
  const bindings = await ctx.db
2315
2356
  .query("identityBindings")
@@ -2480,6 +2521,115 @@ async function resolveActiveTelegramBotToken(ctx, secretRefs) {
2480
2521
  }
2481
2522
  return null;
2482
2523
  }
2524
+ async function persistTelegramAttachmentFromCandidate(ctx, telegramBotToken, attachment, expiresAt) {
2525
+ const filePath = await fetchTelegramFilePath(telegramBotToken, attachment.telegramFileId);
2526
+ const downloaded = await downloadTelegramFile(telegramBotToken, filePath);
2527
+ const resolvedMimeType = resolvePreferredTelegramAttachmentMimeType(attachment.mimeType, attachment.fileName, downloaded.mimeType, filePath);
2528
+ const uploadTarget = await ctx.runMutation(api.queue.generateMediaUploadUrl, {});
2529
+ const uploadResponse = await fetch(uploadTarget.uploadUrl, {
2530
+ method: "POST",
2531
+ headers: resolvedMimeType.length > 0
2532
+ ? {
2533
+ "Content-Type": resolvedMimeType,
2534
+ }
2535
+ : undefined,
2536
+ body: downloaded.blob,
2537
+ });
2538
+ const uploadPayload = (await uploadResponse.json().catch(() => ({})));
2539
+ if (!uploadResponse.ok || !uploadPayload.storageId) {
2540
+ throw new Error(`Convex storage upload failed for Telegram ${attachment.kind} attachment`);
2541
+ }
2542
+ return {
2543
+ kind: attachment.kind,
2544
+ status: "ready",
2545
+ storageId: uploadPayload.storageId,
2546
+ telegramFileId: attachment.telegramFileId,
2547
+ fileName: attachment.fileName,
2548
+ mimeType: resolvedMimeType,
2549
+ sizeBytes: attachment.sizeBytes ?? downloaded.blob.size,
2550
+ expiresAt,
2551
+ };
2552
+ }
2553
+ async function fetchTelegramFilePath(telegramBotToken, telegramFileId) {
2554
+ const telegramApiBaseUrl = `https://api.telegram.org/bot${encodeURIComponent(telegramBotToken)}`;
2555
+ const response = await fetch(`${telegramApiBaseUrl}/getFile?file_id=${encodeURIComponent(telegramFileId)}`);
2556
+ const payload = (await response.json().catch(() => ({})));
2557
+ if (!response.ok || payload.ok !== true || typeof payload.result?.file_path !== "string") {
2558
+ throw new Error(`Telegram getFile failed: ${typeof payload.description === "string" ? payload.description : "missing file_path"}`);
2559
+ }
2560
+ return payload.result.file_path;
2561
+ }
2562
+ async function downloadTelegramFile(telegramBotToken, filePath) {
2563
+ const response = await fetch(`https://api.telegram.org/file/bot${encodeURIComponent(telegramBotToken)}/${filePath}`);
2564
+ if (!response.ok) {
2565
+ throw new Error(`Telegram file download failed with status ${response.status}`);
2566
+ }
2567
+ const blob = await response.blob();
2568
+ const mimeType = response.headers.get("Content-Type") ??
2569
+ blob.type ??
2570
+ inferMimeTypeFromFilePath(filePath) ??
2571
+ "application/octet-stream";
2572
+ return {
2573
+ blob: mimeType === blob.type ? blob : new Blob([await blob.arrayBuffer()], { type: mimeType }),
2574
+ mimeType,
2575
+ };
2576
+ }
2577
+ function resolvePreferredTelegramAttachmentMimeType(originalMimeType, fileName, downloadedMimeType, filePath) {
2578
+ const normalizedOriginalMimeType = normalizeNonGenericMimeType(originalMimeType);
2579
+ if (normalizedOriginalMimeType) {
2580
+ return normalizedOriginalMimeType;
2581
+ }
2582
+ const inferredFromFileName = inferMimeTypeFromFileName(fileName);
2583
+ if (inferredFromFileName) {
2584
+ return inferredFromFileName;
2585
+ }
2586
+ const normalizedDownloadedMimeType = normalizeNonGenericMimeType(downloadedMimeType);
2587
+ if (normalizedDownloadedMimeType) {
2588
+ return normalizedDownloadedMimeType;
2589
+ }
2590
+ return inferMimeTypeFromFilePath(filePath) ?? "application/octet-stream";
2591
+ }
2592
+ function normalizeNonGenericMimeType(mimeType) {
2593
+ if (typeof mimeType !== "string") {
2594
+ return null;
2595
+ }
2596
+ const normalizedMimeType = mimeType.trim().toLowerCase();
2597
+ if (!normalizedMimeType || normalizedMimeType === "application/octet-stream") {
2598
+ return null;
2599
+ }
2600
+ return normalizedMimeType;
2601
+ }
2602
+ function inferMimeTypeFromFileName(fileName) {
2603
+ if (typeof fileName !== "string") {
2604
+ return null;
2605
+ }
2606
+ return inferMimeTypeFromFilePath(fileName);
2607
+ }
2608
+ function inferMimeTypeFromFilePath(filePath) {
2609
+ const normalizedPath = filePath.toLowerCase();
2610
+ if (normalizedPath.endsWith(".jpg") || normalizedPath.endsWith(".jpeg")) {
2611
+ return "image/jpeg";
2612
+ }
2613
+ if (normalizedPath.endsWith(".png")) {
2614
+ return "image/png";
2615
+ }
2616
+ if (normalizedPath.endsWith(".webp")) {
2617
+ return "image/webp";
2618
+ }
2619
+ if (normalizedPath.endsWith(".mp4")) {
2620
+ return "video/mp4";
2621
+ }
2622
+ if (normalizedPath.endsWith(".mp3")) {
2623
+ return "audio/mpeg";
2624
+ }
2625
+ if (normalizedPath.endsWith(".ogg")) {
2626
+ return "audio/ogg";
2627
+ }
2628
+ if (normalizedPath.endsWith(".pdf")) {
2629
+ return "application/pdf";
2630
+ }
2631
+ return null;
2632
+ }
2483
2633
  async function buildGlobalSkillMaterialization(skill) {
2484
2634
  const skillDirName = normalizeGlobalSkillDirName(skill.slug);
2485
2635
  const scriptExt = skill.moduleFormat === "cjs" ? "cjs" : "mjs";