laohuang 0.3.2 → 0.4.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.
Files changed (86) hide show
  1. package/README.md +142 -16
  2. package/dist/agent.d.ts +166 -0
  3. package/dist/agent.js +858 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/bash-runner.d.ts +79 -0
  6. package/dist/bash-runner.js +464 -0
  7. package/dist/bash-runner.js.map +1 -0
  8. package/dist/cancellation.d.ts +36 -0
  9. package/dist/cancellation.js +123 -0
  10. package/dist/cancellation.js.map +1 -0
  11. package/dist/cli.d.ts +117 -0
  12. package/dist/cli.js +1307 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/client.d.ts +21 -0
  15. package/dist/client.js +17 -0
  16. package/dist/client.js.map +1 -0
  17. package/dist/commands.d.ts +123 -0
  18. package/dist/commands.js +660 -0
  19. package/dist/commands.js.map +1 -0
  20. package/dist/config.d.ts +51 -0
  21. package/dist/config.js +183 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/credentials.d.ts +12 -0
  24. package/dist/credentials.js +102 -0
  25. package/dist/credentials.js.map +1 -0
  26. package/dist/events.d.ts +288 -0
  27. package/dist/events.js +838 -0
  28. package/dist/events.js.map +1 -0
  29. package/dist/model-adapter.d.ts +138 -0
  30. package/dist/model-adapter.js +244 -0
  31. package/dist/model-adapter.js.map +1 -0
  32. package/dist/model-selection.d.ts +67 -0
  33. package/dist/model-selection.js +148 -0
  34. package/dist/model-selection.js.map +1 -0
  35. package/dist/model-stream.d.ts +128 -0
  36. package/dist/model-stream.js +582 -0
  37. package/dist/model-stream.js.map +1 -0
  38. package/dist/project-instructions.d.ts +99 -0
  39. package/dist/project-instructions.js +348 -0
  40. package/dist/project-instructions.js.map +1 -0
  41. package/dist/providers.d.ts +9 -0
  42. package/dist/providers.js +26 -0
  43. package/dist/providers.js.map +1 -0
  44. package/dist/routing.d.ts +170 -0
  45. package/dist/routing.js +669 -0
  46. package/dist/routing.js.map +1 -0
  47. package/dist/semantic-classifier.d.ts +66 -0
  48. package/dist/semantic-classifier.js +86 -0
  49. package/dist/semantic-classifier.js.map +1 -0
  50. package/dist/session.d.ts +162 -0
  51. package/dist/session.js +871 -0
  52. package/dist/session.js.map +1 -0
  53. package/dist/system-prompt.d.ts +16 -0
  54. package/dist/system-prompt.js +42 -0
  55. package/dist/system-prompt.js.map +1 -0
  56. package/dist/terminal/editor.d.ts +161 -0
  57. package/dist/terminal/editor.js +1060 -0
  58. package/dist/terminal/editor.js.map +1 -0
  59. package/dist/terminal/input.d.ts +61 -0
  60. package/dist/terminal/input.js +276 -0
  61. package/dist/terminal/input.js.map +1 -0
  62. package/dist/terminal/markdown.d.ts +20 -0
  63. package/dist/terminal/markdown.js +621 -0
  64. package/dist/terminal/markdown.js.map +1 -0
  65. package/dist/terminal/screen.d.ts +66 -0
  66. package/dist/terminal/screen.js +624 -0
  67. package/dist/terminal/screen.js.map +1 -0
  68. package/dist/terminal/theme.d.ts +23 -0
  69. package/dist/terminal/theme.js +101 -0
  70. package/dist/terminal/theme.js.map +1 -0
  71. package/dist/terminal/ui.d.ts +285 -0
  72. package/dist/terminal/ui.js +1815 -0
  73. package/dist/terminal/ui.js.map +1 -0
  74. package/dist/tools.d.ts +95 -0
  75. package/dist/tools.js +444 -0
  76. package/dist/tools.js.map +1 -0
  77. package/dist/ui-state.d.ts +51 -0
  78. package/dist/ui-state.js +194 -0
  79. package/dist/ui-state.js.map +1 -0
  80. package/dist/web.d.ts +56 -0
  81. package/dist/web.js +247 -0
  82. package/dist/web.js.map +1 -0
  83. package/package.json +24 -21
  84. package/bin/laohuang.js +0 -10
  85. package/lib/launcher.js +0 -133
  86. package/vendor/laohuangcode-0.3.2-py3-none-any.whl +0 -0
package/dist/events.js ADDED
@@ -0,0 +1,838 @@
1
+ /** Canonical events and the event bus used by the runtime. */
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { randomUUID } from "node:crypto";
4
+ export const EventKind = {
5
+ InputUserMessage: "input.user_message",
6
+ /** Compatibility alias for InputUserMessage. */
7
+ InputReceived: "input.user_message",
8
+ InputSlashCommand: "input.slash_command",
9
+ InputPending: "input.pending",
10
+ InputHeld: "input.held",
11
+ InputCancelRequested: "input.cancel_requested",
12
+ TaskStarted: "task.started",
13
+ TaskStateChanged: "task.state_changed",
14
+ TaskCompleted: "task.completed",
15
+ TaskFailed: "task.failed",
16
+ TaskCancelled: "task.cancelled",
17
+ ModelRequestStarted: "model.request_started",
18
+ ModelTextDelta: "model.text_delta",
19
+ ModelReasoningDelta: "model.reasoning_delta",
20
+ ModelToolCallDelta: "model.tool_call_delta",
21
+ ModelResponseValidating: "model.response_validating",
22
+ ModelResponseCommitted: "model.response_committed",
23
+ ModelResponseAborted: "model.response_aborted",
24
+ ModelRequestFailed: "model.request_failed",
25
+ ModelResponseSummary: "model.response_summary",
26
+ ModelSwitched: "model.switched",
27
+ AgentGuardTriggered: "agent.guard_triggered",
28
+ AgentGuardFailed: "agent.guard_failed",
29
+ ToolStarted: "tool.started",
30
+ ToolOutputDelta: "tool.output_delta",
31
+ ToolFinished: "tool.finished",
32
+ SessionReady: "session.ready",
33
+ SessionStopped: "session.stopped",
34
+ UiMessage: "ui.message",
35
+ RoutingDecided: "routing.decided",
36
+ /** Compatibility alias for RoutingDecided. */
37
+ RouteDecided: "routing.decided",
38
+ RoutingRejected: "routing.rejected",
39
+ };
40
+ const EVENT_KIND_VALUES = new Set(Object.values(EventKind));
41
+ export const EventSource = {
42
+ User: "user",
43
+ Cli: "cli",
44
+ Model: "model",
45
+ Tool: "tool",
46
+ Session: "session",
47
+ Router: "router",
48
+ System: "system",
49
+ };
50
+ const EVENT_SOURCE_VALUES = new Set(Object.values(EventSource));
51
+ export class EventValidationError extends Error {
52
+ constructor(message, options) {
53
+ super(message, options);
54
+ this.name = "EventValidationError";
55
+ }
56
+ }
57
+ /** Deep-copy containers into deeply frozen ones, like Python's ``_freeze``. */
58
+ function freezeValue(value) {
59
+ if (Array.isArray(value)) {
60
+ return Object.freeze(value.map((item) => freezeValue(item)));
61
+ }
62
+ if (value !== null && typeof value === "object") {
63
+ if (value instanceof Map || value instanceof Set) {
64
+ return Object.freeze(value);
65
+ }
66
+ const copy = {};
67
+ for (const [key, item] of Object.entries(value)) {
68
+ copy[key] = freezeValue(item);
69
+ }
70
+ return Object.freeze(copy);
71
+ }
72
+ return value;
73
+ }
74
+ function gapOf(payload) {
75
+ const value = payload["_projection_dropped"];
76
+ return typeof value === "number" && value > 0 ? value : 0;
77
+ }
78
+ const DEFAULT_MAX_PAYLOAD_CHARS = 1_000_000;
79
+ function payloadFieldMatches(value, expected) {
80
+ switch (expected) {
81
+ case "string":
82
+ return typeof value === "string";
83
+ case "integer":
84
+ return typeof value === "number" && Number.isInteger(value);
85
+ case "object":
86
+ return value !== null && typeof value === "object" && !Array.isArray(value);
87
+ }
88
+ }
89
+ /** Validation rules for a single event kind. */
90
+ export class EventSpec {
91
+ kind;
92
+ sources;
93
+ required_payload;
94
+ payload_types;
95
+ require_task_id;
96
+ require_correlation_id;
97
+ max_payload_chars;
98
+ validator;
99
+ constructor(options) {
100
+ this.kind = options.kind;
101
+ this.sources = options.sources ?? null;
102
+ this.required_payload = options.required_payload ?? new Set();
103
+ this.payload_types = options.payload_types ?? {};
104
+ this.require_task_id = options.require_task_id ?? false;
105
+ this.require_correlation_id = options.require_correlation_id ?? false;
106
+ this.max_payload_chars =
107
+ options.max_payload_chars ?? DEFAULT_MAX_PAYLOAD_CHARS;
108
+ this.validator = options.validator ?? null;
109
+ }
110
+ validate(event) {
111
+ if (event.kind !== this.kind) {
112
+ throw new EventValidationError(`expected event kind ${this.kind}, got ${event.kind}`);
113
+ }
114
+ if (this.sources !== null && !this.sources.has(event.source)) {
115
+ throw new EventValidationError(`source ${event.source} is not valid for ${event.kind}`);
116
+ }
117
+ if (this.require_task_id && !event.task_id) {
118
+ throw new EventValidationError(`event ${event.kind} requires task_id`);
119
+ }
120
+ if (this.require_correlation_id && !event.correlation_id) {
121
+ throw new EventValidationError(`event ${event.kind} requires correlation_id`);
122
+ }
123
+ const payload = event.payload;
124
+ const missing = [...this.required_payload]
125
+ .filter((name) => !(name in payload))
126
+ .sort();
127
+ if (missing.length > 0) {
128
+ throw new EventValidationError(`event ${event.kind} is missing payload fields: ${missing.join(", ")}`);
129
+ }
130
+ for (const [name, expected] of Object.entries(this.payload_types)) {
131
+ if (name in payload && !payloadFieldMatches(payload[name], expected)) {
132
+ throw new EventValidationError(`event ${event.kind} payload field '${name}' has invalid type`);
133
+ }
134
+ }
135
+ let payloadSize;
136
+ try {
137
+ payloadSize = JSON.stringify(payload).length;
138
+ }
139
+ catch (error) {
140
+ throw new EventValidationError(`event ${event.kind} payload must be JSON-compatible`, { cause: error });
141
+ }
142
+ if (payloadSize > this.max_payload_chars) {
143
+ throw new EventValidationError(`event ${event.kind} payload exceeds ${this.max_payload_chars} characters`);
144
+ }
145
+ if (this.validator !== null && this.validator(event) === false) {
146
+ throw new EventValidationError(`custom validation failed for ${event.kind}`);
147
+ }
148
+ }
149
+ }
150
+ function spec(kind, init) {
151
+ return new EventSpec({
152
+ kind,
153
+ sources: new Set(init.sources),
154
+ required_payload: new Set(init.required_payload),
155
+ payload_types: init.payload_types,
156
+ require_task_id: init.require_task_id,
157
+ require_correlation_id: init.require_correlation_id,
158
+ max_payload_chars: init.max_payload_chars,
159
+ });
160
+ }
161
+ function taskLifecycleSpec(kind) {
162
+ return spec(kind, { sources: [EventSource.Session], require_task_id: true });
163
+ }
164
+ function modelEventSpec(kind, delta) {
165
+ return spec(kind, {
166
+ sources: [EventSource.Model],
167
+ require_task_id: true,
168
+ require_correlation_id: true,
169
+ max_payload_chars: delta ? 16_384 : 100_000,
170
+ });
171
+ }
172
+ function toolEventSpec(kind) {
173
+ return spec(kind, {
174
+ sources: [EventSource.Tool],
175
+ require_task_id: true,
176
+ require_correlation_id: true,
177
+ max_payload_chars: 100_000,
178
+ });
179
+ }
180
+ // The default boundary prevents external/user events from impersonating model,
181
+ // tool, router, and lifecycle callbacks. Integrations may add stricter payload
182
+ // specs through EventFactory.register without changing the envelope protocol.
183
+ export const EVENT_SPECS = new Map([
184
+ spec(EventKind.InputUserMessage, {
185
+ sources: [EventSource.User, EventSource.Cli],
186
+ required_payload: ["content"],
187
+ payload_types: { content: "string", strategy: "string" },
188
+ max_payload_chars: 100_000,
189
+ }),
190
+ spec(EventKind.InputSlashCommand, {
191
+ sources: [EventSource.User, EventSource.Cli],
192
+ required_payload: ["content"],
193
+ payload_types: { content: "string" },
194
+ max_payload_chars: 16_384,
195
+ }),
196
+ spec(EventKind.InputPending, {
197
+ sources: [EventSource.Session],
198
+ required_payload: ["pending_count"],
199
+ payload_types: { pending_count: "integer" },
200
+ require_task_id: true,
201
+ require_correlation_id: true,
202
+ }),
203
+ spec(EventKind.InputHeld, {
204
+ sources: [EventSource.Session],
205
+ required_payload: ["held_count"],
206
+ payload_types: { held_count: "integer" },
207
+ require_task_id: true,
208
+ require_correlation_id: true,
209
+ }),
210
+ spec(EventKind.InputCancelRequested, {
211
+ sources: [EventSource.User, EventSource.Cli, EventSource.Session],
212
+ }),
213
+ taskLifecycleSpec(EventKind.TaskStarted),
214
+ spec(EventKind.TaskStateChanged, {
215
+ sources: [EventSource.Session],
216
+ required_payload: ["state"],
217
+ payload_types: { state: "string" },
218
+ require_task_id: true,
219
+ }),
220
+ taskLifecycleSpec(EventKind.TaskCompleted),
221
+ taskLifecycleSpec(EventKind.TaskFailed),
222
+ taskLifecycleSpec(EventKind.TaskCancelled),
223
+ spec(EventKind.SessionReady, { sources: [EventSource.Session] }),
224
+ spec(EventKind.SessionStopped, { sources: [EventSource.Session] }),
225
+ spec(EventKind.ModelRequestStarted, {
226
+ sources: [EventSource.Model],
227
+ required_payload: ["request_id"],
228
+ payload_types: { request_id: "string" },
229
+ require_task_id: true,
230
+ require_correlation_id: true,
231
+ max_payload_chars: 100_000,
232
+ }),
233
+ spec(EventKind.ModelTextDelta, {
234
+ sources: [EventSource.Model],
235
+ required_payload: ["text"],
236
+ payload_types: { text: "string", request_id: "string" },
237
+ require_task_id: true,
238
+ require_correlation_id: true,
239
+ max_payload_chars: 16_384,
240
+ }),
241
+ spec(EventKind.ModelReasoningDelta, {
242
+ sources: [EventSource.Model],
243
+ required_payload: ["text"],
244
+ payload_types: { text: "string", request_id: "string" },
245
+ require_task_id: true,
246
+ require_correlation_id: true,
247
+ max_payload_chars: 16_384,
248
+ }),
249
+ spec(EventKind.ModelToolCallDelta, {
250
+ sources: [EventSource.Model],
251
+ required_payload: ["index"],
252
+ payload_types: { index: "integer", request_id: "string" },
253
+ require_task_id: true,
254
+ require_correlation_id: true,
255
+ max_payload_chars: 16_384,
256
+ }),
257
+ modelEventSpec(EventKind.ModelResponseValidating, false),
258
+ modelEventSpec(EventKind.ModelResponseCommitted, false),
259
+ modelEventSpec(EventKind.ModelResponseAborted, false),
260
+ modelEventSpec(EventKind.ModelRequestFailed, false),
261
+ modelEventSpec(EventKind.ModelResponseSummary, false),
262
+ spec(EventKind.ModelSwitched, {
263
+ sources: [EventSource.Model, EventSource.Session, EventSource.System],
264
+ }),
265
+ spec(EventKind.AgentGuardTriggered, {
266
+ sources: [EventSource.System],
267
+ required_payload: ["reason"],
268
+ payload_types: {
269
+ reason: "string",
270
+ tool_rounds: "integer",
271
+ model_requests: "integer",
272
+ total_tokens: "integer",
273
+ elapsed_ms: "integer",
274
+ },
275
+ require_task_id: true,
276
+ max_payload_chars: 100_000,
277
+ }),
278
+ spec(EventKind.AgentGuardFailed, {
279
+ sources: [EventSource.System],
280
+ required_payload: ["reason"],
281
+ payload_types: {
282
+ reason: "string",
283
+ tool_rounds: "integer",
284
+ model_requests: "integer",
285
+ total_tokens: "integer",
286
+ elapsed_ms: "integer",
287
+ },
288
+ require_task_id: true,
289
+ max_payload_chars: 100_000,
290
+ }),
291
+ spec(EventKind.UiMessage, {
292
+ sources: [EventSource.Cli, EventSource.Session, EventSource.System],
293
+ required_payload: ["text"],
294
+ payload_types: { text: "string", style: "string" },
295
+ max_payload_chars: 100_000,
296
+ }),
297
+ spec(EventKind.ToolStarted, {
298
+ sources: [EventSource.Tool],
299
+ required_payload: ["name", "arguments"],
300
+ payload_types: { name: "string", arguments: "object" },
301
+ require_task_id: true,
302
+ require_correlation_id: true,
303
+ max_payload_chars: 100_000,
304
+ }),
305
+ spec(EventKind.ToolOutputDelta, {
306
+ sources: [EventSource.Tool],
307
+ required_payload: ["stream", "text"],
308
+ payload_types: { stream: "string", text: "string" },
309
+ require_task_id: true,
310
+ require_correlation_id: true,
311
+ max_payload_chars: 16_384,
312
+ }),
313
+ spec(EventKind.ToolFinished, {
314
+ sources: [EventSource.Tool],
315
+ required_payload: ["status"],
316
+ payload_types: { status: "string" },
317
+ require_task_id: true,
318
+ require_correlation_id: true,
319
+ max_payload_chars: 100_000,
320
+ }),
321
+ spec(EventKind.RoutingDecided, {
322
+ sources: [EventSource.Router],
323
+ required_payload: ["destination", "reason"],
324
+ payload_types: { destination: "string", reason: "string" },
325
+ require_correlation_id: true,
326
+ }),
327
+ spec(EventKind.RoutingRejected, {
328
+ sources: [EventSource.Router],
329
+ required_payload: ["reason"],
330
+ payload_types: { reason: "string" },
331
+ require_correlation_id: true,
332
+ }),
333
+ ].map((eventSpec) => [eventSpec.kind, eventSpec]));
334
+ /** Create validated envelopes without assigning publication order. */
335
+ export class EventFactory {
336
+ specs;
337
+ constructor(specs) {
338
+ this.specs = new Map(specs ?? EVENT_SPECS);
339
+ }
340
+ register(eventSpec) {
341
+ this.specs.set(eventSpec.kind, eventSpec);
342
+ }
343
+ create(kind, options) {
344
+ if (!EVENT_KIND_VALUES.has(kind) && !this.specs.has(kind)) {
345
+ throw new EventValidationError(`'${String(kind)}' is not a valid EventKind`);
346
+ }
347
+ if (!EVENT_SOURCE_VALUES.has(options.source)) {
348
+ throw new EventValidationError(`'${String(options.source)}' is not a valid EventSource`);
349
+ }
350
+ const eventKind = kind;
351
+ const eventSource = options.source;
352
+ const eventId = options.event_id ?? randomUUID().replaceAll("-", "");
353
+ const sessionId = options.session_id;
354
+ const sequence = options.sequence ?? 0;
355
+ if (!eventId) {
356
+ throw new EventValidationError("event_id must not be empty");
357
+ }
358
+ if (!sessionId) {
359
+ throw new EventValidationError("session_id must not be empty");
360
+ }
361
+ if (sequence < 0) {
362
+ throw new EventValidationError("sequence must be non-negative");
363
+ }
364
+ const envelope = {
365
+ event_id: eventId,
366
+ kind: eventKind,
367
+ source: eventSource,
368
+ session_id: sessionId,
369
+ task_id: options.task_id ?? null,
370
+ correlation_id: options.correlation_id ?? null,
371
+ sequence,
372
+ payload: freezeValue(options.payload ?? {}),
373
+ };
374
+ const eventSpec = this.specs.get(eventKind);
375
+ if (eventSpec !== undefined) {
376
+ eventSpec.validate(envelope);
377
+ }
378
+ return envelope;
379
+ }
380
+ validate(event) {
381
+ const eventSpec = this.specs.get(event.kind);
382
+ if (eventSpec !== undefined) {
383
+ eventSpec.validate(event);
384
+ }
385
+ }
386
+ }
387
+ const SENSITIVE_FIELDS = new Set([
388
+ "api_key",
389
+ "apikey",
390
+ "authorization",
391
+ "token",
392
+ "access_token",
393
+ "refresh_token",
394
+ "secret",
395
+ "password",
396
+ ]);
397
+ const SENSITIVE_TEXT_PATTERNS = [
398
+ /(authorization\s*:\s*bearer\s+)[^\s'"]+/gi,
399
+ /(\b(?:api[_-]?key|access[_-]?token|token|password|secret)\s*[:=]\s*)[^\s'"]+/gi,
400
+ ];
401
+ function projectValue(value) {
402
+ if (Array.isArray(value)) {
403
+ return value.map((item) => projectValue(item));
404
+ }
405
+ if (value !== null && typeof value === "object") {
406
+ const projected = {};
407
+ for (const [key, item] of Object.entries(value)) {
408
+ const normalized = key.toLowerCase().replaceAll("-", "_");
409
+ if (SENSITIVE_FIELDS.has(normalized) || normalized.endsWith("_secret")) {
410
+ projected[key] = "[REDACTED]";
411
+ }
412
+ else {
413
+ projected[key] = projectValue(item);
414
+ }
415
+ }
416
+ return projected;
417
+ }
418
+ if (typeof value === "string") {
419
+ let projected = value;
420
+ for (const pattern of SENSITIVE_TEXT_PATTERNS) {
421
+ projected = projected.replace(pattern, "$1[REDACTED]");
422
+ }
423
+ return projected;
424
+ }
425
+ return value;
426
+ }
427
+ /** Produce JSON-friendly, recursively redacted audience projections. */
428
+ export class EventProjector {
429
+ static AUDIENCES = new Set([
430
+ "terminal",
431
+ "web",
432
+ "log",
433
+ "router",
434
+ ]);
435
+ project(event, audience = "terminal") {
436
+ if (!EventProjector.AUDIENCES.has(audience)) {
437
+ throw new Error(`unknown event audience: ${audience}`);
438
+ }
439
+ return {
440
+ event_id: event.event_id,
441
+ kind: event.kind,
442
+ source: event.source,
443
+ session_id: event.session_id,
444
+ task_id: event.task_id,
445
+ correlation_id: event.correlation_id,
446
+ sequence: event.sequence,
447
+ payload: projectValue(event.payload),
448
+ };
449
+ }
450
+ }
451
+ const COALESCIBLE_EVENTS = new Set([
452
+ EventKind.ModelTextDelta,
453
+ EventKind.ModelReasoningDelta,
454
+ EventKind.ModelToolCallDelta,
455
+ EventKind.ToolOutputDelta,
456
+ ]);
457
+ const MAX_COALESCED_TEXT_CHARS = 65_536;
458
+ // Reserved headroom so control/lifecycle events always fit in a mailbox even
459
+ // when display-only deltas flood it.
460
+ const MAILBOX_CRITICAL_HEADROOM = 64;
461
+ function withPayload(event, payload) {
462
+ return {
463
+ ...event,
464
+ payload: freezeValue(payload),
465
+ };
466
+ }
467
+ /**
468
+ * One bounded queue per subscriber, drained by a promise loop.
469
+ *
470
+ * A slow terminal or web projection can no longer stall another subscriber.
471
+ * Under pressure only adjacent compatible deltas are combined; lifecycle and
472
+ * control events are never discarded.
473
+ */
474
+ /**
475
+ * Identifies the mailbox whose callback is currently executing, the
476
+ * single-threaded analogue of Python's ``self._worker is current_thread()``
477
+ * self-join guard in ``_SubscriberMailbox.close``.
478
+ */
479
+ const subscriberCallbackContext = new AsyncLocalStorage();
480
+ class SubscriberMailbox {
481
+ callback;
482
+ maxItems;
483
+ items = [];
484
+ unfinished = 0;
485
+ dropped = 0;
486
+ pendingGap = 0;
487
+ closed = false;
488
+ wakeWaiters = [];
489
+ flushWaiters = [];
490
+ workerDone;
491
+ constructor(callback, maxItems) {
492
+ this.callback = callback;
493
+ this.maxItems = maxItems;
494
+ this.workerDone = this.run();
495
+ }
496
+ static merge(left, right) {
497
+ if (left.kind !== right.kind ||
498
+ left.task_id !== right.task_id ||
499
+ left.correlation_id !== right.correlation_id ||
500
+ !COALESCIBLE_EVENTS.has(left.kind)) {
501
+ return null;
502
+ }
503
+ const leftText = left.payload["text"];
504
+ const rightText = right.payload["text"];
505
+ if (typeof leftText !== "string" || typeof rightText !== "string") {
506
+ return null;
507
+ }
508
+ if (leftText.length + rightText.length > MAX_COALESCED_TEXT_CHARS) {
509
+ return null;
510
+ }
511
+ const payload = { ...right.payload };
512
+ payload["text"] = leftText + rightText;
513
+ const inheritedGap = gapOf(left.payload);
514
+ if (inheritedGap > 0) {
515
+ payload["_projection_dropped"] = gapOf(right.payload) + inheritedGap;
516
+ }
517
+ payload["coalesced_from_sequence"] =
518
+ left.payload["coalesced_from_sequence"] ?? left.sequence;
519
+ return withPayload(right, payload);
520
+ }
521
+ enqueue(event) {
522
+ if (this.closed) {
523
+ return false;
524
+ }
525
+ if (this.items.length > 0) {
526
+ const last = this.items[this.items.length - 1];
527
+ const merged = SubscriberMailbox.merge(last, event);
528
+ if (merged !== null &&
529
+ this.items.length >= this.maxItems - MAILBOX_CRITICAL_HEADROOM) {
530
+ this.items[this.items.length - 1] = this.withGapMarker(merged);
531
+ return true;
532
+ }
533
+ }
534
+ if (COALESCIBLE_EVENTS.has(event.kind) &&
535
+ this.items.length >= this.maxItems - MAILBOX_CRITICAL_HEADROOM) {
536
+ this.recordDrop();
537
+ return false;
538
+ }
539
+ if (this.items.length >= this.maxItems) {
540
+ // Preserve control/lifecycle events by evicting an older display-only
541
+ // delta. If a subscriber cannot consume even the reserved critical
542
+ // capacity, detach at this projection boundary instead of blocking the
543
+ // runtime and cancellation.
544
+ let droppedIndex = this.items.findIndex((queued) => COALESCIBLE_EVENTS.has(queued.kind));
545
+ if (droppedIndex === -1) {
546
+ droppedIndex = 0;
547
+ }
548
+ const droppedEvent = this.items[droppedIndex];
549
+ this.items.splice(droppedIndex, 1);
550
+ this.unfinished -= 1;
551
+ this.recordDrop(1 + gapOf(droppedEvent.payload));
552
+ }
553
+ this.items.push(this.withGapMarker(event));
554
+ this.unfinished += 1;
555
+ this.wake();
556
+ return true;
557
+ }
558
+ recordDrop(count = 1) {
559
+ this.dropped += count;
560
+ this.pendingGap += count;
561
+ }
562
+ withGapMarker(event) {
563
+ if (this.pendingGap === 0) {
564
+ return event;
565
+ }
566
+ const payload = { ...event.payload };
567
+ payload["_projection_dropped"] = gapOf(payload) + this.pendingGap;
568
+ this.pendingGap = 0;
569
+ return withPayload(event, payload);
570
+ }
571
+ async flush(timeoutMs) {
572
+ const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
573
+ while (this.unfinished > 0) {
574
+ if (deadline === undefined) {
575
+ await new Promise((resolve) => {
576
+ this.flushWaiters.push(resolve);
577
+ });
578
+ continue;
579
+ }
580
+ const remaining = deadline - Date.now();
581
+ if (remaining <= 0) {
582
+ return false;
583
+ }
584
+ const waiter = new Promise((resolve) => {
585
+ this.flushWaiters.push(resolve);
586
+ });
587
+ let timer;
588
+ const timeout = new Promise((resolve) => {
589
+ timer = setTimeout(resolve, remaining);
590
+ });
591
+ await Promise.race([waiter, timeout]);
592
+ clearTimeout(timer);
593
+ }
594
+ return true;
595
+ }
596
+ async close() {
597
+ if (subscriberCallbackContext.getStore() === this) {
598
+ // Self-join guard: closing from inside the subscriber callback must
599
+ // not flush or await the worker — the worker is this very call stack,
600
+ // so waiting here would deadlock. Remaining items are still drained
601
+ // by the worker before it exits, as in the Python original.
602
+ this.closed = true;
603
+ this.wake();
604
+ this.notifyFlushWaiters();
605
+ return;
606
+ }
607
+ const drained = await this.flush(2000);
608
+ this.closed = true;
609
+ if (!drained) {
610
+ const queued = this.items.length;
611
+ this.items = [];
612
+ this.unfinished -= queued;
613
+ this.recordDrop(queued);
614
+ }
615
+ this.wake();
616
+ this.notifyFlushWaiters();
617
+ await this.workerDone;
618
+ }
619
+ wake() {
620
+ const waiters = this.wakeWaiters;
621
+ this.wakeWaiters = [];
622
+ for (const resolve of waiters) {
623
+ resolve();
624
+ }
625
+ }
626
+ notifyFlushWaiters() {
627
+ const waiters = this.flushWaiters;
628
+ this.flushWaiters = [];
629
+ for (const resolve of waiters) {
630
+ resolve();
631
+ }
632
+ }
633
+ async run() {
634
+ for (;;) {
635
+ while (this.items.length === 0 && !this.closed) {
636
+ await new Promise((resolve) => {
637
+ this.wakeWaiters.push(resolve);
638
+ });
639
+ }
640
+ if (this.items.length === 0 && this.closed) {
641
+ return;
642
+ }
643
+ const event = this.items.shift();
644
+ try {
645
+ await subscriberCallbackContext.run(this, () => this.callback(event));
646
+ }
647
+ catch {
648
+ // A broken projection is isolated to its own mailbox.
649
+ }
650
+ finally {
651
+ this.unfinished -= 1;
652
+ this.notifyFlushWaiters();
653
+ }
654
+ }
655
+ }
656
+ }
657
+ export class EventBufferEmptyError extends Error {
658
+ constructor() {
659
+ super("no buffered events");
660
+ this.name = "EventBufferEmptyError";
661
+ }
662
+ }
663
+ export class EventBufferTimeoutError extends Error {
664
+ constructor() {
665
+ super("timed out waiting for a buffered event");
666
+ this.name = "EventBufferTimeoutError";
667
+ }
668
+ }
669
+ /**
670
+ * Thrown by publish/subscribe and used to reject pending pull waiters once
671
+ * the bus has been closed, so consumers can detect shutdown via `instanceof`
672
+ * instead of matching the message.
673
+ */
674
+ export class EventBusClosedError extends Error {
675
+ constructor() {
676
+ super("event bus is closed");
677
+ this.name = "EventBusClosedError";
678
+ }
679
+ }
680
+ /** A FIFO bus that assigns one strict sequence across all publishers. */
681
+ export class EventBus {
682
+ factory;
683
+ maxBufferedEvents;
684
+ subscriberMailboxSize;
685
+ buffer = [];
686
+ bufferWaiters = [];
687
+ subscribers = new Map();
688
+ subscriberId = 0;
689
+ sequence = 0;
690
+ closed = false;
691
+ constructor(options = {}) {
692
+ const maxBufferedEvents = options.max_buffered_events ?? 4_096;
693
+ if (maxBufferedEvents <= 0) {
694
+ throw new RangeError("max_buffered_events must be positive");
695
+ }
696
+ const subscriberMailboxSize = options.subscriber_mailbox_size ?? 4_096;
697
+ if (subscriberMailboxSize <= MAILBOX_CRITICAL_HEADROOM) {
698
+ throw new RangeError("subscriber_mailbox_size must be greater than 64");
699
+ }
700
+ this.factory = options.factory ?? new EventFactory();
701
+ this.maxBufferedEvents = maxBufferedEvents;
702
+ this.subscriberMailboxSize = subscriberMailboxSize;
703
+ }
704
+ get last_sequence() {
705
+ return this.sequence;
706
+ }
707
+ publish(kind, options) {
708
+ const event = this.factory.create(kind, options);
709
+ return this.publishEvent(event);
710
+ }
711
+ publishEvent(event) {
712
+ this.factory.validate(event);
713
+ if (this.closed) {
714
+ throw new EventBusClosedError();
715
+ }
716
+ this.sequence += 1;
717
+ const published = { ...event, sequence: this.sequence };
718
+ if (this.buffer.length >= this.maxBufferedEvents) {
719
+ // Subscriber mailboxes retain/coalesce independently. The pull buffer
720
+ // is bounded so an unused diagnostic queue cannot grow forever during
721
+ // long Bash/model streams.
722
+ this.buffer.shift();
723
+ }
724
+ this.buffer.push(published);
725
+ this.feedBufferWaiters();
726
+ for (const mailbox of this.subscribers.values()) {
727
+ mailbox.enqueue(published);
728
+ }
729
+ return published;
730
+ }
731
+ /** Wait until every published event reached current subscribers. */
732
+ async flush() {
733
+ for (;;) {
734
+ const before = this.sequence;
735
+ for (const mailbox of [...this.subscribers.values()]) {
736
+ await mailbox.flush();
737
+ }
738
+ if (this.sequence === before) {
739
+ return;
740
+ }
741
+ }
742
+ }
743
+ /** Deliver queued events and stop subscriber workers exactly once. */
744
+ async close() {
745
+ if (this.closed) {
746
+ return;
747
+ }
748
+ this.closed = true;
749
+ const mailboxes = [...this.subscribers.values()];
750
+ this.subscribers.clear();
751
+ await Promise.all(mailboxes.map((mailbox) => mailbox.close()));
752
+ for (const waiter of this.bufferWaiters.splice(0)) {
753
+ if (waiter.timer !== undefined) {
754
+ clearTimeout(waiter.timer);
755
+ }
756
+ waiter.reject(new EventBusClosedError());
757
+ }
758
+ }
759
+ /**
760
+ * Fan out every future event and return an unsubscribe callback.
761
+ *
762
+ * The returned callback detaches the subscriber immediately and resolves
763
+ * only after the mailbox drained, so once it returns no further events
764
+ * can be delivered to that callback (matching Python's blocking
765
+ * ``unsubscribe``).
766
+ */
767
+ subscribe(callback) {
768
+ if (this.closed) {
769
+ throw new EventBusClosedError();
770
+ }
771
+ this.subscriberId += 1;
772
+ const id = this.subscriberId;
773
+ const mailbox = new SubscriberMailbox(callback, this.subscriberMailboxSize);
774
+ this.subscribers.set(id, mailbox);
775
+ return async () => {
776
+ const removed = this.subscribers.get(id);
777
+ if (removed !== undefined) {
778
+ this.subscribers.delete(id);
779
+ await removed.close();
780
+ }
781
+ };
782
+ }
783
+ /** Pull the next buffered event, waiting up to ``timeoutMs`` if given. */
784
+ get(timeoutMs) {
785
+ const buffered = this.buffer.shift();
786
+ if (buffered !== undefined) {
787
+ return Promise.resolve(buffered);
788
+ }
789
+ if (this.closed) {
790
+ return Promise.reject(new EventBusClosedError());
791
+ }
792
+ return new Promise((resolve, reject) => {
793
+ const waiter = { resolve, reject };
794
+ if (timeoutMs !== undefined) {
795
+ waiter.timer = setTimeout(() => {
796
+ const index = this.bufferWaiters.indexOf(waiter);
797
+ if (index !== -1) {
798
+ this.bufferWaiters.splice(index, 1);
799
+ }
800
+ reject(new EventBufferTimeoutError());
801
+ }, timeoutMs);
802
+ }
803
+ this.bufferWaiters.push(waiter);
804
+ });
805
+ }
806
+ getNowait() {
807
+ const buffered = this.buffer.shift();
808
+ if (buffered === undefined) {
809
+ throw new EventBufferEmptyError();
810
+ }
811
+ return buffered;
812
+ }
813
+ drain(limit) {
814
+ const events = [];
815
+ while (limit === undefined || events.length < limit) {
816
+ const buffered = this.buffer.shift();
817
+ if (buffered === undefined) {
818
+ break;
819
+ }
820
+ events.push(buffered);
821
+ }
822
+ return events;
823
+ }
824
+ qsize() {
825
+ return this.buffer.length;
826
+ }
827
+ feedBufferWaiters() {
828
+ while (this.bufferWaiters.length > 0 && this.buffer.length > 0) {
829
+ const waiter = this.bufferWaiters.shift();
830
+ const event = this.buffer.shift();
831
+ if (waiter.timer !== undefined) {
832
+ clearTimeout(waiter.timer);
833
+ }
834
+ waiter.resolve(event);
835
+ }
836
+ }
837
+ }
838
+ //# sourceMappingURL=events.js.map