@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.
Files changed (77) hide show
  1. package/README.md +59 -277
  2. package/dist/adapters/embeddings/openai.d.ts +45 -0
  3. package/dist/adapters/embeddings/openai.js +109 -0
  4. package/dist/agents/agent.d.ts +22 -1
  5. package/dist/agents/agent.js +97 -11
  6. package/dist/agents/engine.d.ts +28 -0
  7. package/dist/agents/engine.js +194 -8
  8. package/dist/capabilities/index.d.ts +5 -1
  9. package/dist/capabilities/index.js +23 -0
  10. package/dist/capabilities/runtime.d.ts +8 -0
  11. package/dist/core/budget.d.ts +55 -0
  12. package/dist/core/budget.js +56 -0
  13. package/dist/core/content-parts.d.ts +8 -0
  14. package/dist/core/events.d.ts +68 -2
  15. package/dist/core/events.js +2 -0
  16. package/dist/core/index.d.ts +1 -0
  17. package/dist/core/index.js +1 -0
  18. package/dist/documents/index.d.ts +14 -0
  19. package/dist/documents/parsers/text.d.ts +16 -0
  20. package/dist/documents/parsers/text.js +54 -2
  21. package/dist/entries/guardrails.d.ts +14 -0
  22. package/dist/entries/guardrails.js +14 -0
  23. package/dist/entries/knowledge.d.ts +9 -0
  24. package/dist/entries/knowledge.js +8 -0
  25. package/dist/graphql/resolvers.d.ts +4 -0
  26. package/dist/graphql/resolvers.js +6 -0
  27. package/dist/graphql/schema.d.ts +1 -1
  28. package/dist/graphql/schema.js +44 -0
  29. package/dist/guardrails/index.d.ts +115 -0
  30. package/dist/guardrails/index.js +108 -0
  31. package/dist/guardrails/moderation.d.ts +53 -0
  32. package/dist/guardrails/moderation.js +75 -0
  33. package/dist/guardrails/pii.d.ts +75 -0
  34. package/dist/guardrails/pii.js +193 -0
  35. package/dist/knowledge/index.d.ts +1 -0
  36. package/dist/knowledge/index.js +1 -0
  37. package/dist/knowledge/navigate.d.ts +89 -0
  38. package/dist/knowledge/navigate.js +107 -0
  39. package/dist/knowledge/retrieval.d.ts +73 -5
  40. package/dist/knowledge/retrieval.js +82 -28
  41. package/dist/models/streaming.d.ts +22 -1
  42. package/dist/models/streaming.js +5 -1
  43. package/dist/security/checklist.js +9 -0
  44. package/dist/security/findings.js +18 -9
  45. package/dist/skills/catalogue.d.ts +49 -0
  46. package/dist/skills/catalogue.js +61 -0
  47. package/dist/skills/index.d.ts +1 -0
  48. package/dist/skills/index.js +1 -0
  49. package/dist/telemetry/spans.js +12 -0
  50. package/dist/toolkit/files.d.ts +125 -0
  51. package/dist/toolkit/files.js +320 -0
  52. package/dist/toolkit/index.d.ts +4 -0
  53. package/dist/toolkit/index.js +2 -0
  54. package/dist/toolkit/sandbox.d.ts +119 -0
  55. package/dist/toolkit/sandbox.js +239 -0
  56. package/dist/toolkit/web.d.ts +13 -0
  57. package/dist/toolkit/web.js +7 -1
  58. package/dist/tools/budget.d.ts +28 -0
  59. package/dist/tools/budget.js +35 -0
  60. package/dist/tools/credentials.d.ts +57 -0
  61. package/dist/tools/credentials.js +54 -0
  62. package/dist/tools/define.d.ts +31 -0
  63. package/dist/tools/define.js +23 -0
  64. package/dist/tools/find.d.ts +109 -0
  65. package/dist/tools/find.js +210 -0
  66. package/dist/tools/index.d.ts +14 -2
  67. package/dist/tools/index.js +4 -0
  68. package/dist/tools/library/fs.d.ts +24 -0
  69. package/dist/tools/library/fs.js +102 -0
  70. package/dist/tools/library/index.d.ts +29 -2
  71. package/dist/tools/library/index.js +40 -0
  72. package/dist/tools/library/shell.d.ts +45 -0
  73. package/dist/tools/library/shell.js +70 -0
  74. package/dist/tools/meta-tools.js +8 -0
  75. package/dist/tools/registry.d.ts +113 -0
  76. package/dist/tools/registry.js +180 -4
  77. package/package.json +5 -1
@@ -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."),
@@ -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;
@@ -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
- const usable = all.filter((t) => !duplicated.has(t.descriptor.name));
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
- return {
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
- return { preloaded, discoverable, meta: META_TOOL_DESCRIPTOR_LIST.map(compact) };
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, input) {
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.1.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"