@dsh-agentic-roleplay/state

中文文档
0.12.4发布于 2026/11/13GPL-3.0-onlyTypeScriptES Module构建来源已验证近 30 天下载 6483

DSH plugin for roleplay session state: one SQLite database per session, exposed to other plugins as a service over canonical models.

原始使用文档
文档目录8 节
  1. Using it from another plugin
  2. The records API and the model-facing tools
  3. Configuration
  4. Layers
  5. The session
  6. Schema (version 1)
  7. Changing the schema
  8. Errors

@dsh-agentic-roleplay/state

The persistent session-state and memory service for DSH Agentic Roleplay, maintained by Firefox2100.

Most users should install @dsh-agentic-roleplay/all. Install this package separately when another DSH plugin needs the roleplay database, repositories, recall engine, or model-facing record tools without the complete roleplay interface.

A DeepSeek Harness plugin that keeps the state of roleplay sessions: one SQLite database per session, reached through repositories over canonical models. It does no orchestration and has no UI, so it can be installed on its own to get a roleplay database and state management without the rest of the roleplay bundle. Other plugins use it through the roleplayState service. Model-facing tools are included and can be enabled through its configuration.

Requires DSH 0.2 and Node 22.19 or later (it uses the built-in node:sqlite).

dsh plugin --profile <name> add @dsh-agentic-roleplay/state

Using it from another plugin

import type {} from '@dsh-agentic-roleplay/state'            // types `ctx.roleplayState`
import type { Context } from '@deepseek-ai/cordis'

export const inject = ['roleplayState']

export function apply(ctx: Context) {
  // Creates sessions/<uuid>/session.sqlite and opens it.
  const session = ctx.roleplayState.createSession('/path/to/sessions', {
    name: 'First night',
    world: { id: 'blackwater-observatory', name: 'The Blackwater Observatory' },
  })

  session.transaction(() => {
    const mayor = session.characters.add({ name: 'Eleanor Graves', /* … */ control: 'agent' })
    session.characterStates.create(mayor.id, { publicState: '…', innerState: '…', activity: '…' })
    session.intents.add(mayor.id, { type: 'agenda', name: '…', /* … */ })
  })

  session.characters.findByName('the mayor')       // by name or alias, ignoring case
  session.characterStates.update(1, { activity: 'looking for the key' }, { expectedVersion: 3 })
}

The service creates and opens sessions under a sessions folder you give it, shares one connection per session, and closes them all when the plugin unloads:

MethodDoes
createSession(sessionsDir, init)Creates the session folder and database and returns its repositories. The id is generated (UUID v4) unless given. Nothing is left behind if it fails.
openSession(sessionsDir, id)Returns the repositories of an existing session.
listSessions(sessionsDir)The metadata of every session under the folder (name, world, creation time, last played time, turns). A session that cannot be read is reported in problems, not thrown.
closeSession(sessionsDir, id)Releases the connection; opening again reconnects.
deleteSession(sessionsDir, id)Closes the session and removes its folder.

Session ids become folder names, so only a UUID v4 is accepted. The package also exports the same operations as plain functions (createSession, openSession, createSessionStore, …) for code that does not run inside DSH, such as tests.

The records API and the model-facing tools

The package owns how a story is remembered, not only where it is stored:

  • session.edit(edit) changes the story's records (character states, events with who took each in, beliefs, threads) together or not at all. It checks the whole edit first and throws InvalidEditError naming every problem, so a model's mistakes can be reported back to it. parseRecordsEdit(json) reads the wire format models write (snake_case) and recordsEditSchema({ characters, changes }) is its JSON schema, for a model's structured output or a tool's parameters.
  • session.records(scope) reads the records back in the same wire format.
  • session.recall({ characterId, situation, turn, policy }) is what a character is reminded of: events it took in and things it believes, by who the situation mentions, shared words and tags, importance and story-time recency; with policy.flaky the older and less important are told as faded or vague, or are gone. Age is elapsed story time from session.clock, never turn count; an unimportant event has a six-story-day half-life. It counts what it brings back as recalled, which makes it last longer.
  • session.recallStored(query, { queryVector? }) adds the session's search index to the cues: texts that match the situation by words and, with the vector of the situation, by meaning. session.searchEvents({ text, vector?, fromTurn, toTurn, minImportance, limit }) finds what happened by the same two searches, merged by reciprocal rank.
  • session.recallRanked(query, signal, { queryVector? }) is the call for anything that reminds a character of something: recall, ranked by meaning when a plugin has registered a ranker with roleplayState.rankMemories(ranker) (an embedding model, a reranker; see docs/capabilities.md). session.recallCandidates(query) gives what could be recalled, with the text to match, for a ranker. With explain: true in the query each recalled thing carries why: the cues of the moment (people named, words shared, place), what each search scored it, how much it matters, how long ago it was, how much of it is left in mind and what it was ranked by.
  • session.knowledge is the closed-world awareness graph for locations, items and directed exits (learn, knows, get, list, forget). Directional relationships are the corresponding character-to-character edges. Awareness grants identity only, not current state or whereabouts. session.planKnownMove plans through only a character's known places and exits; planMove remains the authoritative world planner.
  • session.factions, factionStates, factionMemberships, factionRelationships and factionIntents store persistent organisations separately from characters that act in scenes. Membership is many-to-many; relationships involving factions are directional and double as closed-world awareness edges.
  • roleplayState.bind(resolver) tells the tools which roleplay session an agent works in (the plugin that runs the agents supplies it, with the turn being played and the memory policy).

Tools registered with the DSH tool registry (defineTool, typed output, honoring the call's signal), resolved through that binding: list_characters, get_character, search_events, recall_memories, list_threads, and, only when the plugin is configured with writeTools: true, update_character_state, record_event, remember, open_thread, resolve_thread. They fail with a plain message for an agent that works in no roleplay session. Characters and other one-shot sub-agents see none of them (they are started with no tools).

Configuration

FieldDefaultMeaning
journalModedeletedelete keeps each session in a single file that can be copied; wal allows readers during writes.
writeToolsfalseAlso register the model-facing tools that change the records. Leave off where another agent is responsible for them.

Layers

LayerWhat it is
Canonical modelThe types in dsh-roleplay-model (re-exported here): Character, CharacterState, CharacterIntent, SessionMetadata, …
RepositoriesMetadataRepository, CharacterRepository, CharacterStateRepository, IntentRepository: the only access to the data. They take and return canonical models and throw the errors below.
SQLite implementationMaps rows to models and back; the only place that knows tables and SQL.
DriverSqlDriver / SqlDatabase: the few operations the implementation needs (exec, run, get, all, transaction). nodeSqliteDriver is built on node:sqlite.

Everything is synchronous, as the embedded SQLite libraries are.

This is an independent community project, not an official DSH package. Documentation · Source · Issues · GPL-3.0-only

The session

A session is the folder sessions/<uuid>/ with its database session.sqlite. The database is the only file the state needs; with the default journal mode it stays a single file that can be copied or archived.

The world itself is not stored. metadata keeps a world reference: the world's stable id (the id in its world.json, not a database id), its name, and optionally a version, a fingerprint of the content the session was started from, its source, and free-form extra fields. It is saved as one document, so provenance can grow without a schema change.

The metadata also carries what a list of sessions shows: the session's name (chosen when it is created), its creation time, the time of the last play (absent until a turn has been played) and the number of turns played. Whatever plays a turn calls metadata.recordTurn(), which counts it and sets the time.

Characters have session-local integer ids. source_id keeps the id the character had in the world, so a session can be traced back to its world; characters created during play have none.

Schema (version 1)

TableHolds
metadataKey/value rows: session.id, session.name, session.created_at, session.last_played_at, session.turns, world (JSON), plus free-form namespaced entries.
charactersThe definition: name, age, gender, pronouns, kind, tier, role, summary, personality, background, appearance, speech, and control (player or agent, chosen for the session). source_id is unique.
character_aliasesOne row per alias, in order, with a case-folded key for lookup.
character_stateThe state that changes in play: public state, inner state, activity, version, updated_at. One row per character.
character_emotionOptional baseline and immediate valence/arousal/dominance of a state.
intentsA character's intents: type, name, description, status, horizon, priority, urgency, plan, open threads, constraints, deadline.
relationships, consolidation_itemsHow a character stands towards another (a profile per direction: label, summary, familiarity, trust, affinity, tension) and the characters and places made up in play that still have to be fitted into the world. lintWorld reports what is inconsistent. The edit can make characters, places and ways when it is the authoring variant (recordsEditSchema({ authoring: true })).
entity_knowledgeExplicit character-to-location/item/exit awareness: the first learned turn and optional last directly observed turn. Absence means unknown; current facts remain in state, perception and memory.
factions, faction_aliases, faction_state, faction_memberships, faction_relationships, faction_intentsOrganisations, their institutional state and strategy, many-to-many membership, and directional relationships involving a faction.
slots, items, item_holdings, openers, event_itemsThings (one kind for items, equipment and containers: quantity, condition, notable, hidden, the slot it is worn in, a container state, an owner), where each is (a place, a character's hands or a slot, or inside a container item), the places on the body something can be worn (with defaults, data per session), what opens each lock (on items and exits), and which events are about which items. changeItem does what a character does with a thing (take, drop, give, put, wear, remove, open, close, lock, unlock, reveal) by the rules of the story and says every reason when it cannot.
clock, time_logThe story's own time: one row with the mode (simulated or real) and the instant, and every step it took (start, elapsed, real, skip, resync) with the turn and round. session.clock starts it, moves it forward, sets it in either direction when the mode changes, and tells when a round began.
routine_windows, engagements, engagement_participantsThe recurring day of each character (windows of the day with what it is doing and where, first fit applies) and dated commitments with who made them and whether they were kept. session.routines and session.engagements; agendaAt merges them with the character's goals (intents).
locations, exits, placementsPlaces (with features), the ways between them (one direction each, open / locked / blocked, optional travel seconds) and where things stand (a character: place, position, feature; the same table will place items). session.planMove / session.move move a character over open exits only (InvalidMoveError names every reason); the edit wire format can make places, change exits and move characters.
eventsWhat objectively happened: turn, round, text, importance (1 to 5), tags. event_involves lists the characters an event is about. Events are immutable: a draft until sealed, final afterwards (events.seal()); updating or removing a sealed one throws ConflictError.
perceptionsWhat one character took in of an event, in its own terms: mode (witnessed, heard, told, inferred), content, a gist for when it fades, an accuracy label (never shown to the character), and how often it was recalled. One per character and event.
memories, memory_evidenceWhat a character believes or knows beyond single events: kind, text, subjects, importance, confidence, formation turn, and the perceptions that support or contradict it.
story_threadsThings not settled yet: promises, mysteries, goals; open or resolved, and whether the player's character knows of each (secret until told, seen or taken on).
summariesThe account of the story for the conversation's head: digests of recent turns and chapters of older ones, each with the turns it covers.
recall_items, recall_fts, recall_vocab, recall_vecThe search index: one row per perception, memory and event, indexed by words (FTS5) and, once vectors are stored, by meaning (sqlite-vec, cosine, partitioned by character). store.recallIndex lists the texts without a vector (pending), stores vectors (store, which fixes the model and dimensions of the session's vectors) and searches (lexical, vector). Vector support is optional: without the extension everything but vector works.
pending_commitsRecorded rounds whose events and memories are not written yet, kept so that a failure loses nothing.

Events are the ground truth; perceptions (which stay revisable: updatePerception, addPerception), memories and summaries are derived from them and can be rebuilt. The repositories are events, memories, threads, summaries and pendingCommits next to the ones for characters.

Deleting a character deletes everything that belongs to it. The tables are STRICT, enumerations and ranges are CHECK constraints, and foreign keys are enforced. The file records its schema version in user_version and carries an application id, so a file that is not a session database is refused.

Changing the schema

Migrations are applied in order when a session is opened and are never edited once released: a change is a new entry in src/sqlite/schema.ts, and a test pins the migrations of each release. A database from a newer version is refused rather than damaged.

Before a file that is behind the latest schema is upgraded, it must pass SQLite's quick integrity check (a damaged file is refused with CorruptDataError, untouched) and a copy of it as it was is written to migration-backups/schema-<version>.sqlite beside the database, once per old version. migrationBackupPath(location, version) says where.

Errors

ErrorWhen
NotFoundErrorA row, character or session file does not exist.
ConflictErrorA repeated source_id, a state created twice, a stale expectedVersion, or creating a session where one exists.
InvalidValueErrorA value is outside what the schema allows.
SchemaErrorThe file is not a session database, or comes from a newer version.
CorruptDataErrorStored data cannot be read back as a model.

版本与发布记录

@dsh-agentic-roleplay/state 共发布 34 个版本,首次发布于 2026/10/13;过去 12 个月发布了 34 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.12.4latest2026/11/13今天
0.12.32026/11/121 天前
0.12.22026/11/121 天前
0.12.12026/11/112 天前
0.12.02026/11/103 天前
0.11.12026/11/076 天前

关于 @dsh-agentic-roleplay/state 的常见问题

@dsh-agentic-roleplay/state 是什么?

DSH plugin for roleplay session state: one SQLite database per session, exposed to other plugins as a service over canonical models.

如何安装 @dsh-agentic-roleplay/state?

在项目目录运行 npm install @dsh-agentic-roleplay/state,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@deepseek-ai/dsh(>=0.2.2-rc.1 <0.2.3)、@deepseek-ai/schemastery(^3.18.5)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@dsh-agentic-roleplay/state 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dsh-agentic-roleplay/state 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@dsh-agentic-roleplay/state 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js ^22.19.0 || >=24.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@dsh-agentic-roleplay/state 可以商用吗?

@dsh-agentic-roleplay/state 使用 GPL-3.0-only 许可。强 Copyleft:分发包含该代码的软件时,整个作品需以 GPL-3.0 兼容许可开源,并附带专利与反锁定条款。仅内部使用不触发此义务。以上为概括,不构成法律意见,请以许可原文为准。

@dsh-agentic-roleplay/state 的下载量有多少?

根据 npm 官方下载统计,@dsh-agentic-roleplay/state 在 2026/10/13 – 2026/11/11 这 30 天内被下载 6,483 次,日均约 216 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dsh-agentic-roleplay/state 还在维护吗?

最新版本 0.12.4 发布于 2026/11/13(今天);至今共发布 34 个版本,其中过去 12 个月发布了 34 个;首次发布于 2026/10/13。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。