@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,290 @@
1
+ // Resumable streaming chat: POST /chat/start → GET /chat/stream(?afterSeq) → POST /chat/cancel.
2
+ //
3
+ // Lives in core (transport-agnostic) so both the Fastify route and Web-handler
4
+ // createOrchestrator() can use the same state machine. The Fastify route
5
+ // (apps/orchestrator/src/routes/chat.ts) currently has its own copy of this
6
+ // logic — it can be migrated to use this module when convenient; the existing
7
+ // Fastify integration tests guard the contract.
8
+ //
9
+ // What lives here:
10
+ // - ResumableStreamStore: streamContexts map, sweep timer, allocate/cancel,
11
+ // buffered emit with seq numbers, subscriber set with broadcast
12
+ // - runResumableChatStream(): glues the store to runChatStream() plus the
13
+ // heartbeat + applying-idle timers that the Fastify route exercises
14
+ //
15
+ // What does NOT live here:
16
+ // - HTTP framing (SSE wire format, headers, status codes) — caller's job
17
+ // - The subscriber's transport. Callers pass a Subscriber interface; the
18
+ // Fastify route would back it with reply.raw, the Web handler with a
19
+ // ReadableStreamDefaultController.
20
+ import { randomUUID } from "node:crypto";
21
+ import { runChatStream } from "./chat-stream.js";
22
+ const DEFAULTS = {
23
+ contextTtlMs: 60_000,
24
+ runTtlMs: 900_000,
25
+ maxEvents: 2000,
26
+ maxPendingPerSession: 3,
27
+ sweepIntervalMs: 60_000
28
+ };
29
+ export class ResumableStreamStore {
30
+ contexts = new Map();
31
+ opts;
32
+ sweepTimer = null;
33
+ constructor(options = {}) {
34
+ this.opts = { ...DEFAULTS, ...options };
35
+ if (this.opts.sweepIntervalMs > 0) {
36
+ this.sweepTimer = setInterval(() => this.sweepTerminal(), this.opts.sweepIntervalMs);
37
+ if (this.sweepTimer && typeof this.sweepTimer === "object" && "unref" in this.sweepTimer) {
38
+ this.sweepTimer.unref();
39
+ }
40
+ }
41
+ }
42
+ /** Stop the sweep timer. Call before process exit in tests. */
43
+ dispose() {
44
+ if (this.sweepTimer) {
45
+ clearInterval(this.sweepTimer);
46
+ this.sweepTimer = null;
47
+ }
48
+ }
49
+ get(streamId) {
50
+ return this.contexts.get(streamId);
51
+ }
52
+ countPendingForSession(session, siteId) {
53
+ let count = 0;
54
+ for (const entry of this.contexts.values()) {
55
+ if (entry.session === session && entry.siteId === siteId && (entry.state === "pending" || entry.state === "active")) {
56
+ count++;
57
+ }
58
+ }
59
+ return count;
60
+ }
61
+ findActiveForSession(session, siteId) {
62
+ for (const entry of this.contexts.values()) {
63
+ if (entry.session === session && entry.siteId === siteId && entry.state === "active")
64
+ return entry;
65
+ }
66
+ return null;
67
+ }
68
+ /** Allocate a new pending stream. Throws if session is over the pending cap. */
69
+ allocate(args) {
70
+ this.cleanExpiredPending();
71
+ if (this.countPendingForSession(args.session, args.siteId) >= this.opts.maxPendingPerSession) {
72
+ throw new TooManyPendingStreamsError(args.session, args.siteId);
73
+ }
74
+ const streamId = randomUUID();
75
+ const entry = {
76
+ streamId,
77
+ body: args.body,
78
+ session: args.session,
79
+ siteId: args.siteId,
80
+ origin: args.origin,
81
+ createdAt: Date.now(),
82
+ state: "pending",
83
+ abortController: new AbortController(),
84
+ events: [],
85
+ lastSeq: 0,
86
+ subscribers: new Set()
87
+ };
88
+ this.contexts.set(streamId, entry);
89
+ return entry;
90
+ }
91
+ markState(streamId, state) {
92
+ const entry = this.contexts.get(streamId);
93
+ if (entry)
94
+ entry.state = state;
95
+ }
96
+ /** Cancel by streamId, or by (session, siteId) for the active run on that session. */
97
+ cancel(by) {
98
+ let entry;
99
+ if (by.streamId)
100
+ entry = this.contexts.get(by.streamId);
101
+ else if (by.session !== undefined) {
102
+ const found = this.findActiveForSession(by.session, by.siteId ?? "");
103
+ if (found)
104
+ entry = found;
105
+ }
106
+ if (!entry)
107
+ return { status: "not_found" };
108
+ if (isTerminalState(entry.state))
109
+ return { status: "already_terminal", streamId: entry.streamId };
110
+ entry.state = "canceled";
111
+ entry.abortController.abort("user_canceled");
112
+ this.emit(entry.streamId, { type: "status", message: "Cancellation requested..." });
113
+ return { status: "canceled", streamId: entry.streamId };
114
+ }
115
+ /**
116
+ * Append an event to the buffer (with assigned seq number) and broadcast to
117
+ * all current subscribers. Returns the assigned seq, or -1 if streamId is
118
+ * unknown.
119
+ */
120
+ emit(streamId, event) {
121
+ const entry = this.contexts.get(streamId);
122
+ if (!entry)
123
+ return -1;
124
+ entry.lastSeq += 1;
125
+ const buffered = {
126
+ seq: entry.lastSeq,
127
+ event,
128
+ at: new Date().toISOString()
129
+ };
130
+ if (entry.events.length >= this.opts.maxEvents)
131
+ entry.events.shift();
132
+ entry.events.push(buffered);
133
+ const envelope = { ...event, _seq: entry.lastSeq };
134
+ for (const sub of entry.subscribers) {
135
+ try {
136
+ sub.emit(envelope);
137
+ }
138
+ catch { /* subscriber's transport may be closed */ }
139
+ }
140
+ return entry.lastSeq;
141
+ }
142
+ /** Subscribe and immediately replay any events with seq > afterSeq. Returns an unsubscribe fn. */
143
+ subscribe(streamId, sub, afterSeq = 0) {
144
+ const entry = this.contexts.get(streamId);
145
+ if (!entry)
146
+ return () => { };
147
+ for (const buffered of entry.events) {
148
+ if (buffered.seq > afterSeq) {
149
+ try {
150
+ sub.emit({ ...buffered.event, _seq: buffered.seq });
151
+ }
152
+ catch { /* */ }
153
+ }
154
+ }
155
+ if (isTerminalState(entry.state)) {
156
+ try {
157
+ sub.close();
158
+ }
159
+ catch { /* */ }
160
+ return () => { };
161
+ }
162
+ entry.subscribers.add(sub);
163
+ return () => { entry.subscribers.delete(sub); };
164
+ }
165
+ /** Close all subscribers for a stream. Call when the run reaches a terminal state. */
166
+ closeAllSubscribers(streamId) {
167
+ const entry = this.contexts.get(streamId);
168
+ if (!entry)
169
+ return;
170
+ for (const sub of entry.subscribers) {
171
+ try {
172
+ sub.close();
173
+ }
174
+ catch { /* */ }
175
+ }
176
+ entry.subscribers.clear();
177
+ }
178
+ cleanExpiredPending() {
179
+ const now = Date.now();
180
+ for (const [id, entry] of this.contexts) {
181
+ if (entry.state === "pending" && now - entry.createdAt > this.opts.contextTtlMs) {
182
+ this.contexts.delete(id);
183
+ }
184
+ }
185
+ }
186
+ sweepTerminal() {
187
+ const now = Date.now();
188
+ for (const [id, entry] of this.contexts) {
189
+ if (now - entry.createdAt > this.opts.runTtlMs)
190
+ this.contexts.delete(id);
191
+ }
192
+ }
193
+ }
194
+ export function isTerminalState(state) {
195
+ return state === "done" || state === "error" || state === "canceled";
196
+ }
197
+ export class TooManyPendingStreamsError extends Error {
198
+ session;
199
+ siteId;
200
+ constructor(session, siteId) {
201
+ super(`too many pending streams for session=${session} siteId=${siteId}`);
202
+ this.session = session;
203
+ this.siteId = siteId;
204
+ this.name = "TooManyPendingStreamsError";
205
+ }
206
+ }
207
+ /**
208
+ * Drive `runChatStream` through a store entry. Transitions the entry's state
209
+ * (active → done/error/canceled), buffers events, manages heartbeat + idle
210
+ * timers, and closes subscribers on terminal. Returns the pipeline's HTTP code.
211
+ *
212
+ * Caller should have already called `store.markState(streamId, "active")` if
213
+ * the run was kicked off by a GET /chat/stream. (Done here as a safety net.)
214
+ */
215
+ export async function runResumableChatStream(ctx, store, streamId, scopedBody, options = {}) {
216
+ const entry = store.get(streamId);
217
+ if (!entry)
218
+ return { code: 410 };
219
+ if (entry.state === "pending")
220
+ entry.state = "active";
221
+ const heartbeatMs = options.heartbeatIntervalMs ?? 1000;
222
+ const applyingIdleMs = options.applyingIdleMs ?? 1500;
223
+ const startedAt = Date.now();
224
+ let heartbeatStage = "planning";
225
+ let heartbeatLabel = "Planning";
226
+ let heartbeatTimer = null;
227
+ if (heartbeatMs > 0) {
228
+ heartbeatTimer = setInterval(() => {
229
+ store.emit(streamId, {
230
+ type: "heartbeat",
231
+ stage: heartbeatStage,
232
+ label: heartbeatLabel,
233
+ elapsedMs: Date.now() - startedAt
234
+ });
235
+ }, heartbeatMs);
236
+ if (heartbeatTimer && typeof heartbeatTimer === "object" && "unref" in heartbeatTimer) {
237
+ heartbeatTimer.unref();
238
+ }
239
+ }
240
+ let applyingIdleTimer = null;
241
+ const clearApplyingIdleTimer = () => {
242
+ if (applyingIdleTimer) {
243
+ clearTimeout(applyingIdleTimer);
244
+ applyingIdleTimer = null;
245
+ }
246
+ };
247
+ // Wrap the store's emit so we can observe specific events for label transitions.
248
+ const emit = (event) => {
249
+ if (event.type === "plan_meta") {
250
+ const n = event.estimatedOps ?? 0;
251
+ heartbeatLabel = n > 0 ? `Plan ready (${n} change${n === 1 ? "" : "s"})` : "Plan ready";
252
+ }
253
+ else if (event.type === "op_applied") {
254
+ heartbeatStage = "applying";
255
+ // Only show the (i/total) fraction for multi-op plans — "(1/1)" undersells a
256
+ // single op that rewrites many fields (e.g. a whole-section translate).
257
+ heartbeatLabel = event.total > 1
258
+ ? `Applying changes (${event.index}/${event.total})`
259
+ : "Applying changes";
260
+ clearApplyingIdleTimer();
261
+ if (applyingIdleMs > 0) {
262
+ applyingIdleTimer = setTimeout(() => {
263
+ applyingIdleTimer = null;
264
+ heartbeatLabel = "Wrapping up…";
265
+ store.emit(streamId, { type: "status", message: heartbeatLabel });
266
+ }, applyingIdleMs);
267
+ }
268
+ }
269
+ else if (event.type === "status") {
270
+ heartbeatLabel = event.message;
271
+ clearApplyingIdleTimer();
272
+ }
273
+ store.emit(streamId, event);
274
+ };
275
+ try {
276
+ const result = await runChatStream(ctx, scopedBody, {
277
+ emit,
278
+ signal: entry.abortController.signal
279
+ });
280
+ const terminal = result.code >= 400 ? (result.code === 499 ? "canceled" : "error") : "done";
281
+ entry.state = terminal;
282
+ return { code: result.code };
283
+ }
284
+ finally {
285
+ if (heartbeatTimer)
286
+ clearInterval(heartbeatTimer);
287
+ clearApplyingIdleTimer();
288
+ store.closeAllSubscribers(streamId);
289
+ }
290
+ }
@@ -0,0 +1,99 @@
1
+ import { type ChatPipelineContext } from "../chat/chat-pipeline.ts";
2
+ import type { ChatRequestBody } from "../nlp/intent-detection.ts";
3
+ export type ChatStreamEvent = {
4
+ type: "token";
5
+ text: string;
6
+ } | {
7
+ type: "thinking_start";
8
+ } | {
9
+ type: "thinking_token";
10
+ text: string;
11
+ } | {
12
+ type: "thinking_end";
13
+ durationMs: number;
14
+ } | {
15
+ type: "field_draft";
16
+ blockId: string;
17
+ editablePath: string;
18
+ value: string;
19
+ pageSlug?: string;
20
+ } | {
21
+ type: "summary_token";
22
+ text: string;
23
+ } | {
24
+ type: "changelog_entry";
25
+ entry: string;
26
+ } | {
27
+ type: "op_candidate";
28
+ index: number;
29
+ op: unknown;
30
+ } | {
31
+ type: "op_skipped";
32
+ index: number;
33
+ total: number;
34
+ op: unknown;
35
+ reason: string;
36
+ } | {
37
+ type: "plan_meta";
38
+ intent: unknown;
39
+ summary: string;
40
+ estimatedOps: number;
41
+ } | {
42
+ type: "op_applied";
43
+ index: number;
44
+ total: number;
45
+ op: unknown;
46
+ previewVersion: number;
47
+ focusBlockId: string | null;
48
+ updatedSlug?: string;
49
+ } | {
50
+ type: "rollback_started";
51
+ appliedCount: number;
52
+ reason: string;
53
+ } | {
54
+ type: "rollback_done";
55
+ restoredVersion: number;
56
+ } | {
57
+ type: "status";
58
+ message: string;
59
+ } | {
60
+ type: "image_progress";
61
+ percent: number;
62
+ stage: string;
63
+ } | {
64
+ type: "heartbeat";
65
+ stage: "planning" | "applying";
66
+ label: string;
67
+ elapsedMs: number;
68
+ } | {
69
+ type: "final";
70
+ result: unknown;
71
+ } | {
72
+ type: "error";
73
+ result: unknown;
74
+ code: number;
75
+ } | {
76
+ type: "canceled";
77
+ message: string;
78
+ };
79
+ export type ChatStreamEmit = (event: ChatStreamEvent) => void;
80
+ export interface RunChatStreamOptions {
81
+ emit: ChatStreamEmit;
82
+ signal?: AbortSignal;
83
+ }
84
+ /**
85
+ * Runs the planner pipeline and emits events through `emit`. Returns when the
86
+ * pipeline has produced its terminal event (final / error / canceled).
87
+ *
88
+ * The caller is responsible for scoping the session key already; we don't
89
+ * re-scope to keep the contract symmetric with `runChatPipeline`.
90
+ */
91
+ export declare function runChatStream(ctx: ChatPipelineContext, body: ChatRequestBody, options: RunChatStreamOptions): Promise<{
92
+ code: number;
93
+ }>;
94
+ /**
95
+ * Format an event as a single SSE `data: …\n\n` frame. Most callers will use
96
+ * this — both the Fastify route and the Web-handler version of /chat/stream
97
+ * want the same wire format.
98
+ */
99
+ export declare function formatSseFrame(event: ChatStreamEvent): string;
@@ -0,0 +1,92 @@
1
+ // Transport-agnostic streaming wrapper around `runChatPipeline`.
2
+ //
3
+ // The Fastify route at apps/orchestrator/src/routes/chat.ts and the Web-handler
4
+ // `createOrchestrator()` in packages/site-sdk/src/server both call this helper.
5
+ // It accepts a single `emit(type, payload)` sink instead of 10 `on*` callbacks,
6
+ // runs the planner pipeline, and translates terminal outcomes (success / error
7
+ // / cancel) into a final event.
8
+ //
9
+ // What this helper does NOT cover:
10
+ // - SSE wire format (callers encode events themselves: `data: ${json}\n\n`)
11
+ // - The resumable replay layer (streamContexts map, /chat/start triplet)
12
+ // - Heartbeat / idle timers — caller's responsibility, since those need a
13
+ // timer that survives the caller's transport lifecycle
14
+ //
15
+ // Why those are out of scope: they're transport-coupled (Fastify reply.raw vs
16
+ // Web ReadableStream controller) and would force this helper to know about
17
+ // timers + writable streams. Keeping it pure makes both call sites trivial.
18
+ import { isCancelError, cleanupImagePlaceholders } from "../chat/chat-pipeline-shared.js";
19
+ import { runChatPipeline } from "../chat/chat-pipeline.js";
20
+ /**
21
+ * Runs the planner pipeline and emits events through `emit`. Returns when the
22
+ * pipeline has produced its terminal event (final / error / canceled).
23
+ *
24
+ * The caller is responsible for scoping the session key already; we don't
25
+ * re-scope to keep the contract symmetric with `runChatPipeline`.
26
+ */
27
+ export async function runChatStream(ctx, body, options) {
28
+ const { emit, signal } = options;
29
+ try {
30
+ const result = await runChatPipeline(ctx, body, {
31
+ signal,
32
+ onPlanningToken: (token) => emit({ type: "token", text: token }),
33
+ onThinking: (event) => {
34
+ if (event.type === "start")
35
+ emit({ type: "thinking_start" });
36
+ else if (event.type === "token")
37
+ emit({ type: "thinking_token", text: event.text });
38
+ else if (event.type === "end")
39
+ emit({ type: "thinking_end", durationMs: event.durationMs });
40
+ },
41
+ onFieldDraft: (event) => emit({
42
+ type: "field_draft",
43
+ blockId: event.blockId,
44
+ editablePath: event.editablePath,
45
+ value: event.value,
46
+ ...(event.pageSlug ? { pageSlug: event.pageSlug } : {})
47
+ }),
48
+ onSummaryChunk: (text) => emit({ type: "summary_token", text }),
49
+ onChangeLogEntry: (entry) => emit({ type: "changelog_entry", entry }),
50
+ onPlannedOp: (event) => emit({ type: "op_candidate", index: event.index, op: event.op }),
51
+ onOpSkipped: (event) => emit({ type: "op_skipped", index: event.index, total: event.total, op: event.op, reason: event.reason }),
52
+ onPlanMeta: (event) => emit({ type: "plan_meta", intent: event.intent, summary: event.summary, estimatedOps: event.estimatedOps }),
53
+ onOpApplied: (event) => emit({
54
+ type: "op_applied",
55
+ index: event.index,
56
+ total: event.total,
57
+ op: event.op,
58
+ previewVersion: event.previewVersion,
59
+ focusBlockId: event.focusBlockId ?? null,
60
+ ...(event.updatedSlug ? { updatedSlug: event.updatedSlug } : {})
61
+ }),
62
+ onRollbackStarted: (event) => emit({ type: "rollback_started", appliedCount: event.appliedCount, reason: event.reason }),
63
+ onRollbackDone: (event) => emit({ type: "rollback_done", restoredVersion: event.restoredVersion }),
64
+ onStatusUpdate: (message) => emit({ type: "status", message }),
65
+ onImageProgress: (event) => emit({ type: "image_progress", percent: event.percent, stage: event.stage })
66
+ });
67
+ if (result.code >= 400) {
68
+ emit({ type: "error", result: result.payload, code: result.code });
69
+ return { code: result.code };
70
+ }
71
+ emit({ type: "final", result: result.payload });
72
+ return { code: result.code };
73
+ }
74
+ catch (err) {
75
+ if (isCancelError(err)) {
76
+ cleanupImagePlaceholders(body.session ?? "dev");
77
+ emit({ type: "canceled", message: "Stopped this request." });
78
+ return { code: 499 };
79
+ }
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ emit({ type: "error", result: { error: message }, code: 500 });
82
+ return { code: 500 };
83
+ }
84
+ }
85
+ /**
86
+ * Format an event as a single SSE `data: …\n\n` frame. Most callers will use
87
+ * this — both the Fastify route and the Web-handler version of /chat/stream
88
+ * want the same wire format.
89
+ */
90
+ export function formatSseFrame(event) {
91
+ return `data: ${JSON.stringify(event)}\n\n`;
92
+ }
@@ -0,0 +1,22 @@
1
+ import type { ImageLogger } from "./image-helpers.ts";
2
+ export type GDriveImageItem = {
3
+ id: string;
4
+ name: string;
5
+ mimeType: string;
6
+ thumbnailLink: string;
7
+ size: number;
8
+ width?: number;
9
+ height?: number;
10
+ };
11
+ /** Reset cached client (useful after env changes or in tests). */
12
+ export declare function resetDriveClient(): void;
13
+ /** Convert a file name like "hero_image-v2.jpg" to alt text "hero image v2". */
14
+ export declare function fileNameToAlt(name: string): string;
15
+ export declare function isGdriveConfigured(): boolean;
16
+ /** Resolve folder ID from an override or fall back to env var. Returns undefined if neither is set. */
17
+ export declare function resolveGdriveFolderId(override?: string): string | undefined;
18
+ export declare function listImages(folderId: string, query?: string, log?: ImageLogger, maxResults?: number, skipCache?: boolean): Promise<GDriveImageItem[]>;
19
+ export declare function downloadImage(fileId: string, log?: ImageLogger): Promise<{
20
+ filePath: string;
21
+ fileName: string;
22
+ } | null>;