@spirit/agent-kit is a zero-dependency TypeScript library that ships three building blocks every Spirit agent with a sensor stream or a hidden-signal protocol will need: a sentinel extractor that strips hidden tokens from user-facing text while routing them to tool calls, a validator that catches vision and sensor model “drift” at the ingest boundary, and a session-keyed ring buffer for bounded perception history. All three primitives were extracted directly from the SOLIENNE encounter pipeline and generalized for reuse. The package is ESM-only with no runtime dependencies.
Installation
Until @spirit/agent-kit is published to the npm registry, reference it by path from a sibling project:
{
"dependencies": {
"@spirit/agent-kit": "file:../agent-kit"
}
}
Your consuming project must be ESM — set "type": "module" in its package.json.
Zero runtime dependencies means no supply-chain surprises and no transitive version conflicts. The package compiles to dist/ via tsc and ships submodule exports so you can import only what you use.
You can import from the root barrel or from individual submodules — both work:
// Submodule (tree-shakes to the minimum)
import { extractAllSentinels } from '@spirit/agent-kit/sentinels';
import { validatePerceptionObservation } from '@spirit/agent-kit/validator';
import { PerceptionHistory } from '@spirit/agent-kit/history';
// Barrel (one import site, slightly larger bundle)
import {
extractAllSentinels,
validate,
PerceptionHistory,
} from '@spirit/agent-kit';
Primitive 1 — sentinels
Agents in encounter pipelines emit tokens the user never sees — things like [look: door] or [[image: red coat]] — that route to tool calls or sensor lookups. The sentinels module strips these tokens from the user-facing text stream in a single pass while collecting structured payloads for downstream consumers.
The quickest path: call extractAllSentinels and get back cleanText (what the user sees), looks (single-bracket look tokens), and images (double-bracket image tokens).
import { extractAllSentinels } from '@spirit/agent-kit/sentinels';
const { cleanText, looks, images } = extractAllSentinels(
'walking [look: door] past [[image: doorway]] slowly',
);
console.log(cleanText); // 'walking past slowly'
console.log(looks); // [{ hint: 'door' }]
console.log(images); // [{ query: 'doorway' }]
Define your own sentinel
Conform to SentinelSpec<T> to extract any custom token shape. Supply a probe string for fast pre-screening, a regex that captures the token, and a project function that maps the match to your payload type.
import { extractSentinels, type SentinelSpec } from '@spirit/agent-kit/sentinels';
// Extract [cmd: <value>] tokens
const cmdSentinel: SentinelSpec<{ cmd: string }> = {
name: 'cmd',
probe: '[cmd:',
pattern: /\[cmd:\s*([^\]]+)\]/g,
project: (m) => ({ cmd: m[1].trim() }),
};
const { cleanText, payloads } = extractSentinels(
'please [cmd: open-door] proceed',
[cmdSentinel],
);
console.log(cleanText); // 'please proceed'
console.log(payloads[0].cmd); // 'open-door'
The probe string is a fast pre-filter — extractSentinels skips the regex entirely if the probe isn’t found in the input. Keep probes short and specific to the opening bracket pattern.
The built-in lookSentinel and imageSentinel mirror SOLIENNE’s shapes exactly. If your agent uses the same token conventions, you can import and reuse them directly.
Primitive 2 — validator
Vision and sensor models drift. A model asked to describe what a camera sees will sometimes return speculation — “seems lonely”, “probably looking at the exit” — rather than observations. The validator catches this drift at the ingest boundary so you can downgrade or discard a drifted payload instead of letting fabricated content enter your prompt loop.
validatePerceptionObservation
The built-in validator checks a perception payload against the reference rule set used in the SOLIENNE pipeline.
import { validatePerceptionObservation } from '@spirit/agent-kit/validator';
// This payload contains a fabrication marker ("probably")
const result = validatePerceptionObservation({
summary: 'Visitor probably looking for something',
posture: 'upright',
gaze: 'scanning',
affect: 'neutral',
stillness: 'still',
shift: 'new',
trigger: 'push',
});
console.log(result.valid); // false
console.log(result.failures);
// [{ rule: 'no_fabrication_markers', reason: '...' }]
A clean payload passes:
const clean = validatePerceptionObservation({
summary: 'Visitor is standing near the entrance scanning the room',
posture: 'upright',
gaze: 'scanning',
affect: 'neutral',
stillness: 'still',
shift: 'new',
trigger: 'push',
});
console.log(clean.valid); // true
Compose your own rule set
Call validate directly with any value and a custom array of ValidationRule<T> objects. Each rule returns null on pass or a failure object on fail.
import { validate, type ValidationRule } from '@spirit/agent-kit/validator';
interface SensorReading {
score: number;
label: string;
}
const positiveScore: ValidationRule<SensorReading> = {
name: 'positive_score',
check: (v) =>
v.score >= 0
? null
: { rule: 'positive_score', reason: `got ${v.score}` },
};
const noEmptyLabel: ValidationRule<SensorReading> = {
name: 'no_empty_label',
check: (v) =>
v.label.trim().length > 0
? null
: { rule: 'no_empty_label', reason: 'label is blank' },
};
const result = validate({ score: -1, label: '' }, [positiveScore, noEmptyLabel]);
console.log(result.valid); // false
console.log(result.failures.length) // 2
The validator is synchronous and allocation-light by design — it runs at the ingest boundary on every perception event, so it needs to be fast. Avoid async rule checks.
Primitive 3 — history
Perception pipelines need bounded per-session memory. PerceptionHistory is a generic ring buffer keyed by session ID. It supports two read modes: insertion-order recency (recent(n)) and wall-clock window (arc(windowMs)). You provide the timestamp accessor so the buffer stays generic over your observation shape.
Basic usage
import { PerceptionHistory } from '@spirit/agent-kit/history';
interface MyObs {
ts: number; // Unix ms
label: string;
posture: string;
}
const history = new PerceptionHistory<MyObs>({
slots: 20, // max entries per session
getTimestamp: (obs) => obs.ts, // how to read the timestamp
});
// Push observations as they arrive
history.push(sessionId, obs);
// Read the 3 most recent observations (insertion order)
const lastThree = history.recent(sessionId, 3);
// Read everything within the last 60 seconds
const lastMinute = history.arc(sessionId, 60_000);
// Clean up on session end
history.clear(sessionId);
recent(sessionId, n)
Returns the n most recently pushed observations for a session, newest last. If fewer than n observations exist, returns all of them.
const last5 = history.recent(sessionId, 5);
console.log(`Got ${last5.length} observations`); // up to 5
arc(sessionId, windowMs)
Returns all observations whose timestamp falls within windowMs milliseconds of the current wall clock. Useful for rate detection, gaze dwell analysis, or any pattern that depends on real elapsed time.
// All observations in the last 2 minutes
const recentWindow = history.arc(sessionId, 2 * 60 * 1000);
// Detect high activity: more than 10 events in 30 seconds
const burst = history.arc(sessionId, 30_000);
if (burst.length > 10) {
console.log('High-activity window detected');
}
Configuration
Maximum number of entries stored per session. When the buffer is full, the oldest entry is evicted to make room for each new push (ring behavior).
getTimestamp
(obs: T) => number
required
Accessor function that returns the Unix millisecond timestamp for an observation. Called during arc() reads.
Full Pipeline Example
The following shows all three primitives wired together in a minimal encounter loop, mirroring how SOLIENNE uses them:
import { extractAllSentinels } from '@spirit/agent-kit/sentinels';
import { validatePerceptionObservation } from '@spirit/agent-kit/validator';
import { PerceptionHistory } from '@spirit/agent-kit/history';
interface PerceptionObs {
ts: number;
summary: string;
posture: string;
gaze: string;
affect: string;
stillness: string;
shift: string;
trigger: string;
}
const history = new PerceptionHistory<PerceptionObs>({
slots: 30,
getTimestamp: (o) => o.ts,
});
async function handleAgentOutput(sessionId: string, rawText: string) {
// 1. Strip hidden signals from user-facing stream
const { cleanText, looks, images } = extractAllSentinels(rawText);
// Route sentinel payloads to tool calls
for (const look of looks) console.log('[LOOK]', look.hint);
for (const img of images) console.log('[IMAGE]', img.query);
// 2. Validate the perception payload before storing
const payload = await getPerceptionFromSensor(); // your sensor call
const check = validatePerceptionObservation(payload);
if (!check.valid) {
console.warn('Drifted payload dropped:', check.failures);
return cleanText; // return clean text but skip storing drifted data
}
// 3. Store validated observation in session history
history.push(sessionId, { ...payload, ts: Date.now() });
// Read context window for next prompt
const context = history.arc(sessionId, 90_000); // last 90 seconds
console.log(`${context.length} observations in context window`);
return cleanText;
}
First Consumer
The reference rule set in validator.ts (perceptionRules) and the reference sentinels in sentinels.ts (lookSentinel, imageSentinel) directly mirror the shapes used in the SOLIENNE encounter pipeline — Spirit Protocol’s first autonomous AI artist. You can import and reuse those rule sets and sentinel definitions directly, or treat them as a starting point and define your own by conforming to SentinelSpec and ValidationRule.