@intx/agent 0.2.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/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
@@ -71,6 +71,22 @@ const DEFAULT_SEND_TO = "agent@local";
71
71
  const DEFAULT_SEND_QUEUE_MAX = 16;
72
72
  const DEFAULT_STREAM_BUFFER_MAX = 1024;
73
73
  const DEFAULT_CLOSE_TIMEOUT_MS = 5000;
74
+ /**
75
+ * A `reactor.gate.blocked` event settled the active send but carried no
76
+ * `correlationId`, so the resulting suspension has no handle a caller
77
+ * could resume against. The reactor omits `correlationId` for gates
78
+ * parked without a correlation (e.g. a director suspend with no
79
+ * correlated external decision), and `send()` cannot hand back an
80
+ * unresumable outcome -- it surfaces this instead.
81
+ */
82
+ export class GateSuspendedWithoutCorrelationError extends Error {
83
+ gateId;
84
+ constructor(gateId) {
85
+ super(`reactor suspended on gate ${gateId} without a correlationId; the send has no handle to resume against`);
86
+ this.name = "GateSuspendedWithoutCorrelationError";
87
+ this.gateId = gateId;
88
+ }
89
+ }
74
90
  export class AgentClosedError extends Error {
75
91
  constructor() {
76
92
  super("agent is closed");
@@ -391,7 +407,34 @@ export async function createAgent(def, env) {
391
407
  const turn = activeCycle.lastAssistantTurn ??
392
408
  buildSyntheticTurn(event.data.content);
393
409
  activeCycle = null;
394
- sendQueue.resolveActive({ reply: event.data.content, turn });
410
+ sendQueue.resolveActive({
411
+ type: "reply",
412
+ reply: event.data.content,
413
+ turn,
414
+ });
415
+ }
416
+ else if (event.type === "reactor.gate.blocked") {
417
+ // The reactor parked on a gate before producing a reply. This
418
+ // is a terminal outcome for the active send: the cycle will not
419
+ // continue until the correlated external decision is delivered,
420
+ // and a parked cycle does not emit connector.reply or
421
+ // reactor.done, so leaving the send unsettled would hang the
422
+ // caller. Resolve with the suspended outcome so the caller can
423
+ // resume against the correlationId. A gate parked without a
424
+ // correlationId is unresumable -- surface it rather than hand
425
+ // back an outcome with no handle.
426
+ const { correlationId, approvalSnapshot } = event.data;
427
+ activeCycle = null;
428
+ if (correlationId === undefined) {
429
+ sendQueue.rejectActive(new GateSuspendedWithoutCorrelationError(event.data.gateId));
430
+ }
431
+ else {
432
+ sendQueue.resolveActive({
433
+ type: "suspended",
434
+ correlationId,
435
+ ...(approvalSnapshot !== undefined ? { approvalSnapshot } : {}),
436
+ });
437
+ }
395
438
  }
396
439
  else if (event.type === "reactor.error" && event.data.fatal) {
397
440
  // Only fatal reactor errors terminate the active send. Non-fatal
@@ -451,6 +494,7 @@ export async function createAgent(def, env) {
451
494
  onEvent: handleEvent,
452
495
  auditStore,
453
496
  authorize,
497
+ toolDefinitions: resolvedTools.definitions,
454
498
  onShutdown: async () => {
455
499
  try {
456
500
  await flushErrors();
@@ -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/index.d.ts CHANGED
@@ -1,14 +1,14 @@
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
5
  export { validateNamespacedId } from "./namespace.js";
6
6
  export { CanonicalizationError, canonicalizeForHash } from "./canonicalize.js";
7
- export { type DefinedDirector, defineDirector } from "./director.js";
8
- export { createDefaultDirectorRegistry, createDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
7
+ export { type DefinedDirector, defineDirector, isAnnotatedDirectorFactory, } from "./director.js";
8
+ export { createDefaultDirectorRegistry, createDirectorRegistry, createWorkflowDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
9
9
  export { type DefaultDirectorConfig, buildDefaultDirectorRef, defaultDirectorFactory, } from "./default-director.js";
10
10
  export { type SourceRegistry, InvalidInferenceSourceError, SourceNotFoundError, createSourceRegistry, } from "./source.js";
11
- export { type Agent, type SendOptions, type SendResult, AgentClosedError, createAgent, } from "./agent.js";
11
+ export { type Agent, type SendOptions, type SendResult, AgentClosedError, GateSuspendedWithoutCorrelationError, createAgent, } from "./agent.js";
12
12
  export { type AgentDefinition, type DefineAgentConfig, type EnvRequiredByAll, type InferencePreference, defineAgent, } from "./definition.js";
13
13
  export { effectiveDirectorRef, getRequiredEnvKeys, validateEnv, } from "./env-validation.js";
14
14
  export type { RequiredEnvKeys } from "./env-validation.js";
package/dist/index.js CHANGED
@@ -7,16 +7,16 @@
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
13
  export { validateNamespacedId } from "./namespace.js";
14
14
  export { CanonicalizationError, canonicalizeForHash } from "./canonicalize.js";
15
- export { defineDirector } from "./director.js";
16
- export { createDefaultDirectorRegistry, createDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
15
+ export { defineDirector, isAnnotatedDirectorFactory, } from "./director.js";
16
+ export { createDefaultDirectorRegistry, createDirectorRegistry, createWorkflowDirectorRegistry, UnknownDirectorIdError, } from "./director-registry.js";
17
17
  export { buildDefaultDirectorRef, defaultDirectorFactory, } from "./default-director.js";
18
18
  export { InvalidInferenceSourceError, SourceNotFoundError, createSourceRegistry, } from "./source.js";
19
- export { AgentClosedError, createAgent, } from "./agent.js";
19
+ export { AgentClosedError, GateSuspendedWithoutCorrelationError, createAgent, } from "./agent.js";
20
20
  export { defineAgent, } from "./definition.js";
21
21
  export { effectiveDirectorRef, getRequiredEnvKeys, validateEnv, } from "./env-validation.js";
22
22
  export { SendQueueFullError } from "./send-queue.js";
@@ -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) {
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.3.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.3.0",
21
+ "@intx/log": "0.3.0",
22
+ "@intx/mime": "0.3.0",
23
+ "@intx/types": "0.3.0",
23
24
  "arktype": "^2.1.29"
24
25
  },
25
26
  "devDependencies": {
26
- "@intx/storage-isogit": "0.2.2"
27
+ "@intx/storage-isogit": "0.3.0"
27
28
  },
28
29
  "files": [
29
30
  "dist",