@dsh-agentic-roleplay/state 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +163 -2
- package/cordis.patch.yml +3 -0
- package/lib/index.d.ts +3299 -0
- package/lib/index.js +11357 -0
- package/lib/index.js.map +1 -0
- package/package.json +82 -3
package/README.md
CHANGED
|
@@ -1,3 +1,164 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dsh-agentic-roleplay/state
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The persistent session-state and memory service for DSH Agentic Roleplay, maintained by Firefox2100.
|
|
4
|
+
|
|
5
|
+
Most users should install [`@dsh-agentic-roleplay/all`](https://www.npmjs.com/package/@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.
|
|
6
|
+
|
|
7
|
+
A DeepSeek Harness plugin that keeps the state of roleplay sessions: one SQLite
|
|
8
|
+
database per session, reached through repositories over canonical models. It
|
|
9
|
+
does no orchestration and has no UI, so it can be installed on its own to get a
|
|
10
|
+
roleplay database and state management without the rest of the roleplay bundle.
|
|
11
|
+
Other plugins use it through the `roleplayState` service. Model-facing tools
|
|
12
|
+
are included and can be enabled through its configuration.
|
|
13
|
+
|
|
14
|
+
Requires DSH 0.2 and Node 22.19 or later (it uses the built-in `node:sqlite`).
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
dsh plugin --profile <name> add @dsh-agentic-roleplay/state
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Using it from another plugin
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import type {} from '@dsh-agentic-roleplay/state' // types `ctx.roleplayState`
|
|
24
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
25
|
+
|
|
26
|
+
export const inject = ['roleplayState']
|
|
27
|
+
|
|
28
|
+
export function apply(ctx: Context) {
|
|
29
|
+
// Creates sessions/<uuid>/session.sqlite and opens it.
|
|
30
|
+
const session = ctx.roleplayState.createSession('/path/to/sessions', {
|
|
31
|
+
name: 'First night',
|
|
32
|
+
world: { id: 'blackwater-observatory', name: 'The Blackwater Observatory' },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
session.transaction(() => {
|
|
36
|
+
const mayor = session.characters.add({ name: 'Eleanor Graves', /* … */ control: 'agent' })
|
|
37
|
+
session.characterStates.create(mayor.id, { publicState: '…', innerState: '…', activity: '…' })
|
|
38
|
+
session.intents.add(mayor.id, { type: 'agenda', name: '…', /* … */ })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
session.characters.findByName('the mayor') // by name or alias, ignoring case
|
|
42
|
+
session.characterStates.update(1, { activity: 'looking for the key' }, { expectedVersion: 3 })
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The service creates and opens sessions under a sessions folder you give it,
|
|
47
|
+
shares one connection per session, and closes them all when the plugin unloads:
|
|
48
|
+
|
|
49
|
+
| Method | Does |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `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. |
|
|
52
|
+
| `openSession(sessionsDir, id)` | Returns the repositories of an existing session. |
|
|
53
|
+
| `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. |
|
|
54
|
+
| `closeSession(sessionsDir, id)` | Releases the connection; opening again reconnects. |
|
|
55
|
+
| `deleteSession(sessionsDir, id)` | Closes the session and removes its folder. |
|
|
56
|
+
|
|
57
|
+
Session ids become folder names, so only a UUID v4 is accepted. The package also
|
|
58
|
+
exports the same operations as plain functions (`createSession`, `openSession`,
|
|
59
|
+
`createSessionStore`, …) for code that does not run inside DSH, such as tests.
|
|
60
|
+
|
|
61
|
+
## The records API and the model-facing tools
|
|
62
|
+
|
|
63
|
+
The package owns how a story is remembered, not only where it is stored:
|
|
64
|
+
|
|
65
|
+
- `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.
|
|
66
|
+
- `session.records(scope)` reads the records back in the same wire format.
|
|
67
|
+
- `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.
|
|
68
|
+
- `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.
|
|
69
|
+
- `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.
|
|
70
|
+
- `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.
|
|
71
|
+
- `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.
|
|
72
|
+
- `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).
|
|
73
|
+
|
|
74
|
+
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).
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
| Field | Default | Meaning |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `journalMode` | `delete` | `delete` keeps each session in a single file that can be copied; `wal` allows readers during writes. |
|
|
81
|
+
| `writeTools` | `false` | Also register the model-facing tools that change the records. Leave off where another agent is responsible for them. |
|
|
82
|
+
|
|
83
|
+
## Layers
|
|
84
|
+
|
|
85
|
+
| Layer | What it is |
|
|
86
|
+
|---|---|
|
|
87
|
+
| Canonical model | The types in `dsh-roleplay-model` (re-exported here): `Character`, `CharacterState`, `CharacterIntent`, `SessionMetadata`, … |
|
|
88
|
+
| Repositories | `MetadataRepository`, `CharacterRepository`, `CharacterStateRepository`, `IntentRepository`: the only access to the data. They take and return canonical models and throw the errors below. |
|
|
89
|
+
| SQLite implementation | Maps rows to models and back; the only place that knows tables and SQL. |
|
|
90
|
+
| Driver | `SqlDriver` / `SqlDatabase`: the few operations the implementation needs (`exec`, `run`, `get`, `all`, `transaction`). `nodeSqliteDriver` is built on `node:sqlite`. |
|
|
91
|
+
|
|
92
|
+
Everything is synchronous, as the embedded SQLite libraries are.
|
|
93
|
+
|
|
94
|
+
This is an independent community project, not an official DSH package. [Documentation](https://firefox2100.github.io/dsh-agentic-roleplay/docs/developers/state) · [Source](https://github.com/Firefox2100/dsh-agentic-roleplay) · [Issues](https://github.com/Firefox2100/dsh-agentic-roleplay/issues) · GPL-3.0-only
|
|
95
|
+
|
|
96
|
+
## The session
|
|
97
|
+
|
|
98
|
+
A session is the folder `sessions/<uuid>/` with its database `session.sqlite`.
|
|
99
|
+
The database is the only file the state needs; with the default journal mode it
|
|
100
|
+
stays a single file that can be copied or archived.
|
|
101
|
+
|
|
102
|
+
The world itself is not stored. `metadata` keeps a **world reference**: the
|
|
103
|
+
world's stable id (the `id` in its `world.json`, not a database id), its name,
|
|
104
|
+
and optionally a version, a fingerprint of the content the session was started
|
|
105
|
+
from, its source, and free-form `extra` fields. It is saved as one document, so
|
|
106
|
+
provenance can grow without a schema change.
|
|
107
|
+
|
|
108
|
+
The metadata also carries what a list of sessions shows: the session's `name`
|
|
109
|
+
(chosen when it is created), its creation time, the time of the last play
|
|
110
|
+
(absent until a turn has been played) and the number of turns played. Whatever
|
|
111
|
+
plays a turn calls `metadata.recordTurn()`, which counts it and sets the time.
|
|
112
|
+
|
|
113
|
+
Characters have session-local integer ids. `source_id` keeps the id the
|
|
114
|
+
character had in the world, so a session can be traced back to its world;
|
|
115
|
+
characters created during play have none.
|
|
116
|
+
|
|
117
|
+
## Schema (version 1)
|
|
118
|
+
|
|
119
|
+
| Table | Holds |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `metadata` | Key/value rows: `session.id`, `session.name`, `session.created_at`, `session.last_played_at`, `session.turns`, `world` (JSON), plus free-form namespaced entries. |
|
|
122
|
+
| `characters` | The 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. |
|
|
123
|
+
| `character_aliases` | One row per alias, in order, with a case-folded key for lookup. |
|
|
124
|
+
| `character_state` | The state that changes in play: public state, inner state, activity, `version`, `updated_at`. One row per character. |
|
|
125
|
+
| `character_emotion` | Optional baseline and immediate valence/arousal/dominance of a state. |
|
|
126
|
+
| `intents` | A character's intents: type, name, description, status, horizon, priority, urgency, plan, open threads, constraints, deadline. |
|
|
127
|
+
| `relationships`, `consolidation_items` | How 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 })`). |
|
|
128
|
+
| `entity_knowledge` | Explicit 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. |
|
|
129
|
+
| `factions`, `faction_aliases`, `faction_state`, `faction_memberships`, `faction_relationships`, `faction_intents` | Organisations, their institutional state and strategy, many-to-many membership, and directional relationships involving a faction. |
|
|
130
|
+
| `slots`, `items`, `item_holdings`, `openers`, `event_items` | Things (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. |
|
|
131
|
+
| `clock`, `time_log` | The 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. |
|
|
132
|
+
| `routine_windows`, `engagements`, `engagement_participants` | The 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`). |
|
|
133
|
+
| `locations`, `exits`, `placements` | Places (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. |
|
|
134
|
+
| `events` | What 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`. |
|
|
135
|
+
| `perceptions` | What 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. |
|
|
136
|
+
| `memories` | What a character believes or knows beyond single events: kind (belief, relationship, knowledge), text, the characters it is about, importance, the turn it formed in. |
|
|
137
|
+
| `story_threads` | Things not settled yet: promises, mysteries, goals; open or resolved. |
|
|
138
|
+
| `summaries` | The account of the story for the conversation's head: digests of recent turns and chapters of older ones, each with the turns it covers. |
|
|
139
|
+
| `recall_items`, `recall_fts`, `recall_vocab`, `recall_vec` | The 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. |
|
|
140
|
+
| `pending_commits` | Recorded rounds whose events and memories are not written yet, kept so that a failure loses nothing. |
|
|
141
|
+
|
|
142
|
+
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.
|
|
143
|
+
|
|
144
|
+
Deleting a character deletes everything that belongs to it. The tables are
|
|
145
|
+
`STRICT`, enumerations and ranges are `CHECK` constraints, and foreign keys are
|
|
146
|
+
enforced. The file records its schema version in `user_version` and carries an
|
|
147
|
+
application id, so a file that is not a session database is refused.
|
|
148
|
+
|
|
149
|
+
## Changing the schema
|
|
150
|
+
|
|
151
|
+
Migrations are applied in order when a session is opened and are never edited
|
|
152
|
+
once released: a change is a new entry in `src/sqlite/schema.ts`. Before the
|
|
153
|
+
first release the schema is still being shaped, and migration 1 is edited in
|
|
154
|
+
place. A database from a newer version is refused rather than damaged.
|
|
155
|
+
|
|
156
|
+
## Errors
|
|
157
|
+
|
|
158
|
+
| Error | When |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `NotFoundError` | A row, character or session file does not exist. |
|
|
161
|
+
| `ConflictError` | A repeated `source_id`, a state created twice, a stale `expectedVersion`, or creating a session where one exists. |
|
|
162
|
+
| `InvalidValueError` | A value is outside what the schema allows. |
|
|
163
|
+
| `SchemaError` | The file is not a session database, or comes from a newer version. |
|
|
164
|
+
| `CorruptDataError` | Stored data cannot be read back as a model. |
|
package/cordis.patch.yml
ADDED