@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/Kit.ts ADDED
@@ -0,0 +1,705 @@
1
+ import { Effect, Layer, Queue, Ref, pipe, JSONSchema } from 'effect'
2
+ import type { AgentKitConfig, AgentConfig, StorageBundle } from './types/config.js'
3
+ import type { PluginDbService } from './services/PluginDb.js'
4
+ import { StorageError, type StorageService } from './services/Storage.js'
5
+ import type { AlertDefinition } from './Alert.js'
6
+ import type { AgentKitPlugin, PluginContext, SyncPayload } from './Plugin.js'
7
+ import type { PluginMigration } from './services/PluginDb.js'
8
+ import { Storage } from './services/Storage.js'
9
+ import { Platform, PlatformError } from './services/Platform.js'
10
+ import { Steering, type SteeringDecision } from './services/Steering.js'
11
+ import { Blackboard } from './services/Blackboard.js'
12
+ import { bundleTool } from './bundle.js'
13
+ import type { InboundMessage } from './types/index.js'
14
+ import type { ToolDefinition } from './Tool.js'
15
+
16
+ export type AgentKitInstance = {
17
+ /**
18
+ * Sync local agent config + tools to the platform (infra-as-code).
19
+ * Pushes model, systemPrompt, tools, and execution mode to the platform DB.
20
+ */
21
+ readonly sync: () => Promise<void>
22
+
23
+ /**
24
+ * Start the HTTP server that receives turn requests from the platform.
25
+ * Also calls sync() to push config to platform.
26
+ */
27
+ readonly serve: (opts?: { port?: number; host?: string }) => Promise<void>
28
+
29
+ /**
30
+ * Process a single inbound message programmatically (useful for testing).
31
+ */
32
+ readonly handleInbound: (msg: InboundMessage) => Promise<{ response: string; messageId: string }>
33
+
34
+ /**
35
+ * Graceful shutdown.
36
+ */
37
+ readonly shutdown: () => Promise<void>
38
+ }
39
+
40
+ /**
41
+ * @deprecated Use {@link AgentKitInstance} instead. Kept for backwards compatibility.
42
+ */
43
+ export type SmsKitInstance = AgentKitInstance
44
+
45
+ /**
46
+ * Resolved plugin state — the merged result of all plugins.
47
+ * Used internally by Kit, Server, and sync.
48
+ */
49
+ export type ResolvedPlugins = {
50
+ /** Agent config with plugin `init` contributions applied (explicit config wins). */
51
+ readonly agentConfig: AgentConfig
52
+ /** Merged `PluginInitResult.extra` values, seeded into the sync payload's `extra`. */
53
+ readonly initExtra: Record<string, unknown>
54
+ readonly tools: ReadonlyArray<ToolDefinition<any, any>>
55
+ /** Alert definitions contributed by plugins, additive to `agent.alerts`. */
56
+ readonly alerts: ReadonlyArray<AlertDefinition>
57
+ readonly routes: Readonly<Record<string, (req: Request, ctx: any) => Promise<Response>>>
58
+ readonly hooks: {
59
+ readonly beforeTurn: ReadonlyArray<NonNullable<NonNullable<AgentKitPlugin['hooks']>['beforeTurn']>>
60
+ readonly afterTurn: ReadonlyArray<NonNullable<NonNullable<AgentKitPlugin['hooks']>['afterTurn']>>
61
+ readonly beforeToolCall: ReadonlyArray<NonNullable<NonNullable<AgentKitPlugin['hooks']>['beforeToolCall']>>
62
+ readonly afterToolCall: ReadonlyArray<NonNullable<NonNullable<AgentKitPlugin['hooks']>['afterToolCall']>>
63
+ }
64
+ readonly layers: ReadonlyArray<Layer.Layer<any>>
65
+ readonly onSync: ReadonlyArray<NonNullable<AgentKitPlugin['onSync']>>
66
+ readonly onRequest: ReadonlyArray<NonNullable<AgentKitPlugin['onRequest']>>
67
+ readonly onResponse: ReadonlyArray<NonNullable<AgentKitPlugin['onResponse']>>
68
+ readonly middleware: ReadonlyArray<NonNullable<AgentKitPlugin['middleware']>[number]>
69
+ readonly migrations: ReadonlyArray<{ readonly pluginId: string; readonly migrations: ReadonlyArray<PluginMigration> }>
70
+ }
71
+
72
+ /**
73
+ * Merge a plugin's `init` config contribution into the agent config.
74
+ * Explicitly-set agent config keys always win over plugin defaults.
75
+ */
76
+ export function applyPluginConfig(agent: AgentConfig, overrides: Partial<AgentConfig>): AgentConfig {
77
+ const explicit = Object.fromEntries(
78
+ Object.entries(agent).filter(([, value]) => value !== undefined)
79
+ ) as Partial<AgentConfig>
80
+ return { ...agent, ...overrides, ...explicit }
81
+ }
82
+
83
+ /**
84
+ * Resolve all plugins into a single merged structure.
85
+ */
86
+ export function resolvePlugins(config: AgentKitConfig): ResolvedPlugins {
87
+ const plugins = config.plugins ?? []
88
+ let agentConfig: AgentConfig = config.agent
89
+ const initExtra: Record<string, unknown> = {}
90
+
91
+ const tools: ToolDefinition<any, any>[] = []
92
+ const routes: Record<string, (req: Request, ctx: any) => Promise<Response>> = {}
93
+ const beforeTurn: ResolvedPlugins['hooks']['beforeTurn'][number][] = []
94
+ const afterTurn: ResolvedPlugins['hooks']['afterTurn'][number][] = []
95
+ const beforeToolCall: ResolvedPlugins['hooks']['beforeToolCall'][number][] = []
96
+ const afterToolCall: ResolvedPlugins['hooks']['afterToolCall'][number][] = []
97
+ const layers: Layer.Layer<any>[] = []
98
+ const onSync: NonNullable<AgentKitPlugin['onSync']>[] = []
99
+ const onRequest: NonNullable<AgentKitPlugin['onRequest']>[] = []
100
+ const onResponse: NonNullable<AgentKitPlugin['onResponse']>[] = []
101
+ const middleware: NonNullable<AgentKitPlugin['middleware']>[number][] = []
102
+ const migrations: ResolvedPlugins['migrations'][number][] = []
103
+ const alerts: AlertDefinition[] = []
104
+
105
+ for (const plugin of plugins) {
106
+ // Run init — may contribute config overrides (not mutating original).
107
+ // Later plugins see earlier plugins' contributions.
108
+ const ctx: PluginContext = { config, agentConfig }
109
+ const initResult = plugin.init?.(ctx)
110
+ if (initResult?.config) {
111
+ agentConfig = applyPluginConfig(agentConfig, initResult.config)
112
+ }
113
+ if (initResult?.extra) {
114
+ Object.assign(initExtra, initResult.extra)
115
+ }
116
+
117
+ // Collect static migrations declared on the plugin
118
+ if (plugin.migrations && plugin.migrations.length > 0) {
119
+ migrations.push({ pluginId: plugin.id, migrations: plugin.migrations })
120
+ }
121
+
122
+ if (plugin.tools) tools.push(...plugin.tools)
123
+ if (plugin.alerts) alerts.push(...plugin.alerts)
124
+ if (plugin.routes) Object.assign(routes, plugin.routes)
125
+ if (plugin.hooks?.beforeTurn) beforeTurn.push(plugin.hooks.beforeTurn)
126
+ if (plugin.hooks?.afterTurn) afterTurn.push(plugin.hooks.afterTurn)
127
+ if (plugin.hooks?.beforeToolCall) beforeToolCall.push(plugin.hooks.beforeToolCall)
128
+ if (plugin.hooks?.afterToolCall) afterToolCall.push(plugin.hooks.afterToolCall)
129
+ if (plugin.layers) layers.push(...plugin.layers)
130
+ if (plugin.onSync) onSync.push(plugin.onSync)
131
+ if (plugin.onRequest) onRequest.push(plugin.onRequest)
132
+ if (plugin.onResponse) onResponse.push(plugin.onResponse)
133
+ if (plugin.middleware) middleware.push(...plugin.middleware)
134
+ }
135
+
136
+ return {
137
+ agentConfig,
138
+ initExtra,
139
+ tools,
140
+ alerts,
141
+ routes,
142
+ hooks: { beforeTurn, afterTurn, beforeToolCall, afterToolCall },
143
+ layers,
144
+ onSync,
145
+ onRequest,
146
+ onResponse,
147
+ middleware,
148
+ migrations,
149
+ }
150
+ }
151
+
152
+ /**
153
+ * The agent's own alerts plus plugin-contributed ones, deduped by name
154
+ * (first definition wins, so agent config beats plugins). Used by both
155
+ * alert registration in `sync` and the `/alerts/execute` handler so the
156
+ * two cannot diverge.
157
+ */
158
+ export function mergedAlerts(config: AgentKitConfig, resolved: ResolvedPlugins): AlertDefinition[] {
159
+ const seen = new Set<string>()
160
+ return [...(config.agent.alerts ?? []), ...resolved.alerts].filter((a) => {
161
+ if (seen.has(a.name)) return false
162
+ seen.add(a.name)
163
+ return true
164
+ })
165
+ }
166
+
167
+ /**
168
+ * Run all plugin `onSync` hooks over the initial sync payload.
169
+ */
170
+ export function applySyncHooks(
171
+ initial: SyncPayload,
172
+ hooks: ReadonlyArray<NonNullable<AgentKitPlugin['onSync']>>
173
+ ): SyncPayload {
174
+ let payload = initial
175
+ for (const onSync of hooks) {
176
+ payload = onSync(payload)
177
+ }
178
+ return payload
179
+ }
180
+
181
+ function isStorageBundle(s: Layer.Layer<Storage, StorageError> | StorageBundle): s is StorageBundle {
182
+ return 'layer' in s && 'pluginDb' in s
183
+ }
184
+
185
+ /** Cloud agents keep history and users on the platform; every local storage call fails loudly. */
186
+ const NoStorage: Layer.Layer<Storage, StorageError> = Layer.succeed(
187
+ Storage,
188
+ new Proxy({} as StorageService, {
189
+ get: (_, method) => () =>
190
+ Effect.fail(new StorageError(`Storage.${String(method)} called but no \`storage\` was passed to AgentKit.create`)),
191
+ })
192
+ )
193
+
194
+ function unwrapStorage(config: AgentKitConfig): {
195
+ layer: Layer.Layer<Storage, StorageError>
196
+ pluginDb: PluginDbService | undefined
197
+ } {
198
+ if (!config.storage) return { layer: NoStorage, pluginDb: undefined }
199
+ if (isStorageBundle(config.storage)) {
200
+ return { layer: config.storage.layer, pluginDb: config.storage.pluginDb }
201
+ }
202
+ return { layer: config.storage, pluginDb: undefined }
203
+ }
204
+
205
+ /**
206
+ * Run plugin-owned migrations using the configured PluginDb.
207
+ * Each plugin's migrations are tracked in `smskit_plugin_migrations`.
208
+ * Migrations are additive only — tables are NEVER dropped when a plugin is disabled.
209
+ */
210
+ async function runPluginMigrations(
211
+ pluginDb: PluginDbService | undefined,
212
+ migrations: ResolvedPlugins['migrations']
213
+ ): Promise<void> {
214
+ if (migrations.length === 0 || !pluginDb) return
215
+
216
+ const db = pluginDb
217
+
218
+ await Effect.runPromise(
219
+ db.execute(`
220
+ CREATE TABLE IF NOT EXISTS smskit_plugin_migrations (
221
+ plugin_id TEXT NOT NULL,
222
+ version INT NOT NULL,
223
+ name TEXT NOT NULL,
224
+ applied_at TIMESTAMPTZ DEFAULT now(),
225
+ PRIMARY KEY (plugin_id, version)
226
+ )
227
+ `)
228
+ )
229
+
230
+ for (const { pluginId, pluginMigrations } of migrations.map((m) => ({ pluginId: m.pluginId, pluginMigrations: m.migrations }))) {
231
+ const applied = await Effect.runPromise(
232
+ db.query<{ version: number }>(
233
+ `SELECT version FROM smskit_plugin_migrations WHERE plugin_id = $1 ORDER BY version`,
234
+ [pluginId]
235
+ )
236
+ )
237
+ const appliedSet = new Set(applied.map((r) => r.version))
238
+
239
+ for (const migration of [...pluginMigrations].sort((a, b) => a.version - b.version)) {
240
+ if (appliedSet.has(migration.version)) continue
241
+
242
+ await Effect.runPromise(db.execute(migration.up))
243
+ await Effect.runPromise(
244
+ db.execute(
245
+ `INSERT INTO smskit_plugin_migrations (plugin_id, version, name) VALUES ($1, $2, $3)`,
246
+ [pluginId, migration.version, migration.name]
247
+ )
248
+ )
249
+ }
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Create an AgentKit instance from config.
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * import { AgentKit } from '@onkeiki/agents'
259
+ * import { PostgresStorageLive } from '@onkeiki/agents-storage-postgres'
260
+ * import { memory, browser, steering } from '@onkeiki/agents-plugins'
261
+ *
262
+ * const kit = AgentKit.create({
263
+ * apiKey: process.env.KEIKI_API_KEY!,
264
+ * name: 'My Agent',
265
+ * resolveNumber: (ctx) => Effect.succeed('+14155551234'),
266
+ * storage: PostgresStorageLive({ connectionString: process.env.DATABASE_URL! }),
267
+ * plugins: [
268
+ * memory({ provider: 'supermemory', autoRetrieve: true }),
269
+ * browser({ handoff: true }),
270
+ * steering({ debounceWindow: '2 seconds', midTurnStrategy: 'interrupt' }),
271
+ * ],
272
+ * agent: {
273
+ * model: 'google/gemini-3.5-flash',
274
+ * maxSteps: 10,
275
+ * historyLimit: 40,
276
+ * systemPrompt: ({ user }) => `You are an assistant for ${user.phone}`,
277
+ * tools: [],
278
+ * },
279
+ * })
280
+ *
281
+ * kit.serve({ port: 8789 })
282
+ * ```
283
+ */
284
+ export const AgentKit = {
285
+ create(config: AgentKitConfig): AgentKitInstance {
286
+ const resolved = resolvePlugins(config)
287
+ const effectiveConfig: AgentKitConfig = { ...config, agent: resolved.agentConfig }
288
+ const { layer: storageLayer, pluginDb } = unwrapStorage(effectiveConfig)
289
+ const platformLayer = makePlatformLayer(effectiveConfig)
290
+ const steeringLayer = makeSteeringLayer(effectiveConfig.agent)
291
+ const blackboardLayer = makeBlackboardLayer()
292
+
293
+ const fullLayer = pipe(
294
+ storageLayer,
295
+ Layer.merge(platformLayer),
296
+ Layer.merge(steeringLayer),
297
+ Layer.merge(blackboardLayer),
298
+ )
299
+
300
+ return {
301
+ sync: async () => {
302
+ await runPluginMigrations(pluginDb, resolved.migrations)
303
+ await syncToPlatform(effectiveConfig, resolved)
304
+ },
305
+
306
+ serve: async (opts) => {
307
+ const port = opts?.port ?? 8789
308
+ const host = opts?.host ?? '0.0.0.0'
309
+
310
+ // Start server first so it's ready to receive turns
311
+ const { createServer } = await import('./Server.js')
312
+ const server = createServer(effectiveConfig, resolved)
313
+
314
+ Bun.serve({
315
+ port,
316
+ hostname: host,
317
+ fetch: server.fetch,
318
+ })
319
+
320
+ console.log(`@onkeiki/agents serving on ${host}:${port}`)
321
+
322
+ // Run plugin migrations and sync config
323
+ await runPluginMigrations(pluginDb, resolved.migrations)
324
+ await syncToPlatform(effectiveConfig, resolved, port)
325
+ },
326
+
327
+ handleInbound: async (msg) => {
328
+ return Effect.runPromise(
329
+ Effect.gen(function* () {
330
+ const steering = yield* Steering
331
+ yield* steering.enqueue(msg)
332
+ return { response: '', messageId: '' }
333
+ }).pipe(Effect.provide(fullLayer))
334
+ )
335
+ },
336
+
337
+ shutdown: async () => {
338
+ console.log('@onkeiki/agents shutting down')
339
+ },
340
+ }
341
+ },
342
+ }
343
+
344
+ /**
345
+ * @deprecated Use {@link AgentKit} instead. Kept for backwards compatibility.
346
+ */
347
+ export const SmsKit = AgentKit
348
+
349
+ /**
350
+ * Channel-neutral alias of `AgentKit.create()`.
351
+ *
352
+ * @example
353
+ * ```ts
354
+ * import { create } from '@onkeiki/agents'
355
+ *
356
+ * const kit = create({ ... })
357
+ * ```
358
+ */
359
+ export const create = (config: AgentKitConfig): AgentKitInstance => AgentKit.create(config)
360
+
361
+ // --- Internal helpers ---
362
+
363
+ function platformFetch(config: AgentKitConfig, path: string, body?: unknown) {
364
+ return Effect.tryPromise({
365
+ try: async () => {
366
+ const res = await fetch(`${getPlatformUrl()}${path}`, {
367
+ method: body ? 'POST' : 'GET',
368
+ headers: {
369
+ 'Authorization': `Bearer ${config.apiKey}`,
370
+ 'Content-Type': 'application/json',
371
+ },
372
+ ...(body ? { body: JSON.stringify(body) } : {}),
373
+ })
374
+ if (!res.ok) {
375
+ throw new Error(`Platform API error ${res.status}: ${await res.text()}`)
376
+ }
377
+ return await res.json()
378
+ },
379
+ catch: (e) => new PlatformError(String(e), e),
380
+ })
381
+ }
382
+
383
+ function makePlatformLayer(config: AgentKitConfig): Layer.Layer<Platform> {
384
+ return Layer.succeed(Platform, {
385
+ memoryRecall: (phone, query, opts) =>
386
+ platformFetch(config, '/v1/memory/recall', { phone, query, ...opts }).pipe(
387
+ Effect.map((r: any) => r.facts ?? [])
388
+ ),
389
+
390
+ memoryLearn: (phone, facts) =>
391
+ platformFetch(config, '/v1/memory/learn', { phone, facts }).pipe(Effect.asVoid),
392
+
393
+ browse: (instruction, opts) =>
394
+ platformFetch(config, '/v1/browser/browse', { instruction, ...opts }),
395
+
396
+ handoff: (opts) =>
397
+ platformFetch(config, '/v1/browser/handoff', opts),
398
+
399
+ exec: (command, opts) =>
400
+ platformFetch(config, '/v1/sandbox/exec', { command, ...opts }),
401
+
402
+ writeFile: (path, content, opts) =>
403
+ platformFetch(config, '/v1/sandbox/write', { path, content, ...opts }).pipe(Effect.asVoid),
404
+
405
+ readFile: (path, opts) =>
406
+ platformFetch(config, '/v1/sandbox/read', { path, ...opts }).pipe(
407
+ Effect.map((r: any) => r.content as string)
408
+ ),
409
+
410
+ runJob: (opts) =>
411
+ platformFetch(config, '/v1/executor/run', opts),
412
+
413
+ getJobStatus: (jobId) =>
414
+ platformFetch(config, `/v1/executor/status/${jobId}`),
415
+
416
+ escalate: (opts) =>
417
+ platformFetch(config, '/v1/escalation/create', opts),
418
+
419
+ extractFacts: (phone, messages) =>
420
+ platformFetch(config, '/v1/learning/extract', { phone, messages }).pipe(
421
+ Effect.map((r: any) => r.facts ?? [])
422
+ ),
423
+
424
+ transcribeAudio: (url) =>
425
+ platformFetch(config, '/v1/media/transcribe', { url }).pipe(
426
+ Effect.map((r: any) => r.text as string)
427
+ ),
428
+
429
+ describeImage: (url, prompt) =>
430
+ platformFetch(config, '/v1/media/describe', { url, prompt }).pipe(
431
+ Effect.map((r: any) => r.description as string)
432
+ ),
433
+
434
+ extractDocument: (url) =>
435
+ platformFetch(config, '/v1/media/extract-document', { url }).pipe(
436
+ Effect.map((r: any) => r.content as string)
437
+ ),
438
+ })
439
+ }
440
+
441
+ function makeSteeringLayer(agent: AgentConfig): Layer.Layer<Steering> {
442
+ return Layer.effect(
443
+ Steering,
444
+ Effect.gen(function* () {
445
+ const decisionQueue = yield* Queue.unbounded<SteeringDecision>()
446
+ const states = yield* Ref.make(new Map<string, { phone: string; status: 'idle' | 'debouncing' | 'processing'; pending: InboundMessage[] }>())
447
+
448
+ return {
449
+ enqueue: (msg: InboundMessage) =>
450
+ Effect.gen(function* () {
451
+ const stateMap = yield* Ref.get(states)
452
+ const existing = stateMap.get(msg.phone)
453
+
454
+ if (!existing || existing.status === 'idle') {
455
+ yield* Ref.update(states, (m) => {
456
+ const updated = new Map(m)
457
+ updated.set(msg.phone, { phone: msg.phone, status: 'debouncing', pending: [msg] })
458
+ return updated
459
+ })
460
+ } else if (existing.status === 'debouncing') {
461
+ yield* Ref.update(states, (m) => {
462
+ const updated = new Map(m)
463
+ updated.set(msg.phone, { ...existing, pending: [...existing.pending, msg] })
464
+ return updated
465
+ })
466
+ } else if (existing.status === 'processing') {
467
+ const strategy = agent.steering?.midTurnStrategy ?? 'append'
468
+ if (strategy === 'interrupt') {
469
+ yield* Queue.offer(decisionQueue, { type: 'interrupt', messages: [msg] })
470
+ } else {
471
+ yield* Queue.offer(decisionQueue, { type: 'append', message: msg })
472
+ }
473
+ }
474
+ }),
475
+
476
+ decisions: Effect.succeed(decisionQueue),
477
+
478
+ turnCompleted: (phone: string) =>
479
+ Ref.update(states, (m) => {
480
+ const updated = new Map(m)
481
+ updated.set(phone, { phone, status: 'idle', pending: [] })
482
+ return updated
483
+ }),
484
+
485
+ getState: (phone: string) =>
486
+ Ref.get(states).pipe(
487
+ Effect.map((m) => m.get(phone) ?? { phone, status: 'idle' as const, pending: [] })
488
+ ),
489
+ }
490
+ })
491
+ )
492
+ }
493
+
494
+ function makeBlackboardLayer(): Layer.Layer<Blackboard> {
495
+ return Layer.effect(
496
+ Blackboard,
497
+ Effect.gen(function* () {
498
+ const store = yield* Ref.make(new Map<string, unknown>())
499
+
500
+ return {
501
+ read: (key: string) => Ref.get(store).pipe(Effect.map((m) => m.get(key) ?? null)),
502
+ write: (key: string, value: unknown) =>
503
+ Ref.update(store, (m) => { const n = new Map(m); n.set(key, value); return n }),
504
+ delete: (key: string) =>
505
+ Ref.update(store, (m) => { const n = new Map(m); n.delete(key); return n }),
506
+ entries: () => Ref.get(store).pipe(Effect.map((m) => [...m.entries()])),
507
+ clear: () => Ref.set(store, new Map()),
508
+ }
509
+ })
510
+ )
511
+ }
512
+
513
+ /**
514
+ * Sync local agent config + tools + alerts + secrets to the platform (infra-as-code).
515
+ */
516
+ async function syncToPlatform(config: AgentKitConfig, resolved: ResolvedPlugins, port?: number) {
517
+ const platformUrl = getPlatformUrl()
518
+ console.log(`[agents] Syncing to platform at ${platformUrl}`)
519
+
520
+ const headers = {
521
+ 'Authorization': `Bearer ${config.apiKey}`,
522
+ 'Content-Type': 'application/json',
523
+ }
524
+
525
+ // Build a static system prompt for platform storage
526
+ const staticPrompt = config.agent.systemPrompt({
527
+ user: { phone: '', timezone: null, currency: null, metadata: {}, firstSeenAt: new Date() },
528
+ now: new Date(),
529
+ phone: '',
530
+ context: { memorySummary: null, relevantFacts: [] },
531
+ isFirstInteraction: false,
532
+ history: [],
533
+ staticSync: true,
534
+ })
535
+
536
+ // Merge agent tools + plugin tools
537
+ const allTools = [...config.agent.tools, ...resolved.tools]
538
+
539
+ // Build tool definitions for the sync payload
540
+ const isSdkMode = port !== undefined
541
+ const toolDefs = await Promise.all(
542
+ allTools.map(async (tool) => ({
543
+ name: tool.name,
544
+ description: tool.description,
545
+ inputSchema: JSONSchema.make(tool.input),
546
+ executionType: 'code' as const,
547
+ code: isSdkMode ? null : await bundleTool(tool),
548
+ }))
549
+ )
550
+
551
+ // Derive features flags from agent config
552
+ const features = {
553
+ memory: !!config.agent.memory?.enabled,
554
+ browser: !!config.agent.browser?.enabled,
555
+ sandbox: !!config.agent.sandbox?.enabled,
556
+ executor: !!config.agent.executor?.enabled,
557
+ mcp: !!config.agent.mcp?.enabled,
558
+ blackboard: !!config.agent.blackboard,
559
+ promptCache: !!config.agent.promptCache,
560
+ escalation: !!config.agent.escalation?.enabled,
561
+ steering: !!config.agent.steering,
562
+ media: !!config.agent.media,
563
+ loops: !!config.agent.loops?.enabled,
564
+ }
565
+
566
+ const loopPresets = (config.agent.loops?.presets ?? []).map((p) => ({
567
+ name: p.name,
568
+ description: p.description,
569
+ prompt: p.prompt,
570
+ schedule_type: p.scheduleType,
571
+ schedule_expression: p.scheduleExpression,
572
+ timezone: p.timezone ?? null,
573
+ quiet: p.quiet ?? false,
574
+ }))
575
+
576
+ const userConnections = (config.agent.mcp?.userConnections ?? []).map((c) => ({
577
+ name: c.name,
578
+ description: c.description ?? null,
579
+ aliases: c.aliases ?? [],
580
+ vendor: c.vendor ?? null,
581
+ capabilities: c.capabilities ?? [],
582
+ spec: c.spec,
583
+ base_url: c.baseUrl ?? null,
584
+ oauth: {
585
+ authorization_url: c.oauth.authorizationUrl,
586
+ token_url: c.oauth.tokenUrl,
587
+ client_id_secret: c.oauth.clientIdSecret,
588
+ client_secret_secret: c.oauth.clientSecretSecret,
589
+ },
590
+ health_check: c.healthCheck
591
+ ? {
592
+ operation: c.healthCheck.operation,
593
+ args: c.healthCheck.args ?? null,
594
+ identity_field: c.healthCheck.identityField ?? null,
595
+ }
596
+ : null,
597
+ }))
598
+
599
+ // Single atomic sync call
600
+ const syncBody = {
601
+ name: config.name,
602
+ model: config.agent.model,
603
+ max_steps: config.agent.maxSteps,
604
+ history_limit: config.agent.historyLimit,
605
+ system_prompt: staticPrompt,
606
+ execution_mode: port ? 'sdk' : 'platform',
607
+ webhook_url: port ? `http://localhost:${port}` : undefined,
608
+ harness: config.harness?.type ?? 'flue',
609
+ harness_config: config.harness ? {
610
+ mode: config.harness.connectivity,
611
+ dispatchUrl: config.harness.dispatchUrl,
612
+ timeoutMs: config.harness.timeoutMs === undefined
613
+ ? undefined
614
+ : durationInputToMs(config.harness.timeoutMs),
615
+ } : undefined,
616
+ features,
617
+ loop_presets: loopPresets,
618
+ user_connections: userConnections,
619
+ capabilities: config.agent.capabilities ?? undefined,
620
+ tools: toolDefs,
621
+ }
622
+
623
+ // Let plugins augment the sync payload
624
+ const payload = applySyncHooks(
625
+ {
626
+ name: syncBody.name,
627
+ model: syncBody.model,
628
+ tools: toolDefs.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
629
+ features,
630
+ extra: { ...resolved.initExtra },
631
+ },
632
+ resolved.onSync
633
+ )
634
+
635
+ const syncRes = await fetch(`${platformUrl}/api/v1/developer/sync`, {
636
+ method: 'POST',
637
+ headers,
638
+ body: JSON.stringify({
639
+ ...syncBody,
640
+ name: payload.name,
641
+ model: payload.model,
642
+ features: payload.features,
643
+ extra: payload.extra,
644
+ }),
645
+ })
646
+
647
+ let devAgentId: string | undefined
648
+ if (!syncRes.ok) {
649
+ console.warn(`[agents] Sync failed: ${syncRes.status} ${await syncRes.text()}`)
650
+ } else {
651
+ const result = await syncRes.json() as { devAgentId?: string }
652
+ devAgentId = result.devAgentId
653
+ console.log(`[agents] Sync complete: agent="${config.name}" id=${devAgentId} tools=${allTools.length}`)
654
+ }
655
+
656
+ // Sync alerts (if any defined) — the agent's own plus plugin-contributed.
657
+ const alerts = mergedAlerts(config, resolved)
658
+ if (alerts.length > 0) {
659
+ const alertDefs = alerts.map((alert) => ({
660
+ name: alert.name,
661
+ description: alert.description,
662
+ schedule: alert.schedule,
663
+ }))
664
+
665
+ const alertsRes = await fetch(`${platformUrl}/api/v1/developer/alerts/sync`, {
666
+ method: 'POST',
667
+ headers,
668
+ body: JSON.stringify({ devAgentId, alerts: alertDefs }),
669
+ })
670
+
671
+ if (!alertsRes.ok) {
672
+ console.warn(`[agents] Alerts sync failed: ${alertsRes.status} ${await alertsRes.text()}`)
673
+ } else {
674
+ console.log(`[agents] Alerts synced: ${alerts.length} alert(s)`)
675
+ }
676
+ }
677
+
678
+ // Sync secrets (if any defined)
679
+ if (config.secrets && Object.keys(config.secrets).length > 0) {
680
+ const secretsRes = await fetch(`${platformUrl}/api/v1/developer/secrets/sync`, {
681
+ method: 'POST',
682
+ headers,
683
+ body: JSON.stringify({ secrets: config.secrets }),
684
+ })
685
+
686
+ if (!secretsRes.ok) {
687
+ console.warn(`[agents] Secrets sync failed: ${secretsRes.status} ${await secretsRes.text()}`)
688
+ } else {
689
+ console.log(`[agents] Secrets synced: ${Object.keys(config.secrets).length} secret(s)`)
690
+ }
691
+ }
692
+ }
693
+
694
+ function durationInputToMs(value: number | `${number} ${'seconds' | 'minutes' | 'hours'}`): number {
695
+ if (typeof value === 'number') return value
696
+ const [amountText, unit] = value.split(' ')
697
+ const amount = Number(amountText)
698
+ if (unit === 'hours') return amount * 3_600_000
699
+ if (unit === 'minutes') return amount * 60_000
700
+ return amount * 1_000
701
+ }
702
+
703
+ function getPlatformUrl(): string {
704
+ return process.env.KEIKI_PLATFORM_URL ?? 'https://smskit-production.up.railway.app'
705
+ }