@qloo/qloo-harness 0.1.18 → 0.1.19

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.
@@ -786,12 +786,12 @@ var QLOO_LIMIT_SCHEMA = Type.Optional(Type.Integer({
786
786
  var QLOO_ENTITY_INPUTS_SCHEMA = Type.Array(Type.String({ minLength: 1 }), {
787
787
  minItems: 1,
788
788
  maxItems: 10,
789
- description: "Entity names or Qloo entity UUIDs. Keep one combined taste profile in one call."
789
+ description: "Named entities or Qloo entity UUIDs; use tag fields for abstract concepts."
790
790
  });
791
791
  var QLOO_TAG_INPUTS_SCHEMA = Type.Array(Type.String({ minLength: 1 }), {
792
792
  minItems: 1,
793
793
  maxItems: 10,
794
- description: "Natural-language tag concepts or stable Qloo tag URNs."
794
+ description: "Genres, moods, styles, traits, or other concepts, or Qloo tag URNs; not named entities."
795
795
  });
796
796
  var QLOO_DEMOGRAPHIC_SCHEMA = Type.String({
797
797
  minLength: 1,
@@ -951,6 +951,7 @@ var QLOO_WORKFLOW_METADATA = {
951
951
  promptSnippet: "Recommend entities from a combined Qloo taste profile",
952
952
  promptGuidelines: [
953
953
  "Pass every related taste and audience signal together in one qloo_recommend call.",
954
+ "Put named things in signals; put genres, moods, styles, traits, and concepts such as quiet luxury in signal_tags; reuse returned Qloo UUIDs and tag URNs.",
954
955
  "Use signal_location for audience location and filter_location to constrain result geography.",
955
956
  "Include demographic and location in the same call when both describe the audience."
956
957
  ],
@@ -961,7 +962,10 @@ var QLOO_WORKFLOW_METADATA = {
961
962
  label: "Qloo rank",
962
963
  description: "Rank one supplied option set against one shared entity, location, demographic, and tag profile. Scores from separate calls are not comparable.",
963
964
  promptSnippet: "Rank a caller-supplied shortlist with Qloo",
964
- promptGuidelines: ["Pass every option in one qloo_rank call so scores remain comparable."],
965
+ promptGuidelines: [
966
+ "Pass every option in one qloo_rank call so scores remain comparable.",
967
+ "Put named things in entity fields and concepts in tag fields; reuse returned Qloo UUIDs and tag URNs."
968
+ ],
965
969
  documentation: ["https://docs.qloo.com/reference/insights-api-deep-dive"]
966
970
  },
967
971
  describe: {
@@ -1445,7 +1449,7 @@ function interpretedEntity(entity) {
1445
1449
  function interpretedEntities(entities) {
1446
1450
  return entities.map(interpretedEntity);
1447
1451
  }
1448
- async function resolveEntities(provider, inputs, type, signal, correlationId) {
1452
+ async function resolveEntities(provider, inputs, type, signal, correlationId, field) {
1449
1453
  const { outcomes } = await provider.resolveEntities({
1450
1454
  inputs,
1451
1455
  ...type ? { type } : {}
@@ -1466,6 +1470,9 @@ async function resolveEntities(provider, inputs, type, signal, correlationId) {
1466
1470
  issues: outcomes.flatMap((outcome) => outcome.status === "resolved" ? [] : [{
1467
1471
  input: outcome.input,
1468
1472
  kind: outcome.status,
1473
+ input_kind: "entity",
1474
+ field,
1475
+ ...type ? { scope: type } : {},
1469
1476
  ...outcome.candidates ? {
1470
1477
  candidates: outcome.candidates.map((candidate) => ({ ...candidate }))
1471
1478
  } : {},
@@ -1484,7 +1491,7 @@ function interpretedTags(tags) {
1484
1491
  ...tag.score !== void 0 ? { score: tag.score } : {}
1485
1492
  }));
1486
1493
  }
1487
- async function resolveTags(provider, inputs, targetType, purpose, signal, correlationId) {
1494
+ async function resolveTags(provider, inputs, targetType, purpose, signal, correlationId, field) {
1488
1495
  const { outcomes } = await provider.resolveTags({
1489
1496
  inputs,
1490
1497
  ...targetType ? { targetType } : {},
@@ -1509,6 +1516,10 @@ async function resolveTags(provider, inputs, targetType, purpose, signal, correl
1509
1516
  issues: outcomes.flatMap((outcome) => outcome.status === "resolved" ? [] : [{
1510
1517
  input: outcome.input,
1511
1518
  kind: outcome.status,
1519
+ input_kind: "tag",
1520
+ field,
1521
+ ...targetType ? { scope: targetType } : {},
1522
+ purpose,
1512
1523
  ...outcome.candidates ? {
1513
1524
  candidates: outcome.candidates.map((candidate) => ({ ...candidate }))
1514
1525
  } : {},
@@ -1656,10 +1667,10 @@ function makeRecommendTool(clientOption, resolutionProviderOption) {
1656
1667
  return needsInput("recommend", [], "Provide at least one entity, tag, location, or demographic taste signal.");
1657
1668
  }
1658
1669
  const [entities, signalTags, includeTags, excludeTags] = await Promise.all([
1659
- resolveEntities(resolutionProvider, params.signals ?? [], void 0, signal, toolCallId),
1660
- resolveTags(resolutionProvider, params.signal_tags ?? [], targetType, "signal", signal, toolCallId),
1661
- resolveTags(resolutionProvider, params.include_tags ?? [], targetType, "include_filter", signal, toolCallId),
1662
- resolveTags(resolutionProvider, params.exclude_tags ?? [], targetType, "exclude_filter", signal, toolCallId)
1670
+ resolveEntities(resolutionProvider, params.signals ?? [], void 0, signal, toolCallId, "signals"),
1671
+ resolveTags(resolutionProvider, params.signal_tags ?? [], targetType, "signal", signal, toolCallId, "signal_tags"),
1672
+ resolveTags(resolutionProvider, params.include_tags ?? [], targetType, "include_filter", signal, toolCallId, "include_tags"),
1673
+ resolveTags(resolutionProvider, params.exclude_tags ?? [], targetType, "exclude_filter", signal, toolCallId, "exclude_tags")
1663
1674
  ]);
1664
1675
  const issues = [...entities.issues, ...signalTags.issues, ...includeTags.issues, ...excludeTags.issues];
1665
1676
  if (issues.length > 0)
@@ -1743,10 +1754,10 @@ function makeRankTool(clientOption, resolutionProviderOption) {
1743
1754
  return needsInput("rank", [], "Ranking requires an entity, location, or demographic signal.");
1744
1755
  }
1745
1756
  const [options, signals, includeTags, excludeTags] = await Promise.all([
1746
- resolveEntities(resolutionProvider, params.options, targetType, signal, toolCallId),
1747
- resolveEntities(resolutionProvider, params.signals ?? [], void 0, signal, toolCallId),
1748
- resolveTags(resolutionProvider, params.include_tags ?? [], targetType, "include_filter", signal, toolCallId),
1749
- resolveTags(resolutionProvider, params.exclude_tags ?? [], targetType, "exclude_filter", signal, toolCallId)
1757
+ resolveEntities(resolutionProvider, params.options, targetType, signal, toolCallId, "options"),
1758
+ resolveEntities(resolutionProvider, params.signals ?? [], void 0, signal, toolCallId, "signals"),
1759
+ resolveTags(resolutionProvider, params.include_tags ?? [], targetType, "include_filter", signal, toolCallId, "include_tags"),
1760
+ resolveTags(resolutionProvider, params.exclude_tags ?? [], targetType, "exclude_filter", signal, toolCallId, "exclude_tags")
1750
1761
  ]);
1751
1762
  const issues = [...options.issues, ...signals.issues, ...includeTags.issues, ...excludeTags.issues];
1752
1763
  if (issues.length > 0)
@@ -1805,7 +1816,7 @@ function makeDescribeTool(clientOption, resolutionProviderOption) {
1805
1816
  const client = requireClient(clientOption);
1806
1817
  const resolutionProvider = requireResolutionProvider(resolutionProviderOption);
1807
1818
  const type = params.type ? resolveSearchType(params.type) : void 0;
1808
- const resolution = await resolveEntities(resolutionProvider, [params.entity], type, signal, toolCallId);
1819
+ const resolution = await resolveEntities(resolutionProvider, [params.entity], type, signal, toolCallId, "entity");
1809
1820
  if (resolution.issues.length > 0)
1810
1821
  return needsInput("describe", resolution.issues, "Choose the intended Qloo entity.");
1811
1822
  const needsDetailHydration = resolution.resolved.some(({ match }) => match !== "identifier");
@@ -1842,7 +1853,7 @@ function makeWherePopularTool(clientOption, resolutionProviderOption) {
1842
1853
  const client = requireClient(clientOption);
1843
1854
  const resolutionProvider = requireResolutionProvider(resolutionProviderOption);
1844
1855
  const type = params.entity_type ? resolveSearchType(params.entity_type) : void 0;
1845
- const resolution = await resolveEntities(resolutionProvider, [params.entity], type, signal, toolCallId);
1856
+ const resolution = await resolveEntities(resolutionProvider, [params.entity], type, signal, toolCallId, "entity");
1846
1857
  if (resolution.issues.length > 0)
1847
1858
  return needsInput("where_popular", resolution.issues, "Choose the intended Qloo entity.");
1848
1859
  const entity = resolution.resolved[0];
@@ -1880,8 +1891,8 @@ function makeCompareAudiencesTool(clientOption, resolutionProviderOption) {
1880
1891
  const client = requireClient(clientOption);
1881
1892
  const resolutionProvider = requireResolutionProvider(resolutionProviderOption);
1882
1893
  const [groupA, groupB] = await Promise.all([
1883
- resolveEntities(resolutionProvider, params.group_a, void 0, signal, toolCallId),
1884
- resolveEntities(resolutionProvider, params.group_b, void 0, signal, toolCallId)
1894
+ resolveEntities(resolutionProvider, params.group_a, void 0, signal, toolCallId, "group_a"),
1895
+ resolveEntities(resolutionProvider, params.group_b, void 0, signal, toolCallId, "group_b")
1885
1896
  ]);
1886
1897
  const issues = [...groupA.issues, ...groupB.issues];
1887
1898
  if (issues.length > 0)
@@ -1920,7 +1931,7 @@ function makeEntityTagsTool(clientOption, resolutionProviderOption) {
1920
1931
  const client = requireClient(clientOption);
1921
1932
  const resolutionProvider = requireResolutionProvider(resolutionProviderOption);
1922
1933
  const type = params.entity_type ? resolveSearchType(params.entity_type) : void 0;
1923
- const resolution = await resolveEntities(resolutionProvider, params.entities, type, signal, toolCallId);
1934
+ const resolution = await resolveEntities(resolutionProvider, params.entities, type, signal, toolCallId, "entities");
1924
1935
  if (resolution.issues.length > 0)
1925
1936
  return needsInput("entity_tags", resolution.issues, "Choose or correct the unresolved Qloo entities.");
1926
1937
  const query = {
@@ -1953,7 +1964,7 @@ function makeAudienceDemographicsTool(clientOption, resolutionProviderOption) {
1953
1964
  const client = requireClient(clientOption);
1954
1965
  const resolutionProvider = requireResolutionProvider(resolutionProviderOption);
1955
1966
  const type = params.entity_type ? resolveSearchType(params.entity_type) : void 0;
1956
- const resolution = await resolveEntities(resolutionProvider, [params.entity], type, signal, toolCallId);
1967
+ const resolution = await resolveEntities(resolutionProvider, [params.entity], type, signal, toolCallId, "entity");
1957
1968
  if (resolution.issues.length > 0)
1958
1969
  return needsInput("audience_demographics", resolution.issues, "Choose the intended Qloo entity.");
1959
1970
  const entity = resolution.resolved[0];
@@ -1992,7 +2003,7 @@ function makeTrendsTool(clientOption, resolutionProviderOption) {
1992
2003
  throw new Error("Trend dates must be valid YYYY-MM-DD values with start_date on or before end_date.");
1993
2004
  }
1994
2005
  const type = resolveInsightsType(params.entity_type);
1995
- const resolution = await resolveEntities(resolutionProvider, params.entities, type, signal, toolCallId);
2006
+ const resolution = await resolveEntities(resolutionProvider, params.entities, type, signal, toolCallId, "entities");
1996
2007
  if (resolution.issues.length > 0)
1997
2008
  return needsInput("trends", resolution.issues, "Choose or correct the unresolved Qloo entities.");
1998
2009
  const requests = resolution.resolved.map((entity) => ({
@@ -188,12 +188,12 @@ var QLOO_LIMIT_SCHEMA = Type.Optional(Type.Integer({
188
188
  var QLOO_ENTITY_INPUTS_SCHEMA = Type.Array(Type.String({ minLength: 1 }), {
189
189
  minItems: 1,
190
190
  maxItems: 10,
191
- description: "Entity names or Qloo entity UUIDs. Keep one combined taste profile in one call."
191
+ description: "Named entities or Qloo entity UUIDs; use tag fields for abstract concepts."
192
192
  });
193
193
  var QLOO_TAG_INPUTS_SCHEMA = Type.Array(Type.String({ minLength: 1 }), {
194
194
  minItems: 1,
195
195
  maxItems: 10,
196
- description: "Natural-language tag concepts or stable Qloo tag URNs."
196
+ description: "Genres, moods, styles, traits, or other concepts, or Qloo tag URNs; not named entities."
197
197
  });
198
198
  var QLOO_DEMOGRAPHIC_SCHEMA = Type.String({
199
199
  minLength: 1,
@@ -353,6 +353,7 @@ var QLOO_WORKFLOW_METADATA = {
353
353
  promptSnippet: "Recommend entities from a combined Qloo taste profile",
354
354
  promptGuidelines: [
355
355
  "Pass every related taste and audience signal together in one qloo_recommend call.",
356
+ "Put named things in signals; put genres, moods, styles, traits, and concepts such as quiet luxury in signal_tags; reuse returned Qloo UUIDs and tag URNs.",
356
357
  "Use signal_location for audience location and filter_location to constrain result geography.",
357
358
  "Include demographic and location in the same call when both describe the audience."
358
359
  ],
@@ -363,7 +364,10 @@ var QLOO_WORKFLOW_METADATA = {
363
364
  label: "Qloo rank",
364
365
  description: "Rank one supplied option set against one shared entity, location, demographic, and tag profile. Scores from separate calls are not comparable.",
365
366
  promptSnippet: "Rank a caller-supplied shortlist with Qloo",
366
- promptGuidelines: ["Pass every option in one qloo_rank call so scores remain comparable."],
367
+ promptGuidelines: [
368
+ "Pass every option in one qloo_rank call so scores remain comparable.",
369
+ "Put named things in entity fields and concepts in tag fields; reuse returned Qloo UUIDs and tag URNs."
370
+ ],
367
371
  documentation: ["https://docs.qloo.com/reference/insights-api-deep-dive"]
368
372
  },
369
373
  describe: {
@@ -578,6 +582,60 @@ function scalar(record, ...keys) {
578
582
  function normalizeName(value) {
579
583
  return value.normalize("NFKC").trim().toLocaleLowerCase("en-US");
580
584
  }
585
+ var ENTITY_CONTEXT_STOP_WORDS = /* @__PURE__ */ new Set([
586
+ "a",
587
+ "an",
588
+ "and",
589
+ "at",
590
+ "by",
591
+ "for",
592
+ "from",
593
+ "in",
594
+ "of",
595
+ "on",
596
+ "the",
597
+ "to",
598
+ "with"
599
+ ]);
600
+ function contextTokens(value) {
601
+ return [...new Set(value.normalize("NFKC").toLocaleLowerCase("en-US").match(/[\p{L}\p{N}]+/gu)?.filter((token) => token.length >= 2 && !ENTITY_CONTEXT_STOP_WORDS.has(token)) ?? [])];
602
+ }
603
+ function candidateContextText(candidate) {
604
+ const address = typeof candidate.address === "string" ? candidate.address : candidate.address === void 0 ? "" : JSON.stringify(candidate.address);
605
+ return [candidate.name, candidate.type, candidate.description, address].filter((value) => typeof value === "string").join(" ");
606
+ }
607
+ function contextualEntityMatch(input, candidates) {
608
+ if (candidates.length < 2)
609
+ return { candidates };
610
+ const inputTokens = contextTokens(input);
611
+ if (inputTokens.length < 2)
612
+ return { candidates };
613
+ const ranked = candidates.map((candidate) => {
614
+ const searchable = new Set(contextTokens(candidateContextText(candidate)));
615
+ return {
616
+ candidate,
617
+ matched: inputTokens.filter((token) => searchable.has(token)).length
618
+ };
619
+ });
620
+ const maximum = Math.max(...ranked.map(({ matched }) => matched));
621
+ const strongest = ranked.filter(({ matched }) => matched === maximum);
622
+ const decisive = maximum === inputTokens.length && strongest.length === 1 && ranked.some(({ matched }) => matched < maximum);
623
+ if (decisive && strongest[0]) {
624
+ return {
625
+ candidates: [strongest[0].candidate],
626
+ selected: strongest[0].candidate,
627
+ alternatives: candidates.filter(({ id }) => id !== strongest[0]?.candidate.id).slice(0, 4),
628
+ warning: "Qloo selected the only candidate matching every supplied name and location term; reuse its Qloo ID to override future name resolution."
629
+ };
630
+ }
631
+ if (maximum === inputTokens.length && strongest.length < candidates.length) {
632
+ return {
633
+ candidates: strongest.map(({ candidate }) => candidate),
634
+ warning: "Qloo candidate choices were narrowed using the supplied name and location terms."
635
+ };
636
+ }
637
+ return { candidates };
638
+ }
581
639
  function requestOptions(context) {
582
640
  return {
583
641
  ...context.signal ? { signal: context.signal } : {},
@@ -686,7 +744,7 @@ function createNativeQlooResolutionProvider(client, options = {}) {
686
744
  contract_version: QLOO_RESOLUTION_CONTRACT_VERSION,
687
745
  provider_id: "qloo_api",
688
746
  name: "Qloo native resolution",
689
- entity_strategy: "exact_name_or_choice",
747
+ entity_strategy: "exact_name_context_or_choice",
690
748
  tag_strategy: "target_scoped_semantic_search_exact_name_or_choice",
691
749
  remote: true,
692
750
  uses_model: false,
@@ -696,6 +754,7 @@ function createNativeQlooResolutionProvider(client, options = {}) {
696
754
  const cacheKey = (kind, input, scope) => JSON.stringify([
697
755
  QLOO_RESOLUTION_CONTRACT_VERSION,
698
756
  descriptor.provider_id,
757
+ descriptor.entity_strategy,
699
758
  descriptor.tag_strategy,
700
759
  kind,
701
760
  scope ?? "",
@@ -752,7 +811,7 @@ function createNativeQlooResolutionProvider(client, options = {}) {
752
811
  return { input, status: "ambiguous", candidates: exact2.slice(0, 5), provenance: source2 };
753
812
  return { input, status: "not_found", provenance: source2 };
754
813
  }
755
- const source = provenance(descriptor, "/search", "exact_name_or_choice");
814
+ const source = provenance(descriptor, "/search", "exact_name_context_or_choice");
756
815
  const response = await client.searchEntities({
757
816
  query: input,
758
817
  ...type ? { types: type } : {},
@@ -766,11 +825,19 @@ function createNativeQlooResolutionProvider(client, options = {}) {
766
825
  const alternatives = candidates.filter(({ id }) => id !== exact[0]?.id).slice(0, 4);
767
826
  return resolved(input, exact[0], "exact", source, { alternatives });
768
827
  }
828
+ const contextual = contextualEntityMatch(input, candidates);
829
+ if (contextual.selected) {
830
+ return resolved(input, contextual.selected, "semantic", source, {
831
+ ...contextual.alternatives ? { alternatives: contextual.alternatives } : {},
832
+ ...contextual.warning ? { warnings: [contextual.warning] } : {}
833
+ });
834
+ }
769
835
  return {
770
836
  input,
771
837
  status: "ambiguous",
772
- candidates: (exact.length > 0 ? exact : candidates).slice(0, 5),
773
- provenance: source
838
+ candidates: (exact.length > 0 ? exact : contextual.candidates).slice(0, 5),
839
+ provenance: source,
840
+ ...contextual.warning ? { warnings: [contextual.warning] } : {}
774
841
  };
775
842
  };
776
843
  const resolveTag = async (input, targetType, context) => {
package/dist/router.js CHANGED
@@ -3940,7 +3940,7 @@ var qloo_workflow_contract_default = {
3940
3940
  },
3941
3941
  minItems: 1,
3942
3942
  maxItems: 10,
3943
- description: "Entity names or Qloo entity UUIDs. Keep one combined taste profile in one call."
3943
+ description: "Named entities or Qloo entity UUIDs; use tag fields for abstract concepts."
3944
3944
  },
3945
3945
  signal_tags: {
3946
3946
  type: "array",
@@ -3950,7 +3950,7 @@ var qloo_workflow_contract_default = {
3950
3950
  },
3951
3951
  minItems: 1,
3952
3952
  maxItems: 10,
3953
- description: "Natural-language tag concepts or stable Qloo tag URNs."
3953
+ description: "Genres, moods, styles, traits, or other concepts, or Qloo tag URNs; not named entities."
3954
3954
  },
3955
3955
  signal_tags_operator: {
3956
3956
  type: "string",
@@ -3986,7 +3986,7 @@ var qloo_workflow_contract_default = {
3986
3986
  },
3987
3987
  minItems: 1,
3988
3988
  maxItems: 10,
3989
- description: "Natural-language tag concepts or stable Qloo tag URNs."
3989
+ description: "Genres, moods, styles, traits, or other concepts, or Qloo tag URNs; not named entities."
3990
3990
  },
3991
3991
  include_tags_operator: {
3992
3992
  type: "string",
@@ -4004,7 +4004,7 @@ var qloo_workflow_contract_default = {
4004
4004
  },
4005
4005
  minItems: 1,
4006
4006
  maxItems: 10,
4007
- description: "Natural-language tag concepts or stable Qloo tag URNs."
4007
+ description: "Genres, moods, styles, traits, or other concepts, or Qloo tag URNs; not named entities."
4008
4008
  },
4009
4009
  exclude_tags_operator: {
4010
4010
  type: "string",
@@ -4075,7 +4075,7 @@ var qloo_workflow_contract_default = {
4075
4075
  },
4076
4076
  minItems: 1,
4077
4077
  maxItems: 10,
4078
- description: "Entity names or Qloo entity UUIDs. Keep one combined taste profile in one call."
4078
+ description: "Named entities or Qloo entity UUIDs; use tag fields for abstract concepts."
4079
4079
  },
4080
4080
  option_type: {
4081
4081
  type: "string",
@@ -4100,7 +4100,7 @@ var qloo_workflow_contract_default = {
4100
4100
  },
4101
4101
  minItems: 1,
4102
4102
  maxItems: 10,
4103
- description: "Entity names or Qloo entity UUIDs. Keep one combined taste profile in one call."
4103
+ description: "Named entities or Qloo entity UUIDs; use tag fields for abstract concepts."
4104
4104
  },
4105
4105
  signal_location: {
4106
4106
  type: "string",
@@ -4124,7 +4124,7 @@ var qloo_workflow_contract_default = {
4124
4124
  },
4125
4125
  minItems: 1,
4126
4126
  maxItems: 10,
4127
- description: "Natural-language tag concepts or stable Qloo tag URNs."
4127
+ description: "Genres, moods, styles, traits, or other concepts, or Qloo tag URNs; not named entities."
4128
4128
  },
4129
4129
  exclude_tags: {
4130
4130
  type: "array",
@@ -4134,7 +4134,7 @@ var qloo_workflow_contract_default = {
4134
4134
  },
4135
4135
  minItems: 1,
4136
4136
  maxItems: 10,
4137
- description: "Natural-language tag concepts or stable Qloo tag URNs."
4137
+ description: "Genres, moods, styles, traits, or other concepts, or Qloo tag URNs; not named entities."
4138
4138
  }
4139
4139
  },
4140
4140
  additionalProperties: false
@@ -4330,7 +4330,7 @@ var qloo_workflow_contract_default = {
4330
4330
  },
4331
4331
  minItems: 1,
4332
4332
  maxItems: 10,
4333
- description: "Entity names or Qloo entity UUIDs. Keep one combined taste profile in one call."
4333
+ description: "Named entities or Qloo entity UUIDs; use tag fields for abstract concepts."
4334
4334
  },
4335
4335
  group_b: {
4336
4336
  type: "array",
@@ -4340,7 +4340,7 @@ var qloo_workflow_contract_default = {
4340
4340
  },
4341
4341
  minItems: 1,
4342
4342
  maxItems: 10,
4343
- description: "Entity names or Qloo entity UUIDs. Keep one combined taste profile in one call."
4343
+ description: "Named entities or Qloo entity UUIDs; use tag fields for abstract concepts."
4344
4344
  },
4345
4345
  target_type: {
4346
4346
  type: "string",
@@ -4413,7 +4413,7 @@ var qloo_workflow_contract_default = {
4413
4413
  },
4414
4414
  minItems: 1,
4415
4415
  maxItems: 10,
4416
- description: "Entity names or Qloo entity UUIDs. Keep one combined taste profile in one call."
4416
+ description: "Named entities or Qloo entity UUIDs; use tag fields for abstract concepts."
4417
4417
  },
4418
4418
  entity_type: {
4419
4419
  type: "string",
@@ -4721,7 +4721,7 @@ var qloo_workflow_contract_default = {
4721
4721
  }
4722
4722
  ]
4723
4723
  },
4724
- contract_checksum: "sha256:d356f1c6f6163c3f44c4c92d70d789e651763c4dae3ad2d150aa83acc85a6a62"
4724
+ contract_checksum: "sha256:762bdcc5e14ff797b6de739d2658a83f78af35e68d7a5a3449455be6046924f5"
4725
4725
  };
4726
4726
 
4727
4727
  // apps/qloo-cli/dist/mcp.js
@@ -4,10 +4,12 @@ const require = __qlooCreateRequire(import.meta.url);
4
4
  // apps/qloo-harness/dist/runtime/explore-policy.js
5
5
  import { findGuidedGoal, guidedGoalLabel, QLOO_GUIDED_GOALS, qlooNextActions, qlooStarterIdeaLines } from "../guided-journey.js";
6
6
  import { getQlooProfileDefinition } from "../profiles.js";
7
- import { applyResolutionChoices, formatQlooResolutionCandidateLabels, paginateQlooResult, parseQlooRawPage, resolutionCandidates } from "../qloo-presentation.js";
7
+ import { applyResolutionChoices, paginateQlooResult, parseQlooRawPage, resolutionCandidates } from "../qloo-presentation.js";
8
8
  import { QLOO_NO_GUIDED_START_ENVIRONMENT_VARIABLE } from "../setup.js";
9
9
  import { isBlockedPiInteractiveCommand } from "./pi-command-policy.js";
10
10
  import { renderQlooHeader } from "./qloo-header.js";
11
+ import { selectQlooResolutionChoices } from "./resolution-choice-ui.js";
12
+ import { QLOO_RESOLUTION_CHOICE_ENTRY_TYPE, QlooResolutionMemory, clearResolutionChoicesEvent, forgetResolutionChoiceEvent, rememberResolutionChoicesEvent } from "./resolution-memory.js";
11
13
  var QLOO_WORKSPACE_TOOL_NAMES = [
12
14
  ...getQlooProfileDefinition("build").workspaceTools
13
15
  ];
@@ -141,6 +143,7 @@ function createHarnessPolicyExtension(profile, allowedToolNames, runtimeInfo, co
141
143
  let lastQlooToolName;
142
144
  let rawPage = 1;
143
145
  let activeProfile = profile;
146
+ const resolutionMemory = new QlooResolutionMemory();
144
147
  const customToolNames = [...allowedToolNames].filter((name) => !reservedBuiltInTools.has(name));
145
148
  const activeDefinition = () => getQlooProfileDefinition(activeProfile);
146
149
  const activeAllowedToolNames = () => /* @__PURE__ */ new Set([
@@ -156,6 +159,7 @@ function createHarnessPolicyExtension(profile, allowedToolNames, runtimeInfo, co
156
159
  };
157
160
  const restoreLastQlooResult = (context) => {
158
161
  const entries = context.sessionManager?.getBranch() ?? [];
162
+ resolutionMemory.restore(entries);
159
163
  for (let index = entries.length - 1; index >= 0; index -= 1) {
160
164
  const entry = entries[index];
161
165
  if (entry === null || typeof entry !== "object")
@@ -190,7 +194,64 @@ ${bounded}`);
190
194
  const setNextWidget = (context) => {
191
195
  if (context?.mode !== "tui" || !context.ui?.setWidget)
192
196
  return;
193
- context.ui.setWidget(QLOO_NEXT_WIDGET_KEY, ["Next: /next choose a follow-up \xB7 /why interpretation \xB7 /request API request"], { placement: "belowEditor" });
197
+ context.ui.setWidget(QLOO_NEXT_WIDGET_KEY, ["Next: /next follow-up \xB7 /why interpretation \xB7 /request API request \xB7 /matches remembered choices"], { placement: "belowEditor" });
198
+ };
199
+ const bindingLabel = (binding, index) => {
200
+ const prefix = index === void 0 ? "" : `${index + 1}. `;
201
+ const scope = binding.scope ? ` \xB7 ${binding.scope.replace(/^urn:entity:/u, "")}` : "";
202
+ return `${prefix}${binding.input} \u2192 ${binding.selected.name} \xB7 ${binding.input_kind}${scope}`;
203
+ };
204
+ const decorateConfirmedChoices = (details, bindings) => {
205
+ if (bindings.length === 0)
206
+ return { ...details };
207
+ const resolution = details.resolution !== null && typeof details.resolution === "object" && !Array.isArray(details.resolution) ? details.resolution : {};
208
+ return {
209
+ ...details,
210
+ resolution: {
211
+ ...resolution,
212
+ confirmed_choices: bindings.map((binding) => ({
213
+ input: binding.input,
214
+ input_kind: binding.input_kind,
215
+ field: binding.field,
216
+ ...binding.scope ? { scope: binding.scope } : {},
217
+ selected: { ...binding.selected },
218
+ instruction: "Reuse this Qloo identifier verbatim for this meaning in later calls."
219
+ }))
220
+ }
221
+ };
222
+ };
223
+ const showRememberedMatches = async (args, context) => {
224
+ if (args.trim().length > 0) {
225
+ context.ui.notify("Usage: /matches", "warning");
226
+ return;
227
+ }
228
+ const bindings = resolutionMemory.list();
229
+ if (bindings.length === 0) {
230
+ context.ui.notify("No user-confirmed Qloo matches are remembered in this session.");
231
+ return;
232
+ }
233
+ const visible = bindings.slice(-20);
234
+ const forgetLabels = visible.map((binding2, index2) => `Forget ${bindingLabel(binding2, index2)}`);
235
+ const clearLabel = `Forget all ${bindings.length} remembered matches`;
236
+ const cancelLabel = "Keep remembered matches";
237
+ const selected = await context.ui.select("Remembered Qloo matches", [...forgetLabels, clearLabel, cancelLabel]);
238
+ if (!selected || selected === cancelLabel)
239
+ return;
240
+ if (selected === clearLabel) {
241
+ const confirmed = await context.ui.confirm("Forget all remembered Qloo matches?", "The harness will ask again when any of these inputs are ambiguous.");
242
+ if (!confirmed)
243
+ return;
244
+ resolutionMemory.clear();
245
+ pi.appendEntry(QLOO_RESOLUTION_CHOICE_ENTRY_TYPE, clearResolutionChoicesEvent());
246
+ context.ui.notify("Forgot all remembered Qloo matches.");
247
+ return;
248
+ }
249
+ const index = forgetLabels.indexOf(selected);
250
+ const binding = visible[index];
251
+ if (index < 0 || !binding || !resolutionMemory.forget(binding))
252
+ return;
253
+ pi.appendEntry(QLOO_RESOLUTION_CHOICE_ENTRY_TYPE, forgetResolutionChoiceEvent(binding));
254
+ context.ui.notify(`Forgot \u201C${binding.input}\u201D \u2192 ${binding.selected.name}.`);
194
255
  };
195
256
  const recordQlooResult = (toolName, input, details, context) => {
196
257
  const boundedInput = boundedWorkflowInput(input);
@@ -384,6 +445,7 @@ ${bounded}`);
384
445
  `Fallback: ${runtimeInfo?.fallback ?? "none"}`,
385
446
  `Model: ${model}`,
386
447
  `Workspace: ${context.cwd}`,
448
+ `Remembered matches: ${resolutionMemory.list().length}`,
387
449
  `Context: ${usage?.tokens ?? "unknown"}/${usage?.contextWindow ?? "unknown"} tokens${usage?.percent === null || usage?.percent === void 0 ? "" : ` (${usage.percent.toFixed(1)}%)`}`,
388
450
  `Turn budget: ${activeDefinition().maxToolCallsPerTurn} calls; ${activeDefinition().maxIdenticalToolCallsPerTurn} identical calls`
389
451
  ].join("\n"));
@@ -483,6 +545,10 @@ ${bounded}`);
483
545
  await retryLastWorkflow(context);
484
546
  }
485
547
  });
548
+ pi.registerCommand("matches", {
549
+ description: "Inspect or forget user-confirmed Qloo entity and tag matches",
550
+ handler: async (args, context) => showRememberedMatches(args, context)
551
+ });
486
552
  pi.on("session_start", async (event, context) => {
487
553
  if (activeProfile === "build" && event.reason !== "startup") {
488
554
  activateProfile("plan", context);
@@ -549,42 +615,35 @@ ${bounded}`);
549
615
  if (event.details === null || typeof event.details !== "object" || Array.isArray(event.details))
550
616
  return;
551
617
  const details = event.details;
552
- const issues = resolutionCandidates(details);
618
+ const issues = resolutionCandidates(details, event.toolName, event.input);
553
619
  if (issues.length === 0 || !context.hasUI || !commandActions.retry) {
554
620
  recordQlooResult(event.toolName, event.input, details, context);
555
621
  await maybeRunAutomaticDiagnosis(details, context);
556
622
  return;
557
623
  }
558
- const choices = [];
559
- for (const issue of issues) {
560
- const labels = formatQlooResolutionCandidateLabels(issue.candidates).map((label, index) => `${index + 1}. ${label}`);
561
- const cancelLabel = "Cancel and ask me";
562
- const selected = await context.ui.select(`Choose the Qloo match for \u201C${issue.input.slice(0, 120)}\u201D`, [...labels, cancelLabel]);
563
- if (!selected || selected === cancelLabel) {
564
- recordQlooResult(event.toolName, event.input, details, context);
565
- return;
566
- }
567
- const selectedIndex = labels.indexOf(selected);
568
- const selectedId = issue.candidates[selectedIndex]?.id;
569
- if (selectedIndex < 0 || typeof selectedId !== "string") {
570
- recordQlooResult(event.toolName, event.input, details, context);
571
- return;
572
- }
573
- choices.push({ input: issue.input, selectedId });
624
+ const choices = await selectQlooResolutionChoices(context.ui, issues, context.mode === "tui");
625
+ if (!choices) {
626
+ recordQlooResult(event.toolName, event.input, details, context);
627
+ return;
574
628
  }
575
629
  const patchedInput = applyResolutionChoices(event.toolName, event.input, choices);
576
630
  if (!patchedInput) {
577
631
  recordQlooResult(event.toolName, event.input, details, context);
578
632
  return;
579
633
  }
634
+ const remembered = resolutionMemory.remember(event.toolName, choices);
635
+ if (remembered.length > 0) {
636
+ pi.appendEntry(QLOO_RESOLUTION_CHOICE_ENTRY_TYPE, rememberResolutionChoicesEvent(remembered));
637
+ }
580
638
  try {
581
639
  context.ui.setWorkingMessage("Qloo \xB7 applying your selection\u2026");
582
640
  const retriedDetails = await commandActions.retry(event.toolName, patchedInput);
583
- recordQlooResult(event.toolName, patchedInput, retriedDetails, context);
584
- await maybeRunAutomaticDiagnosis(retriedDetails, context);
641
+ const confirmedDetails = decorateConfirmedChoices(retriedDetails, remembered);
642
+ recordQlooResult(event.toolName, patchedInput, confirmedDetails, context);
643
+ await maybeRunAutomaticDiagnosis(confirmedDetails, context);
585
644
  return {
586
- content: [{ type: "text", text: JSON.stringify(retriedDetails) }],
587
- details: retriedDetails,
645
+ content: [{ type: "text", text: JSON.stringify(confirmedDetails) }],
646
+ details: confirmedDetails,
588
647
  isError: false
589
648
  };
590
649
  } catch {
@@ -603,6 +662,11 @@ ${bounded}`);
603
662
  reason: `Tool "${event.toolName}" is unavailable in Qloo's ${activeProfile} profile.`
604
663
  };
605
664
  }
665
+ if (event.toolName.startsWith("qloo_") && event.toolName !== "qloo_capabilities") {
666
+ const remembered = resolutionMemory.apply(event.toolName, event.input);
667
+ if (remembered)
668
+ Object.assign(event.input, remembered.input);
669
+ }
606
670
  toolCallsThisTurn += 1;
607
671
  if (toolCallsThisTurn > definition.maxToolCallsPerTurn) {
608
672
  return {