@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.
- package/LICENSE +176 -0
- package/README.md +80 -5
- package/dist/agent.d.ts +116 -0
- package/dist/agent.js +682 -0
- package/dist/canonicalize.d.ts +15 -0
- package/dist/canonicalize.js +160 -0
- package/dist/default-director.d.ts +24 -0
- package/dist/default-director.js +45 -0
- package/dist/definition.d.ts +139 -0
- package/dist/definition.js +40 -0
- package/dist/director-registry.d.ts +47 -0
- package/dist/director-registry.js +87 -0
- package/dist/director-types.d.ts +80 -0
- package/dist/director-types.js +13 -0
- package/dist/director.d.ts +70 -0
- package/dist/director.js +131 -0
- package/dist/env-validation.d.ts +59 -0
- package/dist/env-validation.js +180 -0
- package/dist/env.d.ts +160 -0
- package/dist/env.js +53 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +23 -0
- package/dist/internal-fixtures/mail.d.ts +39 -0
- package/dist/internal-fixtures/mail.js +86 -0
- package/dist/internal-fixtures/planner.d.ts +19 -0
- package/dist/internal-fixtures/planner.js +49 -0
- package/dist/lock.d.ts +16 -0
- package/dist/lock.js +47 -0
- package/dist/namespace.d.ts +12 -0
- package/dist/namespace.js +39 -0
- package/dist/send-queue.d.ts +25 -0
- package/dist/send-queue.js +147 -0
- package/dist/source.d.ts +43 -0
- package/dist/source.js +118 -0
- package/dist/stream.d.ts +16 -0
- package/dist/stream.js +115 -0
- package/dist/testing/audit-noop.d.ts +7 -0
- package/dist/testing/audit-noop.js +25 -0
- package/dist/testing/authorize-allow.d.ts +8 -0
- package/dist/testing/authorize-allow.js +19 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +17 -0
- package/dist/tool.d.ts +238 -0
- package/dist/tool.js +244 -0
- package/package.json +26 -7
- package/src/agent.test.ts +0 -46
- package/src/agent.ts +0 -494
- package/src/index.ts +0 -38
- package/src/lock.test.ts +0 -93
- package/src/lock.ts +0 -57
- package/src/send-queue.test.ts +0 -207
- package/src/send-queue.ts +0 -200
- package/src/source.test.ts +0 -171
- package/src/source.ts +0 -93
- package/src/stream.test.ts +0 -167
- package/src/stream.ts +0 -142
- package/src/tool.test.ts +0 -217
- package/src/tool.ts +0 -148
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/source.d.ts
ADDED
|
@@ -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;
|