summaryrefslogtreecommitdiffhomepage
path: root/packages/provider-concurrency/src/provider-wrapper.ts
blob: 1e3f2c0491c27c937382dedb8b20a62a17b345da (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import type {
  ChatMessage,
  ProviderContract,
  ProviderEvent,
  ProviderStreamOptions,
  ToolContract,
} from "@dispatch/kernel";
import type { ConcurrencyLimiter } from "./concurrency-manager.js";

/**
 * Wrap a provider's `stream` method with concurrency limiting.
 *
 * A slot is acquired BEFORE the first event is yielded (before the HTTP
 * request is sent — the `await limiter.acquire()` runs before the generator
 * body starts iterating the inner stream). The slot is released in a `finally`
 * block AFTER the inner stream completes (the full response stream, not just
 * HTTP headers — matching the Umans concurrency model where a slot is held
 * only while tokens are actually generating).
 *
 * 429 detection: if the provider yields an `error` event with `code: "429"`,
 * the limiter is notified so it can pause the queue for that provider.
 *
 * @param provider         The underlying provider to wrap.
 * @param limiter          The concurrency limiter (acquire/release/reportRateLimit).
 * @param conversationId   The agent requesting the stream (for slot attribution).
 * @param workspaceId      The workspace the agent belongs to (for starred
 *                         priority scheduling in the limiter queue).
 * @param promptStartedAt  When the agent's current prompt (turn) started
 *                         (epoch-ms, for oldest-agent-first scheduling).
 * @param onQueued          Called synchronously when `acquire()` decides to
 *                         queue the request (cannot grant immediately).
 *                         Lets the caller emit a "queued" status signal.
 * @param onAcquired       Called when `acquire()` resolves (slot granted,
 *                         whether immediately or after queueing). Lets the
 *                         caller emit an "active" status signal.
 */
export function wrapProviderWithConcurrency(
  provider: ProviderContract,
  limiter: ConcurrencyLimiter,
  conversationId: string,
  workspaceId: string,
  promptStartedAt: number,
  onQueued?: () => void,
  onAcquired?: () => void,
): ProviderContract {
  const innerStream = provider.stream;
  const providerId = provider.id;

  return {
    id: provider.id,
    stream: async function* (
      messages: readonly ChatMessage[],
      tools: readonly ToolContract[],
      opts?: ProviderStreamOptions,
    ): AsyncIterable<ProviderEvent> {
      const release = await limiter.acquire(
        providerId,
        conversationId,
        workspaceId,
        promptStartedAt,
        onQueued,
      );
      onAcquired?.();
      try {
        for await (const event of innerStream(messages, tools, opts)) {
          if (event.type === "error" && event.code === "429") {
            limiter.reportRateLimit(providerId);
          }
          yield event;
        }
      } finally {
        release();
      }
    },
    ...(provider.listModels !== undefined ? { listModels: provider.listModels } : {}),
  };
}