@onkeiki/agents 0.1.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 (111) hide show
  1. package/dist/Alert.d.ts +56 -0
  2. package/dist/Alert.d.ts.map +1 -0
  3. package/dist/Alert.js +8 -0
  4. package/dist/Alert.js.map +1 -0
  5. package/dist/Entity.d.ts +158 -0
  6. package/dist/Entity.d.ts.map +1 -0
  7. package/dist/Entity.js +289 -0
  8. package/dist/Entity.js.map +1 -0
  9. package/dist/Kit.d.ts +138 -0
  10. package/dist/Kit.d.ts.map +1 -0
  11. package/dist/Kit.js +517 -0
  12. package/dist/Kit.js.map +1 -0
  13. package/dist/OpenApi.d.ts +151 -0
  14. package/dist/OpenApi.d.ts.map +1 -0
  15. package/dist/OpenApi.js +222 -0
  16. package/dist/OpenApi.js.map +1 -0
  17. package/dist/Plugin.d.ts +183 -0
  18. package/dist/Plugin.d.ts.map +1 -0
  19. package/dist/Plugin.js +10 -0
  20. package/dist/Plugin.js.map +1 -0
  21. package/dist/Server.d.ts +71 -0
  22. package/dist/Server.d.ts.map +1 -0
  23. package/dist/Server.js +476 -0
  24. package/dist/Server.js.map +1 -0
  25. package/dist/Tool.d.ts +86 -0
  26. package/dist/Tool.d.ts.map +1 -0
  27. package/dist/Tool.js +27 -0
  28. package/dist/Tool.js.map +1 -0
  29. package/dist/bundle.d.ts +17 -0
  30. package/dist/bundle.d.ts.map +1 -0
  31. package/dist/bundle.js +101 -0
  32. package/dist/bundle.js.map +1 -0
  33. package/dist/index.d.ts +19 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +18 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/services/Blackboard.d.ts +18 -0
  38. package/dist/services/Blackboard.d.ts.map +1 -0
  39. package/dist/services/Blackboard.js +4 -0
  40. package/dist/services/Blackboard.js.map +1 -0
  41. package/dist/services/Observability.d.ts +102 -0
  42. package/dist/services/Observability.d.ts.map +1 -0
  43. package/dist/services/Observability.js +238 -0
  44. package/dist/services/Observability.js.map +1 -0
  45. package/dist/services/Platform.d.ts +112 -0
  46. package/dist/services/Platform.d.ts.map +1 -0
  47. package/dist/services/Platform.js +13 -0
  48. package/dist/services/Platform.js.map +1 -0
  49. package/dist/services/PluginDb.d.ts +44 -0
  50. package/dist/services/PluginDb.d.ts.map +1 -0
  51. package/dist/services/PluginDb.js +5 -0
  52. package/dist/services/PluginDb.js.map +1 -0
  53. package/dist/services/Sandbox.d.ts +68 -0
  54. package/dist/services/Sandbox.d.ts.map +1 -0
  55. package/dist/services/Sandbox.js +4 -0
  56. package/dist/services/Sandbox.js.map +1 -0
  57. package/dist/services/Steering.d.ts +46 -0
  58. package/dist/services/Steering.d.ts.map +1 -0
  59. package/dist/services/Steering.js +4 -0
  60. package/dist/services/Steering.js.map +1 -0
  61. package/dist/services/Storage.d.ts +63 -0
  62. package/dist/services/Storage.d.ts.map +1 -0
  63. package/dist/services/Storage.js +13 -0
  64. package/dist/services/Storage.js.map +1 -0
  65. package/dist/services/Transport.d.ts +19 -0
  66. package/dist/services/Transport.d.ts.map +1 -0
  67. package/dist/services/Transport.js +4 -0
  68. package/dist/services/Transport.js.map +1 -0
  69. package/dist/services/index.d.ts +9 -0
  70. package/dist/services/index.d.ts.map +1 -0
  71. package/dist/services/index.js +9 -0
  72. package/dist/services/index.js.map +1 -0
  73. package/dist/types/config.d.ts +526 -0
  74. package/dist/types/config.d.ts.map +1 -0
  75. package/dist/types/config.js +2 -0
  76. package/dist/types/config.js.map +1 -0
  77. package/dist/types/index.d.ts +4 -0
  78. package/dist/types/index.d.ts.map +1 -0
  79. package/dist/types/index.js +4 -0
  80. package/dist/types/index.js.map +1 -0
  81. package/dist/types/message.d.ts +54 -0
  82. package/dist/types/message.d.ts.map +1 -0
  83. package/dist/types/message.js +47 -0
  84. package/dist/types/message.js.map +1 -0
  85. package/dist/types/user.d.ts +16 -0
  86. package/dist/types/user.d.ts.map +1 -0
  87. package/dist/types/user.js +14 -0
  88. package/dist/types/user.js.map +1 -0
  89. package/package.json +39 -0
  90. package/src/Alert.ts +51 -0
  91. package/src/Entity.ts +543 -0
  92. package/src/Kit.ts +705 -0
  93. package/src/OpenApi.ts +343 -0
  94. package/src/Plugin.ts +209 -0
  95. package/src/Server.ts +618 -0
  96. package/src/Tool.ts +110 -0
  97. package/src/bundle.ts +114 -0
  98. package/src/index.ts +209 -0
  99. package/src/services/Blackboard.ts +19 -0
  100. package/src/services/Observability.ts +320 -0
  101. package/src/services/Platform.ts +125 -0
  102. package/src/services/PluginDb.ts +48 -0
  103. package/src/services/Sandbox.ts +74 -0
  104. package/src/services/Steering.ts +45 -0
  105. package/src/services/Storage.ts +76 -0
  106. package/src/services/Transport.ts +21 -0
  107. package/src/services/index.ts +8 -0
  108. package/src/types/config.ts +531 -0
  109. package/src/types/index.ts +3 -0
  110. package/src/types/message.ts +60 -0
  111. package/src/types/user.ts +17 -0
package/src/OpenApi.ts ADDED
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Builders for the compact OpenAPI documents a user connection registers with
3
+ * the Executor daemon.
4
+ *
5
+ * Why write a document at all, when every provider publishes one: the daemon
6
+ * derives the OAuth authorization request from the document's security scheme,
7
+ * so a published description is also a scope request. Google's Gmail
8
+ * description declares fourteen scopes headed by `https://mail.google.com/` —
9
+ * full mailbox read, send, delete and settings — which is the hardest thing to
10
+ * get through verification (restricted scope, brand review, annual CASA
11
+ * assessment) and what the consent screen would ask a stranger for over SMS.
12
+ * A connection that reads unread mail and sends replies needs two scopes.
13
+ *
14
+ * So the deal is: name the scopes you want and the operations you drive, and
15
+ * this returns the document declaring exactly that.
16
+ *
17
+ * ```ts
18
+ * const drive = googleOpenApiSpec({
19
+ * title: 'Google Drive',
20
+ * baseUrl: 'https://www.googleapis.com/drive/v3',
21
+ * scopes: { 'https://www.googleapis.com/auth/drive.readonly': 'Read the user’s files' },
22
+ * operations: [
23
+ * { operationId: 'files.list', path: '/files', summary: 'Search the user’s files',
24
+ * parameters: [{ name: 'q', in: 'query', schema: { type: 'string' } }] },
25
+ * ],
26
+ * })
27
+ *
28
+ * mcp({ userConnections: [{ name: 'drive', spec: drive.spec, baseUrl: drive.baseUrl, ... }] })
29
+ * ```
30
+ *
31
+ * The checks below all catch mistakes that are invisible until a user clicks a
32
+ * connect link, which is the worst place to find them.
33
+ */
34
+
35
+ export type OpenApiMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'
36
+
37
+ export type OpenApiParameter = {
38
+ readonly name: string
39
+ readonly in: 'path' | 'query' | 'header'
40
+ readonly required?: boolean
41
+ readonly description?: string
42
+ readonly schema?: unknown
43
+ }
44
+
45
+ /** A parameter declared once in `parameters` and referenced by name. */
46
+ export type OpenApiParameterRef = { readonly ref: string }
47
+
48
+ export type OpenApiOperation = {
49
+ /**
50
+ * The name the model calls the operation by, and the name a health check
51
+ * names (e.g. `users.messages.send`). Dotted provider-style names keep the
52
+ * generated tools recognizable to a model that knows the real API.
53
+ */
54
+ readonly operationId: string
55
+ /** Defaults to `get`. */
56
+ readonly method?: OpenApiMethod
57
+ /** Server-relative, with `{braces}` around path parameters. */
58
+ readonly path: string
59
+ readonly summary?: string
60
+ /**
61
+ * Worth writing whenever the provider does something the schema cannot say
62
+ * — an encoding, a 404 that is not an error, a field that must be echoed
63
+ * back. This is the only documentation the model gets.
64
+ */
65
+ readonly description?: string
66
+ readonly parameters?: ReadonlyArray<OpenApiParameter | OpenApiParameterRef>
67
+ readonly requestBody?: {
68
+ readonly required?: boolean
69
+ readonly description?: string
70
+ readonly schema: unknown
71
+ }
72
+ readonly response?: {
73
+ /** Success status. Defaults to `200`; a delete usually answers `204`. */
74
+ readonly status?: string
75
+ readonly description?: string
76
+ /** Omit for a status that has no body. */
77
+ readonly schema?: unknown
78
+ }
79
+ }
80
+
81
+ export type OAuthOpenApiSpecInput = {
82
+ readonly title: string
83
+ readonly description?: string
84
+ /** Document version, not the provider's API version. Defaults to `1.0`. */
85
+ readonly version?: string
86
+ /** Absolute base every path is relative to, without a trailing slash. */
87
+ readonly baseUrl: string
88
+ /** Security scheme name, e.g. `googleOAuth`. */
89
+ readonly schemeName: string
90
+ readonly authorizationUrl: string
91
+ readonly tokenUrl: string
92
+ /**
93
+ * Scope -> what it is for. Every one of these lands on the consent screen,
94
+ * so the map is the review surface: if a scope has no operation here that
95
+ * needs it, drop it.
96
+ */
97
+ readonly scopes: Readonly<Record<string, string>>
98
+ /** Shared `#/components/schemas` entries the operations `$ref`. */
99
+ readonly schemas?: Readonly<Record<string, unknown>>
100
+ /** Shared `#/components/parameters` entries, referenced by `{ ref: name }`. */
101
+ readonly parameters?: Readonly<Record<string, OpenApiParameter>>
102
+ readonly operations: ReadonlyArray<OpenApiOperation>
103
+ }
104
+
105
+ export type OAuthOpenApiSpec = {
106
+ /** Serialized document, ready for a user connection's `spec`. */
107
+ readonly spec: string
108
+ /** Matches the connection's `baseUrl` — the daemon calls operations there. */
109
+ readonly baseUrl: string
110
+ /** Declared scopes, in order: what the client's consent screen must allow. */
111
+ readonly scopes: ReadonlyArray<string>
112
+ /** Declared operation ids: what a `healthCheck.operation` may name. */
113
+ readonly operations: ReadonlyArray<string>
114
+ }
115
+
116
+ export class OpenApiSpecError extends Error {
117
+ override readonly name = 'OpenApiSpecError'
118
+ }
119
+
120
+ const PATH_PARAM = /\{([^}]+)\}/g
121
+
122
+ function pathParameterNames(path: string): string[] {
123
+ return [...path.matchAll(PATH_PARAM)].map((m) => m[1]!)
124
+ }
125
+
126
+ /**
127
+ * An OpenAPI document with a single OAuth2 authorization-code scheme required
128
+ * for every operation.
129
+ *
130
+ * @throws OpenApiSpecError when the document would be accepted and then
131
+ * misbehave: no scopes (the daemon finds no OAuth scheme and the connection
132
+ * silently becomes unauthenticated), a duplicate operation id (one operation
133
+ * shadows the other), a path parameter nothing declares (the daemon sends
134
+ * `{braces}` to the provider), or a method reused on one path.
135
+ */
136
+ export function oauthOpenApiSpec(input: OAuthOpenApiSpecInput): OAuthOpenApiSpec {
137
+ const scopes = Object.keys(input.scopes)
138
+ if (scopes.length === 0) {
139
+ throw new OpenApiSpecError(
140
+ `"${input.title}" declares no OAuth scopes: the daemon would find no security scheme `
141
+ + 'and authorize nothing.'
142
+ )
143
+ }
144
+ if (input.operations.length === 0) {
145
+ throw new OpenApiSpecError(`"${input.title}" declares no operations.`)
146
+ }
147
+ if (input.baseUrl.endsWith('/')) {
148
+ throw new OpenApiSpecError(
149
+ `baseUrl "${input.baseUrl}" ends in a slash, which doubles up with every path.`
150
+ )
151
+ }
152
+
153
+ const shared = input.parameters ?? {}
154
+ const paths: Record<string, Record<string, unknown>> = {}
155
+ const seenOperations = new Set<string>()
156
+
157
+ for (const op of input.operations) {
158
+ if (seenOperations.has(op.operationId)) {
159
+ throw new OpenApiSpecError(`Duplicate operationId "${op.operationId}".`)
160
+ }
161
+ seenOperations.add(op.operationId)
162
+
163
+ if (!op.path.startsWith('/')) {
164
+ throw new OpenApiSpecError(
165
+ `Path "${op.path}" of "${op.operationId}" has to be server-relative and start with "/".`
166
+ )
167
+ }
168
+
169
+ const parameters = (op.parameters ?? []).map((p) => {
170
+ if (!('ref' in p)) return p
171
+ if (!(p.ref in shared)) {
172
+ throw new OpenApiSpecError(
173
+ `"${op.operationId}" references parameter "${p.ref}", which is not declared.`
174
+ )
175
+ }
176
+ return { $ref: `#/components/parameters/${p.ref}` }
177
+ })
178
+
179
+ // Resolved names, so a `{userId}` answered by a shared parameter counts.
180
+ const declared = new Set(
181
+ (op.parameters ?? []).map((p) => ('ref' in p ? shared[p.ref]!.name : p.name))
182
+ )
183
+ for (const name of pathParameterNames(op.path)) {
184
+ if (!declared.has(name)) {
185
+ throw new OpenApiSpecError(
186
+ `"${op.operationId}" has a path parameter "{${name}}" that no parameter declares, `
187
+ + 'so the daemon would send the braces to the provider verbatim.'
188
+ )
189
+ }
190
+ }
191
+
192
+ const method = op.method ?? 'get'
193
+ const entry = (paths[op.path] ??= {})
194
+ if (method in entry) {
195
+ throw new OpenApiSpecError(`"${op.path}" declares ${method.toUpperCase()} twice.`)
196
+ }
197
+
198
+ entry[method] = {
199
+ operationId: op.operationId,
200
+ ...(op.summary ? { summary: op.summary } : {}),
201
+ ...(op.description ? { description: op.description } : {}),
202
+ ...(parameters.length > 0 ? { parameters } : {}),
203
+ ...(op.requestBody
204
+ ? {
205
+ requestBody: {
206
+ required: op.requestBody.required ?? true,
207
+ ...(op.requestBody.description ? { description: op.requestBody.description } : {}),
208
+ content: { 'application/json': { schema: op.requestBody.schema } },
209
+ },
210
+ }
211
+ : {}),
212
+ responses: {
213
+ [op.response?.status ?? '200']: {
214
+ description: op.response?.description ?? 'Success.',
215
+ ...(op.response?.schema
216
+ ? { content: { 'application/json': { schema: op.response.schema } } }
217
+ : {}),
218
+ },
219
+ },
220
+ }
221
+ }
222
+
223
+ const spec = {
224
+ openapi: '3.0.3',
225
+ info: {
226
+ title: input.title,
227
+ ...(input.description ? { description: input.description } : {}),
228
+ version: input.version ?? '1.0',
229
+ },
230
+ servers: [{ url: input.baseUrl }],
231
+ security: [{ [input.schemeName]: scopes }],
232
+ components: {
233
+ securitySchemes: {
234
+ [input.schemeName]: {
235
+ type: 'oauth2',
236
+ flows: {
237
+ authorizationCode: {
238
+ authorizationUrl: input.authorizationUrl,
239
+ tokenUrl: input.tokenUrl,
240
+ scopes: input.scopes,
241
+ },
242
+ },
243
+ },
244
+ },
245
+ ...(Object.keys(shared).length > 0 ? { parameters: shared } : {}),
246
+ ...(input.schemas && Object.keys(input.schemas).length > 0
247
+ ? { schemas: input.schemas }
248
+ : {}),
249
+ },
250
+ paths,
251
+ }
252
+
253
+ return {
254
+ spec: JSON.stringify(spec),
255
+ baseUrl: input.baseUrl,
256
+ scopes,
257
+ operations: [...seenOperations],
258
+ }
259
+ }
260
+
261
+ export const GOOGLE_AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
262
+ export const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
263
+
264
+ /**
265
+ * Scopes that cover a whole Google service rather than a use of it. Each is a
266
+ * restricted or sensitive scope whose consent screen reads like handing over
267
+ * the account, and each has a narrow sibling that does the actual job
268
+ * (`gmail.readonly` + `gmail.send`, `calendar.events`, `drive.file`).
269
+ */
270
+ const GOOGLE_WHOLE_ACCOUNT_SCOPES = new Set([
271
+ 'https://mail.google.com/',
272
+ 'https://www.googleapis.com/auth/gmail.modify',
273
+ 'https://www.googleapis.com/auth/calendar',
274
+ 'https://www.googleapis.com/auth/drive',
275
+ 'https://www.googleapis.com/auth/contacts',
276
+ 'https://www.googleapis.com/auth/cloud-platform',
277
+ ])
278
+
279
+ /**
280
+ * `oauthOpenApiSpec` with Google's endpoints and a `googleOAuth` scheme.
281
+ *
282
+ * Two Google-specific refusals, both of which otherwise surface as a dead
283
+ * consent screen or a scope no one meant to ask for:
284
+ * - `offline_access` is not a Google scope (Microsoft's is). A refresh token
285
+ * comes from `access_type=offline&prompt=consent`, which the daemon appends
286
+ * to the authorize URL itself.
287
+ * - whole-service scopes, which cost months of verification and ask a user
288
+ * over SMS for their entire mailbox or calendar. Pass `allowBroadScopes`
289
+ * when a connection genuinely needs one.
290
+ */
291
+ export function googleOpenApiSpec(
292
+ input: Omit<OAuthOpenApiSpecInput, 'schemeName' | 'authorizationUrl' | 'tokenUrl'>
293
+ & { readonly allowBroadScopes?: boolean }
294
+ ): OAuthOpenApiSpec {
295
+ for (const scope of Object.keys(input.scopes)) {
296
+ if (scope === 'offline_access') {
297
+ throw new OpenApiSpecError(
298
+ 'Google rejects "offline_access" as a scope; refresh tokens come from '
299
+ + 'access_type=offline, which the daemon requests for you.'
300
+ )
301
+ }
302
+ if (!input.allowBroadScopes && GOOGLE_WHOLE_ACCOUNT_SCOPES.has(scope)) {
303
+ throw new OpenApiSpecError(
304
+ `"${scope}" covers the whole service. Declare the narrow scopes the operations need `
305
+ + '(e.g. gmail.readonly + gmail.send instead of https://mail.google.com/), or pass '
306
+ + 'allowBroadScopes when the connection really does need it.'
307
+ )
308
+ }
309
+ }
310
+
311
+ return oauthOpenApiSpec({
312
+ ...input,
313
+ schemeName: 'googleOAuth',
314
+ authorizationUrl: GOOGLE_AUTHORIZATION_URL,
315
+ tokenUrl: GOOGLE_TOKEN_URL,
316
+ })
317
+ }
318
+
319
+ export const MICROSOFT_AUTHORIZATION_URL =
320
+ 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize'
321
+ export const MICROSOFT_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
322
+
323
+ /**
324
+ * `oauthOpenApiSpec` with Microsoft identity-platform endpoints and a
325
+ * `microsoftOAuth` scheme. `offline_access` is added when absent: unlike
326
+ * Google, Microsoft only issues a refresh token when that scope is requested,
327
+ * and a connection without one dies at the first token expiry.
328
+ */
329
+ export function microsoftOpenApiSpec(
330
+ input: Omit<OAuthOpenApiSpecInput, 'schemeName' | 'authorizationUrl' | 'tokenUrl'>
331
+ ): OAuthOpenApiSpec {
332
+ const scopes = 'offline_access' in input.scopes
333
+ ? input.scopes
334
+ : { offline_access: 'Keep access when the user is away (refresh token)', ...input.scopes }
335
+
336
+ return oauthOpenApiSpec({
337
+ ...input,
338
+ scopes,
339
+ schemeName: 'microsoftOAuth',
340
+ authorizationUrl: MICROSOFT_AUTHORIZATION_URL,
341
+ tokenUrl: MICROSOFT_TOKEN_URL,
342
+ })
343
+ }
package/src/Plugin.ts ADDED
@@ -0,0 +1,209 @@
1
+ import type { Effect, Layer } from 'effect'
2
+ import type { AlertDefinition } from './Alert.js'
3
+ import type { ToolDefinition } from './Tool.js'
4
+ import type { AgentConfig, AgentKitConfig } from './types/config.js'
5
+ import type { PluginMigration } from './services/PluginDb.js'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Plugin lifecycle types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export type TurnEvent = {
12
+ readonly phone: string
13
+ readonly messages: ReadonlyArray<{ content: string; direction: 'inbound' | 'outbound' }>
14
+ readonly context: Record<string, unknown>
15
+ }
16
+
17
+ export type TurnResult = {
18
+ readonly phone: string
19
+ readonly response: string
20
+ readonly toolCalls: ReadonlyArray<{ name: string; input: unknown; output: unknown }>
21
+ readonly tokensUsed: { in: number; out: number }
22
+ readonly durationMs: number
23
+ }
24
+
25
+ export type ToolCallEvent = {
26
+ readonly phone: string
27
+ readonly toolName: string
28
+ readonly input: unknown
29
+ }
30
+
31
+ export type ToolCallResult = {
32
+ readonly phone: string
33
+ readonly toolName: string
34
+ readonly input: unknown
35
+ readonly output: unknown
36
+ readonly durationMs: number
37
+ readonly error?: string
38
+ }
39
+
40
+ export type RouteContext = {
41
+ readonly config: AgentKitConfig
42
+ readonly request: Request
43
+ }
44
+
45
+ export type PluginContext = {
46
+ readonly config: AgentKitConfig
47
+ readonly agentConfig: AgentConfig
48
+ }
49
+
50
+ export type SyncPayload = {
51
+ readonly name: string
52
+ readonly model: string
53
+ readonly tools: ReadonlyArray<{ name: string; description: string; inputSchema: unknown }>
54
+ readonly features: Record<string, boolean>
55
+ readonly extra: Record<string, unknown>
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Plugin interface (Better Auth–inspired)
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /**
63
+ * A plugin extends AgentKit with tools, hooks, routes, and layers.
64
+ *
65
+ * Modeled after Better Auth's plugin system — composable, isolated,
66
+ * and declarative. Each plugin owns its own config and contributes
67
+ * capabilities to the runtime without touching the core agent config.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { definePlugin } from '@onkeiki/agents'
72
+ *
73
+ * export const rateLimiter = (opts: { maxTurnsPerMinute: number }) =>
74
+ * definePlugin({
75
+ * id: 'rate-limiter',
76
+ * hooks: {
77
+ * beforeTurn: (turn) =>
78
+ * Effect.gen(function* () {
79
+ * // check rate, throw if exceeded
80
+ * return turn
81
+ * }),
82
+ * },
83
+ * })
84
+ * ```
85
+ */
86
+ export type AgentKitPlugin = {
87
+ /** Unique plugin identifier. */
88
+ readonly id: string
89
+
90
+ /**
91
+ * Called once at boot. May return partial config overrides
92
+ * that are merged into the agent config (e.g. inject default tools).
93
+ * Similar to Better Auth's `init`.
94
+ */
95
+ readonly init?: (ctx: PluginContext) => PluginInitResult | void
96
+
97
+ /**
98
+ * Tools this plugin contributes to the agent.
99
+ */
100
+ readonly tools?: ReadonlyArray<ToolDefinition<any, any>>
101
+
102
+ /**
103
+ * Alert definitions this plugin contributes, registered with the platform
104
+ * alongside the agent's own `alerts` at `kit.sync()`. Additive — never
105
+ * overridden by the agent config.
106
+ */
107
+ readonly alerts?: ReadonlyArray<AlertDefinition>
108
+
109
+ /**
110
+ * HTTP routes this plugin serves (e.g. webhooks, alert execution).
111
+ * Keys are pathnames like "/alerts/execute".
112
+ * Similar to Better Auth's `endpoints`.
113
+ */
114
+ readonly routes?: Readonly<Record<string, (req: Request, ctx: RouteContext) => Promise<Response>>>
115
+
116
+ /**
117
+ * Lifecycle hooks around turns and tool calls.
118
+ * Similar to Better Auth's `hooks.before` / `hooks.after`.
119
+ */
120
+ readonly hooks?: {
121
+ readonly beforeTurn?: (turn: TurnEvent) => Effect.Effect<TurnEvent>
122
+ readonly afterTurn?: (result: TurnResult) => Effect.Effect<void>
123
+ readonly beforeToolCall?: (call: ToolCallEvent) => Effect.Effect<ToolCallEvent>
124
+ readonly afterToolCall?: (result: ToolCallResult) => Effect.Effect<ToolCallResult>
125
+ }
126
+
127
+ /**
128
+ * Global request interceptor. Return a Response to short-circuit,
129
+ * a modified Request to rewrite, or void to pass through.
130
+ * Similar to Better Auth's `onRequest`.
131
+ */
132
+ readonly onRequest?: (req: Request) => Effect.Effect<Request | Response | void>
133
+
134
+ /**
135
+ * Global response interceptor.
136
+ * Similar to Better Auth's `onResponse`.
137
+ */
138
+ readonly onResponse?: (res: Response) => Effect.Effect<Response>
139
+
140
+ /**
141
+ * Additional Effect layers to provide to the runtime.
142
+ */
143
+ readonly layers?: ReadonlyArray<Layer.Layer<any>>
144
+
145
+ /**
146
+ * Hook into the platform sync. Receives the current payload
147
+ * and returns a modified version. Used by plugins that need
148
+ * to push their own state (alerts, secrets, schemas, etc.)
149
+ *
150
+ * `name`, `model`, `features`, and `extra` from the returned payload are
151
+ * sent to the platform. The `tools` list is read-only context — the sync
152
+ * request always carries the fully-bundled tool definitions.
153
+ */
154
+ readonly onSync?: (payload: SyncPayload) => SyncPayload
155
+
156
+ /**
157
+ * Middleware that runs on every request before route matching.
158
+ * Return void to pass through, or a Response to short-circuit.
159
+ */
160
+ readonly middleware?: ReadonlyArray<{
161
+ readonly path: string
162
+ readonly handler: (req: Request, ctx: RouteContext) => Effect.Effect<Response | void>
163
+ }>
164
+
165
+ /**
166
+ * Database migrations owned by this plugin. Declared statically so they
167
+ * can be collected before storage initialization. Run once by the storage
168
+ * adapter at boot, tracked in `smskit_plugin_migrations`. Tables are
169
+ * NEVER dropped when the plugin is disabled — data is preserved.
170
+ */
171
+ readonly migrations?: ReadonlyArray<PluginMigration>
172
+ }
173
+
174
+ export type PluginInitResult = {
175
+ readonly config?: Partial<AgentConfig>
176
+ readonly extra?: Record<string, unknown>
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Legacy Plugin type (deprecated — use AgentKitPlugin)
181
+ // ---------------------------------------------------------------------------
182
+
183
+ /**
184
+ * @deprecated Use `AgentKitPlugin` instead. This type is kept for backwards
185
+ * compatibility and will be removed in a future release.
186
+ */
187
+ export type Plugin = {
188
+ readonly name: string
189
+ readonly tools?: ReadonlyArray<ToolDefinition<any, any>>
190
+ readonly onTurn?: (turn: TurnEvent) => Effect.Effect<TurnEvent>
191
+ readonly afterTurn?: (result: TurnResult) => Effect.Effect<void>
192
+ readonly layers?: ReadonlyArray<Layer.Layer<any>>
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Helpers
197
+ // ---------------------------------------------------------------------------
198
+
199
+ /**
200
+ * Define a type-safe AgentKit plugin.
201
+ */
202
+ export function definePlugin(plugin: AgentKitPlugin): AgentKitPlugin {
203
+ return plugin
204
+ }
205
+
206
+ /**
207
+ * @deprecated Use {@link AgentKitPlugin} instead. Kept for backwards compatibility.
208
+ */
209
+ export type SmsKitPlugin = AgentKitPlugin