@rsconcept/rstool 0.10.3 → 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.
- package/README.md +41 -31
- package/dist/agent-workflow-D-PSIb-m.d.ts +70 -0
- package/dist/analysis-LLnPhmGa.d.ts +23 -0
- package/dist/{common-DxLg3eXX.d.ts → common-DHJalS-Q.d.ts} +6 -1
- package/dist/constituenta-DnGR6bnM.d.ts +54 -0
- package/dist/diagnostic-D9yl_mEL.d.ts +19 -0
- package/dist/evaluation-Cns8BFm4.d.ts +31 -0
- package/dist/index.d.ts +11 -11
- package/dist/index.js +1 -1
- package/dist/mappers/model-adapter.d.ts +3 -3
- package/dist/mappers/schema-adapter.d.ts +4 -4
- package/dist/mappers/types.d.ts +6 -2
- package/dist/mappers/types.js +2 -0
- package/dist/mappers/types.js.map +1 -1
- package/dist/{model-value-SFAVj0dw.d.ts → model-value-BbonPzMz.d.ts} +14 -3
- package/dist/models/agent-workflow.d.ts +2 -0
- package/dist/models/agent-workflow.js +1 -0
- package/dist/models/analysis.d.ts +1 -1
- package/dist/models/common.d.ts +1 -1
- package/dist/models/constituenta.d.ts +2 -2
- package/dist/models/diagnostic.d.ts +1 -1
- package/dist/models/evaluation.d.ts +2 -2
- package/dist/models/index.d.ts +11 -11
- package/dist/models/index.js +2 -2
- package/dist/models/model-value.d.ts +2 -2
- package/dist/models/rstool-agent.d.ts +1 -1
- package/dist/models/rstool-agent.js +1 -1
- package/dist/models/session.d.ts +1 -1
- package/dist/models/tool-contract.d.ts +2 -2
- package/dist/models/tool-contract.js +2 -1
- package/dist/models/tool-contract.js.map +1 -1
- package/dist/rstool-agent-_8bplZnb.d.ts +71 -0
- package/dist/rstool-agent-kijHA9ML.js +476 -0
- package/dist/rstool-agent-kijHA9ML.js.map +1 -0
- package/dist/session/session-store.d.ts +18 -5
- package/dist/session/session-store.js +1 -64
- package/dist/{session-BPgsE80c.d.ts → session-ChexW8i7.d.ts} +11 -8
- package/dist/session-store-C3jyOSqI.js +142 -0
- package/dist/session-store-C3jyOSqI.js.map +1 -0
- package/dist/tool-contract-5_Q44DGE.d.ts +164 -0
- package/dist/wrapper/client.d.ts +23 -0
- package/dist/wrapper/client.js +17 -0
- package/dist/wrapper/client.js.map +1 -1
- package/dist/wrapper/stdio-wrapper.js +62 -52
- package/dist/wrapper/stdio-wrapper.js.map +1 -1
- package/docs/CONSTITUENTA.md +2 -2
- package/docs/DIAGNOSTICS.md +6 -5
- package/docs/MODEL-TESTING.md +3 -3
- package/docs/PORTAL-API.md +24 -18
- package/examples/README.md +1 -1
- package/examples/agent-client.ts +11 -41
- package/examples/build-chocolate-nim-rsform.ts +23 -18
- package/examples/chocolate-nim/build-rsform.ts +23 -18
- package/examples/chocolate-nim/build-rsmodel.ts +10 -12
- package/examples/chocolate-nim/rsform-session.json +290 -290
- package/examples/chocolate-nim/rsmodel-session.json +291 -291
- package/examples/expression-bank/bank-constituents.ts +304 -53
- package/examples/expression-bank/build-rsform.ts +19 -16
- package/examples/expression-bank/rsform-session.json +1551 -1551
- package/examples/kinship/build-rsform.ts +23 -18
- package/examples/kinship/build-rsmodel.ts +13 -15
- package/examples/kinship/rsform-session.json +219 -219
- package/examples/kinship/rsmodel-session.json +221 -221
- package/examples/kinship/session.ts +19 -21
- package/examples/movd/build-rsform.ts +23 -18
- package/examples/movd/build-rsmodel.ts +18 -20
- package/examples/movd/rsform-session.json +262 -262
- package/examples/movd/rsmodel-session.json +264 -264
- package/examples/sample/build-rsform.ts +19 -50
- package/examples/sample/build-rsmodel.ts +25 -44
- package/examples/sample/rsform-session.json +36 -33
- package/examples/sample/rsmodel-session.json +36 -33
- package/examples/template-apply/build-rsform.ts +27 -24
- package/examples/template-apply/rsform-session.json +48 -48
- package/package.json +2 -2
- package/skills/rstool-helper/EXAMPLES.md +44 -116
- package/skills/rstool-helper/GUIDE.md +40 -25
- package/skills/rstool-helper/REFERENCE.md +40 -177
- package/src/index.ts +24 -17
- package/src/mappers/portal-adapter.ts +43 -0
- package/src/mappers/types.ts +4 -0
- package/src/models/agent-workflow.ts +78 -0
- package/src/models/analysis.ts +7 -0
- package/src/models/common.ts +7 -0
- package/src/models/constituenta.ts +24 -6
- package/src/models/diagnostic.ts +4 -0
- package/src/models/evaluation.ts +11 -0
- package/src/models/import-detect.ts +39 -0
- package/src/models/import-export.ts +24 -0
- package/src/models/index.ts +22 -14
- package/src/models/model-value.ts +12 -0
- package/src/models/portal-json.ts +44 -0
- package/src/models/rstool-agent.test.ts +300 -147
- package/src/models/rstool-agent.ts +350 -93
- package/src/models/session.ts +8 -5
- package/src/models/tool-contract.ts +81 -42
- package/src/session/batch-apply.test.ts +28 -0
- package/src/session/batch-apply.ts +47 -0
- package/src/session/persistence.ts +56 -0
- package/src/session/session-store.ts +67 -4
- package/src/wrapper/client.ts +23 -0
- package/src/wrapper/stdio-wrapper.ts +59 -49
- package/dist/analysis-JiwOYDKx.d.ts +0 -16
- package/dist/constituenta-Dnd6iToB.d.ts +0 -36
- package/dist/diagnostic-BMYvciz8.d.ts +0 -15
- package/dist/evaluation-CCVYH0wA.d.ts +0 -21
- package/dist/index-uhkmwruf.d.ts +0 -46
- package/dist/rstool-agent-BZi5jO1y.js +0 -158
- package/dist/rstool-agent-BZi5jO1y.js.map +0 -1
- package/dist/rstool-agent-pRaPnZay.d.ts +0 -35
- package/dist/session/session-store.js.map +0 -1
- 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
|
|
12
|
-
type
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from './
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/wrapper/client.ts
CHANGED
|
@@ -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
|
|
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
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
29
|
+
'getCurrentSession',
|
|
30
|
+
'setCurrentSession',
|
|
31
|
+
'applySchemaPatch',
|
|
32
|
+
'getSessionState',
|
|
30
33
|
'listDiagnostics',
|
|
34
|
+
'analyzeExpression',
|
|
31
35
|
'commitStep',
|
|
32
36
|
'exportSession',
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'setConstituentaValue',
|
|
37
|
-
'setConstituentaValues',
|
|
38
|
-
'clearConstituentaValues',
|
|
37
|
+
'exportPortal',
|
|
38
|
+
'importData',
|
|
39
|
+
'setModelValues',
|
|
39
40
|
'getModelState',
|
|
40
|
-
'
|
|
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 '
|
|
89
|
+
case 'ensureSession':
|
|
80
90
|
return {
|
|
81
91
|
id: request.id,
|
|
82
92
|
ok: true,
|
|
83
|
-
result: tool.
|
|
93
|
+
result: tool.ensureSession(params.initial as never)
|
|
84
94
|
};
|
|
85
|
-
case '
|
|
95
|
+
case 'createSession':
|
|
86
96
|
return {
|
|
87
97
|
id: request.id,
|
|
88
98
|
ok: true,
|
|
89
|
-
result: tool.
|
|
99
|
+
result: tool.createSession(params.initial as never)
|
|
90
100
|
};
|
|
91
|
-
case '
|
|
101
|
+
case 'getCurrentSession':
|
|
92
102
|
return {
|
|
93
103
|
id: request.id,
|
|
94
104
|
ok: true,
|
|
95
|
-
result: tool.
|
|
105
|
+
result: tool.getCurrentSession()
|
|
96
106
|
};
|
|
97
|
-
case '
|
|
107
|
+
case 'setCurrentSession':
|
|
98
108
|
return {
|
|
99
109
|
id: request.id,
|
|
100
110
|
ok: true,
|
|
101
|
-
result: tool.
|
|
111
|
+
result: tool.setCurrentSession(requiredString(params, 'sessionId'))
|
|
102
112
|
};
|
|
103
|
-
case '
|
|
113
|
+
case 'applySchemaPatch':
|
|
104
114
|
return {
|
|
105
115
|
id: request.id,
|
|
106
116
|
ok: true,
|
|
107
|
-
result: tool.
|
|
117
|
+
result: tool.applySchemaPatch(omitSessionId(params) as never, optionalSessionId(params))
|
|
108
118
|
};
|
|
109
|
-
case '
|
|
119
|
+
case 'getSessionState':
|
|
110
120
|
return {
|
|
111
121
|
id: request.id,
|
|
112
122
|
ok: true,
|
|
113
|
-
result: tool.
|
|
123
|
+
result: tool.getSessionState(
|
|
124
|
+
(params.detail as 'summary' | 'full' | undefined) ?? 'summary',
|
|
125
|
+
optionalSessionId(params)
|
|
126
|
+
)
|
|
114
127
|
};
|
|
115
|
-
case '
|
|
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.
|
|
134
|
+
result: tool.listDiagnostics(filters, optionalSessionId(params))
|
|
120
135
|
};
|
|
121
|
-
|
|
136
|
+
}
|
|
137
|
+
case 'analyzeExpression':
|
|
122
138
|
return {
|
|
123
139
|
id: request.id,
|
|
124
140
|
ok: true,
|
|
125
|
-
result: tool.
|
|
141
|
+
result: tool.analyzeExpression(omitSessionId(params) as never, optionalSessionId(params))
|
|
126
142
|
};
|
|
127
|
-
case '
|
|
143
|
+
case 'commitStep':
|
|
128
144
|
return {
|
|
129
145
|
id: request.id,
|
|
130
146
|
ok: true,
|
|
131
|
-
result: tool.
|
|
147
|
+
result: tool.commitStep(params.message as string | undefined, optionalSessionId(params))
|
|
132
148
|
};
|
|
133
|
-
case '
|
|
149
|
+
case 'exportSession':
|
|
134
150
|
return {
|
|
135
151
|
id: request.id,
|
|
136
152
|
ok: true,
|
|
137
|
-
result: tool.
|
|
153
|
+
result: tool.exportSession(optionalSessionId(params))
|
|
138
154
|
};
|
|
139
|
-
case '
|
|
155
|
+
case 'exportPortal':
|
|
140
156
|
return {
|
|
141
157
|
id: request.id,
|
|
142
158
|
ok: true,
|
|
143
|
-
result:
|
|
159
|
+
result: tool.exportPortal(omitSessionId(params) as never, optionalSessionId(params))
|
|
144
160
|
};
|
|
145
|
-
case '
|
|
161
|
+
case 'importData':
|
|
146
162
|
return {
|
|
147
163
|
id: request.id,
|
|
148
164
|
ok: true,
|
|
149
|
-
result:
|
|
165
|
+
result: tool.importData(params.payload as string | object, params.kind as never)
|
|
150
166
|
};
|
|
151
|
-
case '
|
|
167
|
+
case 'setModelValues':
|
|
152
168
|
return {
|
|
153
169
|
id: request.id,
|
|
154
170
|
ok: true,
|
|
155
|
-
result: await tool.
|
|
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(
|
|
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 '
|
|
179
|
+
case 'evaluate':
|
|
170
180
|
return {
|
|
171
181
|
id: request.id,
|
|
172
182
|
ok: true,
|
|
173
|
-
result: tool.
|
|
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(
|
|
189
|
+
result: tool.recalculateModel(optionalSessionId(params))
|
|
180
190
|
};
|
|
181
191
|
default:
|
|
182
192
|
return {
|