@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.
- package/dist/Alert.d.ts +56 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Alert.js +8 -0
- package/dist/Alert.js.map +1 -0
- package/dist/Entity.d.ts +158 -0
- package/dist/Entity.d.ts.map +1 -0
- package/dist/Entity.js +289 -0
- package/dist/Entity.js.map +1 -0
- package/dist/Kit.d.ts +138 -0
- package/dist/Kit.d.ts.map +1 -0
- package/dist/Kit.js +517 -0
- package/dist/Kit.js.map +1 -0
- package/dist/OpenApi.d.ts +151 -0
- package/dist/OpenApi.d.ts.map +1 -0
- package/dist/OpenApi.js +222 -0
- package/dist/OpenApi.js.map +1 -0
- package/dist/Plugin.d.ts +183 -0
- package/dist/Plugin.d.ts.map +1 -0
- package/dist/Plugin.js +10 -0
- package/dist/Plugin.js.map +1 -0
- package/dist/Server.d.ts +71 -0
- package/dist/Server.d.ts.map +1 -0
- package/dist/Server.js +476 -0
- package/dist/Server.js.map +1 -0
- package/dist/Tool.d.ts +86 -0
- package/dist/Tool.d.ts.map +1 -0
- package/dist/Tool.js +27 -0
- package/dist/Tool.js.map +1 -0
- package/dist/bundle.d.ts +17 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +101 -0
- package/dist/bundle.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/services/Blackboard.d.ts +18 -0
- package/dist/services/Blackboard.d.ts.map +1 -0
- package/dist/services/Blackboard.js +4 -0
- package/dist/services/Blackboard.js.map +1 -0
- package/dist/services/Observability.d.ts +102 -0
- package/dist/services/Observability.d.ts.map +1 -0
- package/dist/services/Observability.js +238 -0
- package/dist/services/Observability.js.map +1 -0
- package/dist/services/Platform.d.ts +112 -0
- package/dist/services/Platform.d.ts.map +1 -0
- package/dist/services/Platform.js +13 -0
- package/dist/services/Platform.js.map +1 -0
- package/dist/services/PluginDb.d.ts +44 -0
- package/dist/services/PluginDb.d.ts.map +1 -0
- package/dist/services/PluginDb.js +5 -0
- package/dist/services/PluginDb.js.map +1 -0
- package/dist/services/Sandbox.d.ts +68 -0
- package/dist/services/Sandbox.d.ts.map +1 -0
- package/dist/services/Sandbox.js +4 -0
- package/dist/services/Sandbox.js.map +1 -0
- package/dist/services/Steering.d.ts +46 -0
- package/dist/services/Steering.d.ts.map +1 -0
- package/dist/services/Steering.js +4 -0
- package/dist/services/Steering.js.map +1 -0
- package/dist/services/Storage.d.ts +63 -0
- package/dist/services/Storage.d.ts.map +1 -0
- package/dist/services/Storage.js +13 -0
- package/dist/services/Storage.js.map +1 -0
- package/dist/services/Transport.d.ts +19 -0
- package/dist/services/Transport.d.ts.map +1 -0
- package/dist/services/Transport.js +4 -0
- package/dist/services/Transport.js.map +1 -0
- package/dist/services/index.d.ts +9 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +9 -0
- package/dist/services/index.js.map +1 -0
- package/dist/types/config.d.ts +526 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +2 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/message.d.ts +54 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +47 -0
- package/dist/types/message.js.map +1 -0
- package/dist/types/user.d.ts +16 -0
- package/dist/types/user.d.ts.map +1 -0
- package/dist/types/user.js +14 -0
- package/dist/types/user.js.map +1 -0
- package/package.json +39 -0
- package/src/Alert.ts +51 -0
- package/src/Entity.ts +543 -0
- package/src/Kit.ts +705 -0
- package/src/OpenApi.ts +343 -0
- package/src/Plugin.ts +209 -0
- package/src/Server.ts +618 -0
- package/src/Tool.ts +110 -0
- package/src/bundle.ts +114 -0
- package/src/index.ts +209 -0
- package/src/services/Blackboard.ts +19 -0
- package/src/services/Observability.ts +320 -0
- package/src/services/Platform.ts +125 -0
- package/src/services/PluginDb.ts +48 -0
- package/src/services/Sandbox.ts +74 -0
- package/src/services/Steering.ts +45 -0
- package/src/services/Storage.ts +76 -0
- package/src/services/Transport.ts +21 -0
- package/src/services/index.ts +8 -0
- package/src/types/config.ts +531 -0
- package/src/types/index.ts +3 -0
- package/src/types/message.ts +60 -0
- 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
|
+
}
|