@retinue/agentkit 0.1.0 → 0.2.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 +59 -277
- package/dist/adapters/embeddings/openai.d.ts +45 -0
- package/dist/adapters/embeddings/openai.js +109 -0
- package/dist/agents/agent.d.ts +22 -1
- package/dist/agents/agent.js +97 -11
- package/dist/agents/engine.d.ts +28 -0
- package/dist/agents/engine.js +194 -8
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/index.js +23 -0
- package/dist/capabilities/runtime.d.ts +8 -0
- package/dist/core/budget.d.ts +55 -0
- package/dist/core/budget.js +56 -0
- package/dist/core/content-parts.d.ts +8 -0
- package/dist/core/events.d.ts +68 -2
- package/dist/core/events.js +2 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/documents/index.d.ts +14 -0
- package/dist/documents/parsers/text.d.ts +16 -0
- package/dist/documents/parsers/text.js +54 -2
- package/dist/entries/guardrails.d.ts +14 -0
- package/dist/entries/guardrails.js +14 -0
- package/dist/entries/knowledge.d.ts +9 -0
- package/dist/entries/knowledge.js +8 -0
- package/dist/graphql/resolvers.d.ts +4 -0
- package/dist/graphql/resolvers.js +6 -0
- package/dist/graphql/schema.d.ts +1 -1
- package/dist/graphql/schema.js +44 -0
- package/dist/guardrails/index.d.ts +115 -0
- package/dist/guardrails/index.js +108 -0
- package/dist/guardrails/moderation.d.ts +53 -0
- package/dist/guardrails/moderation.js +75 -0
- package/dist/guardrails/pii.d.ts +75 -0
- package/dist/guardrails/pii.js +193 -0
- package/dist/knowledge/index.d.ts +1 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/navigate.d.ts +89 -0
- package/dist/knowledge/navigate.js +107 -0
- package/dist/knowledge/retrieval.d.ts +73 -5
- package/dist/knowledge/retrieval.js +82 -28
- package/dist/models/streaming.d.ts +22 -1
- package/dist/models/streaming.js +5 -1
- package/dist/security/checklist.js +9 -0
- package/dist/security/findings.js +18 -9
- package/dist/skills/catalogue.d.ts +49 -0
- package/dist/skills/catalogue.js +61 -0
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +1 -0
- package/dist/telemetry/spans.js +12 -0
- package/dist/toolkit/files.d.ts +125 -0
- package/dist/toolkit/files.js +320 -0
- package/dist/toolkit/index.d.ts +4 -0
- package/dist/toolkit/index.js +2 -0
- package/dist/toolkit/sandbox.d.ts +119 -0
- package/dist/toolkit/sandbox.js +239 -0
- package/dist/toolkit/web.d.ts +13 -0
- package/dist/toolkit/web.js +7 -1
- package/dist/tools/budget.d.ts +28 -0
- package/dist/tools/budget.js +35 -0
- package/dist/tools/credentials.d.ts +57 -0
- package/dist/tools/credentials.js +54 -0
- package/dist/tools/define.d.ts +31 -0
- package/dist/tools/define.js +23 -0
- package/dist/tools/find.d.ts +109 -0
- package/dist/tools/find.js +210 -0
- package/dist/tools/index.d.ts +14 -2
- package/dist/tools/index.js +4 -0
- package/dist/tools/library/fs.d.ts +24 -0
- package/dist/tools/library/fs.js +102 -0
- package/dist/tools/library/index.d.ts +29 -2
- package/dist/tools/library/index.js +40 -0
- package/dist/tools/library/shell.d.ts +45 -0
- package/dist/tools/library/shell.js +70 -0
- package/dist/tools/meta-tools.js +8 -0
- package/dist/tools/registry.d.ts +113 -0
- package/dist/tools/registry.js +180 -4
- package/package.json +5 -1
package/dist/tools/meta-tools.js
CHANGED
|
@@ -26,6 +26,14 @@ const meta = (name, label, description) => ({
|
|
|
26
26
|
});
|
|
27
27
|
export const META_TOOL_DESCRIPTORS = {
|
|
28
28
|
learn_tools: meta("learn_tools", "Learn tools", "Fetch the full input/output schemas for named tools before using them."),
|
|
29
|
+
/**
|
|
30
|
+
* REQ-045 (#204), task #210.
|
|
31
|
+
*
|
|
32
|
+
* The description says "not all of them are listed", and that sentence is doing real work: a model that
|
|
33
|
+
* believes the catalogue it can see is complete will not search, and a budget that dropped fourteen tools
|
|
34
|
+
* would then behave exactly like fourteen tools that do not exist.
|
|
35
|
+
*/
|
|
36
|
+
find_tools: meta("find_tools", "Find tools", "Search for a tool by describing what you need to do. Not all available tools are listed in the catalogue, so search before concluding that something cannot be done. Returns names and descriptions; use learn_tools for a schema."),
|
|
29
37
|
execute_tool: meta("execute_tool", "Execute tool", "Run a tool by name with validated input; authorization is rechecked at execution."),
|
|
30
38
|
load_skill: meta("load_skill", "Load skill", "Load a named skill's instructions into context on demand."),
|
|
31
39
|
ask_questions: meta("ask_questions", "Ask questions", "Ask the user consequential questions that cannot be resolved from context or tools."),
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
* result instead of firing the side effect twice.
|
|
17
17
|
*/
|
|
18
18
|
import type { ExecutionContext } from "../core/context.js";
|
|
19
|
+
import type { PlatformError } from "../core/errors.js";
|
|
19
20
|
import type { BlobRef } from "../core/ids.js";
|
|
20
21
|
import { type AuthorizationPolicy } from "../authorization/index.js";
|
|
21
22
|
import { type IdempotencyStore } from "../idempotency/index.js";
|
|
22
23
|
import type { BlobStore } from "../persistence/index.js";
|
|
24
|
+
import { type TokenBudget } from "../core/budget.js";
|
|
25
|
+
import type { ToolSearch, ToolSearchOutcome } from "./find.js";
|
|
23
26
|
import type { OneTimeApprovalRef, ShadowRecorder, ToolCatalogEntry, ToolDescriptor, ToolProvider, ToolResult } from "./index.js";
|
|
24
27
|
/** Validates a tool input against its (opaque) `inputSchema`. Default duck-types a zod schema. */
|
|
25
28
|
export interface SchemaValidator {
|
|
@@ -33,6 +36,55 @@ export interface SchemaValidator {
|
|
|
33
36
|
}
|
|
34
37
|
/** Default validator: run a zod-like schema's `safeParse`; pass through anything else (tool self-validates). */
|
|
35
38
|
export declare const zodishValidator: SchemaValidator;
|
|
39
|
+
/**
|
|
40
|
+
* `execute_tool({ name, input })` → the call it names. Anything else passes through untouched.
|
|
41
|
+
*
|
|
42
|
+
* Refuses to target another meta-tool: `execute_tool` calling itself is an unbounded recursion a model can start
|
|
43
|
+
* with one call, and the others have their own entry points. One level of indirection is the feature; a stack of
|
|
44
|
+
* it is a way to hide what a call actually was from every log that records the outer name.
|
|
45
|
+
*/
|
|
46
|
+
export type ExecuteToolRequest = {
|
|
47
|
+
name: string;
|
|
48
|
+
input: unknown;
|
|
49
|
+
idempotencyKey?: string;
|
|
50
|
+
toolCallId?: string;
|
|
51
|
+
approval?: OneTimeApprovalRef;
|
|
52
|
+
};
|
|
53
|
+
export declare const unwrapExecuteTool: (request: ExecuteToolRequest) => ExecuteToolRequest | {
|
|
54
|
+
readonly error: PlatformError;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* What a tenant has switched on — REQ-045 (#204), task #210, AC-4.
|
|
58
|
+
*
|
|
59
|
+
* Authorization answers *may this principal use this tool*. Nothing answered *does this tenant want it at all*,
|
|
60
|
+
* and without the second question a catalogue is only ever as small as its largest customer: every tenant pays
|
|
61
|
+
* the context cost of every integration anybody wired.
|
|
62
|
+
*
|
|
63
|
+
* Categories rather than names, deliberately. A tenant switching off `communication` should not have to name
|
|
64
|
+
* five Slack tools and then miss the sixth when it ships.
|
|
65
|
+
*/
|
|
66
|
+
export type TenantToolset = {
|
|
67
|
+
/** An allow-list. Present means *only* these categories, which is the safer shape for a tenant opting in. */
|
|
68
|
+
readonly enabledCategories?: readonly string[];
|
|
69
|
+
/** A deny-list, applied after any allow-list. */
|
|
70
|
+
readonly disabledCategories?: readonly string[];
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Resolves a tenant's toolset. A port, because where this lives is a deployment's decision — a column, a
|
|
74
|
+
* settings service, a static map.
|
|
75
|
+
*/
|
|
76
|
+
export interface ToolsetResolver {
|
|
77
|
+
resolve(context: ExecutionContext): Promise<TenantToolset>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Categories a tenant may not switch off.
|
|
81
|
+
*
|
|
82
|
+
* `meta` is the model's route back to everything else. A tenant that disabled it would have an agent that cannot
|
|
83
|
+
* learn a schema or search the catalogue — which is not a smaller toolset, it is a broken one.
|
|
84
|
+
*/
|
|
85
|
+
export declare const UNDISABLEABLE_CATEGORIES: readonly string[];
|
|
86
|
+
/** Whether a category survives a tenant's toolset. Exported because the filtering is worth testing directly. */
|
|
87
|
+
export declare const categoryEnabled: (toolset: TenantToolset, category: string) => boolean;
|
|
36
88
|
export type ToolPolicyView = {
|
|
37
89
|
readonly preloaded: readonly string[];
|
|
38
90
|
readonly categories: readonly string[];
|
|
@@ -45,6 +97,28 @@ export type ToolCatalog = {
|
|
|
45
97
|
readonly discoverable: readonly ToolCatalogEntry[];
|
|
46
98
|
/** Always-present meta-tools. */
|
|
47
99
|
readonly meta: readonly ToolCatalogEntry[];
|
|
100
|
+
/**
|
|
101
|
+
* Present only when a budget bound — REQ-045 (#204), task #210, AC-3.
|
|
102
|
+
*
|
|
103
|
+
* On the catalogue as well as in the run event log, because the two have different readers: the event is for
|
|
104
|
+
* whoever reviews the run afterwards, and this is for the client rendering the catalogue *now*. A UI showing
|
|
105
|
+
* a shortened list with no indication it was shortened is the same invisible failure in a different place.
|
|
106
|
+
*/
|
|
107
|
+
readonly truncation?: {
|
|
108
|
+
readonly budgetTokens: number;
|
|
109
|
+
readonly residentTokens: number;
|
|
110
|
+
readonly dropped: readonly string[];
|
|
111
|
+
readonly findable: boolean;
|
|
112
|
+
readonly overBudget: boolean;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The tenant's toolset as it was applied — AC-4's "visible in the capability declaration".
|
|
116
|
+
*
|
|
117
|
+
* This catalogue *is* the declaration a client reads: it is the only place the platform states what an agent
|
|
118
|
+
* can do. A tenant setting that silently narrowed it, with nothing in the answer saying so, would be
|
|
119
|
+
* indistinguishable from tools that were never built.
|
|
120
|
+
*/
|
|
121
|
+
readonly toolset?: TenantToolset;
|
|
48
122
|
};
|
|
49
123
|
/** Structural approval check (satisfied by the HITL `ApprovalGate`) — kept structural to avoid a
|
|
50
124
|
* tools→hitl dependency. Returns false when the tool needs approval and the call carries neither a
|
|
@@ -127,9 +201,48 @@ export type ToolRegistryConfig = {
|
|
|
127
201
|
* than performed, the same fail-closed rule as the envelope's.
|
|
128
202
|
*/
|
|
129
203
|
readonly shadow?: ShadowRecorder;
|
|
204
|
+
/**
|
|
205
|
+
* Search over the catalogue, which is what makes `find_tools` exist — AC-1.
|
|
206
|
+
*
|
|
207
|
+
* Absent means no `find_tools` in the catalogue at all, rather than one that always answers "not configured".
|
|
208
|
+
* Wiring is the toggle, the same rule the tool library already follows for `web_search`.
|
|
209
|
+
*/
|
|
210
|
+
readonly search?: ToolSearch;
|
|
211
|
+
/** A tenant's category switches, applied *before* authorization filtering — AC-4. */
|
|
212
|
+
readonly toolsets?: ToolsetResolver;
|
|
213
|
+
/**
|
|
214
|
+
* A ceiling in tokens on the discoverable catalogue — AC-3.
|
|
215
|
+
*
|
|
216
|
+
* Applies to the compact entries only. Preloaded tools are an explicit instruction from the host and are not
|
|
217
|
+
* silently withdrawn; a host that preloads more than its own budget is told so through `overBudget` rather
|
|
218
|
+
* than having its instruction quietly reversed.
|
|
219
|
+
*/
|
|
220
|
+
readonly catalogBudget?: TokenBudget;
|
|
130
221
|
};
|
|
131
222
|
export interface ToolRegistry {
|
|
132
223
|
catalog(context: ExecutionContext, policy: ToolPolicyView): Promise<ToolCatalog>;
|
|
224
|
+
/**
|
|
225
|
+
* Search the catalogue — AC-1.
|
|
226
|
+
*
|
|
227
|
+
* Filtered by the same authorization policy as discovery, which is not a nicety: an unfiltered search is an
|
|
228
|
+
* enumeration oracle. A principal who cannot see `github_merge_pull_request` in the catalogue but can confirm
|
|
229
|
+
* it exists by searching for "merge" has learned what the deployment does, and hiding a tool from discovery
|
|
230
|
+
* while making it findable is worse than not hiding it, because it looks like it was hidden.
|
|
231
|
+
*/
|
|
232
|
+
find(context: ExecutionContext, input: {
|
|
233
|
+
readonly query: string;
|
|
234
|
+
readonly limit?: number;
|
|
235
|
+
}): Promise<ToolSearchOutcome>;
|
|
236
|
+
/**
|
|
237
|
+
* Every tool this caller may use, with schemas — the list `buildTools` should hand a model.
|
|
238
|
+
*
|
|
239
|
+
* Exists because the embedded facade was doing this itself: gathering providers, flattening, and filtering by
|
|
240
|
+
* authorization, in its own copy of the four lines this registry already owns. The copy had no duplicate-name
|
|
241
|
+
* check and no tenant toolset, so a category a tenant had switched off was invisible in the catalogue, absent
|
|
242
|
+
* from `find_tools`, refused at execution — and *still handed to the model*, which would then call it and be
|
|
243
|
+
* refused. One implementation, and this is it.
|
|
244
|
+
*/
|
|
245
|
+
listAuthorized(context: ExecutionContext): Promise<readonly ToolDescriptor[]>;
|
|
133
246
|
learn(context: ExecutionContext, names: readonly string[]): Promise<readonly ToolDescriptor[]>;
|
|
134
247
|
execute(context: ExecutionContext, input: {
|
|
135
248
|
name: string;
|
package/dist/tools/registry.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import { assertToolAuthorized } from "../authorization/index.js";
|
|
19
19
|
import { deriveIdempotencyKey } from "../idempotency/index.js";
|
|
20
20
|
import { META_TOOL_DESCRIPTOR_LIST } from "./meta-tools.js";
|
|
21
|
+
import { applyTokenBudget } from "../core/budget.js";
|
|
22
|
+
import { entryTokens } from "./budget.js";
|
|
21
23
|
/** Default validator: run a zod-like schema's `safeParse`; pass through anything else (tool self-validates). */
|
|
22
24
|
export const zodishValidator = {
|
|
23
25
|
validate(schema, value) {
|
|
@@ -39,6 +41,26 @@ const compact = (d) => ({
|
|
|
39
41
|
effect: d.effect,
|
|
40
42
|
});
|
|
41
43
|
const invalidInput = (message) => ({ code: "invalid_input", message, retryable: false });
|
|
44
|
+
/** The registry's own meta-tools, which a model may not target through `execute_tool`. */
|
|
45
|
+
const META_ONLY = new Set(["execute_tool", "find_tools", "learn_tools", "read_tool_output"]);
|
|
46
|
+
export const unwrapExecuteTool = (request) => {
|
|
47
|
+
if (request.name !== "execute_tool")
|
|
48
|
+
return request;
|
|
49
|
+
const asked = (request.input ?? {});
|
|
50
|
+
if (typeof asked.name !== "string" || asked.name.trim() === "")
|
|
51
|
+
return { error: invalidInput("execute_tool needs the name of the tool to run.") };
|
|
52
|
+
if (META_ONLY.has(asked.name))
|
|
53
|
+
return { error: invalidInput(`execute_tool cannot call ${asked.name}; call it directly.`) };
|
|
54
|
+
return {
|
|
55
|
+
name: asked.name,
|
|
56
|
+
input: asked.input,
|
|
57
|
+
...(typeof asked.idempotencyKey === "string" ? { idempotencyKey: asked.idempotencyKey } : {}),
|
|
58
|
+
// The *outer* call's identity is kept: the tool call the model made is the one the run event log records,
|
|
59
|
+
// and rewriting it here would make a transcript disagree with the model's own history.
|
|
60
|
+
...(request.toolCallId === undefined ? {} : { toolCallId: request.toolCallId }),
|
|
61
|
+
...(request.approval === undefined ? {} : { approval: request.approval }),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
42
64
|
/** A wiring problem, not a caller problem: retrying the identical call cannot help. */
|
|
43
65
|
const capabilityUnavailable = (message) => ({
|
|
44
66
|
code: "capability_unavailable",
|
|
@@ -46,6 +68,21 @@ const capabilityUnavailable = (message) => ({
|
|
|
46
68
|
retryable: false,
|
|
47
69
|
});
|
|
48
70
|
const requiresKey = (effect, requires) => requires || effect === "external-write" || effect === "destructive";
|
|
71
|
+
/**
|
|
72
|
+
* Categories a tenant may not switch off.
|
|
73
|
+
*
|
|
74
|
+
* `meta` is the model's route back to everything else. A tenant that disabled it would have an agent that cannot
|
|
75
|
+
* learn a schema or search the catalogue — which is not a smaller toolset, it is a broken one.
|
|
76
|
+
*/
|
|
77
|
+
export const UNDISABLEABLE_CATEGORIES = ["meta"];
|
|
78
|
+
/** Whether a category survives a tenant's toolset. Exported because the filtering is worth testing directly. */
|
|
79
|
+
export const categoryEnabled = (toolset, category) => {
|
|
80
|
+
if (UNDISABLEABLE_CATEGORIES.includes(category))
|
|
81
|
+
return true;
|
|
82
|
+
if (toolset.enabledCategories !== undefined && !toolset.enabledCategories.includes(category))
|
|
83
|
+
return false;
|
|
84
|
+
return !(toolset.disabledCategories ?? []).includes(category);
|
|
85
|
+
};
|
|
49
86
|
export const createToolRegistry = (config) => {
|
|
50
87
|
const maxInline = config.maxInlineOutputBytes ?? 8 * 1024;
|
|
51
88
|
const validator = config.validator ?? zodishValidator;
|
|
@@ -110,13 +147,38 @@ export const createToolRegistry = (config) => {
|
|
|
110
147
|
});
|
|
111
148
|
}
|
|
112
149
|
}
|
|
113
|
-
|
|
150
|
+
/**
|
|
151
|
+
* The tenant's toolset, applied **before** authorization — AC-4.
|
|
152
|
+
*
|
|
153
|
+
* Order matters and this is the order the AC asks for. A tool a tenant switched off is not a tool the
|
|
154
|
+
* principal is unauthorized for: it does not exist for that tenant, so it must not reach the authorization
|
|
155
|
+
* policy, must not appear in a policy's audit of what it filtered, and must not be findable.
|
|
156
|
+
*/
|
|
157
|
+
const toolset = config.toolsets === undefined ? undefined : await config.toolsets.resolve(context);
|
|
158
|
+
const wanted = toolset === undefined
|
|
159
|
+
? all.filter((t) => !duplicated.has(t.descriptor.name))
|
|
160
|
+
: all.filter((t) => !duplicated.has(t.descriptor.name) && categoryEnabled(toolset, t.descriptor.category));
|
|
161
|
+
const usable = wanted;
|
|
114
162
|
const descriptors = usable.map((t) => t.descriptor);
|
|
115
163
|
const permitted = new Set((await config.authorization.filterTools(context, descriptors)).map((d) => d.name));
|
|
116
164
|
return usable.filter((t) => permitted.has(t.descriptor.name));
|
|
117
165
|
};
|
|
118
166
|
const findAuthorized = async (context, name) => (await authorizedTools(context)).find((t) => t.descriptor.name === name) ?? null;
|
|
119
|
-
|
|
167
|
+
/**
|
|
168
|
+
* One implementation, reached two ways: `registry.find` for a host, and `execute("find_tools")` for a model.
|
|
169
|
+
*
|
|
170
|
+
* `find_tools` is not authorized as a tool in its own right, and that is deliberate: like every other
|
|
171
|
+
* meta-tool it is part of the interface rather than a capability a role grants. What *is* authorized is
|
|
172
|
+
* everything it can return — the corpus is the caller's own authorized tool list — so the worst a principal
|
|
173
|
+
* with no tools can learn from it is that they have none.
|
|
174
|
+
*/
|
|
175
|
+
const runFind = async (context, input) => {
|
|
176
|
+
if (config.search === undefined)
|
|
177
|
+
return { hits: [], modes: [] };
|
|
178
|
+
const tools = (await authorizedTools(context)).map((t) => t.descriptor);
|
|
179
|
+
return config.search.search({ query: input.query, tools, limit: input.limit ?? 10 });
|
|
180
|
+
};
|
|
181
|
+
const api = {
|
|
120
182
|
async catalog(context, policy) {
|
|
121
183
|
const excluded = new Set(policy.excluded);
|
|
122
184
|
const preloadNames = new Set(policy.preloaded);
|
|
@@ -131,14 +193,127 @@ export const createToolRegistry = (config) => {
|
|
|
131
193
|
else
|
|
132
194
|
discoverable.push(compact(d));
|
|
133
195
|
}
|
|
134
|
-
|
|
196
|
+
/**
|
|
197
|
+
* `find_tools` is advertised only when a search is wired.
|
|
198
|
+
*
|
|
199
|
+
* The alternative — a permanent descriptor that fails at execution — costs the model a call to discover
|
|
200
|
+
* and reads in a transcript exactly like a broken platform.
|
|
201
|
+
*/
|
|
202
|
+
const meta = META_TOOL_DESCRIPTOR_LIST.filter((d) => d.name !== "find_tools" || config.search !== undefined).map(compact);
|
|
203
|
+
const toolset = config.toolsets === undefined ? undefined : await config.toolsets.resolve(context);
|
|
204
|
+
if (config.catalogBudget === undefined)
|
|
205
|
+
return {
|
|
206
|
+
preloaded,
|
|
207
|
+
discoverable,
|
|
208
|
+
meta,
|
|
209
|
+
...(toolset === undefined ? {} : { toolset }),
|
|
210
|
+
};
|
|
211
|
+
// Preloaded entries and the meta-tools are charged against the budget but never dropped: they are the
|
|
212
|
+
// host's own instruction and the model's route back to what was withheld.
|
|
213
|
+
const fixed = preloaded.reduce((total, d) => total + entryTokens(compact(d)), 0) +
|
|
214
|
+
meta.reduce((total, entry) => total + entryTokens(entry), 0);
|
|
215
|
+
const outcome = applyTokenBudget({
|
|
216
|
+
items: discoverable,
|
|
217
|
+
budget: { maxTokens: Math.max(0, config.catalogBudget.maxTokens - fixed) },
|
|
218
|
+
tokensOf: entryTokens,
|
|
219
|
+
nameOf: (entry) => entry.name,
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
preloaded,
|
|
223
|
+
discoverable: outcome.resident,
|
|
224
|
+
meta,
|
|
225
|
+
...(toolset === undefined ? {} : { toolset }),
|
|
226
|
+
...(outcome.dropped.length === 0 && !outcome.overBudget
|
|
227
|
+
? {}
|
|
228
|
+
: {
|
|
229
|
+
truncation: {
|
|
230
|
+
budgetTokens: config.catalogBudget.maxTokens,
|
|
231
|
+
residentTokens: outcome.residentTokens + fixed,
|
|
232
|
+
dropped: outcome.dropped,
|
|
233
|
+
findable: config.search !== undefined,
|
|
234
|
+
overBudget: outcome.residentTokens + fixed > config.catalogBudget.maxTokens,
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
find: runFind,
|
|
240
|
+
async listAuthorized(context) {
|
|
241
|
+
return (await authorizedTools(context)).map((t) => t.descriptor);
|
|
135
242
|
},
|
|
136
243
|
async learn(context, names) {
|
|
137
244
|
// Only authorized tools are returned — an unauthorized name is silently unlearnable.
|
|
138
245
|
const wanted = new Set(names);
|
|
139
246
|
return (await authorizedTools(context)).map((t) => t.descriptor).filter((d) => wanted.has(d.name));
|
|
140
247
|
},
|
|
141
|
-
async execute(context,
|
|
248
|
+
async execute(context, outer) {
|
|
249
|
+
/**
|
|
250
|
+
* `execute_tool`, unwrapped before anything else — task #210.
|
|
251
|
+
*
|
|
252
|
+
* Without this, `find_tools` finds a tool the model **cannot call**: a truncated tool list means the name
|
|
253
|
+
* it just learned is not in its own tool set, so search would return an answer and leave the model stuck.
|
|
254
|
+
* That is the difference between a deferral and an amputation, and it was missing — `execute_tool` has been
|
|
255
|
+
* in `META_TOOLS` since the registry was written and nothing implemented it.
|
|
256
|
+
*
|
|
257
|
+
* Unwrapping rather than dispatching: the inner call then goes through *every* check below — authorization,
|
|
258
|
+
* the toolset, the approval gate, validation, idempotency, the shadow recorder — because it is the same
|
|
259
|
+
* code path. A separate branch that called the tool directly would be a way around all of them, reachable
|
|
260
|
+
* by name from a model.
|
|
261
|
+
*/
|
|
262
|
+
if (outer.name === "execute_tool") {
|
|
263
|
+
const unwrapped = unwrapExecuteTool(outer);
|
|
264
|
+
if ("error" in unwrapped)
|
|
265
|
+
return { ok: false, error: unwrapped.error };
|
|
266
|
+
/**
|
|
267
|
+
* Re-entered through the public surface, and tagged once on the way out.
|
|
268
|
+
*
|
|
269
|
+
* Re-entering rather than falling through with a rewritten argument: the inner call then passes every
|
|
270
|
+
* check below exactly as a direct call would — authorization, the tenant's toolset, the approval gate,
|
|
271
|
+
* validation, idempotency, the shadow recorder — because it *is* a direct call. A fall-through would work
|
|
272
|
+
* today and become a bypass the first time somebody adds a check above this line.
|
|
273
|
+
*
|
|
274
|
+
* `ranToolName` is attached here, in the one place the indirection is known. The audit trail's question
|
|
275
|
+
* is "what was done", and `execute_tool` is not an answer to it.
|
|
276
|
+
*/
|
|
277
|
+
const inner = await api.execute(context, unwrapped);
|
|
278
|
+
return { ...inner, ranToolName: unwrapped.name };
|
|
279
|
+
}
|
|
280
|
+
const input = outer;
|
|
281
|
+
/**
|
|
282
|
+
* `learn_tools`, handled here — task #210, and the leg that was missing.
|
|
283
|
+
*
|
|
284
|
+
* `find_tools` returns names and descriptions. A model that then calls the tool through `execute_tool` has
|
|
285
|
+
* to guess its arguments, and in the 200-tool measurement it did exactly that: searched, found the right
|
|
286
|
+
* tool, and called it wrongly or not at all. Search without schemas is a dead end, and the descriptor for
|
|
287
|
+
* `learn_tools` had been in `META_TOOLS` since the registry was written with nothing implementing it.
|
|
288
|
+
*/
|
|
289
|
+
if (input.name === "learn_tools") {
|
|
290
|
+
const asked = (input.input ?? {});
|
|
291
|
+
const names = Array.isArray(asked.names) ? asked.names.filter((n) => typeof n === "string") : [];
|
|
292
|
+
if (names.length === 0)
|
|
293
|
+
return { ok: false, error: invalidInput("learn_tools needs `names`: the tools whose schemas you want.") };
|
|
294
|
+
// Authorized only, like discovery and like search — an unauthorized name is silently unlearnable.
|
|
295
|
+
return { ok: true, data: { tools: await api.learn(context, names) } };
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* `find_tools`, handled here rather than by a provider — AC-1.
|
|
299
|
+
*
|
|
300
|
+
* It has to be the registry: the corpus *is* the registry's authorized tool list, and a provider-supplied
|
|
301
|
+
* search tool would either need the registry passed into it (a construction cycle) or its own idea of what
|
|
302
|
+
* exists, which is the second implementation AC-2 forbids.
|
|
303
|
+
*/
|
|
304
|
+
if (input.name === "find_tools") {
|
|
305
|
+
if (config.search === undefined)
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
error: capabilityUnavailable("find_tools is not available: no tool search is configured (see ToolRegistryConfig.search)."),
|
|
309
|
+
};
|
|
310
|
+
const asked = (input.input ?? {});
|
|
311
|
+
if (typeof asked.query !== "string" || asked.query.trim() === "")
|
|
312
|
+
return { ok: false, error: invalidInput("find_tools needs a query describing what you are trying to do.") };
|
|
313
|
+
const limit = typeof asked.limit === "number" && asked.limit > 0 ? Math.min(Math.floor(asked.limit), 25) : 10;
|
|
314
|
+
const outcome = await runFind(context, { query: asked.query, limit });
|
|
315
|
+
return { ok: true, data: outcome };
|
|
316
|
+
}
|
|
142
317
|
const tool = await findAuthorized(context, input.name);
|
|
143
318
|
// Not found OR not authorized → both reject; execution is never a way around discovery filtering.
|
|
144
319
|
if (!tool) {
|
|
@@ -272,6 +447,7 @@ export const createToolRegistry = (config) => {
|
|
|
272
447
|
return { ok: true, data: value };
|
|
273
448
|
},
|
|
274
449
|
};
|
|
450
|
+
return api;
|
|
275
451
|
/** Spill an oversize success payload to blob storage and reference it. */
|
|
276
452
|
async function maybeSpill(context, result) {
|
|
277
453
|
if (!result.ok || result.spilledOutputRef !== undefined)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retinue/agentkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A provider-neutral, durable AI agent runtime for TypeScript: agents, tools, approvals, context, knowledge and persistence behind ports.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
"types": "./dist/entries/hitl.d.ts",
|
|
40
40
|
"default": "./dist/entries/hitl.js"
|
|
41
41
|
},
|
|
42
|
+
"./guardrails": {
|
|
43
|
+
"types": "./dist/entries/guardrails.d.ts",
|
|
44
|
+
"default": "./dist/entries/guardrails.js"
|
|
45
|
+
},
|
|
42
46
|
"./usage": {
|
|
43
47
|
"types": "./dist/entries/usage.d.ts",
|
|
44
48
|
"default": "./dist/entries/usage.js"
|