@okrlinkhub/agent-factory 0.2.12 → 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.
@@ -65,6 +65,23 @@ 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
+
72
+ const globalSkillStatusValidator = v.union(v.literal("active"), v.literal("disabled"));
73
+ const globalSkillReleaseChannelValidator = v.union(v.literal("stable"), v.literal("canary"));
74
+ const globalSkillModuleFormatValidator = v.union(v.literal("esm"), v.literal("cjs"));
75
+
76
+ const globalSkillManifestItemValidator = v.object({
77
+ slug: v.string(),
78
+ version: v.string(),
79
+ moduleFormat: globalSkillModuleFormatValidator,
80
+ entryPoint: v.string(),
81
+ sourceJs: v.string(),
82
+ sha256: v.string(),
83
+ });
84
+
68
85
  const BRIDGE_SECRET_REFS = {
69
86
  serviceKey: "agent-bridge.serviceKey",
70
87
  baseUrl: "agent-bridge.baseUrl",
@@ -73,6 +90,11 @@ const BRIDGE_SECRET_REFS = {
73
90
  appKey: "agent-bridge.appKey",
74
91
  } as const;
75
92
 
93
+ const RUNTIME_CONFIG_KEYS = {
94
+ provider: "provider",
95
+ message: "message",
96
+ } as const;
97
+
76
98
  export const enqueueMessage = mutation({
77
99
  args: {
78
100
  conversationId: v.string(),
@@ -87,6 +109,10 @@ export const enqueueMessage = mutation({
87
109
  returns: v.id("messageQueue"),
88
110
  handler: async (ctx, args) => {
89
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();
90
116
  const profile = await ctx.db
91
117
  .query("agentProfiles")
92
118
  .withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
@@ -113,6 +139,10 @@ export const enqueueMessage = mutation({
113
139
 
114
140
  const payload = {
115
141
  ...args.payload,
142
+ messageText: appendSystemPromptToMessage(
143
+ args.payload.messageText,
144
+ messageRuntimeConfigRow?.messageConfig?.systemPrompt,
145
+ ),
116
146
  providerUserId: providerUserIdStr,
117
147
  metadata: {
118
148
  ...(args.payload.metadata ?? {}),
@@ -239,9 +269,9 @@ export const upsertAgentProfile = mutation({
239
269
  agentKey: v.string(),
240
270
  providerUserId: v.optional(v.string()),
241
271
  version: v.string(),
242
- soulMd: v.string(),
272
+ soulMd: v.optional(v.string()),
243
273
  clientMd: v.optional(v.string()),
244
- skills: v.array(v.string()),
274
+ skills: v.optional(v.array(v.string())),
245
275
  secretsRef: v.array(v.string()),
246
276
  bridgeConfig: v.optional(bridgeProfileConfigValidator),
247
277
  enabled: v.boolean(),
@@ -265,6 +295,85 @@ export const upsertAgentProfile = mutation({
265
295
  },
266
296
  });
267
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
+
268
377
  export const importPlaintextSecret = mutation({
269
378
  args: {
270
379
  secretRef: v.string(),
@@ -365,9 +474,9 @@ export const getProviderRuntimeConfig = internalQuery({
365
474
  handler: async (ctx) => {
366
475
  const row = await ctx.db
367
476
  .query("runtimeConfig")
368
- .withIndex("by_key", (q) => q.eq("key", "provider"))
477
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.provider))
369
478
  .unique();
370
- if (!row) {
479
+ if (!row?.providerConfig) {
371
480
  return null;
372
481
  }
373
482
  return row.providerConfig;
@@ -384,11 +493,11 @@ export const upsertProviderRuntimeConfig = internalMutation({
384
493
  const nowMs = args.nowMs ?? Date.now();
385
494
  const existing = await ctx.db
386
495
  .query("runtimeConfig")
387
- .withIndex("by_key", (q) => q.eq("key", "provider"))
496
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.provider))
388
497
  .unique();
389
498
  if (!existing) {
390
499
  await ctx.db.insert("runtimeConfig", {
391
- key: "provider",
500
+ key: RUNTIME_CONFIG_KEYS.provider,
392
501
  providerConfig: args.providerConfig,
393
502
  updatedAt: nowMs,
394
503
  });
@@ -408,9 +517,9 @@ export const providerRuntimeConfig = query({
408
517
  handler: async (ctx) => {
409
518
  const row = await ctx.db
410
519
  .query("runtimeConfig")
411
- .withIndex("by_key", (q) => q.eq("key", "provider"))
520
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.provider))
412
521
  .unique();
413
- if (!row) {
522
+ if (!row?.providerConfig) {
414
523
  return null;
415
524
  }
416
525
  return row.providerConfig;
@@ -427,11 +536,11 @@ export const setProviderRuntimeConfig = mutation({
427
536
  const nowMs = args.nowMs ?? Date.now();
428
537
  const existing = await ctx.db
429
538
  .query("runtimeConfig")
430
- .withIndex("by_key", (q) => q.eq("key", "provider"))
539
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.provider))
431
540
  .unique();
432
541
  if (!existing) {
433
542
  await ctx.db.insert("runtimeConfig", {
434
- key: "provider",
543
+ key: RUNTIME_CONFIG_KEYS.provider,
435
544
  providerConfig: args.providerConfig,
436
545
  updatedAt: nowMs,
437
546
  });
@@ -445,6 +554,511 @@ export const setProviderRuntimeConfig = mutation({
445
554
  },
446
555
  });
447
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
+
657
+ export const deployGlobalSkill = mutation({
658
+ args: {
659
+ slug: v.string(),
660
+ displayName: v.optional(v.string()),
661
+ description: v.optional(v.string()),
662
+ version: v.string(),
663
+ sourceJs: v.string(),
664
+ entryPoint: v.optional(v.string()),
665
+ moduleFormat: v.optional(globalSkillModuleFormatValidator),
666
+ releaseChannel: v.optional(globalSkillReleaseChannelValidator),
667
+ actor: v.optional(v.string()),
668
+ nowMs: v.optional(v.number()),
669
+ },
670
+ returns: v.object({
671
+ skillId: v.id("globalSkills"),
672
+ versionId: v.id("globalSkillVersions"),
673
+ releaseId: v.id("globalSkillReleases"),
674
+ slug: v.string(),
675
+ version: v.string(),
676
+ sha256: v.string(),
677
+ releaseChannel: globalSkillReleaseChannelValidator,
678
+ }),
679
+ handler: async (ctx, args) => {
680
+ const nowMs = args.nowMs ?? Date.now();
681
+ const slug = args.slug.trim().toLowerCase();
682
+ const version = args.version.trim();
683
+ const entryPoint = (args.entryPoint ?? "default").trim();
684
+ const releaseChannel = args.releaseChannel ?? "stable";
685
+ const moduleFormat = args.moduleFormat ?? "esm";
686
+ const actor = args.actor?.trim() || "system";
687
+ const sourceJs = args.sourceJs.trim();
688
+
689
+ if (!/^[a-z0-9][a-z0-9-_]{1,127}$/.test(slug)) {
690
+ throw new Error("Invalid skill slug. Use lowercase letters, numbers, '-' and '_'.");
691
+ }
692
+ if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) {
693
+ throw new Error("Invalid skill version. Use semantic versioning format.");
694
+ }
695
+ if (sourceJs.length < 16) {
696
+ throw new Error("Skill source is too short.");
697
+ }
698
+ if (sourceJs.length > 200_000) {
699
+ throw new Error("Skill source too large (max 200KB).");
700
+ }
701
+ if (!entryPoint) {
702
+ throw new Error("entryPoint is required.");
703
+ }
704
+
705
+ const sha256 = await computeSha256Hex(sourceJs);
706
+
707
+ const existingSkill = await ctx.db
708
+ .query("globalSkills")
709
+ .withIndex("by_slug", (q) => q.eq("slug", slug))
710
+ .unique();
711
+ const skillId =
712
+ existingSkill?._id ??
713
+ (await ctx.db.insert("globalSkills", {
714
+ slug,
715
+ displayName: args.displayName?.trim() || slug,
716
+ description: args.description?.trim(),
717
+ status: "active",
718
+ createdBy: actor,
719
+ updatedBy: actor,
720
+ createdAt: nowMs,
721
+ updatedAt: nowMs,
722
+ }));
723
+
724
+ if (existingSkill) {
725
+ await ctx.db.patch(skillId, {
726
+ displayName: args.displayName?.trim() || existingSkill.displayName,
727
+ description:
728
+ args.description !== undefined ? args.description.trim() : existingSkill.description,
729
+ status: "active",
730
+ updatedBy: actor,
731
+ updatedAt: nowMs,
732
+ });
733
+ }
734
+
735
+ const existingVersion = await ctx.db
736
+ .query("globalSkillVersions")
737
+ .withIndex("by_skillId_and_version", (q) => q.eq("skillId", skillId).eq("version", version))
738
+ .unique();
739
+
740
+ let versionId = existingVersion?._id;
741
+ if (!existingVersion) {
742
+ versionId = await ctx.db.insert("globalSkillVersions", {
743
+ skillId,
744
+ version,
745
+ moduleFormat,
746
+ entryPoint,
747
+ sourceJs,
748
+ sha256,
749
+ createdBy: actor,
750
+ createdAt: nowMs,
751
+ });
752
+ } else if (existingVersion.sha256 !== sha256) {
753
+ throw new Error(`Skill ${slug}@${version} already exists with a different source.`);
754
+ }
755
+
756
+ const activeReleases = await ctx.db
757
+ .query("globalSkillReleases")
758
+ .withIndex("by_skillId_and_releaseChannel_and_isActive", (q) =>
759
+ q.eq("skillId", skillId).eq("releaseChannel", releaseChannel).eq("isActive", true),
760
+ )
761
+ .collect();
762
+ for (const release of activeReleases) {
763
+ await ctx.db.patch(release._id, { isActive: false });
764
+ }
765
+
766
+ const releaseId = await ctx.db.insert("globalSkillReleases", {
767
+ skillId,
768
+ versionId: versionId!,
769
+ releaseChannel,
770
+ isActive: true,
771
+ activatedBy: actor,
772
+ activatedAt: nowMs,
773
+ });
774
+
775
+ return {
776
+ skillId,
777
+ versionId: versionId!,
778
+ releaseId,
779
+ slug,
780
+ version,
781
+ sha256,
782
+ releaseChannel,
783
+ };
784
+ },
785
+ });
786
+
787
+ export const listGlobalSkills = query({
788
+ args: {
789
+ releaseChannel: v.optional(globalSkillReleaseChannelValidator),
790
+ status: v.optional(globalSkillStatusValidator),
791
+ limit: v.optional(v.number()),
792
+ },
793
+ returns: v.array(
794
+ v.object({
795
+ skillId: v.id("globalSkills"),
796
+ slug: v.string(),
797
+ displayName: v.string(),
798
+ description: v.optional(v.string()),
799
+ status: globalSkillStatusValidator,
800
+ updatedAt: v.number(),
801
+ activeRelease: v.union(
802
+ v.null(),
803
+ v.object({
804
+ releaseId: v.id("globalSkillReleases"),
805
+ versionId: v.id("globalSkillVersions"),
806
+ version: v.string(),
807
+ sha256: v.string(),
808
+ moduleFormat: globalSkillModuleFormatValidator,
809
+ entryPoint: v.string(),
810
+ releaseChannel: globalSkillReleaseChannelValidator,
811
+ activatedAt: v.number(),
812
+ }),
813
+ ),
814
+ }),
815
+ ),
816
+ handler: async (ctx, args) => {
817
+ const releaseChannel = args.releaseChannel ?? "stable";
818
+ const limit = Math.max(1, Math.min(args.limit ?? 200, 500));
819
+ const skills =
820
+ args.status !== undefined
821
+ ? await ctx.db
822
+ .query("globalSkills")
823
+ .withIndex("by_status", (q) => q.eq("status", args.status!))
824
+ .take(limit)
825
+ : await ctx.db.query("globalSkills").take(limit);
826
+
827
+ const sortedSkills = [...skills].sort((a, b) => a.slug.localeCompare(b.slug));
828
+ const out: Array<{
829
+ skillId: any;
830
+ slug: string;
831
+ displayName: string;
832
+ description?: string;
833
+ status: "active" | "disabled";
834
+ updatedAt: number;
835
+ activeRelease: {
836
+ releaseId: any;
837
+ versionId: any;
838
+ version: string;
839
+ sha256: string;
840
+ moduleFormat: "esm" | "cjs";
841
+ entryPoint: string;
842
+ releaseChannel: "stable" | "canary";
843
+ activatedAt: number;
844
+ } | null;
845
+ }> = [];
846
+
847
+ for (const skill of sortedSkills) {
848
+ const activeRelease = await ctx.db
849
+ .query("globalSkillReleases")
850
+ .withIndex("by_skillId_and_releaseChannel_and_isActive", (q) =>
851
+ q.eq("skillId", skill._id).eq("releaseChannel", releaseChannel).eq("isActive", true),
852
+ )
853
+ .first();
854
+
855
+ let activeReleaseRow: (typeof out)[number]["activeRelease"] = null;
856
+ if (activeRelease) {
857
+ const version = await ctx.db.get(activeRelease.versionId);
858
+ if (version) {
859
+ activeReleaseRow = {
860
+ releaseId: activeRelease._id,
861
+ versionId: version._id,
862
+ version: version.version,
863
+ sha256: version.sha256,
864
+ moduleFormat: version.moduleFormat,
865
+ entryPoint: version.entryPoint,
866
+ releaseChannel: activeRelease.releaseChannel,
867
+ activatedAt: activeRelease.activatedAt,
868
+ };
869
+ }
870
+ }
871
+
872
+ out.push({
873
+ skillId: skill._id,
874
+ slug: skill.slug,
875
+ displayName: skill.displayName,
876
+ description: skill.description,
877
+ status: skill.status,
878
+ updatedAt: skill.updatedAt,
879
+ activeRelease: activeReleaseRow,
880
+ });
881
+ }
882
+ return out;
883
+ },
884
+ });
885
+
886
+ export const getWorkerGlobalSkillsManifest = query({
887
+ args: {
888
+ workspaceId: v.optional(v.string()),
889
+ workerId: v.optional(v.string()),
890
+ releaseChannel: v.optional(globalSkillReleaseChannelValidator),
891
+ },
892
+ returns: v.object({
893
+ manifestVersion: v.string(),
894
+ generatedAt: v.number(),
895
+ releaseChannel: globalSkillReleaseChannelValidator,
896
+ workspaceId: v.string(),
897
+ skills: v.array(globalSkillManifestItemValidator),
898
+ }),
899
+ handler: async (ctx, args) => {
900
+ const nowMs = Date.now();
901
+ const releaseChannel = args.releaseChannel ?? "stable";
902
+ const activeSkills = await ctx.db
903
+ .query("globalSkills")
904
+ .withIndex("by_status", (q) => q.eq("status", "active"))
905
+ .collect();
906
+ const sortedSkills = [...activeSkills].sort((a, b) => a.slug.localeCompare(b.slug));
907
+
908
+ const manifestSkills: Array<{
909
+ slug: string;
910
+ version: string;
911
+ moduleFormat: "esm" | "cjs";
912
+ entryPoint: string;
913
+ sourceJs: string;
914
+ sha256: string;
915
+ }> = [];
916
+
917
+ for (const skill of sortedSkills) {
918
+ const activeRelease = await ctx.db
919
+ .query("globalSkillReleases")
920
+ .withIndex("by_skillId_and_releaseChannel_and_isActive", (q) =>
921
+ q.eq("skillId", skill._id).eq("releaseChannel", releaseChannel).eq("isActive", true),
922
+ )
923
+ .first();
924
+ if (!activeRelease) continue;
925
+
926
+ const version = await ctx.db.get(activeRelease.versionId);
927
+ if (!version) continue;
928
+ manifestSkills.push({
929
+ slug: skill.slug,
930
+ version: version.version,
931
+ moduleFormat: version.moduleFormat,
932
+ entryPoint: version.entryPoint,
933
+ sourceJs: version.sourceJs,
934
+ sha256: version.sha256,
935
+ });
936
+ }
937
+
938
+ manifestSkills.sort((a, b) => {
939
+ if (a.slug !== b.slug) return a.slug.localeCompare(b.slug);
940
+ return a.version.localeCompare(b.version);
941
+ });
942
+
943
+ const fingerprintSeed = manifestSkills
944
+ .map((row) => `${row.slug}@${row.version}:${row.sha256}`)
945
+ .join("|");
946
+ const manifestVersion = await computeSha256Hex(fingerprintSeed || "empty");
947
+
948
+ return {
949
+ manifestVersion,
950
+ generatedAt: nowMs,
951
+ releaseChannel,
952
+ workspaceId: args.workspaceId ?? "default",
953
+ skills: manifestSkills,
954
+ };
955
+ },
956
+ });
957
+
958
+ export const setGlobalSkillStatus = mutation({
959
+ args: {
960
+ slug: v.string(),
961
+ status: globalSkillStatusValidator,
962
+ actor: v.optional(v.string()),
963
+ nowMs: v.optional(v.number()),
964
+ },
965
+ returns: v.object({
966
+ updated: v.boolean(),
967
+ slug: v.string(),
968
+ status: globalSkillStatusValidator,
969
+ }),
970
+ handler: async (ctx, args) => {
971
+ const slug = args.slug.trim().toLowerCase();
972
+ const nowMs = args.nowMs ?? Date.now();
973
+ const actor = args.actor?.trim() || "system";
974
+ const skill = await ctx.db
975
+ .query("globalSkills")
976
+ .withIndex("by_slug", (q) => q.eq("slug", slug))
977
+ .unique();
978
+ if (!skill) {
979
+ return { updated: false, slug, status: args.status };
980
+ }
981
+ await ctx.db.patch(skill._id, {
982
+ status: args.status,
983
+ updatedBy: actor,
984
+ updatedAt: nowMs,
985
+ });
986
+ return { updated: true, slug, status: args.status };
987
+ },
988
+ });
989
+
990
+ export const deleteGlobalSkill = mutation({
991
+ args: {
992
+ slug: v.string(),
993
+ },
994
+ returns: v.object({
995
+ deleted: v.boolean(),
996
+ slug: v.string(),
997
+ deletedVersions: v.number(),
998
+ deletedReleases: v.number(),
999
+ }),
1000
+ handler: async (ctx, args) => {
1001
+ const slug = args.slug.trim().toLowerCase();
1002
+ const skill = await ctx.db
1003
+ .query("globalSkills")
1004
+ .withIndex("by_slug", (q) => q.eq("slug", slug))
1005
+ .unique();
1006
+ if (!skill) {
1007
+ return { deleted: false, slug, deletedVersions: 0, deletedReleases: 0 };
1008
+ }
1009
+
1010
+ const stableActiveReleases = await ctx.db
1011
+ .query("globalSkillReleases")
1012
+ .withIndex("by_skillId_and_releaseChannel_and_isActive", (q) =>
1013
+ q.eq("skillId", skill._id).eq("releaseChannel", "stable").eq("isActive", true),
1014
+ )
1015
+ .collect();
1016
+ const stableInactiveReleases = await ctx.db
1017
+ .query("globalSkillReleases")
1018
+ .withIndex("by_skillId_and_releaseChannel_and_isActive", (q) =>
1019
+ q.eq("skillId", skill._id).eq("releaseChannel", "stable").eq("isActive", false),
1020
+ )
1021
+ .collect();
1022
+ const canaryActiveReleases = await ctx.db
1023
+ .query("globalSkillReleases")
1024
+ .withIndex("by_skillId_and_releaseChannel_and_isActive", (q) =>
1025
+ q.eq("skillId", skill._id).eq("releaseChannel", "canary").eq("isActive", true),
1026
+ )
1027
+ .collect();
1028
+ const canaryInactiveReleases = await ctx.db
1029
+ .query("globalSkillReleases")
1030
+ .withIndex("by_skillId_and_releaseChannel_and_isActive", (q) =>
1031
+ q.eq("skillId", skill._id).eq("releaseChannel", "canary").eq("isActive", false),
1032
+ )
1033
+ .collect();
1034
+ const allReleases = [
1035
+ ...stableActiveReleases,
1036
+ ...stableInactiveReleases,
1037
+ ...canaryActiveReleases,
1038
+ ...canaryInactiveReleases,
1039
+ ];
1040
+ const versions = await ctx.db
1041
+ .query("globalSkillVersions")
1042
+ .withIndex("by_skillId_and_createdAt", (q) => q.eq("skillId", skill._id))
1043
+ .collect();
1044
+
1045
+ for (const release of allReleases) {
1046
+ await ctx.db.delete(release._id);
1047
+ }
1048
+ for (const version of versions) {
1049
+ await ctx.db.delete(version._id);
1050
+ }
1051
+ await ctx.db.delete(skill._id);
1052
+
1053
+ return {
1054
+ deleted: true,
1055
+ slug,
1056
+ deletedVersions: versions.length,
1057
+ deletedReleases: allReleases.length,
1058
+ };
1059
+ },
1060
+ });
1061
+
448
1062
  export const generateMediaUploadUrl = mutation({
449
1063
  args: {},
450
1064
  returns: v.object({
@@ -1623,6 +2237,36 @@ async function resolveBridgeRuntimeConfig(
1623
2237
  };
1624
2238
  }
1625
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
+
1626
2270
  function getBridgeSecretRefsForProfile(
1627
2271
  agentKey: string,
1628
2272
  bridgeConfig:
@@ -1688,6 +2332,14 @@ function fingerprintConversationDelta(
1688
2332
  return `f${(hash >>> 0).toString(16)}`;
1689
2333
  }
1690
2334
 
2335
+ async function computeSha256Hex(input: string): Promise<string> {
2336
+ const bytes = new TextEncoder().encode(input);
2337
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
2338
+ return Array.from(new Uint8Array(digest))
2339
+ .map((value) => value.toString(16).padStart(2, "0"))
2340
+ .join("");
2341
+ }
2342
+
1691
2343
  function encryptSecretValue(plaintext: string): string {
1692
2344
  const units = Array.from(plaintext);
1693
2345
  return units