@intx/agent 0.1.2 → 0.3.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 (60) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +80 -5
  3. package/dist/agent.d.ts +116 -0
  4. package/dist/agent.js +682 -0
  5. package/dist/canonicalize.d.ts +15 -0
  6. package/dist/canonicalize.js +160 -0
  7. package/dist/default-director.d.ts +24 -0
  8. package/dist/default-director.js +45 -0
  9. package/dist/definition.d.ts +139 -0
  10. package/dist/definition.js +40 -0
  11. package/dist/director-registry.d.ts +47 -0
  12. package/dist/director-registry.js +87 -0
  13. package/dist/director-types.d.ts +80 -0
  14. package/dist/director-types.js +13 -0
  15. package/dist/director.d.ts +70 -0
  16. package/dist/director.js +131 -0
  17. package/dist/env-validation.d.ts +59 -0
  18. package/dist/env-validation.js +180 -0
  19. package/dist/env.d.ts +160 -0
  20. package/dist/env.js +53 -0
  21. package/dist/index.d.ts +16 -0
  22. package/dist/index.js +23 -0
  23. package/dist/internal-fixtures/mail.d.ts +39 -0
  24. package/dist/internal-fixtures/mail.js +86 -0
  25. package/dist/internal-fixtures/planner.d.ts +19 -0
  26. package/dist/internal-fixtures/planner.js +49 -0
  27. package/dist/lock.d.ts +16 -0
  28. package/dist/lock.js +47 -0
  29. package/dist/namespace.d.ts +12 -0
  30. package/dist/namespace.js +39 -0
  31. package/dist/send-queue.d.ts +25 -0
  32. package/dist/send-queue.js +147 -0
  33. package/dist/source.d.ts +43 -0
  34. package/dist/source.js +118 -0
  35. package/dist/stream.d.ts +16 -0
  36. package/dist/stream.js +115 -0
  37. package/dist/testing/audit-noop.d.ts +7 -0
  38. package/dist/testing/audit-noop.js +25 -0
  39. package/dist/testing/authorize-allow.d.ts +8 -0
  40. package/dist/testing/authorize-allow.js +19 -0
  41. package/dist/testing/index.d.ts +2 -0
  42. package/dist/testing/index.js +17 -0
  43. package/dist/tool.d.ts +238 -0
  44. package/dist/tool.js +244 -0
  45. package/package.json +26 -7
  46. package/src/agent.test.ts +0 -46
  47. package/src/agent.ts +0 -494
  48. package/src/index.ts +0 -38
  49. package/src/lock.test.ts +0 -93
  50. package/src/lock.ts +0 -57
  51. package/src/send-queue.test.ts +0 -207
  52. package/src/send-queue.ts +0 -200
  53. package/src/source.test.ts +0 -171
  54. package/src/source.ts +0 -93
  55. package/src/stream.test.ts +0 -167
  56. package/src/stream.ts +0 -142
  57. package/src/tool.test.ts +0 -217
  58. package/src/tool.ts +0 -148
  59. package/tsconfig.json +0 -4
  60. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,16 @@
1
+ export { AgentContextLockError } from "./lock.js";
2
+ export { type AgentTool, type AgentToolRunner, type AnnotatedPluginFactory, type AnnotatedPluginMeta, type AnnotatedToolFactory, type PluginFactory, type StringToolHandler, type ToolBundle, type ToolDeclaration, type ToolFactory, type ToolFactoryMeta, type ToolHandler, DuplicateToolError, PLUGIN_MARKER, TOOL_PLUGIN_KIND, type ToolPluginKind, createToolRunner, defineTool, definePlugin, fromToolRunner, isAnnotatedPluginFactory, isToolPluginInstance, stringTool, tool, toolApprovalEffect, } from "./tool.js";
3
+ export { type AuthorizeFn, type BaseEnv, type Dependencies, AgentEnvError, } from "./env.js";
4
+ export { type AnnotatedDirectorFactory, type DirectorAgentContext, type DirectorConfigSchema, type DirectorFactory, type DirectorFactoryMeta, type DirectorRef, type DirectorRegistry, } from "./director-types.js";
5
+ export { validateNamespacedId } from "./namespace.js";
6
+ export { CanonicalizationError, canonicalizeForHash } from "./canonicalize.js";
7
+ export { type DefinedDirector, defineDirector, isAnnotatedDirectorFactory, } from "./director.js";
8
+ export { createDefaultDirectorRegistry, createDirectorRegistry, createWorkflowDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
9
+ export { type DefaultDirectorConfig, buildDefaultDirectorRef, defaultDirectorFactory, } from "./default-director.js";
10
+ export { type SourceRegistry, InvalidInferenceSourceError, SourceNotFoundError, createSourceRegistry, } from "./source.js";
11
+ export { type Agent, type SendOptions, type SendResult, AgentClosedError, GateSuspendedWithoutCorrelationError, createAgent, } from "./agent.js";
12
+ export { type AgentDefinition, type DefineAgentConfig, type EnvRequiredByAll, type InferencePreference, defineAgent, } from "./definition.js";
13
+ export { effectiveDirectorRef, getRequiredEnvKeys, validateEnv, } from "./env-validation.js";
14
+ export type { RequiredEnvKeys } from "./env-validation.js";
15
+ export { SendQueueFullError } from "./send-queue.js";
16
+ export { StreamBackpressureError } from "./stream.js";
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ // @intx/agent — in-process agent runtime.
2
+ //
3
+ // Sits on top of `createReactorAssembly` from `@intx/inference` to
4
+ // provide a code-driven agent surface: send a message, stream events,
5
+ // project history, hot-swap inference sources. Peer to `@intx/harness`;
6
+ // the harness drives the reactor from a mail transport (INBOX watch,
7
+ // connector threads, outbound replies via MessageTransport) while the
8
+ // agent drives it from in-process calls.
9
+ export { AgentContextLockError } from "./lock.js";
10
+ export { DuplicateToolError, PLUGIN_MARKER, TOOL_PLUGIN_KIND, createToolRunner, defineTool, definePlugin, fromToolRunner, isAnnotatedPluginFactory, isToolPluginInstance, stringTool, tool, toolApprovalEffect, } from "./tool.js";
11
+ export { AgentEnvError, } from "./env.js";
12
+ export {} from "./director-types.js";
13
+ export { validateNamespacedId } from "./namespace.js";
14
+ export { CanonicalizationError, canonicalizeForHash } from "./canonicalize.js";
15
+ export { defineDirector, isAnnotatedDirectorFactory, } from "./director.js";
16
+ export { createDefaultDirectorRegistry, createDirectorRegistry, createWorkflowDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
17
+ export { buildDefaultDirectorRef, defaultDirectorFactory, } from "./default-director.js";
18
+ export { InvalidInferenceSourceError, SourceNotFoundError, createSourceRegistry, } from "./source.js";
19
+ export { AgentClosedError, GateSuspendedWithoutCorrelationError, createAgent, } from "./agent.js";
20
+ export { defineAgent, } from "./definition.js";
21
+ export { effectiveDirectorRef, getRequiredEnvKeys, validateEnv, } from "./env-validation.js";
22
+ export { SendQueueFullError } from "./send-queue.js";
23
+ export { StreamBackpressureError } from "./stream.js";
@@ -0,0 +1,39 @@
1
+ import type { ContextStore, InferenceSource } from "@intx/types/runtime";
2
+ import { type AgentDefinition } from "../definition.js";
3
+ import { type BaseEnv } from "../env.js";
4
+ export declare const MAIL_SOURCE: InferenceSource;
5
+ export declare const MAIL_ADDRESS = "support@fixture.local";
6
+ /**
7
+ * Env extension declared by the fixture's mail tool. Production code
8
+ * uses the `MailEnv` from `@intx/harness`; the fixture redeclares the
9
+ * shape locally to avoid the cross-package import.
10
+ */
11
+ export interface FixtureMailEnv extends BaseEnv {
12
+ transport: unknown;
13
+ address: string;
14
+ }
15
+ /**
16
+ * A no-op mail tool factory declaring `requires: ["transport",
17
+ * "address"]`. The bundle's definitions are empty -- the fixture's
18
+ * tests do not invoke any tool; they verify env validation behaviour.
19
+ */
20
+ export declare const fixtureMailFactory: import("../index.js").AnnotatedToolFactory<FixtureMailEnv>;
21
+ export declare const mailAgentDefinition: AgentDefinition<FixtureMailEnv>;
22
+ /**
23
+ * Build a bare `BaseEnv` lacking `transport` and `address`. The mail
24
+ * factory's `requires` makes this env short; `validateEnv` throws
25
+ * `AgentEnvError` blaming the factory.
26
+ */
27
+ export declare function createBareMailEnv(opts: {
28
+ storage: ContextStore;
29
+ workdir: string;
30
+ }): BaseEnv;
31
+ /**
32
+ * Build a transport-bearing `FixtureMailEnv` for the success path.
33
+ * `transport` is opaque to the fixture (the mail factory's run is a
34
+ * no-op) so the test does not need to supply a real transport.
35
+ */
36
+ export declare function createTransportMailEnv(opts: {
37
+ storage: ContextStore;
38
+ workdir: string;
39
+ }): FixtureMailEnv;
@@ -0,0 +1,86 @@
1
+ // Mail-participating agent fixture for the reactor-once tests.
2
+ //
3
+ // The fixture's definition has a tool factory that declares
4
+ // `requires: ["transport", "address"]`. A bare `BaseEnv` is short on
5
+ // those keys; instantiation must fail with `AgentEnvError`. A
6
+ // transport-bearing env satisfies the requirement and instantiation
7
+ // succeeds; the reactor-once assertion lives in `mail.test.ts`.
8
+ //
9
+ // The fixture does not import `@intx/harness` -- the composition
10
+ // layer's reactor-once invariant is exercised by the harness's own
11
+ // tests against its own surface; cross-importing harness here would
12
+ // introduce a `@intx/agent <-> @intx/harness` cycle.
13
+ import { defineAgent } from "../definition.js";
14
+ import { createDefaultDirectorRegistry } from "../director-registry.js";
15
+ import {} from "../env.js";
16
+ import { noopAuditStore } from "../testing/audit-noop.js";
17
+ import { permissiveAuthorize } from "../testing/authorize-allow.js";
18
+ import { defineTool } from "../tool.js";
19
+ export const MAIL_SOURCE = {
20
+ id: "anthropic:claude-opus-4-6",
21
+ provider: "anthropic",
22
+ baseURL: "https://api.anthropic.com",
23
+ apiKey: "sk-test-mail",
24
+ model: "claude-opus-4-6",
25
+ };
26
+ export const MAIL_ADDRESS = "support@fixture.local";
27
+ /**
28
+ * A no-op mail tool factory declaring `requires: ["transport",
29
+ * "address"]`. The bundle's definitions are empty -- the fixture's
30
+ * tests do not invoke any tool; they verify env validation behaviour.
31
+ */
32
+ export const fixtureMailFactory = defineTool({
33
+ id: "@intx-fixtures/mail/bundle",
34
+ requires: ["transport", "address"],
35
+ definitions: [],
36
+ factory: () => ({
37
+ definitions: [],
38
+ async run(call) {
39
+ return { callId: call.id, content: "" };
40
+ },
41
+ }),
42
+ });
43
+ export const mailAgentDefinition = defineAgent({
44
+ id: "mail-fixture",
45
+ description: "Mail-participating fixture agent",
46
+ systemPrompt: "You handle mail.",
47
+ tools: [fixtureMailFactory],
48
+ capabilities: [],
49
+ inference: {
50
+ sources: [{ provider: MAIL_SOURCE.provider, model: MAIL_SOURCE.model }],
51
+ },
52
+ });
53
+ /**
54
+ * Build a bare `BaseEnv` lacking `transport` and `address`. The mail
55
+ * factory's `requires` makes this env short; `validateEnv` throws
56
+ * `AgentEnvError` blaming the factory.
57
+ */
58
+ export function createBareMailEnv(opts) {
59
+ return {
60
+ sources: [MAIL_SOURCE],
61
+ defaultSource: MAIL_SOURCE.id,
62
+ storage: opts.storage,
63
+ workdir: opts.workdir,
64
+ audit: noopAuditStore(),
65
+ authorize: permissiveAuthorize(),
66
+ directors: createDefaultDirectorRegistry(),
67
+ };
68
+ }
69
+ /**
70
+ * Build a transport-bearing `FixtureMailEnv` for the success path.
71
+ * `transport` is opaque to the fixture (the mail factory's run is a
72
+ * no-op) so the test does not need to supply a real transport.
73
+ */
74
+ export function createTransportMailEnv(opts) {
75
+ return {
76
+ sources: [MAIL_SOURCE],
77
+ defaultSource: MAIL_SOURCE.id,
78
+ storage: opts.storage,
79
+ workdir: opts.workdir,
80
+ audit: noopAuditStore(),
81
+ authorize: permissiveAuthorize(),
82
+ directors: createDefaultDirectorRegistry(),
83
+ transport: { kind: "fake-transport" },
84
+ address: MAIL_ADDRESS,
85
+ };
86
+ }
@@ -0,0 +1,19 @@
1
+ import type { ContextStore, InferenceSource } from "@intx/types/runtime";
2
+ import { type AgentDefinition } from "../definition.js";
3
+ import type { BaseEnv } from "../env.js";
4
+ export declare const PLANNER_SOURCE: InferenceSource;
5
+ /**
6
+ * Planner-shape definition: no tool factories, capabilities, or
7
+ * director ref. The default director from the registry handles the
8
+ * loop; the bare env covers every required `BaseEnv` key.
9
+ */
10
+ export declare const plannerAgentDefinition: AgentDefinition<BaseEnv>;
11
+ /**
12
+ * Build a bare `BaseEnv` for the planner fixture. The caller supplies
13
+ * the storage and workdir; everything else is filled from
14
+ * `@intx/agent/testing` no-ops.
15
+ */
16
+ export declare function createPlannerEnv(opts: {
17
+ storage: ContextStore;
18
+ workdir: string;
19
+ }): BaseEnv;
@@ -0,0 +1,49 @@
1
+ // Planner-shape agent fixture for the reactor-once tests.
2
+ //
3
+ // A pure in-process agent: no transport, no connector, no mail. Used by
4
+ // `planner.test.ts` to assert that `createAgent(def, env)` wraps the
5
+ // reactor exactly once per instantiation against a bare `BaseEnv`.
6
+ import { defineAgent } from "../definition.js";
7
+ import { createDefaultDirectorRegistry } from "../director-registry.js";
8
+ import { noopAuditStore } from "../testing/audit-noop.js";
9
+ import { permissiveAuthorize } from "../testing/authorize-allow.js";
10
+ export const PLANNER_SOURCE = {
11
+ id: "anthropic:claude-opus-4-6",
12
+ provider: "anthropic",
13
+ baseURL: "https://api.anthropic.com",
14
+ apiKey: "sk-test-planner",
15
+ model: "claude-opus-4-6",
16
+ };
17
+ /**
18
+ * Planner-shape definition: no tool factories, capabilities, or
19
+ * director ref. The default director from the registry handles the
20
+ * loop; the bare env covers every required `BaseEnv` key.
21
+ */
22
+ export const plannerAgentDefinition = defineAgent({
23
+ id: "planner",
24
+ description: "Decomposes goals into a plan",
25
+ systemPrompt: "You are the planner.",
26
+ tools: [],
27
+ capabilities: [],
28
+ inference: {
29
+ sources: [
30
+ { provider: PLANNER_SOURCE.provider, model: PLANNER_SOURCE.model },
31
+ ],
32
+ },
33
+ });
34
+ /**
35
+ * Build a bare `BaseEnv` for the planner fixture. The caller supplies
36
+ * the storage and workdir; everything else is filled from
37
+ * `@intx/agent/testing` no-ops.
38
+ */
39
+ export function createPlannerEnv(opts) {
40
+ return {
41
+ sources: [PLANNER_SOURCE],
42
+ defaultSource: PLANNER_SOURCE.id,
43
+ storage: opts.storage,
44
+ workdir: opts.workdir,
45
+ audit: noopAuditStore(),
46
+ authorize: permissiveAuthorize(),
47
+ directors: createDefaultDirectorRegistry(),
48
+ };
49
+ }
package/dist/lock.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export declare class AgentContextLockError extends Error {
2
+ readonly workdir: string;
3
+ constructor(workdir: string);
4
+ }
5
+ export type ContextDirLock = {
6
+ /** Absolute, resolved path of the locked directory. */
7
+ readonly path: string;
8
+ /** Release the lock. Idempotent. */
9
+ release(): void;
10
+ };
11
+ /**
12
+ * Acquire the process-wide lock for `workdir`. Throws
13
+ * `AgentContextLockError` if another agent already holds it. The
14
+ * returned `release` is idempotent.
15
+ */
16
+ export declare function acquireContextDirLock(workdir: string): ContextDirLock;
package/dist/lock.js ADDED
@@ -0,0 +1,47 @@
1
+ // Process-wide registry of held workdir locks.
2
+ //
3
+ // The agent enforces a runtime singleton-per-workdir invariant: at most one
4
+ // in-process agent may own a given workdir at a time. Holding two agents
5
+ // against the same workdir simultaneously corrupts both the git state of
6
+ // any isogit-backed `ContextStore` rooted there and the audit collector's
7
+ // bookkeeping.
8
+ //
9
+ // This is a best-effort in-process check. It does not coordinate across OS
10
+ // processes, and it compares lexically-resolved absolute paths — two paths
11
+ // that point to the same directory through symlinks or `..`/`/./` segments
12
+ // are normalized by `path.resolve`, but a hard link or a separately mounted
13
+ // bind to the same inode will not be detected. Callers are responsible for
14
+ // ensuring `env.workdir` matches the directory backing their `env.storage`
15
+ // (see `BaseEnv.workdir` for the documented invariant).
16
+ import { resolve } from "node:path";
17
+ const heldLocks = new Set();
18
+ export class AgentContextLockError extends Error {
19
+ workdir;
20
+ constructor(workdir) {
21
+ super(`an agent is already open for workdir: ${workdir}`);
22
+ this.name = "AgentContextLockError";
23
+ this.workdir = workdir;
24
+ }
25
+ }
26
+ /**
27
+ * Acquire the process-wide lock for `workdir`. Throws
28
+ * `AgentContextLockError` if another agent already holds it. The
29
+ * returned `release` is idempotent.
30
+ */
31
+ export function acquireContextDirLock(workdir) {
32
+ const path = resolve(workdir);
33
+ if (heldLocks.has(path)) {
34
+ throw new AgentContextLockError(path);
35
+ }
36
+ heldLocks.add(path);
37
+ let released = false;
38
+ return {
39
+ path,
40
+ release() {
41
+ if (released)
42
+ return;
43
+ released = true;
44
+ heldLocks.delete(path);
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Validate a package-namespaced id. Throws with a precise diagnostic
3
+ * when the id is not in one of the two supported shapes.
4
+ *
5
+ * "@intx/agent/default" -> ok (scoped)
6
+ * "@my-org/my-workflow/special" -> ok (scoped)
7
+ * "lodash-style/director-name" -> ok (unscoped)
8
+ * "default" -> rejected (no package portion)
9
+ * "@intx/agent" -> rejected (missing name segment)
10
+ * "@intx/agent/" -> rejected (empty name segment)
11
+ */
12
+ export declare function validateNamespacedId(id: string): void;
@@ -0,0 +1,39 @@
1
+ // Package-namespaced id validation for tool and director factories.
2
+ //
3
+ // Ids must be either scoped ("@scope/pkg/name") or unscoped
4
+ // ("pkg/name"). The package portion is the identity anchor; the trailing
5
+ // segment names the tool or director within that package. Bare ids
6
+ // without a package portion are rejected at definition time so two
7
+ // independently-authored bundles cannot accidentally collide on an
8
+ // otherwise plausible name like "default".
9
+ // Per-segment character set. Mirrors what npm and most package-manager
10
+ // ecosystems accept inside an id segment: alphanumerics, dot, hyphen,
11
+ // underscore. Whitespace (space, tab, newline) and other punctuation
12
+ // are excluded so an id never carries characters that would render
13
+ // strangely in error messages, break log-line parsing, or trip
14
+ // downstream tooling that splits on whitespace.
15
+ const SEGMENT = "[A-Za-z0-9._-]+";
16
+ // Scoped: "@scope/pkg/name". Three slash-separated segments; the first
17
+ // starts with "@" followed by the segment character set. Each segment
18
+ // must be non-empty.
19
+ const SCOPED = new RegExp(`^@${SEGMENT}\\/${SEGMENT}\\/${SEGMENT}$`);
20
+ // Unscoped: "pkg/name". Two slash-separated segments. The package
21
+ // segment cannot start with "@" -- that route is the scoped form.
22
+ const UNSCOPED = new RegExp(`^${SEGMENT}\\/${SEGMENT}$`);
23
+ /**
24
+ * Validate a package-namespaced id. Throws with a precise diagnostic
25
+ * when the id is not in one of the two supported shapes.
26
+ *
27
+ * "@intx/agent/default" -> ok (scoped)
28
+ * "@my-org/my-workflow/special" -> ok (scoped)
29
+ * "lodash-style/director-name" -> ok (unscoped)
30
+ * "default" -> rejected (no package portion)
31
+ * "@intx/agent" -> rejected (missing name segment)
32
+ * "@intx/agent/" -> rejected (empty name segment)
33
+ */
34
+ export function validateNamespacedId(id) {
35
+ if (!SCOPED.test(id) && !UNSCOPED.test(id)) {
36
+ throw new Error(`id must be package-namespaced ` +
37
+ `(e.g. "@vendor/pkg/name" or "pkg/name"); got ${JSON.stringify(id)}`);
38
+ }
39
+ }
@@ -0,0 +1,25 @@
1
+ export declare class SendQueueFullError extends Error {
2
+ readonly maxDepth: number;
3
+ constructor(maxDepth: number);
4
+ }
5
+ export type SendQueueOptions<T> = {
6
+ maxDepth: number;
7
+ /**
8
+ * Called when a job moves from pending to active. The consumer drives
9
+ * the underlying work and must eventually call `resolveActive` or
10
+ * `rejectActive` exactly once.
11
+ */
12
+ start: (item: T) => void;
13
+ };
14
+ export type SendQueue<T, R> = {
15
+ enqueue(item: T, signal?: AbortSignal): Promise<R>;
16
+ /** Mark the active job complete with success and pump the next. */
17
+ resolveActive(value: R): void;
18
+ /** Mark the active job complete with failure and pump the next. */
19
+ rejectActive(reason: unknown): void;
20
+ /** Reject the active job (if any) and every pending job with `reason`. */
21
+ drain(reason: unknown): void;
22
+ /** Current pending count (queued + active). */
23
+ readonly depth: number;
24
+ };
25
+ export declare function createSendQueue<T, R>(opts: SendQueueOptions<T>): SendQueue<T, R>;
@@ -0,0 +1,147 @@
1
+ // FIFO queue for serializing send() calls against a single reactor.
2
+ //
3
+ // The agent processes one reactor cycle at a time, so concurrent send()
4
+ // callers are queued. Each queued item carries the caller's resolve/reject
5
+ // hooks and an optional AbortSignal:
6
+ //
7
+ // - If the signal is already aborted when enqueue() is called the queue
8
+ // rejects synchronously without enqueueing.
9
+ // - If the signal fires while the item is still queued the item is
10
+ // removed and rejected.
11
+ // - If the signal fires while the item is active the caller-facing
12
+ // promise rejects immediately, but the reactor cycle continues in the
13
+ // background. The queue does not start the next item until the consumer
14
+ // reports the cycle done via resolveActive/rejectActive. This keeps the
15
+ // queue ordered against actual reactor cycles — two send() promises
16
+ // cannot interleave at the reactor level.
17
+ //
18
+ // Queue depth (active + pending) is bounded by `maxDepth`; exceeding it
19
+ // throws `SendQueueFullError` synchronously from enqueue() so a buggy
20
+ // caller flooding sends fails loud instead of silently buffering.
21
+ export class SendQueueFullError extends Error {
22
+ maxDepth;
23
+ constructor(maxDepth) {
24
+ super(`send queue is full (max depth ${String(maxDepth)})`);
25
+ this.name = "SendQueueFullError";
26
+ this.maxDepth = maxDepth;
27
+ }
28
+ }
29
+ function abortReason(signal) {
30
+ return signal.reason ?? new DOMException("aborted", "AbortError");
31
+ }
32
+ export function createSendQueue(opts) {
33
+ const pending = [];
34
+ let active = null;
35
+ function settle(job, kind, value) {
36
+ if (job.settled)
37
+ return;
38
+ job.settled = true;
39
+ if (job.abortHandler !== undefined && job.signal !== undefined) {
40
+ job.signal.removeEventListener("abort", job.abortHandler);
41
+ }
42
+ if (kind === "resolve") {
43
+ // The queue's value type is checked at enqueue / resolveActive; the
44
+ // generic narrowing here is safe by construction.
45
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic resolve value
46
+ job.resolve(value);
47
+ }
48
+ else {
49
+ job.reject(value);
50
+ }
51
+ }
52
+ function pump() {
53
+ while (active === null && pending.length > 0) {
54
+ const next = pending.shift();
55
+ if (next === undefined)
56
+ return;
57
+ if (next.signal?.aborted === true) {
58
+ settle(next, "reject", abortReason(next.signal));
59
+ continue;
60
+ }
61
+ active = next;
62
+ opts.start(next.item);
63
+ return;
64
+ }
65
+ }
66
+ function enqueue(item, signal) {
67
+ if (signal?.aborted === true) {
68
+ return Promise.reject(abortReason(signal));
69
+ }
70
+ const depth = pending.length + (active !== null ? 1 : 0);
71
+ if (depth >= opts.maxDepth) {
72
+ throw new SendQueueFullError(opts.maxDepth);
73
+ }
74
+ let resolve;
75
+ let reject;
76
+ const promise = new Promise((res, rej) => {
77
+ resolve = res;
78
+ reject = rej;
79
+ });
80
+ const job = {
81
+ item,
82
+ ...(signal !== undefined ? { signal } : {}),
83
+ resolve,
84
+ reject,
85
+ settled: false,
86
+ };
87
+ if (signal !== undefined) {
88
+ const handler = () => {
89
+ const reason = abortReason(signal);
90
+ if (active === job) {
91
+ // In flight: settle the caller now; the consumer will eventually
92
+ // call resolveActive/rejectActive which becomes a no-op and
93
+ // advances the queue.
94
+ settle(job, "reject", reason);
95
+ }
96
+ else {
97
+ const idx = pending.indexOf(job);
98
+ if (idx >= 0)
99
+ pending.splice(idx, 1);
100
+ settle(job, "reject", reason);
101
+ }
102
+ };
103
+ signal.addEventListener("abort", handler, { once: true });
104
+ job.abortHandler = handler;
105
+ }
106
+ pending.push(job);
107
+ pump();
108
+ return promise;
109
+ }
110
+ function resolveActive(value) {
111
+ if (active === null)
112
+ return;
113
+ const job = active;
114
+ active = null;
115
+ settle(job, "resolve", value);
116
+ pump();
117
+ }
118
+ function rejectActive(reason) {
119
+ if (active === null)
120
+ return;
121
+ const job = active;
122
+ active = null;
123
+ settle(job, "reject", reason);
124
+ pump();
125
+ }
126
+ function drain(reason) {
127
+ const drained = [];
128
+ if (active !== null) {
129
+ drained.push(active);
130
+ active = null;
131
+ }
132
+ drained.push(...pending);
133
+ pending.length = 0;
134
+ for (const job of drained) {
135
+ settle(job, "reject", reason);
136
+ }
137
+ }
138
+ return {
139
+ enqueue,
140
+ resolveActive,
141
+ rejectActive,
142
+ drain,
143
+ get depth() {
144
+ return pending.length + (active !== null ? 1 : 0);
145
+ },
146
+ };
147
+ }
@@ -0,0 +1,43 @@
1
+ import { type InferenceSource } from "@intx/types/runtime";
2
+ export declare class InvalidInferenceSourceError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare class SourceNotFoundError extends Error {
6
+ readonly id: string;
7
+ constructor(id: string);
8
+ }
9
+ export type SourceRegistry = {
10
+ /**
11
+ * The mutable active source. The same object reference is held by the
12
+ * reactor; mutating it (through `setSource`, `setSources`,
13
+ * `failOverToNextSource`, or `resetToPreferredSource`) is what swaps the
14
+ * source for subsequent inference calls.
15
+ */
16
+ readonly active: InferenceSource;
17
+ /** Replace the active source's fields in place. */
18
+ setSource(source: InferenceSource): void;
19
+ /**
20
+ * Replace the whole ordered list and position the active source at
21
+ * `defaultSource`. Used when the control plane pushes a re-resolved
22
+ * source list to a running agent.
23
+ */
24
+ setSources(sources: InferenceSource[], defaultSource: string): void;
25
+ /**
26
+ * Fail over the active source to the next entry in priority order, in
27
+ * place. Returns false when the active source is already the last in the
28
+ * list — there is no further failover target. The ordered list and the
29
+ * cursor are private; only the registry mutates which source is active,
30
+ * so the single-active-source invariant the reactor relies on holds.
31
+ */
32
+ failOverToNextSource(): boolean;
33
+ /**
34
+ * Reset the active source to the most-preferred (highest-priority) one, in
35
+ * place. The reactor calls this at the start of each inference cycle so a
36
+ * failover never permanently demotes the agent off its preferred source.
37
+ */
38
+ resetToPreferredSource(): void;
39
+ };
40
+ export declare function createSourceRegistry(opts: {
41
+ sources: InferenceSource[];
42
+ defaultSource: string;
43
+ }): SourceRegistry;