@okrlinkhub/agent-factory 0.2.13 → 0.2.14

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.
@@ -52,6 +52,181 @@ describe("component lib", () => {
52
52
  expect(claimed?.conversationId).toBe("telegram:chat:1");
53
53
  });
54
54
 
55
+ test("minimal agent profile should work when payload provides providerUserId", async () => {
56
+ const t = initConvexTest();
57
+ await t.mutation(api.queue.upsertAgentProfile, {
58
+ agentKey: "minimal-agent",
59
+ version: "1.0.0",
60
+ secretsRef: [],
61
+ enabled: true,
62
+ });
63
+
64
+ const messageId = await t.mutation(api.lib.enqueue, {
65
+ conversationId: "telegram:chat:minimal",
66
+ agentKey: "minimal-agent",
67
+ payload: {
68
+ provider: "telegram",
69
+ providerUserId: "u-minimal-1",
70
+ messageText: "hello",
71
+ },
72
+ });
73
+
74
+ const claim = await t.mutation(api.lib.claim, {
75
+ workerId: "worker-minimal-1",
76
+ });
77
+ expect(claim?.messageId).toBe(messageId);
78
+ expect(claim?.payload.providerUserId).toBe("u-minimal-1");
79
+
80
+ const bundle = await t.query(api.lib.getHydrationBundle, {
81
+ messageId,
82
+ workspaceId: "default",
83
+ });
84
+ expect(bundle).not.toBeNull();
85
+ expect(bundle?.payload.providerUserId).toBe("u-minimal-1");
86
+ expect(bundle?.bridgeRuntimeConfig).toBeNull();
87
+ });
88
+
89
+ test("enqueue should append global system prompt to queued message", async () => {
90
+ const t = initConvexTest();
91
+ await t.mutation(api.queue.upsertAgentProfile, {
92
+ agentKey: "system-prompt-agent",
93
+ version: "1.0.0",
94
+ secretsRef: [],
95
+ enabled: true,
96
+ });
97
+ await t.mutation(api.lib.setMessageRuntimeConfig, {
98
+ messageConfig: {
99
+ systemPrompt: " Rispondi sempre con un breve riassunto finale. ",
100
+ },
101
+ });
102
+
103
+ const storedMessageConfig = await t.query(api.lib.messageRuntimeConfig, {});
104
+ expect(storedMessageConfig).toEqual({
105
+ systemPrompt: "Rispondi sempre con un breve riassunto finale.",
106
+ });
107
+
108
+ const messageId = await t.mutation(api.lib.enqueue, {
109
+ conversationId: "telegram:chat:system-prompt",
110
+ agentKey: "system-prompt-agent",
111
+ payload: {
112
+ provider: "telegram",
113
+ providerUserId: "u-system-prompt-1",
114
+ messageText: "Come va?",
115
+ },
116
+ });
117
+
118
+ const claim = await t.mutation(api.lib.claim, {
119
+ workerId: "worker-system-prompt-1",
120
+ });
121
+ expect(claim?.messageId).toBe(messageId);
122
+ expect(claim?.payload.messageText).toBe(
123
+ "Come va?\n\nRispondi sempre con un breve riassunto finale.",
124
+ );
125
+ });
126
+
127
+ test("blank global system prompt should not modify queued messages", async () => {
128
+ const t = initConvexTest();
129
+ await t.mutation(api.queue.upsertAgentProfile, {
130
+ agentKey: "blank-system-prompt-agent",
131
+ version: "1.0.0",
132
+ secretsRef: [],
133
+ enabled: true,
134
+ });
135
+ await t.mutation(api.lib.setMessageRuntimeConfig, {
136
+ messageConfig: {
137
+ systemPrompt: " ",
138
+ },
139
+ });
140
+
141
+ const storedMessageConfig = await t.query(api.lib.messageRuntimeConfig, {});
142
+ expect(storedMessageConfig).toBeNull();
143
+
144
+ const messageId = await t.mutation(api.lib.enqueue, {
145
+ conversationId: "telegram:chat:blank-system-prompt",
146
+ agentKey: "blank-system-prompt-agent",
147
+ payload: {
148
+ provider: "telegram",
149
+ providerUserId: "u-blank-system-prompt-1",
150
+ messageText: "hello",
151
+ },
152
+ });
153
+
154
+ const claim = await t.mutation(api.lib.claim, {
155
+ workerId: "worker-blank-system-prompt-1",
156
+ });
157
+ expect(claim?.messageId).toBe(messageId);
158
+ expect(claim?.payload.messageText).toBe("hello");
159
+ });
160
+
161
+ test("enqueue should fail when providerUserId is blank in both profile and payload", async () => {
162
+ const t = initConvexTest();
163
+ await t.mutation(api.queue.upsertAgentProfile, {
164
+ agentKey: "missing-provider-user-agent",
165
+ version: "1.0.0",
166
+ providerUserId: " ",
167
+ secretsRef: [],
168
+ enabled: true,
169
+ });
170
+
171
+ await expect(
172
+ t.mutation(api.queue.enqueueMessage, {
173
+ conversationId: "telegram:chat:missing-provider-user",
174
+ agentKey: "missing-provider-user-agent",
175
+ payload: {
176
+ provider: "telegram",
177
+ providerUserId: " ",
178
+ messageText: "hello",
179
+ },
180
+ }),
181
+ ).rejects.toThrow("providerUserId is required but missing");
182
+ });
183
+
184
+ test("clearDeprecatedAgentProfileFields should remove deprecated profile fields", async () => {
185
+ const t = initConvexTest();
186
+ await t.mutation(api.queue.upsertAgentProfile, {
187
+ agentKey: "cleanup-agent",
188
+ version: "1.0.0",
189
+ providerUserId: "legacy-user-1",
190
+ soulMd: "# Legacy Soul",
191
+ clientMd: "# Legacy Client",
192
+ skills: ["agent-bridge"],
193
+ secretsRef: [],
194
+ enabled: true,
195
+ });
196
+ await t.mutation(api.queue.upsertAgentProfile, {
197
+ agentKey: "already-clean-agent",
198
+ version: "1.0.0",
199
+ secretsRef: [],
200
+ enabled: true,
201
+ });
202
+
203
+ const dryRun = await t.mutation((api.lib as any).clearDeprecatedAgentProfileFields, {
204
+ dryRun: true,
205
+ });
206
+ expect(dryRun.dryRun).toBe(true);
207
+ expect(dryRun.scanned).toBe(2);
208
+ expect(dryRun.updated).toBe(1);
209
+ expect(dryRun.unchanged).toBe(1);
210
+ expect(dryRun.clearedProviderUserId).toBe(1);
211
+ expect(dryRun.clearedSoulMd).toBe(1);
212
+ expect(dryRun.clearedClientMd).toBe(1);
213
+ expect(dryRun.clearedSkills).toBe(1);
214
+ expect(dryRun.updatedAgentKeys).toEqual(["cleanup-agent"]);
215
+
216
+ const cleanup = await t.mutation((api.lib as any).clearDeprecatedAgentProfileFields, {});
217
+ expect(cleanup.dryRun).toBe(false);
218
+ expect(cleanup.updated).toBe(1);
219
+ expect(cleanup.updatedAgentKeys).toEqual(["cleanup-agent"]);
220
+
221
+ const secondPass = await t.mutation((api.lib as any).clearDeprecatedAgentProfileFields, {});
222
+ expect(secondPass.updated).toBe(0);
223
+ expect(secondPass.unchanged).toBe(2);
224
+ expect(secondPass.clearedProviderUserId).toBe(0);
225
+ expect(secondPass.clearedSoulMd).toBe(0);
226
+ expect(secondPass.clearedClientMd).toBe(0);
227
+ expect(secondPass.clearedSkills).toBe(0);
228
+ });
229
+
55
230
  test("identity binding should resolve, rebind and revoke", async () => {
56
231
  const t = initConvexTest();
57
232
  await t.mutation(api.queue.upsertAgentProfile, {
@@ -1,9 +1,12 @@
1
1
  export {
2
2
  upsertAgentProfile as configureAgent,
3
+ clearDeprecatedAgentProfileFields,
3
4
  importPlaintextSecret as importSecret,
4
5
  getSecretsStatus as secretStatus,
5
6
  providerRuntimeConfig,
6
7
  setProviderRuntimeConfig,
8
+ messageRuntimeConfig,
9
+ setMessageRuntimeConfig,
7
10
  enqueueMessage as enqueue,
8
11
  releaseStuckJobs,
9
12
  appendConversationMessages,
@@ -65,6 +65,10 @@ const bridgeRuntimeConfigValidator = v.object({
65
65
  serviceKeySecretRef: v.union(v.null(), v.string()),
66
66
  });
67
67
 
68
+ const messageRuntimeConfigValidator = v.object({
69
+ systemPrompt: v.optional(v.string()),
70
+ });
71
+
68
72
  const globalSkillStatusValidator = v.union(v.literal("active"), v.literal("disabled"));
69
73
  const globalSkillReleaseChannelValidator = v.union(v.literal("stable"), v.literal("canary"));
70
74
  const globalSkillModuleFormatValidator = v.union(v.literal("esm"), v.literal("cjs"));
@@ -86,6 +90,11 @@ const BRIDGE_SECRET_REFS = {
86
90
  appKey: "agent-bridge.appKey",
87
91
  } as const;
88
92
 
93
+ const RUNTIME_CONFIG_KEYS = {
94
+ provider: "provider",
95
+ message: "message",
96
+ } as const;
97
+
89
98
  export const enqueueMessage = mutation({
90
99
  args: {
91
100
  conversationId: v.string(),
@@ -100,6 +109,10 @@ export const enqueueMessage = mutation({
100
109
  returns: v.id("messageQueue"),
101
110
  handler: async (ctx, args) => {
102
111
  const nowMs = args.nowMs ?? Date.now();
112
+ const messageRuntimeConfigRow = await ctx.db
113
+ .query("runtimeConfig")
114
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.message))
115
+ .unique();
103
116
  const profile = await ctx.db
104
117
  .query("agentProfiles")
105
118
  .withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
@@ -126,6 +139,10 @@ export const enqueueMessage = mutation({
126
139
 
127
140
  const payload = {
128
141
  ...args.payload,
142
+ messageText: appendSystemPromptToMessage(
143
+ args.payload.messageText,
144
+ messageRuntimeConfigRow?.messageConfig?.systemPrompt,
145
+ ),
129
146
  providerUserId: providerUserIdStr,
130
147
  metadata: {
131
148
  ...(args.payload.metadata ?? {}),
@@ -252,9 +269,9 @@ export const upsertAgentProfile = mutation({
252
269
  agentKey: v.string(),
253
270
  providerUserId: v.optional(v.string()),
254
271
  version: v.string(),
255
- soulMd: v.string(),
272
+ soulMd: v.optional(v.string()),
256
273
  clientMd: v.optional(v.string()),
257
- skills: v.array(v.string()),
274
+ skills: v.optional(v.array(v.string())),
258
275
  secretsRef: v.array(v.string()),
259
276
  bridgeConfig: v.optional(bridgeProfileConfigValidator),
260
277
  enabled: v.boolean(),
@@ -278,6 +295,85 @@ export const upsertAgentProfile = mutation({
278
295
  },
279
296
  });
280
297
 
298
+ export const clearDeprecatedAgentProfileFields = mutation({
299
+ args: {
300
+ dryRun: v.optional(v.boolean()),
301
+ },
302
+ returns: v.object({
303
+ dryRun: v.boolean(),
304
+ scanned: v.number(),
305
+ updated: v.number(),
306
+ unchanged: v.number(),
307
+ clearedProviderUserId: v.number(),
308
+ clearedSoulMd: v.number(),
309
+ clearedClientMd: v.number(),
310
+ clearedSkills: v.number(),
311
+ updatedAgentKeys: v.array(v.string()),
312
+ }),
313
+ handler: async (ctx, args) => {
314
+ const profiles = await ctx.db.query("agentProfiles").collect();
315
+ const dryRun = args.dryRun ?? false;
316
+
317
+ let updated = 0;
318
+ let clearedProviderUserId = 0;
319
+ let clearedSoulMd = 0;
320
+ let clearedClientMd = 0;
321
+ let clearedSkills = 0;
322
+ const updatedAgentKeys: Array<string> = [];
323
+
324
+ for (const profile of profiles) {
325
+ const patch: {
326
+ providerUserId?: undefined;
327
+ soulMd?: undefined;
328
+ clientMd?: undefined;
329
+ skills?: undefined;
330
+ } = {};
331
+ let shouldPatch = false;
332
+
333
+ if (profile.providerUserId !== undefined) {
334
+ patch.providerUserId = undefined;
335
+ clearedProviderUserId += 1;
336
+ shouldPatch = true;
337
+ }
338
+ if (profile.soulMd !== undefined) {
339
+ patch.soulMd = undefined;
340
+ clearedSoulMd += 1;
341
+ shouldPatch = true;
342
+ }
343
+ if (profile.clientMd !== undefined) {
344
+ patch.clientMd = undefined;
345
+ clearedClientMd += 1;
346
+ shouldPatch = true;
347
+ }
348
+ if (profile.skills !== undefined) {
349
+ patch.skills = undefined;
350
+ clearedSkills += 1;
351
+ shouldPatch = true;
352
+ }
353
+
354
+ if (!shouldPatch) continue;
355
+
356
+ updated += 1;
357
+ updatedAgentKeys.push(profile.agentKey);
358
+ if (!dryRun) {
359
+ await ctx.db.patch(profile._id, patch);
360
+ }
361
+ }
362
+
363
+ return {
364
+ dryRun,
365
+ scanned: profiles.length,
366
+ updated,
367
+ unchanged: profiles.length - updated,
368
+ clearedProviderUserId,
369
+ clearedSoulMd,
370
+ clearedClientMd,
371
+ clearedSkills,
372
+ updatedAgentKeys,
373
+ };
374
+ },
375
+ });
376
+
281
377
  export const importPlaintextSecret = mutation({
282
378
  args: {
283
379
  secretRef: v.string(),
@@ -378,9 +474,9 @@ export const getProviderRuntimeConfig = internalQuery({
378
474
  handler: async (ctx) => {
379
475
  const row = await ctx.db
380
476
  .query("runtimeConfig")
381
- .withIndex("by_key", (q) => q.eq("key", "provider"))
477
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.provider))
382
478
  .unique();
383
- if (!row) {
479
+ if (!row?.providerConfig) {
384
480
  return null;
385
481
  }
386
482
  return row.providerConfig;
@@ -397,11 +493,11 @@ export const upsertProviderRuntimeConfig = internalMutation({
397
493
  const nowMs = args.nowMs ?? Date.now();
398
494
  const existing = await ctx.db
399
495
  .query("runtimeConfig")
400
- .withIndex("by_key", (q) => q.eq("key", "provider"))
496
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.provider))
401
497
  .unique();
402
498
  if (!existing) {
403
499
  await ctx.db.insert("runtimeConfig", {
404
- key: "provider",
500
+ key: RUNTIME_CONFIG_KEYS.provider,
405
501
  providerConfig: args.providerConfig,
406
502
  updatedAt: nowMs,
407
503
  });
@@ -421,9 +517,9 @@ export const providerRuntimeConfig = query({
421
517
  handler: async (ctx) => {
422
518
  const row = await ctx.db
423
519
  .query("runtimeConfig")
424
- .withIndex("by_key", (q) => q.eq("key", "provider"))
520
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.provider))
425
521
  .unique();
426
- if (!row) {
522
+ if (!row?.providerConfig) {
427
523
  return null;
428
524
  }
429
525
  return row.providerConfig;
@@ -440,11 +536,11 @@ export const setProviderRuntimeConfig = mutation({
440
536
  const nowMs = args.nowMs ?? Date.now();
441
537
  const existing = await ctx.db
442
538
  .query("runtimeConfig")
443
- .withIndex("by_key", (q) => q.eq("key", "provider"))
539
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.provider))
444
540
  .unique();
445
541
  if (!existing) {
446
542
  await ctx.db.insert("runtimeConfig", {
447
- key: "provider",
543
+ key: RUNTIME_CONFIG_KEYS.provider,
448
544
  providerConfig: args.providerConfig,
449
545
  updatedAt: nowMs,
450
546
  });
@@ -458,6 +554,106 @@ export const setProviderRuntimeConfig = mutation({
458
554
  },
459
555
  });
460
556
 
557
+ export const getMessageRuntimeConfig = internalQuery({
558
+ args: {},
559
+ returns: v.union(v.null(), messageRuntimeConfigValidator),
560
+ handler: async (ctx) => {
561
+ const row = await ctx.db
562
+ .query("runtimeConfig")
563
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.message))
564
+ .unique();
565
+ if (!row?.messageConfig) {
566
+ return null;
567
+ }
568
+ return row.messageConfig;
569
+ },
570
+ });
571
+
572
+ export const upsertMessageRuntimeConfig = internalMutation({
573
+ args: {
574
+ messageConfig: messageRuntimeConfigValidator,
575
+ nowMs: v.optional(v.number()),
576
+ },
577
+ returns: v.null(),
578
+ handler: async (ctx, args) => {
579
+ const nowMs = args.nowMs ?? Date.now();
580
+ const normalizedMessageConfig = normalizeMessageRuntimeConfig(args.messageConfig);
581
+ const existing = await ctx.db
582
+ .query("runtimeConfig")
583
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.message))
584
+ .unique();
585
+ if (normalizedMessageConfig === null) {
586
+ if (existing) {
587
+ await ctx.db.delete(existing._id);
588
+ }
589
+ return null;
590
+ }
591
+ if (!existing) {
592
+ await ctx.db.insert("runtimeConfig", {
593
+ key: RUNTIME_CONFIG_KEYS.message,
594
+ messageConfig: normalizedMessageConfig,
595
+ updatedAt: nowMs,
596
+ });
597
+ return null;
598
+ }
599
+ await ctx.db.patch(existing._id, {
600
+ messageConfig: normalizedMessageConfig,
601
+ updatedAt: nowMs,
602
+ });
603
+ return null;
604
+ },
605
+ });
606
+
607
+ export const messageRuntimeConfig = query({
608
+ args: {},
609
+ returns: v.union(v.null(), messageRuntimeConfigValidator),
610
+ handler: async (ctx) => {
611
+ const row = await ctx.db
612
+ .query("runtimeConfig")
613
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.message))
614
+ .unique();
615
+ if (!row?.messageConfig) {
616
+ return null;
617
+ }
618
+ return row.messageConfig;
619
+ },
620
+ });
621
+
622
+ export const setMessageRuntimeConfig = mutation({
623
+ args: {
624
+ messageConfig: messageRuntimeConfigValidator,
625
+ nowMs: v.optional(v.number()),
626
+ },
627
+ returns: v.null(),
628
+ handler: async (ctx, args) => {
629
+ const nowMs = args.nowMs ?? Date.now();
630
+ const normalizedMessageConfig = normalizeMessageRuntimeConfig(args.messageConfig);
631
+ const existing = await ctx.db
632
+ .query("runtimeConfig")
633
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.message))
634
+ .unique();
635
+ if (normalizedMessageConfig === null) {
636
+ if (existing) {
637
+ await ctx.db.delete(existing._id);
638
+ }
639
+ return null;
640
+ }
641
+ if (!existing) {
642
+ await ctx.db.insert("runtimeConfig", {
643
+ key: RUNTIME_CONFIG_KEYS.message,
644
+ messageConfig: normalizedMessageConfig,
645
+ updatedAt: nowMs,
646
+ });
647
+ return null;
648
+ }
649
+ await ctx.db.patch(existing._id, {
650
+ messageConfig: normalizedMessageConfig,
651
+ updatedAt: nowMs,
652
+ });
653
+ return null;
654
+ },
655
+ });
656
+
461
657
  export const deployGlobalSkill = mutation({
462
658
  args: {
463
659
  slug: v.string(),
@@ -2041,6 +2237,36 @@ async function resolveBridgeRuntimeConfig(
2041
2237
  };
2042
2238
  }
2043
2239
 
2240
+ function appendSystemPromptToMessage(messageText: string, systemPrompt?: string): string {
2241
+ const normalizedSystemPrompt = normalizeSystemPrompt(systemPrompt);
2242
+ if (normalizedSystemPrompt === null) {
2243
+ return messageText;
2244
+ }
2245
+ const normalizedMessageText = messageText.trimEnd();
2246
+ if (normalizedMessageText.length === 0) {
2247
+ return normalizedSystemPrompt;
2248
+ }
2249
+ return `${normalizedMessageText}\n\n${normalizedSystemPrompt}`;
2250
+ }
2251
+
2252
+ function normalizeMessageRuntimeConfig(
2253
+ messageConfig: { systemPrompt?: string } | null | undefined,
2254
+ ): { systemPrompt?: string } | null {
2255
+ const systemPrompt = normalizeSystemPrompt(messageConfig?.systemPrompt);
2256
+ if (systemPrompt === null) {
2257
+ return null;
2258
+ }
2259
+ return { systemPrompt };
2260
+ }
2261
+
2262
+ function normalizeSystemPrompt(systemPrompt?: string | null): string | null {
2263
+ if (typeof systemPrompt !== "string") {
2264
+ return null;
2265
+ }
2266
+ const normalizedSystemPrompt = systemPrompt.trim();
2267
+ return normalizedSystemPrompt.length > 0 ? normalizedSystemPrompt : null;
2268
+ }
2269
+
2044
2270
  function getBridgeSecretRefsForProfile(
2045
2271
  agentKey: string,
2046
2272
  bridgeConfig:
@@ -6,9 +6,9 @@ export default defineSchema({
6
6
  agentKey: v.string(),
7
7
  providerUserId: v.optional(v.string()),
8
8
  version: v.string(),
9
- soulMd: v.string(),
9
+ soulMd: v.optional(v.string()),
10
10
  clientMd: v.optional(v.string()),
11
- skills: v.array(v.string()),
11
+ skills: v.optional(v.array(v.string())),
12
12
  secretsRef: v.array(v.string()),
13
13
  bridgeConfig: v.optional(
14
14
  v.object({
@@ -134,16 +134,23 @@ export default defineSchema({
134
134
 
135
135
  runtimeConfig: defineTable({
136
136
  key: v.string(),
137
- providerConfig: v.object({
138
- kind: v.union(v.literal("fly"), v.literal("runpod"), v.literal("ecs")),
139
- appName: v.string(),
140
- organizationSlug: v.string(),
141
- image: v.string(),
142
- region: v.string(),
143
- volumeName: v.string(),
144
- volumePath: v.string(),
145
- volumeSizeGb: v.number(),
146
- }),
137
+ providerConfig: v.optional(
138
+ v.object({
139
+ kind: v.union(v.literal("fly"), v.literal("runpod"), v.literal("ecs")),
140
+ appName: v.string(),
141
+ organizationSlug: v.string(),
142
+ image: v.string(),
143
+ region: v.string(),
144
+ volumeName: v.string(),
145
+ volumePath: v.string(),
146
+ volumeSizeGb: v.number(),
147
+ }),
148
+ ),
149
+ messageConfig: v.optional(
150
+ v.object({
151
+ systemPrompt: v.optional(v.string()),
152
+ }),
153
+ ),
147
154
  updatedAt: v.number(),
148
155
  }).index("by_key", ["key"]),
149
156