Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 105 additions & 101 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions .beads/sync_base.jsonl

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion .gengine/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ directorConfig:
# Default director risk threshold used by the demo UI and Director when not set per-call
# Value between 0.0 (strict) and 1.0 (lenient). Default: 0.4
riskThreshold: 0.4

# Enable use of embeddings in the local GEngine environment (true/false)
enableEmbeddings: true

# Notes:
# - The proxy prefers CLI args, then environment variables, then this file.
# - To expand settings, update scripts/cors-proxy.js and package.json.

28 changes: 28 additions & 0 deletions docs/dev/embeddings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Embedding Integration (runtime)

Overview
--------
The Director can optionally use local semantic embeddings to improve scoring for thematic consistency, lore adherence, and character voice. This feature is opt-in and disabled by default.

Enabling
--------
- Set `enableEmbeddings: true` in `.gengine/config.yaml` under `directorConfig`, or pass `{ enableEmbeddings: true }` via the `evaluate()` `config` argument.
- For Node integration tests you may set environment flags (used by embedding service): `EMBED_NODE=1` to enable Node fallback.

Telemetry
---------
When embeddings are enabled, Director emits embedding telemetry inside the `director_decision` event under `metrics.embedding` with fields:
- `used` (boolean) - whether embeddings were successfully computed
- `latencyMs` (number) - inference time in milliseconds
- `fallback` (boolean) - true when embeddings were not used and placeholders were applied
- `metrics` (optional object) - similarity metrics (0..1) for `thematic`, `lore`, and `voice` when available

Implementation notes
--------------------
- `evaluate()` computes embeddings asynchronously (using `web/demo/js/embedding-service.js` when available) and derives similarity metrics when story-level embeddings are provided on `storyContext` as `themeEmbedding`, `loreEmbedding`, `voiceEmbedding` arrays.
- `computeRiskScore()` remains synchronous and reads precomputed `config.embeddingMetrics` (if present) to convert similarities into placeholder risks. This keeps the core scoring deterministic and testable.

Follow-ups
----------
1) Precompute story-level embeddings at load-time and attach them to story context. (bead created)
2) Optionally emit a dedicated `embedding_inference` telemetry event in addition to including embedding metadata in director telemetry. (bead created)
11 changes: 8 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"postinstall": "npx playwright install --with-deps chromium",
"serve-demo": "http-server web",
"build": "echo 'no-op build'",
"validate-story": "node scripts/validate-story.js --glob \"web/stories/**/*.ink\" --output json --max-steps 2000",
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/director-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ const defaults = {

placeholderDefault: 0.3,

// Enable embedding-based scoring in the runtime. Disabled by default.
// Can be toggled via local .gengine/config.yaml or environment overrides
// (see loadLocalConfig/ENV parsing in this file). When enabled the Director
// will attempt to compute semantic embeddings for proposals and use
// similarity-derived metrics for thematic/lore/voice scoring.
enableEmbeddings: false,

// Global default decision threshold used by the Director when not overridden per-call
// Value is in 0.0..1.0 where lower is stricter (default 0.4)
riskThreshold: 0.4
Expand Down
8 changes: 6 additions & 2 deletions tests/demo.telemetry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ test('Director threshold slider updates stored settings', async ({ page }) => {
await expect(page.locator('.ai-config-section')).toBeVisible();

const slider = page.locator('#director-risk-threshold');
await expect(slider).toHaveValue('0.4');
// Wait for defaults to hydrate from ApiKeyManager. Expect UI default (0.4)
// regardless of server-provided director-config.
await expect(slider).toHaveValue('0.4', { timeout: 5000 });

await slider.evaluate((el) => {
(el as HTMLInputElement).value = '0.65';
Expand All @@ -161,8 +163,10 @@ test('Director threshold slider updates stored settings', async ({ page }) => {

await expect(page.locator('#director-threshold-value')).toHaveText('0.65');

// Allow change handler to persist before reading (input -> change is async for storage)
await page.waitForTimeout(50);
const saved = await page.evaluate(() => window.ApiKeyManager.getSettings().directorRiskThreshold);
expect(saved).toBeCloseTo(0.65, 2);
await expect(saved).toBeCloseTo(0.65, 2);
});

test('invalid threshold input clamps to range', async ({ page }) => {
Expand Down
41 changes: 41 additions & 0 deletions tests/helpers/node-no-webstorage-environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Custom Jest environment that strips Node's Web Storage accessors, which throw
// unless `--localstorage-file` is provided (Node 25+). We temporarily remove the
// accessors before loading `jest-environment-node` so Jest won't copy them into
// the test context, then restore them for the rest of the process.

const keys = ['localStorage', 'sessionStorage'];
const saved = [];

for (const key of keys) {
const desc = Object.getOwnPropertyDescriptor(globalThis, key);
if (desc && desc.configurable) {
saved.push([key, desc]);
try {
delete globalThis[key];
} catch (err) {
// If delete fails, fall back to defining a harmless value.
try {
Object.defineProperty(globalThis, key, {
value: undefined,
writable: true,
configurable: true,
enumerable: desc.enumerable,
});
} catch (err2) {
// ignore
}
}
}
}

const NodeEnvironment = require('jest-environment-node').TestEnvironment || require('jest-environment-node');

for (const [key, desc] of saved) {
try {
Object.defineProperty(globalThis, key, desc);
} catch (err) {
// ignore
}
}

module.exports = class NodeNoWebStorageEnvironment extends NodeEnvironment {};
4 changes: 1 addition & 3 deletions tests/integration/embedding.integration.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/** @jest-environment node */

/** @jest-environment node */
/** @jest-environment ./tests/helpers/node-no-webstorage-environment.js */

// Optional integration test. Requires network + model download. Skip in CI by default.
// Set EMBED_NODE=1 to force Node fallback (no Worker) or INTEGRATION_EMBEDDING=1 for browser path.
Expand Down
2 changes: 1 addition & 1 deletion tests/validate-story/rotation-state.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @jest-environment node */
/** @jest-environment ./tests/helpers/node-no-webstorage-environment.js */
const fs = require('fs')
const path = require('path')
const os = require('os')
Expand Down
7 changes: 5 additions & 2 deletions tests/validate-story/validate-story.integration.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @jest-environment node */
/** @jest-environment ./tests/helpers/node-no-webstorage-environment.js */
const cp = require('child_process')
const path = require('path')
const fs = require('fs')
Expand All @@ -7,7 +7,10 @@ const os = require('os')
const CLI = path.resolve(__dirname, '../../scripts/validate-story.js')

function runCLI(args, env = {}){
const res = cp.spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', env: { ...process.env, ...env } })
const res = cp.spawnSync(process.execPath, [CLI, ...args], {
encoding: 'utf8',
env: { ...process.env, ...env },
})
return res
}

Expand Down
5 changes: 3 additions & 2 deletions web/demo/config/director-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
},
"pacingToleranceFactor": 0.6,
"placeholderDefault": 0.3,
"riskThreshold": 0.4
}
"enableEmbeddings": true,
"riskThreshold": 0.8
}
1 change: 1 addition & 0 deletions web/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ <h1>InkJS Smoke Demo</h1>
<!-- AI Writer modules -->
<script src="js/lore-assembler.js"></script>
<script src="js/director-config-loader.js"></script>
<script src="js/embedding-service.js"></script>
<script src="js/prompt-engine.js"></script>
<script src="js/llm-adapter.js"></script>
<script src="js/api-key-manager.js"></script>
Expand Down
Loading