@okrlinkhub/agent-factory 3.0.0 → 3.0.2

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.
Files changed (39) hide show
  1. package/dist/client/index.d.ts +42 -14
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +68 -2
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/_generated/api.d.ts +2 -0
  6. package/dist/component/_generated/api.d.ts.map +1 -1
  7. package/dist/component/_generated/api.js.map +1 -1
  8. package/dist/component/_generated/component.d.ts +114 -0
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/lib.d.ts +2 -1
  11. package/dist/component/lib.d.ts.map +1 -1
  12. package/dist/component/lib.js +2 -1
  13. package/dist/component/lib.js.map +1 -1
  14. package/dist/component/messageTemplates.d.ts +37 -0
  15. package/dist/component/messageTemplates.d.ts.map +1 -0
  16. package/dist/component/messageTemplates.js +177 -0
  17. package/dist/component/messageTemplates.js.map +1 -0
  18. package/dist/component/pushing.d.ts +14 -14
  19. package/dist/component/queue.d.ts +33 -0
  20. package/dist/component/queue.d.ts.map +1 -1
  21. package/dist/component/queue.js +89 -2
  22. package/dist/component/queue.js.map +1 -1
  23. package/dist/component/scheduler.d.ts.map +1 -1
  24. package/dist/component/scheduler.js +37 -10
  25. package/dist/component/scheduler.js.map +1 -1
  26. package/dist/component/schema.d.ts +34 -8
  27. package/dist/component/schema.d.ts.map +1 -1
  28. package/dist/component/schema.js +14 -0
  29. package/dist/component/schema.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/client/index.ts +69 -2
  32. package/src/component/_generated/api.ts +2 -0
  33. package/src/component/_generated/component.ts +158 -0
  34. package/src/component/lib.test.ts +216 -1
  35. package/src/component/lib.ts +8 -0
  36. package/src/component/messageTemplates.ts +205 -0
  37. package/src/component/queue.ts +110 -2
  38. package/src/component/scheduler.ts +77 -14
  39. package/src/component/schema.ts +15 -0
@@ -188,6 +188,11 @@ const workerSpawnOpenClawEnvValidator = v.object({
188
188
  OPENCLAW_LINKING_SHARED_SECRET: v.optional(v.string()),
189
189
  });
190
190
 
191
+ const schedulerConversationTargetValidator = v.object({
192
+ conversationId: v.string(),
193
+ agentKey: v.string(),
194
+ });
195
+
191
196
  const messageRuntimeConfigValidator = v.object({
192
197
  systemPrompt: v.optional(v.string()),
193
198
  telegramAttachmentRetentionMs: v.optional(v.number()),
@@ -489,6 +494,47 @@ export const getWorkerSpawnOpenClawEnv = internalQuery({
489
494
  },
490
495
  });
491
496
 
497
+ export const getActiveConversationsForScheduler = internalQuery({
498
+ args: {
499
+ nowMs: v.optional(v.number()),
500
+ limit: v.optional(v.number()),
501
+ },
502
+ returns: v.array(schedulerConversationTargetValidator),
503
+ handler: async (ctx, args) => {
504
+ const nowMs = args.nowMs ?? Date.now();
505
+ const limit = Math.max(1, args.limit ?? 1000);
506
+ const queuedJobs = await ctx.db
507
+ .query("messageQueue")
508
+ .withIndex("by_status_and_scheduledFor", (q) =>
509
+ q.eq("status", "queued").lte("scheduledFor", nowMs),
510
+ )
511
+ .take(limit);
512
+ const processingJobs = await ctx.db
513
+ .query("messageQueue")
514
+ .withIndex("by_status_and_leaseExpiresAt", (q) =>
515
+ q.eq("status", "processing").gt("leaseExpiresAt", nowMs),
516
+ )
517
+ .take(limit);
518
+
519
+ const conversations = new Map<string, { conversationId: string; agentKey: string }>();
520
+ for (const job of [...queuedJobs, ...processingJobs]) {
521
+ const key = `${job.agentKey}::${job.conversationId}`;
522
+ if (!conversations.has(key)) {
523
+ conversations.set(key, {
524
+ conversationId: job.conversationId,
525
+ agentKey: job.agentKey,
526
+ });
527
+ }
528
+ }
529
+
530
+ return Array.from(conversations.values()).sort(
531
+ (left, right) =>
532
+ left.agentKey.localeCompare(right.agentKey) ||
533
+ left.conversationId.localeCompare(right.conversationId),
534
+ );
535
+ },
536
+ });
537
+
492
538
  export const getProviderRuntimeConfig = internalQuery({
493
539
  args: {},
494
540
  returns: v.union(v.null(), providerConfigValidator),
@@ -2013,6 +2059,67 @@ export const sendMessageToUserAgent = mutation({
2013
2059
  },
2014
2060
  });
2015
2061
 
2062
+ export const sendMessageTemplateToUserAgent = mutation({
2063
+ args: {
2064
+ consumerUserId: v.string(),
2065
+ agentKey: v.string(),
2066
+ templateId: v.id("messageTemplates"),
2067
+ metadata: v.optional(v.record(v.string(), v.string())),
2068
+ nowMs: v.optional(v.number()),
2069
+ providerConfig: v.optional(providerConfigValidator),
2070
+ },
2071
+ returns: v.object({
2072
+ messageId: v.id("messageQueue"),
2073
+ usageCount: v.number(),
2074
+ }),
2075
+ handler: async (ctx, args) => {
2076
+ const template = await ctx.db.get(args.templateId);
2077
+ if (!template || !template.enabled) {
2078
+ throw new Error("Message template not found");
2079
+ }
2080
+
2081
+ const nowMs = args.nowMs ?? Date.now();
2082
+ const target = await resolveConversationTargetForUserAgent(
2083
+ ctx,
2084
+ args.consumerUserId,
2085
+ args.agentKey,
2086
+ true,
2087
+ );
2088
+ const providerUserId =
2089
+ target.telegramUserId ?? target.telegramChatId ?? args.consumerUserId;
2090
+ const usageCount = template.usageCount + 1;
2091
+ const messageId = await enqueueMessageRecord(ctx, {
2092
+ conversationId: target.conversationId,
2093
+ agentKey: args.agentKey,
2094
+ payload: {
2095
+ provider: target.provider,
2096
+ providerUserId,
2097
+ messageText: template.text,
2098
+ metadata: {
2099
+ ...(args.metadata ?? {}),
2100
+ consumerUserId: args.consumerUserId,
2101
+ source: "message_template",
2102
+ templateId: String(template._id),
2103
+ templateKey: template.templateKey,
2104
+ ...(target.telegramChatId ? { telegramChatId: target.telegramChatId } : {}),
2105
+ ...(target.telegramUserId ? { telegramUserId: target.telegramUserId } : {}),
2106
+ },
2107
+ },
2108
+ scheduledFor: nowMs,
2109
+ providerConfig: args.providerConfig,
2110
+ });
2111
+
2112
+ await ctx.db.patch(template._id, {
2113
+ usageCount,
2114
+ });
2115
+
2116
+ return {
2117
+ messageId,
2118
+ usageCount,
2119
+ };
2120
+ },
2121
+ });
2122
+
2016
2123
  export const listSnapshotsForConversation = query({
2017
2124
  args: {
2018
2125
  conversationId: v.string(),
@@ -2216,6 +2323,7 @@ export const upsertWorkerState = internalMutation({
2216
2323
  machineId: v.optional(v.string()),
2217
2324
  appName: v.optional(v.string()),
2218
2325
  region: v.optional(v.string()),
2326
+ assignment: v.optional(v.union(v.null(), workerAssignmentValidator)),
2219
2327
  clearLastSnapshotId: v.optional(v.boolean()),
2220
2328
  clearMachineRef: v.optional(v.boolean()),
2221
2329
  },
@@ -2238,7 +2346,7 @@ export const upsertWorkerState = internalMutation({
2238
2346
  args.status === "stopped" || args.status === "stopping"
2239
2347
  ? (args.stoppedAt ?? nowMs)
2240
2348
  : undefined,
2241
- assignment: undefined,
2349
+ assignment: args.assignment ?? undefined,
2242
2350
  machineRef:
2243
2351
  args.machineId && args.appName
2244
2352
  ? {
@@ -2268,7 +2376,7 @@ export const upsertWorkerState = internalMutation({
2268
2376
  ? (args.stoppedAt ?? worker.stoppedAt ?? nowMs)
2269
2377
  : undefined,
2270
2378
  lastSnapshotId: args.clearLastSnapshotId ? undefined : worker.lastSnapshotId,
2271
- assignment: worker.assignment,
2379
+ assignment: args.assignment === undefined ? worker.assignment : (args.assignment ?? undefined),
2272
2380
  machineRef:
2273
2381
  args.clearMachineRef
2274
2382
  ? undefined
@@ -75,6 +75,11 @@ type SchedulerWorkerRow = {
75
75
  region: string | null;
76
76
  };
77
77
 
78
+ type SchedulerConversationTarget = {
79
+ conversationId: string;
80
+ agentKey: string;
81
+ };
82
+
78
83
  const PROVIDER_RECONCILE_GRACE_MS = 90_000;
79
84
 
80
85
  export const reconcileWorkerPool = action({
@@ -182,11 +187,11 @@ async function runReconcileWorkerPool(
182
187
  }
183
188
  const workspaceId = args.workspaceId ?? "default";
184
189
  const provider = resolveProvider(providerConfig.kind, flyApiToken);
185
- const activeConversationIds: Array<string> = await ctx.runQuery(
186
- (internal.queue as any).getActiveConversationIdsForScheduler,
190
+ const activeConversations: Array<SchedulerConversationTarget> = await ctx.runQuery(
191
+ (internal.queue as any).getActiveConversationsForScheduler,
187
192
  { nowMs, limit: 1000 },
188
193
  );
189
- const activeConversationCount = activeConversationIds.length;
194
+ const activeConversationCount = activeConversations.length;
190
195
  const cycle = await runWorkerLifecycleCycle(ctx, {
191
196
  nowMs,
192
197
  provider,
@@ -195,7 +200,7 @@ async function runReconcileWorkerPool(
195
200
  allowSpawn: true,
196
201
  convexUrl,
197
202
  workspaceId,
198
- activeConversationIds,
203
+ activeConversations,
199
204
  desiredActiveWorkers: clamp(activeConversationCount, 0, scaling.maxWorkers),
200
205
  });
201
206
  if (activeConversationCount > 0 || cycle.pending > 0) {
@@ -258,7 +263,7 @@ async function runEnforceIdleShutdowns(
258
263
  scaling: DEFAULT_CONFIG.scaling,
259
264
  allowSpawn: false,
260
265
  desiredActiveWorkers: 0,
261
- activeConversationIds: [],
266
+ activeConversations: [],
262
267
  });
263
268
 
264
269
  if (cycle.pending > 0) {
@@ -282,7 +287,7 @@ async function runWorkerLifecycleCycle(
282
287
  scaling: typeof DEFAULT_CONFIG.scaling;
283
288
  allowSpawn: boolean;
284
289
  desiredActiveWorkers: number;
285
- activeConversationIds: Array<string>;
290
+ activeConversations: Array<SchedulerConversationTarget>;
286
291
  convexUrl?: string;
287
292
  workspaceId?: string;
288
293
  },
@@ -338,7 +343,7 @@ async function runWorkerLifecycleCycle(
338
343
  if (input.allowSpawn && input.desiredActiveWorkers > 0) {
339
344
  const claimableWorkers = countWorkersAvailableForActiveConversations(
340
345
  filterScopedWorkers(workerRows, input.providerConfig.appName),
341
- input.activeConversationIds,
346
+ input.activeConversations,
342
347
  staleHeartbeatCutoff,
343
348
  );
344
349
  if (input.desiredActiveWorkers > claimableWorkers) {
@@ -350,8 +355,23 @@ async function runWorkerLifecycleCycle(
350
355
  input.scaling.spawnStep,
351
356
  input.desiredActiveWorkers - claimableWorkers,
352
357
  );
358
+ const spawnTargets = selectSpawnTargetsForActiveConversations(
359
+ filterScopedWorkers(workerRows, input.providerConfig.appName),
360
+ input.activeConversations,
361
+ staleHeartbeatCutoff,
362
+ toSpawn,
363
+ );
353
364
  for (let index = 0; index < toSpawn; index += 1) {
354
365
  const workerId = `afw-${input.nowMs}-${index}`;
366
+ const target = spawnTargets[index];
367
+ const assignment = target
368
+ ? {
369
+ conversationId: target.conversationId,
370
+ agentKey: target.agentKey,
371
+ leaseId: `spawn:${workerId}`,
372
+ assignedAt: input.nowMs,
373
+ }
374
+ : undefined;
355
375
  await ctx.runMutation(internal.queue.upsertWorkerState, {
356
376
  workerId,
357
377
  provider: input.providerConfig.kind,
@@ -359,6 +379,7 @@ async function runWorkerLifecycleCycle(
359
379
  load: 0,
360
380
  nowMs: input.nowMs,
361
381
  scheduledShutdownAt: input.nowMs + input.scaling.idleTimeoutMs,
382
+ assignment,
362
383
  });
363
384
  let created;
364
385
  try {
@@ -377,6 +398,8 @@ async function runWorkerLifecycleCycle(
377
398
  WORKSPACE_ID: input.workspaceId ?? "default",
378
399
  WORKER_ID: workerId,
379
400
  WORKER_IDLE_TIMEOUT_MS: String(input.scaling.idleTimeoutMs),
401
+ OPENCLAW_AGENT_KEY: target?.agentKey,
402
+ OPENCLAW_CONVERSATION_ID: target?.conversationId,
380
403
  }),
381
404
  });
382
405
  } catch (error) {
@@ -456,6 +479,7 @@ async function runWorkerLifecycleCycle(
456
479
  machineId: created.machineId,
457
480
  appName: input.providerConfig.appName,
458
481
  region: created.region,
482
+ assignment,
459
483
  });
460
484
  await scheduleIdleShutdownWatch(
461
485
  ctx,
@@ -862,10 +886,46 @@ function filterScopedWorkers(workerRows: Array<SchedulerWorkerRow>, appName: str
862
886
 
863
887
  function countWorkersAvailableForActiveConversations(
864
888
  workerRows: Array<SchedulerWorkerRow>,
865
- activeConversationIds: Array<string>,
889
+ activeConversations: Array<SchedulerConversationTarget>,
866
890
  staleHeartbeatCutoff: number,
867
891
  ) {
868
- const activeConversationSet = new Set(activeConversationIds);
892
+ const coverage = summarizeWorkerConversationCoverage(
893
+ workerRows,
894
+ activeConversations,
895
+ staleHeartbeatCutoff,
896
+ );
897
+ return coverage.unassignedWorkers + coverage.assignedConversationKeys.size;
898
+ }
899
+
900
+ function selectSpawnTargetsForActiveConversations(
901
+ workerRows: Array<SchedulerWorkerRow>,
902
+ activeConversations: Array<SchedulerConversationTarget>,
903
+ staleHeartbeatCutoff: number,
904
+ limit: number,
905
+ ) {
906
+ const coverage = summarizeWorkerConversationCoverage(
907
+ workerRows,
908
+ activeConversations,
909
+ staleHeartbeatCutoff,
910
+ );
911
+ const uncoveredConversations = activeConversations.filter(
912
+ (conversation) =>
913
+ !coverage.assignedConversationKeys.has(getConversationTargetKey(conversation)),
914
+ );
915
+ return uncoveredConversations.slice(
916
+ coverage.unassignedWorkers,
917
+ coverage.unassignedWorkers + Math.max(0, limit),
918
+ );
919
+ }
920
+
921
+ function summarizeWorkerConversationCoverage(
922
+ workerRows: Array<SchedulerWorkerRow>,
923
+ activeConversations: Array<SchedulerConversationTarget>,
924
+ staleHeartbeatCutoff: number,
925
+ ) {
926
+ const activeConversationSet = new Set(
927
+ activeConversations.map((conversation) => getConversationTargetKey(conversation)),
928
+ );
869
929
  const assignedConversationKeys = new Set<string>();
870
930
  let unassignedWorkers = 0;
871
931
  for (const worker of workerRows) {
@@ -876,13 +936,16 @@ function countWorkersAvailableForActiveConversations(
876
936
  unassignedWorkers += 1;
877
937
  continue;
878
938
  }
879
- if (activeConversationSet.has(worker.assignment.conversationId)) {
880
- assignedConversationKeys.add(
881
- `${worker.assignment.agentKey}::${worker.assignment.conversationId}`,
882
- );
939
+ const assignmentKey = getConversationTargetKey(worker.assignment);
940
+ if (activeConversationSet.has(assignmentKey)) {
941
+ assignedConversationKeys.add(assignmentKey);
883
942
  }
884
943
  }
885
- return unassignedWorkers + assignedConversationKeys.size;
944
+ return { assignedConversationKeys, unassignedWorkers };
945
+ }
946
+
947
+ function getConversationTargetKey(input: { conversationId: string; agentKey: string }) {
948
+ return `${input.agentKey}::${input.conversationId}`;
886
949
  }
887
950
 
888
951
  function deriveScheduledShutdownAt(
@@ -410,6 +410,21 @@ export default defineSchema({
410
410
  .index("by_companyId_and_templateKey", ["companyId", "templateKey"])
411
411
  .index("by_companyId_and_enabled", ["companyId", "enabled"]),
412
412
 
413
+ messageTemplates: defineTable({
414
+ templateKey: v.string(),
415
+ title: v.string(),
416
+ text: v.string(),
417
+ tags: v.array(v.string()),
418
+ usageCount: v.number(),
419
+ enabled: v.boolean(),
420
+ createdBy: v.string(),
421
+ updatedBy: v.string(),
422
+ createdAt: v.number(),
423
+ updatedAt: v.number(),
424
+ })
425
+ .index("by_templateKey", ["templateKey"])
426
+ .index("by_enabled", ["enabled"]),
427
+
413
428
  messagePushJobs: defineTable({
414
429
  companyId: v.string(),
415
430
  consumerUserId: v.string(),