@kheleshikhi/studio-agent 0.6.1 → 0.8.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 CHANGED
@@ -13,10 +13,140 @@ nerdbro connect
13
13
  mkdir my-kheleshikhi-work
14
14
  cd my-kheleshikhi-work
15
15
  nerdbro agent init --provider codex # or claude / generic
16
- nerdbro studio create --title "Ball Lab" --target combined --concept "Projectile range"
16
+ nerdbro studio create --title "Ball Lab" --concept "Projectile range" \
17
+ --graph-purpose "Range against launch angle" # target defaults to sceneComposition
17
18
  nerdbro studio checkout <asset-id>
18
19
  ```
19
20
 
21
+ ## Mental model: Scene + Experience
22
+
23
+ Since 0.8.0 the CLI follows ADR-0003, the unified model the server projects:
24
+
25
+ - **Everything visual is a Scene** (`sceneComposition`). A graph is not a
26
+ separate asset or target: it is a **Scene math plane** (`mathPlanes`, Scene
27
+ Source v15, manifest v19) inside the Scene. A graph-only lesson is a Scene
28
+ holding one plane; a physical + graph lesson is one Scene holding both.
29
+ - **Interaction is an Experience** (`kheleshikhi.experience.v3`): final learner
30
+ content containing one or more Scenes, with `interaction` (inputs, requests,
31
+ handlers, readouts, exports, tools), `rounds`, `presentation` and
32
+ `assessment.outcomes`. `game` carries scoring only (sections and metrics).
33
+ `requiredCapabilities` are derived by the server, never authored; every
34
+ canonical capability is available in every host.
35
+ - **Frozen:** the `interactiveScene` part shape and the `mathGraph` and
36
+ `combined` targets are read so existing assets keep playing, but are never
37
+ authored for new work. Experience V2 is read through one normalizer and never
38
+ authored again.
39
+
40
+ The CLI does not bundle these lists. `nerdbro context` returns the live
41
+ projection — `authoring_targets`, `frozen_authoring_targets`, `deprecations`,
42
+ `scene_schema`, `scene_manifest_schema`, `experience_schema`,
43
+ `experience_capabilities`, `experience_tools` — plus a `summary` with the
44
+ compatibility verdict. `studio create` defaults `--target` to `sceneComposition`
45
+ and refuses a target only when the server's context lists it as frozen
46
+ (`STUDIO_TARGET_FROZEN`, exit `2`, with the server's deprecation text); any
47
+ other target is sent to the server, which remains the authority. Graph intent
48
+ (`--math-plane`, or `--graph-purpose`/`--graph-x`/`--graph-y`/`--graph-sync`)
49
+ sets the brief's `requires_graph`, meaning "this Scene holds a math plane",
50
+ whatever the target.
51
+
52
+ `nerdbro schema experience` returns the live V3 contract with the universal
53
+ capability list attached (`universal_capabilities`), and
54
+ `nerdbro capabilities` returns that list and the canonical tools beside the
55
+ Studio capability registry. A schema topic the server marks deprecated (for
56
+ example `mathGraph.config`) carries `deprecated` in its result.
57
+
58
+ A Scene holding a math plane (validated and compiled by the server's Scene
59
+ parser; the plane fields are documented by `nerdbro docs open mathgraph`, and
60
+ `nerdbro schema scene.versions` names the current authoring schema):
61
+
62
+ ```json
63
+ {
64
+ "schema": "kheleshikhi.scene-source.v15",
65
+ "id": "parabola-lab",
66
+ "titleEn": "Parabola",
67
+ "titleBn": "পরাবৃত্ত",
68
+ "instructionEn": "Change k and watch the curve.",
69
+ "instructionBn": "k বদলাও, বক্ররেখা দেখো।",
70
+ "canvas": { "width": 960, "height": 540 },
71
+ "variables": [{ "name": "k", "type": "number", "value": 1, "min": -3, "max": 3 }],
72
+ "bands": [{ "id": "graphs", "kind": "mathPath", "placement": "compiled" }],
73
+ "instances": [],
74
+ "bindings": [],
75
+ "mathPlanes": [
76
+ {
77
+ "id": "graph",
78
+ "band": "graphs",
79
+ "title": "y equals k x squared",
80
+ "rect": { "x": 280, "y": 120, "width": 400, "height": 300 },
81
+ "viewport": { "minX": -5, "maxX": 5, "minY": -4, "maxY": 4 },
82
+ "axes": { "xLabel": "x", "yLabel": "y" },
83
+ "series": [{ "id": "curve", "kind": "explicit", "formula": "k*x^2", "label": "y = kx²" }]
84
+ }
85
+ ]
86
+ }
87
+ ```
88
+
89
+ An Experience V3 using that kind of Scene, with inputs, requests, readouts,
90
+ exports, a tool, a round, presentation and assessment outcomes (validated by
91
+ the server's Experience parser; read `nerdbro schema experience` for the live
92
+ field lists and `nerdbro capabilities` for tool types):
93
+
94
+ ```json
95
+ {
96
+ "schema": "kheleshikhi.experience.v3",
97
+ "id": "ramp",
98
+ "kind": "lesson",
99
+ "title": "Ramp",
100
+ "language": "bn",
101
+ "scenes": { "main": { "asset": "<approved Scene asset id>" } },
102
+ "variables": { "placed": false },
103
+ "interaction": {
104
+ "inputs": [
105
+ { "type": "directional2d", "axes": ["moveX"] },
106
+ { "type": "action", "action": "primary", "labelEn": "Go", "labelBn": "চলো" }
107
+ ],
108
+ "requests": {
109
+ "speed": { "type": "number", "presentation": "dial", "min": 0, "max": 10, "step": 1 },
110
+ "hint": { "type": "hint", "textEn": "Look at the slope." }
111
+ },
112
+ "handlers": [
113
+ { "topic": "input.action.primary.pressed", "kind": "event", "sceneEvent": "begin" }
114
+ ],
115
+ "readouts": [
116
+ {
117
+ "topic": "ramp.speed",
118
+ "kind": "state",
119
+ "from": { "scene": "main", "variable": "speed" },
120
+ "display": { "labelEn": "Speed", "labelBn": "বেগ", "unit": "m/s" }
121
+ }
122
+ ],
123
+ "exports": [{ "topic": "session.complete", "kind": "event", "on": "allPlaced" }],
124
+ "tools": [{ "id": "replay", "type": "replay-scrubber", "config": {} }]
125
+ },
126
+ "rounds": {
127
+ "count": 1,
128
+ "pool": [{ "id": "one", "promptEn": "Place it", "promptBn": "রাখো", "value": 1 }]
129
+ },
130
+ "presentation": { "orientation": "landscape", "aspectRatio": "16:9", "controlsLayout": "standard" },
131
+ "assessment": {
132
+ "outcomes": [
133
+ { "id": "placed", "on": "targetMatched", "status": "correct", "evidence": "placed" },
134
+ { "id": "missed", "on": "targetMissed", "status": "incorrect", "retry": true },
135
+ { "id": "done", "on": "allPlaced", "status": "completed" }
136
+ ]
137
+ }
138
+ }
139
+ ```
140
+
141
+ ### Version negotiation
142
+
143
+ Every command first reads `/v1/agent/context`. A different `agent_protocol`
144
+ or a CLI older than `minimum_cli_version` exits `5`
145
+ (`AGENT_PROTOCOL_INCOMPATIBLE`, `AGENT_CLI_UPGRADE_REQUIRED`) with the exact
146
+ `npm install -g @kheleshikhi/studio-agent@<version>` to run. A CLI older than
147
+ `recommended_cli_version` keeps working and prints one upgrade notice on
148
+ stderr. The server currently recommends 0.8.0.
149
+
20
150
  The browser pairing page uses the normal NerdBro login. The daemon stores the
21
151
  scoped production credential only in memory. It binds to `127.0.0.1` on a
22
152
  random port and protects local RPC with a random discovery secret. The
@@ -30,7 +160,7 @@ are CSS layout sizes; they do not simulate a low-density display. Headless mode
30
160
  uses the same explicit responsive viewports for automated inspection, but its
31
161
  screenshot pixels are not promised to be byte-identical across hardware.
32
162
  Chrome's device-metrics API applies the CSS size while leaving the host device
33
- scale native; NerdBro does not force a DPR value. Version 0.6.1 keeps the
163
+ scale native; NerdBro does not force a DPR value. Since 0.6.2 the CLI keeps the
34
164
  Chrome DevTools viewport session alive for the browser tab, so a requested
35
165
  desktop, mobile, or custom CSS size remains in effect through inspection and
36
166
  capture.
@@ -81,15 +211,61 @@ Use `nerdbro schema experience` for the final learner content contract and
81
211
  `nerdbro docs open experiences` for one-Scene, narrated, mixed, and multi-Band
82
212
  examples. Use `nerdbro schema scene.variables`, `scene.events`, `scene.gestures`, `scene.diagram`,
83
213
  `scene.motions`, `scene.captions`, `scene.audio`, `scene.stateMachines`,
84
- `expressions`, `stateMachine.actions`, `mathGraph.behaviorProjection`,
85
- `assessment`, or the `runtime.*` topics to request a narrow projection of
86
- the parser/runtime contract. `nerdbro schema` returns the topic index and
87
- summary contracts.
214
+ `scene.versions`, `scene.limits`, `expressions`, `stateMachine.actions`,
215
+ `assessment`, or the other listed topics to request a narrow projection of
216
+ the parser/runtime contract. `nerdbro schema` returns the live topic index and
217
+ summary contracts. The `mathGraph.*` and `runtime.*` topics describe frozen
218
+ shapes kept for existing assets; new graphs are Scene math planes.
88
219
  Search Components and Behaviors narrowly. Example search rejects an empty
89
220
  query, returns ranked summaries with a small default limit, and leaves full
90
221
  source to `example inspect <id>`; normally inspect no more than three relevant
91
222
  examples.
92
223
 
224
+ `nerdbro experience inspect-browser <id>` opens the canonical Experience
225
+ workspace. `nerdbro experience drive <id> --scenario scenario.json` runs a
226
+ bounded scenario against the mounted Experience Preview of **your local
227
+ `source.json`, saved or not** (0.7.0+): the exact candidate is mounted in the
228
+ daemon's own browser through the non-persisting Draft Preview and bound by its
229
+ canonical hash (`source_origin: "local"`, `source_hash`); nothing is stored.
230
+ `--saved` drives the saved revision instead. It accepts master
231
+ events (`fire`), Scene semantic events (`fireSceneEvent`), learner presses of
232
+ the active Scene's available events (`press`), Timeline play/pause/seek, Cue
233
+ dispatch, `previewState` (a bare master State id with master-variable
234
+ `setup`, or `machine/state` in the active Scene with `setup` naming variables
235
+ that Scene declares — type and min/max checked, reported as
236
+ `probe.sceneSetup`), `select` (`state:repair`), restart,
237
+ waits, assertions, and captures. It never saves or publishes the source.
238
+ Observation paths read the shared `kheleshikhi.studio-observation.v2`
239
+ domains the Maker's Observatory shows — `experience.activeState`,
240
+ `experience.masterVariables.<name>`, `scene.activeStates.<machine>`,
241
+ `timeline.ms`, `probe.active`, `probe.setup.<name>`, `selection.sourcePath` —
242
+ and the older flat keys (`active_state`, `master_variables`, `timeline_ms`,
243
+ `scene_active_states`, `preview_override`, …) still work.
244
+
245
+ ### Source is the authority; probes are not journeys
246
+
247
+ You author the same canonical Source as the Maker. Every drive returns
248
+ `evidence_mode`: `probe` when it jumped to a State, used synthetic `setup`,
249
+ fired an event a learner could not press, or seeked — useful development
250
+ evidence that never proves a learner path or certifies anything — and
251
+ `journey` when it ran from the initial State with learner-reachable actions
252
+ only. Certification refuses a probed runtime observation.
253
+
254
+ ### Agent Presence
255
+
256
+ The Maker's open Studio shows what you are doing, through
257
+ `PUT /v1/agent/presence` (scope `STUDIO_OBSERVE`): status, changed source
258
+ paths, the last semantic command and result, where your runtime is, your
259
+ selection, your last capture. The CLI reports it around each
260
+ validate/compile/preview/save/certify/capture/drive and per drive step.
261
+ `nerdbro studio watch <asset> [--once]` and `nerdbro experience watch <id>
262
+ [--once]` report which JSON Pointers your local `source.json` changed
263
+ against the saved head, debounced; they never send the source. Presence is
264
+ overwritten in place, expires after 90 seconds, is cleared on disconnect, and
265
+ carries no reasoning, credentials or local paths. A Maker may **Follow
266
+ Agent**: their Studio mirrors your selection, source path and runtime
267
+ position, but you never control their browser or runtime.
268
+
93
269
  The daemon reuses the authenticated context response for at most 30 seconds,
94
270
  then revalidates it; connecting or disconnecting clears the cache. This cuts
95
271
  repeated compatibility reads without caching mutable drafts.
@@ -97,9 +273,9 @@ repeated compatibility reads without caching mutable drafts.
97
273
  Check out once, edit only `source.json`, and use `studio status` for later
98
274
  synchronization. Validate, compile, preview when useful, save, then inspect the
99
275
  real Studio. The daemon reuses its short-lived read-only browser credential in
100
- memory and clears it on disconnect, stop, reconnect, or origin change. A graph
101
- requirement without a teaching purpose or axis/synchronization meaning is a
102
- clarification request, not permission to invent filler.
276
+ memory and clears it on disconnect, stop, reconnect, or origin change. A math
277
+ plane requirement without a teaching purpose or axis/synchronization meaning is
278
+ a clarification request, not permission to invent filler.
103
279
 
104
280
  `studio create` needs the `STUDIO_ASSET_CREATE` pairing scope and returns a
105
281
  Maker-owned draft identity and its server starter-source hash. `--scratch`
@@ -107,7 +283,8 @@ only labels the intent to use an ordinary new, unapproved asset as a disposable
107
283
  spike. It has no separate lifecycle, automatic expiry, promotion, or garbage
108
284
  collection. Save its exact ID for explicit cleanup after success. Use
109
285
  `studio brief <asset>` and `studio brief update <asset> --graph-purpose ...`
110
- to inspect or update authoring intent with a concurrency check.
286
+ (or `--math-plane` / `--no-math-plane`) to inspect or update authoring intent
287
+ with a concurrency check.
111
288
 
112
289
  `studio save` creates a draft. `studio certify` records technical evidence.
113
290
  Neither approves or publishes. `studio export --revision ...` accepts only the
@@ -147,8 +324,28 @@ and has at most 64 steps and a two-minute run budget:
147
324
  }
148
325
  ```
149
326
 
150
- `set`, `fire`, `press`, `tap`, `drag`, `play`, `pause`, `seek`, and `reset`
151
- address only declared variables, events, instances, and gestures. `press`
327
+ To start mid-flow without replaying the Scene, jump to a State of one
328
+ machine — always `machine/state`, because two machines may share a State id —
329
+ optionally with synthetic setup for 1–16 declared variables:
330
+
331
+ ```json
332
+ {"version": 1, "steps": [
333
+ {"op": "previewState", "target": "launch/armed", "setup": {"force": 9, "armed": true}},
334
+ {"op": "assert", "path": "probe.active", "equals": true},
335
+ {"op": "select", "target": "instance:cart"},
336
+ {"op": "assert", "path": "selection.sourcePath", "equals": "/sources/mainScene/document/instances/0"},
337
+ {"op": "press", "target": "disarmPressed"},
338
+ {"op": "wait", "path": "scene.activeStates.launch", "equals": "idle"},
339
+ {"op": "capture", "name": "after-disarm"},
340
+ {"op": "restart"}
341
+ ]}
342
+ ```
343
+
344
+ `set`, `fire`, `press`, `tap`, `drag`, `play`, `pause`, `seek`, `reset`,
345
+ `restart`, `previewState` and `select` address only declared variables,
346
+ events, instances, gestures, machines and States. A `set` on a variable the
347
+ learner has no visible control for is synthetic setup and makes the run a
348
+ probe. `press`
152
349
  honours conditional availability; `drag` derives a Scene point from the
153
350
  authored gesture range. `wait` and `assert` read allowlisted observation paths,
154
351
  never arbitrary code. `capture` records the actual browser pixels. The result
@@ -199,7 +396,12 @@ edits without `--force`. `status` reports `local_changed`, `remote_changed` and
199
396
  saving unchanged content creates no draft but still refreshes the lock, and a
200
397
  stale base exits `3` (`VOCABULARY_DRAFT_CONFLICT`) with `source.json` untouched.
201
398
  `validate` and `certify` default to the lock's head revision. `preview` sends
202
- the local, unsaved `source.json` and writes `preview.json` beside it.
399
+ the local, unsaved `source.json` and writes `preview.json` beside it; for an
400
+ Experience its summary reports the schema, Scene and audio counts, V3
401
+ interaction block counts, rounds, outcomes and the server-derived
402
+ `required_capabilities`. When a checked-out Experience is not the context's
403
+ `experience_schema` (for example a V2 document), `checkout`, `status` and
404
+ `preview` add a `schema_notice` with the server's deprecation text.
203
405
  `inspect-browser` captures the Studio Library workspace to `screenshot.png` and
204
406
  `browser.json` in the checkout directory; its bounded `browser.capture` activity
205
407
  record is reported locally in the command result only.
package/dist/browser.d.ts CHANGED
@@ -1,6 +1,15 @@
1
+ import { type RenderRecipe } from "./video.js";
1
2
  import { type Page } from "playwright";
2
3
  import { AgentApi } from "./protocol.js";
3
- import { type DriveScenario } from "./scenario.js";
4
+ import { type DriveScenario, type ExperienceScenario } from "./scenario.js";
5
+ /** One semantic step as it happened, for Agent Presence and the activity feed. */
6
+ export type DriveStepReport = {
7
+ index: number;
8
+ summary: Record<string, unknown>;
9
+ observation?: Record<string, unknown>;
10
+ };
11
+ /** Called after each semantic step; failures inside it never fail the drive. */
12
+ export type DriveStepListener = (report: DriveStepReport) => void | Promise<void>;
4
13
  export declare function studioBrowserLaunchOptions(headless: boolean): {
5
14
  readonly headless: boolean;
6
15
  readonly viewport: null;
@@ -19,7 +28,7 @@ export declare function waitForCapturePaint(page: Page): Promise<void>;
19
28
  export declare function applyCssViewport(page: Page, dimensions: {
20
29
  width: number;
21
30
  height: number;
22
- }): Promise<void>;
31
+ }, deviceScaleFactor?: number): Promise<void>;
23
32
  export declare function renderingDiagnostics(page: Page, screenshot: Buffer): Promise<Record<string, unknown>>;
24
33
  export declare const VIEW_QUERY: Record<string, Record<string, string>>;
25
34
  export declare function viewRequiresRuntimeObservation(view: string): boolean;
@@ -58,6 +67,8 @@ export interface BrowserCaptureRequest {
58
67
  timeoutMs?: number;
59
68
  observation?: boolean;
60
69
  drive?: boolean;
70
+ /** An Experience drive that will mount an unsaved candidate itself. */
71
+ awaitsCandidate?: boolean;
61
72
  /** Library workspace route (`?workspace=<name>&selection=<stable id>`) instead of a Scene asset. */
62
73
  library?: {
63
74
  workspace: string;
@@ -65,6 +76,12 @@ export interface BrowserCaptureRequest {
65
76
  revision?: string;
66
77
  definitionHash?: string;
67
78
  };
79
+ /** Semantic step reports for presence; never part of the request sent to the page. */
80
+ onStep?: DriveStepListener;
81
+ /** The presentation render host (`studio_present=1`): the learner stage alone, filling the viewport. */
82
+ present?: boolean;
83
+ /** CSS-to-device pixel scale; 0 keeps the display's own. */
84
+ deviceScaleFactor?: number;
68
85
  }
69
86
  export interface PreviewObservationRequest extends BrowserCaptureRequest {
70
87
  mode?: string;
@@ -96,6 +113,61 @@ export declare class StudioBrowser {
96
113
  candidate: Record<string, unknown>;
97
114
  candidateHash: string;
98
115
  }): Promise<Record<string, unknown>>;
116
+ driveExperience(request: BrowserCaptureRequest & {
117
+ scenario: ExperienceScenario;
118
+ candidate?: unknown;
119
+ candidateHash?: string;
120
+ }): Promise<Record<string, unknown>>;
121
+ /**
122
+ * Presentation frames (STU-001) of a Scene, from the presentation host.
123
+ *
124
+ * Mounts and verifies the candidate exactly as `drive` does, but in the
125
+ * presentation host (`studio_present=1`) at a viewport of exactly the
126
+ * output size and device scale 1. `renderBegin` silences the Scene's
127
+ * sounds into a log and restarts it at 0 s; then for every frame video
128
+ * time is stepped to `start + k / fps`, the runtime confirms that instant
129
+ * and a paint, and the whole viewport is captured: one PNG of exactly the
130
+ * output size, proved before it is kept. Scheduled learner actions happen
131
+ * at their own seconds. Afterwards the audio log and each logged sound's
132
+ * canonical bytes are collected for the mixer.
133
+ */
134
+ renderFrames(request: BrowserCaptureRequest & {
135
+ candidate: Record<string, unknown>;
136
+ candidateHash: string;
137
+ recipe: RenderRecipe;
138
+ framesDirectory: string;
139
+ audioDirectory: string;
140
+ /** Optional: afterwards, play in real time and sample frame intervals. */
141
+ measurePlaybackSeconds?: number;
142
+ }): Promise<Record<string, unknown>>;
143
+ /**
144
+ * Presentation frames of an Experience: the canonical Experience player
145
+ * in the presentation host, driven by video time (`renderBegin`, then
146
+ * `step` per frame). The master timeline advances with video time only
147
+ * while it runs, so a learner pause or a wait for a choice stays paused,
148
+ * as it would in playback; Scenes change, cues fire and sounds start at
149
+ * the exact video second they would.
150
+ */
151
+ renderExperienceFrames(request: BrowserCaptureRequest & {
152
+ candidate?: unknown;
153
+ candidateHash?: string;
154
+ recipe: RenderRecipe;
155
+ framesDirectory: string;
156
+ audioDirectory: string;
157
+ }): Promise<Record<string, unknown>>;
158
+ private requirePage;
159
+ /** The page's runtime observation and its one-at-a-time drive channel. */
160
+ private renderChannel;
161
+ /** The newest observation once [ready] holds (the page publishes every 150 ms). */
162
+ private waitForRender;
163
+ /**
164
+ * The frame loop both subjects share: actions at their seconds, a step to
165
+ * each frame's exact second, the subject's own arrival check, a paint,
166
+ * and a whole-viewport PNG proved to be exactly the output size.
167
+ */
168
+ private captureFrames;
169
+ /** Every logged sound's canonical bytes, written once by key; checked against the logged SHA-256. */
170
+ private collectAudio;
99
171
  /** Local-agent capture from the same browser page and renderer the Maker sees. */
100
172
  capturePreview(request: PreviewObservationRequest): Promise<Record<string, unknown>>;
101
173
  }