@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/source.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Inference source registry.
|
|
2
|
+
//
|
|
3
|
+
// The agent accepts an array of pre-configured inference sources and a
|
|
4
|
+
// `defaultSource` id at construction. The source whose `id` matches
|
|
5
|
+
// `defaultSource` becomes the active source — the same object reference
|
|
6
|
+
// is what the reactor's assembly holds and reads lazily at each
|
|
7
|
+
// inference call.
|
|
8
|
+
//
|
|
9
|
+
// `setSource` mutates that shared object in place so the next inference
|
|
10
|
+
// call observes the new credentials, model, and bound defaults. In-flight
|
|
11
|
+
// calls keep using the values they read at start-of-call (the reactor
|
|
12
|
+
// does not refetch mid-stream); the swap is therefore safe with respect
|
|
13
|
+
// to torn state.
|
|
14
|
+
import { type } from "arktype";
|
|
15
|
+
import { InferenceSource as InferenceSourceValidator, applyInferenceSourceFields, } from "@intx/types/runtime";
|
|
16
|
+
export class InvalidInferenceSourceError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "InvalidInferenceSourceError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class SourceNotFoundError extends Error {
|
|
23
|
+
id;
|
|
24
|
+
constructor(id) {
|
|
25
|
+
super(`no source in sources[] has id ${id}`);
|
|
26
|
+
this.name = "SourceNotFoundError";
|
|
27
|
+
this.id = id;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function validateSources(sources) {
|
|
31
|
+
if (sources.length === 0) {
|
|
32
|
+
throw new InvalidInferenceSourceError("sources[] must be non-empty");
|
|
33
|
+
}
|
|
34
|
+
const validated = [];
|
|
35
|
+
const seenIds = new Set();
|
|
36
|
+
for (const [i, raw] of sources.entries()) {
|
|
37
|
+
const parsed = InferenceSourceValidator(raw);
|
|
38
|
+
if (parsed instanceof type.errors) {
|
|
39
|
+
throw new InvalidInferenceSourceError(`sources[${String(i)}]: ${parsed.summary}`);
|
|
40
|
+
}
|
|
41
|
+
if (seenIds.has(parsed.id)) {
|
|
42
|
+
throw new InvalidInferenceSourceError(`sources[${String(i)}]: duplicate id ${parsed.id}`);
|
|
43
|
+
}
|
|
44
|
+
seenIds.add(parsed.id);
|
|
45
|
+
validated.push(parsed);
|
|
46
|
+
}
|
|
47
|
+
return validated;
|
|
48
|
+
}
|
|
49
|
+
export function createSourceRegistry(opts) {
|
|
50
|
+
// The ordered list, the default index, and the active cursor are private
|
|
51
|
+
// to the registry. Only the registry mutates which source is active.
|
|
52
|
+
let list = validateSources(opts.sources);
|
|
53
|
+
let defaultIndex = indexOfDefault(list, opts.defaultSource);
|
|
54
|
+
let activeIndex = defaultIndex;
|
|
55
|
+
const active = { ...sourceAt(list, activeIndex) };
|
|
56
|
+
function setSource(source) {
|
|
57
|
+
const parsed = InferenceSourceValidator(source);
|
|
58
|
+
if (parsed instanceof type.errors) {
|
|
59
|
+
throw new InvalidInferenceSourceError(parsed.summary);
|
|
60
|
+
}
|
|
61
|
+
applyInferenceSourceFields(active, parsed);
|
|
62
|
+
// A hot-swap is an explicit override of the active source, possibly to a
|
|
63
|
+
// source that is not in the list at all. Park the cursor at the default
|
|
64
|
+
// so the next per-cycle resetToPreferredSource is a no-op and the
|
|
65
|
+
// override survives — even when a failover had moved the cursor off the
|
|
66
|
+
// default before the swap.
|
|
67
|
+
activeIndex = defaultIndex;
|
|
68
|
+
}
|
|
69
|
+
function setSources(sources, defaultSource) {
|
|
70
|
+
const validated = validateSources(sources);
|
|
71
|
+
const index = indexOfDefault(validated, defaultSource);
|
|
72
|
+
list = validated;
|
|
73
|
+
defaultIndex = index;
|
|
74
|
+
activeIndex = index;
|
|
75
|
+
applyInferenceSourceFields(active, sourceAt(list, activeIndex));
|
|
76
|
+
}
|
|
77
|
+
function failOverToNextSource() {
|
|
78
|
+
if (activeIndex >= list.length - 1)
|
|
79
|
+
return false;
|
|
80
|
+
activeIndex += 1;
|
|
81
|
+
applyInferenceSourceFields(active, sourceAt(list, activeIndex));
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
function resetToPreferredSource() {
|
|
85
|
+
// Only undo a failover that actually moved the cursor. When the active
|
|
86
|
+
// source is already the preferred one, leave `active` untouched — a
|
|
87
|
+
// caller may have hot-swapped it via setSource (e.g. a director rotating
|
|
88
|
+
// the model), and that override must survive the per-cycle reset.
|
|
89
|
+
if (activeIndex === defaultIndex)
|
|
90
|
+
return;
|
|
91
|
+
activeIndex = defaultIndex;
|
|
92
|
+
applyInferenceSourceFields(active, sourceAt(list, activeIndex));
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
active,
|
|
96
|
+
setSource,
|
|
97
|
+
setSources,
|
|
98
|
+
failOverToNextSource,
|
|
99
|
+
resetToPreferredSource,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function indexOfDefault(list, defaultSource) {
|
|
103
|
+
const match = list.find((s) => s.id === defaultSource);
|
|
104
|
+
if (match === undefined) {
|
|
105
|
+
throw new SourceNotFoundError(defaultSource);
|
|
106
|
+
}
|
|
107
|
+
return list.indexOf(match);
|
|
108
|
+
}
|
|
109
|
+
function sourceAt(list, index) {
|
|
110
|
+
const source = list[index];
|
|
111
|
+
if (source === undefined) {
|
|
112
|
+
// Unreachable: callers only ever pass an in-range index. The guard
|
|
113
|
+
// satisfies noUncheckedIndexedAccess without a non-null assertion and
|
|
114
|
+
// fails loud if that invariant is ever broken.
|
|
115
|
+
throw new InvalidInferenceSourceError(`no source at index ${String(index)}`);
|
|
116
|
+
}
|
|
117
|
+
return source;
|
|
118
|
+
}
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactorEmittedEvent } from "@intx/inference";
|
|
2
|
+
export declare class StreamBackpressureError extends Error {
|
|
3
|
+
readonly maxBuffer: number;
|
|
4
|
+
constructor(maxBuffer: number);
|
|
5
|
+
}
|
|
6
|
+
export type StreamConsumer = {
|
|
7
|
+
/** Deliver an event to this consumer's buffer. */
|
|
8
|
+
push(event: ReactorEmittedEvent): void;
|
|
9
|
+
/** Cleanly terminate the iterator with `done: true`. */
|
|
10
|
+
close(): void;
|
|
11
|
+
/** True once close() or an overflow has poisoned the consumer. */
|
|
12
|
+
readonly closed: boolean;
|
|
13
|
+
/** Iterator handed back to the caller of `stream()`. */
|
|
14
|
+
iterator(): AsyncIterableIterator<ReactorEmittedEvent>;
|
|
15
|
+
};
|
|
16
|
+
export declare function createStreamConsumer(maxBuffer: number): StreamConsumer;
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Bounded per-consumer fan-out for the agent's reactor event stream.
|
|
2
|
+
//
|
|
3
|
+
// Each call to `agent.stream()` creates a fresh `StreamConsumer`. The
|
|
4
|
+
// agent feeds every reactor event to every consumer; consumers buffer
|
|
5
|
+
// independently. If a consumer falls more than `maxBuffer` events behind
|
|
6
|
+
// it is poisoned with `StreamBackpressureError` and its iterator throws
|
|
7
|
+
// on the next read — the consumer is removed but other consumers keep
|
|
8
|
+
// running.
|
|
9
|
+
//
|
|
10
|
+
// Loud failure matches the defensive-coding rule: silently dropping
|
|
11
|
+
// events would hide consumer bugs, and unbounded buffering would let a
|
|
12
|
+
// stalled consumer balloon the agent's memory. The cap is configurable
|
|
13
|
+
// via `streamBufferMax` on `BaseEnv`.
|
|
14
|
+
export class StreamBackpressureError extends Error {
|
|
15
|
+
maxBuffer;
|
|
16
|
+
constructor(maxBuffer) {
|
|
17
|
+
super(`stream consumer fell more than ${String(maxBuffer)} events behind`);
|
|
18
|
+
this.name = "StreamBackpressureError";
|
|
19
|
+
this.maxBuffer = maxBuffer;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function createStreamConsumer(maxBuffer) {
|
|
23
|
+
if (maxBuffer < 1) {
|
|
24
|
+
throw new Error(`streamBufferMax must be >= 1, got ${String(maxBuffer)}`);
|
|
25
|
+
}
|
|
26
|
+
const buffer = [];
|
|
27
|
+
const waiters = [];
|
|
28
|
+
let overflow;
|
|
29
|
+
let done = false;
|
|
30
|
+
function settleOverflowedWaiters(err) {
|
|
31
|
+
while (waiters.length > 0) {
|
|
32
|
+
const w = waiters.shift();
|
|
33
|
+
if (w === undefined)
|
|
34
|
+
return;
|
|
35
|
+
w.reject(err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function settleDoneWaiters() {
|
|
39
|
+
while (waiters.length > 0) {
|
|
40
|
+
const w = waiters.shift();
|
|
41
|
+
if (w === undefined)
|
|
42
|
+
return;
|
|
43
|
+
w.resolve({ value: undefined, done: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function push(event) {
|
|
47
|
+
if (done || overflow !== undefined)
|
|
48
|
+
return;
|
|
49
|
+
if (waiters.length > 0) {
|
|
50
|
+
const w = waiters.shift();
|
|
51
|
+
if (w === undefined)
|
|
52
|
+
return;
|
|
53
|
+
w.resolve({ value: event, done: false });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (buffer.length >= maxBuffer) {
|
|
57
|
+
overflow = new StreamBackpressureError(maxBuffer);
|
|
58
|
+
settleOverflowedWaiters(overflow);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
buffer.push(event);
|
|
62
|
+
}
|
|
63
|
+
function close() {
|
|
64
|
+
if (done)
|
|
65
|
+
return;
|
|
66
|
+
done = true;
|
|
67
|
+
settleDoneWaiters();
|
|
68
|
+
}
|
|
69
|
+
function nextResult() {
|
|
70
|
+
if (overflow !== undefined) {
|
|
71
|
+
// Drain any buffered events before throwing so the caller sees
|
|
72
|
+
// every event up to the overflow point.
|
|
73
|
+
if (buffer.length > 0) {
|
|
74
|
+
const ev = buffer.shift();
|
|
75
|
+
if (ev !== undefined) {
|
|
76
|
+
return Promise.resolve({ value: ev, done: false });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return Promise.reject(overflow);
|
|
80
|
+
}
|
|
81
|
+
if (buffer.length > 0) {
|
|
82
|
+
const ev = buffer.shift();
|
|
83
|
+
if (ev !== undefined) {
|
|
84
|
+
return Promise.resolve({ value: ev, done: false });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (done) {
|
|
88
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
89
|
+
}
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
waiters.push({ resolve, reject });
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function iterator() {
|
|
95
|
+
const it = {
|
|
96
|
+
next: nextResult,
|
|
97
|
+
async return() {
|
|
98
|
+
close();
|
|
99
|
+
return { value: undefined, done: true };
|
|
100
|
+
},
|
|
101
|
+
[Symbol.asyncIterator]() {
|
|
102
|
+
return it;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
return it;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
push,
|
|
109
|
+
close,
|
|
110
|
+
get closed() {
|
|
111
|
+
return done || overflow !== undefined;
|
|
112
|
+
},
|
|
113
|
+
iterator,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AuditStore } from "@intx/types/runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Construct a no-op AuditStore. Each call returns a fresh object so
|
|
4
|
+
* tests that introspect the store identity (e.g. asserting two agents
|
|
5
|
+
* received different stores) can do so.
|
|
6
|
+
*/
|
|
7
|
+
export declare function noopAuditStore(): AuditStore;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// No-op AuditStore for tests and examples.
|
|
2
|
+
//
|
|
3
|
+
// Returns immediately for every commit; returns empty arrays for every
|
|
4
|
+
// load. Useful when the agent is exercised in tests that do not assert
|
|
5
|
+
// audit content, or in examples whose purpose is the agent surface
|
|
6
|
+
// rather than the audit ledger. Production callers must supply a real
|
|
7
|
+
// audit store.
|
|
8
|
+
/**
|
|
9
|
+
* Construct a no-op AuditStore. Each call returns a fresh object so
|
|
10
|
+
* tests that introspect the store identity (e.g. asserting two agents
|
|
11
|
+
* received different stores) can do so.
|
|
12
|
+
*/
|
|
13
|
+
export function noopAuditStore() {
|
|
14
|
+
return {
|
|
15
|
+
async commitAudit(_records) {
|
|
16
|
+
// No-op.
|
|
17
|
+
},
|
|
18
|
+
async commitErrors(_errors) {
|
|
19
|
+
// No-op.
|
|
20
|
+
},
|
|
21
|
+
async loadAudit(_sessionId) {
|
|
22
|
+
return [];
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AuthorizeFn } from "../env.js";
|
|
2
|
+
/**
|
|
3
|
+
* Construct a permissive AuthorizeFn that allows every call. The
|
|
4
|
+
* returned function ignores its arguments (including the third
|
|
5
|
+
* per-call context parameter) and returns the same shape the
|
|
6
|
+
* production authz extension expects.
|
|
7
|
+
*/
|
|
8
|
+
export declare function permissiveAuthorize(): AuthorizeFn;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Permissive AuthorizeFn for tests and examples.
|
|
2
|
+
//
|
|
3
|
+
// Returns { effect: "allow" } for every call. Useful when the agent is
|
|
4
|
+
// exercised without grants -- the test cares about the agent surface
|
|
5
|
+
// rather than the authz decision. Production callers must supply a real
|
|
6
|
+
// authorize function tied to actual policy.
|
|
7
|
+
/**
|
|
8
|
+
* Construct a permissive AuthorizeFn that allows every call. The
|
|
9
|
+
* returned function ignores its arguments (including the third
|
|
10
|
+
* per-call context parameter) and returns the same shape the
|
|
11
|
+
* production authz extension expects.
|
|
12
|
+
*/
|
|
13
|
+
export function permissiveAuthorize() {
|
|
14
|
+
return async (_resource, _action, _context) => ({
|
|
15
|
+
effect: "allow",
|
|
16
|
+
matchingGrants: [],
|
|
17
|
+
resolvedBy: null,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @intx/agent/testing -- no-op implementations of the env contract's
|
|
2
|
+
// required fields, for tests and examples.
|
|
3
|
+
//
|
|
4
|
+
// The exports here silently permit every authz decision and discard
|
|
5
|
+
// every audit record. They exist so test fixtures, the in-tree
|
|
6
|
+
// examples, and short-lived demos can satisfy `BaseEnv` without
|
|
7
|
+
// bringing in a real audit store or policy engine. Production
|
|
8
|
+
// deployments replace these with a real `AuditStore` (durably
|
|
9
|
+
// recording audit and error events) and a real `authorize` callback
|
|
10
|
+
// (gating tool calls per the deployment's policy). Importing from
|
|
11
|
+
// this subpath in production silently disables auditing and allows
|
|
12
|
+
// every tool call, which is almost never what a production caller
|
|
13
|
+
// actually wants -- treat the subpath the same way you would treat a
|
|
14
|
+
// hard-coded `() => true` permission check elsewhere in the
|
|
15
|
+
// codebase.
|
|
16
|
+
export { noopAuditStore } from "./audit-noop.js";
|
|
17
|
+
export { permissiveAuthorize } from "./authorize-allow.js";
|
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { GrantEffect } from "@intx/types";
|
|
2
|
+
import type { ToolCall, ToolDefinition, ToolResult, ToolRunner } from "@intx/types/runtime";
|
|
3
|
+
import type { BaseEnv } from "./env.js";
|
|
4
|
+
export type ToolHandler = (call: ToolCall, signal: AbortSignal) => Promise<ToolResult>;
|
|
5
|
+
export type StringToolHandler = (args: Record<string, unknown>, signal: AbortSignal) => Promise<string>;
|
|
6
|
+
export type AgentTool = {
|
|
7
|
+
kind: "full";
|
|
8
|
+
definition: ToolDefinition;
|
|
9
|
+
handler: ToolHandler;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "string";
|
|
12
|
+
definition: ToolDefinition;
|
|
13
|
+
handler: StringToolHandler;
|
|
14
|
+
};
|
|
15
|
+
export declare function tool(args: {
|
|
16
|
+
definition: ToolDefinition;
|
|
17
|
+
handler: ToolHandler;
|
|
18
|
+
}): AgentTool;
|
|
19
|
+
export declare function stringTool(args: {
|
|
20
|
+
definition: ToolDefinition;
|
|
21
|
+
handler: StringToolHandler;
|
|
22
|
+
}): AgentTool;
|
|
23
|
+
/**
|
|
24
|
+
* Adapt a pre-built ToolRunner (e.g. the one returned by
|
|
25
|
+
* `createPosixTools`) into a list of AgentTools that can be passed to
|
|
26
|
+
* `createAgent({ tools })`. Each definition becomes a full-handler
|
|
27
|
+
* AgentTool that delegates to the runner's `run`.
|
|
28
|
+
*
|
|
29
|
+
* Use this when integrating tool packages whose public surface is a
|
|
30
|
+
* single ToolRunner rather than individual handlers.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fromToolRunner(runner: {
|
|
33
|
+
readonly definitions: readonly ToolDefinition[];
|
|
34
|
+
run: ToolRunner["run"];
|
|
35
|
+
}): AgentTool[];
|
|
36
|
+
export declare class DuplicateToolError extends Error {
|
|
37
|
+
readonly toolName: string;
|
|
38
|
+
constructor(toolName: string);
|
|
39
|
+
}
|
|
40
|
+
export type AgentToolRunner = ToolRunner & {
|
|
41
|
+
readonly definitions: readonly ToolDefinition[];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A bundle of tools constructed by an `AnnotatedToolFactory`. Exposes
|
|
45
|
+
* the set of tool definitions the model sees, a single dispatcher
|
|
46
|
+
* (`run`), and an optional disposer the caller invokes after the agent
|
|
47
|
+
* closes.
|
|
48
|
+
*
|
|
49
|
+
* Disposer ownership lives with the caller -- the env is the agent's
|
|
50
|
+
* dependency contract; the caller owns the lifetime of what it puts in
|
|
51
|
+
* env. The agent does not invoke `dispose` itself.
|
|
52
|
+
*/
|
|
53
|
+
export interface ToolBundle {
|
|
54
|
+
readonly definitions: readonly ToolDefinition[];
|
|
55
|
+
run(call: ToolCall, signal: AbortSignal): Promise<ToolResult>;
|
|
56
|
+
dispose?(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Factory function shape -- consumes an env extending `BaseEnv` and
|
|
60
|
+
* produces a `ToolBundle`. The factory is invoked once per agent
|
|
61
|
+
* instantiation.
|
|
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;
|
|
91
|
+
/**
|
|
92
|
+
* Runtime metadata attached to a `ToolFactory` by `defineTool`. `id` is
|
|
93
|
+
* package-namespaced; `requires` enumerates env keys the factory touches
|
|
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.
|
|
97
|
+
*/
|
|
98
|
+
export interface ToolFactoryMeta {
|
|
99
|
+
readonly id: string;
|
|
100
|
+
readonly requires: readonly string[];
|
|
101
|
+
readonly definitions: readonly ToolDeclaration[];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A tool factory carrying its runtime metadata. `defineTool` is the only
|
|
105
|
+
* sanctioned construction path.
|
|
106
|
+
*
|
|
107
|
+
* The intersection `ToolFactory<EnvReq> & ToolFactoryMeta` is the
|
|
108
|
+
* type-level surface; at runtime the meta fields are attached to the
|
|
109
|
+
* factory function via `Object.assign`.
|
|
110
|
+
*/
|
|
111
|
+
export type AnnotatedToolFactory<EnvReq extends BaseEnv = BaseEnv> = ToolFactory<EnvReq> & ToolFactoryMeta;
|
|
112
|
+
/**
|
|
113
|
+
* Define a tool bundle factory.
|
|
114
|
+
*
|
|
115
|
+
* - `id` must be package-namespaced ("@vendor/pkg/name" or
|
|
116
|
+
* "pkg/name"). Bare ids throw `Error` at definition time.
|
|
117
|
+
* - `requires` enumerates the env keys this factory touches beyond
|
|
118
|
+
* `BaseEnv`'s six core fields. The runtime `validateEnv` checks
|
|
119
|
+
* presence; the factory itself may also fail loud at construction
|
|
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.
|
|
124
|
+
* - `factory(env)` returns a `ToolBundle`. Invoked once per agent
|
|
125
|
+
* instantiation; the bundle's lifetime is tied to that agent.
|
|
126
|
+
*
|
|
127
|
+
* The returned object is the same callable as the supplied `factory`
|
|
128
|
+
* with `id`, a frozen `requires` array, and a frozen `definitions`
|
|
129
|
+
* array attached.
|
|
130
|
+
*/
|
|
131
|
+
export declare function defineTool<EnvReq extends BaseEnv = BaseEnv>(opts: {
|
|
132
|
+
id: string;
|
|
133
|
+
requires?: readonly string[];
|
|
134
|
+
definitions: readonly ToolDeclaration[];
|
|
135
|
+
factory: ToolFactory<EnvReq>;
|
|
136
|
+
}): AnnotatedToolFactory<EnvReq>;
|
|
137
|
+
/**
|
|
138
|
+
* A plugin contributes capabilities (extra tools, middleware, anything
|
|
139
|
+
* a host plugin protocol defines) without producing a `ToolBundle`
|
|
140
|
+
* itself. Plugins are first-class entries in an `interchange.tools`
|
|
141
|
+
* module alongside `AnnotatedToolFactory` exports.
|
|
142
|
+
*
|
|
143
|
+
* The shape the factory returns is host-defined: tool packages that
|
|
144
|
+
* accept plugins read `env.plugins` and dispatch by structural shape
|
|
145
|
+
* (or by an explicit kind marker the host agrees on). The agent
|
|
146
|
+
* runtime does not interpret plugin shapes; it only delivers them.
|
|
147
|
+
*
|
|
148
|
+
* The marker is a `Symbol.for`-registered key (PLUGIN_MARKER) so a
|
|
149
|
+
* duck-typed loader can separate plugins from `AnnotatedToolFactory`s
|
|
150
|
+
* without re-running `defineTool`/`definePlugin` against each export.
|
|
151
|
+
* Using a registered symbol (rather than a string key like `_plugin`)
|
|
152
|
+
* prevents third-party objects from accidentally satisfying the
|
|
153
|
+
* marker check by happening to have a property of the same name.
|
|
154
|
+
*/
|
|
155
|
+
export type PluginFactory<EnvReq extends BaseEnv, Result> = (env: EnvReq) => Result;
|
|
156
|
+
/**
|
|
157
|
+
* Registered symbol that tags `AnnotatedPluginFactory` values. Exported
|
|
158
|
+
* so consumers that need to introspect plugin factories directly can
|
|
159
|
+
* read the marker without re-registering the key.
|
|
160
|
+
*/
|
|
161
|
+
export declare const PLUGIN_MARKER: unique symbol;
|
|
162
|
+
export interface AnnotatedPluginMeta {
|
|
163
|
+
readonly id: string;
|
|
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[];
|
|
179
|
+
readonly [PLUGIN_MARKER]: true;
|
|
180
|
+
}
|
|
181
|
+
export type AnnotatedPluginFactory<EnvReq extends BaseEnv = BaseEnv, Result = unknown> = PluginFactory<EnvReq, Result> & AnnotatedPluginMeta;
|
|
182
|
+
/**
|
|
183
|
+
* Constant on every plugin instance returned by `definePlugin`. Hosts
|
|
184
|
+
* that need to distinguish plugin instances from arbitrary objects
|
|
185
|
+
* received via `env.plugins` check this field before duck-typing on
|
|
186
|
+
* shape. The string value is the operative form of the contract — the
|
|
187
|
+
* value-side marker exists because the factory-side symbol marker
|
|
188
|
+
* (PLUGIN_MARKER above) is only visible to code that imported it,
|
|
189
|
+
* while the kind string travels through pure-JSON inspection too.
|
|
190
|
+
*/
|
|
191
|
+
export declare const TOOL_PLUGIN_KIND: "tool-plugin";
|
|
192
|
+
/** Type-level form of the kind marker. */
|
|
193
|
+
export type ToolPluginKind = typeof TOOL_PLUGIN_KIND;
|
|
194
|
+
/**
|
|
195
|
+
* Predicate hosts use to confirm a value off `env.plugins` was minted
|
|
196
|
+
* by `definePlugin` rather than happening to satisfy a shape-based
|
|
197
|
+
* check. Returns true iff the value is an object carrying the
|
|
198
|
+
* literal `kind: "tool-plugin"` marker.
|
|
199
|
+
*/
|
|
200
|
+
export declare function isToolPluginInstance(value: unknown): value is Record<string, unknown> & {
|
|
201
|
+
kind: ToolPluginKind;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Define a plugin factory. The plugin's `Result` is host-defined and
|
|
205
|
+
* surfaces in `env.plugins` for the host-side tool factories that
|
|
206
|
+
* consume it. The returned instance is tagged with
|
|
207
|
+
* `kind: "tool-plugin"` so hosts can identify plugin instances
|
|
208
|
+
* structurally without falling back to duck-typing on the result's
|
|
209
|
+
* own shape.
|
|
210
|
+
*
|
|
211
|
+
* `id` must be package-namespaced — same rule as `defineTool` — so
|
|
212
|
+
* audit provenance threads through plugins as cleanly as through
|
|
213
|
+
* tools.
|
|
214
|
+
*/
|
|
215
|
+
export declare function definePlugin<Result extends object, EnvReq extends BaseEnv = BaseEnv>(opts: {
|
|
216
|
+
id: string;
|
|
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[];
|
|
224
|
+
factory: PluginFactory<EnvReq, Result>;
|
|
225
|
+
}): AnnotatedPluginFactory<EnvReq, Result & {
|
|
226
|
+
kind: ToolPluginKind;
|
|
227
|
+
}>;
|
|
228
|
+
/** Type predicate distinguishing plugin factories from tool factories. */
|
|
229
|
+
export declare function isAnnotatedPluginFactory(value: unknown): value is AnnotatedPluginFactory;
|
|
230
|
+
/**
|
|
231
|
+
* Build a `ToolRunner` that dispatches by tool name. Throws
|
|
232
|
+
* `DuplicateToolError` at construction if any two tools share a name.
|
|
233
|
+
*
|
|
234
|
+
* At call time, unknown tool names and exceptions from handlers are
|
|
235
|
+
* converted to `ToolResult { isError: true }` so the contract on
|
|
236
|
+
* `ToolRunner.run` ("must not throw") is upheld.
|
|
237
|
+
*/
|
|
238
|
+
export declare function createToolRunner(tools: AgentTool[]): AgentToolRunner;
|