@stackbone/cli 0.7.0 → 0.7.1

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.
@@ -0,0 +1,16 @@
1
+ -- STA3-33: Studio and the box address a person by this opaque per-agent id
2
+ -- instead of the phone or email in user_id, which leaked into URLs and logs.
3
+ -- It lives on the revision row because an erase bumps the revision and keeps
4
+ -- the row, so a person who is deleted and comes back keeps the same id.
5
+
6
+ ALTER TABLE stackbone_platform.memory_user_revisions
7
+ ADD COLUMN person_id uuid NOT NULL DEFAULT gen_random_uuid(),
8
+ ADD CONSTRAINT memory_user_revisions_person_id_key UNIQUE (person_id);
9
+
10
+ -- A person with facts or calls but no revision row yet has no id to open.
11
+ INSERT INTO stackbone_platform.memory_user_revisions (agent_id, user_id, revision)
12
+ SELECT agent_id, user_id, 0 FROM (
13
+ SELECT agent_id, user_id FROM stackbone_platform.memory_facts
14
+ UNION SELECT agent_id, user_id FROM stackbone_platform.memory_calls
15
+ ) AS people
16
+ ON CONFLICT (agent_id, user_id) DO NOTHING;
@@ -0,0 +1,31 @@
1
+ -- STA3-35: an erase keeps the revision row as a tombstone, so an add that
2
+ -- snapshotted before the erase still fails its revision check and cannot write
3
+ -- the erased memory back. The tombstone kept the phone or email in user_id; it
4
+ -- now keeps only the opaque person_id, so user_id is nullable and person_id is
5
+ -- the key. UNIQUE (agent_id, user_id) stays the lookup the SDK's
6
+ -- ON CONFLICT (agent_id, user_id) infers, and Postgres treats NULLs as
7
+ -- distinct, so an agent can hold many erased rows.
8
+
9
+ ALTER TABLE stackbone_platform.memory_user_revisions
10
+ DROP CONSTRAINT memory_user_revisions_pkey;
11
+ ALTER TABLE stackbone_platform.memory_user_revisions
12
+ ALTER COLUMN user_id DROP NOT NULL,
13
+ ADD CONSTRAINT memory_user_revisions_pkey PRIMARY KEY (person_id),
14
+ DROP CONSTRAINT memory_user_revisions_person_id_key,
15
+ ADD CONSTRAINT memory_user_revisions_agent_id_user_id_key UNIQUE (agent_id, user_id);
16
+
17
+ -- People erased before this change. Only an erase deletes memory_calls, and
18
+ -- every committed add writes a call with its revision bump, so a row with no
19
+ -- facts and no calls on its own agent is an erased tombstone. The rule holds
20
+ -- only here: once adds claim the row at snapshot, a failed add can leave such
21
+ -- a row for a live person.
22
+ UPDATE stackbone_platform.memory_user_revisions AS revisions
23
+ SET user_id = NULL
24
+ WHERE NOT EXISTS (
25
+ SELECT 1 FROM stackbone_platform.memory_facts AS facts
26
+ WHERE facts.agent_id = revisions.agent_id AND facts.user_id = revisions.user_id
27
+ )
28
+ AND NOT EXISTS (
29
+ SELECT 1 FROM stackbone_platform.memory_calls AS calls
30
+ WHERE calls.agent_id = revisions.agent_id AND calls.user_id = revisions.user_id
31
+ );
@@ -0,0 +1,14 @@
1
+ -- STA3-57: lexical + hybrid search, opt-in per collection.
2
+ ALTER TABLE stackbone_platform.rag_collections
3
+ ADD COLUMN IF NOT EXISTS hybrid_enabled boolean NOT NULL DEFAULT false,
4
+ ADD COLUMN IF NOT EXISTS number_format text NOT NULL DEFAULT 'eu'
5
+ CONSTRAINT rag_collections_number_format_check CHECK (number_format IN ('eu', 'us'));
6
+
7
+ -- Nullable: ingest writes the terms only while the collection has hybrid on
8
+ -- (STA3-57 D11). The terms are normalised in TypeScript, so no `unaccent` (D6).
9
+ ALTER TABLE stackbone_platform.rag_chunks
10
+ ADD COLUMN IF NOT EXISTS lexical tsvector;
11
+
12
+ -- Built under the migrator's advisory lock, like the 1536 index in 0103.
13
+ CREATE INDEX IF NOT EXISTS rag_chunks_lexical_gin
14
+ ON stackbone_platform.rag_chunks USING gin (lexical);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbone/cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {
Binary file
@@ -557,6 +557,7 @@ var STUDIO_RAG_COLLECTION_REGEX = /^[A-Za-z0-9][A-Za-z0-9._\-/:]{0,127}$/;
557
557
  var ragCollectionNameParamSchema = z7.string().min(1).max(128).regex(STUDIO_RAG_COLLECTION_REGEX, "collection must be 1..128 ASCII identifier-safe chars");
558
558
  var ragDistanceSchema = z7.enum(["cosine", "l2", "ip"]);
559
559
  var ragIndexKindSchema = z7.enum(["hnsw", "exact"]);
560
+ var ragNumberFormatSchema = z7.enum(["eu", "us"]);
560
561
  var ragCollectionSchema = z7.object({
561
562
  name: z7.string(),
562
563
  dimensions: z7.number().int().min(1),
@@ -570,7 +571,10 @@ var ragCollectionSchema = z7.object({
570
571
  // Maximum `created_at` across the collection's documents. Null when the
571
572
  // collection has zero documents (an empty collection row created before its
572
573
  // first ingest).
573
- last_ingested_at: z7.iso.datetime().nullable()
574
+ last_ingested_at: z7.iso.datetime().nullable(),
575
+ // STA3-57. Optional so a list from a box older than hybrid search still parses.
576
+ hybrid_enabled: z7.boolean().optional(),
577
+ number_format: ragNumberFormatSchema.optional()
574
578
  });
575
579
  var ragCollectionListResponseSchema = z7.object({
576
580
  items: z7.array(ragCollectionSchema)
@@ -617,6 +621,51 @@ var ragDocumentSchema = z7.object({
617
621
  pending_version: z7.number().int().min(1).nullable().optional()
618
622
  });
619
623
  var ragFilterSchema = jsonObject();
624
+ var ragDateBoundSchema = z7.string().length(10).regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/).refine((value) => {
625
+ const year = Number(value.slice(0, 4));
626
+ const month = Number(value.slice(5, 7));
627
+ const day = Number(value.slice(8, 10));
628
+ const date = /* @__PURE__ */ new Date(0);
629
+ date.setUTCFullYear(year, month - 1, day);
630
+ return year >= 1 && year <= 9999 && date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
631
+ }, "date must be a real YYYY-MM-DD day in years 0001\u20139999");
632
+ var ragRangeFieldSchema = z7.string().min(1).refine((field) => !field.includes("."), {
633
+ message: "field must be a literal top-level metadata key without dots"
634
+ });
635
+ var ragRangeSchema = z7.discriminatedUnion("type", [
636
+ z7.strictObject({
637
+ field: ragRangeFieldSchema,
638
+ type: z7.literal("number"),
639
+ gt: z7.number().finite().optional(),
640
+ gte: z7.number().finite().optional(),
641
+ lt: z7.number().finite().optional(),
642
+ lte: z7.number().finite().optional()
643
+ }),
644
+ z7.strictObject({
645
+ field: ragRangeFieldSchema,
646
+ type: z7.literal("date"),
647
+ gt: ragDateBoundSchema.optional(),
648
+ gte: ragDateBoundSchema.optional(),
649
+ lt: ragDateBoundSchema.optional(),
650
+ lte: ragDateBoundSchema.optional()
651
+ })
652
+ ]).superRefine((range, ctx) => {
653
+ const lower = range.gt ?? range.gte;
654
+ const upper = range.lt ?? range.lte;
655
+ if (lower === void 0 && upper === void 0) {
656
+ ctx.addIssue({ code: "custom", message: "range must have at least one bound" });
657
+ }
658
+ if (range.gt !== void 0 && range.gte !== void 0) {
659
+ ctx.addIssue({ code: "custom", message: "gt and gte cannot be combined", path: ["gte"] });
660
+ }
661
+ if (range.lt !== void 0 && range.lte !== void 0) {
662
+ ctx.addIssue({ code: "custom", message: "lt and lte cannot be combined", path: ["lte"] });
663
+ }
664
+ if (lower !== void 0 && upper !== void 0 && lower > upper) {
665
+ ctx.addIssue({ code: "custom", message: "lower bound must not exceed upper bound" });
666
+ }
667
+ });
668
+ var ragRangesSchema = z7.array(ragRangeSchema).max(10);
620
669
  var listRagDocsQuerySchema = forwardPageQuerySchema.extend({
621
670
  limit: limitParamSchema(STUDIO_RAG_DOCS_LIMIT_MAX).default(STUDIO_RAG_DOCS_LIMIT_DEFAULT),
622
671
  // Filter is sent JSON-encoded inside a single query param so the UI can
@@ -655,7 +704,9 @@ var ragQueryRequestSchema = z7.object({
655
704
  model: z7.string().min(1).optional(),
656
705
  topK: z7.coerce.number().int().min(1).max(STUDIO_RAG_TOPK_MAX).default(STUDIO_RAG_TOPK_DEFAULT),
657
706
  filter: ragFilterSchema.optional(),
658
- preview: z7.object({ docId: z7.string().min(1), version: ragDocumentVersionNumberSchema }).optional()
707
+ ranges: ragRangesSchema.optional(),
708
+ preview: z7.object({ docId: z7.string().min(1), version: ragDocumentVersionNumberSchema }).optional(),
709
+ hybrid: z7.boolean().optional()
659
710
  }).refine((value) => value.embedding !== void 0 || value.text !== void 0, {
660
711
  message: "either `embedding` or `text` must be provided",
661
712
  path: ["embedding"]
@@ -667,6 +718,9 @@ var ragQueryHitSchema = z7.object({
667
718
  // query)` over `stackbone_rag_chunks`, so higher is closer. We surface the
668
719
  // raw value; the UI formats it.
669
720
  score: z7.number(),
721
+ // The reciprocal rank fusion score a hybrid search ordered by (STA3-57 D10).
722
+ // Absent when only the search by meaning ran, and from a box older than it.
723
+ mixed_score: z7.number().optional(),
670
724
  content: z7.string(),
671
725
  metadata: jsonObject()
672
726
  });
@@ -710,7 +764,10 @@ var createRagCollectionRequestSchema = z7.object({
710
764
  name: ragCollectionNameParamSchema,
711
765
  metadata: jsonObject().optional(),
712
766
  embedding_provider: pickedModelProviderIdSchema.optional(),
713
- embedding_model: z7.string().trim().min(1).optional()
767
+ embedding_model: z7.string().trim().min(1).optional(),
768
+ // Omitted, the box creates the collection with hybrid search off and `eu`.
769
+ hybrid_enabled: z7.boolean().optional(),
770
+ number_format: ragNumberFormatSchema.optional()
714
771
  }).refine(
715
772
  (value) => value.embedding_provider === void 0 === (value.embedding_model === void 0),
716
773
  {
@@ -718,6 +775,17 @@ var createRagCollectionRequestSchema = z7.object({
718
775
  path: ["embedding_model"]
719
776
  }
720
777
  );
778
+ var updateRagCollectionRequestSchema = z7.strictObject({
779
+ hybrid_enabled: z7.boolean().optional(),
780
+ number_format: ragNumberFormatSchema.optional()
781
+ }).refine((value) => value.hybrid_enabled !== void 0 || value.number_format !== void 0, {
782
+ message: "Send hybrid_enabled, number_format or both"
783
+ });
784
+ var updateRagCollectionResponseSchema = z7.object({
785
+ name: z7.string(),
786
+ hybrid_enabled: z7.boolean(),
787
+ number_format: ragNumberFormatSchema
788
+ });
721
789
  var deleteRagCollectionResponseSchema = z7.object({
722
790
  name: z7.string(),
723
791
  deleted: z7.literal(true)
@@ -858,7 +926,14 @@ var effectiveVoiceSettingsObjectSchema = z8.strictObject({
858
926
  */
859
927
  fastModelRouting: z8.boolean(),
860
928
  /** The box runs the search before the model's turn, so the worker never reads it. */
861
- rag: ragSchema
929
+ rag: ragSchema,
930
+ /**
931
+ * What the agent says when an input guardrail stops the caller's sentence (FDE-66):
932
+ * a policy message written for a chat reader would leak the rule if a TTS read it.
933
+ * The box answers the turn itself, so the worker never reads it. Not nullable: a
934
+ * blocked turn always says something.
935
+ */
936
+ refusalPhrase: z8.string().trim().min(1).max(300)
862
937
  });
863
938
  var checkCrossFieldRules = (value, ctx) => {
864
939
  const min = value.turn?.minEndpointingDelayMs;
@@ -889,7 +964,8 @@ var effectiveVoiceSettingsSchema = effectiveVoiceSettingsObjectSchema.superRefin
889
964
  var callVoiceSettingsObjectSchema = effectiveVoiceSettingsObjectSchema.omit({
890
965
  recording: true,
891
966
  fastModelRouting: true,
892
- rag: true
967
+ rag: true,
968
+ refusalPhrase: true
893
969
  });
894
970
  var callVoiceSettingsSchema = callVoiceSettingsObjectSchema.superRefine(checkCrossFieldRules);
895
971
  var VOICE_SETTINGS_LEAF_PATHS = [
@@ -918,7 +994,8 @@ var VOICE_SETTINGS_LEAF_PATHS = [
918
994
  "rag.minScore",
919
995
  "rag.minWords",
920
996
  "rag.topK",
921
- "rag.timeoutMs"
997
+ "rag.timeoutMs",
998
+ "refusalPhrase"
922
999
  ];
923
1000
  var voiceSettingOriginSchema = z8.enum(["studio", "code", "default"]);
924
1001
  var effectiveVoiceSettingsViewSchema = z8.strictObject({