@persistent-ai/tam (0.2.0)
Installation
@persistent-ai:registry=npm install @persistent-ai/tam@0.2.0"@persistent-ai/tam": "0.2.0"About this package
@persistent-ai/tam
tam is a React package for an agent-friendly geospatial investigation canvas.
It owns:
- validated JSON canvas commands
- Effector-backed isolated canvas instances
- serializable snapshots and hydration
- React provider/hooks
- MapLibre and deck.gl rendering primitives
- popup state and popup rendering hooks
The package deliberately does not depend on backend APIs, graph databases, commodity models, LLM providers, or chat history storage. Host applications provide those adapters.
Install
@persistent-ai/tam is published to GitHub Packages, not npmjs.com. Consumers need a one-time .npmrc setup pointing the @persistent-ai scope at npm.pkg.github.com. This applies to public packages too — it's how GitHub Packages works.
Add to your project's .npmrc (or ~/.npmrc):
@persistent-ai:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
GITHUB_TOKEN must be a personal access token with at least the read:packages scope. Then:
npm install @persistent-ai/tam
# or
bun add @persistent-ai/tam
Peer dependencies (install separately if you don't have them already):
bun add react react-dom maplibre-gl react-map-gl @deck.gl/core @deck.gl/layers @deck.gl/mapbox
Basic Use
import {
TamProvider,
TamMap,
TamPopups,
applyTamCommands,
createTamCanvas,
serializeTamCanvas,
} from "@persistent-ai/tam";
const canvas = createTamCanvas();
applyTamCommands(canvas, commandBatch);
export function MapExperience() {
return (
<TamProvider canvas={canvas}>
<TamMap basemapStyle="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" />
<TamPopups />
</TamProvider>
);
}
const snapshot = serializeTamCanvas(canvas);
See fixtures/basic-investigation.commands.json for a deterministic command batch that adds nodes, edges, styles, route sets, and popups.
examples/basic-demo.tsx shows a host-side React demo that starts empty, applies the fixture command batch, serializes the canvas, and hydrates from the matching snapshot.
Mapbox Basemaps
tam renders on MapLibre, which does not resolve the mapbox:// scheme that
every stock Mapbox style uses for its sources, sprites and glyphs. Passing a
Mapbox style URL straight to basemapStyle gets a blank canvas. Mapbox
supports the combination — it just needs the request hook MapLibre provides for
it:
import { TamMap, createMapboxTransformRequest } from "@persistent-ai/tam";
const transformRequest = createMapboxTransformRequest(import.meta.env.VITE_MAPBOX_TOKEN);
<TamMap
basemapStyle="https://api.mapbox.com/styles/v1/mapbox/outdoors-v12"
transformRequest={transformRequest}
cooperativeGestures
maxPitch={85}
/>;
Use a public token (pk.…) restricted by URL in your Mapbox account — it ships
in the browser bundle. A secret token (sk.…) is rejected outright. With no
token the hook rewrites nothing, so a missing environment variable reads as a
missing variable rather than as an outage.
Two things worth knowing before you commit to Mapbox as a tile source:
- Consumed through MapLibre, Mapbox bills per tile request rather than per map load, so the free tier goes further on paper than in practice. Measure your own scenario.
- The dependency stays one string.
basemapStyleis the whole of it, so moving to MapTiler, Protomaps or your own tiles is a change of configuration.
transformRequest, cooperativeGestures, maxPitch and terrain are handed
to MapLibre untouched; leaving one out leaves MapLibre's own default in place.
Agent Streaming With AG-UI
tam supports AG-UI-shaped streaming without depending on @ag-ui/core. The current @ag-ui/core package depends on zod v3, while tam stays on zod v4, so the integration is a small JSON bridge instead of a runtime dependency.
Agents stream map edits as CUSTOM events named tam.commands:
{
"type": "CUSTOM",
"name": "tam.commands",
"value": {
"commands": [
{
"type": "addNodes",
"nodes": [
{
"id": "Port/Santos",
"label": "Export Port",
"name": "Santos",
"latitude": -23.9618,
"longitude": -46.3322,
"properties": {}
}
]
}
]
}
}
Host code applies those events through the bridge:
import { createTamAgUiBridge, createTamCanvas } from "@persistent-ai/tam";
const canvas = createTamCanvas();
const bridge = createTamAgUiBridge(canvas);
for await (const event of agUiEventStream) {
const result = bridge.applyEvent(event);
if (result) {
sendAgUiEvent(bridge.commandResultEvent(result));
}
}
The bridge validates the full command batch before mutating Effector state. It can also emit STATE_SNAPSHOT events containing { tam: snapshot } and CUSTOM tam.selection events so the agent can observe current map state and user selection.
See fixtures/ag-ui-stream.events.json for a deterministic AG-UI stream fixture and fixtures/ag-ui-stream.snapshot.json for the expected canvas state after applying it through createTamAgUiBridge.
SVG Marker Icons
Agents can register external SVG marker icons through canvas commands, then reference them from node styles. The icon registry is part of tam state, so it serializes, hydrates, and stays independent of any host app component registry.
[
{
"type": "registerSvgIcon",
"icon": {
"id": "export-port",
"svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><path d=\"M8 42h48l-8 12H16z\"/></svg>",
"width": 64,
"height": 64,
"mask": true
}
},
{
"type": "addNodes",
"nodes": [
{
"id": "Port/Santos",
"label": "Export Port",
"latitude": -23.9618,
"longitude": -46.3322,
"properties": {},
"style": {
"icon": "export-port",
"iconSize": 28,
"color": "#0ea5e9"
}
}
]
}
]
Set mask: true to tint the SVG from style.color. Set mask: false when the SVG should keep its own colors.
Dependencies
Dependencies
| ID | Version |
|---|---|
| effector | ^23.4.2 |
| effector-react | ^23.3.0 |
| zod | ^4.1.12 |
Development Dependencies
| ID | Version |
|---|---|
| @changesets/changelog-github | ^0.7.0 |
| @changesets/cli | ^2.31.0 |
| @deck.gl/core | ^9.2.11 |
| @deck.gl/layers | ^9.2.11 |
| @deck.gl/mapbox | ^9.2.11 |
| @types/geojson | ^7946.0.16 |
| @types/react | ^19.0.0 |
| @types/react-dom | ^19.0.0 |
| @vitejs/plugin-react | ^4.3.0 |
| maplibre-gl | ^5.22.0 |
| oxlint | ^1.20.0 |
| react | ^19.0.0 |
| react-dom | ^19.0.0 |
| react-map-gl | ^8.1.0 |
| typescript | ~5.7.0 |
| vite | ^5.4.0 |
| vitest | ^2.1.9 |
Peer Dependencies
| ID | Version |
|---|---|
| @deck.gl/core | ^9.2.0 |
| @deck.gl/layers | ^9.2.0 |
| @deck.gl/mapbox | ^9.2.0 |
| maplibre-gl | ^5.0.0 |
| react | ^19.0.0 |
| react-dom | ^19.0.0 |
| react-map-gl | ^8.1.0 |