Skip to content

Commit 2f075de

Browse files
authored
feat(tags): officially surface tags_match=exact across UI, docs, clients (#2230)
The `exact` set-equality match mode landed in the API + generated clients in #2149 but was never exposed in the control plane, documented, or added to the hand-maintained SDK wrappers. This completes the feature. Control plane: add `exact` to the TagsMatch type/unions and to the tags_match dropdowns in think-view, search-debug-view, and both mental-model trigger forms; add translated labels to all 10 locales. Docs: document `exact` in the recall tags_match table + tag_groups, the reflect tags value list, and the observations scope-listing guide; regenerate the docs skill mirror. Clients: add `exact` to the hand-maintained Python and TypeScript wrapper Literals/unions and docstrings (generated clients already had it; Rust is generated from openapi.json at build time). Supersedes #2159.
1 parent 08cfa5d commit 2f075de

22 files changed

Lines changed: 77 additions & 21 deletions

File tree

hindsight-clients/python/hindsight_client/hindsight_client.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ def recall(
383383
include_source_facts: bool = False,
384384
max_source_facts_tokens: int = 4096,
385385
tags: list[str] | None = None,
386-
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
386+
tags_match: Literal["any", "all", "any_strict", "all_strict", "exact"] = "any",
387387
tag_groups: list[dict[str, Any]] | None = None,
388388
) -> RecallResponse:
389389
"""
@@ -406,7 +406,8 @@ def recall(
406406
max_source_facts_tokens: Maximum tokens for source facts (default: 4096)
407407
tags: Optional list of tags to filter memories by
408408
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged),
409-
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged). Default: "any"
409+
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged),
410+
"exact" (set equality, excludes untagged). Default: "any"
410411
tag_groups: Optional list of tag group filters for advanced boolean tag matching.
411412
412413
Returns:
@@ -442,7 +443,7 @@ def reflect(
442443
max_tokens: int | None = None,
443444
response_schema: dict[str, Any] | None = None,
444445
tags: list[str] | None = None,
445-
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
446+
tags_match: Literal["any", "all", "any_strict", "all_strict", "exact"] = "any",
446447
include_facts: bool = False,
447448
include_tool_calls: bool = False,
448449
include_tool_call_output: bool = True,
@@ -465,7 +466,8 @@ def reflect(
465466
response parsed according to this schema.
466467
tags: Optional list of tags to filter memories by
467468
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged),
468-
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged). Default: "any"
469+
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged),
470+
"exact" (set equality, excludes untagged). Default: "any"
469471
include_facts: If True, the response will include a 'based_on' field listing
470472
the memories, mental models, and directives used to construct the answer.
471473
include_tool_calls: If True, the response will include a 'trace' field with the
@@ -855,7 +857,7 @@ async def arecall(
855857
include_source_facts: bool = False,
856858
max_source_facts_tokens: int = 4096,
857859
tags: list[str] | None = None,
858-
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
860+
tags_match: Literal["any", "all", "any_strict", "all_strict", "exact"] = "any",
859861
tag_groups: list[dict[str, Any]] | None = None,
860862
) -> RecallResponse:
861863
"""
@@ -878,7 +880,8 @@ async def arecall(
878880
max_source_facts_tokens: Maximum tokens for source facts (default: 4096)
879881
tags: Optional list of tags to filter memories by
880882
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged),
881-
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged). Default: "any"
883+
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged),
884+
"exact" (set equality, excludes untagged). Default: "any"
882885
tag_groups: Optional list of tag group filters for advanced boolean tag matching.
883886
Each element is a dict representing a tag group node (TagGroupLeaf, TagGroupAnd,
884887
TagGroupOr, or TagGroupNot). Example::
@@ -935,7 +938,7 @@ async def areflect(
935938
max_tokens: int | None = None,
936939
response_schema: dict[str, Any] | None = None,
937940
tags: list[str] | None = None,
938-
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
941+
tags_match: Literal["any", "all", "any_strict", "all_strict", "exact"] = "any",
939942
include_facts: bool = False,
940943
include_tool_calls: bool = False,
941944
include_tool_call_output: bool = True,
@@ -958,7 +961,8 @@ async def areflect(
958961
response parsed according to this schema.
959962
tags: Optional list of tags to filter memories by
960963
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged),
961-
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged). Default: "any"
964+
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged),
965+
"exact" (set equality, excludes untagged). Default: "any"
962966
include_facts: If True, the response will include a 'based_on' field listing
963967
the memories, mental models, and directives used to construct the answer.
964968
include_tool_calls: If True, the response will include a 'trace' field with the

hindsight-clients/typescript/src/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,8 @@ export class HindsightClient {
318318
maxSourceFactsTokens?: number;
319319
/** Optional list of tags to filter memories by */
320320
tags?: string[];
321-
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any' */
322-
tagsMatch?: "any" | "all" | "any_strict" | "all_strict";
321+
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged), 'exact' (set equality, excludes untagged). Default: 'any' */
322+
tagsMatch?: "any" | "all" | "any_strict" | "all_strict" | "exact";
323323
/** Compound tag filter using boolean groups. Groups are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}. Mutually exclusive with tags/tagsMatch. */
324324
tagGroups?: Array<TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput>;
325325
signal?: AbortSignal;
@@ -370,8 +370,8 @@ export class HindsightClient {
370370
budget?: Budget;
371371
/** Optional list of tags to filter memories by */
372372
tags?: string[];
373-
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any' */
374-
tagsMatch?: "any" | "all" | "any_strict" | "all_strict";
373+
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged), 'exact' (set equality, excludes untagged). Default: 'any' */
374+
tagsMatch?: "any" | "all" | "any_strict" | "all_strict" | "exact";
375375
/** Compound tag filter using boolean groups. Groups are AND-ed. Mutually exclusive with tags/tagsMatch. */
376376
tagGroups?: Array<TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput>;
377377
/** Optional JSON Schema for structured output. When provided, the response includes a 'structured_output' field. */

hindsight-control-plane/src/components/mental-models-view.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,7 @@ function CreateMentalModelDialog({
967967
<SelectItem value="all">{t("optionsTagsMatchAll")}</SelectItem>
968968
<SelectItem value="any_strict">{t("optionsTagsMatchAnyStrict")}</SelectItem>
969969
<SelectItem value="all_strict">{t("optionsTagsMatchAllStrict")}</SelectItem>
970+
<SelectItem value="exact">{t("optionsTagsMatchExact")}</SelectItem>
970971
</SelectContent>
971972
</Select>
972973
<p className="text-xs text-muted-foreground">
@@ -1366,6 +1367,7 @@ function UpdateMentalModelDialog({
13661367
<SelectItem value="all">{t("optionsTagsMatchAll")}</SelectItem>
13671368
<SelectItem value="any_strict">{t("optionsTagsMatchAnyStrict")}</SelectItem>
13681369
<SelectItem value="all_strict">{t("optionsTagsMatchAllStrict")}</SelectItem>
1370+
<SelectItem value="exact">{t("optionsTagsMatchExact")}</SelectItem>
13691371
</SelectContent>
13701372
</Select>
13711373
<p className="text-xs text-muted-foreground">

hindsight-control-plane/src/components/search-debug-view.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import "react18-json-view/src/style.css";
3636
import { MemoryDetailPanel } from "./memory-detail-panel";
3737

3838
type Budget = "low" | "mid" | "high";
39-
type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
39+
type TagsMatch = "any" | "all" | "any_strict" | "all_strict" | "exact";
4040
type ViewMode = "results" | "trace" | "json";
4141

4242
export function SearchDebugView() {
@@ -277,6 +277,7 @@ export function SearchDebugView() {
277277
<SelectItem value="all">{t("tagsMatchAll")}</SelectItem>
278278
<SelectItem value="any_strict">{t("tagsMatchAnyStrict")}</SelectItem>
279279
<SelectItem value="all_strict">{t("tagsMatchAllStrict")}</SelectItem>
280+
<SelectItem value="exact">{t("tagsMatchExact")}</SelectItem>
280281
</SelectContent>
281282
</Select>
282283
</div>

hindsight-control-plane/src/components/think-view.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { MentalModelDetailModal } from "./mental-model-detail-modal";
3737
import ReactMarkdown from "react-markdown";
3838
import remarkGfm from "remark-gfm";
3939

40-
type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
40+
type TagsMatch = "any" | "all" | "any_strict" | "all_strict" | "exact";
4141
type ViewMode = "answer" | "trace" | "json";
4242
type BasedOnTab = "directives" | "mental_models" | "observations" | "world" | "experience";
4343

@@ -286,6 +286,7 @@ export function ThinkView() {
286286
<SelectItem value="all">{t("tagsMatchAll")}</SelectItem>
287287
<SelectItem value="any_strict">{t("tagsMatchAnyStrict")}</SelectItem>
288288
<SelectItem value="all_strict">{t("tagsMatchAllStrict")}</SelectItem>
289+
<SelectItem value="exact">{t("tagsMatchExact")}</SelectItem>
289290
</SelectContent>
290291
</Select>
291292
</div>

hindsight-control-plane/src/lib/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export interface OperationProgress {
168168
detail?: Record<string, number> | null;
169169
}
170170

171-
export type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
171+
export type TagsMatch = "any" | "all" | "any_strict" | "all_strict" | "exact";
172172

173173
export type TagGroup =
174174
| { tags: string[]; match?: TagsMatch }
@@ -354,7 +354,7 @@ export class ControlPlaneClient {
354354
};
355355
query_timestamp?: string;
356356
tags?: string[];
357-
tags_match?: "any" | "all" | "any_strict" | "all_strict";
357+
tags_match?: "any" | "all" | "any_strict" | "all_strict" | "exact";
358358
}) {
359359
return this.fetchApi("/api/recall", {
360360
method: "POST",
@@ -373,7 +373,7 @@ export class ControlPlaneClient {
373373
include_facts?: boolean;
374374
include_tool_calls?: boolean;
375375
tags?: string[];
376-
tags_match?: "any" | "all" | "any_strict" | "all_strict";
376+
tags_match?: "any" | "all" | "any_strict" | "all_strict" | "exact";
377377
fact_types?: Array<"world" | "experience" | "observation">;
378378
exclude_mental_models?: boolean;
379379
exclude_mental_model_ids?: string[];

hindsight-control-plane/src/messages/de.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@
794794
"optionsTagsMatchAll": "all — UND-Abgleich, schließt Einträge ohne Tags ein",
795795
"optionsTagsMatchAnyStrict": "any_strict — ODER-Abgleich, schließt Einträge ohne Tags aus",
796796
"optionsTagsMatchAllStrict": "all_strict — UND-Abgleich, schließt Einträge ohne Tags aus",
797+
"optionsTagsMatchExact": "exact — Mengengleichheit, schließt Einträge ohne Tags aus",
797798
"optionsTagsMatchDescription": "Steuert, wie die Tags des Modells Erinnerungen während der Aktualisierung filtern.",
798799
"optionsTagGroupsLabel": "Tag-Gruppen (JSON)",
799800
"optionsTagGroupsDescription": "Zusammengesetzte boolesche Tag-Ausdrücke zur Filterung bei der Aktualisierung. Überschreibt flache Tags, wenn gesetzt.",
@@ -843,6 +844,7 @@
843844
"tagsMatchAll": "Alle (inkl. ohne Tags)",
844845
"tagsMatchAnyStrict": "Beliebig (strikt)",
845846
"tagsMatchAllStrict": "Alle (strikt)",
847+
"tagsMatchExact": "Exakt (Mengengleich)",
846848
"excludeMentalModels": "Mentale Modelle ausschließen",
847849
"excludeIdsLabel": "IDs ausschließen:",
848850
"excludeIdsPlaceholder": "modell-a, modell-b",
@@ -919,6 +921,7 @@
919921
"tagsMatchAll": "Alle (inkl. ohne Tags)",
920922
"tagsMatchAnyStrict": "Beliebig (strikt)",
921923
"tagsMatchAllStrict": "Alle (strikt)",
924+
"tagsMatchExact": "Exakt (Mengengleich)",
922925
"budgetLow": "Niedrig",
923926
"budgetMid": "Mittel",
924927
"budgetHigh": "Hoch",

hindsight-control-plane/src/messages/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@
794794
"optionsTagsMatchAll": "all — AND matching, includes untagged",
795795
"optionsTagsMatchAnyStrict": "any_strict — OR matching, excludes untagged",
796796
"optionsTagsMatchAllStrict": "all_strict — AND matching, excludes untagged",
797+
"optionsTagsMatchExact": "exact — set equality, excludes untagged",
797798
"optionsTagsMatchDescription": "Controls how the model's tags filter memories during refresh.",
798799
"optionsTagGroupsLabel": "Tag Groups (JSON)",
799800
"optionsTagGroupsDescription": "Compound boolean tag expressions for refresh filtering. Overrides flat tags when set.",
@@ -843,6 +844,7 @@
843844
"tagsMatchAll": "All (incl. untagged)",
844845
"tagsMatchAnyStrict": "Any (strict)",
845846
"tagsMatchAllStrict": "All (strict)",
847+
"tagsMatchExact": "Exact (set match)",
846848
"excludeMentalModels": "Exclude mental models",
847849
"excludeIdsLabel": "Exclude IDs:",
848850
"excludeIdsPlaceholder": "model-a, model-b",
@@ -919,6 +921,7 @@
919921
"tagsMatchAll": "All (incl. untagged)",
920922
"tagsMatchAnyStrict": "Any (strict)",
921923
"tagsMatchAllStrict": "All (strict)",
924+
"tagsMatchExact": "Exact (set match)",
922925
"budgetLow": "Low",
923926
"budgetMid": "Mid",
924927
"budgetHigh": "High",

hindsight-control-plane/src/messages/es.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@
794794
"optionsTagsMatchAll": "all — coincidencia AND, incluye sin etiqueta",
795795
"optionsTagsMatchAnyStrict": "any_strict — coincidencia OR, excluye sin etiqueta",
796796
"optionsTagsMatchAllStrict": "all_strict — coincidencia AND, excluye sin etiqueta",
797+
"optionsTagsMatchExact": "exact — igualdad de conjuntos, excluye sin etiqueta",
797798
"optionsTagsMatchDescription": "Controla cómo las etiquetas del modelo filtran las memorias durante la actualización.",
798799
"optionsTagGroupsLabel": "Grupos de etiquetas (JSON)",
799800
"optionsTagGroupsDescription": "Expresiones booleanas compuestas de etiquetas para filtrar durante la actualización. Anula las etiquetas planas cuando está configurado.",
@@ -843,6 +844,7 @@
843844
"tagsMatchAll": "Todas (incl. sin etiqueta)",
844845
"tagsMatchAnyStrict": "Cualquiera (estricto)",
845846
"tagsMatchAllStrict": "Todas (estricto)",
847+
"tagsMatchExact": "Exacto (conjunto)",
846848
"excludeMentalModels": "Excluir modelos mentales",
847849
"excludeIdsLabel": "Excluir IDs:",
848850
"excludeIdsPlaceholder": "modelo-a, modelo-b",
@@ -919,6 +921,7 @@
919921
"tagsMatchAll": "Todas (incl. sin etiqueta)",
920922
"tagsMatchAnyStrict": "Cualquiera (estricto)",
921923
"tagsMatchAllStrict": "Todas (estricto)",
924+
"tagsMatchExact": "Exacto (conjunto)",
922925
"budgetLow": "Bajo",
923926
"budgetMid": "Medio",
924927
"budgetHigh": "Alto",

hindsight-control-plane/src/messages/fr.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@
794794
"optionsTagsMatchAll": "all — correspondance ET, inclut non taggés",
795795
"optionsTagsMatchAnyStrict": "any_strict — correspondance OU, exclut non taggés",
796796
"optionsTagsMatchAllStrict": "all_strict — correspondance ET, exclut non taggés",
797+
"optionsTagsMatchExact": "exact — égalité d'ensembles, exclut non taggés",
797798
"optionsTagsMatchDescription": "Contrôle la façon dont les tags du modèle filtrent les souvenirs lors de l'actualisation.",
798799
"optionsTagGroupsLabel": "Groupes de tags (JSON)",
799800
"optionsTagGroupsDescription": "Expressions booléennes composées de tags pour le filtrage lors de l'actualisation. Remplace les tags plats lorsque défini.",
@@ -843,6 +844,7 @@
843844
"tagsMatchAll": "Tous (incl. non taggés)",
844845
"tagsMatchAnyStrict": "N'importe lequel (strict)",
845846
"tagsMatchAllStrict": "Tous (strict)",
847+
"tagsMatchExact": "Exact (ensemble)",
846848
"excludeMentalModels": "Exclure les modèles mentaux",
847849
"excludeIdsLabel": "Exclure les IDs :",
848850
"excludeIdsPlaceholder": "modèle-a, modèle-b",
@@ -919,6 +921,7 @@
919921
"tagsMatchAll": "Tous (incl. non taggés)",
920922
"tagsMatchAnyStrict": "N'importe lequel (strict)",
921923
"tagsMatchAllStrict": "Tous (strict)",
924+
"tagsMatchExact": "Exact (ensemble)",
922925
"budgetLow": "Faible",
923926
"budgetMid": "Moyen",
924927
"budgetHigh": "Élevé",

0 commit comments

Comments
 (0)