entangled-worlds-orbiters-shared 2.17.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/package.json +2 -7
- package/src/render/viewportCompositor.js +59 -1
- package/dist/capabilities/errors.d.ts +0 -79
- package/dist/capabilities/errors.js +0 -87
- package/dist/capabilities/index.d.ts +0 -9
- package/dist/capabilities/index.js +0 -9
- package/dist/capabilities/registry.d.ts +0 -106
- package/dist/capabilities/registry.js +0 -215
- package/dist/capabilities/schema.d.ts +0 -52
- package/dist/capabilities/schema.js +0 -139
- package/src/capabilities/errors.ts +0 -115
- package/src/capabilities/index.ts +0 -22
- package/src/capabilities/registry.ts +0 -307
- package/src/capabilities/schema.ts +0 -164
package/README.md
CHANGED
|
@@ -81,10 +81,8 @@ import * as EntangledWorlds from 'entangled-worlds-orbiters-shared';
|
|
|
81
81
|
- Real binaries (close pair from component masses, a distant companion for wide pairs) and an optional debris disc; `mainSequenceTemperature` / `Radius` / `Luminosity` helpers
|
|
82
82
|
- Perf + look harness: `npm run bench:star` (see `bench/star/results/REPORT.md`)
|
|
83
83
|
|
|
84
|
-
### Capabilities
|
|
85
|
-
|
|
86
|
-
- `validate`, `assertValid` - JSON-Schema-subset validator used for capability inputs
|
|
87
|
-
- Error taxonomy - `ValidationError`, `UnavailableError`, `PrecheckError`, …
|
|
84
|
+
### Capabilities → moved
|
|
85
|
+
The capability registry (`createCapabilityRegistry`, `validate` / `assertValid`, the error taxonomy) moved to its own package, **`plantasia.space-orchidea/capabilities`** (removed here in 3.0.0).
|
|
88
86
|
|
|
89
87
|
### Studio session (`entangled-worlds-orbiters-shared/studio`)
|
|
90
88
|
- `createHistory` - Snapshot undo/redo with injected snapshot/restore
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "entangled-worlds-orbiters-shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Shared library for entangled worlds orbiters - contains core world rendering, moons system, and UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -38,10 +38,6 @@
|
|
|
38
38
|
"types": "./dist/clock/curves.d.ts",
|
|
39
39
|
"import": "./dist/clock/curves.js"
|
|
40
40
|
},
|
|
41
|
-
"./capabilities": {
|
|
42
|
-
"types": "./dist/capabilities/index.d.ts",
|
|
43
|
-
"import": "./dist/capabilities/index.js"
|
|
44
|
-
},
|
|
45
41
|
"./faust": {
|
|
46
42
|
"types": "./dist/faust/index.d.ts",
|
|
47
43
|
"import": "./dist/faust/index.js"
|
|
@@ -129,14 +125,13 @@
|
|
|
129
125
|
"prepare": "npm run build:ts",
|
|
130
126
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
131
127
|
"test:clock": "node --test --import tsx src/clock/*.test.ts",
|
|
132
|
-
"test:capabilities": "node --test --import tsx src/capabilities/*.test.js",
|
|
133
128
|
"test:notes": "node --test --import tsx src/notes/*.test.ts",
|
|
134
129
|
"test:faust": "node --test --import tsx src/faust/*.test.ts",
|
|
135
130
|
"test:studio": "node --test --import tsx src/studio/*.test.js",
|
|
136
131
|
"test:uploads": "node --test --import tsx src/uploads/*.test.js",
|
|
137
132
|
"test:audio": "node --test --import tsx src/audio/*.test.ts",
|
|
138
133
|
"test:render": "node --test src/render/*.test.js",
|
|
139
|
-
"test": "npm run test:clock && npm run test:
|
|
134
|
+
"test": "npm run test:clock && npm run test:notes && npm run test:faust && npm run test:studio && npm run test:uploads && npm run test:audio && npm run test:render && npm run test:moons && npm run test:star",
|
|
140
135
|
"test:moons": "node --test src/moons/*.test.js",
|
|
141
136
|
"test:star": "node --test --import tsx src/star/*.test.ts",
|
|
142
137
|
"bench:star:build": "esbuild bench/star/main.ts --bundle --format=esm --target=es2022 --outfile=bench/star/build/main.js --log-level=warning",
|
|
@@ -158,6 +158,10 @@ export function createViewportCompositor({
|
|
|
158
158
|
let grid = null;
|
|
159
159
|
let raf = null;
|
|
160
160
|
let disposed = false;
|
|
161
|
+
// A load meter's watch on the realm's frames (`setFrameProbe`): null when nobody is watching.
|
|
162
|
+
let frameProbe = null;
|
|
163
|
+
let frameProbeAt = 0;
|
|
164
|
+
let probePixel = null;
|
|
161
165
|
let lastCanvasW = 0;
|
|
162
166
|
let lastCanvasH = 0;
|
|
163
167
|
let viewportH = 0; // per-cell cull band (CSS px), refreshed with the rects
|
|
@@ -514,7 +518,60 @@ export function createViewportCompositor({
|
|
|
514
518
|
// at half the realm's rate. Skipping the whole frame instead leaves every cell showing the last
|
|
515
519
|
// frame that was drawn — together, which is the only way a shared canvas can be consistent.
|
|
516
520
|
if (performanceOn && !frameBudget.shouldRender(nowMs)) return;
|
|
517
|
-
|
|
521
|
+
if (!frameProbe) {
|
|
522
|
+
renderOnce();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const probe = frameProbe;
|
|
526
|
+
probe.onFrame?.(nowMs);
|
|
527
|
+
const timed = nowMs - frameProbeAt >= probe.everyMs;
|
|
528
|
+
if (!timed) {
|
|
529
|
+
renderOnce();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
frameProbeAt = nowMs;
|
|
533
|
+
// Every cell's draw counted together: one composited frame is several renders.
|
|
534
|
+
const info = renderer.info;
|
|
535
|
+
const autoReset = info.autoReset;
|
|
536
|
+
info.autoReset = false;
|
|
537
|
+
info.reset();
|
|
538
|
+
try {
|
|
539
|
+
const started = performance.now();
|
|
540
|
+
renderOnce();
|
|
541
|
+
// A lost context draws nothing and answers nothing: no reading, not a good one.
|
|
542
|
+
if (waitForDraw()) {
|
|
543
|
+
const drawn = performance.now();
|
|
544
|
+
// Asked again with nothing left to draw: what the asking itself costs, taken off the reading.
|
|
545
|
+
waitForDraw();
|
|
546
|
+
probe.onDraw(Math.max(0, drawn - started - (performance.now() - drawn)), info.render.calls, info.render.triangles);
|
|
547
|
+
}
|
|
548
|
+
} finally {
|
|
549
|
+
// Put back as it was, even when a voice throws mid-frame.
|
|
550
|
+
info.autoReset = autoReset;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Block until the graphics chip has drawn what was sent: reading a pixel back is the one wait every
|
|
555
|
+
* browser honours. False when there is no context to ask. */
|
|
556
|
+
function waitForDraw() {
|
|
557
|
+
const gl = renderer.getContext?.();
|
|
558
|
+
if (!gl || gl.isContextLost()) return false;
|
|
559
|
+
probePixel ??= new Uint8Array(4);
|
|
560
|
+
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, probePixel);
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Watch the realm's frames, for a load meter: `onFrame(nowMs)` for every frame drawn, and one frame
|
|
566
|
+
* every `everyMs` timed to the end of its drawing (`onDraw(workMs, calls, triangles)`, every cell
|
|
567
|
+
* together). Only the timed frame waits for the graphics chip; every other frame is untouched.
|
|
568
|
+
* `null` stops it.
|
|
569
|
+
*
|
|
570
|
+
* @param {{ everyMs: number, onFrame?: (nowMs: number) => void, onDraw: (workMs: number, calls: number, triangles: number) => void } | null} probe
|
|
571
|
+
*/
|
|
572
|
+
function setFrameProbe(probe) {
|
|
573
|
+
frameProbe = probe ?? null;
|
|
574
|
+
frameProbeAt = 0;
|
|
518
575
|
}
|
|
519
576
|
|
|
520
577
|
/**
|
|
@@ -572,6 +629,7 @@ export function createViewportCompositor({
|
|
|
572
629
|
removeVoice,
|
|
573
630
|
renderOnce,
|
|
574
631
|
setPerformanceMode,
|
|
632
|
+
setFrameProbe,
|
|
575
633
|
dispose,
|
|
576
634
|
};
|
|
577
635
|
// Dev handle: lets tooling force a composited frame when rAF is throttled (e.g. a hidden/background
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capability layer error taxonomy (shared machinery).
|
|
3
|
-
*
|
|
4
|
-
* This is the ONE generic capability machinery, app-agnostic and layer-neutral
|
|
5
|
-
* (`src/capabilities/` imports nothing app-level — see `.agents/decisions/`). Both the studio
|
|
6
|
-
* registry (`src/studio/capabilities/`, the credit-metered superset caller) and the edit-mode
|
|
7
|
-
* registry (`src/core/edit/capabilities/`, the lean preview caller) build on it. It supersedes
|
|
8
|
-
* the previously duplicated `studio/capabilities/errors.js` + `core/edit/capabilities/errors.ts`.
|
|
9
|
-
*
|
|
10
|
-
* One base class so callers — today the UI, tomorrow the agent loop — branch on `err.code`
|
|
11
|
-
* instead of string-matching messages. Every error carries the offending capability name when
|
|
12
|
-
* known, so failures are traceable without a stack dive.
|
|
13
|
-
*/
|
|
14
|
-
export type CapabilityErrorCode = 'CAPABILITY_ERROR' | 'VALIDATION' | 'UNAVAILABLE' | 'PRECHECK_FAILED' | 'NOT_IMPLEMENTED' | 'INSUFFICIENT_CREDITS' | 'UNKNOWN_CAPABILITY';
|
|
15
|
-
export interface SchemaIssue {
|
|
16
|
-
path: string;
|
|
17
|
-
message: string;
|
|
18
|
-
}
|
|
19
|
-
export declare class CapabilityError extends Error {
|
|
20
|
-
code: CapabilityErrorCode;
|
|
21
|
-
capability: string | null;
|
|
22
|
-
cause?: unknown;
|
|
23
|
-
constructor(message: string, { code, capability, cause }?: {
|
|
24
|
-
code?: CapabilityErrorCode;
|
|
25
|
-
capability?: string | null;
|
|
26
|
-
cause?: unknown;
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
/** Input args failed the capability's inputSchema. Thrown before any write. */
|
|
30
|
-
export declare class ValidationError extends CapabilityError {
|
|
31
|
-
issues: SchemaIssue[];
|
|
32
|
-
constructor(message: string, { capability, issues }?: {
|
|
33
|
-
capability?: string | null;
|
|
34
|
-
issues?: SchemaIssue[];
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
/** Capability is declared but not invokable yet (status: 'planned' / available()===false). */
|
|
38
|
-
export declare class UnavailableError extends CapabilityError {
|
|
39
|
-
reason: string | null;
|
|
40
|
-
constructor(message: string, { capability, reason }?: {
|
|
41
|
-
capability?: string | null;
|
|
42
|
-
reason?: string | null;
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Capability's precheck rejected the args against live state (no-op target, missing element,
|
|
47
|
-
* guard condition). Thrown before pushHistory / any write.
|
|
48
|
-
*/
|
|
49
|
-
export declare class PrecheckError extends CapabilityError {
|
|
50
|
-
reason: string | null;
|
|
51
|
-
constructor(message: string, { capability, reason }?: {
|
|
52
|
-
capability?: string | null;
|
|
53
|
-
reason?: string | null;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Wired capability whose execute() is not implemented (generative stubs). Distinct from
|
|
58
|
-
* UnavailableError: "active but unfinished", reachable only if status was flipped to 'active'
|
|
59
|
-
* before the implementation landed.
|
|
60
|
-
*/
|
|
61
|
-
export declare class NotImplementedError extends CapabilityError {
|
|
62
|
-
constructor(capability: string, message?: string | null);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Reserved for the future generative path: the backend credit check failed (402-style).
|
|
66
|
-
* Declared now so the agent loop can catch it without a follow-up refactor.
|
|
67
|
-
*/
|
|
68
|
-
export declare class InsufficientCreditsError extends CapabilityError {
|
|
69
|
-
required: number | null;
|
|
70
|
-
available: number | null;
|
|
71
|
-
constructor(message: string, { capability, required, available }?: {
|
|
72
|
-
capability?: string | null;
|
|
73
|
-
required?: number | null;
|
|
74
|
-
available?: number | null;
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
export declare class UnknownCapabilityError extends CapabilityError {
|
|
78
|
-
constructor(name: string);
|
|
79
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capability layer error taxonomy (shared machinery).
|
|
3
|
-
*
|
|
4
|
-
* This is the ONE generic capability machinery, app-agnostic and layer-neutral
|
|
5
|
-
* (`src/capabilities/` imports nothing app-level — see `.agents/decisions/`). Both the studio
|
|
6
|
-
* registry (`src/studio/capabilities/`, the credit-metered superset caller) and the edit-mode
|
|
7
|
-
* registry (`src/core/edit/capabilities/`, the lean preview caller) build on it. It supersedes
|
|
8
|
-
* the previously duplicated `studio/capabilities/errors.js` + `core/edit/capabilities/errors.ts`.
|
|
9
|
-
*
|
|
10
|
-
* One base class so callers — today the UI, tomorrow the agent loop — branch on `err.code`
|
|
11
|
-
* instead of string-matching messages. Every error carries the offending capability name when
|
|
12
|
-
* known, so failures are traceable without a stack dive.
|
|
13
|
-
*/
|
|
14
|
-
export class CapabilityError extends Error {
|
|
15
|
-
code;
|
|
16
|
-
capability;
|
|
17
|
-
cause;
|
|
18
|
-
constructor(message, { code = 'CAPABILITY_ERROR', capability = null, cause = null } = {}) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.name = 'CapabilityError';
|
|
21
|
-
this.code = code;
|
|
22
|
-
this.capability = capability;
|
|
23
|
-
if (cause)
|
|
24
|
-
this.cause = cause;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/** Input args failed the capability's inputSchema. Thrown before any write. */
|
|
28
|
-
export class ValidationError extends CapabilityError {
|
|
29
|
-
issues;
|
|
30
|
-
constructor(message, { capability = null, issues = [] } = {}) {
|
|
31
|
-
super(message, { code: 'VALIDATION', capability });
|
|
32
|
-
this.name = 'ValidationError';
|
|
33
|
-
this.issues = issues;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/** Capability is declared but not invokable yet (status: 'planned' / available()===false). */
|
|
37
|
-
export class UnavailableError extends CapabilityError {
|
|
38
|
-
reason;
|
|
39
|
-
constructor(message, { capability = null, reason = null } = {}) {
|
|
40
|
-
super(message, { code: 'UNAVAILABLE', capability });
|
|
41
|
-
this.name = 'UnavailableError';
|
|
42
|
-
this.reason = reason;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Capability's precheck rejected the args against live state (no-op target, missing element,
|
|
47
|
-
* guard condition). Thrown before pushHistory / any write.
|
|
48
|
-
*/
|
|
49
|
-
export class PrecheckError extends CapabilityError {
|
|
50
|
-
reason;
|
|
51
|
-
constructor(message, { capability = null, reason = null } = {}) {
|
|
52
|
-
super(message, { code: 'PRECHECK_FAILED', capability });
|
|
53
|
-
this.name = 'PrecheckError';
|
|
54
|
-
this.reason = reason;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Wired capability whose execute() is not implemented (generative stubs). Distinct from
|
|
59
|
-
* UnavailableError: "active but unfinished", reachable only if status was flipped to 'active'
|
|
60
|
-
* before the implementation landed.
|
|
61
|
-
*/
|
|
62
|
-
export class NotImplementedError extends CapabilityError {
|
|
63
|
-
constructor(capability, message = null) {
|
|
64
|
-
super(message || `Capability "${capability}" is not implemented yet`, { code: 'NOT_IMPLEMENTED', capability });
|
|
65
|
-
this.name = 'NotImplementedError';
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Reserved for the future generative path: the backend credit check failed (402-style).
|
|
70
|
-
* Declared now so the agent loop can catch it without a follow-up refactor.
|
|
71
|
-
*/
|
|
72
|
-
export class InsufficientCreditsError extends CapabilityError {
|
|
73
|
-
required;
|
|
74
|
-
available;
|
|
75
|
-
constructor(message, { capability = null, required = null, available = null } = {}) {
|
|
76
|
-
super(message, { code: 'INSUFFICIENT_CREDITS', capability });
|
|
77
|
-
this.name = 'InsufficientCreditsError';
|
|
78
|
-
this.required = required;
|
|
79
|
-
this.available = available;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
export class UnknownCapabilityError extends CapabilityError {
|
|
83
|
-
constructor(name) {
|
|
84
|
-
super(`Unknown capability "${name}"`, { code: 'UNKNOWN_CAPABILITY', capability: name });
|
|
85
|
-
this.name = 'UnknownCapabilityError';
|
|
86
|
-
}
|
|
87
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared capability machinery — app-agnostic, layer-neutral registry + validator +
|
|
3
|
-
* error taxonomy. Both `src/studio/capabilities/` and `src/core/edit/capabilities/` build their
|
|
4
|
-
* own registry instances and descriptors on top of this. Imports nothing app-level (see
|
|
5
|
-
* `.agents/decisions/`). This is the generic core of the AI-ready capability seam.
|
|
6
|
-
*/
|
|
7
|
-
export { createCapabilityRegistry, type CapabilityRegistry, type CapabilityDescriptor, type CapabilityInfo, type CapabilityKind, type CapabilityStatus, type Reactivity, type InvokeMeta, type InvokeMode, type PrecheckResult, type RegistryCtx, } from './registry.js';
|
|
8
|
-
export { validate, assertValid, type JsonSchema, type JsonType } from './schema.js';
|
|
9
|
-
export * from './errors.js';
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared capability machinery — app-agnostic, layer-neutral registry + validator +
|
|
3
|
-
* error taxonomy. Both `src/studio/capabilities/` and `src/core/edit/capabilities/` build their
|
|
4
|
-
* own registry instances and descriptors on top of this. Imports nothing app-level (see
|
|
5
|
-
* `.agents/decisions/`). This is the generic core of the AI-ready capability seam.
|
|
6
|
-
*/
|
|
7
|
-
export { createCapabilityRegistry, } from './registry.js';
|
|
8
|
-
export { validate, assertValid } from './schema.js';
|
|
9
|
-
export * from './errors.js';
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capability registry — the single, named, validated chokepoint for edits (shared machinery).
|
|
3
|
-
*
|
|
4
|
-
* The ONE registry both callers build on (supersedes the duplicated
|
|
5
|
-
* `studio/capabilities/registry.js` + `core/edit/capabilities/registry.ts`). It is the SUPERSET:
|
|
6
|
-
* the studio uses the full pipeline (precheck → reversible history → credit cost → generative),
|
|
7
|
-
* the edit-mode caller uses the lean subset (validation + named dispatch + the preview/commit
|
|
8
|
-
* `mode`) and simply leaves the extra features unset. App-agnostic: `src/capabilities/` imports
|
|
9
|
-
* nothing app-level (see `.agents/decisions/`); all state/functions arrive via the injected ctx.
|
|
10
|
-
*
|
|
11
|
-
* Today the UI calls invoke()/invokeSync() with source:'ui'; tomorrow the agent loop calls the
|
|
12
|
-
* SAME registry with source:'agent'. Every mutation flows through one pipeline so history,
|
|
13
|
-
* validation, and availability are enforced in exactly one place.
|
|
14
|
-
*
|
|
15
|
-
* invoke() pipeline:
|
|
16
|
-
* 1. resolve unknown name -> UnknownCapabilityError
|
|
17
|
-
* 2. assertAvailable status:'planned' / available(ctx)===false -> UnavailableError (no writes)
|
|
18
|
-
* 3. validate args vs inputSchema -> ValidationError (no writes)
|
|
19
|
-
* 4. precheck PURE, no writes. Returns { ok, reason, willMutate }. !ok -> PrecheckError
|
|
20
|
-
* 5. commit only now: if reversible && willMutate !== false -> ctx.pushHistory()
|
|
21
|
-
* 6. execute the actual write
|
|
22
|
-
*
|
|
23
|
-
* `mode` is the edit-mode preview/commit distinction (ignored by studio capabilities):
|
|
24
|
-
* 'preview' → live side effect only, no host broadcast 'commit' → side effect + emit.
|
|
25
|
-
*/
|
|
26
|
-
import { type JsonSchema } from './schema.js';
|
|
27
|
-
export type CapabilityKind = 'mutation' | 'query' | 'generative';
|
|
28
|
-
export type Reactivity = 'live' | 'structural';
|
|
29
|
-
export type CapabilityStatus = 'active' | 'planned';
|
|
30
|
-
export type InvokeMode = 'preview' | 'commit';
|
|
31
|
-
export interface InvokeMeta {
|
|
32
|
-
source: string;
|
|
33
|
-
mode: InvokeMode;
|
|
34
|
-
}
|
|
35
|
-
export interface PrecheckResult {
|
|
36
|
-
ok: boolean;
|
|
37
|
-
reason?: string | null;
|
|
38
|
-
/** false suppresses the no-op history snapshot at the commit boundary. */
|
|
39
|
-
willMutate?: boolean;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* The optional history hook the registry calls at the commit boundary for reversible mutations.
|
|
43
|
-
* A caller's ctx may include it (the studio does) or not (edit-mode has no history). It is NOT a
|
|
44
|
-
* generic constraint — the ctx type is free-form per caller — the registry just reads pushHistory
|
|
45
|
-
* off it when present.
|
|
46
|
-
*/
|
|
47
|
-
export interface RegistryCtx {
|
|
48
|
-
pushHistory?: () => void;
|
|
49
|
-
}
|
|
50
|
-
export interface CapabilityDescriptor<Ctx = Record<string, unknown>, Args = Record<string, unknown>> {
|
|
51
|
-
name: string;
|
|
52
|
-
kind: CapabilityKind;
|
|
53
|
-
description?: string;
|
|
54
|
-
inputSchema?: JsonSchema;
|
|
55
|
-
status?: CapabilityStatus;
|
|
56
|
-
reversible?: boolean;
|
|
57
|
-
/** Cost class of applying the edit: 'live' (cheap in-place) vs 'structural' (rebuild). */
|
|
58
|
-
reactivity?: Reactivity;
|
|
59
|
-
available?: (ctx: Ctx) => boolean;
|
|
60
|
-
cost?: (args: Args, ctx: Ctx) => number;
|
|
61
|
-
precheck?: (args: Args, ctx: Ctx, meta: InvokeMeta) => PrecheckResult | Promise<PrecheckResult>;
|
|
62
|
-
execute: (args: Args, ctx: Ctx, meta: InvokeMeta) => unknown;
|
|
63
|
-
}
|
|
64
|
-
/** Public, function-free view of a capability — what the agent tool-catalog builder consumes. */
|
|
65
|
-
export interface CapabilityInfo {
|
|
66
|
-
name: string;
|
|
67
|
-
kind: CapabilityKind;
|
|
68
|
-
status: CapabilityStatus;
|
|
69
|
-
description: string;
|
|
70
|
-
inputSchema: JsonSchema;
|
|
71
|
-
reversible: boolean;
|
|
72
|
-
reactivity: Reactivity | null;
|
|
73
|
-
}
|
|
74
|
-
interface NormalizedCapability<Ctx> {
|
|
75
|
-
name: string;
|
|
76
|
-
kind: CapabilityKind;
|
|
77
|
-
status: CapabilityStatus;
|
|
78
|
-
description: string;
|
|
79
|
-
inputSchema: JsonSchema;
|
|
80
|
-
reversible: boolean;
|
|
81
|
-
reactivity: Reactivity | null;
|
|
82
|
-
available: (ctx: Ctx) => boolean;
|
|
83
|
-
cost: (args: never, ctx: Ctx) => number;
|
|
84
|
-
precheck: (args: never, ctx: Ctx, meta: InvokeMeta) => PrecheckResult | Promise<PrecheckResult>;
|
|
85
|
-
execute: (args: never, ctx: Ctx, meta: InvokeMeta) => unknown;
|
|
86
|
-
}
|
|
87
|
-
export interface CapabilityRegistry<Ctx = Record<string, unknown>> {
|
|
88
|
-
register: (descriptor: CapabilityDescriptor<Ctx>) => NormalizedCapability<Ctx>;
|
|
89
|
-
registerAll: (descriptors: CapabilityDescriptor<Ctx>[]) => CapabilityRegistry<Ctx>;
|
|
90
|
-
has: (name: string) => boolean;
|
|
91
|
-
get: (name: string) => NormalizedCapability<Ctx> | null;
|
|
92
|
-
/** Public catalog view of one capability by name (null if unknown). */
|
|
93
|
-
describe: (name: string) => CapabilityInfo | null;
|
|
94
|
-
list: (opts?: {
|
|
95
|
-
includePlanned?: boolean;
|
|
96
|
-
kind?: CapabilityKind | null;
|
|
97
|
-
}) => CapabilityInfo[];
|
|
98
|
-
isAvailable: (name: string) => boolean;
|
|
99
|
-
cost: (name: string, args?: Record<string, unknown>) => number;
|
|
100
|
-
invoke: (name: string, args?: Record<string, unknown>, opts?: Partial<InvokeMeta>) => Promise<unknown>;
|
|
101
|
-
invokeSync: (name: string, args?: Record<string, unknown>, opts?: Partial<InvokeMeta>) => unknown;
|
|
102
|
-
}
|
|
103
|
-
export declare function createCapabilityRegistry<Ctx = Record<string, unknown>>({ ctx }?: {
|
|
104
|
-
ctx?: Ctx;
|
|
105
|
-
}): CapabilityRegistry<Ctx>;
|
|
106
|
-
export {};
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capability registry — the single, named, validated chokepoint for edits (shared machinery).
|
|
3
|
-
*
|
|
4
|
-
* The ONE registry both callers build on (supersedes the duplicated
|
|
5
|
-
* `studio/capabilities/registry.js` + `core/edit/capabilities/registry.ts`). It is the SUPERSET:
|
|
6
|
-
* the studio uses the full pipeline (precheck → reversible history → credit cost → generative),
|
|
7
|
-
* the edit-mode caller uses the lean subset (validation + named dispatch + the preview/commit
|
|
8
|
-
* `mode`) and simply leaves the extra features unset. App-agnostic: `src/capabilities/` imports
|
|
9
|
-
* nothing app-level (see `.agents/decisions/`); all state/functions arrive via the injected ctx.
|
|
10
|
-
*
|
|
11
|
-
* Today the UI calls invoke()/invokeSync() with source:'ui'; tomorrow the agent loop calls the
|
|
12
|
-
* SAME registry with source:'agent'. Every mutation flows through one pipeline so history,
|
|
13
|
-
* validation, and availability are enforced in exactly one place.
|
|
14
|
-
*
|
|
15
|
-
* invoke() pipeline:
|
|
16
|
-
* 1. resolve unknown name -> UnknownCapabilityError
|
|
17
|
-
* 2. assertAvailable status:'planned' / available(ctx)===false -> UnavailableError (no writes)
|
|
18
|
-
* 3. validate args vs inputSchema -> ValidationError (no writes)
|
|
19
|
-
* 4. precheck PURE, no writes. Returns { ok, reason, willMutate }. !ok -> PrecheckError
|
|
20
|
-
* 5. commit only now: if reversible && willMutate !== false -> ctx.pushHistory()
|
|
21
|
-
* 6. execute the actual write
|
|
22
|
-
*
|
|
23
|
-
* `mode` is the edit-mode preview/commit distinction (ignored by studio capabilities):
|
|
24
|
-
* 'preview' → live side effect only, no host broadcast 'commit' → side effect + emit.
|
|
25
|
-
*/
|
|
26
|
-
import { PrecheckError, UnavailableError, UnknownCapabilityError, ValidationError, } from './errors.js';
|
|
27
|
-
import { assertValid } from './schema.js';
|
|
28
|
-
const VALID_KINDS = new Set(['mutation', 'query', 'generative']);
|
|
29
|
-
// Fill descriptor defaults once at registration so invoke() stays branch-light.
|
|
30
|
-
function normalize(descriptor) {
|
|
31
|
-
if (!descriptor || typeof descriptor !== 'object')
|
|
32
|
-
throw new Error('[capabilities] descriptor must be an object');
|
|
33
|
-
const { name, kind } = descriptor;
|
|
34
|
-
if (typeof name !== 'string' || !name)
|
|
35
|
-
throw new Error('[capabilities] descriptor.name is required');
|
|
36
|
-
if (!VALID_KINDS.has(kind)) {
|
|
37
|
-
throw new Error(`[capabilities] "${name}" has invalid kind "${kind}" (expected mutation|query|generative)`);
|
|
38
|
-
}
|
|
39
|
-
const status = descriptor.status ?? 'active';
|
|
40
|
-
if (status !== 'active' && status !== 'planned') {
|
|
41
|
-
throw new Error(`[capabilities] "${name}" has invalid status "${status}" (expected active|planned)`);
|
|
42
|
-
}
|
|
43
|
-
if (typeof descriptor.execute !== 'function')
|
|
44
|
-
throw new Error(`[capabilities] "${name}" must define an execute() function`);
|
|
45
|
-
return {
|
|
46
|
-
name,
|
|
47
|
-
kind,
|
|
48
|
-
status,
|
|
49
|
-
description: descriptor.description ?? '',
|
|
50
|
-
inputSchema: descriptor.inputSchema ?? { type: 'object' },
|
|
51
|
-
reversible: descriptor.reversible === true,
|
|
52
|
-
reactivity: descriptor.reactivity ?? null,
|
|
53
|
-
available: typeof descriptor.available === 'function' ? descriptor.available : () => status === 'active',
|
|
54
|
-
cost: (typeof descriptor.cost === 'function' ? descriptor.cost : () => 0),
|
|
55
|
-
precheck: (typeof descriptor.precheck === 'function' ? descriptor.precheck : () => ({ ok: true })),
|
|
56
|
-
execute: descriptor.execute,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
export function createCapabilityRegistry({ ctx = {} } = {}) {
|
|
60
|
-
const caps = new Map();
|
|
61
|
-
function register(descriptor) {
|
|
62
|
-
const cap = normalize(descriptor);
|
|
63
|
-
if (caps.has(cap.name))
|
|
64
|
-
throw new Error(`[capabilities] "${cap.name}" is already registered`);
|
|
65
|
-
caps.set(cap.name, cap);
|
|
66
|
-
return cap;
|
|
67
|
-
}
|
|
68
|
-
const has = (name) => caps.has(name);
|
|
69
|
-
const get = (name) => caps.get(name) || null;
|
|
70
|
-
// Function-free catalog view of a capability — never leaks execute/precheck to catalog-only
|
|
71
|
-
// callers (the future tool-schema builder).
|
|
72
|
-
function info(cap) {
|
|
73
|
-
return {
|
|
74
|
-
name: cap.name,
|
|
75
|
-
kind: cap.kind,
|
|
76
|
-
status: cap.status,
|
|
77
|
-
description: cap.description,
|
|
78
|
-
inputSchema: cap.inputSchema,
|
|
79
|
-
reversible: cap.reversible,
|
|
80
|
-
reactivity: cap.reactivity,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function describe(name) {
|
|
84
|
-
const cap = get(name);
|
|
85
|
-
return cap ? info(cap) : null;
|
|
86
|
-
}
|
|
87
|
-
function list({ includePlanned = false, kind = null } = {}) {
|
|
88
|
-
const out = [];
|
|
89
|
-
for (const cap of caps.values()) {
|
|
90
|
-
if (!includePlanned && cap.status !== 'active')
|
|
91
|
-
continue;
|
|
92
|
-
if (kind && cap.kind !== kind)
|
|
93
|
-
continue;
|
|
94
|
-
out.push(info(cap));
|
|
95
|
-
}
|
|
96
|
-
return out;
|
|
97
|
-
}
|
|
98
|
-
function isAvailable(name) {
|
|
99
|
-
const cap = get(name);
|
|
100
|
-
if (!cap || cap.status !== 'active')
|
|
101
|
-
return false;
|
|
102
|
-
try {
|
|
103
|
-
return cap.available(ctx) !== false;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function cost(name, args = {}) {
|
|
110
|
-
const cap = get(name);
|
|
111
|
-
if (!cap)
|
|
112
|
-
return 0;
|
|
113
|
-
try {
|
|
114
|
-
return Number(cap.cost(args, ctx)) || 0;
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return 0;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// Steps 1–3 (resolve → availability → validate): pure, no side effects. Shared by both paths.
|
|
121
|
-
function prepareInvocation(name, args, meta) {
|
|
122
|
-
const cap = get(name);
|
|
123
|
-
if (!cap)
|
|
124
|
-
throw new UnknownCapabilityError(name);
|
|
125
|
-
if (cap.status !== 'active') {
|
|
126
|
-
throw new UnavailableError(`Capability "${name}" is declared but not available yet`, { capability: name, reason: `status:${cap.status}` });
|
|
127
|
-
}
|
|
128
|
-
let availResult;
|
|
129
|
-
try {
|
|
130
|
-
availResult = cap.available(ctx);
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
throw new UnavailableError(`Capability "${name}" availability check threw`, { capability: name, reason: err?.message || String(err) });
|
|
134
|
-
}
|
|
135
|
-
if (availResult === false) {
|
|
136
|
-
throw new UnavailableError(`Capability "${name}" is not currently available`, { capability: name, reason: 'available(ctx) returned false' });
|
|
137
|
-
}
|
|
138
|
-
assertValid(cap.inputSchema, args, name);
|
|
139
|
-
void meta;
|
|
140
|
-
return cap;
|
|
141
|
-
}
|
|
142
|
-
function wrapPrecheckError(name, err) {
|
|
143
|
-
if (err instanceof ValidationError || err instanceof PrecheckError)
|
|
144
|
-
return err;
|
|
145
|
-
return new PrecheckError(`Precheck for "${name}" threw: ${err?.message || err}`, { capability: name, reason: err?.message || String(err) });
|
|
146
|
-
}
|
|
147
|
-
function resolvePrecheck(name, pre) {
|
|
148
|
-
const r = pre || { ok: true };
|
|
149
|
-
if (r.ok === false) {
|
|
150
|
-
throw new PrecheckError(`Precheck for "${name}" rejected: ${r.reason || 'no reason given'}`, { capability: name, reason: r.reason ?? null });
|
|
151
|
-
}
|
|
152
|
-
return r;
|
|
153
|
-
}
|
|
154
|
-
// commit boundary — snapshot only a change that will actually mutate.
|
|
155
|
-
function commitHistory(cap, pre) {
|
|
156
|
-
const pushHistory = ctx?.pushHistory;
|
|
157
|
-
if (cap.reversible && pre.willMutate !== false && typeof pushHistory === 'function') {
|
|
158
|
-
pushHistory();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
const fullMeta = (opts) => ({ source: opts?.source ?? 'ui', mode: opts?.mode ?? 'commit' });
|
|
162
|
-
// Async pipeline — supports async precheck/execute (the generative/agent path).
|
|
163
|
-
async function invoke(name, args = {}, opts) {
|
|
164
|
-
const meta = fullMeta(opts);
|
|
165
|
-
const cap = prepareInvocation(name, args, meta);
|
|
166
|
-
let pre;
|
|
167
|
-
try {
|
|
168
|
-
pre = resolvePrecheck(name, await cap.precheck(args, ctx, meta));
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
throw wrapPrecheckError(name, err);
|
|
172
|
-
}
|
|
173
|
-
commitHistory(cap, pre);
|
|
174
|
-
return cap.execute(args, ctx, meta);
|
|
175
|
-
}
|
|
176
|
-
// Synchronous pipeline for UI mutation/query callers — preserves handler ordering (the write
|
|
177
|
-
// lands before the caller's following side effects). Rejects async precheck/execute.
|
|
178
|
-
function invokeSync(name, args = {}, opts) {
|
|
179
|
-
const meta = fullMeta(opts);
|
|
180
|
-
const cap = prepareInvocation(name, args, meta);
|
|
181
|
-
let preResult;
|
|
182
|
-
try {
|
|
183
|
-
preResult = cap.precheck(args, ctx, meta);
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
throw wrapPrecheckError(name, err);
|
|
187
|
-
}
|
|
188
|
-
if (preResult && typeof preResult.then === 'function') {
|
|
189
|
-
throw new Error(`Capability "${name}" has an async precheck — use invoke()`);
|
|
190
|
-
}
|
|
191
|
-
const pre = resolvePrecheck(name, preResult);
|
|
192
|
-
commitHistory(cap, pre);
|
|
193
|
-
const result = cap.execute(args, ctx, meta);
|
|
194
|
-
if (result && typeof result.then === 'function') {
|
|
195
|
-
throw new Error(`Capability "${name}" execute() is async — use invoke() (e.g. generative)`);
|
|
196
|
-
}
|
|
197
|
-
return result;
|
|
198
|
-
}
|
|
199
|
-
const api = {
|
|
200
|
-
register,
|
|
201
|
-
registerAll(descriptors) {
|
|
202
|
-
descriptors.forEach(register);
|
|
203
|
-
return api;
|
|
204
|
-
},
|
|
205
|
-
has,
|
|
206
|
-
get,
|
|
207
|
-
describe,
|
|
208
|
-
list,
|
|
209
|
-
isAvailable,
|
|
210
|
-
cost,
|
|
211
|
-
invoke,
|
|
212
|
-
invokeSync,
|
|
213
|
-
};
|
|
214
|
-
return api;
|
|
215
|
-
}
|