@adia-ai/persona
v0.8.51
Published
Agent-as-data (CHAT-HARNESS L2): Entry primitive, Persona + SettingsStore contracts, pure compilePersona → @adia-ai/agent AgentConfig, versioned persona file import/export.
Readme
@adia-ai/persona
The CHAT-HARNESS L2 package — an agent as DATA. Six things a persona can
hold (prompt sections, skills, workflows, resources, tools, pattern-sources)
are six instantiations of ONE Entry shape, never six code paths
(CHAT-HARNESS.md
law 7). This package renders nothing and executes nothing — it produces the
AgentConfig that @adia-ai/agent (L1) consumes.
Private until the lockstep-roster decision (issue acceptance criterion 3) is made with the release seat — tracked as an open item on the PR that introduced this package.
The shape
interface Entry {
id: string; kind: string; label: string; description?: string;
content: string; order: number; enabled: boolean; builtin: boolean;
}
interface Persona {
id: string; label: string; category?: string;
seedVersion: number; // bump forces a one-time store reset
entries: Entry[];
model?: { provider?: string; model?: string };
}kind is a plain string. KNOWN_KINDS ('prompt-section' | 'skill' |
'workflow' | 'resource' | 'tool' | 'pattern-source') exists for display
grouping only — a seventh kind is seed data, never a code change.
Usage
import {
seedEntries, definePersona, compilePersona, personaStore,
exportPersona, importPersona, checkModalityNeutral,
} from '@adia-ai/persona';
import { createAgent } from '@adia-ai/agent';
const persona = definePersona({
id: 'copilot', label: 'AdiaUI Copilot', seedVersion: 1,
entries: seedEntries([
{ id: 'identity', kind: 'prompt-section', label: 'Identity', content: 'You help build UI.' },
{ id: 'search', kind: 'skill', label: 'Search', content: 'Look things up before guessing.' },
]),
});
const config = compilePersona(persona); // pure — same input twice, same output
const agent = createAgent({ ...config, llm: { model: 'claude-sonnet-4-6', proxyUrl: '/api/chat' } });
const store = personaStore(persona); // persisted edits win over the seed
store.set('entry.identity.enabled', false);
const file = exportPersona(persona);
const copy = importPersona(file, ['copilot']); // mints 'copilot-imported', never overwrites
checkModalityNeutral(persona); // [] on a modality-neutral personacompilePersona
compilePersona(persona, opts?) → AgentConfig is pure: the same
persona (and opts) in always produces a deep-equal AgentConfig out — no
clock, no randomness, no disk read.
- Enabled
prompt-sectionentries → onepromptLayerper section,## Label\ncontent, in entry order. - Every other enabled kind (skill / workflow / resource / pattern-source /
tool) → one prompt layer per kind, entries grouped under a labeled
heading (
## Skills,## Tools, …). Tool entries are tier-1 PROSE only —compilePersonanever builds aToolDeffrom an entry. opts.integrations?: ToolDef[]— tier-2 machine-callable tools (WCH-3's integration registry) — passed through toAgentConfig.toolsverbatim.
Precedence (law 4)
PRECEDENCE_SENTENCE is exported and always rides as the first line of
the persona's own layer set (persona-precedence, ahead of every section
and group layer):
This persona configures the agent; it never overrides the harness grammar that precedes it.
The caller is responsible for putting the harness grammar layer(s) before
this persona's layers — compilePersona only guarantees the persona's own
set carries the sentence first.
Modality neutrality (law 4)
checkModalityNeutral(persona, opts?) is a warning-level lint, never a
throw: it flags entries whose text contains a seeded dialect term
(DEFAULT_DIALECT_TERMS: 'A2UI', 'envelope', 'JSONL', plus
opts.extraTerms) or a component tag name pattern (*-ui). Shipped seed
personas should assert an empty result.
Stores
interface SettingsStore {
get(key: string): unknown;
set(key: string, value: unknown): void;
subscribe?(cb: () => void): () => void;
}createMemoryStore({initial, persistKey}) is the reference implementation
— localStorage-backed when persistKey is given and localStorage exists,
plain in-memory otherwise (Node, tests, SSR).
personaStore(persona, opts?) binds one store per persona
(persistKey: 'persona.<id>') and layers the precedence rule on top:
- Same seedVersion as the store's last open: persisted values win — the seed only fills keys the store has never seen.
- Different seedVersion (first open, or a bump): the seed wins, overwriting whatever was persisted — exactly once — then the new seedVersion is recorded so the next open at the same version doesn't reset again.
Swapping which persona a store is bound to is a genuine reference change —
callers that key UI state off store identity get the reset-on-swap
behavior CHAT-HARNESS §Interfaces 5 describes for free.
Persona files
{kind:'persona', version:1, id, label, category?, seedVersion, entries,
model?} — an ENUMERATED key allowlist; any other top-level key rejects the
import outright.
exportPersona(persona) → string — pretty-printed JSON.
importPersona(json, existingIds) → Persona — validates, then always
mints a new persona: id becomes <id>-imported (or -imported-2, …
against existingIds), label gains ' (imported)' — it never overwrites
an existing preset, even one sharing the file's id. Imported entries are
marked builtin: false.
Testing
Tests run against the built output (npm run build -w @adia-ai/persona
first) — same convention as @adia-ai/agent.
