@collegium/sdk 0.0.1-beta.7 → 0.0.1-beta.9

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
@@ -38,10 +38,10 @@ export default defineTool({
38
38
  approval: (args) => ({ body: `save contact "${args.id}": ${args.name}`, presentation: 'verbatim' }),
39
39
  description: 'Save or update a contact.',
40
40
  execute: async (args, { err, settings, storage }) => {
41
- if ((await storage.contacts.list()).length >= settings.maxContacts) {
41
+ if ((await storage.contacts.findMany()).length >= settings.maxContacts) {
42
42
  err.invalidArguments('contact limit reached; delete one first');
43
43
  }
44
- await storage.contacts.put(args.id, { email: args.email, name: args.name });
44
+ await storage.contacts.create({ email: args.email, id: args.id, name: args.name });
45
45
  return `contact ${args.id} saved`;
46
46
  },
47
47
  parameters: z.object({ email: z.email(), id: z.string().min(1), name: z.string().min(1) })
@@ -50,6 +50,20 @@ export default defineTool({
50
50
 
51
51
  A tool with `approval` always stops for a human, who sees the full payload before it runs; one without never gates. The channel and the trace disclose both, line by line. `execute` returns the text the model reads, and raises the two failures a tool controls through `err`: `invalidArguments` continues the turn, `unresolved` ends it as an unconfirmed side effect.
52
52
 
53
+ **Storage.** Each declared collection is a set of records: the schema's output plus `id`, `createdAt`, and `updatedAt`, which the store stamps. The handle has `create`, `findMany`, `findById`, `updateById`, and `deleteById`. `create` takes the schema's input with an optional `id`, minting a cuid2 when none is given. `findMany` with no argument lists everything; with a `where` over the schema's top-level scalar fields and `id` — a value for equality, `{ in: [...] }` for membership, `{ contains: text }` for a case-insensitive substring on a string — and an optional `limit`, it filters. Field names and value types come from the schema, so a bad query does not compile.
54
+
55
+ **Testing.** `@collegium/sdk/testing` builds the context `execute` receives, over in-memory storage that validates and parses as the deployment's store does. Pass your config; settings go through your schema, so defaults apply.
56
+
57
+ ```ts
58
+ import { createTestContext, PluginToolFailureError } from '@collegium/sdk/testing';
59
+
60
+ const context = createTestContext(config, { settings: { maxContacts: 1 } });
61
+ await save.execute({ email: 'ana@example.com', id: 'ana', name: 'Ana' }, context);
62
+ await expect(save.execute({ email: 'ben@example.com', id: 'ben', name: 'Ben' }, context)).rejects.toThrow(
63
+ PluginToolFailureError
64
+ );
65
+ ```
66
+
53
67
  **zod is a peer dependency.** Install it beside the SDK and import it directly. A plugin may import `@collegium/sdk`, `zod`, and `node:` builtins; the compiler refuses every other bare specifier at boot.
54
68
 
55
69
  **Your installed copies are for development.** The deployment compiles a mounted plugin against the SDK and zod its image carries, not the copies in your `node_modules` — those serve your editor, `tsc`, and your tests. Exactly one zod runs in the process.
package/dist/index.d.ts CHANGED
@@ -1,151 +1,2 @@
1
- import { z } from "zod";
2
- import { Promisable } from "type-fest";
3
- //#region src/config.d.ts
4
- type CollectionsDeclaration$1 = {
5
- readonly [key: string]: z.ZodType;
6
- };
7
- /**
8
- * The config file's default export: the settings schema agents are configured by, and the storage
9
- * collections the plugin owns. Its generics are what carry the settings and storage types to every
10
- * tool, through `Register`.
11
- */
12
- type PluginConfig<TSettings extends undefined | z.ZodType = undefined | z.ZodType, TCollections extends CollectionsDeclaration$1 = CollectionsDeclaration$1> = {
13
- readonly settings: TSettings;
14
- readonly storage: TCollections;
15
- };
16
- /**
17
- * The augmentation point. `src/config.ts` declares its config here, and every tool file receives
18
- * the declared settings and storage types without importing the config:
19
- *
20
- * ```ts
21
- * declare module '@collegium/sdk' {
22
- * interface Register { config: typeof config }
23
- * }
24
- * ```
25
- */
26
- interface Register {}
27
- type RegisteredConfig = Register extends {
28
- readonly config: infer TConfig extends PluginConfig;
29
- } ? TConfig : PluginConfig;
30
- declare function defineConfig<TSettings extends undefined | z.ZodType = undefined, const TCollections extends CollectionsDeclaration$1 = {}>(config: {
31
- readonly settings?: TSettings;
32
- readonly storage?: TCollections;
33
- }): PluginConfig<TSettings, TCollections>;
34
- //#endregion
35
- //#region ../core/src/approvals/approvals.types.d.ts
36
- /** how a gated tool's approval payload is presented (§6.2): collapsed behind a control, or verbatim */
37
- type ApprovalPayloadPresentation = 'collapse' | 'verbatim';
38
- //#endregion
39
- //#region ../core/src/utils/token.utils.d.ts
40
- declare const SERVICE_INSTANCE: unique symbol;
41
- /**
42
- * A NestJS-compatible injection token branded with the instance type it resolves to. Declared in a
43
- * leaf `<module>.tokens.ts` beside `import type` of the service alone, which is what keeps a
44
- * toolset declaration inert (§2): the entrypoint and provisioning import one without pulling the
45
- * service's runtime module into their graphs.
46
- */
47
- type ServiceToken<TInstance> = {
48
- readonly [SERVICE_INSTANCE]?: TInstance;
49
- } & symbol;
50
- //#endregion
51
- //#region ../core/src/tools/tools.types.d.ts
52
- /** the four facts of the running turn (§4) — everything else a tool needs, its toolset declares */
53
- type ToolTurnScope = {
54
- readonly agentUsername: string;
55
- readonly channelId: string;
56
- /** provenance for anything a tool records; null on a turn no post triggered */
57
- readonly triggeringPostId: null | string;
58
- readonly turnId: string;
59
- };
60
- /** §6.2 — the full payload the approver reads; presence of the `approval` hook is what gates a tool (§5) */
61
- type ToolApprovalPayload = {
62
- body: string;
63
- presentation: ApprovalPayloadPresentation;
64
- };
65
- /**
66
- * A durable record's disclosure (§3.6), returned by the tool that created it; the turn writes the
67
- * event and the trace lines. `reference` names the record for later reads, e.g. a memory id.
68
- */
69
- type ToolDisclosure = {
70
- readonly body: string;
71
- readonly description: string;
72
- readonly reference: string;
73
- readonly supersededDescriptions?: readonly string[];
74
- };
75
- //#endregion
76
- //#region ../core/src/plugins/plugins.types.d.ts
77
- /** what a plugin tool body may return: the text alone, or the text beside a durable record's disclosure (§3.4) */
78
- type PluginToolOutput = string | {
79
- readonly disclosure?: ToolDisclosure;
80
- readonly text: string;
81
- };
82
- /**
83
- * The two failures a tool body may raise itself — the rest of the taxonomy (§7.1) is the
84
- * framework's to raise. Each throws; the perimeter wrapper maps the throw into the taxonomy.
85
- */
86
- type PluginToolErr = {
87
- /** the arguments were rejected — returned to the model as the tool result; the turn continues */
88
- invalidArguments(message: string): never;
89
- /** a committed side effect whose outcome cannot be established; the turn ends stating the ambiguity */
90
- unresolved(message: string): never;
91
- };
92
- /** one tool as a plugin declares it: the framework's tool minus `budgetExempt`, returning plain output */
93
- type PluginToolDeclaration<TContext, TParams extends z.ZodType> = {
94
- /** present ⇒ the tool always gates (§5); renders the payload the approver reads and cannot decline */
95
- approval?(args: z.infer<TParams>): ToolApprovalPayload;
96
- readonly description: string;
97
- execute(args: z.infer<TParams>, context: TContext): Promisable<PluginToolOutput>;
98
- readonly parameters: TParams;
99
- /** §7.2 — whether a timed-out call may be reported to the model as a plain failure; false ends the turn as unconfirmable */
100
- readonly retryable?: boolean;
101
- readonly timeoutMs?: number;
102
- /** §8.1 — the one-line summary beside the name in the status post; absent shows the name alone */
103
- traceDetail?(args: z.infer<TParams>): string;
104
- };
105
- //#endregion
106
- //#region ../core/src/toolsets/toolsets.types.d.ts
107
- /** `settings`, `storage`, and `turn` are the context's own keys, so a service may not claim them */
108
- type ServicesDeclaration = {
109
- readonly [key: string]: ServiceToken<unknown>;
110
- } & {
111
- readonly settings?: never;
112
- readonly storage?: never;
113
- readonly turn?: never;
114
- };
115
- type CollectionsDeclaration = {
116
- readonly [key: string]: z.ZodType;
117
- };
118
- type EmptyDeclaration = {};
119
- /** a toolset-scoped handle over one declared storage collection; rows are validated on write and parsed on read */
120
- type ToolsetCollection<TValue> = {
121
- delete(key: string): Promise<boolean>;
122
- get(key: string): Promise<null | TValue>;
123
- list(): Promise<{
124
- key: string;
125
- value: TValue;
126
- }[]>;
127
- put(key: string, value: TValue): Promise<void>;
128
- };
129
- /**
130
- * What `execute` receives, assembled from exactly what the toolset declared (§4): each service
131
- * under its own name, `settings` and `storage` only when declared, and always the turn. Reaching
132
- * anything undeclared is a compile error.
133
- */
134
- type ToolsetContext<TServices extends ServicesDeclaration = EmptyDeclaration, TSettings extends undefined | z.ZodType = undefined, TCollections extends CollectionsDeclaration = EmptyDeclaration> = { readonly [K in keyof TServices]: TServices[K] extends ServiceToken<infer TInstance> ? TInstance : never; } & {
135
- readonly turn: ToolTurnScope;
136
- } & (keyof TCollections extends never ? unknown : {
137
- readonly storage: { readonly [K in keyof TCollections]: ToolsetCollection<z.infer<TCollections[K]>>; };
138
- }) & (TSettings extends z.ZodType ? {
139
- readonly settings: z.infer<TSettings>;
140
- } : unknown);
141
- //#endregion
142
- //#region src/tool.d.ts
143
- /** what `execute` receives: the registered config's settings and storage, the failure raisers, and the four facts of the turn */
144
- type ToolContext = ToolsetContext<EmptyDeclaration, RegisteredConfig['settings'], RegisteredConfig['storage']> & {
145
- readonly err: PluginToolErr;
146
- };
147
- type PluginTool<TParams extends z.ZodType> = PluginToolDeclaration<ToolContext, TParams>;
148
- /** identity at runtime; what it is for is typing `args` from `parameters` across the whole declaration */
149
- declare function defineTool<TParams extends z.ZodType>(tool: PluginTool<TParams>): PluginTool<TParams>;
150
- //#endregion
1
+ import { c as ToolTurnScope, d as defineConfig, i as defineTool, n as ToolContext, o as ToolApprovalPayload, s as ToolDisclosure, t as PluginTool, u as Register } from "./tool-Vqe6D48m.js";
151
2
  export { type PluginTool, type Register, type ToolApprovalPayload, type ToolContext, type ToolDisclosure, type ToolTurnScope, defineConfig, defineTool };
@@ -0,0 +1,16 @@
1
+ import { a as PluginToolFailureError, c as ToolTurnScope, l as PluginConfig, r as ToolContextFor } from "./tool-Vqe6D48m.js";
2
+ import { z } from "zod";
3
+ //#region src/testing.d.ts
4
+ /** `settings` as the declared schema accepts them, so defaults apply as they do at boot; `turn` overrides the four facts */
5
+ type TestContextOptions<TConfig extends PluginConfig> = {
6
+ readonly settings?: TConfig['settings'] extends z.ZodType ? z.input<TConfig['settings']> : never;
7
+ readonly turn?: Partial<ToolTurnScope>;
8
+ };
9
+ /**
10
+ * The context a deployment hands `execute`, over in-memory storage: each declared collection
11
+ * validates on write and parses on read as the real store does, settings pass through the declared
12
+ * schema so defaults apply, and `err` raises exactly what the framework's wrapper catches.
13
+ */
14
+ declare function createTestContext<TConfig extends PluginConfig>(config: TConfig, options?: TestContextOptions<TConfig>): ToolContextFor<TConfig>;
15
+ //#endregion
16
+ export { PluginToolFailureError, TestContextOptions, createTestContext };
@@ -0,0 +1,174 @@
1
+ //#region ../core/dist/plugins/plugins.errors.js
2
+ /** what `err.invalidArguments` and `err.unresolved` throw; the perimeter wrapper catches it and nothing else */
3
+ var PluginToolFailureError = class extends Error {
4
+ failure;
5
+ constructor(failure) {
6
+ super(failure.message);
7
+ this.failure = failure;
8
+ }
9
+ };
10
+ //#endregion
11
+ //#region ../../node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
12
+ /**
13
+ * Checks if a given value is a plain object.
14
+ *
15
+ * @param value - The value to check.
16
+ * @returns True if the value is a plain object, otherwise false.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // ✅👇 True
21
+ *
22
+ * isPlainObject({ }); // ✅
23
+ * isPlainObject({ key: 'value' }); // ✅
24
+ * isPlainObject({ key: new Date() }); // ✅
25
+ * isPlainObject(new Object()); // ✅
26
+ * isPlainObject(Object.create(null)); // ✅
27
+ * isPlainObject({ nested: { key: true} }); // ✅
28
+ * isPlainObject(new Proxy({}, {})); // ✅
29
+ * isPlainObject({ [Symbol('tag')]: 'A' }); // ✅
30
+ *
31
+ * // ✅👇 (cross-realms, node context, workers, ...)
32
+ * const runInNewContext = await import('node:vm').then(
33
+ * (mod) => mod.runInNewContext
34
+ * );
35
+ * isPlainObject(runInNewContext('({})')); // ✅
36
+ *
37
+ * // ❌👇 False
38
+ *
39
+ * class Test { };
40
+ * isPlainObject(new Test()) // ❌
41
+ * isPlainObject(10); // ❌
42
+ * isPlainObject(null); // ❌
43
+ * isPlainObject('hello'); // ❌
44
+ * isPlainObject([]); // ❌
45
+ * isPlainObject(new Date()); // ❌
46
+ * isPlainObject(new Uint8Array([1])); // ❌
47
+ * isPlainObject(Buffer.from('ABC')); // ❌
48
+ * isPlainObject(Promise.resolve({})); // ❌
49
+ * isPlainObject(Object.create({})); // ❌
50
+ * isPlainObject(new (class Cls {})); // ❌
51
+ * isPlainObject(globalThis); // ❌,
52
+ * ```
53
+ */
54
+ function isPlainObject(value) {
55
+ if (!value || typeof value !== "object") return false;
56
+ const proto = Object.getPrototypeOf(value);
57
+ if (!(proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null)) return false;
58
+ return Object.prototype.toString.call(value) === "[object Object]";
59
+ }
60
+ //#endregion
61
+ //#region ../core/dist/toolsets/storage/collection-query.utils.js
62
+ /** folds ASCII letters only, matching SQLite's `lower()` without ICU, so the store and this evaluator agree on every string */
63
+ const foldAsciiCase = (text) => text.replace(/[A-Z]+/g, (upper) => upper.toLowerCase());
64
+ const readField = (record, field) => isPlainObject(record) ? record[field] : void 0;
65
+ const matchesScalar = (actual, expected) => expected === null ? actual === null || actual === void 0 : actual === expected;
66
+ const matchesCondition = (actual, condition) => {
67
+ if (condition === null || typeof condition !== "object") return matchesScalar(actual, condition);
68
+ if ("in" in condition) return condition.in.some((candidate) => matchesScalar(actual, candidate));
69
+ return typeof actual === "string" && foldAsciiCase(actual).includes(foldAsciiCase(condition.contains));
70
+ };
71
+ const readConditions = (where) => {
72
+ const conditions = [];
73
+ for (const [field, condition] of Object.entries(where ?? {})) if (condition !== void 0) conditions.push([field, condition]);
74
+ return conditions;
75
+ };
76
+ /** the stated conditions of a query, by field, an undefined one dropped as unstated */
77
+ function collectionQueryConditions(query) {
78
+ return readConditions(query.where);
79
+ }
80
+ /** the query over records already in memory, in their own order — the semantics the store's compiled form must reproduce */
81
+ function applyCollectionQuery(records, query) {
82
+ const conditions = collectionQueryConditions(query);
83
+ const matching = records.filter((record) => conditions.every(([field, condition]) => matchesCondition(readField(record, field), condition)));
84
+ return query.limit === void 0 ? matching : matching.slice(0, query.limit);
85
+ }
86
+ //#endregion
87
+ //#region ../core/dist/plugins/plugins.utils.js
88
+ /** what a plugin tool receives as `err`; shared with the SDK's testing entry, so a test raises the failure a deployment would */
89
+ const PLUGIN_TOOL_ERR = {
90
+ invalidArguments(message) {
91
+ throw new PluginToolFailureError({
92
+ kind: "invalid-arguments",
93
+ message
94
+ });
95
+ },
96
+ unresolved(message) {
97
+ throw new PluginToolFailureError({
98
+ kind: "unresolved",
99
+ message
100
+ });
101
+ }
102
+ };
103
+ //#endregion
104
+ //#region src/testing.ts
105
+ const DEFAULT_TURN = {
106
+ agentUsername: "tester",
107
+ channelId: "test-channel",
108
+ triggeringPostId: null,
109
+ turnId: "test-turn"
110
+ };
111
+ /** the deployment's store mints a cuid2; here any unique string serves, and a test that needs a known id passes one */
112
+ function createCollection(schema) {
113
+ const rows = /* @__PURE__ */ new Map();
114
+ const toRecord = ({ createdAt, id, updatedAt, ...value }) => ({
115
+ ...schema.parse(value),
116
+ createdAt,
117
+ id,
118
+ updatedAt
119
+ });
120
+ return {
121
+ create: ({ id = crypto.randomUUID(), ...data }) => Promise.try(() => {
122
+ if (rows.has(id)) throw new Error(`storage collection already holds a record with id "${id}"`);
123
+ const now = /* @__PURE__ */ new Date();
124
+ rows.set(id, {
125
+ ...schema.parse(data),
126
+ createdAt: now,
127
+ id,
128
+ updatedAt: now
129
+ });
130
+ return toRecord(rows.get(id));
131
+ }),
132
+ deleteById: (id) => Promise.try(() => rows.delete(id)),
133
+ findById: (id) => Promise.try(() => rows.has(id) ? toRecord(rows.get(id)) : null),
134
+ findMany: (query = {}) => Promise.try(() => applyCollectionQuery([...rows.values()].map(toRecord), query)),
135
+ updateById: (id, patch) => Promise.try(() => {
136
+ const row = rows.get(id);
137
+ if (row === void 0) return null;
138
+ const { createdAt, id: _id, updatedAt: _updatedAt, ...value } = row;
139
+ rows.set(id, {
140
+ ...schema.parse({
141
+ ...schema.parse(value),
142
+ ...patch
143
+ }),
144
+ createdAt,
145
+ id,
146
+ updatedAt: /* @__PURE__ */ new Date()
147
+ });
148
+ return toRecord(rows.get(id));
149
+ })
150
+ };
151
+ }
152
+ /**
153
+ * The context a deployment hands `execute`, over in-memory storage: each declared collection
154
+ * validates on write and parses on read as the real store does, settings pass through the declared
155
+ * schema so defaults apply, and `err` raises exactly what the framework's wrapper catches.
156
+ */
157
+ function createTestContext(config, options = {}) {
158
+ const storage = Object.fromEntries(Object.entries(config.storage).map(([name, schema]) => [name, createCollection(schema)]));
159
+ const context = {
160
+ err: PLUGIN_TOOL_ERR,
161
+ storage,
162
+ turn: {
163
+ ...DEFAULT_TURN,
164
+ ...options.turn
165
+ }
166
+ };
167
+ const settings = config.settings === void 0 ? {} : { settings: config.settings.parse(options.settings ?? {}) };
168
+ return {
169
+ ...context,
170
+ ...settings
171
+ };
172
+ }
173
+ //#endregion
174
+ export { PluginToolFailureError, createTestContext };
@@ -0,0 +1,231 @@
1
+ import { z } from "zod";
2
+ import { Promisable } from "type-fest";
3
+ //#region src/config.d.ts
4
+ type CollectionsDeclaration$1 = {
5
+ readonly [key: string]: z.ZodObject;
6
+ };
7
+ /**
8
+ * The config file's default export: the settings schema agents are configured by, and the storage
9
+ * collections the plugin owns. Its generics are what carry the settings and storage types to every
10
+ * tool, through `Register`.
11
+ */
12
+ type PluginConfig<TSettings extends undefined | z.ZodType = undefined | z.ZodType, TCollections extends CollectionsDeclaration$1 = CollectionsDeclaration$1> = {
13
+ readonly settings: TSettings;
14
+ readonly storage: TCollections;
15
+ };
16
+ /**
17
+ * The augmentation point. `src/config.ts` declares its config here, and every tool file receives
18
+ * the declared settings and storage types without importing the config:
19
+ *
20
+ * ```ts
21
+ * declare module '@collegium/sdk' {
22
+ * interface Register { config: typeof config }
23
+ * }
24
+ * ```
25
+ */
26
+ interface Register {}
27
+ type RegisteredConfig = Register extends {
28
+ readonly config: infer TConfig extends PluginConfig;
29
+ } ? TConfig : PluginConfig;
30
+ declare function defineConfig<TSettings extends undefined | z.ZodType = undefined, const TCollections extends CollectionsDeclaration$1 = {}>(config: {
31
+ readonly settings?: TSettings;
32
+ readonly storage?: TCollections;
33
+ }): PluginConfig<TSettings, TCollections>;
34
+ //#endregion
35
+ //#region ../core/src/approvals/approvals.types.d.ts
36
+ /** how a gated tool's approval payload is presented (§6.2): collapsed behind a control, or verbatim */
37
+ type ApprovalPayloadPresentation = 'collapse' | 'verbatim';
38
+ //#endregion
39
+ //#region ../core/src/utils/token.utils.d.ts
40
+ declare const SERVICE_INSTANCE: unique symbol;
41
+ /**
42
+ * A NestJS-compatible injection token branded with the instance type it resolves to. Declared in a
43
+ * leaf `<module>.tokens.ts` beside `import type` of the service alone, which is what keeps a
44
+ * toolset declaration inert (§2): the entrypoint and provisioning import one without pulling the
45
+ * service's runtime module into their graphs.
46
+ */
47
+ type ServiceToken<TInstance> = {
48
+ readonly [SERVICE_INSTANCE]?: TInstance;
49
+ } & symbol;
50
+ //#endregion
51
+ //#region ../core/src/tools/tools.types.d.ts
52
+ /** the four facts of the running turn (§4) — everything else a tool needs, its toolset declares */
53
+ type ToolTurnScope = {
54
+ readonly agentUsername: string;
55
+ readonly channelId: string;
56
+ /** provenance for anything a tool records; null on a turn no post triggered */
57
+ readonly triggeringPostId: null | string;
58
+ readonly turnId: string;
59
+ };
60
+ /** §6.2 — the full payload the approver reads; presence of the `approval` hook is what gates a tool (§5) */
61
+ type ToolApprovalPayload = {
62
+ body: string;
63
+ presentation: ApprovalPayloadPresentation;
64
+ };
65
+ /**
66
+ * A durable record's disclosure (§3.6), returned by the tool that created it; the turn writes the
67
+ * event and the trace lines. `reference` names the record for later reads, e.g. a memory id.
68
+ */
69
+ type ToolDisclosure = {
70
+ readonly body: string;
71
+ readonly description: string;
72
+ readonly reference: string;
73
+ readonly supersededDescriptions?: readonly string[];
74
+ };
75
+ declare namespace ToolFailure {
76
+ /** the tool body threw — a semantic failure that terminates the turn (§7.1, §7.2) */
77
+ type Exception = {
78
+ kind: 'exception';
79
+ message: string;
80
+ };
81
+ /** the arguments were rejected — returned to the model as the tool result; the turn continues */
82
+ type InvalidArguments = {
83
+ kind: 'invalid-arguments';
84
+ message: string;
85
+ };
86
+ /** execution outlived `timeoutMs` — for a mutation the side effect is unconfirmed (§7.1, §7.2) */
87
+ type Timeout = {
88
+ kind: 'timeout';
89
+ timeoutMs: number;
90
+ };
91
+ /**
92
+ * The tool committed something whose outcome cannot be established — a send that may or may not
93
+ * have left. The turn ends stating the ambiguity (§7.1); it is never returned to the model,
94
+ * because a model told "unresolved" will try again, which is precisely what must not happen.
95
+ */
96
+ type Unresolved = {
97
+ kind: 'unresolved';
98
+ message: string;
99
+ };
100
+ /** the model named a tool that does not exist or sits outside its configured set (§6.1, §7.2) */
101
+ type UnknownTool = {
102
+ kind: 'unknown-tool';
103
+ message: string;
104
+ };
105
+ type Any = Exception | InvalidArguments | Timeout | UnknownTool | Unresolved;
106
+ }
107
+ type ToolFailure = ToolFailure.Any;
108
+ //#endregion
109
+ //#region ../core/src/plugins/plugins.errors.d.ts
110
+ /** what `err.invalidArguments` and `err.unresolved` throw; the perimeter wrapper catches it and nothing else */
111
+ declare class PluginToolFailureError extends Error {
112
+ readonly failure: ToolFailure.InvalidArguments | ToolFailure.Unresolved;
113
+ constructor(failure: ToolFailure.InvalidArguments | ToolFailure.Unresolved);
114
+ }
115
+ //#endregion
116
+ //#region ../core/src/plugins/plugins.types.d.ts
117
+ /** what a plugin tool body may return: the text alone, or the text beside a durable record's disclosure (§3.4) */
118
+ type PluginToolOutput = string | {
119
+ readonly disclosure?: ToolDisclosure;
120
+ readonly text: string;
121
+ };
122
+ /**
123
+ * The two failures a tool body may raise itself — the rest of the taxonomy (§7.1) is the
124
+ * framework's to raise. Each throws; the perimeter wrapper maps the throw into the taxonomy.
125
+ */
126
+ type PluginToolErr = {
127
+ /** the arguments were rejected — returned to the model as the tool result; the turn continues */
128
+ invalidArguments(message: string): never;
129
+ /** a committed side effect whose outcome cannot be established; the turn ends stating the ambiguity */
130
+ unresolved(message: string): never;
131
+ };
132
+ /** one tool as a plugin declares it: the framework's tool minus `budgetExempt`, returning plain output */
133
+ type PluginToolDeclaration<TContext, TParams extends z.ZodType> = {
134
+ /** present ⇒ the tool always gates (§5); renders the payload the approver reads and cannot decline */
135
+ approval?(args: z.infer<TParams>): ToolApprovalPayload;
136
+ readonly description: string;
137
+ execute(args: z.infer<TParams>, context: TContext): Promisable<PluginToolOutput>;
138
+ readonly parameters: TParams;
139
+ /** §7.2 — whether a timed-out call may be reported to the model as a plain failure; false ends the turn as unconfirmable */
140
+ readonly retryable?: boolean;
141
+ readonly timeoutMs?: number;
142
+ /** §8.1 — the one-line summary beside the name in the status post; absent shows the name alone */
143
+ traceDetail?(args: z.infer<TParams>): string;
144
+ };
145
+ //#endregion
146
+ //#region ../core/src/toolsets/storage/collection-query.types.d.ts
147
+ /** the JSON scalars a query may compare; an object, array, or date field is not queryable */
148
+ type Scalar = boolean | null | number | string;
149
+ /**
150
+ * The top-level scalar fields of a record, each stripped of the undefined an optional field
151
+ * carries. The intersection with the scalar union changes nothing for a concrete record and is what
152
+ * lets the compiler see, over a generic one, that every condition is a scalar condition.
153
+ */
154
+ type QueryableFields<TRecord> = { readonly [K in keyof TRecord as Exclude<TRecord[K], undefined> extends Scalar ? K : never]-?: Exclude<TRecord[K], undefined> & Scalar; };
155
+ /** one field's condition: equality, membership, or — on a string field — a case-insensitive substring */
156
+ type CollectionFieldCondition<TField> = ([Extract<TField, string>] extends [never] ? never : {
157
+ readonly contains: string;
158
+ }) | TField | {
159
+ readonly in: readonly TField[];
160
+ };
161
+ /** an AND over field conditions, keyed by the record's own scalar fields */
162
+ type CollectionWhere<TRecord> = { readonly [K in keyof QueryableFields<TRecord>]?: CollectionFieldCondition<QueryableFields<TRecord>[K]>; };
163
+ type CollectionQuery<TRecord> = {
164
+ readonly limit?: number;
165
+ readonly where?: CollectionWhere<TRecord>;
166
+ };
167
+ //#endregion
168
+ //#region ../core/src/toolsets/storage/collection.types.d.ts
169
+ /** what the store stamps on every record beside the declared fields; a schema may not declare any of these */
170
+ type CollectionRecordStamp = {
171
+ readonly createdAt: Date;
172
+ readonly id: string;
173
+ readonly updatedAt: Date;
174
+ };
175
+ type CollectionRecord<TValue> = CollectionRecordStamp & TValue;
176
+ /**
177
+ * A toolset-scoped handle over one declared storage collection. Every record is the schema's
178
+ * output plus the stamp; `create` parses through the schema, so defaults apply, and mints a
179
+ * cuid2 unless an id is given. Reads parse each stored value, so a row an older schema wrote
180
+ * fails loudly rather than leaking a stale shape into the tool.
181
+ */
182
+ type ToolsetCollection<TSchema extends z.ZodObject> = {
183
+ create(data: z.input<TSchema> & {
184
+ readonly id?: string;
185
+ }): Promise<CollectionRecord<z.output<TSchema>>>;
186
+ deleteById(id: string): Promise<boolean>;
187
+ findById(id: string): Promise<CollectionRecord<z.output<TSchema>> | null>;
188
+ /** without a query, every record in insertion order */
189
+ findMany(query?: CollectionQuery<CollectionRecord<z.output<TSchema>>>): Promise<CollectionRecord<z.output<TSchema>>[]>;
190
+ /** the patch merged over the stored fields and the whole parsed again, so a patch cannot leave an invalid record */
191
+ updateById(id: string, patch: Partial<z.input<TSchema>>): Promise<CollectionRecord<z.output<TSchema>> | null>;
192
+ };
193
+ //#endregion
194
+ //#region ../core/src/toolsets/toolsets.types.d.ts
195
+ /** `settings`, `storage`, and `turn` are the context's own keys, so a service may not claim them */
196
+ type ServicesDeclaration = {
197
+ readonly [key: string]: ServiceToken<unknown>;
198
+ } & {
199
+ readonly settings?: never;
200
+ readonly storage?: never;
201
+ readonly turn?: never;
202
+ };
203
+ type CollectionsDeclaration = {
204
+ readonly [key: string]: z.ZodObject;
205
+ };
206
+ type EmptyDeclaration = {};
207
+ /**
208
+ * What `execute` receives, assembled from exactly what the toolset declared (§4): each service
209
+ * under its own name, `settings` and `storage` only when declared, and always the turn. Reaching
210
+ * anything undeclared is a compile error.
211
+ */
212
+ type ToolsetContext<TServices extends ServicesDeclaration = EmptyDeclaration, TSettings extends undefined | z.ZodType = undefined, TCollections extends CollectionsDeclaration = EmptyDeclaration> = { readonly [K in keyof TServices]: TServices[K] extends ServiceToken<infer TInstance> ? TInstance : never; } & {
213
+ readonly turn: ToolTurnScope;
214
+ } & (keyof TCollections extends never ? unknown : {
215
+ readonly storage: { readonly [K in keyof TCollections]: ToolsetCollection<TCollections[K]>; };
216
+ }) & (TSettings extends z.ZodType ? {
217
+ readonly settings: z.infer<TSettings>;
218
+ } : unknown);
219
+ //#endregion
220
+ //#region src/tool.d.ts
221
+ /** what `execute` receives under a config: its settings and storage, the failure raisers, and the four facts of the turn */
222
+ type ToolContextFor<TConfig extends PluginConfig> = ToolsetContext<EmptyDeclaration, TConfig['settings'], TConfig['storage']> & {
223
+ readonly err: PluginToolErr;
224
+ };
225
+ /** the context under the registered config: what every tool file's `execute` receives */
226
+ type ToolContext = ToolContextFor<RegisteredConfig>;
227
+ type PluginTool<TParams extends z.ZodType> = PluginToolDeclaration<ToolContext, TParams>;
228
+ /** identity at runtime; what it is for is typing `args` from `parameters` across the whole declaration */
229
+ declare function defineTool<TParams extends z.ZodType>(tool: PluginTool<TParams>): PluginTool<TParams>;
230
+ //#endregion
231
+ export { PluginToolFailureError as a, ToolTurnScope as c, defineConfig as d, defineTool as i, PluginConfig as l, ToolContext as n, ToolApprovalPayload as o, ToolContextFor as r, ToolDisclosure as s, PluginTool as t, Register as u };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@collegium/sdk",
3
3
  "type": "module",
4
- "version": "0.0.1-beta.7",
4
+ "version": "0.0.1-beta.9",
5
5
  "description": "Write a Collegium plugin: declare a toolset with its tools, settings, storage, and skills.",
6
6
  "license": "AGPL-3.0-only",
7
7
  "homepage": "https://collegium.sh",
@@ -21,7 +21,11 @@
21
21
  "types": "./dist/index.d.ts",
22
22
  "default": "./dist/index.js"
23
23
  },
24
- "./package.json": "./package.json"
24
+ "./package.json": "./package.json",
25
+ "./testing": {
26
+ "types": "./dist/testing.d.ts",
27
+ "default": "./dist/testing.js"
28
+ }
25
29
  },
26
30
  "files": [
27
31
  "dist"