@dudousxd/nestjs-agent 1.36.1 → 1.36.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { DynamicModule, Type, OnApplicationBootstrap, OnModuleInit, ConflictException, OnModuleDestroy } from '@nestjs/common';
2
- import { b as AgentModuleOptions, c as AgentModuleAsyncOptions, d as AgentProtocolAdapter, A as AgentDepsFactory, e as AgentSkillsOptions, C as ChatQueueService } from './chat-queue.service-CF2fcJDx.cjs';
3
- export { f as AGENT_TOOL_BRAND, g as AgentAttachmentsOptions, h as AgentDeps, i as AgentEngine, j as AgentMemoryOptions, a as AgentSurface, F as FunctionalTool, L as LedgerQuotaProvider, Q as QueuePlan, k as QueueSettleOutcome, l as QueuedTurn, m as QueuedTurnStarter, n as QuotaLimits, o as QuotaWindowLimits, p as defineTool, q as delegateToolName, r as provideAgentTool, s as provideAgentTools, u as utcDay } from './chat-queue.service-CF2fcJDx.cjs';
2
+ import { b as AgentModuleOptions, c as AgentModuleAsyncOptions, d as AgentProtocolAdapter, A as AgentDepsFactory, e as AgentSkillsOptions, C as ChatQueueService } from './chat-queue.service-fopUN313.cjs';
3
+ export { f as AGENT_TOOL_BRAND, g as AgentAttachmentsOptions, h as AgentDeps, i as AgentEngine, j as AgentMemoryOptions, a as AgentSurface, F as FunctionalTool, L as LedgerQuotaProvider, Q as QueuePlan, k as QueueSettleOutcome, l as QueuedTurn, m as QueuedTurnStarter, n as QuotaLimits, o as QuotaWindowLimits, p as defineTool, q as delegateToolName, r as provideAgentTool, s as provideAgentTools, u as utcDay } from './chat-queue.service-fopUN313.cjs';
4
4
  import * as _dudousxd_nestjs_agent_core from '@dudousxd/nestjs-agent-core';
5
- import { ActorResolver, AttachmentStagingStore, AgentApprovalPort, Actor, AgentStore, StagedAttachment, MessageAttachment, ModelCatalog, AgentClientConfig, ToolSpec, ToolPresentation, AgentHistoryWindow, AgentIntake, Persona, SkillContext, ToolRegistry, AgentRegistry, PromptContributor, SkillsConfig, SkillCatalogEntry, MessageFeedbackValue, MessageFeedback, ModelCatalogView, ToolCatalogEntry, MemoryConfig, MemoryDigestEntry, SkillProvider, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply, ChatQueueState, TokenStreamSink, SinkWriter, ActionProposalOutcome, ActionProposal } from '@dudousxd/nestjs-agent-core';
5
+ import { ActorResolver, AttachmentStagingStore, AgentApprovalPort, Actor, AgentStore, StagedAttachment, MessageAttachment, ModelCatalog, AgentClientConfig, ToolSpec, ToolPresentation, AgentHistoryWindow, AgentIntake, Persona, SkillContext, ToolRegistry, AgentRegistry, PromptContributor, SkillsConfig, SkillCatalogEntry, MessageFeedbackValue, MessageFeedback, ModelCatalogView, ToolCatalogEntry, MemoryConfig, MemoryDigestEntry, SkillProvider, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply, ChatQueueState, TokenStreamSink, SinkWriter, SubscribeOptions, ActionProposalOutcome, ActionProposal } from '@dudousxd/nestjs-agent-core';
6
6
  export * from '@dudousxd/nestjs-agent-core';
7
7
  export { FunctionalToolDefinition } from '@dudousxd/nestjs-agent-core';
8
8
  import { A2uiOptions } from '@dudousxd/nestjs-agent-core/a2ui';
9
9
  import { Request, Response } from 'express';
10
- import { A as AgentService, a as ActionProposalService } from './index-NF93YDF4.cjs';
11
- export { b as A2uiAdapterOptions, c as A2uiRunHandler, d as AttachmentLimits, C as ChatParams, e as ChatSendMode, f as ChatSendResult, D as DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES, g as DEFAULT_MAX_ATTACHMENT_BYTES, M as MAX_ATTACHMENTS_PER_MESSAGE, N as NO_USER_MESSAGE_CODE, Q as QueuedSend, T as ThreadDefaultAgentReader, h as ThreadModelReader, i as a2uiAdapter, j as attachmentLimits } from './index-NF93YDF4.cjs';
10
+ import { A as AgentService, a as ActionProposalService } from './index-CJdTomOT.cjs';
11
+ export { b as A2uiAdapterOptions, c as A2uiRunHandler, d as AttachmentLimits, C as ChatParams, e as ChatSendMode, f as ChatSendResult, D as DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES, g as DEFAULT_MAX_ATTACHMENT_BYTES, M as MAX_ATTACHMENTS_PER_MESSAGE, N as NO_USER_MESSAGE_CODE, Q as QueuedSend, T as ThreadDefaultAgentReader, h as ThreadModelReader, i as a2uiAdapter, j as attachmentLimits } from './index-CJdTomOT.cjs';
12
12
  import { AgentGenui } from './genui/index.cjs';
13
13
  import { StandardSchemaV1 } from '@standard-schema/spec';
14
14
  import { DiscoveryService, MetadataScanner } from '@nestjs/core';
@@ -870,7 +870,8 @@ declare const AGENT_SERVICE: unique symbol;
870
870
 
871
871
  /**
872
872
  * The default `TokenStreamSink`: buffers chunks per run in-process so a reconnecting subscriber
873
- * replays everything so far, then follows live.
873
+ * replays everything so far, then follows live. A run's buffer is dropped {@link ENDED_GRACE_MS}
874
+ * after it ends, or {@link LIVE_TTL_MS} after its last write if it never does.
874
875
  *
875
876
  * Single-replica only. Because the buffer lives in one process's memory, a resume/reconnect that
876
877
  * lands on a different replica than the one running the turn finds an empty buffer — the stream
@@ -879,10 +880,21 @@ declare const AGENT_SERVICE: unique symbol;
879
880
  */
880
881
  declare class InProcessTokenStreamSink implements TokenStreamSink {
881
882
  private readonly runs;
883
+ /** Subscribers of a run nothing has opened yet, woken by its `open()`. */
884
+ private readonly waiting;
882
885
  private buffer;
886
+ private expireAfter;
887
+ /** Forget the run, and let anyone still following it finish as if it had ended. */
888
+ private drop;
883
889
  private wake;
890
+ private finish;
891
+ /**
892
+ * The run's writer. Each call finds the run's buffer afresh, so a run that went quiet past the
893
+ * live TTL (parked on a person) and then writes again starts a new buffer its next subscriber
894
+ * follows, rather than writing into one nothing reads.
895
+ */
884
896
  open(runId: string): SinkWriter;
885
- subscribe(runId: string): AsyncIterable<Uint8Array>;
897
+ subscribe(runId: string, options?: SubscribeOptions): AsyncIterable<Uint8Array>;
886
898
  close(runId: string): void;
887
899
  }
888
900
 
package/dist/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { DynamicModule, Type, OnApplicationBootstrap, OnModuleInit, ConflictException, OnModuleDestroy } from '@nestjs/common';
2
- import { b as AgentModuleOptions, c as AgentModuleAsyncOptions, d as AgentProtocolAdapter, A as AgentDepsFactory, e as AgentSkillsOptions, C as ChatQueueService } from './chat-queue.service-CzXsKVb1.js';
3
- export { f as AGENT_TOOL_BRAND, g as AgentAttachmentsOptions, h as AgentDeps, i as AgentEngine, j as AgentMemoryOptions, a as AgentSurface, F as FunctionalTool, L as LedgerQuotaProvider, Q as QueuePlan, k as QueueSettleOutcome, l as QueuedTurn, m as QueuedTurnStarter, n as QuotaLimits, o as QuotaWindowLimits, p as defineTool, q as delegateToolName, r as provideAgentTool, s as provideAgentTools, u as utcDay } from './chat-queue.service-CzXsKVb1.js';
2
+ import { b as AgentModuleOptions, c as AgentModuleAsyncOptions, d as AgentProtocolAdapter, A as AgentDepsFactory, e as AgentSkillsOptions, C as ChatQueueService } from './chat-queue.service-YjiKVTKE.js';
3
+ export { f as AGENT_TOOL_BRAND, g as AgentAttachmentsOptions, h as AgentDeps, i as AgentEngine, j as AgentMemoryOptions, a as AgentSurface, F as FunctionalTool, L as LedgerQuotaProvider, Q as QueuePlan, k as QueueSettleOutcome, l as QueuedTurn, m as QueuedTurnStarter, n as QuotaLimits, o as QuotaWindowLimits, p as defineTool, q as delegateToolName, r as provideAgentTool, s as provideAgentTools, u as utcDay } from './chat-queue.service-YjiKVTKE.js';
4
4
  import * as _dudousxd_nestjs_agent_core from '@dudousxd/nestjs-agent-core';
5
- import { ActorResolver, AttachmentStagingStore, AgentApprovalPort, Actor, AgentStore, StagedAttachment, MessageAttachment, ModelCatalog, AgentClientConfig, ToolSpec, ToolPresentation, AgentHistoryWindow, AgentIntake, Persona, SkillContext, ToolRegistry, AgentRegistry, PromptContributor, SkillsConfig, SkillCatalogEntry, MessageFeedbackValue, MessageFeedback, ModelCatalogView, ToolCatalogEntry, MemoryConfig, MemoryDigestEntry, SkillProvider, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply, ChatQueueState, TokenStreamSink, SinkWriter, ActionProposalOutcome, ActionProposal } from '@dudousxd/nestjs-agent-core';
5
+ import { ActorResolver, AttachmentStagingStore, AgentApprovalPort, Actor, AgentStore, StagedAttachment, MessageAttachment, ModelCatalog, AgentClientConfig, ToolSpec, ToolPresentation, AgentHistoryWindow, AgentIntake, Persona, SkillContext, ToolRegistry, AgentRegistry, PromptContributor, SkillsConfig, SkillCatalogEntry, MessageFeedbackValue, MessageFeedback, ModelCatalogView, ToolCatalogEntry, MemoryConfig, MemoryDigestEntry, SkillProvider, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply, ChatQueueState, TokenStreamSink, SinkWriter, SubscribeOptions, ActionProposalOutcome, ActionProposal } from '@dudousxd/nestjs-agent-core';
6
6
  export * from '@dudousxd/nestjs-agent-core';
7
7
  export { FunctionalToolDefinition } from '@dudousxd/nestjs-agent-core';
8
8
  import { A2uiOptions } from '@dudousxd/nestjs-agent-core/a2ui';
9
9
  import { Request, Response } from 'express';
10
- import { A as AgentService, a as ActionProposalService } from './index-DpS9P7so.js';
11
- export { b as A2uiAdapterOptions, c as A2uiRunHandler, d as AttachmentLimits, C as ChatParams, e as ChatSendMode, f as ChatSendResult, D as DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES, g as DEFAULT_MAX_ATTACHMENT_BYTES, M as MAX_ATTACHMENTS_PER_MESSAGE, N as NO_USER_MESSAGE_CODE, Q as QueuedSend, T as ThreadDefaultAgentReader, h as ThreadModelReader, i as a2uiAdapter, j as attachmentLimits } from './index-DpS9P7so.js';
10
+ import { A as AgentService, a as ActionProposalService } from './index-D1xRrnCq.js';
11
+ export { b as A2uiAdapterOptions, c as A2uiRunHandler, d as AttachmentLimits, C as ChatParams, e as ChatSendMode, f as ChatSendResult, D as DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES, g as DEFAULT_MAX_ATTACHMENT_BYTES, M as MAX_ATTACHMENTS_PER_MESSAGE, N as NO_USER_MESSAGE_CODE, Q as QueuedSend, T as ThreadDefaultAgentReader, h as ThreadModelReader, i as a2uiAdapter, j as attachmentLimits } from './index-D1xRrnCq.js';
12
12
  import { AgentGenui } from './genui/index.js';
13
13
  import { StandardSchemaV1 } from '@standard-schema/spec';
14
14
  import { DiscoveryService, MetadataScanner } from '@nestjs/core';
@@ -870,7 +870,8 @@ declare const AGENT_SERVICE: unique symbol;
870
870
 
871
871
  /**
872
872
  * The default `TokenStreamSink`: buffers chunks per run in-process so a reconnecting subscriber
873
- * replays everything so far, then follows live.
873
+ * replays everything so far, then follows live. A run's buffer is dropped {@link ENDED_GRACE_MS}
874
+ * after it ends, or {@link LIVE_TTL_MS} after its last write if it never does.
874
875
  *
875
876
  * Single-replica only. Because the buffer lives in one process's memory, a resume/reconnect that
876
877
  * lands on a different replica than the one running the turn finds an empty buffer — the stream
@@ -879,10 +880,21 @@ declare const AGENT_SERVICE: unique symbol;
879
880
  */
880
881
  declare class InProcessTokenStreamSink implements TokenStreamSink {
881
882
  private readonly runs;
883
+ /** Subscribers of a run nothing has opened yet, woken by its `open()`. */
884
+ private readonly waiting;
882
885
  private buffer;
886
+ private expireAfter;
887
+ /** Forget the run, and let anyone still following it finish as if it had ended. */
888
+ private drop;
883
889
  private wake;
890
+ private finish;
891
+ /**
892
+ * The run's writer. Each call finds the run's buffer afresh, so a run that went quiet past the
893
+ * live TTL (parked on a person) and then writes again starts a new buffer its next subscriber
894
+ * follows, rather than writing into one nothing reads.
895
+ */
884
896
  open(runId: string): SinkWriter;
885
- subscribe(runId: string): AsyncIterable<Uint8Array>;
897
+ subscribe(runId: string, options?: SubscribeOptions): AsyncIterable<Uint8Array>;
886
898
  close(runId: string): void;
887
899
  }
888
900
 
package/dist/index.js CHANGED
@@ -1584,6 +1584,26 @@ var RunNotActiveException = class extends ConflictException {
1584
1584
  }
1585
1585
  };
1586
1586
 
1587
+ // src/thread-reads.ts
1588
+ function threadReads(store, threadId, separately) {
1589
+ const projecting = store;
1590
+ if (typeof projecting.threadSettings !== "function") {
1591
+ return separately;
1592
+ }
1593
+ let settings;
1594
+ const read = /* @__PURE__ */ __name(() => {
1595
+ settings ??= projecting.threadSettings?.(threadId) ?? Promise.resolve(null);
1596
+ return settings;
1597
+ }, "read");
1598
+ return {
1599
+ owner: /* @__PURE__ */ __name(async () => (await read())?.actorRef ?? null, "owner"),
1600
+ defaultAgent: /* @__PURE__ */ __name(async () => (await read())?.defaultAgent ?? null, "defaultAgent"),
1601
+ persona: /* @__PURE__ */ __name(async () => (await read())?.persona ?? null, "persona"),
1602
+ model: /* @__PURE__ */ __name(async () => (await read())?.model ?? null, "model")
1603
+ };
1604
+ }
1605
+ __name(threadReads, "threadReads");
1606
+
1587
1607
  // src/agent.service.ts
1588
1608
  function _ts_decorate5(decorators, target, key, desc) {
1589
1609
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -1847,7 +1867,7 @@ var AgentService2 = class {
1847
1867
  * else the thread's pinned one, else none. An agent the catalog locks to one model runs on it
1848
1868
  * whatever was sent or pinned, and a send naming another model is refused.
1849
1869
  */
1850
- async resolveModel(actor, agent, requested, threadId) {
1870
+ async resolveModel(actor, agent, requested, threadId, reads) {
1851
1871
  const locked = this.models !== void 0 ? (await this.models.list({
1852
1872
  actor,
1853
1873
  agent
@@ -1858,7 +1878,7 @@ var AgentService2 = class {
1858
1878
  }
1859
1879
  return locked.model;
1860
1880
  }
1861
- const model = requested ?? (threadId !== void 0 ? await this.threadModel(threadId) : null);
1881
+ const model = requested ?? (threadId !== void 0 ? await (reads?.model() ?? this.threadModel(threadId)) : null);
1862
1882
  if (model === null || model === void 0) {
1863
1883
  return void 0;
1864
1884
  }
@@ -1918,10 +1938,11 @@ var AgentService2 = class {
1918
1938
  if ("proposalDecision" in decision) return decision;
1919
1939
  }
1920
1940
  await this.assertWithinQuota(params.actor);
1921
- const target = this.deps.resolveAgent(await this.resolveAgentName(params.agentName, params.threadId));
1941
+ const reads = params.threadId !== void 0 ? this.threadReads(params.threadId) : void 0;
1942
+ const target = this.deps.resolveAgent(await this.resolveAgentName(params.agentName, params.threadId, reads));
1922
1943
  const agentName = target.agentName;
1923
- const persona = await this.resolvePersona(agentName, params.personaId ?? target.persona, params.threadId);
1924
- const model = await this.resolveModel(params.actor, agentName, params.model, params.threadId);
1944
+ const persona = await this.resolvePersona(agentName, params.personaId ?? target.persona, params.threadId, reads);
1945
+ const model = await this.resolveModel(params.actor, agentName, params.model, params.threadId, reads);
1925
1946
  const attachments = await this.resolveAttachments(params.actor, params.attachments ?? []);
1926
1947
  let threadId = params.threadId;
1927
1948
  if (threadId === void 0) {
@@ -1945,9 +1966,9 @@ var AgentService2 = class {
1945
1966
  });
1946
1967
  threadId = created.id;
1947
1968
  } else {
1948
- if (params.authorized !== true) await this.assertOwnsThread(params.actor, threadId);
1969
+ if (params.authorized !== true) await this.assertOwnsThread(params.actor, threadId, reads);
1949
1970
  if (params.personaId !== void 0) {
1950
- await this.pinThreadPersona(threadId, params.personaId);
1971
+ await this.pinThreadPersona(threadId, params.personaId, reads);
1951
1972
  }
1952
1973
  }
1953
1974
  const input = {
@@ -2273,8 +2294,8 @@ var AgentService2 = class {
2273
2294
  * {@link subscribeAs} instead: a runId is the only thing standing between a caller and another
2274
2295
  * actor's answer, tool arguments and tool results.
2275
2296
  */
2276
- subscribe(runId) {
2277
- return this.deps.forAgent().sink.subscribe(runId);
2297
+ subscribe(runId, options) {
2298
+ return this.deps.forAgent().sink.subscribe(runId, options);
2278
2299
  }
2279
2300
  /**
2280
2301
  * {@link subscribe}, gated the way {@link cancel} is: only the actor whose thread is currently
@@ -2282,9 +2303,9 @@ var AgentService2 = class {
2282
2303
  * replayed — the runner clears the thread's active stream as the run ends, and that field is what
2283
2304
  * ownership is derived from.
2284
2305
  */
2285
- async subscribeAs(actor, runId) {
2306
+ async subscribeAs(actor, runId, options) {
2286
2307
  await this.assertOwnsActiveStream(actor, runId);
2287
- return this.subscribe(runId);
2308
+ return this.subscribe(runId, options);
2288
2309
  }
2289
2310
  /**
2290
2311
  * Approve a parked action call as `actor`. Who may is the call's recorded approver's business
@@ -2491,7 +2512,7 @@ var AgentService2 = class {
2491
2512
  * The persona this send runs under — see {@link AgentDepsFactory.resolvePersona}. The thread's
2492
2513
  * pinned persona is read only when the send names none AND the agent has personas to pick from.
2493
2514
  */
2494
- async resolvePersona(agentName, requested, threadId) {
2515
+ async resolvePersona(agentName, requested, threadId, reads) {
2495
2516
  const needsThread = requested === void 0 && threadId !== void 0 && this.deps.personaCatalog(agentName).length > 0;
2496
2517
  return this.deps.resolvePersona({
2497
2518
  agentName,
@@ -2499,16 +2520,16 @@ var AgentService2 = class {
2499
2520
  requested
2500
2521
  } : {},
2501
2522
  ...needsThread ? {
2502
- threadPersona: await threadPersona(this.store, threadId)
2523
+ threadPersona: await (reads?.persona() ?? threadPersona(this.store, threadId))
2503
2524
  } : {}
2504
2525
  });
2505
2526
  }
2506
2527
  /** Pin `persona` on the thread when it is not already — a no-op on a store that cannot. */
2507
- async pinThreadPersona(threadId, persona) {
2528
+ async pinThreadPersona(threadId, persona, reads) {
2508
2529
  if (this.store.updateThread === void 0) {
2509
2530
  return;
2510
2531
  }
2511
- if (await threadPersona(this.store, threadId) !== persona) {
2532
+ if (await (reads?.persona() ?? threadPersona(this.store, threadId)) !== persona) {
2512
2533
  await this.store.updateThread(threadId, {
2513
2534
  persona
2514
2535
  });
@@ -2535,18 +2556,27 @@ var AgentService2 = class {
2535
2556
  * workflow, for a string. A store predating {@link ThreadDefaultAgentReader} still answers, via
2536
2557
  * the full read.
2537
2558
  */
2538
- async resolveAgentName(agentName, threadId) {
2559
+ async resolveAgentName(agentName, threadId, reads) {
2539
2560
  if (agentName !== void 0) {
2540
2561
  return agentName;
2541
2562
  }
2542
2563
  if (threadId !== void 0) {
2543
- const threadDefault = await this.threadDefaultAgent(threadId);
2564
+ const threadDefault = await (reads?.defaultAgent() ?? this.threadDefaultAgent(threadId));
2544
2565
  if (threadDefault !== null) {
2545
2566
  return threadDefault;
2546
2567
  }
2547
2568
  }
2548
2569
  return this.deps.defaultAgentName();
2549
2570
  }
2571
+ /** Everything a send reads off `threadId`, in one read where the store allows it. */
2572
+ threadReads(threadId) {
2573
+ return threadReads(this.store, threadId, {
2574
+ owner: /* @__PURE__ */ __name(() => this.store.ownerOfThread(threadId), "owner"),
2575
+ defaultAgent: /* @__PURE__ */ __name(() => this.threadDefaultAgent(threadId), "defaultAgent"),
2576
+ persona: /* @__PURE__ */ __name(() => threadPersona(this.store, threadId), "persona"),
2577
+ model: /* @__PURE__ */ __name(() => this.threadModel(threadId), "model")
2578
+ });
2579
+ }
2550
2580
  async threadDefaultAgent(threadId) {
2551
2581
  const projecting = this.store;
2552
2582
  if (typeof projecting.defaultAgentForThread === "function") {
@@ -2716,8 +2746,8 @@ var AgentService2 = class {
2716
2746
  * Authorization seam for thread-scoped endpoints: the caller must own the thread. A missing
2717
2747
  * thread is a 404; someone else's thread is a 403 — one actor never touches another's thread.
2718
2748
  */
2719
- async assertOwnsThread(actor, threadId) {
2720
- const owner = await this.store.ownerOfThread(threadId);
2749
+ async assertOwnsThread(actor, threadId, reads) {
2750
+ const owner = await (reads?.owner() ?? this.store.ownerOfThread(threadId));
2721
2751
  if (owner === null) {
2722
2752
  throw new NotFoundException2(`thread ${threadId} not found`);
2723
2753
  }
@@ -3157,6 +3187,18 @@ AttachmentsController = _ts_decorate8([
3157
3187
  import { AGENT_ACTOR_RESOLVER as AGENT_ACTOR_RESOLVER2, AgentStreamError, streamFailure } from "@dudousxd/nestjs-agent-core";
3158
3188
  import { validateUiCapabilities } from "@dudousxd/nestjs-agent-core/genui";
3159
3189
  import { BadRequestException as BadRequestException4, Body, Controller as Controller3, Get as Get3, Inject as Inject8, Param, Post as Post2, Query, Req as Req2, Res } from "@nestjs/common";
3190
+
3191
+ // src/controller/reader-gone.ts
3192
+ function readerGone(res) {
3193
+ const controller = new AbortController();
3194
+ res.once("close", () => controller.abort());
3195
+ return {
3196
+ signal: controller.signal
3197
+ };
3198
+ }
3199
+ __name(readerGone, "readerGone");
3200
+
3201
+ // src/controller/chat.controller.ts
3160
3202
  function _ts_decorate9(decorators, target, key, desc) {
3161
3203
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3162
3204
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -3290,7 +3332,8 @@ var ChatController = class {
3290
3332
  return;
3291
3333
  }
3292
3334
  const { runId, threadId } = result;
3293
- await this.pipe(res, runId, this.agent.subscribe(runId), threadId);
3335
+ const gone = readerGone(res);
3336
+ await this.pipe(res, runId, this.agent.subscribe(runId, gone), threadId, 0, gone.signal);
3294
3337
  }
3295
3338
  /**
3296
3339
  * Attach to a run's stream: every buffered frame, then live ones. `?after=<seq>` (or the SSE
@@ -3300,7 +3343,8 @@ var ChatController = class {
3300
3343
  async stream(req, runId, res, after) {
3301
3344
  const actor = await this.actorResolver.resolve(req);
3302
3345
  const cursor = parseCursor(after) ?? parseCursor(req.headers["last-event-id"]) ?? 0;
3303
- await this.pipe(res, runId, await this.agent.subscribeAs(actor, runId), void 0, cursor);
3346
+ const gone = readerGone(res);
3347
+ await this.pipe(res, runId, await this.agent.subscribeAs(actor, runId, gone), void 0, cursor, gone.signal);
3304
3348
  }
3305
3349
  async cancel(req, runId) {
3306
3350
  const actor = await this.actorResolver.resolve(req);
@@ -3316,8 +3360,11 @@ var ChatController = class {
3316
3360
  * and on any later `GET …/stream`, whichever replica serves it. A reconnecting client passes the
3317
3361
  * last one it saw as `?after=`; frames at or below it are read off the sink but not re-sent.
3318
3362
  * `meta`, `done` and `error` carry no id: `meta` is re-sent on every attach, the terminals once.
3363
+ *
3364
+ * A reader that disconnects aborts `gone`, which the sink honours by ending the subscription even
3365
+ * while the run is silent (parked on a person, or never writing again).
3319
3366
  */
3320
- async pipe(res, runId, events, threadId, after = 0) {
3367
+ async pipe(res, runId, events, threadId, after = 0, gone) {
3321
3368
  res.setHeader("Content-Type", "text/event-stream");
3322
3369
  res.setHeader("Cache-Control", "no-cache");
3323
3370
  res.setHeader("Connection", "keep-alive");
@@ -3346,6 +3393,9 @@ data: ${line}
3346
3393
  }, "forward");
3347
3394
  try {
3348
3395
  for await (const chunk of events) {
3396
+ if (gone?.aborted) {
3397
+ return;
3398
+ }
3349
3399
  buffer += decoder.decode(chunk, {
3350
3400
  stream: true
3351
3401
  });
@@ -3359,11 +3409,17 @@ data: ${line}
3359
3409
  newline = buffer.indexOf("\n");
3360
3410
  }
3361
3411
  }
3412
+ if (gone?.aborted) {
3413
+ return;
3414
+ }
3362
3415
  if (buffer.length > 0) {
3363
3416
  forward(buffer);
3364
3417
  }
3365
3418
  res.write("event: done\ndata: {}\n\n");
3366
3419
  } catch (error) {
3420
+ if (gone?.aborted) {
3421
+ return;
3422
+ }
3367
3423
  const payload = error instanceof AgentStreamError ? {
3368
3424
  code: error.code,
3369
3425
  message: error.message
@@ -5293,49 +5349,121 @@ SkillDiscoveryService = _ts_decorate22([
5293
5349
 
5294
5350
  // src/in-process-sink.ts
5295
5351
  import { AgentStreamError as AgentStreamError2 } from "@dudousxd/nestjs-agent-core";
5352
+ var LIVE_TTL_MS = 36e5;
5353
+ var ENDED_GRACE_MS = 6e4;
5296
5354
  var InProcessTokenStreamSink = class {
5297
5355
  static {
5298
5356
  __name(this, "InProcessTokenStreamSink");
5299
5357
  }
5300
5358
  runs = /* @__PURE__ */ new Map();
5359
+ /** Subscribers of a run nothing has opened yet, woken by its `open()`. */
5360
+ waiting = /* @__PURE__ */ new Map();
5301
5361
  buffer(runId) {
5302
5362
  let buf = this.runs.get(runId);
5303
5363
  if (buf === void 0) {
5304
5364
  buf = {
5305
5365
  chunks: [],
5306
5366
  ended: false,
5307
- notify: /* @__PURE__ */ new Set()
5367
+ notify: /* @__PURE__ */ new Set(),
5368
+ lastWriteAt: Date.now(),
5369
+ expiry: void 0
5308
5370
  };
5309
5371
  this.runs.set(runId, buf);
5372
+ this.expireAfter(runId, buf, LIVE_TTL_MS);
5373
+ const waiters = this.waiting.get(runId);
5374
+ this.waiting.delete(runId);
5375
+ for (const resolve2 of waiters ?? []) {
5376
+ resolve2();
5377
+ }
5310
5378
  }
5311
5379
  return buf;
5312
5380
  }
5381
+ expireAfter(runId, buf, ms) {
5382
+ if (buf.expiry !== void 0) {
5383
+ clearTimeout(buf.expiry);
5384
+ }
5385
+ buf.expiry = setTimeout(() => {
5386
+ buf.expiry = void 0;
5387
+ const idle = Date.now() - buf.lastWriteAt;
5388
+ if (!buf.ended && idle < LIVE_TTL_MS) {
5389
+ this.expireAfter(runId, buf, LIVE_TTL_MS - idle);
5390
+ return;
5391
+ }
5392
+ this.drop(runId, buf);
5393
+ }, ms);
5394
+ buf.expiry.unref?.();
5395
+ }
5396
+ /** Forget the run, and let anyone still following it finish as if it had ended. */
5397
+ drop(runId, buf) {
5398
+ if (buf.expiry !== void 0) {
5399
+ clearTimeout(buf.expiry);
5400
+ buf.expiry = void 0;
5401
+ }
5402
+ if (this.runs.get(runId) === buf) {
5403
+ this.runs.delete(runId);
5404
+ }
5405
+ buf.ended = true;
5406
+ this.wake(buf);
5407
+ }
5313
5408
  wake(buf) {
5314
5409
  for (const resolve2 of buf.notify) {
5315
5410
  resolve2();
5316
5411
  }
5317
5412
  buf.notify.clear();
5318
5413
  }
5414
+ finish(runId, buf) {
5415
+ buf.ended = true;
5416
+ this.wake(buf);
5417
+ this.expireAfter(runId, buf, ENDED_GRACE_MS);
5418
+ }
5419
+ /**
5420
+ * The run's writer. Each call finds the run's buffer afresh, so a run that went quiet past the
5421
+ * live TTL (parked on a person) and then writes again starts a new buffer its next subscriber
5422
+ * follows, rather than writing into one nothing reads.
5423
+ */
5319
5424
  open(runId) {
5320
- const buf = this.buffer(runId);
5425
+ this.buffer(runId);
5321
5426
  return {
5322
5427
  write: /* @__PURE__ */ __name((chunk) => {
5428
+ const buf = this.buffer(runId);
5323
5429
  buf.chunks.push(chunk);
5430
+ buf.lastWriteAt = Date.now();
5324
5431
  this.wake(buf);
5325
5432
  }, "write"),
5326
5433
  end: /* @__PURE__ */ __name(() => {
5327
- buf.ended = true;
5328
- this.wake(buf);
5434
+ this.finish(runId, this.buffer(runId));
5329
5435
  }, "end"),
5330
5436
  fail: /* @__PURE__ */ __name((error) => {
5437
+ const buf = this.buffer(runId);
5331
5438
  buf.failure = error;
5332
- buf.ended = true;
5333
- this.wake(buf);
5439
+ this.finish(runId, buf);
5334
5440
  }, "fail")
5335
5441
  };
5336
5442
  }
5337
- async *subscribe(runId) {
5338
- const buf = this.buffer(runId);
5443
+ async *subscribe(runId, options = {}) {
5444
+ const { signal } = options;
5445
+ let buf = this.runs.get(runId);
5446
+ while (buf === void 0) {
5447
+ const waiters = this.waiting.get(runId) ?? /* @__PURE__ */ new Set();
5448
+ this.waiting.set(runId, waiters);
5449
+ let resolve2 = /* @__PURE__ */ __name(() => {
5450
+ }, "resolve");
5451
+ const opened = new Promise((r) => {
5452
+ resolve2 = r;
5453
+ });
5454
+ waiters.add(resolve2);
5455
+ try {
5456
+ if (!await untilAborted(opened, signal)) {
5457
+ return;
5458
+ }
5459
+ } finally {
5460
+ waiters.delete(resolve2);
5461
+ if (waiters.size === 0 && this.waiting.get(runId) === waiters) {
5462
+ this.waiting.delete(runId);
5463
+ }
5464
+ }
5465
+ buf = this.runs.get(runId);
5466
+ }
5339
5467
  let index = 0;
5340
5468
  while (true) {
5341
5469
  while (index < buf.chunks.length) {
@@ -5351,13 +5479,56 @@ var InProcessTokenStreamSink = class {
5351
5479
  }
5352
5480
  return;
5353
5481
  }
5354
- await new Promise((resolve2) => buf.notify.add(resolve2));
5482
+ const current = buf;
5483
+ let resolve2 = /* @__PURE__ */ __name(() => {
5484
+ }, "resolve");
5485
+ const woken = new Promise((r) => {
5486
+ resolve2 = r;
5487
+ });
5488
+ current.notify.add(resolve2);
5489
+ try {
5490
+ if (!await untilAborted(woken, signal)) {
5491
+ return;
5492
+ }
5493
+ } finally {
5494
+ current.notify.delete(resolve2);
5495
+ }
5355
5496
  }
5356
5497
  }
5357
5498
  close(runId) {
5499
+ const buf = this.runs.get(runId);
5500
+ if (buf?.expiry !== void 0) {
5501
+ clearTimeout(buf.expiry);
5502
+ }
5358
5503
  this.runs.delete(runId);
5359
5504
  }
5360
5505
  };
5506
+ async function untilAborted(promise, signal) {
5507
+ if (signal === void 0) {
5508
+ await promise;
5509
+ return true;
5510
+ }
5511
+ if (signal.aborted) {
5512
+ return false;
5513
+ }
5514
+ let onAbort = /* @__PURE__ */ __name(() => {
5515
+ }, "onAbort");
5516
+ const aborted = new Promise((resolve2) => {
5517
+ onAbort = /* @__PURE__ */ __name(() => resolve2(false), "onAbort");
5518
+ signal.addEventListener("abort", onAbort, {
5519
+ once: true
5520
+ });
5521
+ });
5522
+ try {
5523
+ return await Promise.race([
5524
+ promise.then(() => true),
5525
+ aborted
5526
+ ]);
5527
+ } finally {
5528
+ signal.removeEventListener("abort", onAbort);
5529
+ }
5530
+ }
5531
+ __name(untilAborted, "untilAborted");
5361
5532
 
5362
5533
  // src/ledger-quota-provider.ts
5363
5534
  import { exhaustedWindow, quotaPeriodRange, quotaWarning } from "@dudousxd/nestjs-agent-core";
@@ -5376,13 +5547,22 @@ var LedgerQuotaProvider = class {
5376
5547
  async report(query) {
5377
5548
  const now = query.now ?? /* @__PURE__ */ new Date();
5378
5549
  const actorRef = query.actor.id;
5379
- const windows = [
5380
- await this.dayWindow(actorRef, now)
5381
- ];
5382
- if (this.store.usageBetween !== void 0) {
5383
- const range = quotaPeriodRange("month", now);
5384
- const used = await this.store.usageBetween(actorRef, range.fromDay, range.toDay);
5385
- windows.push(this.window("month", used.usedTokens, this.usd(used), range.resetsAt));
5550
+ const day = quotaPeriodRange("day", now);
5551
+ const month = quotaPeriodRange("month", now);
5552
+ const windows = [];
5553
+ if (this.store.usageWindows !== void 0) {
5554
+ const [dayUsed, monthUsed] = await this.store.usageWindows(actorRef, [
5555
+ day,
5556
+ month
5557
+ ]);
5558
+ windows.push(this.usedWindow("day", dayUsed, day.resetsAt));
5559
+ windows.push(this.usedWindow("month", monthUsed, month.resetsAt));
5560
+ } else {
5561
+ windows.push(this.usedWindow("day", await this.store.quotaToday(actorRef, day.fromDay), day.resetsAt));
5562
+ if (this.store.usageBetween !== void 0) {
5563
+ const used = await this.store.usageBetween(actorRef, month.fromDay, month.toDay);
5564
+ windows.push(this.usedWindow("month", used, month.resetsAt));
5565
+ }
5386
5566
  }
5387
5567
  const blocked = exhaustedWindow(windows);
5388
5568
  const warning = quotaWarning(windows);
@@ -5396,10 +5576,8 @@ var LedgerQuotaProvider = class {
5396
5576
  } : {}
5397
5577
  };
5398
5578
  }
5399
- async dayWindow(actorRef, now) {
5400
- const range = quotaPeriodRange("day", now);
5401
- const used = await this.store.quotaToday(actorRef, range.fromDay);
5402
- return this.window("day", used.usedTokens, this.usd(used), range.resetsAt);
5579
+ usedWindow(period, used, resetsAt) {
5580
+ return this.window(period, used?.usedTokens ?? 0, used === void 0 ? 0 : this.usd(used), resetsAt);
5403
5581
  }
5404
5582
  /** The window's USD: every priced row, or (with `countEstimatedCost: false`) reported cost only. */
5405
5583
  usd(used) {
@@ -7244,7 +7422,7 @@ var AgUiRunHandler = class {
7244
7422
  seq: 0
7245
7423
  };
7246
7424
  try {
7247
- const frames = agUiFramesFromNdjson(this.agent.subscribe(runId));
7425
+ const frames = agUiFramesFromNdjson(this.agent.subscribe(runId, readerGone(res)));
7248
7426
  for await (const event of agUiEvents(frames, {
7249
7427
  ...options,
7250
7428
  cursor
@@ -7575,7 +7753,7 @@ var A2uiRunHandler = class {
7575
7753
  `);
7576
7754
  }, "write");
7577
7755
  try {
7578
- const frames = agUiFramesFromNdjson2(this.agent.subscribe(runId));
7756
+ const frames = agUiFramesFromNdjson2(this.agent.subscribe(runId, readerGone(res)));
7579
7757
  for await (const event of agUiEvents2(frames, {
7580
7758
  threadId: options.threadId ?? runId,
7581
7759
  runId,