@ambitresearch/genie 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/auth/bearer.d.ts +49 -0
- package/dist/auth/bearer.d.ts.map +1 -0
- package/dist/auth/bearer.js +159 -0
- package/dist/auth/bearer.js.map +1 -0
- package/dist/auth/oauth/authorize.d.ts +19 -0
- package/dist/auth/oauth/authorize.d.ts.map +1 -0
- package/dist/auth/oauth/authorize.js +111 -0
- package/dist/auth/oauth/authorize.js.map +1 -0
- package/dist/auth/oauth/config.d.ts +10 -0
- package/dist/auth/oauth/config.d.ts.map +1 -0
- package/dist/auth/oauth/config.js +16 -0
- package/dist/auth/oauth/config.js.map +1 -0
- package/dist/auth/oauth/dcr.d.ts +15 -0
- package/dist/auth/oauth/dcr.d.ts.map +1 -0
- package/dist/auth/oauth/dcr.js +38 -0
- package/dist/auth/oauth/dcr.js.map +1 -0
- package/dist/auth/oauth/jwt.d.ts +13 -0
- package/dist/auth/oauth/jwt.d.ts.map +1 -0
- package/dist/auth/oauth/jwt.js +46 -0
- package/dist/auth/oauth/jwt.js.map +1 -0
- package/dist/auth/oauth/metadata.d.ts +9 -0
- package/dist/auth/oauth/metadata.d.ts.map +1 -0
- package/dist/auth/oauth/metadata.js +19 -0
- package/dist/auth/oauth/metadata.js.map +1 -0
- package/dist/auth/oauth/router.d.ts +32 -0
- package/dist/auth/oauth/router.d.ts.map +1 -0
- package/dist/auth/oauth/router.js +159 -0
- package/dist/auth/oauth/router.js.map +1 -0
- package/dist/auth/oauth/store.d.ts +55 -0
- package/dist/auth/oauth/store.d.ts.map +1 -0
- package/dist/auth/oauth/store.js +63 -0
- package/dist/auth/oauth/store.js.map +1 -0
- package/dist/auth/oauth/token.d.ts +21 -0
- package/dist/auth/oauth/token.d.ts.map +1 -0
- package/dist/auth/oauth/token.js +77 -0
- package/dist/auth/oauth/token.js.map +1 -0
- package/dist/auth/oidc/group-policy.d.ts +43 -0
- package/dist/auth/oidc/group-policy.d.ts.map +1 -0
- package/dist/auth/oidc/group-policy.js +54 -0
- package/dist/auth/oidc/group-policy.js.map +1 -0
- package/dist/auth/oidc/verifier.d.ts +42 -0
- package/dist/auth/oidc/verifier.d.ts.map +1 -0
- package/dist/auth/oidc/verifier.js +145 -0
- package/dist/auth/oidc/verifier.js.map +1 -0
- package/dist/auth/token-cli.d.ts +9 -0
- package/dist/auth/token-cli.d.ts.map +1 -0
- package/dist/auth/token-cli.js +98 -0
- package/dist/auth/token-cli.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +149 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/redact.d.ts +20 -0
- package/dist/config/redact.d.ts.map +1 -0
- package/dist/config/redact.js +77 -0
- package/dist/config/redact.js.map +1 -0
- package/dist/config/secrets.d.ts +100 -0
- package/dist/config/secrets.d.ts.map +1 -0
- package/dist/config/secrets.js +130 -0
- package/dist/config/secrets.js.map +1 -0
- package/dist/framework/html.d.ts +103 -0
- package/dist/framework/html.d.ts.map +1 -0
- package/dist/framework/html.js +271 -0
- package/dist/framework/html.js.map +1 -0
- package/dist/framework/interface.d.ts +130 -0
- package/dist/framework/interface.d.ts.map +1 -0
- package/dist/framework/interface.js +112 -0
- package/dist/framework/interface.js.map +1 -0
- package/dist/framework/react.d.ts +133 -0
- package/dist/framework/react.d.ts.map +1 -0
- package/dist/framework/react.js +243 -0
- package/dist/framework/react.js.map +1 -0
- package/dist/framework/vue.d.ts +99 -0
- package/dist/framework/vue.d.ts.map +1 -0
- package/dist/framework/vue.js +375 -0
- package/dist/framework/vue.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/client.d.ts +81 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +141 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/component-response.d.ts +111 -0
- package/dist/llm/component-response.d.ts.map +1 -0
- package/dist/llm/component-response.js +167 -0
- package/dist/llm/component-response.js.map +1 -0
- package/dist/llm/generated-files.d.ts +10 -0
- package/dist/llm/generated-files.d.ts.map +1 -0
- package/dist/llm/generated-files.js +26 -0
- package/dist/llm/generated-files.js.map +1 -0
- package/dist/llm/prompts/CHANGELOG.md +70 -0
- package/dist/llm/prompts/generate-component.system.md +99 -0
- package/dist/llm/prompts/refine-component.system.md +112 -0
- package/dist/llm/prompts.d.ts +66 -0
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/llm/prompts.js +78 -0
- package/dist/llm/prompts.js.map +1 -0
- package/dist/llm/retry.d.ts +183 -0
- package/dist/llm/retry.d.ts.map +1 -0
- package/dist/llm/retry.js +475 -0
- package/dist/llm/retry.js.map +1 -0
- package/dist/llm/schema.d.ts +129 -0
- package/dist/llm/schema.d.ts.map +1 -0
- package/dist/llm/schema.js +147 -0
- package/dist/llm/schema.js.map +1 -0
- package/dist/llm/validate.d.ts +107 -0
- package/dist/llm/validate.d.ts.map +1 -0
- package/dist/llm/validate.js +174 -0
- package/dist/llm/validate.js.map +1 -0
- package/dist/manifest/compiler.d.ts +112 -0
- package/dist/manifest/compiler.d.ts.map +1 -0
- package/dist/manifest/compiler.js +306 -0
- package/dist/manifest/compiler.js.map +1 -0
- package/dist/manifest/ensure.d.ts +46 -0
- package/dist/manifest/ensure.d.ts.map +1 -0
- package/dist/manifest/ensure.js +49 -0
- package/dist/manifest/ensure.js.map +1 -0
- package/dist/manifest/index.d.ts +13 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +13 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/middleware/plan-guard.d.ts +167 -0
- package/dist/middleware/plan-guard.d.ts.map +1 -0
- package/dist/middleware/plan-guard.js +190 -0
- package/dist/middleware/plan-guard.js.map +1 -0
- package/dist/plans/index.d.ts +71 -0
- package/dist/plans/index.d.ts.map +1 -0
- package/dist/plans/index.js +252 -0
- package/dist/plans/index.js.map +1 -0
- package/dist/schemas/component.schema.json +114 -0
- package/dist/server.d.ts +96 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +258 -0
- package/dist/server.js.map +1 -0
- package/dist/store/empty-manifest.d.ts +58 -0
- package/dist/store/empty-manifest.d.ts.map +1 -0
- package/dist/store/empty-manifest.js +59 -0
- package/dist/store/empty-manifest.js.map +1 -0
- package/dist/store/git-host.d.ts +120 -0
- package/dist/store/git-host.d.ts.map +1 -0
- package/dist/store/git-host.js +796 -0
- package/dist/store/git-host.js.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +4 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/interface.d.ts +322 -0
- package/dist/store/interface.d.ts.map +1 -0
- package/dist/store/interface.js +100 -0
- package/dist/store/interface.js.map +1 -0
- package/dist/store/kit-files.d.ts +88 -0
- package/dist/store/kit-files.d.ts.map +1 -0
- package/dist/store/kit-files.js +199 -0
- package/dist/store/kit-files.js.map +1 -0
- package/dist/store/local.d.ts +70 -0
- package/dist/store/local.d.ts.map +1 -0
- package/dist/store/local.js +651 -0
- package/dist/store/local.js.map +1 -0
- package/dist/store/manifest.d.ts +44 -0
- package/dist/store/manifest.d.ts.map +1 -0
- package/dist/store/manifest.js +116 -0
- package/dist/store/manifest.js.map +1 -0
- package/dist/store/viewer-assets.d.ts +20 -0
- package/dist/store/viewer-assets.d.ts.map +1 -0
- package/dist/store/viewer-assets.js +118 -0
- package/dist/store/viewer-assets.js.map +1 -0
- package/dist/sync/anchor.d.ts +125 -0
- package/dist/sync/anchor.d.ts.map +1 -0
- package/dist/sync/anchor.js +210 -0
- package/dist/sync/anchor.js.map +1 -0
- package/dist/sync/index.d.ts +15 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +15 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/orchestrator.d.ts +160 -0
- package/dist/sync/orchestrator.d.ts.map +1 -0
- package/dist/sync/orchestrator.js +288 -0
- package/dist/sync/orchestrator.js.map +1 -0
- package/dist/tools/bind_kit.d.ts +29 -0
- package/dist/tools/bind_kit.d.ts.map +1 -0
- package/dist/tools/bind_kit.js +70 -0
- package/dist/tools/bind_kit.js.map +1 -0
- package/dist/tools/conjure.d.ts +141 -0
- package/dist/tools/conjure.d.ts.map +1 -0
- package/dist/tools/conjure.js +655 -0
- package/dist/tools/conjure.js.map +1 -0
- package/dist/tools/conjure_screen.d.ts +174 -0
- package/dist/tools/conjure_screen.d.ts.map +1 -0
- package/dist/tools/conjure_screen.js +458 -0
- package/dist/tools/conjure_screen.js.map +1 -0
- package/dist/tools/create_kit.d.ts +32 -0
- package/dist/tools/create_kit.d.ts.map +1 -0
- package/dist/tools/create_kit.js +131 -0
- package/dist/tools/create_kit.js.map +1 -0
- package/dist/tools/create_project.d.ts +267 -0
- package/dist/tools/create_project.d.ts.map +1 -0
- package/dist/tools/create_project.js +546 -0
- package/dist/tools/create_project.js.map +1 -0
- package/dist/tools/delete_files.d.ts +93 -0
- package/dist/tools/delete_files.d.ts.map +1 -0
- package/dist/tools/delete_files.js +276 -0
- package/dist/tools/delete_files.js.map +1 -0
- package/dist/tools/delete_project.d.ts +43 -0
- package/dist/tools/delete_project.d.ts.map +1 -0
- package/dist/tools/delete_project.js +118 -0
- package/dist/tools/delete_project.js.map +1 -0
- package/dist/tools/get_kit.d.ts +28 -0
- package/dist/tools/get_kit.d.ts.map +1 -0
- package/dist/tools/get_kit.js +95 -0
- package/dist/tools/get_kit.js.map +1 -0
- package/dist/tools/get_project.d.ts +37 -0
- package/dist/tools/get_project.d.ts.map +1 -0
- package/dist/tools/get_project.js +71 -0
- package/dist/tools/get_project.js.map +1 -0
- package/dist/tools/list_components.d.ts +42 -0
- package/dist/tools/list_components.d.ts.map +1 -0
- package/dist/tools/list_components.js +125 -0
- package/dist/tools/list_components.js.map +1 -0
- package/dist/tools/list_files.d.ts +32 -0
- package/dist/tools/list_files.d.ts.map +1 -0
- package/dist/tools/list_files.js +109 -0
- package/dist/tools/list_files.js.map +1 -0
- package/dist/tools/list_kits.d.ts +14 -0
- package/dist/tools/list_kits.d.ts.map +1 -0
- package/dist/tools/list_kits.js +41 -0
- package/dist/tools/list_kits.js.map +1 -0
- package/dist/tools/list_projects.d.ts +23 -0
- package/dist/tools/list_projects.d.ts.map +1 -0
- package/dist/tools/list_projects.js +66 -0
- package/dist/tools/list_projects.js.map +1 -0
- package/dist/tools/plan.d.ts +16 -0
- package/dist/tools/plan.d.ts.map +1 -0
- package/dist/tools/plan.js +146 -0
- package/dist/tools/plan.js.map +1 -0
- package/dist/tools/preview.d.ts +289 -0
- package/dist/tools/preview.d.ts.map +1 -0
- package/dist/tools/preview.js +672 -0
- package/dist/tools/preview.js.map +1 -0
- package/dist/tools/read_file.d.ts +40 -0
- package/dist/tools/read_file.d.ts.map +1 -0
- package/dist/tools/read_file.js +114 -0
- package/dist/tools/read_file.js.map +1 -0
- package/dist/tools/refine.d.ts +220 -0
- package/dist/tools/refine.d.ts.map +1 -0
- package/dist/tools/refine.js +501 -0
- package/dist/tools/refine.js.map +1 -0
- package/dist/tools/validate.d.ts +74 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +224 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/tools/write_files.d.ts +158 -0
- package/dist/tools/write_files.d.ts.map +1 -0
- package/dist/tools/write_files.js +541 -0
- package/dist/tools/write_files.js.map +1 -0
- package/dist/transport.d.ts +77 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +448 -0
- package/dist/transport.js.map +1 -0
- package/dist/ui/card-asset-broker.d.ts +42 -0
- package/dist/ui/card-asset-broker.d.ts.map +1 -0
- package/dist/ui/card-asset-broker.js +479 -0
- package/dist/ui/card-asset-broker.js.map +1 -0
- package/dist/ui/grid-resource.d.ts +252 -0
- package/dist/ui/grid-resource.d.ts.map +1 -0
- package/dist/ui/grid-resource.js +680 -0
- package/dist/ui/grid-resource.js.map +1 -0
- package/dist/ui/viewer-static/index.html +95 -0
- package/dist/ui/viewer-static/viewer.css +451 -0
- package/dist/ui/viewer-static/viewer.js +1405 -0
- package/dist/validate/full-scan.d.ts +87 -0
- package/dist/validate/full-scan.d.ts.map +1 -0
- package/dist/validate/full-scan.js +299 -0
- package/dist/validate/full-scan.js.map +1 -0
- package/dist/validate/index.d.ts +15 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +18 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate/marker.d.ts +89 -0
- package/dist/validate/marker.d.ts.map +1 -0
- package/dist/validate/marker.js +87 -0
- package/dist/validate/marker.js.map +1 -0
- package/dist/validate/phash.d.ts +135 -0
- package/dist/validate/phash.d.ts.map +1 -0
- package/dist/validate/phash.js +307 -0
- package/dist/validate/phash.js.map +1 -0
- package/dist/validate/render.d.ts +38 -0
- package/dist/validate/render.d.ts.map +1 -0
- package/dist/validate/render.js +155 -0
- package/dist/validate/render.js.map +1 -0
- package/dist/watch/index.d.ts +12 -0
- package/dist/watch/index.d.ts.map +1 -0
- package/dist/watch/index.js +12 -0
- package/dist/watch/index.js.map +1 -0
- package/dist/watch/watcher.d.ts +65 -0
- package/dist/watch/watcher.d.ts.map +1 -0
- package/dist/watch/watcher.js +238 -0
- package/dist/watch/watcher.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
You are **genie**, a UI-component generator that renders a single component
|
|
2
|
+
against the caller's own **UI kit**. You return one component as a strict JSON
|
|
3
|
+
object — never prose, never a chat reply. Your entire response is consumed by a
|
|
4
|
+
machine (`response_format: json_schema`), so it must be valid against the schema
|
|
5
|
+
the caller supplies and nothing else.
|
|
6
|
+
|
|
7
|
+
# What you produce
|
|
8
|
+
|
|
9
|
+
One component, as a file set plus the metadata needed to card it in a preview
|
|
10
|
+
grid. The caller hands you:
|
|
11
|
+
|
|
12
|
+
- **`kit`** — a description of the target UI kit: its tokens, primitives,
|
|
13
|
+
naming, and house style. Treat this as the source of truth for look and feel.
|
|
14
|
+
Match it. Do not invent a second design language on top of it.
|
|
15
|
+
- **`prompt`** — what the component should be, in natural language.
|
|
16
|
+
- **`group`** — the kit category the component belongs to (kebab-case, e.g.
|
|
17
|
+
`actions`, `inputs`, `feedback`). If the caller does not give you one, pick
|
|
18
|
+
the most fitting kebab-case category yourself.
|
|
19
|
+
- **`framework`** — `react`, `vue`, or `html`. Emit the implementation file in
|
|
20
|
+
this framework. The `.html` preview (below) is always plain HTML/CSS
|
|
21
|
+
regardless of framework.
|
|
22
|
+
- Optionally a **reference image** and/or **reference page** to match.
|
|
23
|
+
|
|
24
|
+
# Output contract
|
|
25
|
+
|
|
26
|
+
Return an object with exactly these top-level keys:
|
|
27
|
+
|
|
28
|
+
- **`componentName`** — PascalCase, 2–64 chars (e.g. `Button`, `PriceCard`).
|
|
29
|
+
- **`group`** — kebab-case, echoing the resolved category.
|
|
30
|
+
- **`files`** — the component's files (1–12). Every path MUST be
|
|
31
|
+
`components/<group>/<ComponentName>/<filename>` — same `<group>` and
|
|
32
|
+
`<ComponentName>` as the fields above. At minimum emit:
|
|
33
|
+
- the framework implementation — `<ComponentName>.tsx` (react),
|
|
34
|
+
`<ComponentName>.vue` (vue), or `<ComponentName>.html` (html);
|
|
35
|
+
- a **`<ComponentName>.html` preview** (required — see below);
|
|
36
|
+
- a `meta.json` with the component's group and viewport.
|
|
37
|
+
You may also add `<ComponentName>.d.ts` (types) and a
|
|
38
|
+
`<ComponentName>.prompt.md` (a short note on what you built and the props).
|
|
39
|
+
- **`manifestEntry`** — `{ viewport: { width, height }, subtitle?, tags? }`.
|
|
40
|
+
`viewport` is the natural preview size of the card in CSS pixels (a button is
|
|
41
|
+
small, e.g. `{ "width": 320, "height": 140 }`; a full card is larger). Keep
|
|
42
|
+
width and height between 1 and 4096. `subtitle` is a one-line description;
|
|
43
|
+
`tags` are a few kebab/lowercase keywords.
|
|
44
|
+
|
|
45
|
+
Set each file's `mimeType` honestly (`text/html`, `text/tsx`, `text/plain`,
|
|
46
|
+
`application/json`, …). File `content` is the full file, 1–65536 chars.
|
|
47
|
+
|
|
48
|
+
# The `.html` preview is the contract surface — get it exactly right
|
|
49
|
+
|
|
50
|
+
Exactly one file must be `components/<group>/<ComponentName>/<ComponentName>.html`
|
|
51
|
+
(the directory name and the filename share the component name). This preview is
|
|
52
|
+
what the grid renders, and it runs under a **strict embedded Content-Security
|
|
53
|
+
Policy: `default-src 'none'`**. That is a hard wall. Inside it:
|
|
54
|
+
|
|
55
|
+
1. **First line, byte-for-byte:** `<!-- @genie group="<group>" -->` where
|
|
56
|
+
`<group>` is the resolved group. This marker is validated as the literal
|
|
57
|
+
first line — no blank line, no doctype before it, nothing.
|
|
58
|
+
2. **No external anything.** No `<link>`, no web fonts, no `<script src>`, no
|
|
59
|
+
remote `<img src>`, no CDN, no Google Fonts. `default-src 'none'` blocks all
|
|
60
|
+
of it and the card would render broken.
|
|
61
|
+
3. **Self-contained.** Inline the CSS in a `<style>` block. Use **system fonts
|
|
62
|
+
only** (`system-ui, -apple-system, Segoe UI, Roboto, sans-serif`, or a
|
|
63
|
+
monospace stack). Embed any imagery as inline SVG or a `data:` URI.
|
|
64
|
+
4. **Byte-identical everywhere.** The same preview must render the same whether
|
|
65
|
+
loaded from `file://`, `localhost`, or the embedded `genie://` surface — so
|
|
66
|
+
never depend on the origin, absolute URLs, or ambient host state.
|
|
67
|
+
5. Honor the kit's tokens (colors, spacing, radius, type scale) as literal CSS
|
|
68
|
+
values in the preview, since it cannot import the kit's stylesheet.
|
|
69
|
+
|
|
70
|
+
The framework implementation file (`.tsx`/`.vue`) is the real component a
|
|
71
|
+
developer drops into their app; the `.html` preview is a faithful static render
|
|
72
|
+
of it for the grid. Keep them visually consistent.
|
|
73
|
+
|
|
74
|
+
# House rules
|
|
75
|
+
|
|
76
|
+
- **Match the kit, don't reinvent it.** If the kit names a clay accent, a radius
|
|
77
|
+
scale, a spacing unit — use those exact values.
|
|
78
|
+
- **Be honest about provenance.** Don't claim to import kit files you weren't
|
|
79
|
+
given; reproduce the kit's _look_ from what the caller described.
|
|
80
|
+
- **Accessibility is not optional.** Semantic elements, a visible focus state,
|
|
81
|
+
labelled controls, sufficient contrast, `alt`/`aria` where needed.
|
|
82
|
+
- **One component per call.** Don't emit a whole page or a component library —
|
|
83
|
+
just the single component the prompt asks for, plus its preview and meta.
|
|
84
|
+
- **Determinism over flair.** No random ids, no timestamps, no "generated at"
|
|
85
|
+
noise. The same request should produce a stable result.
|
|
86
|
+
|
|
87
|
+
# If a reference image or page is attached
|
|
88
|
+
|
|
89
|
+
Treat it as the visual target: match its layout, proportions, color, and
|
|
90
|
+
copy where sensible, while still expressing the kit's tokens. The reference
|
|
91
|
+
shows _what_; the kit governs _how_.
|
|
92
|
+
|
|
93
|
+
# On a retry
|
|
94
|
+
|
|
95
|
+
If the caller re-sends your previous attempt with a validation error appended,
|
|
96
|
+
that attempt failed schema validation. Read the error, fix exactly what it
|
|
97
|
+
names (a bad path, a missing `<ComponentName>.html`, a missing `viewport`, a
|
|
98
|
+
name that isn't PascalCase, …), and return a fully corrected object. Do not
|
|
99
|
+
apologize or explain — just return valid JSON.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
You are **genie**, iterating on a UI component that already exists. The caller
|
|
2
|
+
hands you the component's current source files and an **instruction** describing
|
|
3
|
+
a change ("make the border radius softer", "tighten the padding", "add a
|
|
4
|
+
disabled state"). You return the **complete, updated component** as a strict
|
|
5
|
+
JSON object — never prose, never a chat reply, never a patch or diff. Your
|
|
6
|
+
entire response is consumed by a machine (`response_format: json_schema`), so it
|
|
7
|
+
must be valid against the schema the caller supplies and nothing else.
|
|
8
|
+
|
|
9
|
+
# What you are given
|
|
10
|
+
|
|
11
|
+
- **The current files** — the component's existing source, each as a labelled
|
|
12
|
+
block with its kit-relative path (`components/<group>/<Name>/…`) and full
|
|
13
|
+
content. These are the source of truth for what the component is *today*.
|
|
14
|
+
- **`instruction`** — a natural-language description of the change to make.
|
|
15
|
+
Apply exactly this. Do not redesign the component, rename it, or "improve"
|
|
16
|
+
things the instruction did not ask about.
|
|
17
|
+
- Optionally a **region crop** — a screenshot of a specific rectangle of the
|
|
18
|
+
component's rendered preview, attached as a reference image. When present, the
|
|
19
|
+
instruction is about *that* element/area; focus your edit there and leave the
|
|
20
|
+
rest of the component untouched.
|
|
21
|
+
- Optionally the pixel **coordinates** of that region (in the preview's own
|
|
22
|
+
coordinate space), described in text, for when the crop image itself could not
|
|
23
|
+
be produced.
|
|
24
|
+
|
|
25
|
+
# The cardinal rule: edit, don't rewrite
|
|
26
|
+
|
|
27
|
+
This is a *refinement*. Change only what the instruction requires. Everything
|
|
28
|
+
the instruction does not mention MUST come back byte-for-byte as it was:
|
|
29
|
+
|
|
30
|
+
- Keep the **same `componentName`** and the **same `group`** (and therefore the
|
|
31
|
+
same `components/<group>/<Name>/…` file paths). Renaming is not a refinement.
|
|
32
|
+
- Preserve unrelated markup, classes, props, comments, and whitespace. A caller
|
|
33
|
+
reading the diff between your output and the original should see *only* the
|
|
34
|
+
change they asked for — nothing incidental.
|
|
35
|
+
- Do not drop files. If the component had a `<Name>.tsx`, a `<Name>.html`, and a
|
|
36
|
+
`meta.json`, return all three (updated where the change touches them, verbatim
|
|
37
|
+
where it does not). You may add a file only if the instruction genuinely
|
|
38
|
+
requires one.
|
|
39
|
+
- Keep the framework implementation (`.tsx`/`.vue`/`.html`) and the `.html`
|
|
40
|
+
preview visually consistent after the edit — if you change the radius in one,
|
|
41
|
+
change it in the other.
|
|
42
|
+
|
|
43
|
+
# Output contract
|
|
44
|
+
|
|
45
|
+
Return an object with exactly these top-level keys:
|
|
46
|
+
|
|
47
|
+
- **`componentName`** — PascalCase, echoing the EXISTING component's name
|
|
48
|
+
unchanged.
|
|
49
|
+
- **`group`** — kebab-case, echoing the EXISTING group unchanged.
|
|
50
|
+
- **`files`** — the component's full file set (1–12) after your edit. Every path
|
|
51
|
+
MUST be `components/<group>/<ComponentName>/<filename>` — the same `<group>`
|
|
52
|
+
and `<ComponentName>` as the originals. At minimum return the framework
|
|
53
|
+
implementation, the `<ComponentName>.html` preview, and `meta.json`.
|
|
54
|
+
- **`manifestEntry`** — `{ viewport: { width, height }, subtitle?, tags? }`.
|
|
55
|
+
Carry the original viewport forward unless the instruction changes the
|
|
56
|
+
component's natural size. Keep width and height between 1 and 4096.
|
|
57
|
+
|
|
58
|
+
Set each file's `mimeType` honestly (`text/html`, `text/tsx`, `text/plain`,
|
|
59
|
+
`application/json`, …). File `content` is the full file, 1–65536 chars — the
|
|
60
|
+
WHOLE file after the edit, not a fragment.
|
|
61
|
+
|
|
62
|
+
# The `.html` preview is the contract surface — keep it exact
|
|
63
|
+
|
|
64
|
+
Exactly one file must be `components/<group>/<ComponentName>/<ComponentName>.html`
|
|
65
|
+
(the directory name and the filename share the component name). This preview is
|
|
66
|
+
what the grid renders, under a **strict embedded Content-Security Policy:
|
|
67
|
+
`default-src 'none'`**. That is a hard wall. It was true of the component you
|
|
68
|
+
were given and it must stay true after your edit:
|
|
69
|
+
|
|
70
|
+
1. **First line, byte-for-byte:** `<!-- @genie group="<group>" -->` where
|
|
71
|
+
`<group>` is the (unchanged) resolved group. This marker is validated as the
|
|
72
|
+
literal first line — no blank line, no doctype before it, nothing.
|
|
73
|
+
2. **No external anything.** No `<link>`, no web fonts, no `<script src>`, no
|
|
74
|
+
remote `<img src>`, no CDN, no Google Fonts. `default-src 'none'` blocks all
|
|
75
|
+
of it and the card would render broken.
|
|
76
|
+
3. **Self-contained.** Inline the CSS in a `<style>` block. Use **system fonts
|
|
77
|
+
only** (`system-ui, -apple-system, Segoe UI, Roboto, sans-serif`, or a
|
|
78
|
+
monospace stack). Embed any imagery as inline SVG or a `data:` URI.
|
|
79
|
+
4. **Byte-identical everywhere.** The same preview must render the same whether
|
|
80
|
+
loaded from `file://`, `localhost`, or the embedded `genie://` surface — so
|
|
81
|
+
never depend on the origin, absolute URLs, or ambient host state.
|
|
82
|
+
5. Honor the kit's tokens (colors, spacing, radius, type scale) as literal CSS
|
|
83
|
+
values in the preview, since it cannot import the kit's stylesheet.
|
|
84
|
+
|
|
85
|
+
# If a region crop or coordinates are given
|
|
86
|
+
|
|
87
|
+
The instruction is scoped to that rectangle. Identify which element(s) the crop
|
|
88
|
+
covers, apply the change there, and leave the rest of the component exactly as
|
|
89
|
+
it was. The crop is *where*; the instruction is *what*. If only coordinates are
|
|
90
|
+
given (no image), reason about which element sits at those coordinates from the
|
|
91
|
+
preview's own layout.
|
|
92
|
+
|
|
93
|
+
# House rules
|
|
94
|
+
|
|
95
|
+
- **Match the existing kit, don't reinvent it.** Reuse the component's existing
|
|
96
|
+
tokens, class names, and structure. A refinement inherits the look already
|
|
97
|
+
established; it does not introduce a second design language.
|
|
98
|
+
- **Determinism over flair.** No random ids, no timestamps, no "generated at"
|
|
99
|
+
noise. The same source + instruction should produce a stable result.
|
|
100
|
+
- **Accessibility is not optional.** Preserve (and where the edit touches them,
|
|
101
|
+
maintain) semantic elements, visible focus states, labelled controls,
|
|
102
|
+
sufficient contrast, `alt`/`aria` where needed.
|
|
103
|
+
- **One component per call.** Refine the single component you were given — never
|
|
104
|
+
emit a second component or a whole page.
|
|
105
|
+
|
|
106
|
+
# On a retry
|
|
107
|
+
|
|
108
|
+
If the caller re-sends your previous attempt with a validation error appended,
|
|
109
|
+
that attempt failed schema validation. Read the error, fix exactly what it names
|
|
110
|
+
(a bad path, a missing `<ComponentName>.html`, a missing `viewport`, a name that
|
|
111
|
+
isn't PascalCase, a changed `componentName`/`group`, …), and return a fully
|
|
112
|
+
corrected object. Do not apologize or explain — just return valid JSON.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-prompt loader + versioning (M2-03 · AC5).
|
|
3
|
+
*
|
|
4
|
+
* `conjure` (and later `refine`, M2-04) must load their system prompt from a
|
|
5
|
+
* versioned `*.system.md` file under `prompts/`, and log the prompt's version
|
|
6
|
+
* on every model call (AC5 / AC10). This module owns both halves: reading the
|
|
7
|
+
* markdown off disk once, and deriving a stable content-address for it.
|
|
8
|
+
*
|
|
9
|
+
* ── Why the git *blob* hash is the version ────────────────────────────────────
|
|
10
|
+
* AC5 says "versioned (commit hash logged on every call)". A literal repo commit
|
|
11
|
+
* hash is the wrong unit here: it changes when *any* file in the repo changes,
|
|
12
|
+
* would be null in a shrink-wrapped npm tarball with no `.git`, and can't be
|
|
13
|
+
* computed at runtime without shelling out to git. What we actually want is a
|
|
14
|
+
* fingerprint of *this prompt's bytes* that (a) changes iff the prompt text
|
|
15
|
+
* changes, (b) is computable offline from the file alone, and (c) is
|
|
16
|
+
* independently verifiable. `git hash-object` is exactly that — the SHA-1 of
|
|
17
|
+
* `"blob <byteLength>\0<content>"` — so `promptVersion` for a given file equals
|
|
18
|
+
* `git hash-object <file>`, letting a reviewer confirm which prompt shipped from
|
|
19
|
+
* a log line alone. The prompt CHANGELOG.md carries the human narrative; this
|
|
20
|
+
* hash is the machine-checkable pin.
|
|
21
|
+
*
|
|
22
|
+
* ── Resolution ────────────────────────────────────────────────────────────────
|
|
23
|
+
* Prompts sit next to this module (`llm/prompts/*.system.md`) and are copied
|
|
24
|
+
* into `dist/llm/prompts/` by the build's copy-prompts step, so resolving
|
|
25
|
+
* relative to `import.meta.url` works identically under `tsx` (src) and `node`
|
|
26
|
+
* (dist). Read once and memoized: the file is immutable for the process's life,
|
|
27
|
+
* and re-reading + re-hashing on every `conjure` call would be pointless I/O on
|
|
28
|
+
* genie's hottest path.
|
|
29
|
+
*/
|
|
30
|
+
/** The system prompt for `conjure` (AC5). */
|
|
31
|
+
export declare const GENERATE_COMPONENT_SYSTEM_PROMPT_FILE = "generate-component.system.md";
|
|
32
|
+
/** The system prompt for `refine` (M2-04 · DRO-251). Loaded + versioned by the
|
|
33
|
+
* exact same machinery as the `conjure` prompt — `refine` logs its git blob hash
|
|
34
|
+
* as `promptVersion` on every model call too. */
|
|
35
|
+
export declare const REFINE_COMPONENT_SYSTEM_PROMPT_FILE = "refine-component.system.md";
|
|
36
|
+
/** A loaded prompt: its text and the content-address logged as its version. */
|
|
37
|
+
export interface LoadedPrompt {
|
|
38
|
+
/** The prompt's file name (e.g. `generate-component.system.md`). */
|
|
39
|
+
file: string;
|
|
40
|
+
/** Full markdown text of the prompt. */
|
|
41
|
+
text: string;
|
|
42
|
+
/**
|
|
43
|
+
* The prompt's version — the git blob hash of {@link text}, identical to
|
|
44
|
+
* `git hash-object <file>`. Logged as `promptVersion` on every model call
|
|
45
|
+
* (AC5). Changes iff the prompt's bytes change.
|
|
46
|
+
*/
|
|
47
|
+
version: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The git blob object id of `content` — `sha1("blob " + byteLength + "\0" +
|
|
51
|
+
* content)`, byte-for-byte what `git hash-object` computes. Exported for direct
|
|
52
|
+
* testing against a known `git hash-object` value.
|
|
53
|
+
*/
|
|
54
|
+
export declare function gitBlobHash(content: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Load a prompt from `llm/prompts/<file>`, returning its text and version.
|
|
57
|
+
* Memoized: the first call reads + hashes; subsequent calls return the cached
|
|
58
|
+
* result. Throws if the file is missing (a packaging bug worth failing loudly on
|
|
59
|
+
* — a prompt-less generation verb has nothing to send the model).
|
|
60
|
+
*/
|
|
61
|
+
export declare function loadPrompt(file: string): LoadedPrompt;
|
|
62
|
+
/** Convenience: load the `conjure` system prompt (AC5). */
|
|
63
|
+
export declare function loadGenerateComponentSystemPrompt(): LoadedPrompt;
|
|
64
|
+
/** Convenience: load the `refine` system prompt (M2-04 · DRO-251). */
|
|
65
|
+
export declare function loadRefineComponentSystemPrompt(): LoadedPrompt;
|
|
66
|
+
//# sourceMappingURL=prompts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/llm/prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AASH,6CAA6C;AAC7C,eAAO,MAAM,qCAAqC,iCAAiC,CAAC;AAEpF;;iDAEiD;AACjD,eAAO,MAAM,mCAAmC,+BAA+B,CAAC;AAEhF,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMnD;AAKD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAQrD;AAED,2DAA2D;AAC3D,wBAAgB,iCAAiC,IAAI,YAAY,CAEhE;AAED,sEAAsE;AACtE,wBAAgB,+BAA+B,IAAI,YAAY,CAE9D"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-prompt loader + versioning (M2-03 · AC5).
|
|
3
|
+
*
|
|
4
|
+
* `conjure` (and later `refine`, M2-04) must load their system prompt from a
|
|
5
|
+
* versioned `*.system.md` file under `prompts/`, and log the prompt's version
|
|
6
|
+
* on every model call (AC5 / AC10). This module owns both halves: reading the
|
|
7
|
+
* markdown off disk once, and deriving a stable content-address for it.
|
|
8
|
+
*
|
|
9
|
+
* ── Why the git *blob* hash is the version ────────────────────────────────────
|
|
10
|
+
* AC5 says "versioned (commit hash logged on every call)". A literal repo commit
|
|
11
|
+
* hash is the wrong unit here: it changes when *any* file in the repo changes,
|
|
12
|
+
* would be null in a shrink-wrapped npm tarball with no `.git`, and can't be
|
|
13
|
+
* computed at runtime without shelling out to git. What we actually want is a
|
|
14
|
+
* fingerprint of *this prompt's bytes* that (a) changes iff the prompt text
|
|
15
|
+
* changes, (b) is computable offline from the file alone, and (c) is
|
|
16
|
+
* independently verifiable. `git hash-object` is exactly that — the SHA-1 of
|
|
17
|
+
* `"blob <byteLength>\0<content>"` — so `promptVersion` for a given file equals
|
|
18
|
+
* `git hash-object <file>`, letting a reviewer confirm which prompt shipped from
|
|
19
|
+
* a log line alone. The prompt CHANGELOG.md carries the human narrative; this
|
|
20
|
+
* hash is the machine-checkable pin.
|
|
21
|
+
*
|
|
22
|
+
* ── Resolution ────────────────────────────────────────────────────────────────
|
|
23
|
+
* Prompts sit next to this module (`llm/prompts/*.system.md`) and are copied
|
|
24
|
+
* into `dist/llm/prompts/` by the build's copy-prompts step, so resolving
|
|
25
|
+
* relative to `import.meta.url` works identically under `tsx` (src) and `node`
|
|
26
|
+
* (dist). Read once and memoized: the file is immutable for the process's life,
|
|
27
|
+
* and re-reading + re-hashing on every `conjure` call would be pointless I/O on
|
|
28
|
+
* genie's hottest path.
|
|
29
|
+
*/
|
|
30
|
+
import { readFileSync } from "node:fs";
|
|
31
|
+
import { createHash } from "node:crypto";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
const PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "prompts");
|
|
35
|
+
/** The system prompt for `conjure` (AC5). */
|
|
36
|
+
export const GENERATE_COMPONENT_SYSTEM_PROMPT_FILE = "generate-component.system.md";
|
|
37
|
+
/** The system prompt for `refine` (M2-04 · DRO-251). Loaded + versioned by the
|
|
38
|
+
* exact same machinery as the `conjure` prompt — `refine` logs its git blob hash
|
|
39
|
+
* as `promptVersion` on every model call too. */
|
|
40
|
+
export const REFINE_COMPONENT_SYSTEM_PROMPT_FILE = "refine-component.system.md";
|
|
41
|
+
/**
|
|
42
|
+
* The git blob object id of `content` — `sha1("blob " + byteLength + "\0" +
|
|
43
|
+
* content)`, byte-for-byte what `git hash-object` computes. Exported for direct
|
|
44
|
+
* testing against a known `git hash-object` value.
|
|
45
|
+
*/
|
|
46
|
+
export function gitBlobHash(content) {
|
|
47
|
+
const bytes = Buffer.from(content, "utf-8");
|
|
48
|
+
const header = Buffer.from(`blob ${bytes.length}\0`, "utf-8");
|
|
49
|
+
return createHash("sha1")
|
|
50
|
+
.update(Buffer.concat([header, bytes]))
|
|
51
|
+
.digest("hex");
|
|
52
|
+
}
|
|
53
|
+
// Memoize by file name — each prompt is immutable for the process lifetime.
|
|
54
|
+
const cache = new Map();
|
|
55
|
+
/**
|
|
56
|
+
* Load a prompt from `llm/prompts/<file>`, returning its text and version.
|
|
57
|
+
* Memoized: the first call reads + hashes; subsequent calls return the cached
|
|
58
|
+
* result. Throws if the file is missing (a packaging bug worth failing loudly on
|
|
59
|
+
* — a prompt-less generation verb has nothing to send the model).
|
|
60
|
+
*/
|
|
61
|
+
export function loadPrompt(file) {
|
|
62
|
+
const cached = cache.get(file);
|
|
63
|
+
if (cached)
|
|
64
|
+
return cached;
|
|
65
|
+
const text = readFileSync(join(PROMPTS_DIR, file), "utf-8");
|
|
66
|
+
const loaded = { file, text, version: gitBlobHash(text) };
|
|
67
|
+
cache.set(file, loaded);
|
|
68
|
+
return loaded;
|
|
69
|
+
}
|
|
70
|
+
/** Convenience: load the `conjure` system prompt (AC5). */
|
|
71
|
+
export function loadGenerateComponentSystemPrompt() {
|
|
72
|
+
return loadPrompt(GENERATE_COMPONENT_SYSTEM_PROMPT_FILE);
|
|
73
|
+
}
|
|
74
|
+
/** Convenience: load the `refine` system prompt (M2-04 · DRO-251). */
|
|
75
|
+
export function loadRefineComponentSystemPrompt() {
|
|
76
|
+
return loadPrompt(REFINE_COMPONENT_SYSTEM_PROMPT_FILE);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/llm/prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;AAE7E,6CAA6C;AAC7C,MAAM,CAAC,MAAM,qCAAqC,GAAG,8BAA8B,CAAC;AAEpF;;iDAEiD;AACjD,MAAM,CAAC,MAAM,mCAAmC,GAAG,4BAA4B,CAAC;AAgBhF;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,MAAM,IAAI,EAAE,OAAO,CAAC,CAAC;IAC9D,OAAO,UAAU,CAAC,MAAM,CAAC;SACtB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;SACtC,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC;AAED,4EAA4E;AAC5E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAiB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;IACxE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,iCAAiC;IAC/C,OAAO,UAAU,CAAC,qCAAqC,CAAC,CAAC;AAC3D,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,+BAA+B;IAC7C,OAAO,UAAU,CAAC,mCAAmC,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/** Env var that overrides the default retry ceiling (AC5). */
|
|
2
|
+
export declare const RETRY_MAX_ENV = "GENIE_LLM_RETRY_MAX";
|
|
3
|
+
/** Default retry ceiling when the env var is unset / invalid (AC5). */
|
|
4
|
+
export declare const DEFAULT_RETRY_MAX = 3;
|
|
5
|
+
/** Base backoff delay: 1 s (AC4). */
|
|
6
|
+
export declare const BASE_DELAY_MS = 1000;
|
|
7
|
+
/** Backoff cap: 30 s (AC4). */
|
|
8
|
+
export declare const MAX_DELAY_MS = 30000;
|
|
9
|
+
/** Jitter multiplier applied symmetrically around the exponential value (AC4: ±20 %). */
|
|
10
|
+
export declare const JITTER_RATIO = 0.2;
|
|
11
|
+
/**
|
|
12
|
+
* Hard ceiling on an upstream-honoured `Retry-After` value once it's used as
|
|
13
|
+
* an actual `sleep()` duration (Copilot review on PR #126 / DRO-253).
|
|
14
|
+
* `MAX_DELAY_MS` only bounds OUR OWN computed exponential backoff — a value
|
|
15
|
+
* entirely within this module's control — but an honoured `Retry-After` is
|
|
16
|
+
* trusted verbatim by `parseRetryAfter`. A misconfigured or hostile upstream
|
|
17
|
+
* sending e.g. `Retry-After: 86400` (one day) would otherwise stall the
|
|
18
|
+
* in-flight call — and on the stdio MCP transport, the whole in-flight
|
|
19
|
+
* request — for that entire duration: `GENIE_LLM_REQUEST_TIMEOUT_MS` only
|
|
20
|
+
* bounds each individual HTTP request, not the sleep between retries.
|
|
21
|
+
*
|
|
22
|
+
* Set to 60 s — double `MAX_DELAY_MS` — so an explicit upstream signal is
|
|
23
|
+
* still granted more deference than our own guessed-at exponential
|
|
24
|
+
* schedule, while remaining bounded to a duration reasonable for a
|
|
25
|
+
* synchronous MCP tool call. (The reviewer described this figure as the
|
|
26
|
+
* `openai` SDK's own clamp; the SDK actually applies no such clamp — see
|
|
27
|
+
* `retryRequest`/`calculateDefaultRetryTimeoutMillis` in
|
|
28
|
+
* `openai/client.js`, which honours an unclamped `Retry-After` exactly as
|
|
29
|
+
* this module did before this fix. 60 s is picked on its own merits here,
|
|
30
|
+
* not inherited from upstream SDK behaviour.)
|
|
31
|
+
*
|
|
32
|
+
* Only the `sleep()` duration is clamped. `RateLimitedError.retryAfterMs`
|
|
33
|
+
* (surfaced to the caller on exhaustion, see `withRetry` below)
|
|
34
|
+
* deliberately keeps the TRUE, unclamped upstream value — that field's
|
|
35
|
+
* contract is "what did the upstream ask for" (e.g. so a UI layer can show
|
|
36
|
+
* "come back tomorrow"), not "how long did we actually wait".
|
|
37
|
+
*/
|
|
38
|
+
export declare const MAX_RETRY_AFTER_MS = 60000;
|
|
39
|
+
/**
|
|
40
|
+
* Thrown when a 429 rate-limit response was retried the full budget and the
|
|
41
|
+
* upstream never gave a non-429 answer (AC6). Kept distinct from
|
|
42
|
+
* `TransientError` so callers that want to surface `ERR_RATE_LIMITED`
|
|
43
|
+
* separately from generic-transient-failure can branch on the type — matches
|
|
44
|
+
* the FR-046 PRD note about surfacing rate-limits distinctly.
|
|
45
|
+
*
|
|
46
|
+
* `retryAfterMs` carries the last `Retry-After` value (in milliseconds) the
|
|
47
|
+
* upstream sent, so a UI layer can show "come back in ~30 s" without having
|
|
48
|
+
* to unwrap the cause itself. Undefined if the final response carried no
|
|
49
|
+
* `Retry-After` header.
|
|
50
|
+
*/
|
|
51
|
+
export declare class RateLimitedError extends Error {
|
|
52
|
+
readonly attempts: number;
|
|
53
|
+
readonly cause: Error;
|
|
54
|
+
readonly retryAfterMs?: number | undefined;
|
|
55
|
+
readonly name = "RateLimitedError";
|
|
56
|
+
constructor(message: string, attempts: number, cause: Error, retryAfterMs?: number | undefined);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Thrown when a 5xx / network error was retried the full budget and the
|
|
60
|
+
* upstream never recovered (AC6). Callers that don't care about the
|
|
61
|
+
* distinction from `RateLimitedError` should catch `TransientError |
|
|
62
|
+
* RateLimitedError` as a union; both extend `Error` and both preserve the
|
|
63
|
+
* originating cause on `.cause`.
|
|
64
|
+
*/
|
|
65
|
+
export declare class TransientError extends Error {
|
|
66
|
+
readonly attempts: number;
|
|
67
|
+
readonly cause: Error;
|
|
68
|
+
readonly name = "TransientError";
|
|
69
|
+
constructor(message: string, attempts: number, cause: Error);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolve `GENIE_LLM_RETRY_MAX`, falling back to `DEFAULT_RETRY_MAX` when
|
|
73
|
+
* unset, blank, non-numeric, or negative. Mirrors the established fallback
|
|
74
|
+
* shape of `resolveTimeoutMs` in `client.ts` and `getPlanTTL` in
|
|
75
|
+
* `plans/index.ts`.
|
|
76
|
+
*
|
|
77
|
+
* A concrete `"0"` is a legitimate operator choice — one attempt, no retries
|
|
78
|
+
* — and must NOT be collapsed with "unset". An operator running against a
|
|
79
|
+
* test lab where they own the retry policy externally sets 0 and expects
|
|
80
|
+
* exactly 1 attempt, not 4.
|
|
81
|
+
*
|
|
82
|
+
* Requires the trimmed value to be ENTIRELY digits (Copilot review on PR
|
|
83
|
+
* #126 / DRO-253): `parseInt` alone silently accepts a leading-numeric
|
|
84
|
+
* prefix — `parseInt("5s", 10)` and `parseInt("3.5", 10)` both parse as
|
|
85
|
+
* valid (5 and 3 respectively) despite neither being a clean integer,
|
|
86
|
+
* contradicting this function's own "non-numeric → default" contract and
|
|
87
|
+
* silently misconfiguring an operator who fat-fingered the env var (e.g.
|
|
88
|
+
* copy-pasted a value with a trailing unit). The regex guard rejects
|
|
89
|
+
* anything that isn't purely `[0-9]+` before `parseInt` ever sees it.
|
|
90
|
+
*/
|
|
91
|
+
export declare function resolveRetryMax(env?: NodeJS.ProcessEnv): number;
|
|
92
|
+
/**
|
|
93
|
+
* Parse an HTTP `Retry-After` header (or the non-standard `retry-after-ms`)
|
|
94
|
+
* into milliseconds (AC3).
|
|
95
|
+
*
|
|
96
|
+
* Precedence:
|
|
97
|
+
* 1. `retry-after-ms` (millisecond precision, non-standard but common)
|
|
98
|
+
* 2. `retry-after` as fractional seconds (RFC 9110 delay-seconds form)
|
|
99
|
+
* 3. `retry-after` as HTTP-date (RFC 9110 date form) → delta from now,
|
|
100
|
+
* clamped to 0 if the date is in the past.
|
|
101
|
+
*
|
|
102
|
+
* Returns `null` for absent, empty, or unparseable values — the caller then
|
|
103
|
+
* falls back to the jittered exponential schedule (AC4).
|
|
104
|
+
*
|
|
105
|
+
* The two-level parse (seconds first, HTTP-date fallback) matches the openai
|
|
106
|
+
* SDK's own `retryRequest` logic in `openai/client.js` so anyone already
|
|
107
|
+
* relying on either header form gets consistent behaviour whether they hit
|
|
108
|
+
* this wrapper or the SDK's own internal retry.
|
|
109
|
+
*/
|
|
110
|
+
export declare function parseRetryAfter(headers: Headers | undefined): number | null;
|
|
111
|
+
/**
|
|
112
|
+
* Compute the sleep duration for the given retry `attempt` (1-indexed —
|
|
113
|
+
* `attempt=1` is the delay before the first retry, i.e. after the initial
|
|
114
|
+
* attempt failed). Base 1 s, doubles per attempt, capped at 30 s, then a
|
|
115
|
+
* uniform ±20 % jitter applied on top (AC4).
|
|
116
|
+
*
|
|
117
|
+
* The cap is applied to the exponential value BEFORE the jitter multiplier,
|
|
118
|
+
* so at high attempt counts the delay is still bounded by
|
|
119
|
+
* `MAX_DELAY_MS * (1 + JITTER_RATIO)` on paper — but then clamped back to
|
|
120
|
+
* `MAX_DELAY_MS` a second time, so the effective ceiling is exactly 30 s
|
|
121
|
+
* (a strict interpretation of "cap 30 s" in the spec).
|
|
122
|
+
*
|
|
123
|
+
* `random` is injectable so tests can drive deterministic values without
|
|
124
|
+
* needing to stub `Math.random` globally.
|
|
125
|
+
*/
|
|
126
|
+
export declare function jitteredBackoff(attempt: number, random?: () => number): number;
|
|
127
|
+
/**
|
|
128
|
+
* Options accepted by `withRetry`. All injectable to keep the wrapper unit-
|
|
129
|
+
* testable without wall-clock waits or global `Math.random` / `process.env`
|
|
130
|
+
* stubbing.
|
|
131
|
+
*/
|
|
132
|
+
export interface WithRetryOptions {
|
|
133
|
+
/**
|
|
134
|
+
* Sleep injection — defaults to a `setTimeout`-backed promise. Tests pass
|
|
135
|
+
* `() => Promise.resolve()` to skip real waits without needing fake
|
|
136
|
+
* timers or a global sleep stub.
|
|
137
|
+
*/
|
|
138
|
+
sleep?: (ms: number) => Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* Random source for jitter — defaults to `Math.random`. Tests pass
|
|
141
|
+
* `() => 0.5` for deterministic midpoint behaviour.
|
|
142
|
+
*/
|
|
143
|
+
random?: () => number;
|
|
144
|
+
/**
|
|
145
|
+
* Explicit retry ceiling. When set, overrides both the env var and the
|
|
146
|
+
* default — useful in tests that need to force a specific number of
|
|
147
|
+
* attempts without touching `process.env`. Total attempts = `1 + maxRetries`.
|
|
148
|
+
*/
|
|
149
|
+
maxRetries?: number;
|
|
150
|
+
/**
|
|
151
|
+
* Env source for `maxRetries` resolution when `maxRetries` isn't set.
|
|
152
|
+
* Defaults to `process.env`. Tests pass `{}` to force the default.
|
|
153
|
+
*/
|
|
154
|
+
env?: NodeJS.ProcessEnv;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Wrap an async LLM handler with retry/backoff (AC1). Returns a function
|
|
158
|
+
* with the SAME argument list and return type as the input handler, so
|
|
159
|
+
* `withRetry(createChatCompletion)` is a drop-in replacement for
|
|
160
|
+
* `createChatCompletion` at every call site.
|
|
161
|
+
*
|
|
162
|
+
* Order of operations per attempt:
|
|
163
|
+
* 1. Call the handler.
|
|
164
|
+
* 2. On success → return the result immediately.
|
|
165
|
+
* 3. On failure → classify (AC2). Non-retryable errors propagate as-is
|
|
166
|
+
* (the original error object, not a wrapped one — a caller that
|
|
167
|
+
* already has instanceof/status handling for BadRequestError et al.
|
|
168
|
+
* keeps working unchanged).
|
|
169
|
+
* 4. On retryable failure with remaining budget → emit an `llm.retry`
|
|
170
|
+
* log line (AC7), sleep for `Retry-After` (AC3) or the jittered
|
|
171
|
+
* backoff (AC4), then loop.
|
|
172
|
+
* 5. On retryable failure with exhausted budget → throw
|
|
173
|
+
* `RateLimitedError` (if the tail-cause was a 429) or `TransientError`
|
|
174
|
+
* (otherwise), preserving the original error on `.cause` (AC6).
|
|
175
|
+
*
|
|
176
|
+
* A handler that transitions from one retryable kind to another across
|
|
177
|
+
* attempts (say 429 → 503 → 500) throws whichever kind matches the LAST
|
|
178
|
+
* failing attempt, on the reasoning that "what the caller sees now" is the
|
|
179
|
+
* most useful discriminator for their fallback branch — a 429-turned-503
|
|
180
|
+
* indicates a genuinely degraded upstream, not a persistent rate-limit.
|
|
181
|
+
*/
|
|
182
|
+
export declare function withRetry<A extends readonly unknown[], R>(handler: (...args: A) => Promise<R>, opts?: WithRetryOptions): (...args: A) => Promise<R>;
|
|
183
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/llm/retry.ts"],"names":[],"mappings":"AA4CA,8DAA8D;AAC9D,eAAO,MAAM,aAAa,wBAAwB,CAAC;AACnD,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,qCAAqC;AACrC,eAAO,MAAM,aAAa,OAAQ,CAAC;AACnC,+BAA+B;AAC/B,eAAO,MAAM,YAAY,QAAS,CAAC;AACnC,yFAAyF;AACzF,eAAO,MAAM,YAAY,MAAM,CAAC;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAgDzC;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aAIvB,QAAQ,EAAE,MAAM;aAChB,KAAK,EAAE,KAAK;aACZ,YAAY,CAAC,EAAE,MAAM;IALvC,QAAQ,CAAC,IAAI,sBAAsB;gBAEjC,OAAO,EAAE,MAAM,EACC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,KAAK,EACZ,YAAY,CAAC,EAAE,MAAM,YAAA;CAIxC;AAED;;;;;;GAMG;AACH,qBAAa,cAAe,SAAQ,KAAK;aAIrB,QAAQ,EAAE,MAAM;aAChB,KAAK,EAAE,KAAK;IAJ9B,QAAQ,CAAC,IAAI,oBAAoB;gBAE/B,OAAO,EAAE,MAAM,EACC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,KAAK;CAI/B;AAID;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAY5E;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CA4C3E;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,MAAoB,GAAG,MAAM,CAK3F;AA2HD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,EAAE,CAAC,EACvD,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EACnC,IAAI,GAAE,gBAAqB,GAC1B,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAqE5B"}
|