@intx/agent 0.2.2 → 0.4.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/README.md CHANGED
@@ -16,13 +16,13 @@ import {
16
16
  defineAgent,
17
17
  } from "@intx/agent";
18
18
  import { noopAuditStore, permissiveAuthorize } from "@intx/agent/testing";
19
- import { createIsogitStore } from "@intx/storage-isogit";
19
+ import { createIsogitStore } from "@intx/storage-isogit/node";
20
20
 
21
21
  // `apiKey` and `model` come from the caller's env / config; pick the
22
22
  // shape that fits the deployment. The snippet below uses literals so
23
23
  // it copy-pastes cleanly.
24
24
  const apiKey = process.env.ANTHROPIC_API_KEY ?? "";
25
- const model = "claude-sonnet-4-6";
25
+ const model = "claude-sonnet-5";
26
26
  const source = {
27
27
  id: `anthropic:${model}`,
28
28
  provider: "anthropic",
package/dist/agent.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ReactorEmittedEvent } from "@intx/inference";
2
- import type { BlobReader, ContextCommit, ConversationTurn, InboundMessage, InferenceSource } from "@intx/types/runtime";
2
+ import type { ApprovalSnapshot, BlobReader, ContextCommit, ConversationTurn, InboundMessage, InferenceSource } from "@intx/types/runtime";
3
3
  import type { AgentDefinition } from "./definition.js";
4
4
  import type { BaseEnv } from "./env.js";
5
5
  export type SendOptions = {
@@ -17,6 +17,7 @@ export type SendOptions = {
17
17
  from?: string;
18
18
  };
19
19
  export type SendResult = {
20
+ type: "reply";
20
21
  /** Reply text emitted by the director's `reply` action. */
21
22
  reply: string;
22
23
  /**
@@ -24,7 +25,35 @@ export type SendResult = {
24
25
  * the reactor's `inference.done` event preceding `connector.reply`.
25
26
  */
26
27
  turn: ConversationTurn;
28
+ } | {
29
+ /**
30
+ * The reactor parked on a gate before producing a reply. The cycle
31
+ * is not finished -- it will resume when the correlated external
32
+ * decision is delivered. `correlationId` identifies the pending
33
+ * operation the caller resumes against.
34
+ */
35
+ type: "suspended";
36
+ correlationId: string;
37
+ /**
38
+ * Approver-facing snapshot of the parked tool call, when the reactor
39
+ * carried one on the gate-blocked event. Forwarded so the runtime can
40
+ * register it with the suspension. Absent for suspensions with no
41
+ * snapshot (an authz extension wired with no tool definitions).
42
+ */
43
+ approvalSnapshot?: ApprovalSnapshot;
27
44
  };
45
+ /**
46
+ * A `reactor.gate.blocked` event settled the active send but carried no
47
+ * `correlationId`, so the resulting suspension has no handle a caller
48
+ * could resume against. The reactor omits `correlationId` for gates
49
+ * parked without a correlation (e.g. a director suspend with no
50
+ * correlated external decision), and `send()` cannot hand back an
51
+ * unresumable outcome -- it surfaces this instead.
52
+ */
53
+ export declare class GateSuspendedWithoutCorrelationError extends Error {
54
+ readonly gateId: string;
55
+ constructor(gateId: string);
56
+ }
28
57
  export type Agent = {
29
58
  send(content: string | InboundMessage, opts?: SendOptions): Promise<SendResult>;
30
59
  stream(): AsyncIterable<ReactorEmittedEvent>;
package/dist/agent.js CHANGED
@@ -42,6 +42,7 @@
42
42
  // contract; the caller owns the lifetime of what it puts in env.
43
43
  import { createReactorAssembly, } from "@intx/inference";
44
44
  import { createDefaultDependencies } from "@intx/inference/providers";
45
+ import { createUnconfiguredCredentialResolver } from "./credential-resolver.js";
45
46
  import { getLogger } from "@intx/log";
46
47
  import { createInboundMessage } from "@intx/mime";
47
48
  import { validateDirectorConfig } from "./director.js";
@@ -71,6 +72,22 @@ const DEFAULT_SEND_TO = "agent@local";
71
72
  const DEFAULT_SEND_QUEUE_MAX = 16;
72
73
  const DEFAULT_STREAM_BUFFER_MAX = 1024;
73
74
  const DEFAULT_CLOSE_TIMEOUT_MS = 5000;
75
+ /**
76
+ * A `reactor.gate.blocked` event settled the active send but carried no
77
+ * `correlationId`, so the resulting suspension has no handle a caller
78
+ * could resume against. The reactor omits `correlationId` for gates
79
+ * parked without a correlation (e.g. a director suspend with no
80
+ * correlated external decision), and `send()` cannot hand back an
81
+ * unresumable outcome -- it surfaces this instead.
82
+ */
83
+ export class GateSuspendedWithoutCorrelationError extends Error {
84
+ gateId;
85
+ constructor(gateId) {
86
+ super(`reactor suspended on gate ${gateId} without a correlationId; the send has no handle to resume against`);
87
+ this.name = "GateSuspendedWithoutCorrelationError";
88
+ this.gateId = gateId;
89
+ }
90
+ }
74
91
  export class AgentClosedError extends Error {
75
92
  constructor() {
76
93
  super("agent is closed");
@@ -391,7 +408,34 @@ export async function createAgent(def, env) {
391
408
  const turn = activeCycle.lastAssistantTurn ??
392
409
  buildSyntheticTurn(event.data.content);
393
410
  activeCycle = null;
394
- sendQueue.resolveActive({ reply: event.data.content, turn });
411
+ sendQueue.resolveActive({
412
+ type: "reply",
413
+ reply: event.data.content,
414
+ turn,
415
+ });
416
+ }
417
+ else if (event.type === "reactor.gate.blocked") {
418
+ // The reactor parked on a gate before producing a reply. This
419
+ // is a terminal outcome for the active send: the cycle will not
420
+ // continue until the correlated external decision is delivered,
421
+ // and a parked cycle does not emit connector.reply or
422
+ // reactor.done, so leaving the send unsettled would hang the
423
+ // caller. Resolve with the suspended outcome so the caller can
424
+ // resume against the correlationId. A gate parked without a
425
+ // correlationId is unresumable -- surface it rather than hand
426
+ // back an outcome with no handle.
427
+ const { correlationId, approvalSnapshot } = event.data;
428
+ activeCycle = null;
429
+ if (correlationId === undefined) {
430
+ sendQueue.rejectActive(new GateSuspendedWithoutCorrelationError(event.data.gateId));
431
+ }
432
+ else {
433
+ sendQueue.resolveActive({
434
+ type: "suspended",
435
+ correlationId,
436
+ ...(approvalSnapshot !== undefined ? { approvalSnapshot } : {}),
437
+ });
438
+ }
395
439
  }
396
440
  else if (event.type === "reactor.error" && event.data.fatal) {
397
441
  // Only fatal reactor errors terminate the active send. Non-fatal
@@ -446,11 +490,13 @@ export async function createAgent(def, env) {
446
490
  source: sourceRegistry.active,
447
491
  failOverToNextSource: () => sourceRegistry.failOverToNextSource(),
448
492
  resetToPreferredSource: () => sourceRegistry.resetToPreferredSource(),
493
+ readMaterial: env.readCurrentMaterial ?? createUnconfiguredCredentialResolver(),
449
494
  toolRunner: resolvedTools.runner,
450
495
  contextStore,
451
496
  onEvent: handleEvent,
452
497
  auditStore,
453
498
  authorize,
499
+ toolDefinitions: resolvedTools.definitions,
454
500
  onShutdown: async () => {
455
501
  try {
456
502
  await flushErrors();
@@ -463,6 +509,9 @@ export async function createAgent(def, env) {
463
509
  ...(env.sizeCapMaxChars !== undefined
464
510
  ? { sizeCapMaxChars: env.sizeCapMaxChars }
465
511
  : {}),
512
+ ...(env.doomLoopThreshold !== undefined
513
+ ? { doomLoopThreshold: env.doomLoopThreshold }
514
+ : {}),
466
515
  deps,
467
516
  ...(env.compactors !== undefined ? { compactors: env.compactors } : {}),
468
517
  });
@@ -0,0 +1,16 @@
1
+ import type { CredentialMaterialResolver } from "@intx/types";
2
+ /**
3
+ * A resolver that fails closed on every call. `createAgent` installs this when
4
+ * the env supplies no `readCurrentMaterial`, so an agent whose inference never
5
+ * resolves a credential (a mock adapter emitting no credential sentinel) needs
6
+ * no resolver, while one that DOES reach a credential surfaces a clear error
7
+ * rather than a confusing `undefined`.
8
+ */
9
+ export declare function createUnconfiguredCredentialResolver(): CredentialMaterialResolver;
10
+ /**
11
+ * A resolver over a fixed `credentialId -> secret` map. For callers that hold
12
+ * their secrets in memory rather than a live cell -- examples, tests, and any
13
+ * single-process agent. Fails closed when a source references a credential the
14
+ * map does not carry, mirroring the cell reader's revoked/absent behavior.
15
+ */
16
+ export declare function createStaticCredentialResolver(materials: Record<string, string>): CredentialMaterialResolver;
@@ -0,0 +1,33 @@
1
+ // Helpers for the inference credential-material resolver seam
2
+ // (`CredentialMaterialResolver` in `@intx/types`). An inference call resolves
3
+ // its source's secret by `credentialId` through this seam instead of reading an
4
+ // inline `apiKey`, so the source config carries no secret. The sidecar backs it
5
+ // with the run's live credential cell; the helpers here cover the two simpler
6
+ // cases.
7
+ /**
8
+ * A resolver that fails closed on every call. `createAgent` installs this when
9
+ * the env supplies no `readCurrentMaterial`, so an agent whose inference never
10
+ * resolves a credential (a mock adapter emitting no credential sentinel) needs
11
+ * no resolver, while one that DOES reach a credential surfaces a clear error
12
+ * rather than a confusing `undefined`.
13
+ */
14
+ export function createUnconfiguredCredentialResolver() {
15
+ return (credentialId) => {
16
+ throw new Error(`no credential resolver configured for this agent, but an inference call needs the secret for credential ${credentialId}; supply env.readCurrentMaterial`);
17
+ };
18
+ }
19
+ /**
20
+ * A resolver over a fixed `credentialId -> secret` map. For callers that hold
21
+ * their secrets in memory rather than a live cell -- examples, tests, and any
22
+ * single-process agent. Fails closed when a source references a credential the
23
+ * map does not carry, mirroring the cell reader's revoked/absent behavior.
24
+ */
25
+ export function createStaticCredentialResolver(materials) {
26
+ return (credentialId) => {
27
+ const secret = materials[credentialId];
28
+ if (secret === undefined) {
29
+ throw new Error(`no credential material for ${credentialId} in the static resolver`);
30
+ }
31
+ return { secret };
32
+ };
33
+ }
@@ -1,3 +1,4 @@
1
+ import type { ToolPackagePin } from "@intx/types/tool-packages";
1
2
  import type { AnnotatedToolFactory } from "./tool.js";
2
3
  import type { BaseEnv } from "./env.js";
3
4
  import type { DirectorRef } from "./director-types.js";
@@ -37,6 +38,19 @@ export interface AgentDefinition<EnvReq extends BaseEnv = BaseEnv> {
37
38
  readonly systemPrompt: string;
38
39
  readonly director?: DirectorRef;
39
40
  readonly toolFactories: readonly AnnotatedToolFactory<EnvReq>[];
41
+ /**
42
+ * Tool-package names whose `definePlugin` factories this agent uses
43
+ * (`["@intx/tools-lsp"]`). Unlike a tool factory -- which the agent
44
+ * imports and places in `toolFactories`, so it is agent-visible -- a
45
+ * plugin package contributes NO agent-visible factory: its plugin
46
+ * factory reaches the agent only through `env.plugins`, wired by the
47
+ * host. This explicit per-agent list is therefore the only way per-step
48
+ * plugin scoping and the plugin's contributed tool grants can be known
49
+ * from the definition alone. The field is part of the hashed wire
50
+ * surface (the live->inert projector carries it), so a tampered plugin
51
+ * set fails re-verify. Absent when the agent uses no plugins.
52
+ */
53
+ readonly plugins?: readonly string[];
40
54
  readonly capabilities: readonly string[];
41
55
  readonly inference: {
42
56
  readonly sources: readonly InferencePreference[];
@@ -56,6 +70,13 @@ export interface AgentDefinition<EnvReq extends BaseEnv = BaseEnv> {
56
70
  * encoded JSON in a tag value.
57
71
  */
58
72
  readonly tags?: Readonly<Record<string, string>>;
73
+ /**
74
+ * Tool-package pins the sidecar materializes for this agent, carried on the
75
+ * definition so a folded workflow asset is self-contained rather than
76
+ * depending on pins supplied only at deploy time. Plain-data mirror of the
77
+ * pins the deploy-tree tool channel consumes.
78
+ */
79
+ readonly toolPackagePins?: readonly ToolPackagePin[];
59
80
  }
60
81
  type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
61
82
  type EnvRequiredBy<F> = F extends AnnotatedToolFactory<infer E> ? E : never;
@@ -101,6 +122,8 @@ export interface DefineAgentConfig<Factories extends readonly AnnotatedToolFacto
101
122
  readonly systemPrompt: string;
102
123
  readonly director?: DirectorRef;
103
124
  readonly tools: Factories;
125
+ /** Plugin-package names this agent uses; see `AgentDefinition.plugins`. */
126
+ readonly plugins?: readonly string[];
104
127
  readonly capabilities: readonly string[];
105
128
  readonly inference: {
106
129
  readonly sources: readonly InferencePreference[];
@@ -29,6 +29,7 @@ export function defineAgent(config) {
29
29
  toolFactories,
30
30
  capabilities: config.capabilities,
31
31
  inference: config.inference,
32
+ ...(config.plugins !== undefined ? { plugins: config.plugins } : {}),
32
33
  ...(config.description !== undefined
33
34
  ? { description: config.description }
34
35
  : {}),
@@ -35,4 +35,13 @@ export declare function createDirectorRegistry(opts: {
35
35
  * factories pass them into `createDirectorRegistry` directly.
36
36
  */
37
37
  export declare function createDefaultDirectorRegistry(): DirectorRegistry;
38
+ /**
39
+ * Build the director registry for a workflow closure: the built-in default
40
+ * plus the closure's own `defineDirector` factories. A closure that ships no
41
+ * directors passes `loaded: []` and composes to `[defaultDirectorFactory]` --
42
+ * identical to `createDefaultDirectorRegistry`. A closure director whose id
43
+ * shadows the built-in (or another loaded director) throws at construction,
44
+ * the same fail-loud `createDirectorRegistry` applies to any duplicate.
45
+ */
46
+ export declare function createWorkflowDirectorRegistry(loaded: readonly AnnotatedDirectorFactory<unknown, BaseEnv>[]): DirectorRegistry;
38
47
  export {};
@@ -71,3 +71,17 @@ export function createDefaultDirectorRegistry() {
71
71
  defaultId: defaultDirectorFactory.id,
72
72
  });
73
73
  }
74
+ /**
75
+ * Build the director registry for a workflow closure: the built-in default
76
+ * plus the closure's own `defineDirector` factories. A closure that ships no
77
+ * directors passes `loaded: []` and composes to `[defaultDirectorFactory]` --
78
+ * identical to `createDefaultDirectorRegistry`. A closure director whose id
79
+ * shadows the built-in (or another loaded director) throws at construction,
80
+ * the same fail-loud `createDirectorRegistry` applies to any duplicate.
81
+ */
82
+ export function createWorkflowDirectorRegistry(loaded) {
83
+ return createDirectorRegistry({
84
+ factories: [defaultDirectorFactory, ...loaded],
85
+ defaultId: defaultDirectorFactory.id,
86
+ });
87
+ }
@@ -54,3 +54,17 @@ export declare function defineDirector<Config, EnvReq extends BaseEnv = BaseEnv>
54
54
  * malformed config to the factory.
55
55
  */
56
56
  export declare function validateDirectorConfig(config: unknown, schema: DirectorConfigSchema): void;
57
+ /**
58
+ * Structural check for an `AnnotatedDirectorFactory` export. The shape is
59
+ * callable + `{ id: string, requires: string[], configSchema: function }`.
60
+ * The `configSchema` field is the discriminator against tool factories
61
+ * (which carry only `id` and `requires`); without it, any tool-factory
62
+ * export from a directors-entry module would be accepted as a director.
63
+ *
64
+ * Shared by the tool-package loader (`@intx/tool-packaging`) and the
65
+ * workflow-closure director loader (`@intx/workflow-host`) so both accept
66
+ * and reject exactly the same shapes -- one accept/reject rule the
67
+ * approval-time probe and the runtime cannot drift apart on. Two copies of
68
+ * "is this a valid director" would be a silent congruence hole.
69
+ */
70
+ export declare function isAnnotatedDirectorFactory(value: unknown): value is AnnotatedDirectorFactory<unknown, BaseEnv>;
package/dist/director.js CHANGED
@@ -15,6 +15,7 @@
15
15
  // factories it wants rather than relying on import-order.
16
16
  import { type } from "arktype";
17
17
  import { validateNamespacedId } from "./namespace.js";
18
+ import { isAnnotatedPluginFactory } from "./tool.js";
18
19
  /**
19
20
  * Define a director factory.
20
21
  *
@@ -90,3 +91,41 @@ function validateConfig(config, schema) {
90
91
  export function validateDirectorConfig(config, schema) {
91
92
  validateConfig(config, schema);
92
93
  }
94
+ /**
95
+ * Structural check for an `AnnotatedDirectorFactory` export. The shape is
96
+ * callable + `{ id: string, requires: string[], configSchema: function }`.
97
+ * The `configSchema` field is the discriminator against tool factories
98
+ * (which carry only `id` and `requires`); without it, any tool-factory
99
+ * export from a directors-entry module would be accepted as a director.
100
+ *
101
+ * Shared by the tool-package loader (`@intx/tool-packaging`) and the
102
+ * workflow-closure director loader (`@intx/workflow-host`) so both accept
103
+ * and reject exactly the same shapes -- one accept/reject rule the
104
+ * approval-time probe and the runtime cannot drift apart on. Two copies of
105
+ * "is this a valid director" would be a silent congruence hole.
106
+ */
107
+ export function isAnnotatedDirectorFactory(value) {
108
+ if (typeof value !== "function")
109
+ return false;
110
+ if (isAnnotatedPluginFactory(value))
111
+ return false;
112
+ if (!("id" in value) || !("requires" in value))
113
+ return false;
114
+ if (!("configSchema" in value))
115
+ return false;
116
+ const id = value.id;
117
+ const requires = value.requires;
118
+ const configSchema = value.configSchema;
119
+ if (typeof id !== "string")
120
+ return false;
121
+ if (!Array.isArray(requires))
122
+ return false;
123
+ if (!requires.every((r) => typeof r === "string"))
124
+ return false;
125
+ // `defineDirector` requires a callable arktype validator. A non-callable
126
+ // schema would crash later inside config validation; reject here so the
127
+ // failure surfaces at load time rather than at first config-validation.
128
+ if (typeof configSchema !== "function")
129
+ return false;
130
+ return true;
131
+ }
package/dist/env.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AuthzCallResult, Dependencies } from "@intx/inference";
2
+ import type { CredentialMaterialResolver } from "@intx/types";
2
3
  import type { AuditStore, Compactor, ContextStore, InferenceSource } from "@intx/types/runtime";
3
4
  import type { DirectorRegistry } from "./director-types.js";
4
5
  export type { Dependencies };
@@ -50,6 +51,12 @@ export interface BaseEnv {
50
51
  * `workdir` values pointing at the same on-disk storage directory
51
52
  * will silently corrupt each other -- the invariant is the caller's
52
53
  * to maintain.
54
+ *
55
+ * This is the lock and storage boundary, not the working tree the
56
+ * filesystem tools operate on. A tool that needs a working directory
57
+ * (e.g. `@intx/tools-posix`) reads that from its own env-DI key
58
+ * declared through `defineTool({ requires })`, which the caller may
59
+ * point at a directory distinct from `workdir`.
53
60
  */
54
61
  workdir: string;
55
62
  /** Audit sink. Required; no read-site fallback. */
@@ -90,6 +97,18 @@ export interface BaseEnv {
90
97
  * Optional; do not require this field on the production path.
91
98
  */
92
99
  deps?: Dependencies;
100
+ /**
101
+ * Resolves an inference source's credential secret by `credentialId` from the
102
+ * run's credential-material cell at send time -- the same cell tool
103
+ * credentials resolve from, so the source config carries no inline secret.
104
+ *
105
+ * Optional at this boundary only to spare callers whose inference never
106
+ * resolves a credential (a mock adapter that emits no credential sentinel).
107
+ * `createAgent` fills a fail-closed default that throws if an inference call
108
+ * actually needs a secret; a caller that does real credentialed inference (the
109
+ * sidecar step env, an example, a test with a real adapter) MUST supply one.
110
+ */
111
+ readCurrentMaterial?: CredentialMaterialResolver;
93
112
  /**
94
113
  * Optional deterministic session id. Production callers omit and let
95
114
  * the agent generate a fresh UUID; tests that assert on audit-record
@@ -101,6 +120,13 @@ export interface BaseEnv {
101
120
  * Forwarded to the reactor assembly's size-cap transform.
102
121
  */
103
122
  sizeCapMaxChars?: number;
123
+ /**
124
+ * Override for the default doom-loop detection threshold: the number of
125
+ * identical consecutive tool-call turns that ends the run (default 3).
126
+ * Forwarded to the reactor. Must be a positive integer, or `false` to
127
+ * disable doom-loop detection entirely.
128
+ */
129
+ doomLoopThreshold?: number | false;
104
130
  /**
105
131
  * Maximum number of pending sends (active + queued). Beyond this,
106
132
  * `send()` rejects with `SendQueueFullError`. Defaults to 16.
package/dist/index.d.ts CHANGED
@@ -1,14 +1,15 @@
1
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 ToolFactory, type ToolFactoryMeta, type ToolHandler, DuplicateToolError, PLUGIN_MARKER, TOOL_PLUGIN_KIND, type ToolPluginKind, createToolRunner, defineTool, definePlugin, fromToolRunner, isAnnotatedPluginFactory, isToolPluginInstance, stringTool, tool, } from "./tool.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
3
  export { type AuthorizeFn, type BaseEnv, type Dependencies, AgentEnvError, } from "./env.js";
4
4
  export { type AnnotatedDirectorFactory, type DirectorAgentContext, type DirectorConfigSchema, type DirectorFactory, type DirectorFactoryMeta, type DirectorRef, type DirectorRegistry, } from "./director-types.js";
5
+ export { createStaticCredentialResolver, createUnconfiguredCredentialResolver, } from "./credential-resolver.js";
5
6
  export { validateNamespacedId } from "./namespace.js";
6
7
  export { CanonicalizationError, canonicalizeForHash } from "./canonicalize.js";
7
- export { type DefinedDirector, defineDirector } from "./director.js";
8
- export { createDefaultDirectorRegistry, createDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
8
+ export { type DefinedDirector, defineDirector, isAnnotatedDirectorFactory, } from "./director.js";
9
+ export { createDefaultDirectorRegistry, createDirectorRegistry, createWorkflowDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
9
10
  export { type DefaultDirectorConfig, buildDefaultDirectorRef, defaultDirectorFactory, } from "./default-director.js";
10
11
  export { type SourceRegistry, InvalidInferenceSourceError, SourceNotFoundError, createSourceRegistry, } from "./source.js";
11
- export { type Agent, type SendOptions, type SendResult, AgentClosedError, createAgent, } from "./agent.js";
12
+ export { type Agent, type SendOptions, type SendResult, AgentClosedError, GateSuspendedWithoutCorrelationError, createAgent, } from "./agent.js";
12
13
  export { type AgentDefinition, type DefineAgentConfig, type EnvRequiredByAll, type InferencePreference, defineAgent, } from "./definition.js";
13
14
  export { effectiveDirectorRef, getRequiredEnvKeys, validateEnv, } from "./env-validation.js";
14
15
  export type { RequiredEnvKeys } from "./env-validation.js";
package/dist/index.js CHANGED
@@ -7,16 +7,17 @@
7
7
  // connector threads, outbound replies via MessageTransport) while the
8
8
  // agent drives it from in-process calls.
9
9
  export { AgentContextLockError } from "./lock.js";
10
- export { DuplicateToolError, PLUGIN_MARKER, TOOL_PLUGIN_KIND, createToolRunner, defineTool, definePlugin, fromToolRunner, isAnnotatedPluginFactory, isToolPluginInstance, stringTool, tool, } from "./tool.js";
10
+ export { DuplicateToolError, PLUGIN_MARKER, TOOL_PLUGIN_KIND, createToolRunner, defineTool, definePlugin, fromToolRunner, isAnnotatedPluginFactory, isToolPluginInstance, stringTool, tool, toolApprovalEffect, } from "./tool.js";
11
11
  export { AgentEnvError, } from "./env.js";
12
12
  export {} from "./director-types.js";
13
+ export { createStaticCredentialResolver, createUnconfiguredCredentialResolver, } from "./credential-resolver.js";
13
14
  export { validateNamespacedId } from "./namespace.js";
14
15
  export { CanonicalizationError, canonicalizeForHash } from "./canonicalize.js";
15
- export { defineDirector } from "./director.js";
16
- export { createDefaultDirectorRegistry, createDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
16
+ export { defineDirector, isAnnotatedDirectorFactory, } from "./director.js";
17
+ export { createDefaultDirectorRegistry, createDirectorRegistry, createWorkflowDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
17
18
  export { buildDefaultDirectorRef, defaultDirectorFactory, } from "./default-director.js";
18
19
  export { InvalidInferenceSourceError, SourceNotFoundError, createSourceRegistry, } from "./source.js";
19
- export { AgentClosedError, createAgent, } from "./agent.js";
20
+ export { AgentClosedError, GateSuspendedWithoutCorrelationError, createAgent, } from "./agent.js";
20
21
  export { defineAgent, } from "./definition.js";
21
22
  export { effectiveDirectorRef, getRequiredEnvKeys, validateEnv, } from "./env-validation.js";
22
23
  export { SendQueueFullError } from "./send-queue.js";
@@ -20,7 +20,7 @@ export const MAIL_SOURCE = {
20
20
  id: "anthropic:claude-opus-4-6",
21
21
  provider: "anthropic",
22
22
  baseURL: "https://api.anthropic.com",
23
- apiKey: "sk-test-mail",
23
+ credentialId: "sk-test-mail",
24
24
  model: "claude-opus-4-6",
25
25
  };
26
26
  export const MAIL_ADDRESS = "support@fixture.local";
@@ -32,6 +32,7 @@ export const MAIL_ADDRESS = "support@fixture.local";
32
32
  export const fixtureMailFactory = defineTool({
33
33
  id: "@intx-fixtures/mail/bundle",
34
34
  requires: ["transport", "address"],
35
+ definitions: [],
35
36
  factory: () => ({
36
37
  definitions: [],
37
38
  async run(call) {
@@ -11,7 +11,7 @@ export const PLANNER_SOURCE = {
11
11
  id: "anthropic:claude-opus-4-6",
12
12
  provider: "anthropic",
13
13
  baseURL: "https://api.anthropic.com",
14
- apiKey: "sk-test-planner",
14
+ credentialId: "sk-test-planner",
15
15
  model: "claude-opus-4-6",
16
16
  };
17
17
  /**
@@ -1,2 +1,3 @@
1
1
  export { noopAuditStore } from "./audit-noop.js";
2
2
  export { permissiveAuthorize } from "./authorize-allow.js";
3
+ export { waitForReactorDone } from "./reactor-waiters.js";
@@ -15,3 +15,4 @@
15
15
  // codebase.
16
16
  export { noopAuditStore } from "./audit-noop.js";
17
17
  export { permissiveAuthorize } from "./authorize-allow.js";
18
+ export { waitForReactorDone } from "./reactor-waiters.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Resolve once the stream emits `reactor.done`.
3
+ *
4
+ * Throws if the stream ends without it: returning would resolve as though the
5
+ * run had completed, and the caller would then fail on a later assertion
6
+ * instead of on the real cause.
7
+ *
8
+ * Typed structurally so a caller need not import the reactor's event union to
9
+ * await its terminal event.
10
+ */
11
+ export declare function waitForReactorDone(stream: AsyncIterable<{
12
+ type: string;
13
+ }>): Promise<void>;
@@ -0,0 +1,23 @@
1
+ // Waiter over a reactor's emitted-event stream.
2
+ //
3
+ // A reactor run ends with a `reactor.done` event, including on the fatal
4
+ // error path, so a test that needs the run finished waits for that event
5
+ // rather than for an interval. CONVENTIONS.md names this shape as the one to
6
+ // copy; it lives here so it is imported rather than copied again.
7
+ /**
8
+ * Resolve once the stream emits `reactor.done`.
9
+ *
10
+ * Throws if the stream ends without it: returning would resolve as though the
11
+ * run had completed, and the caller would then fail on a later assertion
12
+ * instead of on the real cause.
13
+ *
14
+ * Typed structurally so a caller need not import the reactor's event union to
15
+ * await its terminal event.
16
+ */
17
+ export async function waitForReactorDone(stream) {
18
+ for await (const event of stream) {
19
+ if (event.type === "reactor.done")
20
+ return;
21
+ }
22
+ throw new Error("reactor event stream ended before reactor.done");
23
+ }
package/dist/tool.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { GrantEffect } from "@intx/types";
1
2
  import type { ToolCall, ToolDefinition, ToolResult, ToolRunner } from "@intx/types/runtime";
2
3
  import type { BaseEnv } from "./env.js";
3
4
  export type ToolHandler = (call: ToolCall, signal: AbortSignal) => Promise<ToolResult>;
@@ -60,14 +61,44 @@ export interface ToolBundle {
60
61
  * instantiation.
61
62
  */
62
63
  export type ToolFactory<EnvReq extends BaseEnv = BaseEnv> = (env: EnvReq) => ToolBundle;
64
+ /**
65
+ * Static, per-definition declaration a tool factory carries so callers
66
+ * (e.g. the deploy-time capability walk) can enumerate the tool names a
67
+ * factory contributes WITHOUT instantiating it. `approval` marks a tool
68
+ * as requiring per-invocation approval; it is a deliberate subset of
69
+ * GrantEffect (only "ask" is expressible here — a declaration can request
70
+ * a gate, never a pre-deny).
71
+ */
72
+ export interface ToolDeclaration {
73
+ readonly name: string;
74
+ readonly approval?: "ask";
75
+ }
76
+ /**
77
+ * Map a tool's static approval mark to the `GrantEffect` floor its
78
+ * `tool:<name>` grant carries: an `ask`-marked tool floors at `ask` (its
79
+ * invocation must clear an approval gate), every other tool floors at
80
+ * `allow`.
81
+ *
82
+ * This is the single canonical derivation of a tool's authorization floor
83
+ * from its declaration. Both the deploy-time capability walk (hub-side)
84
+ * and the per-step tool authorization (sidecar-side) route through here so
85
+ * a pinned tool loaded in the child derives the SAME floor the walk would
86
+ * have derived from an inline declaration. A divergence between the two
87
+ * sites would let a pinned `ask` tool authorize as `allow` (or vice
88
+ * versa), so the mapping lives in exactly one place.
89
+ */
90
+ export declare function toolApprovalEffect(declaration: Pick<ToolDeclaration, "approval">): GrantEffect;
63
91
  /**
64
92
  * Runtime metadata attached to a `ToolFactory` by `defineTool`. `id` is
65
93
  * package-namespaced; `requires` enumerates env keys the factory touches
66
- * beyond `BaseEnv`'s six core fields.
94
+ * beyond `BaseEnv`'s six core fields; `definitions` statically declares
95
+ * the tool names the factory contributes so callers can enumerate them
96
+ * without instantiating the factory.
67
97
  */
68
98
  export interface ToolFactoryMeta {
69
99
  readonly id: string;
70
100
  readonly requires: readonly string[];
101
+ readonly definitions: readonly ToolDeclaration[];
71
102
  }
72
103
  /**
73
104
  * A tool factory carrying its runtime metadata. `defineTool` is the only
@@ -87,15 +118,20 @@ export type AnnotatedToolFactory<EnvReq extends BaseEnv = BaseEnv> = ToolFactory
87
118
  * `BaseEnv`'s six core fields. The runtime `validateEnv` checks
88
119
  * presence; the factory itself may also fail loud at construction
89
120
  * if the env contents are structurally wrong.
121
+ * - `definitions` statically declares the tool names this factory
122
+ * contributes so callers (e.g. the deploy-time capability walk) can
123
+ * enumerate them without instantiating the factory.
90
124
  * - `factory(env)` returns a `ToolBundle`. Invoked once per agent
91
125
  * instantiation; the bundle's lifetime is tied to that agent.
92
126
  *
93
127
  * The returned object is the same callable as the supplied `factory`
94
- * with `id` and a frozen `requires` array attached.
128
+ * with `id`, a frozen `requires` array, and a frozen `definitions`
129
+ * array attached.
95
130
  */
96
131
  export declare function defineTool<EnvReq extends BaseEnv = BaseEnv>(opts: {
97
132
  id: string;
98
133
  requires?: readonly string[];
134
+ definitions: readonly ToolDeclaration[];
99
135
  factory: ToolFactory<EnvReq>;
100
136
  }): AnnotatedToolFactory<EnvReq>;
101
137
  /**
@@ -126,6 +162,20 @@ export declare const PLUGIN_MARKER: unique symbol;
126
162
  export interface AnnotatedPluginMeta {
127
163
  readonly id: string;
128
164
  readonly requires: readonly string[];
165
+ /**
166
+ * Static declaration of the tool names this plugin contributes at
167
+ * runtime, so a caller can enumerate the plugin's tool grant surface
168
+ * WITHOUT instantiating it (which for a plugin like LSP would start a
169
+ * language-server subprocess). A plugin adds its tools indirectly -- it
170
+ * hands a host-defined shape to the tool package that consumes
171
+ * `env.plugins`, which then registers the plugin's tools under its own
172
+ * bundle -- so the plugin's contributed tool names are otherwise
173
+ * invisible until run time. The deploy-time capability walk reads this
174
+ * field to authorize a plugin-contributed tool the same way it
175
+ * authorizes a factory-declared tool. Empty when the plugin contributes
176
+ * no standalone tool (middleware-only plugins).
177
+ */
178
+ readonly definitions: readonly ToolDeclaration[];
129
179
  readonly [PLUGIN_MARKER]: true;
130
180
  }
131
181
  export type AnnotatedPluginFactory<EnvReq extends BaseEnv = BaseEnv, Result = unknown> = PluginFactory<EnvReq, Result> & AnnotatedPluginMeta;
@@ -165,6 +215,12 @@ export declare function isToolPluginInstance(value: unknown): value is Record<st
165
215
  export declare function definePlugin<Result extends object, EnvReq extends BaseEnv = BaseEnv>(opts: {
166
216
  id: string;
167
217
  requires?: readonly string[];
218
+ /**
219
+ * Static declaration of the tool names this plugin contributes at run
220
+ * time. Omit for a middleware-only plugin that adds no standalone tool.
221
+ * See `AnnotatedPluginMeta.definitions`.
222
+ */
223
+ definitions?: readonly ToolDeclaration[];
168
224
  factory: PluginFactory<EnvReq, Result>;
169
225
  }): AnnotatedPluginFactory<EnvReq, Result & {
170
226
  kind: ToolPluginKind;
package/dist/tool.js CHANGED
@@ -66,6 +66,23 @@ export class DuplicateToolError extends Error {
66
66
  this.toolName = toolName;
67
67
  }
68
68
  }
69
+ /**
70
+ * Map a tool's static approval mark to the `GrantEffect` floor its
71
+ * `tool:<name>` grant carries: an `ask`-marked tool floors at `ask` (its
72
+ * invocation must clear an approval gate), every other tool floors at
73
+ * `allow`.
74
+ *
75
+ * This is the single canonical derivation of a tool's authorization floor
76
+ * from its declaration. Both the deploy-time capability walk (hub-side)
77
+ * and the per-step tool authorization (sidecar-side) route through here so
78
+ * a pinned tool loaded in the child derives the SAME floor the walk would
79
+ * have derived from an inline declaration. A divergence between the two
80
+ * sites would let a pinned `ask` tool authorize as `allow` (or vice
81
+ * versa), so the mapping lives in exactly one place.
82
+ */
83
+ export function toolApprovalEffect(declaration) {
84
+ return declaration.approval === "ask" ? "ask" : "allow";
85
+ }
69
86
  /**
70
87
  * Define a tool bundle factory.
71
88
  *
@@ -75,17 +92,24 @@ export class DuplicateToolError extends Error {
75
92
  * `BaseEnv`'s six core fields. The runtime `validateEnv` checks
76
93
  * presence; the factory itself may also fail loud at construction
77
94
  * if the env contents are structurally wrong.
95
+ * - `definitions` statically declares the tool names this factory
96
+ * contributes so callers (e.g. the deploy-time capability walk) can
97
+ * enumerate them without instantiating the factory.
78
98
  * - `factory(env)` returns a `ToolBundle`. Invoked once per agent
79
99
  * instantiation; the bundle's lifetime is tied to that agent.
80
100
  *
81
101
  * The returned object is the same callable as the supplied `factory`
82
- * with `id` and a frozen `requires` array attached.
102
+ * with `id`, a frozen `requires` array, and a frozen `definitions`
103
+ * array attached.
83
104
  */
84
105
  export function defineTool(opts) {
85
106
  validateNamespacedId(opts.id);
86
107
  const requires = Object.freeze([
87
108
  ...(opts.requires ?? []),
88
109
  ]);
110
+ const definitions = Object.freeze([
111
+ ...opts.definitions,
112
+ ]);
89
113
  // Wrap the caller's factory rather than mutating it. A caller that
90
114
  // shares a factory function across multiple `defineTool` calls
91
115
  // (e.g. registering the same constructor under two ids in different
@@ -97,6 +121,7 @@ export function defineTool(opts) {
97
121
  return Object.assign(wrapped, {
98
122
  id: opts.id,
99
123
  requires,
124
+ definitions,
100
125
  });
101
126
  }
102
127
  /**
@@ -145,6 +170,9 @@ export function definePlugin(opts) {
145
170
  const requires = Object.freeze([
146
171
  ...(opts.requires ?? []),
147
172
  ]);
173
+ const definitions = Object.freeze([
174
+ ...(opts.definitions ?? []),
175
+ ]);
148
176
  const wrapped = (env) => {
149
177
  const instance = opts.factory(env);
150
178
  // Re-stamping the marker is harmless if the factory chose to set
@@ -155,6 +183,7 @@ export function definePlugin(opts) {
155
183
  return Object.assign(wrapped, {
156
184
  id: opts.id,
157
185
  requires,
186
+ definitions,
158
187
  [PLUGIN_MARKER]: true,
159
188
  });
160
189
  }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@intx/agent",
3
- "version": "0.2.2",
3
+ "description": "In-process agent runtime: construct an agent, send it a message, get a reply",
4
+ "version": "0.4.0",
4
5
  "license": "LGPL-2.1-only",
5
6
  "type": "module",
6
7
  "exports": {
@@ -16,14 +17,14 @@
16
17
  }
17
18
  },
18
19
  "dependencies": {
19
- "@intx/inference": "0.2.2",
20
- "@intx/log": "0.2.2",
21
- "@intx/mime": "0.2.2",
22
- "@intx/types": "0.2.2",
20
+ "@intx/inference": "0.4.0",
21
+ "@intx/log": "0.4.0",
22
+ "@intx/mime": "0.4.0",
23
+ "@intx/types": "0.4.0",
23
24
  "arktype": "^2.1.29"
24
25
  },
25
26
  "devDependencies": {
26
- "@intx/storage-isogit": "0.2.2"
27
+ "@intx/storage-isogit": "0.4.0"
27
28
  },
28
29
  "files": [
29
30
  "dist",