-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdomain.ts
More file actions
333 lines (285 loc) · 11.5 KB
/
domain.ts
File metadata and controls
333 lines (285 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/**
* Domain types for the MiniAgent actor architecture.
*
* Philosophy: "Agent events are all you need"
* - Events are the fundamental unit
* - All state derives from reducing events
* - triggersAgentTurn on events (not event type) determines LLM requests
*/
import type { Prompt } from "@effect/ai"
import { DateTime, Effect, Option, Schema, type Scope, Stream } from "effect"
// -----------------------------------------------------------------------------
// Branded Types
// -----------------------------------------------------------------------------
export const AgentName = Schema.String.pipe(Schema.brand("AgentName"))
export type AgentName = typeof AgentName.Type
export const ContextName = Schema.String.pipe(Schema.brand("ContextName"))
export type ContextName = typeof ContextName.Type
export const EventId = Schema.String.pipe(Schema.brand("EventId"))
export type EventId = typeof EventId.Type
export const AgentTurnNumber = Schema.Number.pipe(Schema.brand("AgentTurnNumber"))
export type AgentTurnNumber = typeof AgentTurnNumber.Type
// -----------------------------------------------------------------------------
// Event ID Generation
// -----------------------------------------------------------------------------
export const makeEventId = (contextName: ContextName, counter: number): EventId =>
`${contextName}:${String(counter).padStart(4, "0")}` as EventId
// -----------------------------------------------------------------------------
// Base Event Fields
// -----------------------------------------------------------------------------
export const BaseEventFields = {
id: EventId,
timestamp: Schema.DateTimeUtc,
agentName: AgentName,
/**
* Forms a blockchain-style chain where each event points to its predecessor.
* The first event (genesis) has parentEventId = None.
* Future: forking will allow multiple events to share the same parent.
*/
parentEventId: Schema.optionalWith(EventId, { as: "Option" }),
triggersAgentTurn: Schema.Boolean
}
// -----------------------------------------------------------------------------
// Config Types
// -----------------------------------------------------------------------------
export const ApiFormat = Schema.Literal("openai-responses", "openai-chat-completions", "anthropic", "gemini")
export type ApiFormat = typeof ApiFormat.Type
/**
* LLM configuration - stored on ReducedContext.
* Uses apiKeyEnvVar (env var name) not actual keys.
*/
export class LlmConfig extends Schema.Class<LlmConfig>("LlmConfig")({
apiFormat: ApiFormat,
model: Schema.String,
baseUrl: Schema.String,
apiKeyEnvVar: Schema.String
}) {}
// -----------------------------------------------------------------------------
// Event Types - Content
// -----------------------------------------------------------------------------
export class SystemPromptEvent extends Schema.TaggedClass<SystemPromptEvent>()(
"SystemPromptEvent",
{ ...BaseEventFields, content: Schema.String }
) {}
export class UserMessageEvent extends Schema.TaggedClass<UserMessageEvent>()(
"UserMessageEvent",
{
...BaseEventFields,
content: Schema.String,
/** Optional array of image data URIs or URLs */
images: Schema.optional(Schema.Array(Schema.String))
}
) {}
export class AssistantMessageEvent extends Schema.TaggedClass<AssistantMessageEvent>()(
"AssistantMessageEvent",
{ ...BaseEventFields, content: Schema.String }
) {}
export class TextDeltaEvent extends Schema.TaggedClass<TextDeltaEvent>()(
"TextDeltaEvent",
{ ...BaseEventFields, delta: Schema.String }
) {}
// -----------------------------------------------------------------------------
// Event Types - Config
// -----------------------------------------------------------------------------
export class SetLlmConfigEvent extends Schema.TaggedClass<SetLlmConfigEvent>()(
"SetLlmConfigEvent",
{
...BaseEventFields,
apiFormat: ApiFormat,
model: Schema.String,
baseUrl: Schema.String,
apiKeyEnvVar: Schema.String
}
) {}
// -----------------------------------------------------------------------------
// Event Types - Lifecycle
// -----------------------------------------------------------------------------
export const InterruptReason = Schema.Literal("user_cancel", "user_new_message", "timeout", "session_ended")
export type InterruptReason = typeof InterruptReason.Type
export class SessionStartedEvent extends Schema.TaggedClass<SessionStartedEvent>()(
"SessionStartedEvent",
{ ...BaseEventFields }
) {}
export class SessionEndedEvent extends Schema.TaggedClass<SessionEndedEvent>()(
"SessionEndedEvent",
{ ...BaseEventFields }
) {}
export class AgentTurnStartedEvent extends Schema.TaggedClass<AgentTurnStartedEvent>()(
"AgentTurnStartedEvent",
{ ...BaseEventFields, turnNumber: AgentTurnNumber }
) {}
export class AgentTurnCompletedEvent extends Schema.TaggedClass<AgentTurnCompletedEvent>()(
"AgentTurnCompletedEvent",
{ ...BaseEventFields, turnNumber: AgentTurnNumber, durationMs: Schema.Number }
) {}
export class AgentTurnInterruptedEvent extends Schema.TaggedClass<AgentTurnInterruptedEvent>()(
"AgentTurnInterruptedEvent",
{
...BaseEventFields,
turnNumber: AgentTurnNumber,
reason: InterruptReason,
partialResponse: Schema.optionalWith(Schema.String, { as: "Option" }),
/**
* When reason is "user_new_message", this holds the ID of the UserMessageEvent
* that caused the interruption. Used by the UI to reorder display so the
* interrupted assistant response appears before the interrupting user message.
*/
interruptedByEventId: Schema.optionalWith(EventId, { as: "Option" })
}
) {}
export class AgentTurnFailedEvent extends Schema.TaggedClass<AgentTurnFailedEvent>()(
"AgentTurnFailedEvent",
{ ...BaseEventFields, turnNumber: AgentTurnNumber, error: Schema.String }
) {}
// -----------------------------------------------------------------------------
// Event Union
// -----------------------------------------------------------------------------
export const ContextEvent = Schema.Union(
SystemPromptEvent,
UserMessageEvent,
AssistantMessageEvent,
TextDeltaEvent,
SetLlmConfigEvent,
SessionStartedEvent,
SessionEndedEvent,
AgentTurnStartedEvent,
AgentTurnCompletedEvent,
AgentTurnInterruptedEvent,
AgentTurnFailedEvent
)
export type ContextEvent = typeof ContextEvent.Type
// -----------------------------------------------------------------------------
// Reduced Context
// -----------------------------------------------------------------------------
export interface ReducedContext {
readonly messages: ReadonlyArray<Prompt.Message>
readonly llmConfig: Option.Option<LlmConfig>
readonly nextEventNumber: number
readonly currentTurnNumber: AgentTurnNumber
readonly agentTurnStartedAtEventId: Option.Option<EventId>
}
export const ReducedContext = {
isAgentTurnInProgress: (ctx: ReducedContext): boolean => Option.isSome(ctx.agentTurnStartedAtEventId),
canMakeLlmCalls: (ctx: ReducedContext): boolean => Option.isSome(ctx.llmConfig),
nextEventId: (ctx: ReducedContext, contextName: ContextName): EventId =>
makeEventId(contextName, ctx.nextEventNumber),
initial: (llmConfig: Option.Option<LlmConfig> = Option.none()): ReducedContext => ({
messages: [],
llmConfig,
nextEventNumber: 0,
currentTurnNumber: 0 as AgentTurnNumber,
agentTurnStartedAtEventId: Option.none()
})
}
// -----------------------------------------------------------------------------
// Errors
// -----------------------------------------------------------------------------
export class AgentError extends Schema.TaggedError<AgentError>()(
"AgentError",
{
message: Schema.String,
apiFormat: Schema.optionalWith(ApiFormat, { as: "Option" }),
cause: Schema.optionalWith(Schema.Defect, { as: "Option" })
}
) {}
export class ReducerError extends Schema.TaggedError<ReducerError>()(
"ReducerError",
{
message: Schema.String,
eventTag: Schema.optionalWith(Schema.String, { as: "Option" })
}
) {}
export class AgentNotFoundError extends Schema.TaggedError<AgentNotFoundError>()(
"AgentNotFoundError",
{ agentName: AgentName }
) {}
export class ContextLoadError extends Schema.TaggedError<ContextLoadError>()(
"ContextLoadError",
{
contextName: ContextName,
message: Schema.String,
cause: Schema.optionalWith(Schema.Defect, { as: "Option" })
}
) {}
export class ContextSaveError extends Schema.TaggedError<ContextSaveError>()(
"ContextSaveError",
{
contextName: ContextName,
message: Schema.String,
cause: Schema.optionalWith(Schema.Defect, { as: "Option" })
}
) {}
// -----------------------------------------------------------------------------
// Service Interfaces
// -----------------------------------------------------------------------------
/**
* MiniAgentTurn executes a single LLM request.
* Takes ReducedContext, returns stream of events.
*/
export class MiniAgentTurn extends Effect.Service<MiniAgentTurn>()("@mini-agent/MiniAgentTurn", {
succeed: {
execute: (_ctx: ReducedContext): Stream.Stream<ContextEvent, AgentError> =>
Stream.fail(
new AgentError({
message: "MiniAgentTurn not implemented",
apiFormat: Option.none(),
cause: Option.none()
})
)
},
accessors: true
}) {}
/**
* MiniAgent interface - not a service, created by AgentRegistry.
*/
export interface MiniAgent {
readonly agentName: AgentName
readonly contextName: ContextName
/** Fire-and-forget: queues event for processing and returns immediately */
readonly addEvent: (event: ContextEvent) => Effect.Effect<void>
/**
* Subscribe to live events. Returns an Effect that, when it completes,
* guarantees the subscription is established. Use this instead of `events`
* when you need to ensure you don't miss events added immediately after subscribing.
*
* The returned stream is scoped to the caller's scope.
*/
readonly subscribe: Effect.Effect<Stream.Stream<ContextEvent, never>, never, Scope.Scope>
/** @deprecated Use subscribe instead - events stream has race condition on subscription timing */
readonly events: Stream.Stream<ContextEvent, never>
readonly getEvents: Effect.Effect<ReadonlyArray<ContextEvent>>
readonly getReducedContext: Effect.Effect<ReducedContext>
/** Gracefully end session: emit SessionEndedEvent (with AgentTurnInterruptedEvent if mid-turn), then close mailbox */
readonly endSession: Effect.Effect<void>
/** True when no LLM turn is in progress */
readonly isIdle: Effect.Effect<boolean>
/** Interrupt the current turn if one is in progress. Emits AgentTurnInterruptedEvent with reason user_cancel. */
readonly interruptTurn: Effect.Effect<void>
/** @deprecated Use endSession instead. Kept for internal cleanup. */
readonly shutdown: Effect.Effect<void>
}
// -----------------------------------------------------------------------------
// Event Field Helpers
// -----------------------------------------------------------------------------
/** Creates the common base fields for all events */
export const makeBaseEventFields = (
agentName: AgentName,
contextName: ContextName,
nextEventNumber: number,
triggersAgentTurn: boolean,
parentEventId: Option.Option<EventId> = Option.none()
) => ({
id: makeEventId(contextName, nextEventNumber),
timestamp: DateTime.unsafeNow(),
agentName,
parentEventId,
triggersAgentTurn
})
/**
* Set parentEventId on a ContextEvent.
* Schema.TaggedClass uses _tag for discrimination (not instanceof), so spread is safe.
*/
export const withParentEventId = <E extends ContextEvent>(
event: E,
parentEventId: Option.Option<EventId>
): E => ({ ...event, parentEventId }) as E