Files
Nicolò Boschi 41d71a9818 fix(#2808): make mental model tags_match configurable on all creation surfaces (MCP, TS client, CLI) (#2858)
* feat(mcp): let create_mental_model configure tags_match (#2808)

A tagged mental model with no explicit tags_match in its trigger JSON
refreshes under all_strict (a memory must carry every one of the model's
tags), while the staleness check and every recall/reflect path default to
any. Broadly-tagged models reading narrowly-tagged memories therefore get
marked stale and then refresh to empty content.

The HTTP API, generated SDK clients, and Control Plane UI already let users
set trigger.tags_match; the MCP create_mental_model tool did not. Add a
tags_match argument (validated against TagsMatch) to both MCP variants. It
is only written into the trigger when explicitly passed, so the resolved
all_strict default is preserved for existing callers.

Document the all_strict footgun and the tags_match override in the MCP and
mental-models API docs (regen skills/hindsight-docs mirror).

* fix(ts-client): expose tags_match/tag_groups on createMentalModel

The ergonomic TypeScript wrapper's createMentalModel accepted only
{ refreshAfterConsolidation } in its trigger option and dropped every other
trigger field, so a wrapper user could not set tags_match — the exact knob
needed to avoid the empty-refresh footgun in #2808. The low-level generated
sdk already accepts the full MentalModelTriggerInput; thread tagsMatch and
tagGroups through, mirroring how recall/reflect already expose them.

The Python client needs no change: its wrapper takes a pass-through
trigger dict and the generated MentalModelTriggerInput already validates
tags_match.

* test(ts-client): cover createMentalModel trigger mapping

Mock the generated sdk layer (no server needed) and assert the ergonomic
camelCase trigger options map onto the snake_case body: tagsMatch ->
tags_match, tagGroups -> tag_groups, refreshAfterConsolidation still maps,
and omitting trigger sends none (preserving the all_strict default). Locks
in the #2808 wrapper fix.

* docs(mental-models): add tags_match code snippet

Replace the static JSON block in the tags_match override section with a
live CodeSnippet pulled from the Python example, showing how to create a
model with trigger.tags_match="any" so a broadly-tagged model reads
narrowly-tagged memories on refresh (#2808).

* feat(cli): add --tags-match to mental-model create + all-language docs

The Rust CLI's `mental-model create` was the last creation surface with no
way to set tags_match, so a tagged model created via the CLI hit the same
empty-refresh footgun (#2808). Add a `--tags-match` flag (any/all/any_strict/
all_strict/exact) that is only sent when passed, preserving the server's
all_strict default; invalid values are rejected before the request.

Expand the mental-models docs "tags_match override" example from a single
Python snippet to a full Tabs block (Python / Node.js / CLI / Go), each
pulled from the runnable example files, and regen the skills mirror.
2026-07-21 12:04:23 +02:00

158 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Mental Models API examples for Hindsight (Node.js)
* Run: node examples/api/mental-models.mjs
*/
import { HindsightClient } from '@vectorize-io/hindsight-client';
const HINDSIGHT_URL = process.env.HINDSIGHT_API_URL || 'http://localhost:8888';
const BANK_ID = 'mental-models-demo-bank';
// =============================================================================
// Setup (not shown in docs)
// =============================================================================
const client = new HindsightClient({ baseUrl: HINDSIGHT_URL });
await client.createBank(BANK_ID, { name: 'Mental Models Demo' });
await client.retain(BANK_ID, 'The team prefers async communication via Slack');
await client.retain(BANK_ID, 'For urgent issues, use the #incidents channel');
await client.retain(BANK_ID, 'Weekly syncs happen every Monday at 10am');
await new Promise(r => setTimeout(r, 2000));
// =============================================================================
// Doc Examples
// =============================================================================
// [docs:create-mental-model]
// Create a mental model (runs reflect in background)
const result = await client.createMentalModel(
BANK_ID,
'Team Communication Preferences',
'How does the team prefer to communicate?',
{ tags: ['team', 'communication'] },
);
// Returns an operation_id — check operations endpoint for completion
console.log(`Operation ID: ${result.operation_id}`);
// [/docs:create-mental-model]
// [docs:create-mental-model-with-id]
// Create a mental model with a specific custom ID
const resultWithId = await client.createMentalModel(
BANK_ID,
'Communication Policy',
"What are the team's communication guidelines?",
{ id: 'communication-policy' },
);
console.log(`Created with custom ID: ${resultWithId.operation_id}`);
// [/docs:create-mental-model-with-id]
await new Promise(r => setTimeout(r, 5000));
// [docs:create-mental-model-with-trigger]
// Create a mental model with automatic refresh enabled
const result2 = await client.createMentalModel(
BANK_ID,
'Project Status',
'What is the current project status?',
{ trigger: { refreshCron: '0 3 * * *' } },
);
// This mental model checks daily at 03:00 UTC and refreshes when scoped memories changed
console.log(`Operation ID: ${result2.operation_id}`);
// [/docs:create-mental-model-with-trigger]
// [docs:create-mental-model-tags-match]
// Override how the model's tags filter source memories on refresh.
// A tagged model defaults to 'all_strict' (a memory must carry EVERY tag);
// use 'any' when your memories are tagged narrowly (one topic each), so the
// refresh reads any memory carrying at least one of the model's tags.
const result3 = await client.createMentalModel(
BANK_ID,
'Current Projects',
'Which projects is the user currently working on?',
{
tags: ['projects', 'mental-model'],
trigger: { tagsMatch: 'any' },
},
);
console.log(`Operation ID: ${result3.operation_id}`);
// [/docs:create-mental-model-tags-match]
await new Promise(r => setTimeout(r, 5000));
// [docs:list-mental-models]
// List all mental models in a bank
const mentalModels = await client.listMentalModels(BANK_ID);
for (const mm of mentalModels.items) {
console.log(`- ${mm.name}: ${mm.source_query}`);
}
// [/docs:list-mental-models]
const mentalModelId = mentalModels.items[0]?.id;
if (!mentalModelId) {
console.log('mental-models.mjs: All examples passed (no mental models created yet)');
await fetch(`${HINDSIGHT_URL}/v1/default/banks/${BANK_ID}`, { method: 'DELETE' });
process.exit(0);
}
// [docs:get-mental-model]
// Get a specific mental model
const mentalModel = await client.getMentalModel(BANK_ID, mentalModelId);
console.log(`Name: ${mentalModel.name}`);
console.log(`Content: ${mentalModel.content}`);
console.log(`Last refreshed: ${mentalModel.last_refreshed_at}`);
// [/docs:get-mental-model]
// [docs:refresh-mental-model]
// Refresh a mental model to update with current knowledge
const refreshResult = await client.refreshMentalModel(BANK_ID, mentalModelId);
console.log(`Refresh operation ID: ${refreshResult.operation_id}`);
// [/docs:refresh-mental-model]
// [docs:clear-mental-model]
// Clear a mental model's content, then refresh for a full re-synthesis
await client.clearMentalModel(BANK_ID, mentalModelId);
// Trigger a fresh full rebuild
const fullRefreshResult = await client.refreshMentalModel(BANK_ID, mentalModelId);
console.log(`Full refresh operation ID: ${fullRefreshResult.operation_id}`);
// [/docs:clear-mental-model]
// [docs:update-mental-model]
// Update a mental model's metadata
const updated = await client.updateMentalModel(BANK_ID, mentalModelId, {
name: 'Updated Team Communication Preferences',
trigger: { refresh_after_consolidation: true },
});
console.log(`Updated name: ${updated.name}`);
// [/docs:update-mental-model]
// [docs:get-mental-model-history]
// Get the change history of a mental model
const history = await client.getMentalModelHistory(BANK_ID, mentalModelId);
for (const entry of history) {
console.log(`Changed at: ${entry.changed_at}`);
console.log(`Previous content: ${entry.previous_content}`);
}
// [/docs:get-mental-model-history]
// [docs:delete-mental-model]
// Delete a mental model
await client.deleteMentalModel(BANK_ID, mentalModelId);
// [/docs:delete-mental-model]
// =============================================================================
// Cleanup (not shown in docs)
// =============================================================================
await client.deleteBank(BANK_ID);
console.log('mental-models.mjs: All examples passed');