@rsconcept/rstool 0.10.2 → 1.0.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.
Files changed (112) hide show
  1. package/README.md +41 -31
  2. package/dist/agent-workflow-D-PSIb-m.d.ts +70 -0
  3. package/dist/analysis-LLnPhmGa.d.ts +23 -0
  4. package/dist/{common-DxLg3eXX.d.ts → common-DHJalS-Q.d.ts} +6 -1
  5. package/dist/constituenta-DnGR6bnM.d.ts +54 -0
  6. package/dist/diagnostic-D9yl_mEL.d.ts +19 -0
  7. package/dist/evaluation-Cns8BFm4.d.ts +31 -0
  8. package/dist/index.d.ts +11 -11
  9. package/dist/index.js +1 -1
  10. package/dist/mappers/model-adapter.d.ts +3 -3
  11. package/dist/mappers/schema-adapter.d.ts +4 -4
  12. package/dist/mappers/types.d.ts +6 -2
  13. package/dist/mappers/types.js +2 -0
  14. package/dist/mappers/types.js.map +1 -1
  15. package/dist/{model-value-SFAVj0dw.d.ts → model-value-BbonPzMz.d.ts} +14 -3
  16. package/dist/models/agent-workflow.d.ts +2 -0
  17. package/dist/models/agent-workflow.js +1 -0
  18. package/dist/models/analysis.d.ts +1 -1
  19. package/dist/models/common.d.ts +1 -1
  20. package/dist/models/constituenta.d.ts +2 -2
  21. package/dist/models/diagnostic.d.ts +1 -1
  22. package/dist/models/evaluation.d.ts +2 -2
  23. package/dist/models/index.d.ts +11 -11
  24. package/dist/models/index.js +2 -2
  25. package/dist/models/model-value.d.ts +2 -2
  26. package/dist/models/rstool-agent.d.ts +1 -1
  27. package/dist/models/rstool-agent.js +1 -1
  28. package/dist/models/session.d.ts +1 -1
  29. package/dist/models/tool-contract.d.ts +2 -2
  30. package/dist/models/tool-contract.js +2 -1
  31. package/dist/models/tool-contract.js.map +1 -1
  32. package/dist/rstool-agent-_8bplZnb.d.ts +71 -0
  33. package/dist/rstool-agent-kijHA9ML.js +476 -0
  34. package/dist/rstool-agent-kijHA9ML.js.map +1 -0
  35. package/dist/session/session-store.d.ts +18 -5
  36. package/dist/session/session-store.js +1 -64
  37. package/dist/{session-BPgsE80c.d.ts → session-ChexW8i7.d.ts} +11 -8
  38. package/dist/session-store-C3jyOSqI.js +142 -0
  39. package/dist/session-store-C3jyOSqI.js.map +1 -0
  40. package/dist/tool-contract-5_Q44DGE.d.ts +164 -0
  41. package/dist/wrapper/client.d.ts +23 -0
  42. package/dist/wrapper/client.js +17 -0
  43. package/dist/wrapper/client.js.map +1 -1
  44. package/dist/wrapper/stdio-wrapper.js +62 -52
  45. package/dist/wrapper/stdio-wrapper.js.map +1 -1
  46. package/docs/CONSTITUENTA.md +2 -2
  47. package/docs/DIAGNOSTICS.md +20 -18
  48. package/docs/MODEL-TESTING.md +3 -3
  49. package/docs/PORTAL-API.md +24 -18
  50. package/examples/README.md +1 -1
  51. package/examples/agent-client.ts +11 -41
  52. package/examples/build-chocolate-nim-rsform.ts +23 -18
  53. package/examples/chocolate-nim/build-rsform.ts +23 -18
  54. package/examples/chocolate-nim/build-rsmodel.ts +10 -12
  55. package/examples/chocolate-nim/rsform-session.json +290 -290
  56. package/examples/chocolate-nim/rsmodel-session.json +291 -291
  57. package/examples/expression-bank/bank-constituents.ts +304 -53
  58. package/examples/expression-bank/build-rsform.ts +19 -16
  59. package/examples/expression-bank/rsform-session.json +1551 -1551
  60. package/examples/kinship/build-rsform.ts +23 -18
  61. package/examples/kinship/build-rsmodel.ts +13 -15
  62. package/examples/kinship/rsform-session.json +219 -219
  63. package/examples/kinship/rsmodel-session.json +221 -221
  64. package/examples/kinship/session.ts +19 -21
  65. package/examples/movd/build-rsform.ts +23 -18
  66. package/examples/movd/build-rsmodel.ts +18 -20
  67. package/examples/movd/rsform-session.json +262 -262
  68. package/examples/movd/rsmodel-session.json +264 -264
  69. package/examples/sample/build-rsform.ts +19 -50
  70. package/examples/sample/build-rsmodel.ts +25 -44
  71. package/examples/sample/rsform-session.json +36 -33
  72. package/examples/sample/rsmodel-session.json +36 -33
  73. package/examples/template-apply/build-rsform.ts +27 -24
  74. package/examples/template-apply/rsform-session.json +48 -48
  75. package/package.json +3 -3
  76. package/skills/rstool-helper/EXAMPLES.md +44 -116
  77. package/skills/rstool-helper/GUIDE.md +40 -25
  78. package/skills/rstool-helper/REFERENCE.md +40 -177
  79. package/src/index.ts +24 -17
  80. package/src/mappers/portal-adapter.ts +43 -0
  81. package/src/mappers/types.ts +4 -0
  82. package/src/models/agent-workflow.ts +78 -0
  83. package/src/models/analysis.ts +7 -0
  84. package/src/models/common.ts +7 -0
  85. package/src/models/constituenta.ts +24 -6
  86. package/src/models/diagnostic.ts +4 -0
  87. package/src/models/evaluation.ts +11 -0
  88. package/src/models/import-detect.ts +39 -0
  89. package/src/models/import-export.ts +24 -0
  90. package/src/models/index.ts +22 -14
  91. package/src/models/model-value.ts +12 -0
  92. package/src/models/portal-json.ts +44 -0
  93. package/src/models/rstool-agent.test.ts +300 -147
  94. package/src/models/rstool-agent.ts +350 -93
  95. package/src/models/session.ts +8 -5
  96. package/src/models/tool-contract.ts +81 -42
  97. package/src/session/batch-apply.test.ts +28 -0
  98. package/src/session/batch-apply.ts +47 -0
  99. package/src/session/persistence.ts +56 -0
  100. package/src/session/session-store.ts +67 -4
  101. package/src/wrapper/client.ts +23 -0
  102. package/src/wrapper/stdio-wrapper.ts +59 -49
  103. package/dist/analysis-JiwOYDKx.d.ts +0 -16
  104. package/dist/constituenta-Dnd6iToB.d.ts +0 -36
  105. package/dist/diagnostic-BMYvciz8.d.ts +0 -15
  106. package/dist/evaluation-CCVYH0wA.d.ts +0 -21
  107. package/dist/index-uhkmwruf.d.ts +0 -46
  108. package/dist/rstool-agent-BZi5jO1y.js +0 -158
  109. package/dist/rstool-agent-BZi5jO1y.js.map +0 -1
  110. package/dist/rstool-agent-pRaPnZay.d.ts +0 -35
  111. package/dist/session/session-store.js.map +0 -1
  112. package/dist/tool-contract-n1ghUOrK.d.ts +0 -32
@@ -1,49 +1,88 @@
1
- import { type AnalysisResult, type AnalyzeExpressionInput } from './analysis';
2
- import {
3
- type AddOrUpdateConstituentaInput,
4
- type AddOrUpdateConstituentaResult
5
- } from './constituenta';
6
- import {
7
- type DiagnosticRecord,
8
- type ListDiagnosticsFilters
9
- } from './diagnostic';
10
1
  import {
11
- type EvaluateConstituentaInput,
12
- type EvaluateExpressionInput,
13
- type EvaluationResult
14
- } from './evaluation';
15
- import {
16
- type ClearConstituentaValuesInput,
17
- type RecalculateModelResult,
18
- type SessionModelState,
19
- type SetConstituentaValueInput,
20
- type SetConstituentaValuesInput
21
- } from './model-value';
22
- import {
23
- type SessionHandle,
24
- type SessionRevision,
25
- type SessionState
26
- } from './session';
2
+ type ApplySchemaPatchInput,
3
+ type ApplySchemaPatchResult,
4
+ type SessionStateDetail,
5
+ type SessionStateResult
6
+ } from './agent-workflow';
7
+ import { type AnalysisResult, type AnalyzeExpressionInput } from './analysis';
8
+ import { type DiagnosticRecord, type ListDiagnosticsFilters } from './diagnostic';
9
+ import { type EvaluateInput, type EvaluationResult } from './evaluation';
10
+ import { type ExportPortalInput, type ExportPortalResult, type ImportDataKind } from './import-export';
11
+ import { type RecalculateModelResult, type SessionModelState, type SetModelValuesInput } from './model-value';
12
+ import { type SessionHandle, type SessionRevision, type SessionState } from './session';
27
13
 
28
- export const CONTRACT_VERSION = '1.4.0';
14
+ /** Agent-visible contract version; bump on breaking API changes. */
15
+ export const CONTRACT_VERSION = '2.0.0';
29
16
 
17
+ /** Options for constructing an {@link RSToolAgent}. */
18
+ export interface RSToolAgentOptions {
19
+ /** When set, sessions are persisted to this directory and survive process restarts. */
20
+ persistenceDir?: string;
21
+ }
22
+
23
+ /**
24
+ * Public method surface of {@link RSToolAgent}.
25
+ *
26
+ * Each method accepts an optional `sessionId`; when omitted, the current session is used
27
+ * (or a new one is created where noted).
28
+ */
30
29
  export interface RSToolAgentContract {
30
+ /** Current contract version string (same as {@link CONTRACT_VERSION}). */
31
31
  readonly contractVersion: string;
32
+
33
+ /** Return the current session, or create one with optional `initial` metadata. */
34
+ ensureSession(initial?: Partial<SessionState>): SessionHandle;
35
+
36
+ /** Create a new session and make it current. */
32
37
  createSession(initial?: Partial<SessionState>): SessionHandle;
33
- addOrUpdateConstituenta(sessionId: string, input: AddOrUpdateConstituentaInput): AddOrUpdateConstituentaResult;
34
- analyzeExpression(sessionId: string, input: AnalyzeExpressionInput): AnalysisResult;
35
- getFormState(sessionId: string): SessionState;
36
- listDiagnostics(sessionId: string, filters?: ListDiagnosticsFilters): DiagnosticRecord[];
37
- commitStep(sessionId: string, message?: string): SessionRevision;
38
- exportSession(sessionId: string): string;
39
- exportPortalSchema(sessionId: string): string;
40
- exportPortalModel(sessionId: string): string;
41
- importSession(payload: string): SessionHandle;
42
- setConstituentaValue(sessionId: string, input: SetConstituentaValueInput): Promise<SessionModelState>;
43
- setConstituentaValues(sessionId: string, input: SetConstituentaValuesInput): Promise<SessionModelState>;
44
- clearConstituentaValues(sessionId: string, input: ClearConstituentaValuesInput): Promise<SessionModelState>;
45
- getModelState(sessionId: string): SessionModelState;
46
- evaluateExpression(sessionId: string, input: EvaluateExpressionInput): EvaluationResult;
47
- evaluateConstituenta(sessionId: string, input: EvaluateConstituentaInput): EvaluationResult;
48
- recalculateModel(sessionId: string): RecalculateModelResult;
38
+
39
+ /** Return the current session handle, or `null` when none is active. */
40
+ getCurrentSession(): SessionHandle | null;
41
+
42
+ /** Switch the active session; throws when `sessionId` is unknown. */
43
+ setCurrentSession(sessionId: string): SessionHandle;
44
+
45
+ /** Apply constituent patches to the schema (analyze, merge, optionally commit). */
46
+ applySchemaPatch(input: ApplySchemaPatchInput, sessionId?: string): ApplySchemaPatchResult;
47
+
48
+ /** Return session summary (default) or full cloned state when `detail` is `'full'`. */
49
+ getSessionState(detail?: SessionStateDetail, sessionId?: string): SessionStateResult;
50
+
51
+ /** List diagnostics for the session, optionally filtered by constituent. */
52
+ listDiagnostics(filters?: ListDiagnosticsFilters, sessionId?: string): DiagnosticRecord[];
53
+
54
+ /** Parse and type-check an RSLang expression against the current schema context. */
55
+ analyzeExpression(input: AnalyzeExpressionInput, sessionId?: string): AnalysisResult;
56
+
57
+ /** Record a revision checkpoint with an optional message. */
58
+ commitStep(message?: string, sessionId?: string): SessionRevision;
59
+
60
+ /** Export the session as a JSON string (state + diagnostics). */
61
+ exportSession(sessionId?: string): string;
62
+
63
+ /** Export schema or model payload in Portal JSON format. */
64
+ exportPortal(input: ExportPortalInput, sessionId?: string): ExportPortalResult;
65
+
66
+ /**
67
+ * Import a session export or Portal JSON payload and make the new session current.
68
+ *
69
+ * When `kind` is `'auto'`, the payload shape is detected automatically.
70
+ */
71
+ importData(payload: string | object, kind?: ImportDataKind): SessionHandle;
72
+
73
+ /** Set or clear model values for constituents; returns the updated model state. */
74
+ setModelValues(input: SetModelValuesInput, sessionId?: string): Promise<SessionModelState>;
75
+
76
+ /** Return a deep clone of the session model state. */
77
+ getModelState(sessionId?: string): SessionModelState;
78
+
79
+ /**
80
+ * Evaluate a stored constituent or a scratch expression.
81
+ *
82
+ * Provide `constituentId`, or both `expression` and `cstType`.
83
+ */
84
+ evaluate(input: EvaluateInput, sessionId?: string): EvaluationResult;
85
+
86
+ /** Recompute derived model values for all constituents. */
87
+ recalculateModel(sessionId?: string): RecalculateModelResult;
49
88
  }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { CstType } from '../models';
4
+ import { orderDrafts } from './batch-apply';
5
+
6
+ describe('orderDrafts', () => {
7
+ it('orders dependents after suppliers', () => {
8
+ const ordered = orderDrafts(
9
+ [
10
+ {
11
+ id: 1,
12
+ alias: 'X1',
13
+ cstType: CstType.BASE,
14
+ definitionFormal: '',
15
+ term: '',
16
+ definitionText: '',
17
+ convention: '',
18
+ analysis: { success: true, type: null, valueClass: 'value', diagnostics: [] }
19
+ }
20
+ ],
21
+ [
22
+ { id: 3, alias: 'D1', cstType: CstType.TERM, definitionFormal: 'Pr1(S1)' },
23
+ { id: 2, alias: 'S1', cstType: CstType.STRUCTURED, definitionFormal: 'ℬ(X1×X1)' }
24
+ ]
25
+ );
26
+ expect(ordered.map(draft => draft.alias)).toEqual(['S1', 'D1']);
27
+ });
28
+ });
@@ -0,0 +1,47 @@
1
+ import { Graph } from '@rsconcept/domain/graph/graph';
2
+ import { extractGlobals } from '@rsconcept/domain/rslang/api';
3
+
4
+ import { type ConstituentaDraft, type ConstituentaState } from '../models';
5
+
6
+ /** Order drafts so suppliers are applied before dependents. */
7
+ export function orderDrafts(sessionItems: ConstituentaState[], drafts: ConstituentaDraft[]): ConstituentaDraft[] {
8
+ const merged = new Map<number, ConstituentaDraft>();
9
+ for (const item of sessionItems) {
10
+ merged.set(item.id, {
11
+ id: item.id,
12
+ alias: item.alias,
13
+ cstType: item.cstType,
14
+ definitionFormal: item.definitionFormal
15
+ });
16
+ }
17
+ for (const draft of drafts) {
18
+ merged.set(draft.id, draft);
19
+ }
20
+
21
+ const graph = new Graph<number>();
22
+ const aliasToId = new Map<string, number>();
23
+ for (const [id, draft] of merged) {
24
+ graph.addNode(id);
25
+ aliasToId.set(draft.alias, id);
26
+ }
27
+
28
+ for (const [id, draft] of merged) {
29
+ if (!draft.definitionFormal) {
30
+ continue;
31
+ }
32
+ for (const alias of extractGlobals(draft.definitionFormal)) {
33
+ const depId = aliasToId.get(alias);
34
+ if (depId !== undefined && depId !== id) {
35
+ graph.addEdge(depId, id);
36
+ }
37
+ }
38
+ }
39
+
40
+ const draftIds = new Set(drafts.map(draft => draft.id));
41
+ const topoIds = graph.topologicalOrder().filter(id => draftIds.has(id));
42
+ const seen = new Set(topoIds);
43
+ const missing = drafts.filter(draft => !seen.has(draft.id)).map(draft => draft.id);
44
+
45
+ const orderedIds = [...topoIds, ...missing];
46
+ return orderedIds.map(id => drafts.find(draft => draft.id === id)!).filter(Boolean);
47
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { type DiagnosticRecord, type SessionState } from '../models';
5
+
6
+ export interface PersistedSessionEnvelope {
7
+ state: SessionState;
8
+ diagnostics: DiagnosticRecord[];
9
+ }
10
+
11
+ const CURRENT_SESSION_FILE = '_current.json';
12
+
13
+ export class SessionPersistence {
14
+ private readonly dir: string;
15
+
16
+ public constructor(dir: string) {
17
+ this.dir = dir;
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+
21
+ public save(sessionId: string, envelope: PersistedSessionEnvelope): void {
22
+ fs.writeFileSync(this.filePath(sessionId), JSON.stringify(envelope, null, 2), 'utf-8');
23
+ }
24
+
25
+ public load(sessionId: string): PersistedSessionEnvelope | null {
26
+ const file = this.filePath(sessionId);
27
+ if (!fs.existsSync(file)) {
28
+ return null;
29
+ }
30
+ return JSON.parse(fs.readFileSync(file, 'utf-8')) as PersistedSessionEnvelope;
31
+ }
32
+
33
+ public delete(sessionId: string): void {
34
+ const file = this.filePath(sessionId);
35
+ if (fs.existsSync(file)) {
36
+ fs.unlinkSync(file);
37
+ }
38
+ }
39
+
40
+ public saveCurrentSessionId(sessionId: string | null): void {
41
+ fs.writeFileSync(path.join(this.dir, CURRENT_SESSION_FILE), JSON.stringify({ sessionId }, null, 2), 'utf-8');
42
+ }
43
+
44
+ public loadCurrentSessionId(): string | null {
45
+ const file = path.join(this.dir, CURRENT_SESSION_FILE);
46
+ if (!fs.existsSync(file)) {
47
+ return null;
48
+ }
49
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8')) as { sessionId?: string | null };
50
+ return parsed.sessionId ?? null;
51
+ }
52
+
53
+ private filePath(sessionId: string): string {
54
+ return path.join(this.dir, `${sessionId}.json`);
55
+ }
56
+ }
@@ -7,6 +7,11 @@ import {
7
7
  type SessionRevision,
8
8
  type SessionState
9
9
  } from '../models';
10
+ import { SessionPersistence, type PersistedSessionEnvelope } from './persistence';
11
+
12
+ export interface SessionStoreOptions {
13
+ persistenceDir?: string;
14
+ }
10
15
 
11
16
  interface SessionEnvelope {
12
17
  state: SessionState;
@@ -15,6 +20,11 @@ interface SessionEnvelope {
15
20
 
16
21
  export class SessionStore {
17
22
  private sessions = new Map<string, SessionEnvelope>();
23
+ private readonly persistence: SessionPersistence | null;
24
+
25
+ public constructor(options: SessionStoreOptions = {}) {
26
+ this.persistence = options.persistenceDir ? new SessionPersistence(options.persistenceDir) : null;
27
+ }
18
28
 
19
29
  public create(initial?: Partial<SessionState>, contractVersion?: string): SessionHandle {
20
30
  const now = new Date().toISOString();
@@ -30,10 +40,12 @@ export class SessionStore {
30
40
  items: initial?.items ?? [],
31
41
  model: initial?.model ?? { items: [] }
32
42
  };
33
- this.sessions.set(sessionId, {
43
+ const envelope: SessionEnvelope = {
34
44
  state,
35
45
  diagnostics: []
36
- });
46
+ };
47
+ this.sessions.set(sessionId, envelope);
48
+ this.persist(sessionId, envelope);
37
49
  return {
38
50
  sessionId,
39
51
  contractVersion: contractVersion ?? '1.0.0'
@@ -41,19 +53,24 @@ export class SessionStore {
41
53
  }
42
54
 
43
55
  public get(sessionId: string): SessionEnvelope {
44
- const found = this.sessions.get(sessionId);
56
+ const found = this.sessions.get(sessionId) ?? this.loadFromDisk(sessionId);
45
57
  if (!found) {
46
58
  throw new Error(`Unknown session: ${sessionId}`);
47
59
  }
48
60
  return found;
49
61
  }
50
62
 
63
+ public has(sessionId: string): boolean {
64
+ return this.sessions.has(sessionId) || this.persistence?.load(sessionId) !== null;
65
+ }
66
+
51
67
  public replaceState(sessionId: string, nextState: SessionState): void {
52
68
  const found = this.get(sessionId);
53
69
  found.state = {
54
70
  ...nextState,
55
71
  updatedAt: new Date().toISOString()
56
72
  };
73
+ this.persist(sessionId, found);
57
74
  }
58
75
 
59
76
  public addRevision(sessionId: string, message?: string): SessionRevision {
@@ -65,13 +82,28 @@ export class SessionStore {
65
82
  };
66
83
  found.state.revisions.push(revision);
67
84
  found.state.updatedAt = revision.at;
85
+ this.persist(sessionId, found);
68
86
  return revision;
69
87
  }
70
88
 
71
- public appendDiagnostics(sessionId: string, records: DiagnosticRecord[]): void {
89
+ /** Replace active diagnostics for one constituent (or scratch when constituentId is undefined). */
90
+ public replaceDiagnosticsForConstituent(
91
+ sessionId: string,
92
+ constituentId: number | undefined,
93
+ records: DiagnosticRecord[]
94
+ ): void {
72
95
  const found = this.get(sessionId);
96
+ found.diagnostics = found.diagnostics.filter(record => record.constituentId !== constituentId);
73
97
  found.diagnostics.push(...records);
74
98
  found.state.updatedAt = new Date().toISOString();
99
+ this.persist(sessionId, found);
100
+ }
101
+
102
+ public setDiagnostics(sessionId: string, records: DiagnosticRecord[]): void {
103
+ const found = this.get(sessionId);
104
+ found.diagnostics = [...records];
105
+ found.state.updatedAt = new Date().toISOString();
106
+ this.persist(sessionId, found);
75
107
  }
76
108
 
77
109
  public listDiagnostics(sessionId: string, filters?: ListDiagnosticsFilters): DiagnosticRecord[] {
@@ -81,4 +113,35 @@ export class SessionStore {
81
113
  }
82
114
  return found.diagnostics.filter(record => record.constituentId === filters.constituentId);
83
115
  }
116
+
117
+ public snapshot(sessionId: string): SessionEnvelope {
118
+ const found = this.get(sessionId);
119
+ return structuredClone(found);
120
+ }
121
+
122
+ public restore(sessionId: string, snapshot: SessionEnvelope): void {
123
+ this.sessions.set(sessionId, structuredClone(snapshot));
124
+ this.persist(sessionId, this.sessions.get(sessionId)!);
125
+ }
126
+
127
+ public saveCurrentSessionId(sessionId: string | null): void {
128
+ this.persistence?.saveCurrentSessionId(sessionId);
129
+ }
130
+
131
+ public loadCurrentSessionId(): string | null {
132
+ return this.persistence?.loadCurrentSessionId() ?? null;
133
+ }
134
+
135
+ private loadFromDisk(sessionId: string): SessionEnvelope | null {
136
+ const loaded = this.persistence?.load(sessionId);
137
+ if (!loaded) {
138
+ return null;
139
+ }
140
+ this.sessions.set(sessionId, loaded);
141
+ return loaded;
142
+ }
143
+
144
+ private persist(sessionId: string, envelope: SessionEnvelope): void {
145
+ this.persistence?.save(sessionId, envelope as PersistedSessionEnvelope);
146
+ }
84
147
  }
@@ -2,6 +2,7 @@ import { spawn, type ChildProcessByStdio } from 'node:child_process';
2
2
  import readline from 'node:readline';
3
3
  import { type Readable, type Writable } from 'node:stream';
4
4
 
5
+ /** One JSON line emitted by the stdio wrapper (request response or ready event). */
5
6
  export interface WrapperResponse<T = unknown> {
6
7
  id: string | number | null;
7
8
  ok: boolean;
@@ -13,19 +14,32 @@ export interface WrapperResponse<T = unknown> {
13
14
  };
14
15
  }
15
16
 
17
+ /** Options for spawning the `rstool-wrapper` child process. */
16
18
  export interface RSToolWrapperClientOptions {
19
+ /** Executable to spawn. Default: `npm`. */
17
20
  command?: string;
21
+ /** Arguments passed to `command`. Default: `['run', 'wrapper']`. */
18
22
  args?: string[];
23
+ /** Working directory for the child process. Default: `process.cwd()`. */
19
24
  cwd?: string;
25
+ /** Whether to run the command in a shell. Default: `true`. */
20
26
  shell?: boolean;
21
27
  }
22
28
 
29
+ /**
30
+ * JSON-RPC client for the `rstool-wrapper` stdio process.
31
+ *
32
+ * Sends one JSON request per line on stdin and reads one JSON response per line from stdout.
33
+ */
23
34
  export class RSToolWrapperClient {
24
35
  private process: ChildProcessByStdio<Writable, Readable, null>;
25
36
  private input: readline.Interface;
26
37
  private pending = new Map<string, (value: WrapperResponse) => void>();
27
38
  private requestCounter = 1;
28
39
 
40
+ /**
41
+ * @param options - Spawn configuration; defaults run `npm run wrapper` in the current directory.
42
+ */
29
43
  public constructor(options: RSToolWrapperClientOptions = {}) {
30
44
  this.process = spawn(options.command ?? 'npm', options.args ?? ['run', 'wrapper'], {
31
45
  cwd: options.cwd ?? process.cwd(),
@@ -39,6 +53,7 @@ export class RSToolWrapperClient {
39
53
  this.input.on('line', line => this.handleLine(line));
40
54
  }
41
55
 
56
+ /** Block until the wrapper emits its initial `{ ready: true }` event. */
42
57
  public async waitUntilReady(): Promise<void> {
43
58
  for (;;) {
44
59
  const line = await this.readOneEvent();
@@ -54,6 +69,13 @@ export class RSToolWrapperClient {
54
69
  }
55
70
  }
56
71
 
72
+ /**
73
+ * Invoke a wrapper method and return its `result` field.
74
+ *
75
+ * @param method - Stdio method name (matches {@link RSToolAgentContract} operations).
76
+ * @param params - Method parameters object.
77
+ * @throws When the wrapper responds with `ok: false`.
78
+ */
57
79
  public async call<T>(method: string, params: unknown = {}): Promise<T> {
58
80
  const id = String(this.requestCounter++);
59
81
  const payload = JSON.stringify({ id, method, params });
@@ -68,6 +90,7 @@ export class RSToolWrapperClient {
68
90
  return response.result as T;
69
91
  }
70
92
 
93
+ /** Close stdin and terminate the wrapper process. */
71
94
  public async close(): Promise<void> {
72
95
  this.input.close();
73
96
  this.process.stdin.end();
@@ -20,25 +20,25 @@ interface StdioResponse {
20
20
  };
21
21
  }
22
22
 
23
- const tool = new RSToolAgent();
23
+ const persistenceDir = process.env.RSTOOL_PERSISTENCE_DIR;
24
+ const tool = new RSToolAgent(persistenceDir ? { persistenceDir } : {});
24
25
 
25
26
  const METHODS = [
27
+ 'ensureSession',
26
28
  'createSession',
27
- 'addOrUpdateConstituenta',
28
- 'analyzeExpression',
29
- 'getFormState',
29
+ 'getCurrentSession',
30
+ 'setCurrentSession',
31
+ 'applySchemaPatch',
32
+ 'getSessionState',
30
33
  'listDiagnostics',
34
+ 'analyzeExpression',
31
35
  'commitStep',
32
36
  'exportSession',
33
- 'exportPortalSchema',
34
- 'exportPortalModel',
35
- 'importSession',
36
- 'setConstituentaValue',
37
- 'setConstituentaValues',
38
- 'clearConstituentaValues',
37
+ 'exportPortal',
38
+ 'importData',
39
+ 'setModelValues',
39
40
  'getModelState',
40
- 'evaluateExpression',
41
- 'evaluateConstituenta',
41
+ 'evaluate',
42
42
  'recalculateModel'
43
43
  ] as const;
44
44
 
@@ -68,115 +68,125 @@ function requiredString(input: Record<string, unknown>, key: string): string {
68
68
  return value;
69
69
  }
70
70
 
71
+ function optionalSessionId(input: Record<string, unknown>): string | undefined {
72
+ const value = input.sessionId;
73
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
74
+ }
75
+
76
+ function omitSessionId(input: Record<string, unknown>): Record<string, unknown> {
77
+ const { sessionId: _sessionId, ...rest } = input;
78
+ return rest;
79
+ }
80
+
71
81
  async function handleRequest(request: StdioRequest): Promise<StdioResponse> {
72
82
  try {
73
83
  const params = asObject(request.params);
74
84
  switch (request.method) {
75
85
  case 'ping':
76
- return { id: request.id, ok: true, result: { pong: true } };
86
+ return { id: request.id, ok: true, result: { pong: true, contractVersion: tool.contractVersion } };
77
87
  case 'methods':
78
88
  return { id: request.id, ok: true, result: METHODS };
79
- case 'createSession':
89
+ case 'ensureSession':
80
90
  return {
81
91
  id: request.id,
82
92
  ok: true,
83
- result: tool.createSession(params.initial as never)
93
+ result: tool.ensureSession(params.initial as never)
84
94
  };
85
- case 'addOrUpdateConstituenta':
95
+ case 'createSession':
86
96
  return {
87
97
  id: request.id,
88
98
  ok: true,
89
- result: tool.addOrUpdateConstituenta(requiredString(params, 'sessionId'), params.input as never)
99
+ result: tool.createSession(params.initial as never)
90
100
  };
91
- case 'analyzeExpression':
101
+ case 'getCurrentSession':
92
102
  return {
93
103
  id: request.id,
94
104
  ok: true,
95
- result: tool.analyzeExpression(requiredString(params, 'sessionId'), params.input as never)
105
+ result: tool.getCurrentSession()
96
106
  };
97
- case 'getFormState':
107
+ case 'setCurrentSession':
98
108
  return {
99
109
  id: request.id,
100
110
  ok: true,
101
- result: tool.getFormState(requiredString(params, 'sessionId'))
111
+ result: tool.setCurrentSession(requiredString(params, 'sessionId'))
102
112
  };
103
- case 'listDiagnostics':
113
+ case 'applySchemaPatch':
104
114
  return {
105
115
  id: request.id,
106
116
  ok: true,
107
- result: tool.listDiagnostics(requiredString(params, 'sessionId'), params.filters as never)
117
+ result: tool.applySchemaPatch(omitSessionId(params) as never, optionalSessionId(params))
108
118
  };
109
- case 'commitStep':
119
+ case 'getSessionState':
110
120
  return {
111
121
  id: request.id,
112
122
  ok: true,
113
- result: tool.commitStep(requiredString(params, 'sessionId'), params.message as string | undefined)
123
+ result: tool.getSessionState(
124
+ (params.detail as 'summary' | 'full' | undefined) ?? 'summary',
125
+ optionalSessionId(params)
126
+ )
114
127
  };
115
- case 'exportSession':
128
+ case 'listDiagnostics': {
129
+ const constituentId = params.constituentId;
130
+ const filters = typeof constituentId === 'number' ? { constituentId } : (params.filters as never);
116
131
  return {
117
132
  id: request.id,
118
133
  ok: true,
119
- result: tool.exportSession(requiredString(params, 'sessionId'))
134
+ result: tool.listDiagnostics(filters, optionalSessionId(params))
120
135
  };
121
- case 'exportPortalSchema':
136
+ }
137
+ case 'analyzeExpression':
122
138
  return {
123
139
  id: request.id,
124
140
  ok: true,
125
- result: tool.exportPortalSchema(requiredString(params, 'sessionId'))
141
+ result: tool.analyzeExpression(omitSessionId(params) as never, optionalSessionId(params))
126
142
  };
127
- case 'exportPortalModel':
143
+ case 'commitStep':
128
144
  return {
129
145
  id: request.id,
130
146
  ok: true,
131
- result: tool.exportPortalModel(requiredString(params, 'sessionId'))
147
+ result: tool.commitStep(params.message as string | undefined, optionalSessionId(params))
132
148
  };
133
- case 'importSession':
149
+ case 'exportSession':
134
150
  return {
135
151
  id: request.id,
136
152
  ok: true,
137
- result: tool.importSession(requiredString(params, 'payload'))
153
+ result: tool.exportSession(optionalSessionId(params))
138
154
  };
139
- case 'setConstituentaValue':
155
+ case 'exportPortal':
140
156
  return {
141
157
  id: request.id,
142
158
  ok: true,
143
- result: await tool.setConstituentaValue(requiredString(params, 'sessionId'), params.input as never)
159
+ result: tool.exportPortal(omitSessionId(params) as never, optionalSessionId(params))
144
160
  };
145
- case 'setConstituentaValues':
161
+ case 'importData':
146
162
  return {
147
163
  id: request.id,
148
164
  ok: true,
149
- result: await tool.setConstituentaValues(requiredString(params, 'sessionId'), params.input as never)
165
+ result: tool.importData(params.payload as string | object, params.kind as never)
150
166
  };
151
- case 'clearConstituentaValues':
167
+ case 'setModelValues':
152
168
  return {
153
169
  id: request.id,
154
170
  ok: true,
155
- result: await tool.clearConstituentaValues(requiredString(params, 'sessionId'), params.input as never)
171
+ result: await tool.setModelValues(omitSessionId(params) as never, optionalSessionId(params))
156
172
  };
157
173
  case 'getModelState':
158
174
  return {
159
175
  id: request.id,
160
176
  ok: true,
161
- result: tool.getModelState(requiredString(params, 'sessionId'))
162
- };
163
- case 'evaluateExpression':
164
- return {
165
- id: request.id,
166
- ok: true,
167
- result: tool.evaluateExpression(requiredString(params, 'sessionId'), params.input as never)
177
+ result: tool.getModelState(optionalSessionId(params))
168
178
  };
169
- case 'evaluateConstituenta':
179
+ case 'evaluate':
170
180
  return {
171
181
  id: request.id,
172
182
  ok: true,
173
- result: tool.evaluateConstituenta(requiredString(params, 'sessionId'), params.input as never)
183
+ result: tool.evaluate(omitSessionId(params) as never, optionalSessionId(params))
174
184
  };
175
185
  case 'recalculateModel':
176
186
  return {
177
187
  id: request.id,
178
188
  ok: true,
179
- result: tool.recalculateModel(requiredString(params, 'sessionId'))
189
+ result: tool.recalculateModel(optionalSessionId(params))
180
190
  };
181
191
  default:
182
192
  return {