run-dmcp 0.1.0 → 0.2.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/README.md +76 -10
- package/dist/bin/run-dmcp.d.ts +2 -0
- package/dist/bin/run-dmcp.js +55 -0
- package/dist/db/connection.d.ts +32 -0
- package/dist/db/connection.js +38 -16
- package/dist/db/schema.d.ts +29 -1
- package/dist/db/schema.js +439 -7
- package/dist/http/server.js +3 -3
- package/dist/index.d.ts +36 -2
- package/dist/index.js +184 -92
- package/dist/mcp-server.d.ts +49 -0
- package/dist/mcp-server.js +127 -0
- package/dist/reader/turnReader.d.ts +185 -0
- package/dist/reader/turnReader.js +288 -0
- package/dist/register/batch.js +5 -79
- package/dist/register/mcp-resources.d.ts +9 -0
- package/dist/register/mcp-resources.js +16 -62
- package/dist/register/render.d.ts +17 -0
- package/dist/register/render.js +50 -0
- package/dist/register/resolve.d.ts +14 -0
- package/dist/register/resolve.js +102 -0
- package/dist/register/resources.js +11 -4
- package/dist/register/timeline.d.ts +2 -0
- package/dist/register/timeline.js +311 -0
- package/dist/rpg/index.d.ts +29 -0
- package/dist/rpg/index.js +55 -0
- package/dist/rpg/register/abilities.d.ts +2 -0
- package/dist/rpg/register/abilities.js +165 -0
- package/dist/rpg/register/batch.d.ts +2 -0
- package/dist/rpg/register/batch.js +92 -0
- package/dist/rpg/register/combat.d.ts +2 -0
- package/dist/rpg/register/combat.js +207 -0
- package/dist/rpg/register/mcp-prompts.d.ts +2 -0
- package/dist/rpg/register/mcp-prompts.js +684 -0
- package/dist/rpg/register/mcp-resources.d.ts +2 -0
- package/dist/rpg/register/mcp-resources.js +61 -0
- package/dist/rpg/register/quests.d.ts +2 -0
- package/dist/rpg/register/quests.js +118 -0
- package/dist/rpg/register/status.d.ts +2 -0
- package/dist/rpg/register/status.js +130 -0
- package/dist/rpg/register/tables.d.ts +2 -0
- package/dist/rpg/register/tables.js +146 -0
- package/dist/rpg/tools/ability.d.ts +48 -0
- package/dist/rpg/tools/ability.js +238 -0
- package/dist/rpg/tools/combat.d.ts +13 -0
- package/dist/rpg/tools/combat.js +195 -0
- package/dist/rpg/tools/dice.d.ts +23 -0
- package/dist/rpg/tools/dice.js +111 -0
- package/dist/rpg/tools/quest.d.ts +34 -0
- package/dist/rpg/tools/quest.js +164 -0
- package/dist/rpg/tools/status.d.ts +36 -0
- package/dist/rpg/tools/status.js +218 -0
- package/dist/rpg/tools/tables.d.ts +33 -0
- package/dist/rpg/tools/tables.js +209 -0
- package/dist/schemas/index.d.ts +12 -12
- package/dist/timeline/adjudication.d.ts +150 -0
- package/dist/timeline/adjudication.js +174 -0
- package/dist/timeline/changes.d.ts +100 -0
- package/dist/timeline/changes.js +161 -0
- package/dist/timeline/checkpoint.d.ts +69 -0
- package/dist/timeline/checkpoint.js +131 -0
- package/dist/timeline/clock.d.ts +89 -0
- package/dist/timeline/clock.js +173 -0
- package/dist/timeline/constrained.d.ts +220 -0
- package/dist/timeline/constrained.js +671 -0
- package/dist/timeline/export.d.ts +171 -0
- package/dist/timeline/export.js +329 -0
- package/dist/timeline/irreversible.d.ts +85 -0
- package/dist/timeline/irreversible.js +108 -0
- package/dist/timeline/kinds.d.ts +14 -0
- package/dist/timeline/kinds.js +22 -0
- package/dist/timeline/narration.d.ts +175 -0
- package/dist/timeline/narration.js +259 -0
- package/dist/timeline/projection.d.ts +97 -0
- package/dist/timeline/projection.js +330 -0
- package/dist/timeline/provenance.d.ts +66 -0
- package/dist/timeline/provenance.js +45 -0
- package/dist/timeline/registry.d.ts +95 -0
- package/dist/timeline/registry.js +124 -0
- package/dist/timeline/render.d.ts +121 -0
- package/dist/timeline/render.js +187 -0
- package/dist/timeline/replay.d.ts +64 -0
- package/dist/timeline/replay.js +104 -0
- package/dist/timeline/resolve.d.ts +262 -0
- package/dist/timeline/resolve.js +226 -0
- package/dist/timeline/schema.d.ts +13 -0
- package/dist/timeline/schema.js +262 -0
- package/dist/timeline/t.d.ts +80 -0
- package/dist/timeline/t.js +37 -0
- package/dist/tools/constraint.d.ts +44 -80
- package/dist/tools/constraint.js +115 -124
- package/dist/tools/relationship.d.ts +83 -2
- package/dist/tools/relationship.js +139 -62
- package/dist/tools/resource.d.ts +31 -6
- package/dist/tools/resource.js +106 -153
- package/dist/types/index.d.ts +19 -1
- package/dist/utils/output-schemas.d.ts +593 -2
- package/dist/utils/output-schemas.js +3 -0
- package/dist/utils/webui.d.ts +32 -0
- package/dist/utils/webui.js +54 -1
- package/package.json +20 -4
- package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
- package/dist/__tests__/engineVocabulary.test.js +0 -147
- package/dist/db/__tests__/connection.test.d.ts +0 -1
- package/dist/db/__tests__/connection.test.js +0 -72
- package/dist/db/__tests__/testDb.d.ts +0 -33
- package/dist/db/__tests__/testDb.js +0 -41
- package/dist/test-setup.d.ts +0 -1
- package/dist/test-setup.js +0 -13
- package/dist/tools/__tests__/audio.test.d.ts +0 -1
- package/dist/tools/__tests__/audio.test.js +0 -59
- package/dist/tools/__tests__/conserved.test.d.ts +0 -1
- package/dist/tools/__tests__/conserved.test.js +0 -488
- package/dist/tools/__tests__/constraint.test.d.ts +0 -1
- package/dist/tools/__tests__/constraint.test.js +0 -212
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
- package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
- package/dist/tools/__tests__/images.test.d.ts +0 -1
- package/dist/tools/__tests__/images.test.js +0 -59
- package/dist/tools/__tests__/relationship.test.d.ts +0 -1
- package/dist/tools/__tests__/relationship.test.js +0 -132
- package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
- package/dist/tools/__tests__/resource-constraints.test.js +0 -131
- package/dist/tools/__tests__/resource.test.d.ts +0 -1
- package/dist/tools/__tests__/resource.test.js +0 -190
- package/dist/tools/__tests__/time.test.d.ts +0 -1
- package/dist/tools/__tests__/time.test.js +0 -404
- package/dist/tools/__tests__/timers.test.d.ts +0 -1
- package/dist/tools/__tests__/timers.test.js +0 -426
- package/dist/tools/__tests__/world.test.d.ts +0 -1
- package/dist/tools/__tests__/world.test.js +0 -70
- package/dist/utils/__tests__/json.test.d.ts +0 -1
- package/dist/utils/__tests__/json.test.js +0 -55
- package/dist/utils/__tests__/validation.test.d.ts +0 -1
- package/dist/utils/__tests__/validation.test.js +0 -90
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn reader (design §12 seam 3, GitHub issue #15): one model call per
|
|
3
|
+
* unit of progress, answering the questions a server cannot answer with
|
|
4
|
+
* code -- "did the surplus hold", "was the tithe paid" -- structured facts a
|
|
5
|
+
* caller needs to keep its own world consistent, not narration.
|
|
6
|
+
*
|
|
7
|
+
* THE SPLIT, AND IT IS THE WHOLE POINT. The ENGINE owns the call, the
|
|
8
|
+
* citation rule, coercion, the safe default, and the fallback ladder. The
|
|
9
|
+
* CALLER owns the questions and the key vocabulary they are answered in.
|
|
10
|
+
* The engine never reads a question (`ReaderQuestion.prompt` is opaque, the
|
|
11
|
+
* same discipline `Proposal.parameters` observes in resolve.ts and `Claim`
|
|
12
|
+
* observes in narration.ts -- see those modules' own doc comments), never
|
|
13
|
+
* interprets an answer beyond "is this key a member of the set the caller
|
|
14
|
+
* declared", and never learns what any key MEANS. `"continued"` and
|
|
15
|
+
* `"TITHE"` are the same kind of token to this module: an opaque string
|
|
16
|
+
* dispatched or compared, never parsed for content.
|
|
17
|
+
*
|
|
18
|
+
* THE HARD CONSTRAINT THIS MODULE IS BUILT AROUND: `ReaderTransport` is an
|
|
19
|
+
* INJECTED INTERFACE, exactly like `Mechanic.adjudicate` (resolve.ts) is an
|
|
20
|
+
* injected interface rather than a call this module makes itself. There is
|
|
21
|
+
* no vendor SDK import here, no API key, no configured request URL, no
|
|
22
|
+
* `fetch`, no read of ambient environment configuration for any
|
|
23
|
+
* credential, and no network code of any kind --
|
|
24
|
+
* a transport is a plain async function the CALLER wrote and handed to
|
|
25
|
+
* `createTurnReader`, closing over whatever local model, hosted API, or
|
|
26
|
+
* scripted fixture it likes. This module is provably ignorant of what is on
|
|
27
|
+
* the other end of a transport; `noVendorTransports.test.ts` (this
|
|
28
|
+
* directory) enforces that mechanically by scanning every file here for
|
|
29
|
+
* vendor/network tokens, the same device `engineVocabulary.test.ts` uses for
|
|
30
|
+
* client vocabulary.
|
|
31
|
+
*
|
|
32
|
+
* RULE 4'S NARROW DOOR, AND WHY THE CITATION CHECK WALKS THROUGH IT WITHOUT
|
|
33
|
+
* VIOLATING IT. Root CLAUDE.md hard rule 4 forbids deriving state by
|
|
34
|
+
* pattern-matching meaning out of natural language -- and the citation check
|
|
35
|
+
* below (`quote occurs verbatim in that source's text`) is a
|
|
36
|
+
* `String.prototype.includes` call over text a CALLER supplied, checking for
|
|
37
|
+
* a span the CALLER'S OWN transport claims it read there. That is a literal
|
|
38
|
+
* presence test, structurally identical to the "a check for a token WE
|
|
39
|
+
* defined in output WE generated is fine" carve-out root CLAUDE.md states
|
|
40
|
+
* for itself -- except here the token is not one this codebase generated,
|
|
41
|
+
* it is one the caller's source text already contained, and the check is
|
|
42
|
+
* "does this exact byte sequence occur in that exact byte sequence", never
|
|
43
|
+
* "does this mean the same thing as that". No case folding, no whitespace
|
|
44
|
+
* trimming, no normalisation of any kind: the moment this check learned to
|
|
45
|
+
* ignore a difference between two strings, it would have taken one step
|
|
46
|
+
* toward understanding English rather than comparing text, which is exactly
|
|
47
|
+
* the slope hard rule 4 exists to keep this module off of. It is legal
|
|
48
|
+
* because it never asks what the quote or the source MEAN; it asks only
|
|
49
|
+
* whether one is a substring of the other.
|
|
50
|
+
*
|
|
51
|
+
* THE ENGINE RECORDS DECISIONS; IT DOES NOT MAKE THEM (hard rule 2). A
|
|
52
|
+
* rejected offer is a row -- `RejectedOffer.reason`, one of a fixed,
|
|
53
|
+
* literal, engine-defined vocabulary, never a severity or a verdict about
|
|
54
|
+
* whether the transport that offered it is "good" or "bad". There is no
|
|
55
|
+
* `confidence`, no `score`, no `valid`/`ok` field anywhere in this module.
|
|
56
|
+
* The caller's own `safeDefault` is likewise not this module's opinion about
|
|
57
|
+
* which direction is safe -- the caller declared it, per question, at
|
|
58
|
+
* construction; the engine has none of its own.
|
|
59
|
+
*
|
|
60
|
+
* THE FALLBACK LADDER is `transports`, tried in the caller's own order. The
|
|
61
|
+
* engine never learns which rung is "the local model" and which is "the
|
|
62
|
+
* hosted one" -- it is an ordered list of opaque functions, and a rung that
|
|
63
|
+
* throws, rejects, or returns something that is not an array of answers is
|
|
64
|
+
* treated identically: exhausted, advance to the next rung with only the
|
|
65
|
+
* still-unanswered questions. `attemptsPerTransport` gives one rung its own
|
|
66
|
+
* retry budget before the ladder gives up on it and moves on.
|
|
67
|
+
*/
|
|
68
|
+
/** A question the caller wants answered. `prompt` is the caller's text and
|
|
69
|
+
* is never read, parsed, or matched against anything by this module --
|
|
70
|
+
* carried opaquely from `ReadRequest` through to a transport, the same way
|
|
71
|
+
* `Proposal.parameters` (resolve.ts) is handed to a mechanic verbatim. */
|
|
72
|
+
export interface ReaderQuestion {
|
|
73
|
+
id: string;
|
|
74
|
+
/** The caller's text. Opaque to the engine. */
|
|
75
|
+
prompt: string;
|
|
76
|
+
/** The CLOSED set of keys an answer may take. Answers are keys, never
|
|
77
|
+
* prose -- a transport that wants to say "mostly, with an exception"
|
|
78
|
+
* cannot; it must pick one of these, or the offer is discarded. */
|
|
79
|
+
answerKeys: readonly string[];
|
|
80
|
+
/** The key to fall to when the read fails. The CALLER declares which
|
|
81
|
+
* direction is safe; the engine has no opinion of its own. Must be a
|
|
82
|
+
* member of `answerKeys` -- enforced at construction. */
|
|
83
|
+
safeDefault: string;
|
|
84
|
+
}
|
|
85
|
+
/** One source a claim may be cited against. Caller-supplied text; the
|
|
86
|
+
* engine never interprets it beyond a literal substring test (see the
|
|
87
|
+
* module doc comment's rule-4 discussion). */
|
|
88
|
+
export interface ReaderSource {
|
|
89
|
+
id: string;
|
|
90
|
+
text: string;
|
|
91
|
+
}
|
|
92
|
+
/** What a transport is handed for one call: the questions still needing an
|
|
93
|
+
* answer (already-validly-answered questions are never re-asked -- see
|
|
94
|
+
* the module doc comment), and every source available to cite against. */
|
|
95
|
+
export interface ReadRequest {
|
|
96
|
+
questions: readonly ReaderQuestion[];
|
|
97
|
+
sources: readonly ReaderSource[];
|
|
98
|
+
}
|
|
99
|
+
/** What a transport returns: KEYS and citations, never prose. */
|
|
100
|
+
export interface TransportAnswer {
|
|
101
|
+
questionId: string;
|
|
102
|
+
answerKey: string;
|
|
103
|
+
citation: {
|
|
104
|
+
sourceId: string;
|
|
105
|
+
quote: string;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** An injected capability, not a call this module makes itself -- see the
|
|
109
|
+
* module doc comment's "hard constraint" section. A transport may throw,
|
|
110
|
+
* reject, or return `TransportAnswer[]`; anything else (including a
|
|
111
|
+
* non-array return value) is treated as an unusable rung, identically to a
|
|
112
|
+
* throw. */
|
|
113
|
+
export type ReaderTransport = (request: ReadRequest) => Promise<readonly TransportAnswer[]>;
|
|
114
|
+
/** The literal, engine-defined reasons an offered answer is discarded.
|
|
115
|
+
* Never a severity, never a score -- a row names exactly which mechanical
|
|
116
|
+
* check failed (hard rule 2). */
|
|
117
|
+
export type RejectionReason = "unknown-question" | "unknown-answer-key" | "unknown-source-id" | "quote-not-in-source" | "empty-quote" | "duplicate-answer";
|
|
118
|
+
/** One discarded offer, carried verbatim so a reviewer can see exactly what
|
|
119
|
+
* was offered and why it did not count -- never summarised, never
|
|
120
|
+
* scored. `rung` is the index into the caller's own `transports` array
|
|
121
|
+
* that produced this offer; the engine reports the position, it does not
|
|
122
|
+
* interpret what that position means. */
|
|
123
|
+
export interface RejectedOffer {
|
|
124
|
+
reason: RejectionReason;
|
|
125
|
+
rung: number;
|
|
126
|
+
/** The offer exactly as the transport returned it. */
|
|
127
|
+
offer: TransportAnswer;
|
|
128
|
+
}
|
|
129
|
+
/** One row of the result: a question's final answer, whether it came from
|
|
130
|
+
* a transport or from the caller's own safe default, and every offer for
|
|
131
|
+
* that question this read discarded along the way. */
|
|
132
|
+
export interface AnsweredQuestion {
|
|
133
|
+
questionId: string;
|
|
134
|
+
/** Always a member of the question's own `answerKeys` -- true whether
|
|
135
|
+
* this came from a transport or from `safeDefault` itself. */
|
|
136
|
+
answerKey: string;
|
|
137
|
+
fromSafeDefault: boolean;
|
|
138
|
+
/** The index into `transports` that produced this answer; `null` when
|
|
139
|
+
* every rung was exhausted and the question took its safe default. */
|
|
140
|
+
answeredByRung: number | null;
|
|
141
|
+
/** `null` when `fromSafeDefault` is true -- a default was never cited
|
|
142
|
+
* against anything, because nothing was accepted for it to cite. */
|
|
143
|
+
citation: {
|
|
144
|
+
sourceId: string;
|
|
145
|
+
quote: string;
|
|
146
|
+
} | null;
|
|
147
|
+
rejected: readonly RejectedOffer[];
|
|
148
|
+
}
|
|
149
|
+
export interface ReaderResult {
|
|
150
|
+
/** One row per question this read declared, in the caller's own declared
|
|
151
|
+
* order -- deterministic regardless of which rung answered what or in
|
|
152
|
+
* what order a transport's own response array happened to list them. */
|
|
153
|
+
answers: readonly AnsweredQuestion[];
|
|
154
|
+
/** Offers naming a `questionId` that is not any question this read
|
|
155
|
+
* declared -- cannot be attached to a row in `answers` because there is
|
|
156
|
+
* no question for it to belong to. Always `reason: "unknown-question"`. */
|
|
157
|
+
unmatched: readonly RejectedOffer[];
|
|
158
|
+
}
|
|
159
|
+
export interface TurnReader {
|
|
160
|
+
/** Runs the ladder for this reader's declared questions against `sources`
|
|
161
|
+
* -- one call per unit of progress, per the module's own framing.
|
|
162
|
+
* `sources` varies per call; `questions`/`transports` are fixed at
|
|
163
|
+
* construction, because the questions and the vocabulary they are
|
|
164
|
+
* answered in are the caller's declared capability, not something that
|
|
165
|
+
* changes turn to turn. */
|
|
166
|
+
read(sources: readonly ReaderSource[]): Promise<ReaderResult>;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Builds the reader a caller uses for the lifetime of a session -- the
|
|
170
|
+
* `createResolver({ mechanics })` idiom (resolve.ts) copied directly:
|
|
171
|
+
* capability injected at construction, engine owns the protocol and never
|
|
172
|
+
* learns what the injected things mean. `questions` and `transports` are
|
|
173
|
+
* parameters, not globals, for the identical reason resolve.ts's own doc
|
|
174
|
+
* comment gives: a registry would make behaviour depend on import order.
|
|
175
|
+
*
|
|
176
|
+
* An empty `transports` list is legal -- every `read()` call against it
|
|
177
|
+
* returns every question's safe default, which is the correct behaviour
|
|
178
|
+
* for a caller that has not registered any capability to answer with, not
|
|
179
|
+
* a special case this function needs to guard against.
|
|
180
|
+
*/
|
|
181
|
+
export declare function createTurnReader(params: {
|
|
182
|
+
questions: readonly ReaderQuestion[];
|
|
183
|
+
transports: readonly ReaderTransport[];
|
|
184
|
+
attemptsPerTransport?: number;
|
|
185
|
+
}): TurnReader;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn reader (design §12 seam 3, GitHub issue #15): one model call per
|
|
3
|
+
* unit of progress, answering the questions a server cannot answer with
|
|
4
|
+
* code -- "did the surplus hold", "was the tithe paid" -- structured facts a
|
|
5
|
+
* caller needs to keep its own world consistent, not narration.
|
|
6
|
+
*
|
|
7
|
+
* THE SPLIT, AND IT IS THE WHOLE POINT. The ENGINE owns the call, the
|
|
8
|
+
* citation rule, coercion, the safe default, and the fallback ladder. The
|
|
9
|
+
* CALLER owns the questions and the key vocabulary they are answered in.
|
|
10
|
+
* The engine never reads a question (`ReaderQuestion.prompt` is opaque, the
|
|
11
|
+
* same discipline `Proposal.parameters` observes in resolve.ts and `Claim`
|
|
12
|
+
* observes in narration.ts -- see those modules' own doc comments), never
|
|
13
|
+
* interprets an answer beyond "is this key a member of the set the caller
|
|
14
|
+
* declared", and never learns what any key MEANS. `"continued"` and
|
|
15
|
+
* `"TITHE"` are the same kind of token to this module: an opaque string
|
|
16
|
+
* dispatched or compared, never parsed for content.
|
|
17
|
+
*
|
|
18
|
+
* THE HARD CONSTRAINT THIS MODULE IS BUILT AROUND: `ReaderTransport` is an
|
|
19
|
+
* INJECTED INTERFACE, exactly like `Mechanic.adjudicate` (resolve.ts) is an
|
|
20
|
+
* injected interface rather than a call this module makes itself. There is
|
|
21
|
+
* no vendor SDK import here, no API key, no configured request URL, no
|
|
22
|
+
* `fetch`, no read of ambient environment configuration for any
|
|
23
|
+
* credential, and no network code of any kind --
|
|
24
|
+
* a transport is a plain async function the CALLER wrote and handed to
|
|
25
|
+
* `createTurnReader`, closing over whatever local model, hosted API, or
|
|
26
|
+
* scripted fixture it likes. This module is provably ignorant of what is on
|
|
27
|
+
* the other end of a transport; `noVendorTransports.test.ts` (this
|
|
28
|
+
* directory) enforces that mechanically by scanning every file here for
|
|
29
|
+
* vendor/network tokens, the same device `engineVocabulary.test.ts` uses for
|
|
30
|
+
* client vocabulary.
|
|
31
|
+
*
|
|
32
|
+
* RULE 4'S NARROW DOOR, AND WHY THE CITATION CHECK WALKS THROUGH IT WITHOUT
|
|
33
|
+
* VIOLATING IT. Root CLAUDE.md hard rule 4 forbids deriving state by
|
|
34
|
+
* pattern-matching meaning out of natural language -- and the citation check
|
|
35
|
+
* below (`quote occurs verbatim in that source's text`) is a
|
|
36
|
+
* `String.prototype.includes` call over text a CALLER supplied, checking for
|
|
37
|
+
* a span the CALLER'S OWN transport claims it read there. That is a literal
|
|
38
|
+
* presence test, structurally identical to the "a check for a token WE
|
|
39
|
+
* defined in output WE generated is fine" carve-out root CLAUDE.md states
|
|
40
|
+
* for itself -- except here the token is not one this codebase generated,
|
|
41
|
+
* it is one the caller's source text already contained, and the check is
|
|
42
|
+
* "does this exact byte sequence occur in that exact byte sequence", never
|
|
43
|
+
* "does this mean the same thing as that". No case folding, no whitespace
|
|
44
|
+
* trimming, no normalisation of any kind: the moment this check learned to
|
|
45
|
+
* ignore a difference between two strings, it would have taken one step
|
|
46
|
+
* toward understanding English rather than comparing text, which is exactly
|
|
47
|
+
* the slope hard rule 4 exists to keep this module off of. It is legal
|
|
48
|
+
* because it never asks what the quote or the source MEAN; it asks only
|
|
49
|
+
* whether one is a substring of the other.
|
|
50
|
+
*
|
|
51
|
+
* THE ENGINE RECORDS DECISIONS; IT DOES NOT MAKE THEM (hard rule 2). A
|
|
52
|
+
* rejected offer is a row -- `RejectedOffer.reason`, one of a fixed,
|
|
53
|
+
* literal, engine-defined vocabulary, never a severity or a verdict about
|
|
54
|
+
* whether the transport that offered it is "good" or "bad". There is no
|
|
55
|
+
* `confidence`, no `score`, no `valid`/`ok` field anywhere in this module.
|
|
56
|
+
* The caller's own `safeDefault` is likewise not this module's opinion about
|
|
57
|
+
* which direction is safe -- the caller declared it, per question, at
|
|
58
|
+
* construction; the engine has none of its own.
|
|
59
|
+
*
|
|
60
|
+
* THE FALLBACK LADDER is `transports`, tried in the caller's own order. The
|
|
61
|
+
* engine never learns which rung is "the local model" and which is "the
|
|
62
|
+
* hosted one" -- it is an ordered list of opaque functions, and a rung that
|
|
63
|
+
* throws, rejects, or returns something that is not an array of answers is
|
|
64
|
+
* treated identically: exhausted, advance to the next rung with only the
|
|
65
|
+
* still-unanswered questions. `attemptsPerTransport` gives one rung its own
|
|
66
|
+
* retry budget before the ladder gives up on it and moves on.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Construction-time validation, in the same voice as `validateMechanics`
|
|
70
|
+
* (resolve.ts) and `validateMigrations` (src/db/schema.ts): every check
|
|
71
|
+
* that could make an invalid reader constructable runs ONCE, here, so a bad
|
|
72
|
+
* declaration fails loudly before a single `read()` call rather than
|
|
73
|
+
* surfacing as an answer silently outside the declared set three calls
|
|
74
|
+
* later. In particular: a reader that could ever return a key outside its
|
|
75
|
+
* own declared `answerKeys` must be unconstructable, which is why
|
|
76
|
+
* `safeDefault` membership is checked here rather than trusted.
|
|
77
|
+
*/
|
|
78
|
+
function validateQuestions(questions) {
|
|
79
|
+
const seenIds = new Set();
|
|
80
|
+
for (const question of questions) {
|
|
81
|
+
const id = question?.id;
|
|
82
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
83
|
+
throw new Error(`createTurnReader: a question's 'id' must be a non-empty string, got ${JSON.stringify(id)}`);
|
|
84
|
+
}
|
|
85
|
+
if (seenIds.has(id)) {
|
|
86
|
+
throw new Error(`createTurnReader: duplicate question id '${id}'`);
|
|
87
|
+
}
|
|
88
|
+
seenIds.add(id);
|
|
89
|
+
const answerKeys = question.answerKeys;
|
|
90
|
+
if (!Array.isArray(answerKeys) || answerKeys.length === 0) {
|
|
91
|
+
throw new Error(`createTurnReader: question '${id}' must declare at least one answerKey`);
|
|
92
|
+
}
|
|
93
|
+
const seenKeys = new Set();
|
|
94
|
+
for (const key of answerKeys) {
|
|
95
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
96
|
+
throw new Error(`createTurnReader: question '${id}' has an answerKey that is not a non-empty string: ${JSON.stringify(key)}`);
|
|
97
|
+
}
|
|
98
|
+
if (seenKeys.has(key)) {
|
|
99
|
+
throw new Error(`createTurnReader: question '${id}' declares duplicate answerKey '${key}'`);
|
|
100
|
+
}
|
|
101
|
+
seenKeys.add(key);
|
|
102
|
+
}
|
|
103
|
+
if (!answerKeys.includes(question.safeDefault)) {
|
|
104
|
+
throw new Error(`createTurnReader: question '${id}' declares safeDefault '${question.safeDefault}' which is not ` +
|
|
105
|
+
`one of its own answerKeys (${answerKeys.join(", ")})`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function validateAttemptsPerTransport(attemptsPerTransport) {
|
|
110
|
+
if (!Number.isInteger(attemptsPerTransport) || attemptsPerTransport < 1) {
|
|
111
|
+
throw new Error(`createTurnReader: 'attemptsPerTransport' must be an integer >= 1, got ${JSON.stringify(attemptsPerTransport)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Builds the reader a caller uses for the lifetime of a session -- the
|
|
116
|
+
* `createResolver({ mechanics })` idiom (resolve.ts) copied directly:
|
|
117
|
+
* capability injected at construction, engine owns the protocol and never
|
|
118
|
+
* learns what the injected things mean. `questions` and `transports` are
|
|
119
|
+
* parameters, not globals, for the identical reason resolve.ts's own doc
|
|
120
|
+
* comment gives: a registry would make behaviour depend on import order.
|
|
121
|
+
*
|
|
122
|
+
* An empty `transports` list is legal -- every `read()` call against it
|
|
123
|
+
* returns every question's safe default, which is the correct behaviour
|
|
124
|
+
* for a caller that has not registered any capability to answer with, not
|
|
125
|
+
* a special case this function needs to guard against.
|
|
126
|
+
*/
|
|
127
|
+
export function createTurnReader(params) {
|
|
128
|
+
const attemptsPerTransport = params.attemptsPerTransport ?? 1;
|
|
129
|
+
validateQuestions(params.questions);
|
|
130
|
+
validateAttemptsPerTransport(attemptsPerTransport);
|
|
131
|
+
const questions = params.questions;
|
|
132
|
+
const transports = params.transports;
|
|
133
|
+
return {
|
|
134
|
+
read(sources) {
|
|
135
|
+
return runLadder(questions, transports, attemptsPerTransport, sources);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Whether `citation` cites a real source, non-empty, verbatim -- the
|
|
141
|
+
* citation rule's three conjuncts. All three must hold; the order below is
|
|
142
|
+
* only the order a REASON is reported in when more than one fails, and it
|
|
143
|
+
* runs cheapest-first: the quote must be a non-empty string, the source must
|
|
144
|
+
* be one that is actually in the request, and the quote must occur EXACTLY
|
|
145
|
+
* (`String.prototype.includes`, no case folding, no trimming, no fuzzy
|
|
146
|
+
* matching) inside that source's text. An offer that fails two conjuncts is
|
|
147
|
+
* rejected either way -- which reason it carries is a reporting detail, not
|
|
148
|
+
* a difference in whether it counts. Returns the first failing reason, or
|
|
149
|
+
* `null` when the citation is accepted -- see the module doc comment's
|
|
150
|
+
* rule-4 discussion for why this literal presence test does not become the
|
|
151
|
+
* pattern-matching-meaning it is built beside.
|
|
152
|
+
*
|
|
153
|
+
* Defensive against a citation that is missing entirely or missing a field
|
|
154
|
+
* -- a transport is caller-supplied code this module does not control at
|
|
155
|
+
* runtime, and a malformed citation must still be rejected mechanically
|
|
156
|
+
* rather than throwing out of this function and aborting the whole read.
|
|
157
|
+
*/
|
|
158
|
+
function checkCitation(citation, sourcesById) {
|
|
159
|
+
const sourceId = citation?.sourceId;
|
|
160
|
+
const quote = citation?.quote;
|
|
161
|
+
if (typeof quote !== "string" || quote.length === 0) {
|
|
162
|
+
return "empty-quote";
|
|
163
|
+
}
|
|
164
|
+
if (typeof sourceId !== "string") {
|
|
165
|
+
return "unknown-source-id";
|
|
166
|
+
}
|
|
167
|
+
const source = sourcesById.get(sourceId);
|
|
168
|
+
if (!source) {
|
|
169
|
+
return "unknown-source-id";
|
|
170
|
+
}
|
|
171
|
+
if (!source.text.includes(quote)) {
|
|
172
|
+
return "quote-not-in-source";
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Runs the fallback ladder for one `read()` call. `accepted` accumulates
|
|
178
|
+
* across rungs, keyed by `questionId` -- once a question is in this map its
|
|
179
|
+
* answer is FINAL for this read: the next rung's request omits it (rule 4,
|
|
180
|
+
* "questions already answered validly are NOT re-asked"), and any further
|
|
181
|
+
* offer for it from any rung -- including a non-compliant transport that
|
|
182
|
+
* answers a question it was not asked -- is rejected as `duplicate-answer`
|
|
183
|
+
* rather than silently overwriting the first accepted answer. That is what
|
|
184
|
+
* makes duplicate detection uniform across "two offers in one rung's
|
|
185
|
+
* response" and "a later rung re-offers an already-answered question":
|
|
186
|
+
* both are the same check, `accepted.has(questionId)`, evaluated at the
|
|
187
|
+
* moment each offer is processed.
|
|
188
|
+
*/
|
|
189
|
+
async function runLadder(questions, transports, attemptsPerTransport, sources) {
|
|
190
|
+
const questionsById = new Map(questions.map((q) => [q.id, q]));
|
|
191
|
+
const sourcesById = new Map(sources.map((s) => [s.id, s]));
|
|
192
|
+
const accepted = new Map();
|
|
193
|
+
const rejectedByQuestion = new Map();
|
|
194
|
+
const unmatched = [];
|
|
195
|
+
function reject(reason, rung, offer) {
|
|
196
|
+
const row = { reason, rung, offer };
|
|
197
|
+
const question = questionsById.get(offer.questionId);
|
|
198
|
+
if (!question) {
|
|
199
|
+
unmatched.push(row);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const existing = rejectedByQuestion.get(offer.questionId);
|
|
203
|
+
if (existing) {
|
|
204
|
+
existing.push(row);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
rejectedByQuestion.set(offer.questionId, [row]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (let rung = 0; rung < transports.length; rung++) {
|
|
211
|
+
const remaining = questions.filter((q) => !accepted.has(q.id));
|
|
212
|
+
if (remaining.length === 0)
|
|
213
|
+
break; // every question already answered -- nothing left for any further rung
|
|
214
|
+
const transport = transports[rung];
|
|
215
|
+
let answers = null;
|
|
216
|
+
for (let attempt = 0; attempt < attemptsPerTransport; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
const result = await transport({ questions: remaining, sources });
|
|
219
|
+
if (Array.isArray(result)) {
|
|
220
|
+
answers = result;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
// Unusable output -- treated identically to a throw: retry within
|
|
224
|
+
// this rung's budget, then fall through to the next rung.
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Threw or rejected -- retry within this rung's budget, then fall
|
|
228
|
+
// through to the next rung. The engine does not distinguish WHY a
|
|
229
|
+
// rung failed; it only advances.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (answers === null)
|
|
233
|
+
continue; // rung exhausted; the next rung sees the same `remaining` set
|
|
234
|
+
for (const offer of answers) {
|
|
235
|
+
const question = questionsById.get(offer.questionId);
|
|
236
|
+
if (!question) {
|
|
237
|
+
reject("unknown-question", rung, offer);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (accepted.has(offer.questionId)) {
|
|
241
|
+
reject("duplicate-answer", rung, offer);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!question.answerKeys.includes(offer.answerKey)) {
|
|
245
|
+
// Coercion to keys that ACTUALLY exist -- exact membership or
|
|
246
|
+
// nothing (rule 2). No fuzzy match, no case fold, no "nearest key".
|
|
247
|
+
reject("unknown-answer-key", rung, offer);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const citationProblem = checkCitation(offer.citation, sourcesById);
|
|
251
|
+
if (citationProblem) {
|
|
252
|
+
reject(citationProblem, rung, offer);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
accepted.set(offer.questionId, {
|
|
256
|
+
answerKey: offer.answerKey,
|
|
257
|
+
rung,
|
|
258
|
+
citation: { sourceId: offer.citation.sourceId, quote: offer.citation.quote },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const answersOut = questions.map((question) => {
|
|
263
|
+
const rejected = rejectedByQuestion.get(question.id) ?? [];
|
|
264
|
+
const win = accepted.get(question.id);
|
|
265
|
+
if (win) {
|
|
266
|
+
return {
|
|
267
|
+
questionId: question.id,
|
|
268
|
+
answerKey: win.answerKey,
|
|
269
|
+
fromSafeDefault: false,
|
|
270
|
+
answeredByRung: win.rung,
|
|
271
|
+
citation: win.citation,
|
|
272
|
+
rejected,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// Every rung that could answer this question was exhausted (or none
|
|
276
|
+
// were ever registered) -- the caller's own declared safe direction,
|
|
277
|
+
// never the engine's guess (rule 3).
|
|
278
|
+
return {
|
|
279
|
+
questionId: question.id,
|
|
280
|
+
answerKey: question.safeDefault,
|
|
281
|
+
fromSafeDefault: true,
|
|
282
|
+
answeredByRung: null,
|
|
283
|
+
citation: null,
|
|
284
|
+
rejected,
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
return { answers: answersOut, unmatched };
|
|
288
|
+
}
|
package/dist/register/batch.js
CHANGED
|
@@ -2,11 +2,15 @@ import { z } from "zod";
|
|
|
2
2
|
import * as characterTools from "../tools/character.js";
|
|
3
3
|
import * as worldTools from "../tools/world.js";
|
|
4
4
|
import * as inventoryTools from "../tools/inventory.js";
|
|
5
|
-
import * as combatTools from "../tools/combat.js";
|
|
6
5
|
import * as narrativeTools from "../tools/narrative.js";
|
|
7
6
|
import { LIMITS } from "../utils/validation.js";
|
|
8
7
|
import { ANNOTATIONS } from "../utils/tool-annotations.js";
|
|
9
8
|
import { imageGenSchema, voiceSchema } from "../schemas/index.js";
|
|
9
|
+
// Multi-entity workflow tools over entity/property concepts -- characters,
|
|
10
|
+
// locations, inventory. `setup_combat_encounter` used to live here too, but
|
|
11
|
+
// it is the one tool in this file that reaches into combat (design §8, issue
|
|
12
|
+
// #17), so it moved to `src/rpg/register/batch.ts`'s `registerRpgBatchTools`
|
|
13
|
+
// and this file no longer imports combat at all.
|
|
10
14
|
export function registerBatchTools(server) {
|
|
11
15
|
// ============================================================================
|
|
12
16
|
// BATCH CREATE NPCS - Create multiple NPCs at once
|
|
@@ -96,84 +100,6 @@ export function registerBatchTools(server) {
|
|
|
96
100
|
};
|
|
97
101
|
});
|
|
98
102
|
// ============================================================================
|
|
99
|
-
// SETUP COMBAT ENCOUNTER - Create NPCs, start combat with all participants
|
|
100
|
-
// ============================================================================
|
|
101
|
-
server.registerTool("setup_combat_encounter", {
|
|
102
|
-
description: "Complete combat setup in one call: creates enemy NPCs and starts combat with all participants (enemies + players at location). Returns the ready-to-play combat state.",
|
|
103
|
-
inputSchema: {
|
|
104
|
-
gameId: z.string().describe("The game ID"),
|
|
105
|
-
locationId: z.string().describe("Location where combat takes place"),
|
|
106
|
-
enemies: z
|
|
107
|
-
.array(z.object({
|
|
108
|
-
name: z.string().min(1).max(LIMITS.NAME_MAX).describe("Enemy name"),
|
|
109
|
-
attributes: z.record(z.string(), z.number()).optional(),
|
|
110
|
-
status: z
|
|
111
|
-
.object({
|
|
112
|
-
health: z.number().optional(),
|
|
113
|
-
maxHealth: z.number().optional(),
|
|
114
|
-
})
|
|
115
|
-
.optional(),
|
|
116
|
-
}))
|
|
117
|
-
.min(1)
|
|
118
|
-
.max(10)
|
|
119
|
-
.describe("Enemies to create and add to combat"),
|
|
120
|
-
includePlayersAtLocation: z
|
|
121
|
-
.boolean()
|
|
122
|
-
.default(true)
|
|
123
|
-
.describe("Auto-add player characters at this location"),
|
|
124
|
-
},
|
|
125
|
-
annotations: ANNOTATIONS.CREATE,
|
|
126
|
-
}, async ({ gameId, locationId, enemies, includePlayersAtLocation = true }) => {
|
|
127
|
-
// 1. Create enemy NPCs
|
|
128
|
-
const createdEnemies = [];
|
|
129
|
-
for (const enemy of enemies) {
|
|
130
|
-
const character = characterTools.createCharacter({
|
|
131
|
-
gameId,
|
|
132
|
-
name: enemy.name,
|
|
133
|
-
isPlayer: false,
|
|
134
|
-
attributes: enemy.attributes,
|
|
135
|
-
status: enemy.status,
|
|
136
|
-
locationId,
|
|
137
|
-
});
|
|
138
|
-
createdEnemies.push(character);
|
|
139
|
-
}
|
|
140
|
-
// 2. Get players at location if needed
|
|
141
|
-
const participantIds = [...createdEnemies.map((e) => e.id)];
|
|
142
|
-
if (includePlayersAtLocation) {
|
|
143
|
-
const playersAtLocation = characterTools.listCharacters(gameId, {
|
|
144
|
-
isPlayer: true,
|
|
145
|
-
locationId,
|
|
146
|
-
});
|
|
147
|
-
participantIds.push(...playersAtLocation.map((p) => p.id));
|
|
148
|
-
}
|
|
149
|
-
// 3. Start combat with all participants
|
|
150
|
-
const combat = combatTools.startCombat({
|
|
151
|
-
gameId,
|
|
152
|
-
locationId,
|
|
153
|
-
participantIds,
|
|
154
|
-
});
|
|
155
|
-
// 4. Log the encounter start
|
|
156
|
-
narrativeTools.logEvent({
|
|
157
|
-
gameId,
|
|
158
|
-
eventType: "combat",
|
|
159
|
-
content: `Combat begins! ${createdEnemies.length} enemies attack.`,
|
|
160
|
-
metadata: {
|
|
161
|
-
enemyIds: createdEnemies.map((e) => e.id),
|
|
162
|
-
locationId,
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
const result = {
|
|
166
|
-
combat,
|
|
167
|
-
createdEnemies: createdEnemies.map((e) => ({ id: e.id, name: e.name })),
|
|
168
|
-
participantCount: participantIds.length,
|
|
169
|
-
summary: `Combat started with ${participantIds.length} participants. ${createdEnemies.length} enemies created.`,
|
|
170
|
-
};
|
|
171
|
-
return {
|
|
172
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
173
|
-
structuredContent: result,
|
|
174
|
-
};
|
|
175
|
-
});
|
|
176
|
-
// ============================================================================
|
|
177
103
|
// SCENE TRANSITION - Move characters, log event
|
|
178
104
|
// ============================================================================
|
|
179
105
|
server.registerTool("scene_transition", {
|
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export declare const createGameListCallback: (suffix: string, nameFormatter: (g: {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}) => string) => () => Promise<{
|
|
6
|
+
resources: {
|
|
7
|
+
uri: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}[];
|
|
10
|
+
}>;
|
|
2
11
|
export declare function registerMcpResources(server: McpServer): void;
|