From 88db900517697d6c37cbc4902f1cfa95fb55ba35 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Fri, 21 Aug 2026 10:52:33 -0800 Subject: [PATCH 1/2] hermes: add Cloudflare Worker demo with rate limiting and AI wrapper --- quilt_worker/README.md | 48 ++ quilt_worker/quilt_core/cells/ai.d.ts | 106 +++ quilt_worker/quilt_core/cells/ai.d.ts.map | 1 + quilt_worker/quilt_core/cells/ai.js | 113 ++++ quilt_worker/quilt_core/cells/ai.js.map | 1 + quilt_worker/quilt_core/cells/api.d.ts | 77 +++ quilt_worker/quilt_core/cells/api.d.ts.map | 1 + quilt_worker/quilt_core/cells/api.js | 165 +++++ quilt_worker/quilt_core/cells/api.js.map | 1 + quilt_worker/quilt_core/cells/formula.d.ts | 90 +++ .../quilt_core/cells/formula.d.ts.map | 1 + quilt_worker/quilt_core/cells/formula.js | 171 +++++ quilt_worker/quilt_core/cells/formula.js.map | 1 + quilt_worker/quilt_core/cells/io.d.ts | 59 ++ quilt_worker/quilt_core/cells/io.d.ts.map | 1 + quilt_worker/quilt_core/cells/io.js | 57 ++ quilt_worker/quilt_core/cells/io.js.map | 1 + quilt_worker/quilt_core/cells/listener.d.ts | 79 +++ .../quilt_core/cells/listener.d.ts.map | 1 + quilt_worker/quilt_core/cells/listener.js | 96 +++ quilt_worker/quilt_core/cells/listener.js.map | 1 + quilt_worker/quilt_core/cells/program.d.ts | 89 +++ .../quilt_core/cells/program.d.ts.map | 1 + quilt_worker/quilt_core/cells/program.js | 100 +++ quilt_worker/quilt_core/cells/program.js.map | 1 + quilt_worker/quilt_core/cells/router.d.ts | 76 +++ quilt_worker/quilt_core/cells/router.d.ts.map | 1 + quilt_worker/quilt_core/cells/router.js | 115 ++++ quilt_worker/quilt_core/cells/router.js.map | 1 + quilt_worker/quilt_core/cells/sensor.d.ts | 60 ++ quilt_worker/quilt_core/cells/sensor.d.ts.map | 1 + quilt_worker/quilt_core/cells/sensor.js | 58 ++ quilt_worker/quilt_core/cells/sensor.js.map | 1 + quilt_worker/quilt_core/cells/value.d.ts | 51 ++ quilt_worker/quilt_core/cells/value.d.ts.map | 1 + quilt_worker/quilt_core/cells/value.js | 56 ++ quilt_worker/quilt_core/cells/value.js.map | 1 + quilt_worker/quilt_core/context.d.ts | 115 ++++ quilt_worker/quilt_core/context.d.ts.map | 1 + quilt_worker/quilt_core/context.js | 167 +++++ quilt_worker/quilt_core/context.js.map | 1 + quilt_worker/quilt_core/engine.d.ts | 277 ++++++++ quilt_worker/quilt_core/engine.d.ts.map | 1 + quilt_worker/quilt_core/engine.js | 618 ++++++++++++++++++ quilt_worker/quilt_core/engine.js.map | 1 + quilt_worker/quilt_core/index.d.ts | 37 ++ quilt_worker/quilt_core/index.d.ts.map | 1 + quilt_worker/quilt_core/index.js | 47 ++ quilt_worker/quilt_core/index.js.map | 1 + quilt_worker/quilt_core/parser.d.ts | 87 +++ quilt_worker/quilt_core/parser.d.ts.map | 1 + quilt_worker/quilt_core/parser.js | 142 ++++ quilt_worker/quilt_core/parser.js.map | 1 + quilt_worker/quilt_core/types.d.ts | 416 ++++++++++++ quilt_worker/quilt_core/types.d.ts.map | 1 + quilt_worker/quilt_core/types.js | 97 +++ quilt_worker/quilt_core/types.js.map | 1 + quilt_worker/src/demo_sheet.json | 29 + quilt_worker/src/index.js | 35 + quilt_worker/src/simple_ai_engine.js | 49 ++ quilt_worker/wrangler.toml | 17 + 61 files changed, 3827 insertions(+) create mode 100644 quilt_worker/README.md create mode 100644 quilt_worker/quilt_core/cells/ai.d.ts create mode 100644 quilt_worker/quilt_core/cells/ai.d.ts.map create mode 100644 quilt_worker/quilt_core/cells/ai.js create mode 100644 quilt_worker/quilt_core/cells/ai.js.map create mode 100644 quilt_worker/quilt_core/cells/api.d.ts create mode 100644 quilt_worker/quilt_core/cells/api.d.ts.map create mode 100644 quilt_worker/quilt_core/cells/api.js create mode 100644 quilt_worker/quilt_core/cells/api.js.map create mode 100644 quilt_worker/quilt_core/cells/formula.d.ts create mode 100644 quilt_worker/quilt_core/cells/formula.d.ts.map create mode 100644 quilt_worker/quilt_core/cells/formula.js create mode 100644 quilt_worker/quilt_core/cells/formula.js.map create mode 100644 quilt_worker/quilt_core/cells/io.d.ts create mode 100644 quilt_worker/quilt_core/cells/io.d.ts.map create mode 100644 quilt_worker/quilt_core/cells/io.js create mode 100644 quilt_worker/quilt_core/cells/io.js.map create mode 100644 quilt_worker/quilt_core/cells/listener.d.ts create mode 100644 quilt_worker/quilt_core/cells/listener.d.ts.map create mode 100644 quilt_worker/quilt_core/cells/listener.js create mode 100644 quilt_worker/quilt_core/cells/listener.js.map create mode 100644 quilt_worker/quilt_core/cells/program.d.ts create mode 100644 quilt_worker/quilt_core/cells/program.d.ts.map create mode 100644 quilt_worker/quilt_core/cells/program.js create mode 100644 quilt_worker/quilt_core/cells/program.js.map create mode 100644 quilt_worker/quilt_core/cells/router.d.ts create mode 100644 quilt_worker/quilt_core/cells/router.d.ts.map create mode 100644 quilt_worker/quilt_core/cells/router.js create mode 100644 quilt_worker/quilt_core/cells/router.js.map create mode 100644 quilt_worker/quilt_core/cells/sensor.d.ts create mode 100644 quilt_worker/quilt_core/cells/sensor.d.ts.map create mode 100644 quilt_worker/quilt_core/cells/sensor.js create mode 100644 quilt_worker/quilt_core/cells/sensor.js.map create mode 100644 quilt_worker/quilt_core/cells/value.d.ts create mode 100644 quilt_worker/quilt_core/cells/value.d.ts.map create mode 100644 quilt_worker/quilt_core/cells/value.js create mode 100644 quilt_worker/quilt_core/cells/value.js.map create mode 100644 quilt_worker/quilt_core/context.d.ts create mode 100644 quilt_worker/quilt_core/context.d.ts.map create mode 100644 quilt_worker/quilt_core/context.js create mode 100644 quilt_worker/quilt_core/context.js.map create mode 100644 quilt_worker/quilt_core/engine.d.ts create mode 100644 quilt_worker/quilt_core/engine.d.ts.map create mode 100644 quilt_worker/quilt_core/engine.js create mode 100644 quilt_worker/quilt_core/engine.js.map create mode 100644 quilt_worker/quilt_core/index.d.ts create mode 100644 quilt_worker/quilt_core/index.d.ts.map create mode 100644 quilt_worker/quilt_core/index.js create mode 100644 quilt_worker/quilt_core/index.js.map create mode 100644 quilt_worker/quilt_core/parser.d.ts create mode 100644 quilt_worker/quilt_core/parser.d.ts.map create mode 100644 quilt_worker/quilt_core/parser.js create mode 100644 quilt_worker/quilt_core/parser.js.map create mode 100644 quilt_worker/quilt_core/types.d.ts create mode 100644 quilt_worker/quilt_core/types.d.ts.map create mode 100644 quilt_worker/quilt_core/types.js create mode 100644 quilt_worker/quilt_core/types.js.map create mode 100644 quilt_worker/src/demo_sheet.json create mode 100644 quilt_worker/src/index.js create mode 100644 quilt_worker/src/simple_ai_engine.js create mode 100644 quilt_worker/wrangler.toml diff --git a/quilt_worker/README.md b/quilt_worker/README.md new file mode 100644 index 0000000..fd37e4f --- /dev/null +++ b/quilt_worker/README.md @@ -0,0 +1,48 @@ +# Quilt Demo Worker + +This Cloudflare Worker demonstrates the **Quilt** reactive engine in a server‑less environment. + +## What it does +- Loads a static Quilt sheet (`demo_sheet.json`). +- Instantiates a `QuiltEngine` with a **SimpleAIEngine** wrapper that calls Cloudflare AI. +- Enforces a per‑IP rate limit using a Workers KV namespace (`USAGE`). +- Caps AI calls to **5** per visitor per day and limits token usage to **100** per request. +- Returns a JSON object with the evaluated result of every cell in the sheet. + +## Deploying +1. **Login** to Cloudflare: + ```bash + wrangler login + ``` +2. **Create a KV namespace** (run once): + ```bash + wrangler kv:namespace create USAGE + ``` + Copy the generated namespace ID and add it to `wrangler.toml` under the `kv_namespaces` section. +3. **Add secrets** for the AI backend (replace with your own values): + ```bash + wrangler secret put CF_ACCOUNT_ID + wrangler secret put CF_AI_TOKEN + ``` +4. **Deploy**: + ```bash + wrangler deploy + ``` + The worker will be reachable at the URL shown by the CLI. + +## Usage limits +- **VISIT_LIMIT** = 5 – maximum AI calls per unique visitor (IP) per 24 h. +- **max_tokens** in the AI request is capped at 100. +- The `SimpleAIEngine` enforces a per‑process call limit (`maxCalls`). + +## Local testing +You can run the worker locally with: +```bash +wrangler dev +``` +Then open `http://localhost:8787` in a browser – you’ll see the JSON payload. + +## Extending +- Add more cells to `demo_sheet.json` to explore different cell kinds. +- Adjust `VISIT_LIMIT` or token caps in `src/index.js`. +- Hook the worker into the larger SuperInstance ecosystem by exposing the results to other services. diff --git a/quilt_worker/quilt_core/cells/ai.d.ts b/quilt_worker/quilt_core/cells/ai.d.ts new file mode 100644 index 0000000..7b5e66e --- /dev/null +++ b/quilt_worker/quilt_core/cells/ai.d.ts @@ -0,0 +1,106 @@ +/** + * @file cells/ai.ts + * @module @quilt/core + * + * ===================================================================== + * THE AI CELL — language model, embedding, vision, code + * ===================================================================== + * + * An AI cell is a reactive cell that calls an LLM provider when its + * inputs change. It supports 8 sub-kinds via the `ai_kind` field: + * + * - ai.llm — chat completion + * - ai.embed — text → vector + * - ai.image — text → image + * - ai.translate — text → translated text + * - ai.sentiment — text → {label, score} + * - ai.summarize — long text → short text + * - ai.code — description → code + * - ai.vision — image + text → text + * + * The cell is async. The runtime queues it for evaluation when + * upstream cells change. Caching is by input hash. + * + * Providers: zai, kimi, deepseek, cloudflare (or any custom provider + * registered with the AIEngine). + * + * ===================================================================== + * USAGE IN A SHEET + * ===================================================================== + * + * - id: ai.answer + * kind: ai + * ai_kind: ai.llm + * provider: zai + * model: glm-4.5 + * prompt: "What is Quilt?" + * temperature: 0.7 + * max_tokens: 500 + * + * The cell's value is the model's response. If the call fails, the + * cell's error field is set and the value is null. + * + * ===================================================================== + * INTEGRATION WITH @quilt/ai + * ===================================================================== + * + * The AI cell is a thin wrapper around @quilt/ai's AIEngine. To use it: + * + * import { Quilt } from '@quilt/core'; + * import { AIEngine } from '@quilt/ai'; + * + * const ai = new AIEngine({ zaiKey: process.env.ZAI_TOKEN, ... }); + * const q = new Quilt({ ai }); + * + * q.load({ cells: [{ id: 'a', kind: 'ai', provider: 'zai', ... }] }); + * await q.tick(); + * console.log(q.get('a')); // the model's response + * + * ===================================================================== + */ +import type { CellDef, CallerContext } from '../types.js'; +/** The 8 AI cell sub-kinds. */ +export type AIKind = 'ai.llm' | 'ai.embed' | 'ai.image' | 'ai.translate' | 'ai.sentiment' | 'ai.summarize' | 'ai.code' | 'ai.vision'; +/** The AI cell's own config. */ +export interface AICellConfig extends Omit { + kind: 'ai'; + /** The AI sub-kind. */ + ai_kind: AIKind; + /** Provider name. */ + provider: string; + /** Model id. */ + model: string; + /** Prompt template (for ai.llm, ai.code, ai.summarize, ai.vision). */ + prompt?: string; + /** Input cell reference or value (for ai.embed, ai.translate, ai.sentiment, ai.summarize, ai.code). */ + input?: string; + /** Image URL (for ai.vision, ai.image). */ + image?: string; + /** Target language (for ai.translate). */ + target?: string; + /** Language (for ai.code). */ + language?: string | 'javascript' | 'python' | 'wasm'; + /** Max words (for ai.summarize). */ + max_words?: number; + /** Temperature (0 = deterministic, 2 = creative). */ + temperature?: number; + /** Max tokens. */ + max_tokens?: number; + /** System message. */ + system?: string; +} +/** The AIEngine interface (a subset of @quilt/ai's AIEngine). */ +export interface AIEngineLike { + call(config: AICellConfig, opts?: { + useCache?: boolean; + signal?: AbortSignal; + }): Promise; +} +/** Result of evaluating an AI cell. */ +export interface AIEvalResult { + value: unknown; + error: string | null; +} +/** Evaluate an AI cell. */ +export declare function evaluateAI(cell: AICellConfig, _context: CallerContext, engine: AIEngineLike, resolveCellValue?: (id: string) => unknown): Promise; +//# sourceMappingURL=ai.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/ai.d.ts.map b/quilt_worker/quilt_core/cells/ai.d.ts.map new file mode 100644 index 0000000..471a2e1 --- /dev/null +++ b/quilt_worker/quilt_core/cells/ai.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../src/cells/ai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE1D,+BAA+B;AAC/B,MAAM,MAAM,MAAM,GACd,QAAQ,GACR,UAAU,GACV,UAAU,GACV,cAAc,GACd,cAAc,GACd,cAAc,GACd,SAAS,GACT,WAAW,CAAC;AAEhB,gCAAgC;AAChC,MAAM,WAAW,YAAa,SAAQ,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7D,IAAI,EAAE,IAAI,CAAC;IACX,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uGAAuG;IACvG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrD,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnG;AAED,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,2BAA2B;AAC3B,wBAAsB,UAAU,CAC9B,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,YAAY,EACpB,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GACzC,OAAO,CAAC,YAAY,CAAC,CAwCvB"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/ai.js b/quilt_worker/quilt_core/cells/ai.js new file mode 100644 index 0000000..27230b7 --- /dev/null +++ b/quilt_worker/quilt_core/cells/ai.js @@ -0,0 +1,113 @@ +/** + * @file cells/ai.ts + * @module @quilt/core + * + * ===================================================================== + * THE AI CELL — language model, embedding, vision, code + * ===================================================================== + * + * An AI cell is a reactive cell that calls an LLM provider when its + * inputs change. It supports 8 sub-kinds via the `ai_kind` field: + * + * - ai.llm — chat completion + * - ai.embed — text → vector + * - ai.image — text → image + * - ai.translate — text → translated text + * - ai.sentiment — text → {label, score} + * - ai.summarize — long text → short text + * - ai.code — description → code + * - ai.vision — image + text → text + * + * The cell is async. The runtime queues it for evaluation when + * upstream cells change. Caching is by input hash. + * + * Providers: zai, kimi, deepseek, cloudflare (or any custom provider + * registered with the AIEngine). + * + * ===================================================================== + * USAGE IN A SHEET + * ===================================================================== + * + * - id: ai.answer + * kind: ai + * ai_kind: ai.llm + * provider: zai + * model: glm-4.5 + * prompt: "What is Quilt?" + * temperature: 0.7 + * max_tokens: 500 + * + * The cell's value is the model's response. If the call fails, the + * cell's error field is set and the value is null. + * + * ===================================================================== + * INTEGRATION WITH @quilt/ai + * ===================================================================== + * + * The AI cell is a thin wrapper around @quilt/ai's AIEngine. To use it: + * + * import { Quilt } from '@quilt/core'; + * import { AIEngine } from '@quilt/ai'; + * + * const ai = new AIEngine({ zaiKey: process.env.ZAI_TOKEN, ... }); + * const q = new Quilt({ ai }); + * + * q.load({ cells: [{ id: 'a', kind: 'ai', provider: 'zai', ... }] }); + * await q.tick(); + * console.log(q.get('a')); // the model's response + * + * ===================================================================== + */ +/** Evaluate an AI cell. */ +export async function evaluateAI(cell, _context, engine, resolveCellValue) { + // Helper: substitute {{cell.id}} in a string + const substitute = (s) => { + if (!resolveCellValue) + return s; + return s.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_match, id) => { + const v = resolveCellValue(id); + if (v === null || v === undefined) + return ''; + if (typeof v === 'string') + return v; + return JSON.stringify(v); + }); + }; + // Build the AI config from the cell + const config = { + id: cell.id, + kind: 'ai', + ai_kind: cell.ai_kind, + provider: cell.provider, + model: cell.model, + }; + if (cell.prompt !== undefined) + config.prompt = substitute(cell.prompt); + if (cell.input !== undefined) { + // If it's a template like {{cell.id}}, substitute; otherwise use as-is + config.input = cell.input.startsWith('{{') ? substitute(cell.input) : cell.input; + } + if (cell.image !== undefined) + config.image = substitute(cell.image); + if (cell.target !== undefined) + config.target = cell.target; + if (cell.language !== undefined) + config.language = cell.language; + if (cell.max_words !== undefined) + config.max_words = cell.max_words; + if (cell.temperature !== undefined) + config.temperature = cell.temperature; + if (cell.max_tokens !== undefined) + config.max_tokens = cell.max_tokens; + if (cell.system !== undefined) + config.system = substitute(cell.system); + try { + const result = await engine.call(config, { useCache: true }); + return { value: result, error: null }; + } + catch (e) { + const message = e instanceof Error ? e.message : String(e); + return { value: null, error: message }; + } +} +//# sourceMappingURL=ai.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/ai.js.map b/quilt_worker/quilt_core/cells/ai.js.map new file mode 100644 index 0000000..cb23ba0 --- /dev/null +++ b/quilt_worker/quilt_core/cells/ai.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ai.js","sourceRoot":"","sources":["../../src/cells/ai.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/api.d.ts b/quilt_worker/quilt_core/cells/api.d.ts new file mode 100644 index 0000000..9c2166f --- /dev/null +++ b/quilt_worker/quilt_core/cells/api.d.ts @@ -0,0 +1,77 @@ +/** + * @file cells/api.ts + * @module @quilt/core + * + * ===================================================================== + * THE API CELL — external endpoint or model call + * ===================================================================== + * + * An API cell makes a network request. It can be: + * - A plain HTTP endpoint (with optional template substitution) + * - A "model:" pseudo-endpoint (routes to a configured LLM provider) + * - An "mcp://" tool reference (routes to an MCP server) + * + * Async. May have effects (network, model). May be expensive. + * Caller context can route which endpoint/model to use. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * context.ts ◄── CallerContext (used for {{}} substitution) + * ▲ + * │ imports + * │ + * api.ts ◄── THIS FILE: evaluateApi() + * ▲ + * │ imports + * │ + * engine.ts (calls evaluateApi for kind === 'api') + * + * ===================================================================== + * ENDPOINT SHAPES + * ===================================================================== + * + * - HTTP: https://api.example.com/v1/chat?row={{caller.row}} + * - Model: model:openai/gpt-4o + * - MCP tool: mcp://server-name/tool-name + * + * HTTP endpoints support `{{...}}` substitution in the URL with + * fields from the caller context. Example: + * https://api.example.com/tenants/{{caller.row}}/status + * → https://api.example.com/tenants/boat-1/status + * + * Model and MCP endpoints are placeholders for now. The full + * implementation will look up registered providers/connectors. + * + * ===================================================================== + */ +import type { Cell, CellValue, CallerContext } from '../types.js'; +/** + * Injected fetch function. Allows tests to mock HTTP without + * monkey-patching globalThis.fetch (which is brittle). + * + * The default implementation reads globalThis.fetch at call time + * (not at module load) so it picks up test overrides correctly. + */ +export interface ApiExecutor { + fetch: (input: string | URL | Request, init?: RequestInit) => Promise; +} +/** + * Evaluate an API cell. + * + * Steps: + * 1. Dispatch by endpoint kind (model / mcp / http) + * 2. For HTTP, substitute caller context into the URL + * 3. Make the request + * 4. Return parsed response, or error + * + * @param cell - the cell instance + * @param ctx - the caller context (for URL substitution) + * @param input - optional input to pass as body + * @param executor - injected fetch (for tests) + * @returns a CellValue with the response data, or error + */ +export declare function evaluateApi(cell: Cell, ctx: CallerContext, input: unknown, executor?: ApiExecutor): Promise; +//# sourceMappingURL=api.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/api.d.ts.map b/quilt_worker/quilt_core/cells/api.d.ts.map new file mode 100644 index 0000000..98817a4 --- /dev/null +++ b/quilt_worker/quilt_core/cells/api.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/cells/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAElE;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CACjF;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,WAA6B,GACtC,OAAO,CAAC,SAAS,CAAC,CAoDpB"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/api.js b/quilt_worker/quilt_core/cells/api.js new file mode 100644 index 0000000..abff6a6 --- /dev/null +++ b/quilt_worker/quilt_core/cells/api.js @@ -0,0 +1,165 @@ +/** + * @file cells/api.ts + * @module @quilt/core + * + * ===================================================================== + * THE API CELL — external endpoint or model call + * ===================================================================== + * + * An API cell makes a network request. It can be: + * - A plain HTTP endpoint (with optional template substitution) + * - A "model:" pseudo-endpoint (routes to a configured LLM provider) + * - An "mcp://" tool reference (routes to an MCP server) + * + * Async. May have effects (network, model). May be expensive. + * Caller context can route which endpoint/model to use. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * context.ts ◄── CallerContext (used for {{}} substitution) + * ▲ + * │ imports + * │ + * api.ts ◄── THIS FILE: evaluateApi() + * ▲ + * │ imports + * │ + * engine.ts (calls evaluateApi for kind === 'api') + * + * ===================================================================== + * ENDPOINT SHAPES + * ===================================================================== + * + * - HTTP: https://api.example.com/v1/chat?row={{caller.row}} + * - Model: model:openai/gpt-4o + * - MCP tool: mcp://server-name/tool-name + * + * HTTP endpoints support `{{...}}` substitution in the URL with + * fields from the caller context. Example: + * https://api.example.com/tenants/{{caller.row}}/status + * → https://api.example.com/tenants/boat-1/status + * + * Model and MCP endpoints are placeholders for now. The full + * implementation will look up registered providers/connectors. + * + * ===================================================================== + */ +const defaultExecutor = { + fetch: (input, init) => globalThis.fetch(input, init), +}; +/** + * Evaluate an API cell. + * + * Steps: + * 1. Dispatch by endpoint kind (model / mcp / http) + * 2. For HTTP, substitute caller context into the URL + * 3. Make the request + * 4. Return parsed response, or error + * + * @param cell - the cell instance + * @param ctx - the caller context (for URL substitution) + * @param input - optional input to pass as body + * @param executor - injected fetch (for tests) + * @returns a CellValue with the response data, or error + */ +export async function evaluateApi(cell, ctx, input, executor = defaultExecutor) { + const startedAt = Date.now(); + if (!cell.def.endpoint) { + return { data: undefined, status: 'error', error: { message: 'api cell has no endpoint' } }; + } + try { + // Model calls — handled by the model provider registry + if (cell.def.endpoint.startsWith('model:')) { + return await callModel(cell, ctx, input, executor, startedAt); + } + // MCP tool references + if (cell.def.endpoint.startsWith('mcp://')) { + return await callMcpTool(cell, ctx, input, startedAt); + } + // HTTP — substitute {{caller.row}} etc. in URL + const url = substitute(cell.def.endpoint, ctx); + const method = cell.def.method ?? 'GET'; + const headers = cell.def.headers ?? { 'Content-Type': 'application/json' }; + const init = { method, headers }; + if (method !== 'GET' && method !== 'HEAD' && input !== undefined) { + init.body = typeof input === 'string' ? input : JSON.stringify(input); + } + const response = await executor.fetch(url, init); + const duration = Date.now() - startedAt; + if (!response.ok) { + return { + data: undefined, + status: 'error', + error: { message: `HTTP ${response.status} ${response.statusText}` }, + }; + } + const contentType = response.headers.get('content-type') ?? ''; + const data = contentType.includes('application/json') ? await response.json() : await response.text(); + return { + data, + status: 'ready', + computedAt: Date.now(), + effects: [{ kind: 'network', url, method }, { kind: 'compute', ms: duration }], + }; + } + catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { data: undefined, status: 'error', error: { message } }; + } +} +/** + * Call a model provider. Placeholder for now — the real implementation + * will look up the provider, swap models based on caller context, and + * call the appropriate LLM API (OpenAI, Anthropic, Ollama, etc.). + */ +async function callModel(cell, _ctx, _input, _executor, startedAt) { + return { + data: { model: cell.def.endpoint, note: 'model calls not yet implemented' }, + status: 'ready', + computedAt: Date.now(), + effects: [{ kind: 'model', provider: cell.def.endpoint ?? '' }, { kind: 'compute', ms: Date.now() - startedAt }], + }; +} +/** + * Call an MCP tool. Placeholder for now — the real implementation + * will route the call through the registered MCP client. + */ +async function callMcpTool(cell, _ctx, _input, startedAt) { + return { + data: { tool: cell.def.endpoint, note: 'MCP tool calls not yet implemented' }, + status: 'ready', + computedAt: Date.now(), + effects: [{ kind: 'network', url: cell.def.endpoint ?? '', method: 'MCP' }, { kind: 'compute', ms: Date.now() - startedAt }], + }; +} +/** + * Substitute `{{path.to.field}}` patterns in a template string with + * values from the caller context. Dotted paths traverse the context + * object. Missing paths become empty strings. + * + * Example: + * substitute("https://api.example.com/{{caller.row}}", { row: "boat-1", ... }) + * → "https://api.example.com/boat-1" + * + * This is intentionally simple. For complex templates, use a proper + * template engine. + */ +function substitute(template, ctx) { + return template.replace(/\{\{([^}]+)\}\}/g, (_, path) => { + const parts = path.trim().split('.'); + let value = ctx; + for (const p of parts) { + if (value && typeof value === 'object' && p in value) { + value = value[p]; + } + else { + return ''; + } + } + return String(value ?? ''); + }); +} +//# sourceMappingURL=api.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/api.js.map b/quilt_worker/quilt_core/cells/api.js.map new file mode 100644 index 0000000..d554538 --- /dev/null +++ b/quilt_worker/quilt_core/cells/api.js.map @@ -0,0 +1 @@ +{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/cells/api.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/formula.d.ts b/quilt_worker/quilt_core/cells/formula.d.ts new file mode 100644 index 0000000..64cfe4c --- /dev/null +++ b/quilt_worker/quilt_core/cells/formula.d.ts @@ -0,0 +1,90 @@ +/** + * @file cells/formula.ts + * @module @quilt/core + * + * ===================================================================== + * THE FORMULA CELL — pure reactive computation + * ===================================================================== + * + * A formula cell evaluates a small expression that references other + * cells by their stable ids. The runtime auto-tracks dependencies + * (by scanning the expression for known cell ids) and recomputes + * when any of them change. + * + * Pure: no effects, same input → same output (modulo caller context). + * + * The expression language is a tiny safe DSL — just enough to write + * useful formulas without being Turing-complete. It supports: + * + * - Cell references by id: =a + b, =compass.heading + * - Math: =max(a, b), =clamp(x, 0, 100) + * - Caller context: =caller.row > 10 + * - String operations: =name + ' — ' + status + * - Ternary: =x > 0 ? "positive" : "negative" + * + * Implementation note: we use `new Function(...)` to compile the + * expression, then a `with (cells)` block so cell ids are directly + * in scope. This is a tiny sandbox, NOT a security boundary. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * context.ts ◄── contextKey (for per-context caching) + * ▲ + * │ imports + * │ + * formula.ts ◄── THIS FILE: evaluateFormula() + * ▲ + * │ imports + * │ + * engine.ts (calls evaluateFormula for kind === 'formula') + * + * ===================================================================== + * WHY THE EXPRESSION TRANSFORMATION MATTERS + * ===================================================================== + * + * Cell ids can contain dots (e.g. "compass.heading"). JavaScript's + * `with (cells) { ... }` cannot resolve `compass.heading` as a + * single lookup — it tries `cells.compass` (undefined), then falls + * back to global scope (ReferenceError). + * + * So we PRE-PROCESS the expression: replace each known cell id with + * `cells["the.id"]` (bracket access). This way any id works, + * including ids with dots, dashes, or special characters. + * + * Replacement rules: + * - Sort ids longest-first to avoid partial matches + * - Use lookbehind/lookahead to ensure we match whole tokens + * (not parts of longer identifiers) + * + * Example: + * Input: =compass.heading - desired.heading + * Output: =cells["compass.heading"] - cells["desired.heading"] + * + * ===================================================================== + */ +import type { Cell, CellValue, CallerContext, CellId } from '../types.js'; +/** + * Evaluate a formula cell. + * + * Steps: + * 1. Check per-context cache (same context → return cached value) + * 2. Build a cells proxy from all known cells + * 3. Compile the expression (with id rewriting) + * 4. Call the compiled function with the proxy + helpers + * 5. Cache the result and return + * + * Per-context caching means: if the same cell is called with the + * same row/column/identity, the cached value is returned without + * recomputation. This is the "caller-aware memoization" that makes + * routing cheap. + * + * @param cell - the cell instance + * @param ctx - the caller context (used for cache key) + * @param allCells - the full cell map (for expression evaluation) + * @returns the computed value, or an error CellValue if the formula threw + */ +export declare function evaluateFormula(cell: Cell, ctx: CallerContext, allCells: Map): CellValue; +//# sourceMappingURL=formula.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/formula.d.ts.map b/quilt_worker/quilt_core/cells/formula.d.ts.map new file mode 100644 index 0000000..e898b67 --- /dev/null +++ b/quilt_worker/quilt_core/cells/formula.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"formula.d.ts","sourceRoot":"","sources":["../../src/cells/formula.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AA6D1E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,SAAS,CAyDtG"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/formula.js b/quilt_worker/quilt_core/cells/formula.js new file mode 100644 index 0000000..ecdcc74 --- /dev/null +++ b/quilt_worker/quilt_core/cells/formula.js @@ -0,0 +1,171 @@ +/** + * @file cells/formula.ts + * @module @quilt/core + * + * ===================================================================== + * THE FORMULA CELL — pure reactive computation + * ===================================================================== + * + * A formula cell evaluates a small expression that references other + * cells by their stable ids. The runtime auto-tracks dependencies + * (by scanning the expression for known cell ids) and recomputes + * when any of them change. + * + * Pure: no effects, same input → same output (modulo caller context). + * + * The expression language is a tiny safe DSL — just enough to write + * useful formulas without being Turing-complete. It supports: + * + * - Cell references by id: =a + b, =compass.heading + * - Math: =max(a, b), =clamp(x, 0, 100) + * - Caller context: =caller.row > 10 + * - String operations: =name + ' — ' + status + * - Ternary: =x > 0 ? "positive" : "negative" + * + * Implementation note: we use `new Function(...)` to compile the + * expression, then a `with (cells)` block so cell ids are directly + * in scope. This is a tiny sandbox, NOT a security boundary. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * context.ts ◄── contextKey (for per-context caching) + * ▲ + * │ imports + * │ + * formula.ts ◄── THIS FILE: evaluateFormula() + * ▲ + * │ imports + * │ + * engine.ts (calls evaluateFormula for kind === 'formula') + * + * ===================================================================== + * WHY THE EXPRESSION TRANSFORMATION MATTERS + * ===================================================================== + * + * Cell ids can contain dots (e.g. "compass.heading"). JavaScript's + * `with (cells) { ... }` cannot resolve `compass.heading` as a + * single lookup — it tries `cells.compass` (undefined), then falls + * back to global scope (ReferenceError). + * + * So we PRE-PROCESS the expression: replace each known cell id with + * `cells["the.id"]` (bracket access). This way any id works, + * including ids with dots, dashes, or special characters. + * + * Replacement rules: + * - Sort ids longest-first to avoid partial matches + * - Use lookbehind/lookahead to ensure we match whole tokens + * (not parts of longer identifiers) + * + * Example: + * Input: =compass.heading - desired.heading + * Output: =cells["compass.heading"] - cells["desired.heading"] + * + * ===================================================================== + */ +import { contextKey } from '../context.js'; +/** + * Compile a formula expression to a JavaScript function. The returned + * function takes positional args: cells, abs, min, max, clamp, caller. + * + * KNOWN IDS are passed in so we can rewrite them to bracket access. + * The function body uses `with (cells)` to make cell ids visible + * inside the expression (after rewriting). + * + * Why not "use strict"? Because `with` is not allowed in strict mode. + * The function is a tiny sandbox, not a security boundary — for + * untrusted expressions, use a proper expression parser. + */ +function compile(expr, knownIds) { + let body = expr.startsWith('=') ? expr.slice(1) : expr; + // Replace known cell ids with bracket access. Sort longest-first + // so that 'compass.heading' replaces before 'compass'. + const sortedIds = Array.from(knownIds).sort((a, b) => b.length - a.length); + for (const id of sortedIds) { + const safe = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Match `id` as a whole token — not part of a longer identifier. + // \b doesn't work for ids with dots, so we use lookbehind/lookahead. + const re = new RegExp(`(? Math.abs(n); +const helperMin = (...n) => Math.min(...n); +const helperMax = (...n) => Math.max(...n); +const helperClamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi); +/** + * Evaluate a formula cell. + * + * Steps: + * 1. Check per-context cache (same context → return cached value) + * 2. Build a cells proxy from all known cells + * 3. Compile the expression (with id rewriting) + * 4. Call the compiled function with the proxy + helpers + * 5. Cache the result and return + * + * Per-context caching means: if the same cell is called with the + * same row/column/identity, the cached value is returned without + * recomputation. This is the "caller-aware memoization" that makes + * routing cheap. + * + * @param cell - the cell instance + * @param ctx - the caller context (used for cache key) + * @param allCells - the full cell map (for expression evaluation) + * @returns the computed value, or an error CellValue if the formula threw + */ +export function evaluateFormula(cell, ctx, allCells) { + if (!cell.def.expr) { + return { data: undefined, status: 'error', error: { message: 'formula cell has no expr' } }; + } + // Per-context cache check + const key = contextKey(ctx); + const cached = cell.contextCache.get(key); + if (cached && cached.status === 'ready' && !cell.value.error) { + return cached; + } + try { + // Build a Record of all cell values for the formula to access. + // We use a Proxy so any property access returns the value if it + // exists, otherwise undefined (no thrown errors for missing cells). + const cellValues = {}; + for (const [id, other] of allCells) { + cellValues[id] = other.value.data; + } + const cellProxy = new Proxy(cellValues, { + get(target, prop) { + if (typeof prop === 'string' && prop in target) { + return target[prop]; + } + return undefined; + }, + has(target, prop) { + if (typeof prop === 'string') + return prop in target; + return false; + }, + }); + const fn = compile(cell.def.expr, allCells.keys()); + const result = fn(cellProxy, helperAbs, helperMin, helperMax, helperClamp, ctx); + const value = { + data: result, + status: 'ready', + computedAt: Date.now(), + }; + cell.contextCache.set(key, value); + return value; + } + catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (process.env.QUILT_DEBUG) { + console.error(`[quilt] formula ${cell.id} failed:`, err); + } + return { data: undefined, status: 'error', error: { message } }; + } +} +//# sourceMappingURL=formula.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/formula.js.map b/quilt_worker/quilt_core/cells/formula.js.map new file mode 100644 index 0000000..cbf4a25 --- /dev/null +++ b/quilt_worker/quilt_core/cells/formula.js.map @@ -0,0 +1 @@ +{"version":3,"file":"formula.js","sourceRoot":"","sources":["../../src/cells/formula.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/io.d.ts b/quilt_worker/quilt_core/cells/io.d.ts new file mode 100644 index 0000000..89ae7c9 --- /dev/null +++ b/quilt_worker/quilt_core/cells/io.d.ts @@ -0,0 +1,59 @@ +/** + * @file cells/io.ts + * @module @quilt/core + * + * ===================================================================== + * THE IO CELL — bidirectional port to the outside world + * ===================================================================== + * + * An IO cell is the I/O boundary of the system. It can: + * - Receive input from a form, webhook, MCP tool call, GPIO, etc. + * - Send output to the same or a different port. + * + * Inputs come in via the engine's `push` method (same as sensors). + * Outputs are written when the cell's value is consumed (the + * `direction` field controls whether it's push, pull, or both). + * + * In the spreadsheet metaphor: an IO cell is the cell that's bound + * to a UI widget, a webhook, a hardware pin, or an MCP tool. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * ▲ + * │ imports + * │ + * io.ts ◄── THIS FILE: makeIoValue (a tiny factory) + * ▲ + * │ imports + * │ + * engine.ts (calls makeIoValue when push() is invoked) + * adapters/* (external: forms, webhooks, GPIO, MCP — push/pull) + * + * Like sensors, IO cells are PUSH-BASED for inputs. Unlike sensors, + * they can also produce output (when their value is consumed by + * other cells or when an external system reads from them). + * + * ===================================================================== + */ +import type { CellValue } from '../types.js'; +/** + * Result of an IO push. (Reserved for future use — e.g. for + * delivery confirmations or backpressure stats.) + */ +export interface IoPushResult { + delivered: boolean; +} +/** + * Build a `CellValue` wrapping an IO event. + * + * Used by the engine's `push` method when an IO cell receives an + * input from a form, webhook, or MCP tool call. + * + * @param data - the input payload + * @returns a ready CellValue + */ +export declare function makeIoValue(data: unknown): CellValue; +//# sourceMappingURL=io.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/io.d.ts.map b/quilt_worker/quilt_core/cells/io.d.ts.map new file mode 100644 index 0000000..90e4343 --- /dev/null +++ b/quilt_worker/quilt_core/cells/io.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../src/cells/io.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAMpD"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/io.js b/quilt_worker/quilt_core/cells/io.js new file mode 100644 index 0000000..56662e9 --- /dev/null +++ b/quilt_worker/quilt_core/cells/io.js @@ -0,0 +1,57 @@ +/** + * @file cells/io.ts + * @module @quilt/core + * + * ===================================================================== + * THE IO CELL — bidirectional port to the outside world + * ===================================================================== + * + * An IO cell is the I/O boundary of the system. It can: + * - Receive input from a form, webhook, MCP tool call, GPIO, etc. + * - Send output to the same or a different port. + * + * Inputs come in via the engine's `push` method (same as sensors). + * Outputs are written when the cell's value is consumed (the + * `direction` field controls whether it's push, pull, or both). + * + * In the spreadsheet metaphor: an IO cell is the cell that's bound + * to a UI widget, a webhook, a hardware pin, or an MCP tool. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * ▲ + * │ imports + * │ + * io.ts ◄── THIS FILE: makeIoValue (a tiny factory) + * ▲ + * │ imports + * │ + * engine.ts (calls makeIoValue when push() is invoked) + * adapters/* (external: forms, webhooks, GPIO, MCP — push/pull) + * + * Like sensors, IO cells are PUSH-BASED for inputs. Unlike sensors, + * they can also produce output (when their value is consumed by + * other cells or when an external system reads from them). + * + * ===================================================================== + */ +/** + * Build a `CellValue` wrapping an IO event. + * + * Used by the engine's `push` method when an IO cell receives an + * input from a form, webhook, or MCP tool call. + * + * @param data - the input payload + * @returns a ready CellValue + */ +export function makeIoValue(data) { + return { + data, + status: 'ready', + computedAt: Date.now(), + }; +} +//# sourceMappingURL=io.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/io.js.map b/quilt_worker/quilt_core/cells/io.js.map new file mode 100644 index 0000000..9fbccaf --- /dev/null +++ b/quilt_worker/quilt_core/cells/io.js.map @@ -0,0 +1 @@ +{"version":3,"file":"io.js","sourceRoot":"","sources":["../../src/cells/io.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAYH;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,IAAa;IACvC,OAAO;QACL,IAAI;QACJ,MAAM,EAAE,OAAO;QACf,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;KACvB,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/listener.d.ts b/quilt_worker/quilt_core/cells/listener.d.ts new file mode 100644 index 0000000..8411de5 --- /dev/null +++ b/quilt_worker/quilt_core/cells/listener.d.ts @@ -0,0 +1,79 @@ +/** + * @file cells/listener.ts + * @module @quilt/core + * + * ===================================================================== + * THE LISTENER CELL — delta-triggered execution + * ===================================================================== + * + * A listener cell watches other cells. When a watched cell's value + * changes AND the optional `condition` is true, the listener fires + * its `action` (typically a program cell that does something — send + * an alert, write to a database, call an API, etc.). + * + * Listeners are how reactive systems take action on change. They're + * the "if X happens, do Y" primitive. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * context.ts ◄── evalWhen, emptyContext + * ▲ + * │ imports + * │ + * listener.ts ◄── THIS FILE: fireListener() + * ▲ + * │ imports + * │ + * engine.ts (calls fireListener from propagate()) + * + * The listener doesn't have an "evaluate" function like other cells. + * It's triggered by the engine's reactive propagation — when a + * dependency changes, the engine calls `fireListener` for each + * dependent listener cell. + * + * ===================================================================== + * HOW THE TRIGGER WORKS + * ===================================================================== + * + * [value cell: temperature] + * │ + * │ changes from 20 to 25 + * ▼ + * engine.propagate('temperature') + * │ + * │ for each dependent: + * ▼ + * if (cell.kind === 'listener') engine.fireListener(cell, 'temperature', new, old) + * │ + * │ eval condition in context { changed, prev, current } + * ▼ + * if (condition met) runtime.call(action, { changed, value }) + * + * The `action` is typically a program cell id, but it could be any + * callable cell. Listeners are how the grid can act on change. + * + * ===================================================================== + */ +import type { Cell, CellValue, CellId } from '../types.js'; +import type { ProgramRuntime } from './program.js'; +/** + * Fire a listener cell if its condition is met. + * + * Steps: + * 1. Check that the changed cell is in the listener's `watch` list + * 2. Build a context with `{ changed, prev, current }` metadata + * 3. Evaluate the optional `condition` in that context + * 4. If the condition is met, call the `action` cell with the new value + * + * @param cell - the listener cell to fire + * @param changedCellId - the cell that changed + * @param newValue - the cell's new value + * @param prevValue - the cell's previous value + * @param runtime - the runtime handle (used to call the action) + * @returns true if the listener fired, false otherwise + */ +export declare function fireListener(cell: Cell, changedCellId: CellId, newValue: CellValue, prevValue: CellValue, runtime: ProgramRuntime): Promise; +//# sourceMappingURL=listener.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/listener.d.ts.map b/quilt_worker/quilt_core/cells/listener.d.ts.map new file mode 100644 index 0000000..7f15b53 --- /dev/null +++ b/quilt_worker/quilt_core/cells/listener.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../../src/cells/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,IAAI,EACV,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,SAAS,EACnB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,OAAO,CAAC,CAoBlB"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/listener.js b/quilt_worker/quilt_core/cells/listener.js new file mode 100644 index 0000000..fee1baa --- /dev/null +++ b/quilt_worker/quilt_core/cells/listener.js @@ -0,0 +1,96 @@ +/** + * @file cells/listener.ts + * @module @quilt/core + * + * ===================================================================== + * THE LISTENER CELL — delta-triggered execution + * ===================================================================== + * + * A listener cell watches other cells. When a watched cell's value + * changes AND the optional `condition` is true, the listener fires + * its `action` (typically a program cell that does something — send + * an alert, write to a database, call an API, etc.). + * + * Listeners are how reactive systems take action on change. They're + * the "if X happens, do Y" primitive. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * context.ts ◄── evalWhen, emptyContext + * ▲ + * │ imports + * │ + * listener.ts ◄── THIS FILE: fireListener() + * ▲ + * │ imports + * │ + * engine.ts (calls fireListener from propagate()) + * + * The listener doesn't have an "evaluate" function like other cells. + * It's triggered by the engine's reactive propagation — when a + * dependency changes, the engine calls `fireListener` for each + * dependent listener cell. + * + * ===================================================================== + * HOW THE TRIGGER WORKS + * ===================================================================== + * + * [value cell: temperature] + * │ + * │ changes from 20 to 25 + * ▼ + * engine.propagate('temperature') + * │ + * │ for each dependent: + * ▼ + * if (cell.kind === 'listener') engine.fireListener(cell, 'temperature', new, old) + * │ + * │ eval condition in context { changed, prev, current } + * ▼ + * if (condition met) runtime.call(action, { changed, value }) + * + * The `action` is typically a program cell id, but it could be any + * callable cell. Listeners are how the grid can act on change. + * + * ===================================================================== + */ +import { evalWhen, emptyContext } from '../context.js'; +/** + * Fire a listener cell if its condition is met. + * + * Steps: + * 1. Check that the changed cell is in the listener's `watch` list + * 2. Build a context with `{ changed, prev, current }` metadata + * 3. Evaluate the optional `condition` in that context + * 4. If the condition is met, call the `action` cell with the new value + * + * @param cell - the listener cell to fire + * @param changedCellId - the cell that changed + * @param newValue - the cell's new value + * @param prevValue - the cell's previous value + * @param runtime - the runtime handle (used to call the action) + * @returns true if the listener fired, false otherwise + */ +export async function fireListener(cell, changedCellId, newValue, prevValue, runtime) { + if (!cell.def.watch?.length) + return false; + if (!cell.def.watch.includes(changedCellId)) + return false; + const ctx = emptyContext(); + ctx.caller = cell.id; + ctx.metadata = { changed: changedCellId, prev: prevValue.data, current: newValue.data }; + if (cell.def.condition && !evalWhen(cell.def.condition, ctx)) { + return false; + } + // Fire the action. The action is treated as a program call (or + // any callable cell). Future: also support webhooks and MCP tools + // as actions. + if (cell.def.action) { + await runtime.call(cell.def.action, { changed: changedCellId, value: newValue.data }); + } + return true; +} +//# sourceMappingURL=listener.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/listener.js.map b/quilt_worker/quilt_core/cells/listener.js.map new file mode 100644 index 0000000..28621a4 --- /dev/null +++ b/quilt_worker/quilt_core/cells/listener.js.map @@ -0,0 +1 @@ +{"version":3,"file":"listener.js","sourceRoot":"","sources":["../../src/cells/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAIH,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEvD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAU,EACV,aAAqB,EACrB,QAAmB,EACnB,SAAoB,EACpB,OAAuB;IAEvB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC;QAAE,OAAO,KAAK,CAAC;IAE1D,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAC3B,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACrB,GAAG,CAAC,QAAQ,GAAG,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;IAExF,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC;QAC7D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,+DAA+D;IAC/D,kEAAkE;IAClE,cAAc;IACd,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACxF,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/program.d.ts b/quilt_worker/quilt_core/cells/program.d.ts new file mode 100644 index 0000000..a7ed6af --- /dev/null +++ b/quilt_worker/quilt_core/cells/program.d.ts @@ -0,0 +1,89 @@ +/** + * @file cells/program.ts + * @module @quilt/core + * + * ===================================================================== + * THE PROGRAM CELL — stateful, side-effectful logic + * ===================================================================== + * + * A program cell runs a user-provided function. The function receives + * the cell's input (resolved dependency values) and the caller + * context, and returns a value. It can also read/write other cells + * via the runtime handle. + * + * This is the cell type that makes Quilt Turing-complete in the + * "anything you can do in code" sense. Use sparingly — prefer + * formula for pure computations. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * ▲ + * │ imports + * │ + * program.ts ◄── THIS FILE: evaluateProgram() + ProgramRuntime + * ▲ + * │ imports + * │ + * engine.ts (implements ProgramRuntime, calls evaluateProgram) + * + * The engine itself implements the `ProgramRuntime` interface, so + * user code can call `runtime.get('other-cell')` from inside a + * program cell. This is the escape hatch that makes the runtime + * composable from user code. + * + * ===================================================================== + * SECURITY WARNING + * ===================================================================== + * + * Program cells execute arbitrary code in the current process. In + * production, the runtime should sandbox this (e.g. `isolated-vm`, + * `worker_threads`, or WASM). For MVP, we trust the author of the + * sheet. This is documented in the security model and will be + * addressed in v0.2. + * + * The compiler is the host process's `AsyncFunction` constructor. + * Code runs in the global scope of the host. Don't pass user input + * to program cells in a multi-tenant system without sandboxing. + * + * ===================================================================== + */ +import type { Cell, CellValue, CallerContext, CellId } from '../types.js'; +/** + * The runtime handle exposed to user code in program cells. This is + * how a program cell can read, write, or call other cells. + * + * The engine implements this interface (see engine.ts) and passes + * itself in when calling evaluateProgram. + * + * All methods are async because cell calls may themselves be async + * (api cells, program cells, etc.). + */ +export interface ProgramRuntime { + /** Get a cell's current value. */ + get: (id: CellId) => Promise; + /** Set a cell's value. Triggers downstream recomputation. */ + set: (id: CellId, value: unknown) => Promise; + /** Call a cell as a capability, with optional input. */ + call: (id: CellId, input?: unknown) => Promise; +} +/** + * Evaluate a program cell. Compiles the user code as an AsyncFunction + * and invokes it with the input, context, and runtime handle. + * + * The user function can: + * - return any value (becomes the cell's data) + * - throw to produce an error CellValue + * - call `await runtime.get(id)` / `set(id, v)` / `call(id, input)` + * - use the helpers: clamp, abs, min, max + * + * @param cell - the cell instance + * @param ctx - the caller context + * @param input - the cell's input (resolved by the engine) + * @param runtime - the runtime handle for reading/writing other cells + * @returns the computed value, or error + */ +export declare function evaluateProgram(cell: Cell, ctx: CallerContext, input: unknown, runtime: ProgramRuntime): Promise; +//# sourceMappingURL=program.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/program.d.ts.map b/quilt_worker/quilt_core/cells/program.d.ts.map new file mode 100644 index 0000000..53e8513 --- /dev/null +++ b/quilt_worker/quilt_core/cells/program.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/cells/program.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B,kCAAkC;IAClC,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxC,6DAA6D;IAC7D,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,wDAAwD;IACxD,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;CAC3D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,SAAS,CAAC,CA4CpB"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/program.js b/quilt_worker/quilt_core/cells/program.js new file mode 100644 index 0000000..c6fe8a5 --- /dev/null +++ b/quilt_worker/quilt_core/cells/program.js @@ -0,0 +1,100 @@ +/** + * @file cells/program.ts + * @module @quilt/core + * + * ===================================================================== + * THE PROGRAM CELL — stateful, side-effectful logic + * ===================================================================== + * + * A program cell runs a user-provided function. The function receives + * the cell's input (resolved dependency values) and the caller + * context, and returns a value. It can also read/write other cells + * via the runtime handle. + * + * This is the cell type that makes Quilt Turing-complete in the + * "anything you can do in code" sense. Use sparingly — prefer + * formula for pure computations. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * ▲ + * │ imports + * │ + * program.ts ◄── THIS FILE: evaluateProgram() + ProgramRuntime + * ▲ + * │ imports + * │ + * engine.ts (implements ProgramRuntime, calls evaluateProgram) + * + * The engine itself implements the `ProgramRuntime` interface, so + * user code can call `runtime.get('other-cell')` from inside a + * program cell. This is the escape hatch that makes the runtime + * composable from user code. + * + * ===================================================================== + * SECURITY WARNING + * ===================================================================== + * + * Program cells execute arbitrary code in the current process. In + * production, the runtime should sandbox this (e.g. `isolated-vm`, + * `worker_threads`, or WASM). For MVP, we trust the author of the + * sheet. This is documented in the security model and will be + * addressed in v0.2. + * + * The compiler is the host process's `AsyncFunction` constructor. + * Code runs in the global scope of the host. Don't pass user input + * to program cells in a multi-tenant system without sandboxing. + * + * ===================================================================== + */ +/** + * Evaluate a program cell. Compiles the user code as an AsyncFunction + * and invokes it with the input, context, and runtime handle. + * + * The user function can: + * - return any value (becomes the cell's data) + * - throw to produce an error CellValue + * - call `await runtime.get(id)` / `set(id, v)` / `call(id, input)` + * - use the helpers: clamp, abs, min, max + * + * @param cell - the cell instance + * @param ctx - the caller context + * @param input - the cell's input (resolved by the engine) + * @param runtime - the runtime handle for reading/writing other cells + * @returns the computed value, or error + */ +export async function evaluateProgram(cell, ctx, input, runtime) { + if (!cell.def.code) { + return { data: undefined, status: 'error', error: { message: 'program cell has no code' } }; + } + try { + // Build an AsyncFunction so `await` works in user code. The + // constructor of AsyncFunction is `async function (...args) { body }`. + // We grab it from an existing async function's prototype to avoid + // a direct reference to the global (which some bundlers complain + // about). + const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor; + // eslint-disable-next-line no-new-func + const fn = new AsyncFunction('input', 'caller', 'runtime', 'clamp', 'abs', 'min', 'max', cell.def.code); + // Helpers exposed to user code. Clamp is the most useful for + // control systems; abs/min/max are common math primitives. + const clamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi); + const result = await fn(input, ctx, runtime, clamp, Math.abs, Math.min, Math.max); + return { + data: result, + status: 'ready', + computedAt: Date.now(), + }; + } + catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (process.env.QUILT_DEBUG) { + console.error(`[quilt] program cell ${cell.id} failed:`, err); + } + return { data: undefined, status: 'error', error: { message } }; + } +} +//# sourceMappingURL=program.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/program.js.map b/quilt_worker/quilt_core/cells/program.js.map new file mode 100644 index 0000000..dde8a0d --- /dev/null +++ b/quilt_worker/quilt_core/cells/program.js.map @@ -0,0 +1 @@ +{"version":3,"file":"program.js","sourceRoot":"","sources":["../../src/cells/program.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAuBH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAU,EACV,GAAkB,EAClB,KAAc,EACd,OAAuB;IAEvB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACnB,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,0BAA0B,EAAE,EAAE,CAAC;IAC9F,CAAC;IAED,IAAI,CAAC;QACH,4DAA4D;QAC5D,uEAAuE;QACvE,kEAAkE;QAClE,iEAAiE;QACjE,UAAU;QACV,MAAM,aAAa,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,eAAc,CAAC,CAAC,CAAC,WAAW,CAAC;QAC9E,uCAAuC;QACvC,MAAM,EAAE,GAAG,IAAI,aAAa,CAC1B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAC5B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAC5B,IAAI,CAAC,GAAG,CAAC,IAAI,CACd,CAAC;QAEF,6DAA6D;QAC7D,2DAA2D;QAC3D,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAU,EAAE,EAAU,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACnF,MAAM,MAAM,GAAG,MAAM,EAAE,CACrB,KAAK,EACL,GAAG,EACH,OAAO,EACP,KAAK,EACL,IAAI,CAAC,GAAG,EACR,IAAI,CAAC,GAAG,EACR,IAAI,CAAC,GAAG,CACT,CAAC;QAEF,OAAO;YACL,IAAI,EAAE,MAAM;YACZ,MAAM,EAAE,OAAO;YACf,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;SACvB,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAC5B,OAAO,CAAC,KAAK,CAAC,wBAAwB,IAAI,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC;IAClE,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/router.d.ts b/quilt_worker/quilt_core/cells/router.d.ts new file mode 100644 index 0000000..67c92cd --- /dev/null +++ b/quilt_worker/quilt_core/cells/router.d.ts @@ -0,0 +1,76 @@ +/** + * @file cells/router.ts + * @module @quilt/core + * + * ===================================================================== + * THE ROUTER CELL — caller-aware policy + * ===================================================================== + * + * A router cell doesn't compute anything itself. It delegates the + * call to another cell based on rules evaluated in the caller's + * context. This is the "if caller.row > 10 use Model A" primitive. + * + * The grid becomes a *policy mesh* because every router cell can + * read the caller's position and route accordingly. Change one + * router cell's rules — and every caller that routes through it + * changes behavior. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── RouterRule, CallerContext, CellValue + * context.ts ◄── evalWhen (rule evaluation) + * ▲ + * │ imports + * │ + * router.ts ◄── THIS FILE: evaluateRouter() + * ▲ + * │ imports + * │ + * engine.ts (calls evaluateRouter for kind === 'router') + * + * ===================================================================== + * RULE SEMANTICS + * ===================================================================== + * + * Rules are evaluated in order. The first one whose `when` evaluates + * truthy wins. Each rule's `route` can be: + * + * - string: a cell id — delegate to that cell + * e.g. { when: "...", route: "models.precise" } + * + * - { cell, with }: delegate to a cell, then merge `with` into the result + * e.g. { when: "...", route: { cell: "models.precise", with: { tier: "gold" } } } + * + * - { model }: swap the model for the next call (placeholder) + * e.g. { when: "...", route: { model: "gpt-4o" } } + * + * - { value }: return a literal value (no delegation) + * e.g. { when: "...", route: { value: "premium" } } + * + * If no rule matches, the router returns `undefined` with status + * 'ready'. The caller can decide what to do (default, error, etc.). + * + * ===================================================================== + */ +import type { Cell, CellValue, CallerContext } from '../types.js'; +import type { ProgramRuntime } from './program.js'; +/** + * Evaluate a router cell. + * + * Steps: + * 1. Iterate rules in order + * 2. For each rule, eval the `when` in the caller's context + * 3. If truthy, dispatch on the route type + * 4. Return the result of the delegation (or literal) + * 5. If no rule matches, return undefined + * + * @param cell - the cell instance + * @param ctx - the caller context (used to evaluate rules) + * @param input - the input to pass to the delegated cell + * @param runtime - the runtime handle (used to call delegated cells) + * @returns the delegated CellValue, or undefined if no rule matched + */ +export declare function evaluateRouter(cell: Cell, ctx: CallerContext, input: unknown, runtime: ProgramRuntime): Promise; +//# sourceMappingURL=router.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/router.d.ts.map b/quilt_worker/quilt_core/cells/router.d.ts.map new file mode 100644 index 0000000..a247aaf --- /dev/null +++ b/quilt_worker/quilt_core/cells/router.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/cells/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAU,MAAM,aAAa,CAAC;AAE1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,SAAS,CAAC,CA0CpB"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/router.js b/quilt_worker/quilt_core/cells/router.js new file mode 100644 index 0000000..037fdf6 --- /dev/null +++ b/quilt_worker/quilt_core/cells/router.js @@ -0,0 +1,115 @@ +/** + * @file cells/router.ts + * @module @quilt/core + * + * ===================================================================== + * THE ROUTER CELL — caller-aware policy + * ===================================================================== + * + * A router cell doesn't compute anything itself. It delegates the + * call to another cell based on rules evaluated in the caller's + * context. This is the "if caller.row > 10 use Model A" primitive. + * + * The grid becomes a *policy mesh* because every router cell can + * read the caller's position and route accordingly. Change one + * router cell's rules — and every caller that routes through it + * changes behavior. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── RouterRule, CallerContext, CellValue + * context.ts ◄── evalWhen (rule evaluation) + * ▲ + * │ imports + * │ + * router.ts ◄── THIS FILE: evaluateRouter() + * ▲ + * │ imports + * │ + * engine.ts (calls evaluateRouter for kind === 'router') + * + * ===================================================================== + * RULE SEMANTICS + * ===================================================================== + * + * Rules are evaluated in order. The first one whose `when` evaluates + * truthy wins. Each rule's `route` can be: + * + * - string: a cell id — delegate to that cell + * e.g. { when: "...", route: "models.precise" } + * + * - { cell, with }: delegate to a cell, then merge `with` into the result + * e.g. { when: "...", route: { cell: "models.precise", with: { tier: "gold" } } } + * + * - { model }: swap the model for the next call (placeholder) + * e.g. { when: "...", route: { model: "gpt-4o" } } + * + * - { value }: return a literal value (no delegation) + * e.g. { when: "...", route: { value: "premium" } } + * + * If no rule matches, the router returns `undefined` with status + * 'ready'. The caller can decide what to do (default, error, etc.). + * + * ===================================================================== + */ +import { evalWhen } from '../context.js'; +/** + * Evaluate a router cell. + * + * Steps: + * 1. Iterate rules in order + * 2. For each rule, eval the `when` in the caller's context + * 3. If truthy, dispatch on the route type + * 4. Return the result of the delegation (or literal) + * 5. If no rule matches, return undefined + * + * @param cell - the cell instance + * @param ctx - the caller context (used to evaluate rules) + * @param input - the input to pass to the delegated cell + * @param runtime - the runtime handle (used to call delegated cells) + * @returns the delegated CellValue, or undefined if no rule matched + */ +export async function evaluateRouter(cell, ctx, input, runtime) { + if (!cell.def.rules?.length) { + return { data: undefined, status: 'error', error: { message: 'router cell has no rules' } }; + } + for (const rule of cell.def.rules) { + if (evalWhen(rule.when, ctx)) { + // Rule matched — dispatch on route type + if (typeof rule.route === 'string') { + return await runtime.call(rule.route, input); + } + if (typeof rule.route === 'object' && 'cell' in rule.route) { + const result = await runtime.call(rule.route.cell, input); + if (rule.route.with && result.status === 'ready' && result.data && typeof result.data === 'object') { + return { ...result, data: { ...result.data, ...rule.route.with } }; + } + return result; + } + if (typeof rule.route === 'object' && 'model' in rule.route) { + // Model swap — would be implemented by a model provider + return { + data: { model: rule.route.model, input }, + status: 'ready', + computedAt: Date.now(), + }; + } + if (typeof rule.route === 'object' && 'value' in rule.route) { + return { + data: rule.route.value, + status: 'ready', + computedAt: Date.now(), + }; + } + } + } + // No rule matched — return undefined + return { + data: undefined, + status: 'ready', + computedAt: Date.now(), + }; +} +//# sourceMappingURL=router.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/router.js.map b/quilt_worker/quilt_core/cells/router.js.map new file mode 100644 index 0000000..ffa6781 --- /dev/null +++ b/quilt_worker/quilt_core/cells/router.js.map @@ -0,0 +1 @@ +{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/cells/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAU,EACV,GAAkB,EAClB,KAAc,EACd,OAAuB;IAEvB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,0BAA0B,EAAE,EAAE,CAAC;IAC9F,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QAClC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;YAC7B,wCAAwC;YACxC,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;gBACnC,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAe,EAAE,KAAK,CAAC,CAAC;YACzD,CAAC;YACD,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC3D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC1D,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,IAAI,MAAM,CAAC,IAAI,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACnG,OAAO,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,EAAE,GAAI,MAAM,CAAC,IAAe,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;gBACjF,CAAC;gBACD,OAAO,MAAM,CAAC;YAChB,CAAC;YACD,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC5D,wDAAwD;gBACxD,OAAO;oBACL,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE;oBACxC,MAAM,EAAE,OAAO;oBACf,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;iBACvB,CAAC;YACJ,CAAC;YACD,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC5D,OAAO;oBACL,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK;oBACtB,MAAM,EAAE,OAAO;oBACf,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;iBACvB,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,OAAO;QACL,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,OAAO;QACf,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;KACvB,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/sensor.d.ts b/quilt_worker/quilt_core/cells/sensor.d.ts new file mode 100644 index 0000000..5e47c4f --- /dev/null +++ b/quilt_worker/quilt_core/cells/sensor.d.ts @@ -0,0 +1,60 @@ +/** + * @file cells/sensor.ts + * @module @quilt/core + * + * ===================================================================== + * THE SENSOR CELL — streaming input from the outside world + * ===================================================================== + * + * A sensor cell receives values pushed from an external source: an + * MQTT broker, a Modbus device, a GPIO pin, a serial port, a + * simulated stream. The runtime doesn't poll — adapters push values + * in via `engine.push(id, data)`, and the runtime propagates. + * + * For the MVP, the engine's `push` method is the only entry point. + * Real adapters (MQTT, Modbus, GPIO) are external packages that + * call `engine.push` on their event loops. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * ▲ + * │ imports + * │ + * sensor.ts ◄── THIS FILE: makeSensorValue (a tiny factory) + * ▲ + * │ imports + * │ + * engine.ts (calls makeSensorValue when push() is invoked) + * adapters/* (external: MQTT, Modbus, GPIO — push to engine) + * + * Sensors are PUSH-BASED, not pull-based. You cannot call + * `engine.get('sensor.cell')` and expect a fresh reading — you have + * to wait for the adapter to push. This is by design: real sensors + * have their own timing and the runtime shouldn't be in the loop. + * + * ===================================================================== + */ +import type { CellValue } from '../types.js'; +/** + * Result of a push into a sensor cell. (Reserved for future use — + * e.g. for backpressure or throttling stats.) + */ +export interface SensorPushResult { + notified: number; +} +/** + * Build a `CellValue` wrapping a sensor reading. + * + * Used by the engine's `push` method when a sensor cell receives + * an external reading. Adapters that want to push to a sensor + * should use `engine.push(id, data)` instead of calling this + * directly. + * + * @param data - the raw reading from the sensor + * @returns a ready CellValue + */ +export declare function makeSensorValue(data: unknown): CellValue; +//# sourceMappingURL=sensor.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/sensor.d.ts.map b/quilt_worker/quilt_core/cells/sensor.d.ts.map new file mode 100644 index 0000000..330bec7 --- /dev/null +++ b/quilt_worker/quilt_core/cells/sensor.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"sensor.d.ts","sourceRoot":"","sources":["../../src/cells/sensor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAMxD"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/sensor.js b/quilt_worker/quilt_core/cells/sensor.js new file mode 100644 index 0000000..9370144 --- /dev/null +++ b/quilt_worker/quilt_core/cells/sensor.js @@ -0,0 +1,58 @@ +/** + * @file cells/sensor.ts + * @module @quilt/core + * + * ===================================================================== + * THE SENSOR CELL — streaming input from the outside world + * ===================================================================== + * + * A sensor cell receives values pushed from an external source: an + * MQTT broker, a Modbus device, a GPIO pin, a serial port, a + * simulated stream. The runtime doesn't poll — adapters push values + * in via `engine.push(id, data)`, and the runtime propagates. + * + * For the MVP, the engine's `push` method is the only entry point. + * Real adapters (MQTT, Modbus, GPIO) are external packages that + * call `engine.push` on their event loops. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── types + * ▲ + * │ imports + * │ + * sensor.ts ◄── THIS FILE: makeSensorValue (a tiny factory) + * ▲ + * │ imports + * │ + * engine.ts (calls makeSensorValue when push() is invoked) + * adapters/* (external: MQTT, Modbus, GPIO — push to engine) + * + * Sensors are PUSH-BASED, not pull-based. You cannot call + * `engine.get('sensor.cell')` and expect a fresh reading — you have + * to wait for the adapter to push. This is by design: real sensors + * have their own timing and the runtime shouldn't be in the loop. + * + * ===================================================================== + */ +/** + * Build a `CellValue` wrapping a sensor reading. + * + * Used by the engine's `push` method when a sensor cell receives + * an external reading. Adapters that want to push to a sensor + * should use `engine.push(id, data)` instead of calling this + * directly. + * + * @param data - the raw reading from the sensor + * @returns a ready CellValue + */ +export function makeSensorValue(data) { + return { + data, + status: 'ready', + computedAt: Date.now(), + }; +} +//# sourceMappingURL=sensor.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/sensor.js.map b/quilt_worker/quilt_core/cells/sensor.js.map new file mode 100644 index 0000000..e286a5e --- /dev/null +++ b/quilt_worker/quilt_core/cells/sensor.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sensor.js","sourceRoot":"","sources":["../../src/cells/sensor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAYH;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,IAAa;IAC3C,OAAO;QACL,IAAI;QACJ,MAAM,EAAE,OAAO;QACf,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;KACvB,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/value.d.ts b/quilt_worker/quilt_core/cells/value.d.ts new file mode 100644 index 0000000..62f344b --- /dev/null +++ b/quilt_worker/quilt_core/cells/value.d.ts @@ -0,0 +1,51 @@ +/** + * @file cells/value.ts + * @module @quilt/core + * + * ===================================================================== + * THE VALUE CELL — the simplest kind + * ===================================================================== + * + * A value cell is a static piece of data. No dependencies, no + * computation, no effects. Always returns the same value for any + * caller. Used for constants, configuration, and the leaves of the + * dependency graph. + * + * In the spreadsheet metaphor: a value cell is a typed number/string + * in a cell. In the runtime metaphor: a leaf node with no incoming + * edges. In the policy metaphor: a configuration knob. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── Cell, CallerContext, CellValue + * ▲ + * │ imports + * │ + * value.ts ◄── THIS FILE: evaluateValue() + * ▲ + * │ imports + * │ + * engine.ts (calls evaluateValue for kind === 'value') + * + * This is the simplest cell evaluator. It's the spec for what + * "no work" looks like: synchronous, pure, context-independent. + * Use it as a reference when writing other evaluators. + * + * ===================================================================== + */ +import type { Cell, CellValue, CallerContext } from '../types.js'; +/** + * Evaluate a value cell. Trivially returns the configured value. + * + * Pure: same input → same output. No effects. Cached implicitly by + * the engine's `Map` (the cell instance itself is + * the cache). + * + * @param cell - the cell instance (its `def.value` is the payload) + * @param _ctx - the caller context (unused; values don't route) + * @returns a ready CellValue wrapping the static value + */ +export declare function evaluateValue(cell: Cell, _ctx: CallerContext): CellValue; +//# sourceMappingURL=value.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/value.d.ts.map b/quilt_worker/quilt_core/cells/value.d.ts.map new file mode 100644 index 0000000..1f46e10 --- /dev/null +++ b/quilt_worker/quilt_core/cells/value.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"value.d.ts","sourceRoot":"","sources":["../../src/cells/value.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAElE;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,GAAG,SAAS,CAMxE"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/value.js b/quilt_worker/quilt_core/cells/value.js new file mode 100644 index 0000000..69dc406 --- /dev/null +++ b/quilt_worker/quilt_core/cells/value.js @@ -0,0 +1,56 @@ +/** + * @file cells/value.ts + * @module @quilt/core + * + * ===================================================================== + * THE VALUE CELL — the simplest kind + * ===================================================================== + * + * A value cell is a static piece of data. No dependencies, no + * computation, no effects. Always returns the same value for any + * caller. Used for constants, configuration, and the leaves of the + * dependency graph. + * + * In the spreadsheet metaphor: a value cell is a typed number/string + * in a cell. In the runtime metaphor: a leaf node with no incoming + * edges. In the policy metaphor: a configuration knob. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── Cell, CallerContext, CellValue + * ▲ + * │ imports + * │ + * value.ts ◄── THIS FILE: evaluateValue() + * ▲ + * │ imports + * │ + * engine.ts (calls evaluateValue for kind === 'value') + * + * This is the simplest cell evaluator. It's the spec for what + * "no work" looks like: synchronous, pure, context-independent. + * Use it as a reference when writing other evaluators. + * + * ===================================================================== + */ +/** + * Evaluate a value cell. Trivially returns the configured value. + * + * Pure: same input → same output. No effects. Cached implicitly by + * the engine's `Map` (the cell instance itself is + * the cache). + * + * @param cell - the cell instance (its `def.value` is the payload) + * @param _ctx - the caller context (unused; values don't route) + * @returns a ready CellValue wrapping the static value + */ +export function evaluateValue(cell, _ctx) { + return { + data: cell.def.value, + status: 'ready', + computedAt: Date.now(), + }; +} +//# sourceMappingURL=value.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/cells/value.js.map b/quilt_worker/quilt_core/cells/value.js.map new file mode 100644 index 0000000..235cbae --- /dev/null +++ b/quilt_worker/quilt_core/cells/value.js.map @@ -0,0 +1 @@ +{"version":3,"file":"value.js","sourceRoot":"","sources":["../../src/cells/value.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,IAAU,EAAE,IAAmB;IAC3D,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO;QACf,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;KACvB,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/context.d.ts b/quilt_worker/quilt_core/context.d.ts new file mode 100644 index 0000000..0f9462b --- /dev/null +++ b/quilt_worker/quilt_core/context.d.ts @@ -0,0 +1,115 @@ +/** + * @file context.ts + * @module @quilt/core + * + * ===================================================================== + * CALLER CONTEXT PROPAGATION + * ===================================================================== + * + * This file is the heart of "caller-aware" — the primitive that no + * other tool has. Every cell call carries a `CallerContext` (row, + * column, identity, trace). As the call descends into the dependency + * graph, the context extends: the trace accumulates, the caller + * pointer shifts. + * + * A cell can read the context and route on it. `if caller.row > 10 + * then use Model A` is the canonical example. This is what makes + * the grid a *policy mesh* — position is metadata. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── CallerContext, CellId (just types) + * ▲ + * │ imports + * │ + * context.ts ◄── THIS FILE: build, extend, hash, eval-on-context + * ▲ + * │ imports + * │ + * engine.ts (extends context on every call, caches by key) + * cells/router.ts (uses evalWhen to pick rules) + * cells/program.ts (passes context as a parameter) + * cells/api.ts (uses context for URL substitution) + * + * ===================================================================== + * KEY DESIGN DECISIONS + * ===================================================================== + * + * 1. CONTEXT IS IMMUTABLE PER CALL + * The engine extends the context as it descends, but never mutates + * an existing context. Each cell receives a fresh, complete + * snapshot of who called and from where. + * + * 2. CACHE KEY = HASH OF RELEVANT FIELDS + * Same cell + same context → same cached value. The key is built + * from row, column, caller, identity, and tags. The engine uses + * this for per-context memoization. + * + * 3. evalWhen IS A TINY DSL, NOT A FULL LANGUAGE + * The `when` expression in router rules and listener conditions + * is evaluated with `new Function('caller', expr)`. This gives + * us a familiar JS-like syntax without pulling in a parser. The + * security model is "trust the author of the sheet" — for + * untrusted sheets, use a proper expression language. + * + * ===================================================================== + */ +import type { CallerContext, CellId } from './types.js'; +/** + * Build a default empty context. The engine fills in `trace` and + * `timestamp`. Callers add `row`, `column`, `identity`, etc. + */ +export declare function emptyContext(): CallerContext; +/** + * Extend a context as we descend into a dependency. The trace is + * preserved (ancestors), the caller becomes the previous cell, and + * we can attach row/column if the dep is in a specific position. + * + * USAGE: + * When the engine evaluates cell B which depends on A, it calls + * `extendContext(parentCtx, 'A', { row: B.row, column: B.col })`. + * A then sees B as its caller, and B's row/column as its position. + * + * @param parent - the context from the calling cell + * @param childId - the id of the cell being entered + * @param extra - optional overrides (row, column, identity, etc.) + * @returns a fresh CallerContext for the child cell + */ +export declare function extendContext(parent: CallerContext, childId: CellId, extra?: Partial): CallerContext; +/** + * A stable cache key for caller-aware memoization. Same cell, same + * context (by relevant fields) → same cached value. + * + * Fields included: row, column, caller, identity.id, identity.tags. + * Fields excluded: metadata (too volatile), timestamp (always new). + * + * The `` sentinel is used when no relevant fields are set, + * so that "no context" calls still cache. + */ +export declare function contextKey(ctx: CallerContext): string; +/** + * Evaluate a small router expression in a context. Supports a tiny DSL: + * + * caller.row > 10 + * caller.column == "J" + * caller.identity.tags contains "premium" + * caller.row > 10 && caller.column != "A" + * + * How it works: + * 1. The `caller` object is built from the relevant CallerContext fields. + * 2. The expression is compiled with `new Function('caller', ...)`. + * 3. Special syntax `X contains "Y"` is rewritten to `Array.isArray(X) && X.includes("Y")`. + * + * SECURITY: This executes arbitrary JS in the current process. In a + * production deployment with untrusted sheets, replace this with a + * proper expression parser (jsep, expr-eval, etc.) that restricts + * the language. + * + * @param when - the expression to evaluate + * @param ctx - the caller context + * @returns true if the expression evaluates truthy, false otherwise + */ +export declare function evalWhen(when: string, ctx: CallerContext): boolean; +//# sourceMappingURL=context.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/context.d.ts.map b/quilt_worker/quilt_core/context.d.ts.map new file mode 100644 index 0000000..3b2c26d --- /dev/null +++ b/quilt_worker/quilt_core/context.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAExD;;;GAGG;AACH,wBAAgB,YAAY,IAAI,aAAa,CAK5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAC7B,aAAa,CAQf;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAQrD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CA6BlE"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/context.js b/quilt_worker/quilt_core/context.js new file mode 100644 index 0000000..a414ec6 --- /dev/null +++ b/quilt_worker/quilt_core/context.js @@ -0,0 +1,167 @@ +/** + * @file context.ts + * @module @quilt/core + * + * ===================================================================== + * CALLER CONTEXT PROPAGATION + * ===================================================================== + * + * This file is the heart of "caller-aware" — the primitive that no + * other tool has. Every cell call carries a `CallerContext` (row, + * column, identity, trace). As the call descends into the dependency + * graph, the context extends: the trace accumulates, the caller + * pointer shifts. + * + * A cell can read the context and route on it. `if caller.row > 10 + * then use Model A` is the canonical example. This is what makes + * the grid a *policy mesh* — position is metadata. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── CallerContext, CellId (just types) + * ▲ + * │ imports + * │ + * context.ts ◄── THIS FILE: build, extend, hash, eval-on-context + * ▲ + * │ imports + * │ + * engine.ts (extends context on every call, caches by key) + * cells/router.ts (uses evalWhen to pick rules) + * cells/program.ts (passes context as a parameter) + * cells/api.ts (uses context for URL substitution) + * + * ===================================================================== + * KEY DESIGN DECISIONS + * ===================================================================== + * + * 1. CONTEXT IS IMMUTABLE PER CALL + * The engine extends the context as it descends, but never mutates + * an existing context. Each cell receives a fresh, complete + * snapshot of who called and from where. + * + * 2. CACHE KEY = HASH OF RELEVANT FIELDS + * Same cell + same context → same cached value. The key is built + * from row, column, caller, identity, and tags. The engine uses + * this for per-context memoization. + * + * 3. evalWhen IS A TINY DSL, NOT A FULL LANGUAGE + * The `when` expression in router rules and listener conditions + * is evaluated with `new Function('caller', expr)`. This gives + * us a familiar JS-like syntax without pulling in a parser. The + * security model is "trust the author of the sheet" — for + * untrusted sheets, use a proper expression language. + * + * ===================================================================== + */ +/** + * Build a default empty context. The engine fills in `trace` and + * `timestamp`. Callers add `row`, `column`, `identity`, etc. + */ +export function emptyContext() { + return { + trace: [], + timestamp: Date.now(), + }; +} +/** + * Extend a context as we descend into a dependency. The trace is + * preserved (ancestors), the caller becomes the previous cell, and + * we can attach row/column if the dep is in a specific position. + * + * USAGE: + * When the engine evaluates cell B which depends on A, it calls + * `extendContext(parentCtx, 'A', { row: B.row, column: B.col })`. + * A then sees B as its caller, and B's row/column as its position. + * + * @param parent - the context from the calling cell + * @param childId - the id of the cell being entered + * @param extra - optional overrides (row, column, identity, etc.) + * @returns a fresh CallerContext for the child cell + */ +export function extendContext(parent, childId, extra) { + return { + ...parent, + ...extra, + caller: childId, + trace: [...(parent.trace ?? []), parent.caller ?? ''], + timestamp: Date.now(), + }; +} +/** + * A stable cache key for caller-aware memoization. Same cell, same + * context (by relevant fields) → same cached value. + * + * Fields included: row, column, caller, identity.id, identity.tags. + * Fields excluded: metadata (too volatile), timestamp (always new). + * + * The `` sentinel is used when no relevant fields are set, + * so that "no context" calls still cache. + */ +export function contextKey(ctx) { + const parts = []; + if (ctx.row !== undefined) + parts.push(`r:${String(ctx.row)}`); + if (ctx.column !== undefined) + parts.push(`c:${String(ctx.column)}`); + if (ctx.caller) + parts.push(`f:${ctx.caller}`); + if (ctx.identity?.id) + parts.push(`i:${ctx.identity.id}`); + if (ctx.identity?.tags?.length) + parts.push(`t:${ctx.identity.tags.sort().join(',')}`); + return parts.join('|') || ''; +} +/** + * Evaluate a small router expression in a context. Supports a tiny DSL: + * + * caller.row > 10 + * caller.column == "J" + * caller.identity.tags contains "premium" + * caller.row > 10 && caller.column != "A" + * + * How it works: + * 1. The `caller` object is built from the relevant CallerContext fields. + * 2. The expression is compiled with `new Function('caller', ...)`. + * 3. Special syntax `X contains "Y"` is rewritten to `Array.isArray(X) && X.includes("Y")`. + * + * SECURITY: This executes arbitrary JS in the current process. In a + * production deployment with untrusted sheets, replace this with a + * proper expression parser (jsep, expr-eval, etc.) that restricts + * the language. + * + * @param when - the expression to evaluate + * @param ctx - the caller context + * @returns true if the expression evaluates truthy, false otherwise + */ +export function evalWhen(when, ctx) { + try { + // Build a safe-ish scope from the context. We expose only the + // routing-relevant fields so authors can't accidentally (or + // intentionally) read metadata. + const caller = { + row: ctx.row, + column: ctx.column, + sheet: ctx.sheet, + identity: ctx.identity, + metadata: ctx.metadata, + }; + // Replace 'contains' with a method call. This is a syntactic + // convenience — "tags contains 'premium'" reads better than + // "tags.includes('premium')". + const expr = when.replace(/(\w+)\s+contains\s+"([^"]+)"/g, 'Array.isArray($1) && $1.includes("$2")'); + // eslint-disable-next-line no-new-func + const fn = new Function('caller', `return (${expr});`); + return Boolean(fn(caller)); + } + catch (err) { + // Be loud in dev, quiet in prod + if (process.env.QUILT_DEBUG) { + console.error(`[quilt] evalWhen failed: ${when}`, err); + } + return false; + } +} +//# sourceMappingURL=context.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/context.js.map b/quilt_worker/quilt_core/context.js.map new file mode 100644 index 0000000..99817d7 --- /dev/null +++ b/quilt_worker/quilt_core/context.js.map @@ -0,0 +1 @@ +{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAIH;;;GAGG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO;QACL,KAAK,EAAE,EAAE;QACT,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACtB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAqB,EACrB,OAAe,EACf,KAA8B;IAE9B,OAAO;QACL,GAAG,MAAM;QACT,GAAG,KAAK;QACR,MAAM,EAAE,OAAO;QACf,KAAK,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,IAAI,QAAQ,CAAC;QAC3D,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACtB,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,GAAkB;IAC3C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,GAAG,CAAC,GAAG,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9D,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACpE,IAAI,GAAG,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9C,IAAI,GAAG,CAAC,QAAQ,EAAE,EAAE;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;IACzD,IAAI,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACtF,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,GAAkB;IACvD,IAAI,CAAC;QACH,8DAA8D;QAC9D,4DAA4D;QAC5D,gCAAgC;QAChC,MAAM,MAAM,GAAG;YACb,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,QAAQ,EAAE,GAAG,CAAC,QAAQ;SACvB,CAAC;QACF,6DAA6D;QAC7D,4DAA4D;QAC5D,8BAA8B;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CACvB,+BAA+B,EAC/B,wCAAwC,CACzC,CAAC;QACF,uCAAuC;QACvC,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,QAAQ,EAAE,WAAW,IAAI,IAAI,CAAC,CAAC;QACvD,OAAO,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,gCAAgC;QAChC,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAC5B,OAAO,CAAC,KAAK,CAAC,4BAA4B,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/engine.d.ts b/quilt_worker/quilt_core/engine.d.ts new file mode 100644 index 0000000..509af27 --- /dev/null +++ b/quilt_worker/quilt_core/engine.d.ts @@ -0,0 +1,277 @@ +/** + * @file engine.ts + * @module @quilt/core + * + * ===================================================================== + * THE QUILT ENGINE — the reactive runtime + * ===================================================================== + * + * This is the heart of Quilt. It holds the cell graph, tracks + * dependencies, propagates changes, and exposes the universal API: + * `get`, `set`, `call`, `push`, `subscribe`. + * + * Everything else (CLI, MCP, TUI, Web) is a view onto this engine. + * If you understand this file, you understand the system. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── Cell, CellDef, CellId, CellValue, CallerContext + * context.ts ◄── extendContext, contextKey + * cells/*.ts ◄── evaluateValue, evaluateFormula, evaluateApi, + * evaluateProgram, evaluateRouter, fireListener, + * makeSensorValue, makeIoValue + * ▲ + * │ imports + * │ + * engine.ts ◄── THIS FILE: QuiltEngine (the runtime) + * ▲ + * │ imports + * │ + * parser.ts (uses engine.loadSheet to install parsed cells) + * mcp/server.ts (calls engine.get/call/subscribe for MCP tools) + * cli/index.ts (calls engine.get/set/loadSheet for commands) + * + * The engine implements `ProgramRuntime` so that program cells can + * call back into the runtime (read, write, call other cells). This + * is the "user code can compose with the runtime" hook. + * + * ===================================================================== + * THE LIFECYCLE OF A CELL + * ===================================================================== + * + * 1. loadSheet(sheet) — define cells, build dependency graph + * For each CellDef, create a Cell with empty value. + * For formulas, scan the expression and add dep edges. + * For others, use the declared `deps` field. + * + * 2. get(id, ctx) — pull a cell's value, computing if needed + * Pure cells (value, formula): recompute on demand. + * Effectful cells (api, program, router): de-duped, cached. + * Push-based cells (sensor, io, listener): return current. + * + * 3. set(id, value, ctx) — write a value, propagate changes + * Update the cell, mark dependents as stale, + * notify subscribers, fire listeners. + * + * 4. call(id, input, ctx) — invoke a cell as a capability + * Same as get for pure cells. For effectful, pass input. + * + * 5. push(id, data) — push a value into a sensor or IO cell + * External adapters call this. Triggers downstream. + * + * ===================================================================== + * PROPAGATION ALGORITHM + * ===================================================================== + * + * When a cell's value changes (via set or push), the engine walks + * the dependent graph depth-first: + * + * 1. Mark each formula/value dependent as 'stale' and clear its cache + * 2. Recurse into their dependents + * 3. For each listener dependent, check its condition and fire + * 4. Notify external subscribers + * + * Pure cells are not auto-recomputed; they recompute lazily on the + * next get(). Effectful cells are NEVER auto-recomputed by upstream + * changes — they must be called explicitly. + * + * ===================================================================== + * CACHING STRATEGY + * ===================================================================== + * + * Per-context memoization: same cell + same caller context (by + * row/column/identity/tags) → same cached value. + * + * - value cells: no cache needed (always O(1)) + * - formula cells: cache the result by contextKey + * - api/program/router cells: cache the result by contextKey + * - sensor/io/listener cells: not cached (push-based) + * + * Cache is invalidated: + * - On set (to the same cell) + * - On propagation (to formula dependents) + * - Never on context change (we cache per context, not per cell) + * + * ===================================================================== + */ +import type { Cell, CellDef, CellId, CellKind, CallerContext, CellValue, SheetDef, EvaluationTrace } from './types.js'; +import { type AIEngineLike } from './cells/ai.js'; +import type { ProgramRuntime } from './cells/program.js'; +/** + * Engine options. + * + * - maxConcurrency: max simultaneous effectful evaluations + * (not yet enforced in MVP — kept for future scheduler) + * - tracing: whether to record evaluation traces + */ +export interface EngineOptions { + maxConcurrency?: number; + tracing?: boolean; + /** Optional AI engine for `kind: 'ai'` cells. If null, AI cells will error. */ + ai?: AIEngineLike; +} +/** + * The Quilt reactive runtime. One instance per "session" or "agent" + * or "deployment". Holds the cell graph and provides the universal + * API. + */ +export declare class QuiltEngine implements ProgramRuntime { + readonly id: string; + private cells; + private subscriptions; + private inflight; + private traces; + private options; + private subscriptionCounter; + constructor(id?: string, options?: EngineOptions); + /** + * Load a sheet definition into the engine. Resets all cell state. + * + * Steps: + * 1. Clear existing cells, in-flight evaluations, traces + * 2. For each CellDef, create a Cell instance + * 3. Build dependency edges (auto-detect for formulas, declared for others) + * 4. Index dependents (reverse lookup) + * + * After loadSheet, the engine is ready to answer get/set/call. + * No values are computed until something asks for them (pull-based). + */ + loadSheet(sheet: SheetDef): void; + /** + * Define a single cell. Adds it to the graph. + * + * Throws if a cell with the same id already exists. Use this for + * static sheets (via loadSheet) or for one-off cells in tests. + * For dynamic registration (e.g. agents defining cells at runtime), + * use `register()`. + */ + defineCell(def: CellDef): Cell; + /** + * Register a new cell definition after load. Used for dynamic + * registration (e.g. sensors connecting, agents defining new tools). + * + * Unlike defineCell, this also builds dependency edges from the + * declared deps. (Formulas registered dynamically don't get + * auto-detected deps — declare them explicitly.) + */ + register(def: CellDef): Cell; + /** + * Get a cell's value. Evaluates if needed. + * + * Dispatch by cell kind: + * - value: return the static value (no computation) + * - formula: refresh deps, then evaluate + * - api/program/router: de-dupe concurrent calls, evaluate + * - sensor/io/listener: return the current pushed value + * + * Per-context memoization: same cell + same context → cached. + * + * @param id - the cell id + * @param ctx - the caller context (for routing and cache key) + * @returns the cell's value, or an error CellValue + */ + get(id: CellId, ctx?: CallerContext): Promise; + /** + * Set a cell's value. Triggers downstream recomputation. + * + * Steps: + * 1. Update the cell's value and invalidate its caller-aware cache + * 2. Notify external subscribers + * 3. Propagate to dependents (mark stale, fire listeners) + * + * Note: set only changes ONE cell. To update a transaction, you + * make multiple set calls. They're not atomic, but they are + * ordered — set is synchronous from the caller's perspective. + */ + set(id: CellId, value: unknown, ctx?: CallerContext): Promise; + /** + * Call a cell as a capability. Same as get for pure cells, but + * allows passing an `input` argument for effectful cells. + * + * USAGE: + * const v = await engine.call('model.router', userInput, { row: 'boat-1' }); + * // The router receives userInput and routes based on the caller context. + */ + call(id: CellId, input?: unknown, ctx?: CallerContext): Promise; + /** + * Push a value into a sensor or IO cell. Triggers downstream. + * + * Called by external adapters (MQTT, Modbus, GPIO, MCP tools) when + * they have a new reading/event for the cell. + * + * Throws if the cell isn't a sensor or IO. Use set for value/formula. + */ + push(id: CellId, data: unknown, ctx?: CallerContext): Promise; + /** + * Subscribe to a cell's value changes. The callback fires every + * time the cell's value changes (and the optional filter returns + * true, if provided). + * + * Returns a subscription id. Pass it to `unsubscribe` to stop. + */ + subscribe(cellId: CellId, callback: (value: CellValue, prev: CellValue) => void | Promise, filter?: (value: CellValue, prev: CellValue) => boolean): string; + /** + * Stop a subscription. The callback will no longer fire. + */ + unsubscribe(subscriptionId: string): void; + /** + * Get a cell instance by id. Returns undefined if no such cell. + * Use this to inspect a cell's dependencies, dependents, current value. + */ + getCell(id: CellId): Cell | undefined; + /** + * List all cells, optionally filtered by kind. + */ + listCells(kind?: CellKind): Cell[]; + /** + * Get recent evaluation traces (for debugging, time-travel). + */ + getTraces(limit?: number): EvaluationTrace[]; + /** + * Export all cell definitions as an array of CellDef. Used by + * `save` to serialize a runtime state back to YAML. + */ + exportDefs(): CellDef[]; + /** + * Evaluate an effectful cell (api, program, router). Caches the + * result by context, notifies subscribers, and traces if enabled. + */ + private evaluateEffectful; + /** + * Recursively refresh formula/value dependencies before computing + * a formula. This is the "pull" model: we walk down the dep graph + * and ensure all values are computed. + */ + private refreshDeps; + /** + * Propagate a change to all dependents. Mark formula/value + * dependents as stale and invalidate their cache. Fire listener + * dependents whose conditions are met. + */ + private propagate; + /** + * Notify all subscribers of a cell change. Subscriptions can have + * an optional filter that decides whether to fire. + */ + private notify; + /** + * Add a dependency edge: `from` depends on `to`. Updates both + * the forward index (from.dependencies) and the reverse index + * (to.dependents). + */ + private addDep; + /** + * Naive auto-detection of formula dependencies: scan the + * expression for any token that matches a known cell id. + * + * Good enough for MVP. A real implementation would parse the + * expression into an AST and walk it. + * + * For AI cells, we scan the prompt, input, and image fields for + * {{cell.id}} references. + */ + private autoDetectDeps; +} +//# sourceMappingURL=engine.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/engine.d.ts.map b/quilt_worker/quilt_core/engine.d.ts.map new file mode 100644 index 0000000..c983792 --- /dev/null +++ b/quilt_worker/quilt_core/engine.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/engine.js b/quilt_worker/quilt_core/engine.js new file mode 100644 index 0000000..3b2bee8 --- /dev/null +++ b/quilt_worker/quilt_core/engine.js @@ -0,0 +1,618 @@ +/** + * @file engine.ts + * @module @quilt/core + * + * ===================================================================== + * THE QUILT ENGINE — the reactive runtime + * ===================================================================== + * + * This is the heart of Quilt. It holds the cell graph, tracks + * dependencies, propagates changes, and exposes the universal API: + * `get`, `set`, `call`, `push`, `subscribe`. + * + * Everything else (CLI, MCP, TUI, Web) is a view onto this engine. + * If you understand this file, you understand the system. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── Cell, CellDef, CellId, CellValue, CallerContext + * context.ts ◄── extendContext, contextKey + * cells/*.ts ◄── evaluateValue, evaluateFormula, evaluateApi, + * evaluateProgram, evaluateRouter, fireListener, + * makeSensorValue, makeIoValue + * ▲ + * │ imports + * │ + * engine.ts ◄── THIS FILE: QuiltEngine (the runtime) + * ▲ + * │ imports + * │ + * parser.ts (uses engine.loadSheet to install parsed cells) + * mcp/server.ts (calls engine.get/call/subscribe for MCP tools) + * cli/index.ts (calls engine.get/set/loadSheet for commands) + * + * The engine implements `ProgramRuntime` so that program cells can + * call back into the runtime (read, write, call other cells). This + * is the "user code can compose with the runtime" hook. + * + * ===================================================================== + * THE LIFECYCLE OF A CELL + * ===================================================================== + * + * 1. loadSheet(sheet) — define cells, build dependency graph + * For each CellDef, create a Cell with empty value. + * For formulas, scan the expression and add dep edges. + * For others, use the declared `deps` field. + * + * 2. get(id, ctx) — pull a cell's value, computing if needed + * Pure cells (value, formula): recompute on demand. + * Effectful cells (api, program, router): de-duped, cached. + * Push-based cells (sensor, io, listener): return current. + * + * 3. set(id, value, ctx) — write a value, propagate changes + * Update the cell, mark dependents as stale, + * notify subscribers, fire listeners. + * + * 4. call(id, input, ctx) — invoke a cell as a capability + * Same as get for pure cells. For effectful, pass input. + * + * 5. push(id, data) — push a value into a sensor or IO cell + * External adapters call this. Triggers downstream. + * + * ===================================================================== + * PROPAGATION ALGORITHM + * ===================================================================== + * + * When a cell's value changes (via set or push), the engine walks + * the dependent graph depth-first: + * + * 1. Mark each formula/value dependent as 'stale' and clear its cache + * 2. Recurse into their dependents + * 3. For each listener dependent, check its condition and fire + * 4. Notify external subscribers + * + * Pure cells are not auto-recomputed; they recompute lazily on the + * next get(). Effectful cells are NEVER auto-recomputed by upstream + * changes — they must be called explicitly. + * + * ===================================================================== + * CACHING STRATEGY + * ===================================================================== + * + * Per-context memoization: same cell + same caller context (by + * row/column/identity/tags) → same cached value. + * + * - value cells: no cache needed (always O(1)) + * - formula cells: cache the result by contextKey + * - api/program/router cells: cache the result by contextKey + * - sensor/io/listener cells: not cached (push-based) + * + * Cache is invalidated: + * - On set (to the same cell) + * - On propagation (to formula dependents) + * - Never on context change (we cache per context, not per cell) + * + * ===================================================================== + */ +import { emptyContext, extendContext, contextKey } from './context.js'; +import { evaluateValue } from './cells/value.js'; +import { evaluateFormula } from './cells/formula.js'; +import { evaluateApi } from './cells/api.js'; +import { evaluateProgram } from './cells/program.js'; +import { evaluateRouter } from './cells/router.js'; +import { fireListener } from './cells/listener.js'; +import { makeSensorValue } from './cells/sensor.js'; +import { makeIoValue } from './cells/io.js'; +import { evaluateAI } from './cells/ai.js'; +const defaultOptions = { + maxConcurrency: 16, + tracing: false, +}; +/** + * The Quilt reactive runtime. One instance per "session" or "agent" + * or "deployment". Holds the cell graph and provides the universal + * API. + */ +export class QuiltEngine { + id; + cells = new Map(); + subscriptions = new Map(); + inflight = new Map(); + traces = []; + options; + subscriptionCounter = 0; + constructor(id = 'default', options = {}) { + this.id = id; + const { ai, ...rest } = options; + this.options = { ...defaultOptions, ...rest, ai }; + } + // =========================================================================== + // SHEET LIFECYCLE + // =========================================================================== + /** + * Load a sheet definition into the engine. Resets all cell state. + * + * Steps: + * 1. Clear existing cells, in-flight evaluations, traces + * 2. For each CellDef, create a Cell instance + * 3. Build dependency edges (auto-detect for formulas, declared for others) + * 4. Index dependents (reverse lookup) + * + * After loadSheet, the engine is ready to answer get/set/call. + * No values are computed until something asks for them (pull-based). + */ + loadSheet(sheet) { + this.cells.clear(); + this.inflight.clear(); + this.traces = []; + for (const def of sheet.cells) { + this.defineCell(def); + } + // Build dependency edges. For formulas, auto-detect by scanning + // the expression. For everything else, use the declared `deps`. + for (const cell of this.cells.values()) { + if (cell.def.kind === 'formula' || cell.def.kind === 'ai') { + this.autoDetectDeps(cell); + } + for (const dep of cell.def.deps ?? []) { + this.addDep(cell.id, dep); + } + } + } + /** + * Define a single cell. Adds it to the graph. + * + * Throws if a cell with the same id already exists. Use this for + * static sheets (via loadSheet) or for one-off cells in tests. + * For dynamic registration (e.g. agents defining cells at runtime), + * use `register()`. + */ + defineCell(def) { + if (this.cells.has(def.id)) { + throw new Error(`cell already defined: ${def.id}`); + } + // Seed the cell with its initial value. Value cells use + // `def.value`; sensor cells use `def.default` (if any) so + // demo sheets work without an adapter wired up. + let initial = { data: undefined, status: 'idle' }; + if (def.value !== undefined) { + initial = { data: def.value, status: 'ready', computedAt: Date.now() }; + } + else if (def.kind === 'sensor' && def.default !== undefined) { + initial = { data: def.default, status: 'ready', computedAt: Date.now() }; + } + const cell = { + id: def.id, + def, + value: initial, + dependencies: new Set(), + dependents: new Set(), + contextCache: new Map(), + }; + this.cells.set(def.id, cell); + return cell; + } + /** + * Register a new cell definition after load. Used for dynamic + * registration (e.g. sensors connecting, agents defining new tools). + * + * Unlike defineCell, this also builds dependency edges from the + * declared deps. (Formulas registered dynamically don't get + * auto-detected deps — declare them explicitly.) + */ + register(def) { + const cell = this.defineCell(def); + for (const dep of def.deps ?? []) { + this.addDep(def.id, dep); + } + return cell; + } + // =========================================================================== + // THE UNIVERSAL API: get, set, call + // =========================================================================== + /** + * Get a cell's value. Evaluates if needed. + * + * Dispatch by cell kind: + * - value: return the static value (no computation) + * - formula: refresh deps, then evaluate + * - api/program/router: de-dupe concurrent calls, evaluate + * - sensor/io/listener: return the current pushed value + * + * Per-context memoization: same cell + same context → cached. + * + * @param id - the cell id + * @param ctx - the caller context (for routing and cache key) + * @returns the cell's value, or an error CellValue + */ + async get(id, ctx = emptyContext()) { + const cell = this.cells.get(id); + if (!cell) { + return { data: undefined, status: 'error', error: { message: `no such cell: ${id}` } }; + } + const fullCtx = extendContext(ctx, id); + switch (cell.def.kind) { + case 'value': + return evaluateValue(cell, fullCtx); + case 'formula': { + await this.refreshDeps(cell, fullCtx); + return evaluateFormula(cell, fullCtx, this.cells); + } + case 'api': + case 'program': + case 'router': + case 'ai': { + const key = contextKey(fullCtx); + const cached = cell.contextCache.get(key); + if (cached && cached.status === 'ready') { + return cached; + } + if (this.inflight.has(id)) { + return this.inflight.get(id); + } + const promise = this.evaluateEffectful(cell, fullCtx, undefined); + this.inflight.set(id, promise); + try { + const value = await promise; + cell.contextCache.set(key, value); + return value; + } + finally { + this.inflight.delete(id); + } + } + case 'sensor': + case 'io': + case 'listener': + return cell.value; + default: + return { data: undefined, status: 'error', error: { message: `unknown kind: ${cell.def.kind}` } }; + } + } + /** + * Set a cell's value. Triggers downstream recomputation. + * + * Steps: + * 1. Update the cell's value and invalidate its caller-aware cache + * 2. Notify external subscribers + * 3. Propagate to dependents (mark stale, fire listeners) + * + * Note: set only changes ONE cell. To update a transaction, you + * make multiple set calls. They're not atomic, but they are + * ordered — set is synchronous from the caller's perspective. + */ + async set(id, value, ctx = emptyContext()) { + const cell = this.cells.get(id); + if (!cell) { + throw new Error(`no such cell: ${id}`); + } + const fullCtx = extendContext(ctx, id); + const prev = cell.value; + const newValue = { + data: value, + status: 'ready', + computedAt: Date.now(), + }; + cell.value = newValue; + cell.contextCache.clear(); + await this.notify(id, newValue, prev); + await this.propagate(id, fullCtx); + } + /** + * Call a cell as a capability. Same as get for pure cells, but + * allows passing an `input` argument for effectful cells. + * + * USAGE: + * const v = await engine.call('model.router', userInput, { row: 'boat-1' }); + * // The router receives userInput and routes based on the caller context. + */ + async call(id, input, ctx = emptyContext()) { + const cell = this.cells.get(id); + if (!cell) { + return { data: undefined, status: 'error', error: { message: `no such cell: ${id}` } }; + } + const fullCtx = extendContext(ctx, id); + if (cell.def.kind === 'value' || cell.def.kind === 'formula') { + return this.get(id, ctx); + } + if (cell.def.kind === 'sensor' || cell.def.kind === 'io' || cell.def.kind === 'listener') { + return cell.value; + } + const key = contextKey(fullCtx); + const cached = cell.contextCache.get(key); + if (cached && cached.status === 'ready') { + return cached; + } + if (this.inflight.has(id)) { + return this.inflight.get(id); + } + const promise = this.evaluateEffectful(cell, fullCtx, input); + this.inflight.set(id, promise); + try { + const value = await promise; + cell.contextCache.set(key, value); + return value; + } + finally { + this.inflight.delete(id); + } + } + /** + * Push a value into a sensor or IO cell. Triggers downstream. + * + * Called by external adapters (MQTT, Modbus, GPIO, MCP tools) when + * they have a new reading/event for the cell. + * + * Throws if the cell isn't a sensor or IO. Use set for value/formula. + */ + async push(id, data, ctx = emptyContext()) { + const cell = this.cells.get(id); + if (!cell) + throw new Error(`no such cell: ${id}`); + if (cell.def.kind !== 'sensor' && cell.def.kind !== 'io') { + throw new Error(`cannot push to ${cell.def.kind} cell: ${id}`); + } + const newValue = cell.def.kind === 'sensor' ? makeSensorValue(data) : makeIoValue(data); + const prev = cell.value; + cell.value = newValue; + await this.notify(id, newValue, prev); + await this.propagate(id, extendContext(ctx, id)); + } + // =========================================================================== + // SUBSCRIPTIONS + // =========================================================================== + /** + * Subscribe to a cell's value changes. The callback fires every + * time the cell's value changes (and the optional filter returns + * true, if provided). + * + * Returns a subscription id. Pass it to `unsubscribe` to stop. + */ + subscribe(cellId, callback, filter) { + const id = `sub-${++this.subscriptionCounter}`; + this.subscriptions.set(id, { id, cellId, callback, filter }); + return id; + } + /** + * Stop a subscription. The callback will no longer fire. + */ + unsubscribe(subscriptionId) { + this.subscriptions.delete(subscriptionId); + } + // =========================================================================== + // INTROSPECTION + // =========================================================================== + /** + * Get a cell instance by id. Returns undefined if no such cell. + * Use this to inspect a cell's dependencies, dependents, current value. + */ + getCell(id) { + return this.cells.get(id); + } + /** + * List all cells, optionally filtered by kind. + */ + listCells(kind) { + const all = Array.from(this.cells.values()); + return kind ? all.filter(c => c.def.kind === kind) : all; + } + /** + * Get recent evaluation traces (for debugging, time-travel). + */ + getTraces(limit = 100) { + return this.traces.slice(-limit); + } + /** + * Export all cell definitions as an array of CellDef. Used by + * `save` to serialize a runtime state back to YAML. + */ + exportDefs() { + return Array.from(this.cells.values()).map(c => c.def); + } + // =========================================================================== + // INTERNAL: evaluation, propagation, dependencies + // =========================================================================== + /** + * Evaluate an effectful cell (api, program, router). Caches the + * result by context, notifies subscribers, and traces if enabled. + */ + async evaluateEffectful(cell, ctx, input) { + const startedAt = Date.now(); + let result; + if (cell.def.kind === 'api') { + result = await evaluateApi(cell, ctx, input); + } + else if (cell.def.kind === 'program') { + result = await evaluateProgram(cell, ctx, input, this); + } + else if (cell.def.kind === 'router') { + result = await evaluateRouter(cell, ctx, input, this); + } + else if (cell.def.kind === 'ai') { + if (!this.options.ai) { + result = { data: null, status: 'error', error: { message: 'AI cell evaluated but no AI engine configured. Pass an `ai` engine to the QuiltEngine constructor.' } }; + } + else { + // Recursively resolve upstream cell values for {{id}} substitution + const resolved = new Set(); + const resolver = (id) => { + if (resolved.has(id)) + return null; + resolved.add(id); + const c = this.cells.get(id); + if (!c) + return null; + if (c.value.status === 'ready') + return c.value.data; + if (c.value.status === 'computing') { + // Upstream not yet evaluated — for value cells, evaluate sync + if (c.def.kind === 'value') { + c.value = evaluateValue(c, ctx); + return c.value.data; + } + return null; + } + if (c.value.status === 'error') + return null; + return c.value.data; + }; + const aiResult = await evaluateAI(cell.def, ctx, this.options.ai, resolver); + result = { + data: aiResult.value, + status: aiResult.error ? 'error' : 'ready', + error: aiResult.error ? { message: aiResult.error } : undefined, + effects: [{ kind: 'model', provider: cell.def.provider || 'unknown' }], + computedAt: Date.now(), + }; + } + } + else { + result = { data: undefined, status: 'error', error: { message: `not effectful: ${cell.def.kind}` } }; + } + if (this.options.tracing) { + this.traces.push({ + cellId: cell.id, + startedAt, + completedAt: Date.now(), + durationMs: Date.now() - startedAt, + context: ctx, + effects: result.effects, + error: result.error, + }); + } + const prev = cell.value; + cell.value = result; + await this.notify(cell.id, result, prev); + return result; + } + /** + * Recursively refresh formula/value dependencies before computing + * a formula. This is the "pull" model: we walk down the dep graph + * and ensure all values are computed. + */ + async refreshDeps(cell, ctx) { + for (const depId of cell.dependencies) { + const dep = this.cells.get(depId); + if (!dep) + continue; + if (dep.def.kind === 'value' && dep.value.status !== 'ready') { + dep.value = evaluateValue(dep, ctx); + } + else if (dep.def.kind === 'formula' && dep.value.status !== 'ready') { + await this.refreshDeps(dep, ctx); + const v = evaluateFormula(dep, ctx, this.cells); + dep.value = v; + dep.contextCache.set(contextKey(ctx), v); + } + } + } + /** + * Propagate a change to all dependents. Mark formula/value + * dependents as stale and invalidate their cache. Fire listener + * dependents whose conditions are met. + */ + async propagate(changedId, ctx) { + const cell = this.cells.get(changedId); + if (!cell) + return; + for (const depId of cell.dependents) { + const dep = this.cells.get(depId); + if (!dep) + continue; + // Effectful cells (api, program, router, ai) also need cache invalidation + // when an upstream value changes + if (dep.def.kind === 'formula' || dep.def.kind === 'value' || dep.def.kind === 'ai') { + dep.value = { ...dep.value, status: 'stale' }; + dep.contextCache.clear(); + } + await this.propagate(depId, ctx); + } + for (const depId of cell.dependents) { + const dep = this.cells.get(depId); + if (!dep || dep.def.kind !== 'listener') + continue; + await fireListener(dep, changedId, cell.value, cell.value, this); + } + } + /** + * Notify all subscribers of a cell change. Subscriptions can have + * an optional filter that decides whether to fire. + */ + async notify(id, value, prev) { + for (const sub of this.subscriptions.values()) { + if (sub.cellId !== id) + continue; + if (sub.filter && !sub.filter(value, prev)) + continue; + try { + await sub.callback(value, prev); + } + catch (err) { + if (process.env.QUILT_DEBUG) { + console.error(`[quilt] subscription error for ${id}:`, err); + } + } + } + } + /** + * Add a dependency edge: `from` depends on `to`. Updates both + * the forward index (from.dependencies) and the reverse index + * (to.dependents). + */ + addDep(from, to) { + const fromCell = this.cells.get(from); + const toCell = this.cells.get(to); + if (!fromCell || !toCell) + return; + fromCell.dependencies.add(to); + toCell.dependents.add(from); + } + /** + * Naive auto-detection of formula dependencies: scan the + * expression for any token that matches a known cell id. + * + * Good enough for MVP. A real implementation would parse the + * expression into an AST and walk it. + * + * For AI cells, we scan the prompt, input, and image fields for + * {{cell.id}} references. + */ + autoDetectDeps(cell) { + const fields = []; + if (cell.def.expr) + fields.push(cell.def.expr); + if (cell.def.kind === 'ai') { + const ai = cell.def; + if (ai.prompt) + fields.push(ai.prompt); + if (ai.input) + fields.push(ai.input); + if (ai.image) + fields.push(ai.image); + } + if (fields.length === 0) + return; + const text = fields.join(' '); + const knownIds = new Set(this.cells.keys()); + for (const id of knownIds) { + if (id === cell.id) + continue; + // Match either {{id}} (template) or \bid\b (raw token) + const reTemplate = new RegExp(`\\{\\{\\s*${escapeRegex(id)}\\s*\\}\\}`); + const reToken = new RegExp(`\\b${escapeRegex(id)}\\b`); + if (reTemplate.test(text) || reToken.test(text)) { + this.addDep(cell.id, id); + } + } + } +} +/** + * Escape a string for safe use inside a RegExp. + * Used by `autoDetectDeps` when building patterns to match cell ids. + */ +function escapeRegex(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} +//# sourceMappingURL=engine.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/engine.js.map b/quilt_worker/quilt_core/engine.js.map new file mode 100644 index 0000000..bd25dc6 --- /dev/null +++ b/quilt_worker/quilt_core/engine.js.map @@ -0,0 +1 @@ +{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/index.d.ts b/quilt_worker/quilt_core/index.d.ts new file mode 100644 index 0000000..170e1ea --- /dev/null +++ b/quilt_worker/quilt_core/index.d.ts @@ -0,0 +1,37 @@ +/** + * @file index.ts + * @module @quilt/core + * + * ===================================================================== + * PUBLIC API OF @quilt/core + * ===================================================================== + * + * This file is the single entry point for the Quilt runtime. It + * re-exports everything consumers need: types, the engine, the + * parser, and the individual cell evaluators. + * + * Consumers typically import: + * - `QuiltEngine` to instantiate the runtime + * - `parseSheet` to load YAML + * - The types for type annotations + * + * The cell evaluators are exported for advanced use (custom cell + * types, testing) but most consumers won't need them. + * + * ===================================================================== + */ +export type { CellId, CellRef, CellKind, CellStatus, CellValue, Effect, CallerContext, CellDef, Cell, SheetDef, RouterRule, Subscription, EvaluationTrace, } from './types.js'; +export { QuiltEngine } from './engine.js'; +export type { EngineOptions } from './engine.js'; +export { emptyContext, extendContext, contextKey, evalWhen, } from './context.js'; +export { parseSheet, validateSheet, serializeSheet } from './parser.js'; +export { evaluateValue } from './cells/value.js'; +export { evaluateFormula } from './cells/formula.js'; +export { evaluateApi } from './cells/api.js'; +export { evaluateProgram, type ProgramRuntime } from './cells/program.js'; +export { evaluateRouter } from './cells/router.js'; +export { fireListener } from './cells/listener.js'; +export { makeSensorValue } from './cells/sensor.js'; +export { makeIoValue } from './cells/io.js'; +export { evaluateAI, type AIKind, type AICellConfig, type AIEngineLike } from './cells/ai.js'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/index.d.ts.map b/quilt_worker/quilt_core/index.d.ts.map new file mode 100644 index 0000000..5990dc9 --- /dev/null +++ b/quilt_worker/quilt_core/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH,YAAY,EACV,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EACxD,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAChE,eAAe,GAChB,MAAM,YAAY,CAAC;AAMpB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD,OAAO,EACL,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,GAClD,MAAM,cAAc,CAAC;AAMtB,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAMxE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/index.js b/quilt_worker/quilt_core/index.js new file mode 100644 index 0000000..ff9e8fb --- /dev/null +++ b/quilt_worker/quilt_core/index.js @@ -0,0 +1,47 @@ +/** + * @file index.ts + * @module @quilt/core + * + * ===================================================================== + * PUBLIC API OF @quilt/core + * ===================================================================== + * + * This file is the single entry point for the Quilt runtime. It + * re-exports everything consumers need: types, the engine, the + * parser, and the individual cell evaluators. + * + * Consumers typically import: + * - `QuiltEngine` to instantiate the runtime + * - `parseSheet` to load YAML + * - The types for type annotations + * + * The cell evaluators are exported for advanced use (custom cell + * types, testing) but most consumers won't need them. + * + * ===================================================================== + */ +// ===================================================================== +// ENGINE — the runtime +// ===================================================================== +export { QuiltEngine } from './engine.js'; +// ===================================================================== +// CONTEXT — propagation and evaluation +// ===================================================================== +export { emptyContext, extendContext, contextKey, evalWhen, } from './context.js'; +// ===================================================================== +// PARSER — YAML loading and saving +// ===================================================================== +export { parseSheet, validateSheet, serializeSheet } from './parser.js'; +// ===================================================================== +// CELL EVALUATORS — for advanced use and testing +// ===================================================================== +export { evaluateValue } from './cells/value.js'; +export { evaluateFormula } from './cells/formula.js'; +export { evaluateApi } from './cells/api.js'; +export { evaluateProgram } from './cells/program.js'; +export { evaluateRouter } from './cells/router.js'; +export { fireListener } from './cells/listener.js'; +export { makeSensorValue } from './cells/sensor.js'; +export { makeIoValue } from './cells/io.js'; +export { evaluateAI } from './cells/ai.js'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/index.js.map b/quilt_worker/quilt_core/index.js.map new file mode 100644 index 0000000..014dce7 --- /dev/null +++ b/quilt_worker/quilt_core/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,wEAAwE;AACxE,wBAAwB;AACxB,wEAAwE;AAExE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,wEAAwE;AACxE,wCAAwC;AACxC,wEAAwE;AAExE,OAAO,EACL,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,GAClD,MAAM,cAAc,CAAC;AAEtB,wEAAwE;AACxE,oCAAoC;AACpC,wEAAwE;AAExE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAExE,wEAAwE;AACxE,kDAAkD;AAClD,wEAAwE;AAExE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAuB,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAqD,MAAM,eAAe,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/parser.d.ts b/quilt_worker/quilt_core/parser.d.ts new file mode 100644 index 0000000..74d0aa0 --- /dev/null +++ b/quilt_worker/quilt_core/parser.d.ts @@ -0,0 +1,87 @@ +/** + * @file parser.ts + * @module @quilt/core + * + * ===================================================================== + * THE YAML SHEET PARSER + * ===================================================================== + * + * Loads a sheet definition from YAML. Validates against the SheetDef + * schema. Throws clear errors on bad input — bad YAML should never + * crash the runtime. + * + * The parser is intentionally light on validation: it checks shape + * but not semantic correctness. A formula with an unknown cell + * reference will only fail at evaluation time. This is a deliberate + * trade-off — early validation would require resolving all symbols, + * which couples the parser to the engine. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── SheetDef, CellDef, CellKind + * ▲ + * │ imports + * │ + * parser.ts ◄── THIS FILE: parseSheet(), validateSheet(), serializeSheet() + * ▲ + * │ imports + * │ + * cli/index.ts (loads sheets from the filesystem) + * mcp/server.ts (could load sheets on startup) + * + * ===================================================================== + * WHAT THE PARSER DOES (AND DOESN'T) + * ===================================================================== + * + * Does: + * - Parse YAML via the `yaml` package + * - Verify the top-level shape (id, cells) + * - Verify each cell has a valid kind + * - Detect duplicate cell ids + * + * Doesn't (yet): + * - Validate formula expressions + * - Check that referenced cells exist + * - Enforce type signatures + * - Resolve cross-sheet references + * + * These can be added as additional validation passes. The current + * goal is to catch structural errors with clear messages, then let + * the runtime catch semantic errors. + * + * ===================================================================== + */ +import type { SheetDef } from './types.js'; +/** + * Parse a sheet from a YAML string. Returns a validated SheetDef. + * + * Throws on: + * - invalid YAML + * - top-level not an object + * - missing or non-string `id` + * - missing or non-array `cells` + * - cell with missing id or invalid kind + * - duplicate cell ids + * + * @param source - the YAML source string + * @returns a validated SheetDef + */ +export declare function parseSheet(source: string): SheetDef; +/** + * Validate a raw object as a SheetDef. Used by parseSheet and also + * exported so callers that have already parsed YAML (e.g. from JSON) + * can validate without re-parsing. + */ +export declare function validateSheet(raw: unknown): SheetDef; +/** + * Serialize a SheetDef back to YAML. The inverse of parseSheet. + * + * Used by: + * - The CLI's save command + * - The web UI's export feature + * - Tests that round-trip through YAML + */ +export declare function serializeSheet(sheet: SheetDef): string; +//# sourceMappingURL=parser.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/parser.d.ts.map b/quilt_worker/quilt_core/parser.d.ts.map new file mode 100644 index 0000000..003023c --- /dev/null +++ b/quilt_worker/quilt_core/parser.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAqB,MAAM,YAAY,CAAC;AAM9D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAMnD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CA+BpD;AAmBD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAEtD"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/parser.js b/quilt_worker/quilt_core/parser.js new file mode 100644 index 0000000..9265a79 --- /dev/null +++ b/quilt_worker/quilt_core/parser.js @@ -0,0 +1,142 @@ +/** + * @file parser.ts + * @module @quilt/core + * + * ===================================================================== + * THE YAML SHEET PARSER + * ===================================================================== + * + * Loads a sheet definition from YAML. Validates against the SheetDef + * schema. Throws clear errors on bad input — bad YAML should never + * crash the runtime. + * + * The parser is intentionally light on validation: it checks shape + * but not semantic correctness. A formula with an unknown cell + * reference will only fail at evaluation time. This is a deliberate + * trade-off — early validation would require resolving all symbols, + * which couples the parser to the engine. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * types.ts ◄── SheetDef, CellDef, CellKind + * ▲ + * │ imports + * │ + * parser.ts ◄── THIS FILE: parseSheet(), validateSheet(), serializeSheet() + * ▲ + * │ imports + * │ + * cli/index.ts (loads sheets from the filesystem) + * mcp/server.ts (could load sheets on startup) + * + * ===================================================================== + * WHAT THE PARSER DOES (AND DOESN'T) + * ===================================================================== + * + * Does: + * - Parse YAML via the `yaml` package + * - Verify the top-level shape (id, cells) + * - Verify each cell has a valid kind + * - Detect duplicate cell ids + * + * Doesn't (yet): + * - Validate formula expressions + * - Check that referenced cells exist + * - Enforce type signatures + * - Resolve cross-sheet references + * + * These can be added as additional validation passes. The current + * goal is to catch structural errors with clear messages, then let + * the runtime catch semantic errors. + * + * ===================================================================== + */ +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; +const VALID_KINDS = [ + 'value', 'formula', 'api', 'program', 'sensor', 'listener', 'router', 'io', 'ai', +]; +/** + * Parse a sheet from a YAML string. Returns a validated SheetDef. + * + * Throws on: + * - invalid YAML + * - top-level not an object + * - missing or non-string `id` + * - missing or non-array `cells` + * - cell with missing id or invalid kind + * - duplicate cell ids + * + * @param source - the YAML source string + * @returns a validated SheetDef + */ +export function parseSheet(source) { + const raw = parseYaml(source); + if (typeof raw !== 'object' || raw === null) { + throw new Error('sheet must be a YAML mapping'); + } + return validateSheet(raw); +} +/** + * Validate a raw object as a SheetDef. Used by parseSheet and also + * exported so callers that have already parsed YAML (e.g. from JSON) + * can validate without re-parsing. + */ +export function validateSheet(raw) { + if (typeof raw !== 'object' || raw === null) { + throw new Error('sheet must be an object'); + } + const r = raw; + if (typeof r.id !== 'string') { + throw new Error('sheet.id must be a string'); + } + if (!Array.isArray(r.cells)) { + throw new Error('sheet.cells must be an array'); + } + const cells = r.cells.map((c, i) => validateCell(c, i)); + // Check for duplicate ids — a common mistake when copy-pasting + const seen = new Set(); + for (const c of cells) { + if (seen.has(c.id)) { + throw new Error(`duplicate cell id: ${c.id}`); + } + seen.add(c.id); + } + return { + id: r.id, + title: typeof r.title === 'string' ? r.title : undefined, + description: typeof r.description === 'string' ? r.description : undefined, + version: typeof r.version === 'string' ? r.version : undefined, + axes: r.axes && typeof r.axes === 'object' ? r.axes : undefined, + cells, + }; +} +/** + * Validate a raw object as a CellDef. Called per-cell by validateSheet. + */ +function validateCell(raw, index) { + if (typeof raw !== 'object' || raw === null) { + throw new Error(`cell at index ${index} must be an object`); + } + const c = raw; + if (typeof c.id !== 'string') { + throw new Error(`cell at index ${index} must have an id`); + } + if (typeof c.kind !== 'string' || !VALID_KINDS.includes(c.kind)) { + throw new Error(`cell ${c.id} has invalid kind: ${String(c.kind)}`); + } + return c; +} +/** + * Serialize a SheetDef back to YAML. The inverse of parseSheet. + * + * Used by: + * - The CLI's save command + * - The web UI's export feature + * - Tests that round-trip through YAML + */ +export function serializeSheet(sheet) { + return stringifyYaml(sheet, { lineWidth: 120 }); +} +//# sourceMappingURL=parser.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/parser.js.map b/quilt_worker/quilt_core/parser.js.map new file mode 100644 index 0000000..af47f5c --- /dev/null +++ b/quilt_worker/quilt_core/parser.js.map @@ -0,0 +1 @@ +{"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,MAAM,CAAC;AAGtE,MAAM,WAAW,GAAe;IAC9B,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI;CACjF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAC9B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IACD,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC/C,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,KAAK,GAAc,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAEnE,+DAA+D;IAC/D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IAED,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;QACxD,WAAW,EAAE,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;QAC1E,OAAO,EAAE,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;QAC9D,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAwB,CAAC,CAAC,CAAC,SAAS;QACnF,KAAK;KACN,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,GAAY,EAAE,KAAa;IAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,iBAAiB,KAAK,oBAAoB,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,iBAAiB,KAAK,kBAAkB,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAgB,CAAC,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,sBAAsB,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,CAAuB,CAAC;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,OAAO,aAAa,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;AAClD,CAAC"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/types.d.ts b/quilt_worker/quilt_core/types.d.ts new file mode 100644 index 0000000..d68c1d5 --- /dev/null +++ b/quilt_worker/quilt_core/types.d.ts @@ -0,0 +1,416 @@ +/** + * @file types.ts + * @module @quilt/core + * + * ===================================================================== + * THE TYPE VOCABULARY OF THE QUILT RUNTIME + * ===================================================================== + * + * This file is the single source of truth for what a "cell" is, what + * kinds of cells exist, what values flow through them, and what + * context travels with every call. + * + * If you are a zero-shot agent landing in this codebase, START HERE. + * Read this file, then `engine.ts`, then `context.ts`. With those three + * you have the mental model. The cell evaluators in `cells/*.ts` are + * then just implementations of `evaluate*(cell, ctx, ...) -> CellValue`. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * This file is consumed by EVERY other file in the runtime. It is + * leaf-level: it imports nothing from this project. Every other file + * imports from here. + * + * ┌─────────────────────────────────────────────────────────────┐ + * │ types.ts ◄── this file │ + * │ ▲ │ + * │ │ imports │ + * │ │ │ + * │ ├── context.ts (extends CallerContext) │ + * │ ├── engine.ts (holds Map) │ + * │ ├── cells/*.ts (read Cell, CellDef, CellValue) │ + * │ ├── parser.ts (produces CellDef from YAML) │ + * │ └── mcp/server.ts (exposes cells as MCP tools) │ + * └─────────────────────────────────────────────────────────────┘ + * + * ===================================================================== + * KEY DESIGN DECISIONS + * ===================================================================== + * + * 1. STABLE IDENTITY OVER COORDINATES + * A cell is addressed by its `id` (e.g. "compass.heading"), not + * by A1/B2-style coordinates. Coordinates are display-only. This + * means inserting/reordering cells never breaks references. + * + * 2. EIGHT CELL KINDS, NOT ONE + * A cell is a tagged union of eight kinds. Each kind has different + * evaluation semantics (pull vs push, sync vs async, pure vs + * effectful). The kind is the only thing that determines how the + * engine treats the cell. + * + * 3. CALLER CONTEXT IS FIRST-CLASS + * Every cell call carries a `CallerContext` (row, column, identity, + * trace). The engine fills in the trace automatically. This is + * what makes caller-aware routing possible. + * + * 4. VALUES CARRY THEIR OWN STATUS + * A `CellValue` has a `status` field. There's no separate "is it + * ready?" check. A consumer reads the status to know whether the + * value is fresh, computing, errored, or stale. + * + * 5. EFFECTS ARE DECLARED + * Pure cells have no effects. Effectful cells (api, program) declare + * what they did (network calls, model invocations, etc.) via the + * `effects` field. This is what makes cost/latency reasoning and + * decomposition tractable. + * + * ===================================================================== + * THE METAPHOR + * ===================================================================== + * + * A `Cell` is a SOCKET, not a value. You can plug any implementation + * into a socket. Changing the implementation changes every dependent + * automatically — that is dependency injection at the spreadsheet level. + * + * A `CellDef` is the SPEC for a socket. The runtime instantiates a + * `Cell` (the live thing) from a `CellDef` (the static description). + * + * A `CellValue` is the CURRENT STATE of a socket. It carries its own + * status so consumers know what they're getting. + * + * A `CallerContext` is the wiring context. It tells the socket who's + * asking and from where, so it can route. + * + * A `Subscription` is someone watching a socket for changes. + * + * An `Effect` is something a socket did that has consequences outside + * itself. Network call, model invocation, file write. + * + * A `SheetDef` is the wiring diagram: all the sockets and how they + * connect. + * + * ===================================================================== + */ +/** + * The stable identity of a cell. This is what other cells reference. + * NOT a coordinate. Survives reordering, sheet splits, and refactors. + * + * Examples: + * "compass.heading" + * "fleet.boat1.rudder" + * "router.model" + * + * Convention: dot-separated namespace. No length limit. The parser + * does not enforce any particular naming convention; the runtime + * treats ids as opaque strings. + */ +export type CellId = string; +/** + * A reference to another cell. Same shape as `CellId` for now, but + * typed separately so we can later add expressions like + * "fleet.boat*.rudder" (range) or "router.model?caller.row>10" + * (conditional) without breaking existing code. + */ +export type CellRef = string; +/** + * The kind of cell. Determines evaluation semantics. + * + * ┌──────────┬────────────┬──────────┬───────────┬───────────────┐ + * │ Kind │ Trigger │ Pure? │ Async? │ Has effects? │ + * ├──────────┼────────────┼──────────┼───────────┼───────────────┤ + * │ value │ never │ yes │ no │ no │ + * │ formula │ pull │ yes │ no │ no │ + * │ api │ call │ no │ yes │ yes (network) │ + * │ program │ call │ no │ yes │ yes (any) │ + * │ sensor │ push │ no │ no │ no │ + * │ listener │ dep change │ no │ yes │ yes (action) │ + * │ router │ call │ no │ yes │ no (delegate) │ + * │ io │ push │ no │ no │ yes (bidir) │ + * └──────────┴────────────┴──────────┴───────────┴───────────────┘ + * + * The split between PURE (value, formula) and EFFECTFUL (everything + * else) is the most important distinction. Pure cells recompute + * eagerly when dependencies change. Effectful cells recompute only + * when explicitly called or pushed, and they can declare what they + * did via the `effects` field on the resulting `CellValue`. + */ +export type CellKind = 'value' | 'formula' | 'api' | 'program' | 'sensor' | 'listener' | 'router' | 'io' | 'ai'; +/** + * The status of a cell's current value. + * + * - idle: has never been evaluated, or has no value yet + * - computing: evaluation in flight (async) + * - ready: value is fresh and trustworthy + * - error: last evaluation failed (see `error` field) + * - stale: dependencies changed, needs recompute + * + * Consumers read this to know whether the value is usable. There's + * never a separate "is this ready?" call — the status is the answer. + */ +export type CellStatus = 'idle' | 'computing' | 'ready' | 'error' | 'stale'; +/** + * The current value of a cell, with metadata. Cells always know their + * own status. + * + * Example: + * ```ts + * const v: CellValue = { + * data: 42, + * status: 'ready', + * computedAt: 1700000000000, + * effects: [{ kind: 'network', url: '...', method: 'GET' }], + * }; + * ``` + */ +export interface CellValue { + /** The actual payload. Any JSON-serializable value. */ + data: unknown; + /** What the value's status is. */ + status: CellStatus; + /** Epoch ms when this value was last computed. */ + computedAt?: number; + /** Set if status is 'error'. */ + error?: { + message: string; + stack?: string; + }; + /** What the cell did to produce this value. Empty for pure cells. */ + effects?: Effect[]; +} +/** + * Effects are what a cell *did* during evaluation. Pure cells have no + * effects. Effectful cells declare their effects so the runtime can: + * - reason about cost (sum effects over a call chain) + * - debounce/retry (don't fire the same effect twice in N ms) + * - show them in the UI (audit log) + * - decompose them into cheaper cells over time + * + * The `kind` is a discriminator. Each variant has its own fields. + */ +export type Effect = { + kind: 'network'; + url: string; + method: string; +} | { + kind: 'storage'; + op: 'read' | 'write'; + key: string; +} | { + kind: 'io'; + port: string; + direction: 'in' | 'out'; +} | { + kind: 'model'; + provider: string; + tokensIn?: number; + tokensOut?: number; +} | { + kind: 'compute'; + ms: number; +}; +/** + * The context carried with every cell call. This is what makes + * caller-aware routing possible. + * + * The engine fills in `trace` and `timestamp` automatically as a call + * descends into the dependency graph. Callers can attach `identity`, + * `metadata`, and the spatial axes (row/column) for routing. + * + * ┌─────────────┐ + * │ caller.row │──── routes by tenant + * │ caller.col │──── routes by capability + * │ caller.id │──── routes by user + * │ caller.tags │──── routes by tier + * │ caller.trace │──── provenance / debugging + * └─────────────┘ + */ +export interface CallerContext { + /** Spatial row — the "who" (tenant, device, instance) */ + row?: number | string; + /** Spatial column — the "what" (capability, property, model) */ + column?: number | string; + /** Which sheet this is (for cross-sheet refs in the future) */ + sheet?: string; + /** The cell that initiated this call (the immediate caller) */ + caller?: CellId; + /** The full ancestor chain (for provenance/tracing) */ + trace?: CellId[]; + /** Who is making the call */ + identity?: { + id: string; + type: 'human' | 'agent' | 'sensor' | 'system'; + tags?: string[]; + }; + /** Arbitrary metadata. Use this for application-specific context. */ + metadata?: Record; + /** Epoch ms when this call started */ + timestamp: number; +} +/** + * A router rule. The `when` is a small expression evaluated in the + * caller's context. The `route` can be: + * - a string cell id (delegate to that cell) + * - an object with `cell` (delegate) and `with` (merge extra context) + * - an object with `model` (swap the model for the next call) + * - an object with `value` (return a literal) + * + * The first rule that matches wins. If no rule matches, the router + * returns undefined (the caller can decide what to do). + * + * Example: + * ```yaml + * rules: + * - when: 'caller.row > 10' + * route: { cell: 'models.precise' } + * - when: 'caller.identity.tags contains "premium"' + * route: { model: 'gpt-4o' } + * - when: 'true' + * route: { cell: 'models.fast' } + * ``` + */ +export interface RouterRule { + when: string; + route: string | { + cell: CellRef; + with?: Record; + } | { + model: string; + } | { + value: unknown; + }; +} +/** + * A cell definition. This is the spec — what comes out of the YAML + * parser. The runtime instantiates a `Cell` (the live thing) from a + * `CellDef` (the static description). + * + * Field usage by kind: + * - value: `value` + * - formula: `expr` + * - api: `endpoint`, `method`, `headers` + * - program: `code`, `language` + * - sensor: `source`, `rate` + * - listener: `watch`, `condition`, `action` + * - router: `rules` + * - io: `port`, `direction` + * + * `deps` is for explicit dependencies. For formulas, the engine also + * auto-detects by scanning the expression. + */ +export interface CellDef { + id: CellId; + kind: CellKind; + value?: unknown; + expr?: string; + endpoint?: string; + method?: string; + headers?: Record; + code?: string; + language?: 'javascript' | 'python' | 'wasm'; + source?: string; + rate?: number; + default?: unknown; + watch?: CellRef[]; + condition?: string; + action?: string; + rules?: RouterRule[]; + port?: string; + direction?: 'in' | 'out' | 'bidirectional'; + description?: string; + unit?: string; + inputType?: string; + outputType?: string; + deps?: CellRef[]; + permissions?: { + read?: string[]; + write?: string[]; + call?: string[]; + }; +} +/** + * A live cell instance. The runtime's working representation. + * + * Built from a `CellDef` by `engine.loadSheet()` or `engine.register()`. + * The engine holds these in a `Map`. + */ +export interface Cell { + id: CellId; + def: CellDef; + /** Current value (with status) */ + value: CellValue; + /** Dependencies (filled in at load time) */ + dependencies: Set; + /** Cells that depend on this one (reverse index, filled in at load) */ + dependents: Set; + /** Last evaluation context (for diagnostics) */ + lastContext?: CallerContext; + /** + * Per-context cache. Same cell + different caller context = + * different cached value. Key is `contextKey(ctx)`. + */ + contextCache: Map; +} +/** + * A subscription to a cell. Fires when the cell's value changes + * (according to the optional `filter`). + * + * Used by: + * - The MCP server (notify clients of cell changes) + * - Listeners (chain effectful cells) + * - UI components (live updates) + */ +export interface Subscription { + id: string; + cellId: CellId; + callback: (value: CellValue, prev: CellValue) => void | Promise; + filter?: (value: CellValue, prev: CellValue) => boolean; +} +/** + * A complete sheet: title, axes (what rows and columns mean), and + * the cells that compose it. + * + * Axes are semantic: `rows.name = "tenant"` means each row is a + * tenant, not just a coordinate. The engine doesn't enforce this + * — it's documentation for humans and routing rules. + */ +export interface SheetDef { + id: string; + title?: string; + description?: string; + version?: string; + axes?: { + rows?: { + name: string; + values?: unknown[]; + }; + cols?: { + name: string; + values?: unknown[]; + }; + }; + cells: CellDef[]; +} +/** + * The trace of a particular evaluation. Used for: + * - debugging ("why did this cell return X?") + * - time-travel ("what was the value at T?") + * - decomposition (which cells should be replaced?) + * + * Every cell evaluation can emit a trace. The engine keeps a ring + * buffer of recent traces accessible via `engine.getTraces(limit)`. + */ +export interface EvaluationTrace { + cellId: CellId; + startedAt: number; + completedAt?: number; + durationMs?: number; + context: CallerContext; + effects?: Effect[]; + error?: { + message: string; + stack?: string; + }; +} +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/types.d.ts.map b/quilt_worker/quilt_core/types.d.ts.map new file mode 100644 index 0000000..3267f1b --- /dev/null +++ b/quilt_worker/quilt_core/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG;AAMH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAM7B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,QAAQ,GAChB,OAAO,GACP,SAAS,GACT,KAAK,GACL,SAAS,GACT,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,IAAI,GACJ,IAAI,CAAC;AAMT;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAM5E;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,SAAS;IACxB,uDAAuD;IACvD,IAAI,EAAE,OAAO,CAAC;IACd,kCAAkC;IAClC,MAAM,EAAE,UAAU,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAMD;;;;;;;;;GASG;AACH,MAAM,MAAM,MAAM,GACd;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,GAAG,KAAK,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1E;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAMpC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,6BAA6B;IAC7B,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;QAC9C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;IACF,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EACD,MAAM,GACN;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GACjD;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GACjB;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC;CACxB;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IAGf,KAAK,CAAC,EAAE,OAAO,CAAC;IAGhB,IAAI,CAAC,EAAE,MAAM,CAAC;IAGd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAGjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAC;IAG5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAGlB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAGhB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IAGrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,eAAe,CAAC;IAG3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IAGd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IAGjB,WAAW,CAAC,EAAE;QACZ,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAMD;;;;;GAKG;AACH,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,OAAO,CAAC;IACb,kCAAkC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,4CAA4C;IAC5C,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,uEAAuE;IACvE,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,gDAAgD;IAChD,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B;;;OAGG;IACH,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACtC;AAMD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC;CACzD;AAMD;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAA;SAAE,CAAC;QAC5C,IAAI,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAA;SAAE,CAAC;KAC7C,CAAC;IACF,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAMD;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C"} \ No newline at end of file diff --git a/quilt_worker/quilt_core/types.js b/quilt_worker/quilt_core/types.js new file mode 100644 index 0000000..5be84ee --- /dev/null +++ b/quilt_worker/quilt_core/types.js @@ -0,0 +1,97 @@ +/** + * @file types.ts + * @module @quilt/core + * + * ===================================================================== + * THE TYPE VOCABULARY OF THE QUILT RUNTIME + * ===================================================================== + * + * This file is the single source of truth for what a "cell" is, what + * kinds of cells exist, what values flow through them, and what + * context travels with every call. + * + * If you are a zero-shot agent landing in this codebase, START HERE. + * Read this file, then `engine.ts`, then `context.ts`. With those three + * you have the mental model. The cell evaluators in `cells/*.ts` are + * then just implementations of `evaluate*(cell, ctx, ...) -> CellValue`. + * + * ===================================================================== + * ROLE IN THE SYSTEM + * ===================================================================== + * + * This file is consumed by EVERY other file in the runtime. It is + * leaf-level: it imports nothing from this project. Every other file + * imports from here. + * + * ┌─────────────────────────────────────────────────────────────┐ + * │ types.ts ◄── this file │ + * │ ▲ │ + * │ │ imports │ + * │ │ │ + * │ ├── context.ts (extends CallerContext) │ + * │ ├── engine.ts (holds Map) │ + * │ ├── cells/*.ts (read Cell, CellDef, CellValue) │ + * │ ├── parser.ts (produces CellDef from YAML) │ + * │ └── mcp/server.ts (exposes cells as MCP tools) │ + * └─────────────────────────────────────────────────────────────┘ + * + * ===================================================================== + * KEY DESIGN DECISIONS + * ===================================================================== + * + * 1. STABLE IDENTITY OVER COORDINATES + * A cell is addressed by its `id` (e.g. "compass.heading"), not + * by A1/B2-style coordinates. Coordinates are display-only. This + * means inserting/reordering cells never breaks references. + * + * 2. EIGHT CELL KINDS, NOT ONE + * A cell is a tagged union of eight kinds. Each kind has different + * evaluation semantics (pull vs push, sync vs async, pure vs + * effectful). The kind is the only thing that determines how the + * engine treats the cell. + * + * 3. CALLER CONTEXT IS FIRST-CLASS + * Every cell call carries a `CallerContext` (row, column, identity, + * trace). The engine fills in the trace automatically. This is + * what makes caller-aware routing possible. + * + * 4. VALUES CARRY THEIR OWN STATUS + * A `CellValue` has a `status` field. There's no separate "is it + * ready?" check. A consumer reads the status to know whether the + * value is fresh, computing, errored, or stale. + * + * 5. EFFECTS ARE DECLARED + * Pure cells have no effects. Effectful cells (api, program) declare + * what they did (network calls, model invocations, etc.) via the + * `effects` field. This is what makes cost/latency reasoning and + * decomposition tractable. + * + * ===================================================================== + * THE METAPHOR + * ===================================================================== + * + * A `Cell` is a SOCKET, not a value. You can plug any implementation + * into a socket. Changing the implementation changes every dependent + * automatically — that is dependency injection at the spreadsheet level. + * + * A `CellDef` is the SPEC for a socket. The runtime instantiates a + * `Cell` (the live thing) from a `CellDef` (the static description). + * + * A `CellValue` is the CURRENT STATE of a socket. It carries its own + * status so consumers know what they're getting. + * + * A `CallerContext` is the wiring context. It tells the socket who's + * asking and from where, so it can route. + * + * A `Subscription` is someone watching a socket for changes. + * + * An `Effect` is something a socket did that has consequences outside + * itself. Network call, model invocation, file write. + * + * A `SheetDef` is the wiring diagram: all the sockets and how they + * connect. + * + * ===================================================================== + */ +export {}; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/quilt_worker/quilt_core/types.js.map b/quilt_worker/quilt_core/types.js.map new file mode 100644 index 0000000..a022f6b --- /dev/null +++ b/quilt_worker/quilt_core/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG"} \ No newline at end of file diff --git a/quilt_worker/src/demo_sheet.json b/quilt_worker/src/demo_sheet.json new file mode 100644 index 0000000..10852be --- /dev/null +++ b/quilt_worker/src/demo_sheet.json @@ -0,0 +1,29 @@ +{ + "id": "hermes_demo", + "title": "Hermes Demo Sheet", + "cells": [ + {"id": "static_val", "kind": "value", "value": 42}, + {"id": "double", "kind": "formula", "expr": "static_val * 2"}, + {"id": "api_call", "kind": "api", "endpoint": "https://httpbin.org/get", "method": "GET"}, + {"id": "prog", "kind": "program", "code": "return await runtime.get('double');", "language": "javascript"}, + {"id": "router", "kind": "router", "rules": [{"when": "true", "route": "prog"}]}, + { + "id": "ai_cell", + "kind": "ai", + "ai_kind": "ai.llm", + "provider": "zai", + "model": "glm-4.5", + "prompt": "Answer yes/no: is {{static_val}} > 40?", + "temperature": 0.2, + "max_tokens": 50 + }, + {"id": "sensor_input", "kind": "sensor"}, + { + "id": "listener_alert", + "kind": "listener", + "watch": ["static_val"], + "condition": "{{static_val}} > 50", + "action": "console.log('Alert: static_val exceeded 50')" + } + ] +} diff --git a/quilt_worker/src/index.js b/quilt_worker/src/index.js new file mode 100644 index 0000000..e5f9291 --- /dev/null +++ b/quilt_worker/src/index.js @@ -0,0 +1,35 @@ +import { QuiltEngine } from '../quilt_core/index.js'; +import { SimpleAIEngine } from './simple_ai_engine.js'; +import sheetData from './demo_sheet.json'; + + +// Rate limit per IP using KV namespace USAGE +const VISIT_LIMIT = 5; // max AI calls per visitor per day + +export default { + async fetch(request, env) { + const ip = request.headers.get('cf-connecting-ip') || 'unknown'; + const kv = env.USAGE; + const key = `visits:${ip}`; + const countStr = await kv.get(key); + const count = countStr ? parseInt(countStr) : 0; + if (count >= VISIT_LIMIT) { + return new Response('Rate limit exceeded. Try again later.', { status: 429 }); + } + // Increment counter (expire in 24h) + await kv.put(key, (count + 1).toString(), { expirationTtl: 86400 }); + + const aiEngine = new SimpleAIEngine({ maxCalls: VISIT_LIMIT, env }); + const engine = new QuiltEngine({ tracing: false, ai: aiEngine }); + engine.loadSheet(sheetData); + // Evaluate all cells and return results as JSON + const results = {}; + for (const cell of sheetData.cells) { + const res = await engine.get(cell.id); + results[cell.id] = { data: res.data, status: res.status }; + } + return new Response(JSON.stringify(results, null, 2), { + headers: { 'Content-Type': 'application/json' }, + }); + }, +}; diff --git a/quilt_worker/src/simple_ai_engine.js b/quilt_worker/src/simple_ai_engine.js new file mode 100644 index 0000000..b6d5b0d --- /dev/null +++ b/quilt_worker/src/simple_ai_engine.js @@ -0,0 +1,49 @@ +import { URLSearchParams } from 'url'; + +/** Simple Cloudflare AI Engine wrapper for Workers. + * - Uses fetch (available in Workers). + * - Reads CF_ACCOUNT_ID and CF_AI_TOKEN from Secrets. + * - Enforces per‑request call limit (maxCalls) and token cap (max_tokens <= 100). + */ +export class SimpleAIEngine { + constructor({ maxCalls = 5, env = {} } = {}) { + this.maxCalls = maxCalls; + this.callsMade = 0; + this.accountId = env.CF_ACCOUNT_ID; + this.token = env.CF_AI_TOKEN; + if (!this.accountId || !this.token) { + console.warn('CF_ACCOUNT_ID or CF_AI_TOKEN not set – AI calls will fail.'); + } + } + + async call(config, _opts = {}) { + if (config.max_tokens && config.max_tokens > 100) { + throw new Error('Token request exceeds safe limit (100).'); + } + if (this.callsMade >= this.maxCalls) { + throw new Error('Demo AI call limit reached.'); + } + this.callsMade++; + const model = config.model || 'text-davinci-003'; + const endpoint = `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/ai/run/${model}`; + const body = { + prompt: config.prompt || '', + max_tokens: config.max_tokens, + temperature: config.temperature, + }; + const resp = await fetch(endpoint, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + if (!resp.ok) { + const txt = await resp.text(); + throw new Error(`AI request failed: ${resp.status} ${txt}`); + } + const data = await resp.json(); + return data.result?.response ?? data; + } +} diff --git a/quilt_worker/wrangler.toml b/quilt_worker/wrangler.toml new file mode 100644 index 0000000..33a544e --- /dev/null +++ b/quilt_worker/wrangler.toml @@ -0,0 +1,17 @@ +name = "quilt-demo" +main = "src/index.js" +compatibility_date = "2024-01-01" + +[vars] +# No vars needed; secrets will be used for CF_ACCOUNT_ID and CF_AI_TOKEN + +[env.production] +# Production environment can have different limits if needed + +[triggers] +# No scheduled triggers needed + +[kv_namespaces] +# Bind the KV namespace to the env variable USAGE +binding = "USAGE" +# The id will be filled after creation via wrangler kv:namespace create USAGE From fe77a987a6fc21cf07bada880bd7105ea0d9ac32 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 24 Aug 2026 14:51:15 -0800 Subject: [PATCH 2/2] Add GPU integration source + wrangler/engine updates + .gitignore --- .gitignore | 5 + gpu_integration/Cargo.lock | 505 +++++++++++++++++++++++++++ gpu_integration/Cargo.toml | 14 + gpu_integration/commands.h | 93 +++++ gpu_integration/src/main.rs | 103 ++++++ gpu_integration/src/queue.rs | 212 +++++++++++ quilt_worker/src/simple_ai_engine.js | 2 +- quilt_worker/wrangler.toml | 8 +- 8 files changed, 937 insertions(+), 5 deletions(-) create mode 100644 gpu_integration/Cargo.lock create mode 100644 gpu_integration/Cargo.toml create mode 100644 gpu_integration/commands.h create mode 100644 gpu_integration/src/main.rs create mode 100644 gpu_integration/src/queue.rs diff --git a/.gitignore b/.gitignore index a6ca6dd..5803664 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,8 @@ Thumbs.db # CellFlow state .cellflow/ *.cellflow.lock +target/ +.cargo-artifact-lock +.cargo-build-lock +.cargo-lock + diff --git a/gpu_integration/Cargo.lock b/gpu_integration/Cargo.lock new file mode 100644 index 0000000..e2ddeda --- /dev/null +++ b/gpu_integration/Cargo.lock @@ -0,0 +1,505 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "cust" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6cc71911e179f12483b9734120b45bd00bf64fab085cc4818428523eedd469" +dependencies = [ + "bitflags 1.3.2", + "bytemuck", + "cust_core", + "cust_derive", + "cust_raw", + "find_cuda_helper", +] + +[[package]] +name = "cust_core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039f79662cb8f890cbf335e818cd522d6e3a53fe63f61d1aaaf859cd3d975f06" +dependencies = [ + "cust_derive", + "glam", + "mint", + "vek", +] + +[[package]] +name = "cust_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3bc95fe629aed92b2423de6ccff9e40174b21d19cb6ee6281a4d04ac72f66" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cust_raw" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf40d6ade12cb9828bbc844b9875c7b93d25e67a3c9bf61c7aa3ae09e402bf8" +dependencies = [ + "find_cuda_helper", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find_cuda_helper" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f9e65c593dd01ac77daad909ea4ad17f0d6d1776193fc8ea766356177abdad" +dependencies = [ + "glob", +] + +[[package]] +name = "glam" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quilt_gpu" +version = "0.1.0" +dependencies = [ + "crossbeam", + "cust", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vek" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8085882662f9bc47fc8b0cdafa5e19df8f592f650c02b9083da8d45ac9eebd17" +dependencies = [ + "approx", + "num-integer", + "num-traits", + "rustc_version", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/gpu_integration/Cargo.toml b/gpu_integration/Cargo.toml new file mode 100644 index 0000000..ad73b44 --- /dev/null +++ b/gpu_integration/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "quilt_gpu" +version = "0.1.0" +edition = "2021" + +[dependencies] +# CUDA Rust bindings +cust = "0.3" +tokio = { version = "1", features = ["full"] } +# For lock-free queue atomics (we'll use std::sync::atomic) +# Optional: crossbeam for more advanced atomics if needed +crossbeam = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/gpu_integration/commands.h b/gpu_integration/commands.h new file mode 100644 index 0000000..d8ffe23 --- /dev/null +++ b/gpu_integration/commands.h @@ -0,0 +1,93 @@ +#ifndef QUILT_GPU_COMMANDS_H +#define QUILT_GPU_COMMANDS_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Command IDs +typedef enum { + QUILT_CMD_NOP = 0, + QUILT_CMD_EDIT_CELL = 1, + QUILT_CMD_FORMULA_EVAL = 2, + QUILT_CMD_ADD_ROWS = 3, + QUILT_CMD_DEL_ROWS = 4, + QUILT_CMD_ADD_COLS = 5, + QUILT_CMD_DEL_COLS = 6, + QUILT_CMD_SET_FORMAT = 7, + QUILT_CMD_SYNC_CRDT = 8, + QUILT_CMD_MAX +} quilt_cmd_id_t; + +// Shared command header – must match CPU and GPU layout exactly +typedef struct { + quilt_cmd_id_t id; + uint32_t payload_len; // bytes after this header + // Followed by variable-length payload (see specific structs below) +} quilt_cmd_header_t; + +// ==== Specific command payloads ==== + +// EDIT_CELL: set a cell's raw value (as string) – for simplicity, we pass index and length+UTF8 bytes +typedef struct { + uint32_t cell_index; // linear index in the sheet (row * cols + col) + uint32_t value_len; // length of the UTF8 string that follows (not including null) + // char value[value_len]; // appended after this struct +} quilt_cmd_edit_cell_t; + +// FORMULA_EVAL: evaluate a formula over a range of cells +// For now, we assume formula is a null-terminated string stored after the struct. +typedef struct { + uint32_t start_index; // first cell index to evaluate (inclusive) + uint32_t count; // number of consecutive cells to evaluate + uint32_t formula_len; // length of formula string (UTF8, no null) + // char formula[formula_len]; +} quilt_cmd_formula_eval_t; + +// ADD_ROWS: insert `count` rows at position `row_index` +typedef struct { + uint32_t row_index; // where to insert (0-based) + uint32_t count; // number of rows to insert +} quilt_cmd_add_rows_t; + +// DEL_ROWS: delete `count` rows starting at `row_index` +typedef struct { + uint32_t row_index; + uint32_t count; +} quilt_cmd_del_rows_t; + +// Similar for columns (using column index) +typedef struct { + uint32_t col_index; + uint32_t count; +} quilt_cmd_add_cols_t; + +typedef struct { + uint32_t col_index; + uint32_t count; +} quilt_cmd_del_cols_t; + +// SET_FORMAT: apply formatting (e.g., bold, color) to a range – simplified as a bitmask +typedef struct { + uint32_t start_index; + uint32_t count; + uint32_t format_mask; // bits defined by frontend +} quilt_cmd_set_format_t; + +// SYNC_CRDT: apply a batch of CRDT operations (simplified as raw op bytes) +typedef struct { + uint32_t op_len; // length of serialized op buffer that follows + // char ops[op_len]; +} quilt_cmd_sync_crdt_t; + +// Maximum payload size we allow in a single command (adjust as needed) +#define QUILT_GPU_MAX_PAYLOAD_BYTES 4096 + +#ifdef __cplusplus +} +#endif + +#endif // QUILT_GPU_COMMANDS_H \ No newline at end of file diff --git a/gpu_integration/src/main.rs b/gpu_integration/src/main.rs new file mode 100644 index 0000000..c6d656f --- /dev/null +++ b/gpu_integration/src/main.rs @@ -0,0 +1,103 @@ +#![allow(dead_code)] + +mod queue; // <-- declare the local queue module + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; +use queue::{UnifiedQueue, QueueFull}; + +/// Simulated command processing – in a real GPU kernel this would be the +/// actual work done on the device. For this demo we just count the commands. +fn process_command(_cmd: &[u8]) -> u64 { + // Pretend we did some work and return a dummy result. + 1 +} + +/// The persistent kernel loop (runs on a CPU thread for this demo). +/// +/// # Arguments +/// * `queue` – reference to the shared queue. +/// * `running` – atomic flag that tells the kernel when to stop. +fn kernel_loop(queue: &UnifiedQueue, running: &AtomicBool) { + let mut processed = 0u64; + while running.load(Ordering::Acquire) { + if let Some(cmd) = queue.pop() { + // In a real kernel we would interpret the command header and + // dispatch to the appropriate handler. Here we just count. + processed += process_command(&cmd); + // Optional: simulate some work duration. + // std::thread::sleep(Duration::from_nanos(100)); + } else { + // Queue empty – spin briefly or yield to reduce CPU usage. + // Using a short sleep keeps the demo from hogging the CPU. + thread::sleep(Duration::from_micros(100)); + } + } + // When the loop exits we could post a final status, but for demo we just + // print the total. + println!("[GPU Kernel] Processed {} commands", processed); +} + +/// Demo program that measures the round‑trip latency of pushing N commands +/// through the lock‑free queue and having the kernel thread consume them. +fn main() { + let capacity = 1024; // number of slots + let queue = Arc::new(UnifiedQueue::new(capacity)); + let running = Arc::new(AtomicBool::new(true)); + let running_clone = Arc::clone(&running); + let queue_clone = Arc::clone(&queue); + + // Spawn the kernel thread. + let handle = thread::spawn(move || { + kernel_loop(&queue_clone, &running_clone); + }); + + // Number of commands to push in this demo. + let num_cmds = 100_000; + // A simple dummy command: just a byte pattern. + let dummy_cmd = [0u8; 64]; // fits comfortably in SLOT_SIZE (4096) + + let start = Instant::now(); + let mut pushed = 0; + for i in 0..num_cmds { + // In a real scenario we would serialize a proper command struct. + // Here we just push raw bytes. + match queue.push(&dummy_cmd) { + Ok(()) => pushed += 1, + Err(QueueFull) => { + // If the queue fills up we wait a bit and retry. + thread::sleep(Duration::from_micros(10)); + // Retry the same command. + let mut j = i; + while j < num_cmds { + match queue.push(&dummy_cmd) { + Ok(()) => { + pushed += 1; + j += 1; + } + Err(QueueFull) => { + thread::sleep(Duration::from_micros(10)); + } + } + } + break; + } + } + } + let push_duration = start.elapsed(); + println!("[CPU] Pushed {} commands in {:?}", pushed, push_duration); + + // Signal the kernel to stop. + running.store(false, Ordering::Release); + // Wait for the kernel thread to finish. + let _ = handle.join().expect("Kernel thread panicked"); + + let total = start.elapsed(); + println!("[CPU] Total elapsed time: {:?}", total); + println!( + "[CPU] Throughput: {:.2} commands/sec", + pushed as f64 / total.as_secs_f64() + ); +} \ No newline at end of file diff --git a/gpu_integration/src/queue.rs b/gpu_integration/src/queue.rs new file mode 100644 index 0000000..a9f41a0 --- /dev/null +++ b/gpu_integration/src/queue.rs @@ -0,0 +1,212 @@ +//! Lock‑free single‑producer/single‑consumer ring buffer that lives in a +//! unified‑memory buffer (accessible by both CPU and GPU). +//! +//! The buffer is split into fixed‑size slots, each slot large enough to hold +//! the largest command we expect (header + payload). The queue stores only +//! the raw bytes; it is the caller's responsibility to serialize/deserialize +//! the command structures. +//! +//! # Safety +//! The queue assumes a single producer and a single consumer. If multiple +//! threads try to push or pop concurrently the behavior is undefined. +//! +//! Interior mutability is provided via an `UnsafeCell` for the storage buffer, +//! while the head/tail indices are `AtomicUsize` with Release/Acquire ordering. +//! +//! The type is marked as `Send` and `Sync` because the internal buffer is +//! protected by the atomic head/tail indices: only the producer writes to a +//! slot (after claiming it via tail) and only the consumer reads from a slot +//! (after claiming it via head). The release-acquire ordering on the indices +//! ensures proper visibility of the written bytes. + +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::cell::UnsafeCell; + +/// Fixed size of each slot in the queue (in bytes). Must be at least the +/// size of the largest command you intend to store. +pub const SLOT_SIZE: usize = 4096; // matches QUILT_GPU_MAX_PAYLOAD_BYTES + header + +/// A lock‑free SPSC queue backed by a pre‑allocated byte buffer. +pub struct UnifiedQueue { + /// The raw storage for the queue slots, wrapped in UnsafeCell to allow + /// interior mutability for the producer. + buffer: UnsafeCell>, + /// Number of slots in the buffer. + capacity: usize, + /// Index of the slot to read from (consumer). + head: AtomicUsize, + /// Index of the slot to write to (producer). + tail: AtomicUsize, +} + +impl UnifiedQueue { + /// Create a new queue that can hold `capacity` commands. + /// + /// # Arguments + /// * `capacity` – maximum number of commands the queue can hold at once. + /// + /// The allocated buffer size is `capacity * SLOT_SIZE` bytes. + pub fn new(capacity: usize) -> Self { + assert!(capacity > 0, "Queue capacity must be > 0"); + let buf = vec![0u8; capacity * SLOT_SIZE]; + UnifiedQueue { + buffer: UnsafeCell::new(buf), + capacity, + head: AtomicUsize::new(0), + tail: AtomicUsize::new(0), + } + } + + /// Returns the number of slots in the queue. + #[inline] + pub fn capacity(&self) -> usize { + self.capacity + } + + /// Try to push a command into the queue. + /// + /// # Arguments + /// * `data` – the serialized command bytes. Must be ≤ `SLOT_SIZE`. + /// + /// // Returns + /// * `Ok(())` if the command was enqueued. + /// * `Err(QueueFull)` if the queue is full. + #[inline] + pub fn push(&self, data: &[u8]) -> Result<(), QueueFull> { + assert!(data.len() <= SLOT_SIZE, "Command exceeds slot size"); + let mut tail = self.tail.load(Ordering::Relaxed); + loop { + let head = self.head.load(Ordering::Acquire); + let next_tail = if tail + 1 == self.capacity { 0 } else { tail + 1 }; + if next_tail == head { + // Queue full + return Err(QueueFull); + } + // Try to claim the slot at `tail`. + if self + .tail + .compare_exchange_weak(tail, next_tail, Ordering::Release, Ordering::Relaxed) + .is_ok() + { + // We own the slot; copy the data. + // Safety: we have exclusive access to this slot because we own the tail index. + let buf = unsafe { &mut *self.buffer.get() }; + let offset = tail * SLOT_SIZE; + let dst = &mut buf[offset..offset + SLOT_SIZE]; + // Copy the data. + dst[..data.len()].copy_from_slice(data); + // Zero‑out the rest of the slot (optional, helps debugging). + for byte in &mut dst[data.len()..] { + *byte = 0; + } + return Ok(()); + } + // Lost the race to another producer (should not happen in SPSC), + // retry with the current tail value. + tail = self.tail.load(Ordering::Relaxed); + } + } + + /// Try to pop a command from the queue. + /// + /// // Returns + /// * `Some(vec)` containing the raw bytes if a command was available. + /// * `None` if the queue is empty. + #[inline] + pub fn pop(&self) -> Option> { + let mut head = self.head.load(Ordering::Relaxed); + loop { + let tail = self.tail.load(Ordering::Acquire); + if head == tail { + // Queue empty + return None; + } + let next_head = if head + 1 == self.capacity { 0 } else { head + 1 }; + // Try to claim the slot at `head`. + if self + .head + .compare_exchange_weak(head, next_head, Ordering::Release, Ordering::Relaxed) + .is_ok() + { + // We own the slot; read the data. + // Safety: we have exclusive access to this slot because we own the head index. + let buf = unsafe { &*self.buffer.get() }; + let offset = head * SLOT_SIZE; + let src = &buf[offset..offset + SLOT_SIZE]; + // Return a copy of the whole slot; the caller can interpret the header + // and payload length as needed. + let mut v = Vec::with_capacity(SLOT_SIZE); + v.extend_from_slice(src); + return Some(v); + } + // Lost the race to the consumer (should not happen in SPSC), + // retry with the current head. + head = self.head.load(Ordering::Relaxed); + } + } + + /// Returns a slice of the underlying buffer for direct inspection (unsafe). + #[inline] + pub fn as_slice(&self) -> &[u8] { + unsafe { &*self.buffer.get() } + } +} + +/// Error returned when the queue is full. +#[derive(Debug, PartialEq, Eq)] +pub struct QueueFull; + +impl std::fmt::Display for QueueFull { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "queue is full") + } +} + +impl std::error::Error for QueueFull {} + +unsafe impl Send for UnifiedQueue {} +unsafe impl Sync for UnifiedQueue {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_push_pop() { + let q = UnifiedQueue::new(4); + let cmd = b"hello"; + assert_eq!(q.push(cmd), Ok(())); + let popped = q.pop().expect("should have a command"); + assert_eq!(&popped[..cmd.len()], cmd); + assert_eq!(q.pop(), None); + } + + #[test] + fn queue_full() { + let q = UnifiedQueue::new(2); + let cmd = [0u8; SLOT_SIZE]; + assert_eq!(q.push(&cmd), Ok(())); + assert_eq!(q.push(&cmd), Ok(())); + assert_eq!(q.push(&cmd), Err(QueueFull)); + // Consume one, then we can push again. + let _ = q.pop().expect("should have a command"); + assert_eq!(q.push(&cmd), Ok(())); + } + + #[test] + fn wrap_around() { + let q = UnifiedQueue::new(2); + let cmd1 = b"aa"; + let cmd2 = b"bb"; + let cmd3 = b"cc"; + assert_eq!(q.push(&cmd1), Ok(())); + assert_eq!(q.push(&cmd2), Ok(())); + let _ = q.pop().expect("first"); + assert_eq!(q.push(&cmd3), Ok(())); + let popped = q.pop().expect("second"); + assert_eq!(&popped[..cmd2.len()], cmd2); + let popped = q.pop().expect("third"); + assert_eq!(&popped[..cmd3.len()], cmd3); + assert_eq!(q.pop(), None); + } +} \ No newline at end of file diff --git a/quilt_worker/src/simple_ai_engine.js b/quilt_worker/src/simple_ai_engine.js index b6d5b0d..b948fae 100644 --- a/quilt_worker/src/simple_ai_engine.js +++ b/quilt_worker/src/simple_ai_engine.js @@ -1,4 +1,4 @@ -import { URLSearchParams } from 'url'; +import { URLSearchParams } from 'node:url'; /** Simple Cloudflare AI Engine wrapper for Workers. * - Uses fetch (available in Workers). diff --git a/quilt_worker/wrangler.toml b/quilt_worker/wrangler.toml index 33a544e..ed64df2 100644 --- a/quilt_worker/wrangler.toml +++ b/quilt_worker/wrangler.toml @@ -1,6 +1,7 @@ name = "quilt-demo" main = "src/index.js" -compatibility_date = "2024-01-01" +compatibility_date = "2024-09-23" +compatibility_flags = ["nodejs_compat"] [vars] # No vars needed; secrets will be used for CF_ACCOUNT_ID and CF_AI_TOKEN @@ -11,7 +12,6 @@ compatibility_date = "2024-01-01" [triggers] # No scheduled triggers needed -[kv_namespaces] -# Bind the KV namespace to the env variable USAGE +[[kv_namespaces]] binding = "USAGE" -# The id will be filled after creation via wrangler kv:namespace create USAGE +id = "d6bdfd4f3ec64c9baf18ca849ea5265d"