@avocadostudio-ai/orchestrator-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +201 -0
  2. package/dist/agent/agent-context.d.ts +19 -0
  3. package/dist/agent/agent-context.js +67 -0
  4. package/dist/agent/agent-logger.d.ts +5 -0
  5. package/dist/agent/agent-logger.js +22 -0
  6. package/dist/agent/agent-loop-openai.d.ts +8 -0
  7. package/dist/agent/agent-loop-openai.js +172 -0
  8. package/dist/agent/agent-loop.d.ts +56 -0
  9. package/dist/agent/agent-loop.js +167 -0
  10. package/dist/agent/agent-provider.d.ts +28 -0
  11. package/dist/agent/agent-provider.js +63 -0
  12. package/dist/agent/agent-tools.d.ts +28 -0
  13. package/dist/agent/agent-tools.js +899 -0
  14. package/dist/agent/context/editing-guidelines.md +46 -0
  15. package/dist/agent/context/role.md +39 -0
  16. package/dist/agent/integration-prompt.d.ts +9 -0
  17. package/dist/agent/integration-prompt.js +154 -0
  18. package/dist/agent/sites-agent-context.d.ts +12 -0
  19. package/dist/agent/sites-agent-context.js +316 -0
  20. package/dist/agent/sites-agent-shared.d.ts +161 -0
  21. package/dist/agent/sites-agent-shared.js +1101 -0
  22. package/dist/agent/sites-agent-tools.d.ts +18 -0
  23. package/dist/agent/sites-agent-tools.js +1227 -0
  24. package/dist/chat/anthropic-cache.d.ts +20 -0
  25. package/dist/chat/anthropic-cache.js +54 -0
  26. package/dist/chat/anthropic-planner.d.ts +98 -0
  27. package/dist/chat/anthropic-planner.js +1012 -0
  28. package/dist/chat/changelog-coverage-validator.d.ts +37 -0
  29. package/dist/chat/changelog-coverage-validator.js +215 -0
  30. package/dist/chat/chat-pipeline-context.d.ts +211 -0
  31. package/dist/chat/chat-pipeline-context.js +249 -0
  32. package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
  33. package/dist/chat/chat-pipeline-deterministic.js +407 -0
  34. package/dist/chat/chat-pipeline-image.d.ts +86 -0
  35. package/dist/chat/chat-pipeline-image.js +897 -0
  36. package/dist/chat/chat-pipeline-shared.d.ts +69 -0
  37. package/dist/chat/chat-pipeline-shared.js +212 -0
  38. package/dist/chat/chat-pipeline-translation.d.ts +27 -0
  39. package/dist/chat/chat-pipeline-translation.js +417 -0
  40. package/dist/chat/chat-pipeline-ui.d.ts +14 -0
  41. package/dist/chat/chat-pipeline-ui.js +244 -0
  42. package/dist/chat/chat-pipeline.d.ts +99 -0
  43. package/dist/chat/chat-pipeline.js +3999 -0
  44. package/dist/chat/decomposer.d.ts +21 -0
  45. package/dist/chat/decomposer.js +65 -0
  46. package/dist/chat/gemini-planner.d.ts +70 -0
  47. package/dist/chat/gemini-planner.js +541 -0
  48. package/dist/chat/hallucination-validator.d.ts +36 -0
  49. package/dist/chat/hallucination-validator.js +110 -0
  50. package/dist/chat/locale-strings.d.ts +47 -0
  51. package/dist/chat/locale-strings.js +100 -0
  52. package/dist/chat/plan-json-schema.d.ts +133 -0
  53. package/dist/chat/plan-json-schema.js +112 -0
  54. package/dist/chat/planner-types.d.ts +120 -0
  55. package/dist/chat/planner-types.js +66 -0
  56. package/dist/chat/planner.d.ts +148 -0
  57. package/dist/chat/planner.js +1361 -0
  58. package/dist/chat/prompts.d.ts +67 -0
  59. package/dist/chat/prompts.js +356 -0
  60. package/dist/chat/provider-routing.d.ts +14 -0
  61. package/dist/chat/provider-routing.js +27 -0
  62. package/dist/chat/variation-pipeline.d.ts +135 -0
  63. package/dist/chat/variation-pipeline.js +837 -0
  64. package/dist/chat/vision-alt-generator.d.ts +35 -0
  65. package/dist/chat/vision-alt-generator.js +152 -0
  66. package/dist/cms/adapter.d.ts +62 -0
  67. package/dist/cms/adapter.js +1 -0
  68. package/dist/cms/bootstrap.d.ts +17 -0
  69. package/dist/cms/bootstrap.js +85 -0
  70. package/dist/cms/editor-api-adapter.d.ts +23 -0
  71. package/dist/cms/editor-api-adapter.js +71 -0
  72. package/dist/cms/index.d.ts +4 -0
  73. package/dist/cms/index.js +3 -0
  74. package/dist/cms/json-file-adapter.d.ts +11 -0
  75. package/dist/cms/json-file-adapter.js +62 -0
  76. package/dist/demo-mode.d.ts +59 -0
  77. package/dist/demo-mode.js +201 -0
  78. package/dist/errors.d.ts +67 -0
  79. package/dist/errors.js +129 -0
  80. package/dist/http/chat-stream-resumable.d.ts +108 -0
  81. package/dist/http/chat-stream-resumable.js +290 -0
  82. package/dist/http/chat-stream.d.ts +99 -0
  83. package/dist/http/chat-stream.js +92 -0
  84. package/dist/image/gdrive-client.d.ts +22 -0
  85. package/dist/image/gdrive-client.js +215 -0
  86. package/dist/image/image-helpers.d.ts +95 -0
  87. package/dist/image/image-helpers.js +488 -0
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.js +1 -0
  90. package/dist/jira/jira-approval.d.ts +22 -0
  91. package/dist/jira/jira-approval.js +51 -0
  92. package/dist/jira/jira-client.d.ts +44 -0
  93. package/dist/jira/jira-client.js +313 -0
  94. package/dist/jira/jira-poller.d.ts +46 -0
  95. package/dist/jira/jira-poller.js +184 -0
  96. package/dist/jira/jira-processor.d.ts +103 -0
  97. package/dist/jira/jira-processor.js +1085 -0
  98. package/dist/jira/jira-types.d.ts +117 -0
  99. package/dist/jira/jira-types.js +38 -0
  100. package/dist/logger.d.ts +12 -0
  101. package/dist/logger.js +28 -0
  102. package/dist/migration/mcp-server-stdio.d.ts +8 -0
  103. package/dist/migration/mcp-server-stdio.js +672 -0
  104. package/dist/migration/migration-prompt.d.ts +7 -0
  105. package/dist/migration/migration-prompt.js +197 -0
  106. package/dist/migration/migration-tools.d.ts +17 -0
  107. package/dist/migration/migration-tools.js +159 -0
  108. package/dist/migration/scrape-cache.d.ts +9 -0
  109. package/dist/migration/scrape-cache.js +19 -0
  110. package/dist/nlp/deterministic-planner-context.d.ts +141 -0
  111. package/dist/nlp/deterministic-planner-context.js +362 -0
  112. package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
  113. package/dist/nlp/deterministic-planner-pages.js +170 -0
  114. package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
  115. package/dist/nlp/deterministic-planner-patches.js +508 -0
  116. package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
  117. package/dist/nlp/deterministic-planner-refs.js +164 -0
  118. package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
  119. package/dist/nlp/deterministic-planner-suggestions.js +579 -0
  120. package/dist/nlp/deterministic-planner.d.ts +85 -0
  121. package/dist/nlp/deterministic-planner.js +1631 -0
  122. package/dist/nlp/intent-detection.d.ts +309 -0
  123. package/dist/nlp/intent-detection.js +730 -0
  124. package/dist/nlp/intent-helpers.d.ts +15 -0
  125. package/dist/nlp/intent-helpers.js +243 -0
  126. package/dist/nlp/intent-patterns.d.ts +40 -0
  127. package/dist/nlp/intent-patterns.js +223 -0
  128. package/dist/nlp/plan-normalizer.d.ts +41 -0
  129. package/dist/nlp/plan-normalizer.js +1537 -0
  130. package/dist/ops/destructive-action-gate.d.ts +44 -0
  131. package/dist/ops/destructive-action-gate.js +90 -0
  132. package/dist/ops/ops-engine.d.ts +151 -0
  133. package/dist/ops/ops-engine.js +1394 -0
  134. package/dist/publish/diff-engine.d.ts +18 -0
  135. package/dist/publish/diff-engine.js +305 -0
  136. package/dist/publish/publish-helpers.d.ts +87 -0
  137. package/dist/publish/publish-helpers.js +521 -0
  138. package/dist/publish/publish-target-registry.d.ts +7 -0
  139. package/dist/publish/publish-target-registry.js +61 -0
  140. package/dist/publish/publish-target.d.ts +81 -0
  141. package/dist/publish/publish-target.js +1 -0
  142. package/dist/publish/targets/deploy-hook.d.ts +13 -0
  143. package/dist/publish/targets/deploy-hook.js +123 -0
  144. package/dist/publish/targets/git.d.ts +13 -0
  145. package/dist/publish/targets/git.js +55 -0
  146. package/dist/publish/targets/site-contract.d.ts +19 -0
  147. package/dist/publish/targets/site-contract.js +124 -0
  148. package/dist/state/content-source.d.ts +17 -0
  149. package/dist/state/content-source.js +1 -0
  150. package/dist/state/in-memory-content-source.d.ts +27 -0
  151. package/dist/state/in-memory-content-source.js +51 -0
  152. package/dist/state/session-lock.d.ts +13 -0
  153. package/dist/state/session-lock.js +29 -0
  154. package/dist/state/session-state.d.ts +310 -0
  155. package/dist/state/session-state.js +1083 -0
  156. package/dist/state/sqlite-store-singleton.d.ts +31 -0
  157. package/dist/state/sqlite-store-singleton.js +170 -0
  158. package/dist/state/sqlite-store.d.ts +135 -0
  159. package/dist/state/sqlite-store.js +421 -0
  160. package/dist/telemetry/chat-telemetry.d.ts +105 -0
  161. package/dist/telemetry/chat-telemetry.js +247 -0
  162. package/dist/telemetry/eval-candidate-store.d.ts +50 -0
  163. package/dist/telemetry/eval-candidate-store.js +120 -0
  164. package/dist/telemetry/feedback-store.d.ts +34 -0
  165. package/dist/telemetry/feedback-store.js +76 -0
  166. package/dist/telemetry/jira-telemetry.d.ts +57 -0
  167. package/dist/telemetry/jira-telemetry.js +68 -0
  168. package/dist/telemetry/migration-telemetry.d.ts +35 -0
  169. package/dist/telemetry/migration-telemetry.js +40 -0
  170. package/dist/telemetry/usage.d.ts +24 -0
  171. package/dist/telemetry/usage.js +80 -0
  172. package/dist/tools/builtin-registrations.d.ts +12 -0
  173. package/dist/tools/builtin-registrations.js +33 -0
  174. package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
  175. package/dist/tools/builtins/gdrive-browse.js +68 -0
  176. package/dist/tools/builtins/image-generate.d.ts +3 -0
  177. package/dist/tools/builtins/image-generate.js +211 -0
  178. package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
  179. package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
  180. package/dist/tools/builtins/unsplash-search.d.ts +3 -0
  181. package/dist/tools/builtins/unsplash-search.js +74 -0
  182. package/dist/tools/executor.d.ts +23 -0
  183. package/dist/tools/executor.js +169 -0
  184. package/dist/tools/index.d.ts +5 -0
  185. package/dist/tools/index.js +5 -0
  186. package/dist/tools/registry.d.ts +21 -0
  187. package/dist/tools/registry.js +75 -0
  188. package/dist/tools/runtime.d.ts +27 -0
  189. package/dist/tools/runtime.js +48 -0
  190. package/dist/tools/schema-validator.d.ts +24 -0
  191. package/dist/tools/schema-validator.js +88 -0
  192. package/dist/tools/types.d.ts +86 -0
  193. package/dist/tools/types.js +1 -0
  194. package/dist/variation-images.d.ts +19 -0
  195. package/dist/variation-images.js +12 -0
  196. package/package.json +78 -0
@@ -0,0 +1,201 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Demo mode — single-feature playground gated on a shared API key
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // When DEMO_MODE=1 is set, the orchestrator becomes a "try it before you
6
+ // sign up" playground: the hosting team's OPENAI_API_KEY / ANTHROPIC_API_KEY
7
+ // powers the planner, but everything is locked down so casual users can't
8
+ // drain it. The only operation permitted is `update_props` on a Hero block.
9
+ // Everything else (add/remove/move blocks, editing other blocks, agent
10
+ // routes, image generation) returns a friendly rejection.
11
+ //
12
+ // Design choices:
13
+ // - SERVER-ENFORCED: client-trusted `siteCapabilities` flags aren't enough;
14
+ // a curl request could bypass them. We enforce in `applyOpsAtomically`
15
+ // and via a Fastify preHandler hook.
16
+ // - PER-IP ISOLATION: each demo visitor gets their own ephemeral session
17
+ // key `demo-<hash>` seeded from `demoPublishedPages()`. No persistence —
18
+ // state resets on server restart. Avoids visitors stomping on each other.
19
+ // - RATE LIMITING: simple in-memory token bucket per IP. Default 20 req/hr.
20
+ // - NO IMAGES: detectImageOps is short-circuited in demo mode so DALL-E /
21
+ // Unsplash calls can't be triggered. Text-only edits keep latency low
22
+ // and costs bounded.
23
+ import { createHash } from "node:crypto";
24
+ import { OperationError } from "./errors.js";
25
+ // ---------------------------------------------------------------------------
26
+ // Config — read once at module load, memoized
27
+ // ---------------------------------------------------------------------------
28
+ const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
29
+ function envFlag(name, fallback = false) {
30
+ const raw = process.env[name];
31
+ if (raw === undefined)
32
+ return fallback;
33
+ return TRUE_VALUES.has(raw.trim().toLowerCase());
34
+ }
35
+ function envList(name, fallback) {
36
+ const raw = process.env[name];
37
+ if (!raw)
38
+ return fallback;
39
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
40
+ return parts.length > 0 ? parts : fallback;
41
+ }
42
+ function envNumber(name, fallback) {
43
+ const raw = process.env[name];
44
+ if (!raw)
45
+ return fallback;
46
+ const n = Number(raw);
47
+ return Number.isFinite(n) && n > 0 ? n : fallback;
48
+ }
49
+ /** Is demo mode enabled for this orchestrator process? */
50
+ export function isDemoModeEnabled() {
51
+ return envFlag("DEMO_MODE", false);
52
+ }
53
+ /** Operation types allowed in demo mode. Defaults to ["update_props"]. */
54
+ export function getDemoAllowedOpTypes() {
55
+ return envList("DEMO_ALLOWED_OPS", ["update_props"]);
56
+ }
57
+ /** Block types on which update_props is allowed. Defaults to ["Hero"]. */
58
+ export function getDemoAllowedBlockTypes() {
59
+ return envList("DEMO_ALLOWED_BLOCK_TYPES", ["Hero"]);
60
+ }
61
+ /** Max requests per IP per hour. Defaults to 20. */
62
+ export function getDemoRateLimitPerHour() {
63
+ return envNumber("DEMO_RATE_LIMIT_PER_IP_PER_HOUR", 20);
64
+ }
65
+ /** Should image generation be disabled in demo mode? Defaults to true. */
66
+ export function isDemoImageGenDisabled() {
67
+ return envFlag("DEMO_DISABLE_IMAGE_GEN", true);
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // Per-IP session key
71
+ // ---------------------------------------------------------------------------
72
+ /**
73
+ * Deterministic ephemeral session key for a demo visitor.
74
+ *
75
+ * We hash the IP to avoid leaking it, and keep the key short enough to read
76
+ * in logs. The key does NOT contain "::" so it routes through the default
77
+ * `getSessionDraft` path which auto-seeds from `demoPublishedPages()`.
78
+ */
79
+ export function demoSessionKeyForIp(ip) {
80
+ const normalized = (ip || "anon").trim() || "anon";
81
+ const hash = createHash("sha1").update(normalized).digest("hex").slice(0, 10);
82
+ return `demo-${hash}`;
83
+ }
84
+ /**
85
+ * Extract a client IP from a Fastify request. Render puts the real client IP
86
+ * in `x-forwarded-for` (first entry). Falls back to request.ip.
87
+ */
88
+ export function extractClientIp(request) {
89
+ const xff = request.headers["x-forwarded-for"];
90
+ const header = Array.isArray(xff) ? xff[0] : xff;
91
+ if (typeof header === "string" && header.length > 0) {
92
+ const first = header.split(",")[0]?.trim();
93
+ if (first)
94
+ return first;
95
+ }
96
+ return request.ip ?? "anon";
97
+ }
98
+ /**
99
+ * Look up a block by id across all staged pages. Returns its `type` string,
100
+ * or undefined if the block wasn't found.
101
+ */
102
+ function findBlockTypeInStaged(staged, blockId) {
103
+ for (const page of staged.values()) {
104
+ for (const block of page.blocks) {
105
+ if (block.id === blockId)
106
+ return block.type;
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+ /**
112
+ * Validate a list of operations against demo-mode rules. Returns the split
113
+ * between allowed and rejected ops; caller decides whether to proceed with
114
+ * the allowed subset or to throw.
115
+ */
116
+ export function splitDemoOps(ops, staged) {
117
+ const allowedTypes = new Set(getDemoAllowedOpTypes());
118
+ const allowedBlockTypes = new Set(getDemoAllowedBlockTypes());
119
+ const allowed = [];
120
+ const rejected = [];
121
+ for (const op of ops) {
122
+ if (!allowedTypes.has(op.op)) {
123
+ rejected.push({
124
+ op,
125
+ reason: `Operation type "${op.op}" is not available in the demo. Allowed: ${[...allowedTypes].join(", ")}.`
126
+ });
127
+ continue;
128
+ }
129
+ // `update_props` is the only op type we currently allow, so we only need
130
+ // to special-case that. When the allow-list is widened, add branches here.
131
+ if (op.op === "update_props") {
132
+ const blockType = findBlockTypeInStaged(staged, op.blockId);
133
+ if (!blockType) {
134
+ rejected.push({
135
+ op,
136
+ reason: `Block "${op.blockId}" was not found in the current draft.`
137
+ });
138
+ continue;
139
+ }
140
+ if (!allowedBlockTypes.has(blockType)) {
141
+ rejected.push({
142
+ op,
143
+ reason: `Editing "${blockType}" blocks is disabled in the demo. Only ${[...allowedBlockTypes].join(" / ")} edits are available — try changing the hero headline.`
144
+ });
145
+ continue;
146
+ }
147
+ allowed.push(op);
148
+ continue;
149
+ }
150
+ // Belt-and-suspenders: if an op type slipped past allowedTypes but isn't
151
+ // update_props, refuse it explicitly.
152
+ rejected.push({
153
+ op,
154
+ reason: `Operation "${op.op}" is not permitted in the demo.`
155
+ });
156
+ }
157
+ return { allowed, rejected };
158
+ }
159
+ /**
160
+ * Throws an `OperationError` if any of the given ops violates demo-mode
161
+ * rules. Used as the final gate inside `applyOpsAtomically`, so both chat
162
+ * and direct `/ops` paths are covered.
163
+ */
164
+ export function enforceDemoOps(ops, staged) {
165
+ const { rejected } = splitDemoOps(ops, staged);
166
+ if (rejected.length === 0)
167
+ return;
168
+ const firstReason = rejected[0]?.reason ?? "Operation not permitted in demo mode.";
169
+ throw new OperationError(`Demo mode: ${firstReason}`, {
170
+ category: "planner_refusal",
171
+ retryable: false,
172
+ userMessage: "This demo only supports editing the hero section. Try: \"change the hero headline to Welcome to my new site\"."
173
+ });
174
+ }
175
+ const WINDOW_MS = 60 * 60 * 1000; // 1 hour
176
+ const rateBuckets = new Map();
177
+ /**
178
+ * Consume one token for the given IP. Returns `{ ok: true }` if allowed,
179
+ * or `{ ok: false, retryAfterSeconds }` if the quota for the current
180
+ * hourly window is exhausted.
181
+ */
182
+ export function consumeDemoRateToken(ip) {
183
+ const now = Date.now();
184
+ const limit = getDemoRateLimitPerHour();
185
+ const key = ip || "anon";
186
+ const bucket = rateBuckets.get(key);
187
+ if (!bucket || now - bucket.windowStart >= WINDOW_MS) {
188
+ rateBuckets.set(key, { count: 1, windowStart: now });
189
+ return { ok: true };
190
+ }
191
+ if (bucket.count >= limit) {
192
+ const retryAfterMs = WINDOW_MS - (now - bucket.windowStart);
193
+ return { ok: false, retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1000)) };
194
+ }
195
+ bucket.count += 1;
196
+ return { ok: true };
197
+ }
198
+ /** Reset the rate limiter (test hook). */
199
+ export function _resetDemoRateLimiterForTests() {
200
+ rateBuckets.clear();
201
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Combined error category covering guardrails, planner failures, and
3
+ * operational validation. Superset of the former `GuardrailErrorCategory`
4
+ * and `PlannerFailureReasonCategory` types.
5
+ */
6
+ export type ErrorCategory = "schema_violation" | "ambiguity" | "not_found" | "no_effective_change" | "planner_refusal" | "incomplete_output" | "malformed_output" | "internal_error" | "canceled" | "operation_failed";
7
+ export type GuardrailErrorCategory = ErrorCategory;
8
+ export type PlannerFailureReasonCategory = Extract<ErrorCategory, "schema_violation" | "planner_refusal" | "incomplete_output" | "malformed_output" | "internal_error">;
9
+ /**
10
+ * Base error for all orchestrator-layer failures.
11
+ * Carries a machine-readable `category`, a flag indicating whether the
12
+ * caller may retry, and a human-friendly `userMessage`.
13
+ */
14
+ export declare class OrchestrationError extends Error {
15
+ readonly category: ErrorCategory;
16
+ readonly retryable: boolean;
17
+ readonly userMessage: string;
18
+ constructor(message: string, options: {
19
+ category: ErrorCategory;
20
+ retryable?: boolean;
21
+ userMessage?: string;
22
+ });
23
+ }
24
+ /** Raised when the AI planner produces invalid / refused / incomplete output. */
25
+ export declare class PlannerError extends OrchestrationError {
26
+ /** Alias kept for call-sites that read `.reasonCategory`. */
27
+ get reasonCategory(): PlannerFailureReasonCategory;
28
+ constructor(message: string, options: {
29
+ reasonCategory: PlannerFailureReasonCategory;
30
+ retryable?: boolean;
31
+ userMessage?: string;
32
+ });
33
+ }
34
+ /** Raised when input fails a guardrail check before reaching the planner. */
35
+ export declare class GuardrailError extends OrchestrationError {
36
+ constructor(message: string, options: {
37
+ category: GuardrailErrorCategory;
38
+ retryable?: boolean;
39
+ userMessage?: string;
40
+ });
41
+ }
42
+ /** Raised when an operation (add_block, update_props, …) fails during apply. */
43
+ export declare class OperationError extends OrchestrationError {
44
+ constructor(message: string, options?: {
45
+ category?: ErrorCategory;
46
+ retryable?: boolean;
47
+ userMessage?: string;
48
+ });
49
+ }
50
+ /** Raised when the user (or an abort signal) cancels an in-flight request. */
51
+ export declare class CancelError extends OrchestrationError {
52
+ constructor(reason?: string);
53
+ }
54
+ export declare function isOrchestrationError(error: unknown): error is OrchestrationError;
55
+ export declare function isPlannerError(error: unknown): error is PlannerError;
56
+ /** @deprecated Use `isPlannerError` instead. Kept for backward compatibility. */
57
+ export declare function isPlannerOutputError(error: unknown): error is PlannerError;
58
+ export declare function isGuardrailError(error: unknown): error is GuardrailError;
59
+ export declare function isOperationError(error: unknown): error is OperationError;
60
+ export declare function isCancelError(error: unknown): error is CancelError;
61
+ /**
62
+ * Extract a human-readable error string from any thrown value.
63
+ * Handles `Error`, Zod-like `{ issues }` objects, and plain strings.
64
+ */
65
+ export declare function toErrorDetail(error: unknown): string;
66
+ /** @deprecated Use `PlannerError` directly. */
67
+ export declare const PlannerOutputError: typeof PlannerError;
package/dist/errors.js ADDED
@@ -0,0 +1,129 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Unified error hierarchy for the orchestrator
3
+ // ---------------------------------------------------------------------------
4
+ // ---------------------------------------------------------------------------
5
+ // Base class
6
+ // ---------------------------------------------------------------------------
7
+ /**
8
+ * Base error for all orchestrator-layer failures.
9
+ * Carries a machine-readable `category`, a flag indicating whether the
10
+ * caller may retry, and a human-friendly `userMessage`.
11
+ */
12
+ export class OrchestrationError extends Error {
13
+ category;
14
+ retryable;
15
+ userMessage;
16
+ constructor(message, options) {
17
+ super(message);
18
+ this.name = "OrchestrationError";
19
+ this.category = options.category;
20
+ this.retryable = options.retryable ?? false;
21
+ this.userMessage = options.userMessage ?? message;
22
+ }
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // Subclasses
26
+ // ---------------------------------------------------------------------------
27
+ /** Raised when the AI planner produces invalid / refused / incomplete output. */
28
+ export class PlannerError extends OrchestrationError {
29
+ /** Alias kept for call-sites that read `.reasonCategory`. */
30
+ get reasonCategory() {
31
+ return this.category;
32
+ }
33
+ constructor(message, options) {
34
+ super(message, {
35
+ category: options.reasonCategory,
36
+ retryable: options.retryable,
37
+ userMessage: options.userMessage
38
+ });
39
+ this.name = "PlannerError";
40
+ }
41
+ }
42
+ /** Raised when input fails a guardrail check before reaching the planner. */
43
+ export class GuardrailError extends OrchestrationError {
44
+ constructor(message, options) {
45
+ super(message, options);
46
+ this.name = "GuardrailError";
47
+ }
48
+ }
49
+ /** Raised when an operation (add_block, update_props, …) fails during apply. */
50
+ export class OperationError extends OrchestrationError {
51
+ constructor(message, options) {
52
+ super(message, {
53
+ category: options?.category ?? "operation_failed",
54
+ retryable: options?.retryable ?? false,
55
+ userMessage: options?.userMessage
56
+ });
57
+ this.name = "OperationError";
58
+ }
59
+ }
60
+ /** Raised when the user (or an abort signal) cancels an in-flight request. */
61
+ export class CancelError extends OrchestrationError {
62
+ constructor(reason) {
63
+ super(reason ?? "user_canceled", {
64
+ category: "canceled",
65
+ retryable: false,
66
+ userMessage: "Stopped this request."
67
+ });
68
+ this.name = "CancelError";
69
+ }
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Type guards
73
+ // ---------------------------------------------------------------------------
74
+ export function isOrchestrationError(error) {
75
+ return error instanceof OrchestrationError;
76
+ }
77
+ export function isPlannerError(error) {
78
+ return error instanceof PlannerError;
79
+ }
80
+ /** @deprecated Use `isPlannerError` instead. Kept for backward compatibility. */
81
+ export function isPlannerOutputError(error) {
82
+ return isPlannerError(error);
83
+ }
84
+ export function isGuardrailError(error) {
85
+ return error instanceof GuardrailError;
86
+ }
87
+ export function isOperationError(error) {
88
+ return error instanceof OperationError;
89
+ }
90
+ export function isCancelError(error) {
91
+ return error instanceof CancelError;
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // Unified toErrorDetail — single source of truth
95
+ // ---------------------------------------------------------------------------
96
+ /**
97
+ * Extract a human-readable error string from any thrown value.
98
+ * Handles `Error`, Zod-like `{ issues }` objects, and plain strings.
99
+ */
100
+ export function toErrorDetail(error) {
101
+ if (error instanceof OrchestrationError)
102
+ return error.userMessage;
103
+ if (error instanceof Error) {
104
+ // Try to extract a Zod-style message embedded in the error string
105
+ const issueMatch = /"message"\s*:\s*"([^"]+)"/.exec(error.message);
106
+ if (issueMatch?.[1])
107
+ return issueMatch[1];
108
+ return error.message;
109
+ }
110
+ if (error &&
111
+ typeof error === "object" &&
112
+ "issues" in error &&
113
+ Array.isArray(error.issues)) {
114
+ const first = error.issues[0];
115
+ if (first) {
116
+ const msg = typeof first.message === "string" ? first.message : "Invalid model output";
117
+ const path = Array.isArray(first.path) && first.path.length > 0 ? ` at ${first.path.join(".")}` : "";
118
+ return `${msg}${path}`;
119
+ }
120
+ }
121
+ if (typeof error === "string")
122
+ return error;
123
+ return "Unknown planner error";
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Backward-compatible aliases
127
+ // ---------------------------------------------------------------------------
128
+ /** @deprecated Use `PlannerError` directly. */
129
+ export const PlannerOutputError = PlannerError;
@@ -0,0 +1,108 @@
1
+ import type { ChatPipelineContext } from "../chat/chat-pipeline.ts";
2
+ import type { ChatRequestBody } from "../nlp/intent-detection.ts";
3
+ import { type ChatStreamEvent } from "./chat-stream.ts";
4
+ export type StreamState = "pending" | "active" | "done" | "error" | "canceled";
5
+ export type SeqEnvelope = ChatStreamEvent & {
6
+ _seq: number;
7
+ };
8
+ export interface BufferedEvent {
9
+ seq: number;
10
+ event: ChatStreamEvent;
11
+ at: string;
12
+ }
13
+ export interface Subscriber {
14
+ emit(envelope: SeqEnvelope): void;
15
+ close(): void;
16
+ }
17
+ export interface StreamEntry {
18
+ streamId: string;
19
+ body: ChatRequestBody;
20
+ session: string;
21
+ siteId: string;
22
+ origin: string;
23
+ createdAt: number;
24
+ state: StreamState;
25
+ abortController: AbortController;
26
+ events: BufferedEvent[];
27
+ lastSeq: number;
28
+ subscribers: Set<Subscriber>;
29
+ }
30
+ export interface ResumableStreamStoreOptions {
31
+ /** TTL for pending streams that never got a /chat/stream connection (default 60s). */
32
+ contextTtlMs?: number;
33
+ /** TTL for terminal runs (kept around so a reconnect can still replay; default 15 min). */
34
+ runTtlMs?: number;
35
+ /** Ring-buffer cap for buffered events (default 2000). */
36
+ maxEvents?: number;
37
+ /** Max simultaneously pending+active streams per session (default 3). */
38
+ maxPendingPerSession?: number;
39
+ /** Sweep interval for the cleanup timer (default 60s). 0 disables the timer. */
40
+ sweepIntervalMs?: number;
41
+ }
42
+ export declare class ResumableStreamStore {
43
+ private readonly contexts;
44
+ private readonly opts;
45
+ private sweepTimer;
46
+ constructor(options?: ResumableStreamStoreOptions);
47
+ /** Stop the sweep timer. Call before process exit in tests. */
48
+ dispose(): void;
49
+ get(streamId: string): StreamEntry | undefined;
50
+ countPendingForSession(session: string, siteId: string): number;
51
+ findActiveForSession(session: string, siteId: string): StreamEntry | null;
52
+ /** Allocate a new pending stream. Throws if session is over the pending cap. */
53
+ allocate(args: {
54
+ body: ChatRequestBody;
55
+ session: string;
56
+ siteId: string;
57
+ origin: string;
58
+ }): StreamEntry;
59
+ markState(streamId: string, state: StreamState): void;
60
+ /** Cancel by streamId, or by (session, siteId) for the active run on that session. */
61
+ cancel(by: {
62
+ streamId?: string;
63
+ session?: string;
64
+ siteId?: string;
65
+ }): {
66
+ status: "canceled" | "already_terminal" | "not_found";
67
+ streamId?: string;
68
+ };
69
+ /**
70
+ * Append an event to the buffer (with assigned seq number) and broadcast to
71
+ * all current subscribers. Returns the assigned seq, or -1 if streamId is
72
+ * unknown.
73
+ */
74
+ emit(streamId: string, event: ChatStreamEvent): number;
75
+ /** Subscribe and immediately replay any events with seq > afterSeq. Returns an unsubscribe fn. */
76
+ subscribe(streamId: string, sub: Subscriber, afterSeq?: number): () => void;
77
+ /** Close all subscribers for a stream. Call when the run reaches a terminal state. */
78
+ closeAllSubscribers(streamId: string): void;
79
+ private cleanExpiredPending;
80
+ private sweepTerminal;
81
+ }
82
+ export declare function isTerminalState(state: StreamState): boolean;
83
+ export declare class TooManyPendingStreamsError extends Error {
84
+ readonly session: string;
85
+ readonly siteId: string;
86
+ constructor(session: string, siteId: string);
87
+ }
88
+ export interface RunResumableChatStreamOptions {
89
+ /** Heartbeat interval (default 1000 ms). 0 disables. */
90
+ heartbeatIntervalMs?: number;
91
+ /**
92
+ * When the last emitted event is "op_applied" and no new event arrives within
93
+ * this window, switch the heartbeat label to "Wrapping up…". Set 0 to disable.
94
+ * Default 1500 ms — matches the Fastify route.
95
+ */
96
+ applyingIdleMs?: number;
97
+ }
98
+ /**
99
+ * Drive `runChatStream` through a store entry. Transitions the entry's state
100
+ * (active → done/error/canceled), buffers events, manages heartbeat + idle
101
+ * timers, and closes subscribers on terminal. Returns the pipeline's HTTP code.
102
+ *
103
+ * Caller should have already called `store.markState(streamId, "active")` if
104
+ * the run was kicked off by a GET /chat/stream. (Done here as a safety net.)
105
+ */
106
+ export declare function runResumableChatStream(ctx: ChatPipelineContext, store: ResumableStreamStore, streamId: string, scopedBody: ChatRequestBody, options?: RunResumableChatStreamOptions): Promise<{
107
+ code: number;
108
+ }>;