@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
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Observability Service
3
+ *
4
+ * Allows developers to export turn traces to external providers:
5
+ * - Braintrust (AI-specific tracing, evals, scoring)
6
+ * - Axiom (structured log ingestion)
7
+ * - Generic OTEL (any OpenTelemetry-compatible backend)
8
+ * - Custom (developer-provided exporter function)
9
+ *
10
+ * The platform always records traces internally (14-day retention).
11
+ * This service ADDITIONALLY exports to the developer's chosen provider.
12
+ */
13
+ import { Effect, Context } from 'effect'
14
+
15
+ // --- Types ---
16
+
17
+ export type SpanKind = 'llm' | 'tool' | 'retrieval' | 'generation' | 'internal'
18
+ export type SpanStatus = 'running' | 'completed' | 'error'
19
+
20
+ export type TraceEvent = {
21
+ traceId: string
22
+ apiKey: string
23
+ phone: string
24
+ startedAt: Date
25
+ endedAt?: Date
26
+ durationMs?: number
27
+ status: SpanStatus
28
+ model?: string
29
+ tokensIn: number
30
+ tokensOut: number
31
+ totalSteps: number
32
+ error?: string
33
+ metadata: Record<string, unknown>
34
+ spans: SpanEvent[]
35
+ }
36
+
37
+ export type SpanEvent = {
38
+ spanId: string
39
+ traceId: string
40
+ parentSpanId?: string
41
+ name: string
42
+ kind: SpanKind
43
+ startedAt: Date
44
+ endedAt?: Date
45
+ durationMs?: number
46
+ status: SpanStatus
47
+ input?: unknown
48
+ output?: unknown
49
+ error?: string
50
+ metadata: Record<string, unknown>
51
+ }
52
+
53
+ // --- Exporter Interface ---
54
+
55
+ export type TraceExporter = {
56
+ name: string
57
+ exportTrace: (trace: TraceEvent) => Effect.Effect<void, ObservabilityError>
58
+ flush?: () => Effect.Effect<void, ObservabilityError>
59
+ shutdown?: () => Effect.Effect<void, ObservabilityError>
60
+ }
61
+
62
+ export class ObservabilityError {
63
+ readonly _tag = 'ObservabilityError'
64
+ constructor(readonly provider: string, readonly message: string, readonly cause?: unknown) {}
65
+ }
66
+
67
+ // --- Service Tag ---
68
+
69
+ export class Observability extends Context.Tag('Observability')<
70
+ Observability,
71
+ { exporters: TraceExporter[] }
72
+ >() {}
73
+
74
+ // --- Provider Factories ---
75
+
76
+ /**
77
+ * Braintrust exporter.
78
+ * Sends traces as Braintrust spans for AI observability, evals, and scoring.
79
+ */
80
+ export function braintrustExporter(config: {
81
+ apiKey: string
82
+ projectName: string
83
+ baseUrl?: string
84
+ }): TraceExporter {
85
+ const baseUrl = config.baseUrl ?? 'https://api.braintrust.dev'
86
+
87
+ return {
88
+ name: 'braintrust',
89
+ exportTrace: (trace) =>
90
+ Effect.tryPromise({
91
+ try: async () => {
92
+ const spans = trace.spans.map((span) => ({
93
+ id: span.spanId,
94
+ span_id: span.spanId,
95
+ root_span_id: trace.traceId,
96
+ parent_id: span.parentSpanId,
97
+ name: span.name,
98
+ kind: mapSpanKindToBraintrust(span.kind),
99
+ start_time: span.startedAt.toISOString(),
100
+ end_time: span.endedAt?.toISOString(),
101
+ status_code: span.status === 'error' ? 'ERROR' : 'OK',
102
+ input: span.input,
103
+ output: span.output,
104
+ error: span.error,
105
+ metadata: {
106
+ ...span.metadata,
107
+ kind: span.kind,
108
+ phone: trace.phone,
109
+ },
110
+ metrics: span.kind === 'llm' ? {
111
+ tokens: (trace.tokensIn + trace.tokensOut) || undefined,
112
+ prompt_tokens: trace.tokensIn || undefined,
113
+ completion_tokens: trace.tokensOut || undefined,
114
+ } : undefined,
115
+ }))
116
+
117
+ const res = await fetch(`${baseUrl}/v1/project_logs/${encodeURIComponent(config.projectName)}/insert`, {
118
+ method: 'POST',
119
+ headers: {
120
+ 'Authorization': `Bearer ${config.apiKey}`,
121
+ 'Content-Type': 'application/json',
122
+ },
123
+ body: JSON.stringify({ events: spans }),
124
+ })
125
+
126
+ if (!res.ok) {
127
+ throw new Error(`Braintrust export failed: ${res.status} ${await res.text()}`)
128
+ }
129
+ },
130
+ catch: (err) => new ObservabilityError('braintrust', String(err), err),
131
+ }),
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Axiom exporter.
137
+ * Sends traces as structured events to an Axiom dataset.
138
+ */
139
+ export function axiomExporter(config: {
140
+ apiToken: string
141
+ dataset: string
142
+ orgId?: string
143
+ baseUrl?: string
144
+ }): TraceExporter {
145
+ const baseUrl = config.baseUrl ?? 'https://api.axiom.co'
146
+
147
+ return {
148
+ name: 'axiom',
149
+ exportTrace: (trace) =>
150
+ Effect.tryPromise({
151
+ try: async () => {
152
+ // Flatten trace + spans into Axiom events
153
+ const events = [
154
+ // Root trace event
155
+ {
156
+ _time: trace.startedAt.toISOString(),
157
+ type: 'trace',
158
+ traceId: trace.traceId,
159
+ apiKey: trace.apiKey,
160
+ phone: trace.phone,
161
+ model: trace.model,
162
+ status: trace.status,
163
+ durationMs: trace.durationMs,
164
+ tokensIn: trace.tokensIn,
165
+ tokensOut: trace.tokensOut,
166
+ totalSteps: trace.totalSteps,
167
+ error: trace.error,
168
+ ...trace.metadata,
169
+ },
170
+ // Individual span events
171
+ ...trace.spans.map((span) => ({
172
+ _time: span.startedAt.toISOString(),
173
+ type: 'span',
174
+ traceId: span.traceId,
175
+ spanId: span.spanId,
176
+ parentSpanId: span.parentSpanId,
177
+ name: span.name,
178
+ kind: span.kind,
179
+ status: span.status,
180
+ durationMs: span.durationMs,
181
+ error: span.error,
182
+ ...span.metadata,
183
+ })),
184
+ ]
185
+
186
+ const res = await fetch(`${baseUrl}/v1/datasets/${config.dataset}/ingest`, {
187
+ method: 'POST',
188
+ headers: {
189
+ 'Authorization': `Bearer ${config.apiToken}`,
190
+ 'Content-Type': 'application/json',
191
+ ...(config.orgId ? { 'X-Axiom-Org-Id': config.orgId } : {}),
192
+ },
193
+ body: JSON.stringify(events),
194
+ })
195
+
196
+ if (!res.ok) {
197
+ throw new Error(`Axiom export failed: ${res.status} ${await res.text()}`)
198
+ }
199
+ },
200
+ catch: (err) => new ObservabilityError('axiom', String(err), err),
201
+ }),
202
+ }
203
+ }
204
+
205
+ /**
206
+ * OpenTelemetry (OTLP) exporter.
207
+ * Sends traces via OTLP/HTTP (JSON) to any OTEL-compatible backend
208
+ * (Grafana Tempo, Jaeger, Honeycomb, Datadog, etc.)
209
+ */
210
+ export function otelExporter(config: {
211
+ endpoint: string
212
+ headers?: Record<string, string>
213
+ serviceName?: string
214
+ }): TraceExporter {
215
+ const serviceName = config.serviceName ?? 'orchid-sms-agent'
216
+
217
+ return {
218
+ name: 'otel',
219
+ exportTrace: (trace) =>
220
+ Effect.tryPromise({
221
+ try: async () => {
222
+ const resourceSpans = [{
223
+ resource: {
224
+ attributes: [
225
+ { key: 'service.name', value: { stringValue: serviceName } },
226
+ { key: 'orchid.api_key', value: { stringValue: trace.apiKey } },
227
+ ],
228
+ },
229
+ scopeSpans: [{
230
+ scope: { name: 'orchid-agents', version: '0.1.0' },
231
+ spans: trace.spans.map((span) => ({
232
+ traceId: hexToBase64(trace.traceId),
233
+ spanId: hexToBase64(span.spanId.replace(/-/g, '').slice(0, 16)),
234
+ parentSpanId: span.parentSpanId ? hexToBase64(span.parentSpanId.replace(/-/g, '').slice(0, 16)) : undefined,
235
+ name: span.name,
236
+ kind: mapSpanKindToOtel(span.kind),
237
+ startTimeUnixNano: String(span.startedAt.getTime() * 1_000_000),
238
+ endTimeUnixNano: span.endedAt ? String(span.endedAt.getTime() * 1_000_000) : undefined,
239
+ status: {
240
+ code: span.status === 'error' ? 2 : 1,
241
+ message: span.error,
242
+ },
243
+ attributes: [
244
+ { key: 'orchid.span.kind', value: { stringValue: span.kind } },
245
+ { key: 'orchid.phone', value: { stringValue: trace.phone } },
246
+ ...(span.kind === 'llm' ? [
247
+ { key: 'gen_ai.system', value: { stringValue: 'orchid' } },
248
+ { key: 'gen_ai.request.model', value: { stringValue: trace.model ?? '' } },
249
+ { key: 'gen_ai.usage.prompt_tokens', value: { intValue: String(trace.tokensIn) } },
250
+ { key: 'gen_ai.usage.completion_tokens', value: { intValue: String(trace.tokensOut) } },
251
+ ] : []),
252
+ ],
253
+ })),
254
+ }],
255
+ }]
256
+
257
+ const res = await fetch(`${config.endpoint}/v1/traces`, {
258
+ method: 'POST',
259
+ headers: {
260
+ 'Content-Type': 'application/json',
261
+ ...config.headers,
262
+ },
263
+ body: JSON.stringify({ resourceSpans }),
264
+ })
265
+
266
+ if (!res.ok) {
267
+ throw new Error(`OTEL export failed: ${res.status} ${await res.text()}`)
268
+ }
269
+ },
270
+ catch: (err) => new ObservabilityError('otel', String(err), err),
271
+ }),
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Custom exporter — developer provides their own export function.
277
+ */
278
+ export function customExporter(config: {
279
+ name: string
280
+ exportFn: (trace: TraceEvent) => Promise<void>
281
+ }): TraceExporter {
282
+ return {
283
+ name: config.name,
284
+ exportTrace: (trace) =>
285
+ Effect.tryPromise({
286
+ try: () => config.exportFn(trace),
287
+ catch: (err) => new ObservabilityError(config.name, String(err), err),
288
+ }),
289
+ }
290
+ }
291
+
292
+ // --- Helpers ---
293
+
294
+ function mapSpanKindToBraintrust(kind: SpanKind): string {
295
+ switch (kind) {
296
+ case 'llm': return 'llm'
297
+ case 'tool': return 'tool'
298
+ case 'retrieval': return 'retrieval'
299
+ case 'generation': return 'generation'
300
+ default: return 'task'
301
+ }
302
+ }
303
+
304
+ function mapSpanKindToOtel(kind: SpanKind): number {
305
+ switch (kind) {
306
+ case 'llm': return 3 // CLIENT
307
+ case 'tool': return 3 // CLIENT
308
+ case 'retrieval': return 3 // CLIENT
309
+ case 'internal': return 1 // INTERNAL
310
+ default: return 0 // UNSPECIFIED
311
+ }
312
+ }
313
+
314
+ function hexToBase64(hex: string): string {
315
+ const bytes = new Uint8Array(hex.length / 2)
316
+ for (let i = 0; i < hex.length; i += 2) {
317
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16)
318
+ }
319
+ return btoa(String.fromCharCode(...bytes))
320
+ }
@@ -0,0 +1,125 @@
1
+ import { Context, Effect } from 'effect'
2
+
3
+ export class PlatformError {
4
+ readonly _tag = 'PlatformError'
5
+ constructor(readonly message: string, readonly cause?: unknown) {}
6
+ }
7
+
8
+ /**
9
+ * Platform client — communicates with the Orchid platform API
10
+ * to access managed services (memory, browser, sandbox, etc.)
11
+ */
12
+
13
+ export type MemoryFact = {
14
+ readonly id: string
15
+ readonly content: string
16
+ readonly category: string
17
+ readonly createdAt: Date
18
+ }
19
+
20
+ export type BrowseResult = {
21
+ readonly sessionId: string
22
+ readonly url: string
23
+ readonly screenshot?: string
24
+ readonly content?: string
25
+ }
26
+
27
+ export type SandboxExecResult = {
28
+ readonly stdout: string
29
+ readonly stderr: string
30
+ readonly exitCode: number
31
+ readonly files?: ReadonlyArray<{ path: string; url: string }>
32
+ }
33
+
34
+ export type HandoffResult = {
35
+ readonly handoffUrl: string
36
+ readonly sessionId: string
37
+ readonly status: 'waiting' | 'completed' | 'expired'
38
+ }
39
+
40
+ export type EscalationResult = {
41
+ readonly escalationId: string
42
+ readonly routedTo: string
43
+ readonly status: 'pending' | 'acknowledged' | 'resolved'
44
+ }
45
+
46
+ export type BackgroundJobResult = {
47
+ readonly jobId: string
48
+ readonly status: 'queued' | 'running' | 'completed' | 'failed'
49
+ readonly result?: unknown
50
+ }
51
+
52
+ export interface PlatformService {
53
+ // Memory (Supermemory)
54
+ readonly memoryRecall: (phone: string, query: string, opts?: {
55
+ categories?: ReadonlyArray<string>
56
+ limit?: number
57
+ }) => Effect.Effect<ReadonlyArray<MemoryFact>, PlatformError>
58
+
59
+ readonly memoryLearn: (phone: string, facts: ReadonlyArray<{
60
+ content: string
61
+ category: string
62
+ }>) => Effect.Effect<void, PlatformError>
63
+
64
+ // Browser (Browserbase)
65
+ readonly browse: (instruction: string, opts?: {
66
+ sessionId?: string
67
+ screenshot?: boolean
68
+ }) => Effect.Effect<BrowseResult, PlatformError>
69
+
70
+ readonly handoff: (opts: {
71
+ phone: string
72
+ instruction: string
73
+ url?: string
74
+ timeoutMinutes?: number
75
+ }) => Effect.Effect<HandoffResult, PlatformError>
76
+
77
+ // Sandbox (Code execution)
78
+ readonly exec: (command: string, opts?: {
79
+ phone?: string
80
+ persistent?: boolean
81
+ timeout?: number
82
+ }) => Effect.Effect<SandboxExecResult, PlatformError>
83
+
84
+ readonly writeFile: (path: string, content: string, opts?: {
85
+ phone?: string
86
+ }) => Effect.Effect<void, PlatformError>
87
+
88
+ readonly readFile: (path: string, opts?: {
89
+ phone?: string
90
+ }) => Effect.Effect<string, PlatformError>
91
+
92
+ // Executor (Background jobs)
93
+ readonly runJob: (opts: {
94
+ name: string
95
+ command: string
96
+ phone?: string
97
+ timeout?: number
98
+ }) => Effect.Effect<BackgroundJobResult, PlatformError>
99
+
100
+ readonly getJobStatus: (jobId: string) => Effect.Effect<BackgroundJobResult, PlatformError>
101
+
102
+ // Escalation
103
+ readonly escalate: (opts: {
104
+ phone: string
105
+ reason: string
106
+ context?: string
107
+ routes?: { webhook?: string; slack?: { channel: string }; email?: string }
108
+ }) => Effect.Effect<EscalationResult, PlatformError>
109
+
110
+ // Background learning
111
+ readonly extractFacts: (phone: string, messages: ReadonlyArray<{
112
+ content: string
113
+ direction: 'inbound' | 'outbound'
114
+ }>) => Effect.Effect<ReadonlyArray<{ content: string; category: string }>, PlatformError>
115
+
116
+ // Media processing
117
+ readonly transcribeAudio: (url: string) => Effect.Effect<string, PlatformError>
118
+ readonly describeImage: (url: string, prompt?: string) => Effect.Effect<string, PlatformError>
119
+ readonly extractDocument: (url: string) => Effect.Effect<string, PlatformError>
120
+ }
121
+
122
+ export class Platform extends Context.Tag('@onkeiki/agents/Platform')<
123
+ Platform,
124
+ PlatformService
125
+ >() {}
@@ -0,0 +1,48 @@
1
+ import { Context, Effect } from 'effect'
2
+ import { StorageError } from './Storage.js'
3
+
4
+ /**
5
+ * Generic query interface for plugins that need their own persistence.
6
+ *
7
+ * Provided by the active storage adapter (Postgres, MongoDB, etc.) so
8
+ * plugins can create and query their own tables/collections without
9
+ * extending the core `StorageService` interface.
10
+ *
11
+ * The storage adapter is responsible for:
12
+ * - Running plugin migrations at boot (see `PluginMigration`)
13
+ * - Providing a `PluginDb` backed by the same connection pool / client
14
+ *
15
+ * Plugins access this via `PluginDb` in their tools and hooks.
16
+ */
17
+ export interface PluginDbService {
18
+ /** Run a query that returns rows. */
19
+ readonly query: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Effect.Effect<T[], StorageError>
20
+
21
+ /** Run a query that returns a single row or null. */
22
+ readonly queryOne: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Effect.Effect<T | null, StorageError>
23
+
24
+ /** Run a statement that doesn't return rows (INSERT, UPDATE, DELETE). */
25
+ readonly execute: (sql: string, params?: readonly unknown[]) => Effect.Effect<void, StorageError>
26
+ }
27
+
28
+ export class PluginDb extends Context.Tag('@onkeiki/agents/PluginDb')<
29
+ PluginDb,
30
+ PluginDbService
31
+ >() {}
32
+
33
+ /**
34
+ * A single migration contributed by a plugin. Migrations are tracked in
35
+ * a `smskit_plugin_migrations` table and only run once per (pluginId, version).
36
+ *
37
+ * Migrations are NEVER rolled back when a plugin is disabled — disabling
38
+ * a plugin simply stops running its code, but the tables and data remain
39
+ * intact. This prevents accidental data loss.
40
+ */
41
+ export type PluginMigration = {
42
+ /** Monotonically increasing version number (1, 2, 3, ...). */
43
+ readonly version: number
44
+ /** Human-readable name for logging. */
45
+ readonly name: string
46
+ /** SQL statement(s) to execute. May contain multiple statements separated by semicolons. */
47
+ readonly up: string
48
+ }
@@ -0,0 +1,74 @@
1
+ import { Context } from 'effect'
2
+
3
+ /**
4
+ * Result of executing a command in a sandbox.
5
+ */
6
+ export type SandboxExecResult = {
7
+ readonly stdout: string
8
+ readonly stderr: string
9
+ readonly exitCode: number
10
+ }
11
+
12
+ /**
13
+ * A handle to a running sandbox instance. Provides shell execution and
14
+ * network access needed by plugins that run daemons inside sandboxes
15
+ * (e.g. Executor.sh for custom MCP servers).
16
+ */
17
+ export interface SandboxHandle {
18
+ /** Execute a shell command inside the sandbox. */
19
+ readonly exec: (
20
+ command: string,
21
+ opts?: { readonly timeoutSeconds?: number; readonly cwd?: string }
22
+ ) => Promise<SandboxExecResult>
23
+
24
+ /** Get a publicly-reachable URL for a port exposed inside the sandbox. */
25
+ readonly getPublicUrl: (port: number, expiresInSeconds?: number) => Promise<string>
26
+
27
+ /** Absolute home directory of the sandbox user. */
28
+ readonly getHomeDir: () => Promise<string>
29
+
30
+ /**
31
+ * Absolute path of a durable data directory backed by persistent storage
32
+ * (e.g. a Daytona volume) that survives sandbox stop/delete, or `null` when
33
+ * no durable storage is attached. Daemons that keep state (SQLite, OAuth
34
+ * credentials) use it as a backup/restore target across sandbox recreation.
35
+ */
36
+ readonly getPersistentExecutorDataDir?: () => Promise<string | null>
37
+ }
38
+
39
+ /**
40
+ * Abstract sandbox provider — any sandbox company (Daytona, E2B, Runloop,
41
+ * Modal, etc.) can implement this interface and plug into AgentKit.
42
+ *
43
+ * The provider is responsible for lifecycle management (create, start, stop,
44
+ * reuse) of sandboxes. Each `scope` string identifies an isolation boundary
45
+ * (e.g. `"apiKey:phone"` for per-user sandboxes).
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * // Community adapter: @onkeiki/agents-sandbox-daytona
50
+ * import { Daytona } from '@daytona/sdk'
51
+ *
52
+ * export function daytona(opts: { apiKey: string; snapshot?: string }): SandboxProvider {
53
+ * return {
54
+ * resolve: async (scope) => {
55
+ * const sandbox = await getOrCreate(scope)
56
+ * return {
57
+ * exec: (cmd, o) => sandbox.process.executeCommand(cmd, o?.cwd, {}, o?.timeoutSeconds),
58
+ * getPublicUrl: (port, exp) => sandbox.getSignedPreviewUrl(port, exp).then(r => r.url),
59
+ * getHomeDir: () => sandbox.getUserHomeDir().then(h => h ?? '/root'),
60
+ * }
61
+ * },
62
+ * }
63
+ * }
64
+ * ```
65
+ */
66
+ export interface SandboxProviderService {
67
+ /** Get or create a sandbox for the given scope. */
68
+ readonly resolve: (scope: string) => Promise<SandboxHandle>
69
+ }
70
+
71
+ export class SandboxProvider extends Context.Tag('@onkeiki/agents/SandboxProvider')<
72
+ SandboxProvider,
73
+ SandboxProviderService
74
+ >() {}
@@ -0,0 +1,45 @@
1
+ import { Context, Effect, Queue, Fiber, Ref } from 'effect'
2
+ import type { InboundMessage } from '../types/index.js'
3
+ import type { SteeringConfig } from '../types/config.js'
4
+
5
+ export type SteeringDecision =
6
+ | { readonly type: 'batch'; readonly messages: ReadonlyArray<InboundMessage> }
7
+ | { readonly type: 'interrupt'; readonly messages: ReadonlyArray<InboundMessage> }
8
+ | { readonly type: 'append'; readonly message: InboundMessage }
9
+ | { readonly type: 'queue'; readonly message: InboundMessage }
10
+
11
+ export type TurnState = {
12
+ readonly phone: string
13
+ readonly status: 'idle' | 'debouncing' | 'processing'
14
+ readonly pending: ReadonlyArray<InboundMessage>
15
+ readonly currentTurnStartedAt?: Date
16
+ }
17
+
18
+ export interface SteeringService {
19
+ /**
20
+ * Enqueue an inbound message. Steering decides when to
21
+ * release it as a turn (after debounce window) or interrupt
22
+ * an in-progress turn.
23
+ */
24
+ readonly enqueue: (msg: InboundMessage) => Effect.Effect<void>
25
+
26
+ /**
27
+ * Subscribe to turn decisions. The agent runtime consumes these.
28
+ */
29
+ readonly decisions: Effect.Effect<Queue.Dequeue<SteeringDecision>>
30
+
31
+ /**
32
+ * Signal that the current turn for a phone has completed.
33
+ */
34
+ readonly turnCompleted: (phone: string) => Effect.Effect<void>
35
+
36
+ /**
37
+ * Get current state for a phone number.
38
+ */
39
+ readonly getState: (phone: string) => Effect.Effect<TurnState>
40
+ }
41
+
42
+ export class Steering extends Context.Tag('@onkeiki/agents/Steering')<
43
+ Steering,
44
+ SteeringService
45
+ >() {}
@@ -0,0 +1,76 @@
1
+ import { Context, Effect } from 'effect'
2
+ import type { StoredMessage, UserProfile, Subscriber } from '../types/index.js'
3
+
4
+ export class StorageError {
5
+ readonly _tag = 'StorageError'
6
+ constructor(readonly message: string, readonly cause?: unknown) {}
7
+ }
8
+
9
+ export type NewMessage = {
10
+ readonly phone: string
11
+ readonly chatId: string
12
+ readonly direction: 'inbound' | 'outbound'
13
+ readonly content: string
14
+ readonly mediaUrl?: string
15
+ readonly mediaMimeType?: string
16
+ readonly externalMessageId?: string
17
+ }
18
+
19
+ export type DeliveryClaim = {
20
+ readonly phone: string
21
+ readonly alertKey: string
22
+ readonly alertText: string
23
+ }
24
+
25
+ export type UsageEvent = {
26
+ readonly turns?: number
27
+ readonly messages?: number
28
+ readonly tokensIn?: number
29
+ readonly tokensOut?: number
30
+ }
31
+
32
+ export type BillingPeriod = {
33
+ readonly start: Date
34
+ readonly end: Date
35
+ }
36
+
37
+ export type UsageSummary = {
38
+ readonly turns: number
39
+ readonly messages: number
40
+ readonly tokensIn: number
41
+ readonly tokensOut: number
42
+ }
43
+
44
+ export interface StorageService {
45
+ // Messages
46
+ readonly insertMessage: (msg: NewMessage) => Effect.Effect<{ id: string; created: boolean }, StorageError>
47
+ readonly getHistory: (phone: string, limit: number) => Effect.Effect<StoredMessage[], StorageError>
48
+ readonly updateMessageContent: (externalId: string, content: string) => Effect.Effect<void, StorageError>
49
+
50
+ // User profiles
51
+ readonly getUser: (phone: string) => Effect.Effect<UserProfile | null, StorageError>
52
+ readonly upsertUser: (phone: string, patch: Partial<UserProfile>) => Effect.Effect<void, StorageError>
53
+ readonly getUserMeta: (phone: string, key: string) => Effect.Effect<string | null, StorageError>
54
+ readonly setUserMeta: (phone: string, key: string, value: string) => Effect.Effect<void, StorageError>
55
+
56
+ // Alert subscriptions
57
+ readonly listSubscribers: () => Effect.Effect<Subscriber[], StorageError>
58
+ readonly setOptOut: (phone: string, optedOut: boolean) => Effect.Effect<void, StorageError>
59
+
60
+ // Alert delivery dedup
61
+ readonly claimDelivery: (claim: DeliveryClaim) => Effect.Effect<boolean, StorageError>
62
+ readonly markDelivered: (phone: string, alertKey: string, externalId: string) => Effect.Effect<void, StorageError>
63
+
64
+ // Phone line assignments
65
+ readonly getLineAssignment: (phone: string) => Effect.Effect<string | null, StorageError>
66
+ readonly setLineAssignment: (phone: string, line: string) => Effect.Effect<void, StorageError>
67
+
68
+ // Usage metering
69
+ readonly incrementUsage: (keyId: string, event: UsageEvent) => Effect.Effect<void, StorageError>
70
+ readonly getUsage: (keyId: string, period: BillingPeriod) => Effect.Effect<UsageSummary, StorageError>
71
+ }
72
+
73
+ export class Storage extends Context.Tag('@onkeiki/agents/Storage')<
74
+ Storage,
75
+ StorageService
76
+ >() {}