@mozaik-ai/core 4.0.0-beta.9 → 4.0.1
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/README.md +267 -231
- package/dist/index.d.mts +47 -37
- package/dist/index.d.ts +47 -37
- package/dist/index.js +11 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -55,319 +55,355 @@ DeepSeek models run through the OpenAI-compatible chat-completions endpoint, so
|
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
58
|
-
##
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
| `onFunctionCallOutput` | its own function call runner returns a result |
|
|
72
|
-
| `onExternalFunctionCallOutput` | another agent's function call runner returns a result |
|
|
73
|
-
| `onReasoning` | its own inference returns a reasoning item |
|
|
74
|
-
| `onExternalReasoning` | another agent's inference returns a reasoning item |
|
|
75
|
-
| `onModelMessage` | its own inference returns an assistant message |
|
|
76
|
-
| `onExternalModelMessage` | another agent's inference returns an assistant message |
|
|
77
|
-
| `onInternalEvent` | its own inference emits a semantic stream event |
|
|
78
|
-
| `onExternalEvent` | another participant emits a semantic stream event |
|
|
79
|
-
| `onError` | one of its own handlers throws |
|
|
80
|
-
| `onParticipantError` | another participant's handler throws |
|
|
81
|
-
|
|
82
|
-
Every handler defaults to a no-op on `BaseParticipant` — override only the ones you care about.
|
|
83
|
-
|
|
84
|
-
```mermaid
|
|
85
|
-
flowchart LR
|
|
86
|
-
Human[Participant] -->|"sendMessage(env, text, caller)"| Env(("AgenticEnvironment"))
|
|
87
|
-
Agent[Participant] -->|"runInference / executeFunctionCall"| Env
|
|
88
|
-
Observer[Participant] -->|join| Env
|
|
89
|
-
Env -->|"onMessage / onExternal*"| Human
|
|
90
|
-
Env -->|"onFunctionCall / onReasoning / …"| Agent
|
|
91
|
-
Env -->|"onExternal*"| Observer
|
|
92
|
-
Env -->|"onJoined / onLeft / onParticipant*"| All
|
|
93
|
-
```
|
|
58
|
+
## Table of contents
|
|
59
|
+
|
|
60
|
+
- [Runtime](#runtime)
|
|
61
|
+
- [Runtime state](#runtime-state)
|
|
62
|
+
- [Participants](#participants)
|
|
63
|
+
- [Concurrent agents](#concurrent-agents)
|
|
64
|
+
- [Semantic events](#semantic-events)
|
|
65
|
+
- [Situation handlers](#situation-handlers)
|
|
66
|
+
- [The agent loop](#the-agent-loop)
|
|
67
|
+
- [Interception](#interception)
|
|
68
|
+
- [Examples](#examples)
|
|
69
|
+
- [Made with Mozaik](#made-with-mozaik)
|
|
70
|
+
- [Contributing](#contributing)
|
|
94
71
|
|
|
95
72
|
---
|
|
96
73
|
|
|
97
|
-
##
|
|
74
|
+
## Runtime
|
|
98
75
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
| Role | How to build it |
|
|
102
|
-
| -------- | --------------------------------------------------------------------------- |
|
|
103
|
-
| Human | A participant that calls `sendMessage(environment, text, caller)` |
|
|
104
|
-
| Agent | A participant that calls `runInference(...)` and `executeFunctionCall(...)` |
|
|
105
|
-
| Observer | A participant that only overrides handlers and never runs inference |
|
|
76
|
+
The **runtime** is the shared space where participants meet, events are published, and agent loops run. Create one with `defineRuntime`, then call `initializeRuntime` once with a `RuntimeState`:
|
|
106
77
|
|
|
107
78
|
```ts
|
|
108
|
-
import {
|
|
109
|
-
AgenticEnvironment,
|
|
110
|
-
BaseParticipant,
|
|
111
|
-
ModelContext,
|
|
112
|
-
UserMessageItem,
|
|
113
|
-
runInference,
|
|
114
|
-
sendMessage,
|
|
115
|
-
} from "@mozaik-ai/core"
|
|
79
|
+
import { defineRuntime, RuntimeState } from "@mozaik-ai/core"
|
|
116
80
|
|
|
117
|
-
|
|
81
|
+
class AppState extends RuntimeState {}
|
|
118
82
|
|
|
119
|
-
const
|
|
83
|
+
const { initializeRuntime, resolveRuntime, resolveParticipant, join, leave, sendMessage, sendEvent, runLoop } =
|
|
84
|
+
defineRuntime<AppState>()
|
|
120
85
|
|
|
121
|
-
|
|
122
|
-
|
|
86
|
+
initializeRuntime({ state: new AppState() })
|
|
87
|
+
```
|
|
123
88
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
89
|
+
`defineRuntime` returns the functions you use for the rest of the session. They are **not** top-level package exports — keep them in module scope (or re-export them yourself):
|
|
90
|
+
|
|
91
|
+
| Function | Role |
|
|
92
|
+
| ----------------------------------------------------------------- | --------------------------------------------- |
|
|
93
|
+
| `initializeRuntime({ state, inferenceRunnerConfig? })` | Create the runtime. Throws if called twice. |
|
|
94
|
+
| `resolveRuntime()` | The initialized runtime (including `.state`). |
|
|
95
|
+
| `resolveParticipant(id)` | Look up a joined participant by id. |
|
|
96
|
+
| `join(participant)` / `leave(participant)` | Membership. |
|
|
97
|
+
| `sendMessage(message, senderId)` | Publish a `message.sent` event. |
|
|
98
|
+
| `sendEvent(event, senderId)` | Publish any `SemanticEvent`. |
|
|
99
|
+
| `runLoop(agentId, message, inferenceInput, interceptionHandler?)` | Start an agent loop. |
|
|
100
|
+
|
|
101
|
+
Calling `initializeRuntime` a second time throws `"Runtime already initialized"`. Calling any of the other functions before `initializeRuntime` throws `"Runtime not initialized"`.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Runtime state
|
|
106
|
+
|
|
107
|
+
**Runtime state** is the typed store every participant shares. `RuntimeState` already holds the participant registry (`participants: Map<string, Participant>`). Subclass it for anything the team needs to share — an active goal, a work queue, a transcript buffer:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { defineRuntime, RuntimeState } from "@mozaik-ai/core"
|
|
111
|
+
|
|
112
|
+
class AppState extends RuntimeState {
|
|
113
|
+
activeGoal?: string
|
|
128
114
|
}
|
|
129
115
|
|
|
130
|
-
const
|
|
131
|
-
const observer = new BaseParticipant()
|
|
116
|
+
const { initializeRuntime, resolveRuntime } = defineRuntime<AppState>()
|
|
132
117
|
|
|
133
|
-
|
|
134
|
-
agent.join(environment)
|
|
135
|
-
observer.join(environment)
|
|
118
|
+
initializeRuntime({ state: new AppState() })
|
|
136
119
|
|
|
137
|
-
|
|
120
|
+
resolveRuntime().state.activeGoal = "ship the runtime docs"
|
|
138
121
|
```
|
|
139
122
|
|
|
140
|
-
Participants
|
|
123
|
+
Participants read and write this object from situation processors (and from an `InterceptionHandler`). There is no second hidden store.
|
|
141
124
|
|
|
142
125
|
---
|
|
143
126
|
|
|
144
|
-
##
|
|
127
|
+
## Participants
|
|
128
|
+
|
|
129
|
+
A **participant** is anyone who can join the runtime. Mozaik ships two factories:
|
|
145
130
|
|
|
146
|
-
|
|
131
|
+
| Role | Factory | What it carries |
|
|
132
|
+
| ------------ | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
133
|
+
| **Human** | `createHuman({ name, capabilities, handlers })` | A manifest and situation handlers. Typically calls `sendMessage`. |
|
|
134
|
+
| **Agent** | `createAgent({ name, capabilities, instruction, tools, handlers })` | Manifest, handlers, an instruction, tools, and memory (`agent.getMemory().getContext()`). Typically starts thinking with `runLoop`. |
|
|
135
|
+
| **Observer** | Either factory, handlers only | Never calls `sendMessage` or `runLoop` — only reacts. |
|
|
136
|
+
|
|
137
|
+
Every participant has a **manifest** (`id`, `name`, `role`, `capabilities`) and a list of **situation handlers**. Identity is the manifest; behavior is the handlers you register — not method overrides on a base class.
|
|
147
138
|
|
|
148
139
|
```ts
|
|
149
|
-
import {
|
|
150
|
-
BaseParticipant,
|
|
151
|
-
UserMessageItem,
|
|
152
|
-
FunctionCallItem,
|
|
153
|
-
FunctionCallOutputItem,
|
|
154
|
-
ReasoningItem,
|
|
155
|
-
ModelMessageItem,
|
|
156
|
-
AgenticEnvironment,
|
|
157
|
-
ModelContext,
|
|
158
|
-
Tool,
|
|
159
|
-
runInference,
|
|
160
|
-
executeFunctionCall,
|
|
161
|
-
} from "@mozaik-ai/core"
|
|
140
|
+
import { createAgent, createHuman } from "@mozaik-ai/core"
|
|
162
141
|
|
|
163
|
-
|
|
164
|
-
constructor(
|
|
165
|
-
private readonly environment: AgenticEnvironment,
|
|
166
|
-
private readonly context: ModelContext,
|
|
167
|
-
private readonly tools: Tool[] = [],
|
|
168
|
-
) {
|
|
169
|
-
super()
|
|
170
|
-
}
|
|
142
|
+
const human = createHuman({ name: "User", capabilities: [], handlers: [] })
|
|
171
143
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
environment: this.environment,
|
|
181
|
-
})
|
|
182
|
-
}
|
|
144
|
+
const agent = createAgent({
|
|
145
|
+
name: "Assistant",
|
|
146
|
+
capabilities: ["inference"],
|
|
147
|
+
instruction: "You are a helpful teammate.",
|
|
148
|
+
tools: [],
|
|
149
|
+
handlers: [],
|
|
150
|
+
})
|
|
151
|
+
```
|
|
183
152
|
|
|
184
|
-
|
|
185
|
-
async onFunctionCall(item: FunctionCallItem): Promise<void> {
|
|
186
|
-
this.context.addContextItem(item)
|
|
187
|
-
const tool = this.tools.find((t) => t.name === item.name)
|
|
188
|
-
if (tool) executeFunctionCall(this.environment, item, tool, this)
|
|
189
|
-
}
|
|
153
|
+
The role is which functions a participant calls and which specifications it registers. A critic that only watches `model.answer` from others is still just a participant.
|
|
190
154
|
|
|
191
|
-
|
|
192
|
-
async onFunctionCallOutput(item: FunctionCallOutputItem): Promise<void> {
|
|
193
|
-
this.context.addContextItem(item)
|
|
194
|
-
runInference({
|
|
195
|
-
model: "gpt-5.5",
|
|
196
|
-
context: this.context,
|
|
197
|
-
tools: this.tools,
|
|
198
|
-
caller: this,
|
|
199
|
-
environment: this.environment,
|
|
200
|
-
})
|
|
201
|
-
}
|
|
155
|
+
---
|
|
202
156
|
|
|
203
|
-
|
|
204
|
-
async onReasoning(item: ReasoningItem): Promise<void> {
|
|
205
|
-
this.context.addContextItem(item)
|
|
206
|
-
}
|
|
157
|
+
## Concurrent agents
|
|
207
158
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
159
|
+
Several participants can be joined at the same time. Membership is runtime state, and every membership change is an event:
|
|
160
|
+
|
|
161
|
+
```mermaid
|
|
162
|
+
flowchart LR
|
|
163
|
+
joinFn["join(participant)"] --> add["RuntimeState.addParticipant"]
|
|
164
|
+
add --> joined["publish participant.joined"]
|
|
165
|
+
leaveFn["leave(participant)"] --> remove["RuntimeState.removeParticipant"]
|
|
166
|
+
remove --> left["publish participant.left"]
|
|
167
|
+
joined --> all["every joined participant"]
|
|
168
|
+
left --> remaining["remaining participants"]
|
|
212
169
|
```
|
|
213
170
|
|
|
214
|
-
|
|
171
|
+
- `join(participant)` adds the participant to `RuntimeState` and publishes `participant.joined`. Joining an id that is already present is a no-op.
|
|
172
|
+
- `leave(participant)` removes them and publishes `participant.left` to whoever is still joined.
|
|
173
|
+
- Each `runLoop(agentId, ...)` creates a new loop with its own `loopId` and does **not** await it. Two agents can think, call tools, and answer at the same time.
|
|
174
|
+
- Events from any loop fan out to **every** joined participant. A slow processor is not a barrier: the runtime does not await `processor.apply`.
|
|
175
|
+
- Shared mutable data lives on your `RuntimeState` subclass. Situation processors (and interception) are how participants adapt to it.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const observer = createHuman({ name: "Observer", capabilities: [], handlers: [] })
|
|
179
|
+
|
|
180
|
+
join(human)
|
|
181
|
+
join(agent)
|
|
182
|
+
join(observer)
|
|
215
183
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
184
|
+
sendMessage("Hello", human.getId())
|
|
185
|
+
|
|
186
|
+
leave(observer)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
An agent can wait until a collaborator has joined (`participant.joined`) before calling `runLoop`, or clean up shared state when someone leaves (`participant.left`).
|
|
219
190
|
|
|
220
191
|
---
|
|
221
192
|
|
|
222
|
-
##
|
|
193
|
+
## Semantic events
|
|
223
194
|
|
|
224
|
-
|
|
195
|
+
The runtime is an event bus. Everything interesting is a **`SemanticEvent`**:
|
|
225
196
|
|
|
226
|
-
|
|
197
|
+
| Field | Meaning |
|
|
198
|
+
| ------------ | ------------------------------------------------------------------------ |
|
|
199
|
+
| `type` | String name of the event (`"message.sent"`, `"inference.completed"`, …). |
|
|
200
|
+
| `producerId` | Id of the participant that caused it. |
|
|
201
|
+
| `occurredAt` | When it was created. |
|
|
202
|
+
| `payload` | Typed data for that event. |
|
|
227
203
|
|
|
228
|
-
|
|
229
|
-
import { BaseParticipant, Participant, SemanticEvent } from "@mozaik-ai/core"
|
|
204
|
+
`publish` delivers every event to every joined participant. There is no built-in “internal vs external” split — a situation specification filters on `event.type` and on `event.producerId` versus `participant.getId()`.
|
|
230
205
|
|
|
231
|
-
|
|
232
|
-
export class StreamingAgent extends BaseParticipant {
|
|
233
|
-
async onInternalEvent(event: SemanticEvent<unknown>): Promise<void> {
|
|
234
|
-
if (event.type === "response.output_text.delta") {
|
|
235
|
-
// e.g. keep a local buffer of partial output
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
206
|
+
Participants never poll. Custom events go through `sendEvent(event, senderId)` with any `type` string.
|
|
239
207
|
|
|
240
|
-
|
|
241
|
-
export class LiveTranscript extends BaseParticipant {
|
|
242
|
-
async onExternalEvent(source: Participant, event: SemanticEvent<unknown>): Promise<void> {
|
|
243
|
-
if (event.type === "response.output_text.delta") {
|
|
244
|
-
const { delta } = event.data as { delta: string }
|
|
245
|
-
process.stdout.write(delta)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
```
|
|
208
|
+
### Lifecycle and messaging
|
|
250
209
|
|
|
251
|
-
|
|
210
|
+
| Event | Published when | Payload |
|
|
211
|
+
| -------------------- | -------------------------------- | ----------------------------------------------------------- |
|
|
212
|
+
| `participant.joined` | `join(participant)` | Participant manifest (`id`, `name`, `role`, `capabilities`) |
|
|
213
|
+
| `participant.left` | `leave(participant)` | Participant manifest |
|
|
214
|
+
| `message.sent` | `sendMessage(message, senderId)` | `{ message: string }` |
|
|
215
|
+
| _custom_ | `sendEvent(event, senderId)` | Whatever you put on the event |
|
|
252
216
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
217
|
+
### Agent loop
|
|
218
|
+
|
|
219
|
+
Producer is the agent whose loop is running:
|
|
220
|
+
|
|
221
|
+
| Event | Published when | Payload |
|
|
222
|
+
| -------------------------- | -------------------------------------------------- | -------------------------------------------------------- |
|
|
223
|
+
| `context_update.started` | The loop begins appending the user message | Received message plus `loopId` |
|
|
224
|
+
| `context_update.completed` | Context is ready for inference | `InferenceInput` plus `loopId` |
|
|
225
|
+
| `inference.started` | The model call begins | `InferenceInput` |
|
|
226
|
+
| `inference.stream` | Each streaming chunk (only when `streaming: true`) | The inner provider event |
|
|
227
|
+
| `inference.completed` | The model call finished | `InferenceOutput` (`items`, `tokenUsage`, `rowResponse`) |
|
|
228
|
+
| `function_call.started` | A tool is about to run | `{ call, inferenceInput }` |
|
|
229
|
+
| `function_call.completed` | The tool returned | `FunctionCallOutputItem` |
|
|
230
|
+
| `model.answer` | The assistant message is committed | `{ answer: ModelMessageItem }` |
|
|
231
|
+
| `interception.started` | An `InterceptionHandler` matched a transition | The pending `LoopTransition` |
|
|
232
|
+
| `interception.finished` | The handler returned (possibly rewritten) | The transition that will execute |
|
|
233
|
+
|
|
234
|
+
### Streaming
|
|
256
235
|
|
|
257
|
-
Requesting streaming for a model whose specification has `supportsStreaming: false` fails
|
|
236
|
+
Pass `streaming: true` on the `InferenceInput` you give `runLoop`. The loop takes the `inference_streaming` path and publishes each provider chunk as `inference.stream` (the inner event is the payload). Requesting streaming for a model whose specification has `supportsStreaming: false` fails validation before the API is called.
|
|
258
237
|
|
|
259
238
|
---
|
|
260
239
|
|
|
261
|
-
##
|
|
240
|
+
## Situation handlers
|
|
262
241
|
|
|
263
|
-
|
|
242
|
+
Participants **react** by registering situation handlers. The runtime publishes events; a handler is a pair of “when” and “then”:
|
|
243
|
+
|
|
244
|
+
- **`SituationSpecification`** — `isSatisfiedBy({ event, participant })`. Compose with `.and()`, `.or()`, and `.not()`.
|
|
245
|
+
- **`SituationProcessor`** — `apply(context)` is the reaction: `runLoop`, `sendMessage`, mutate runtime state, log, persist, …
|
|
246
|
+
|
|
247
|
+
```mermaid
|
|
248
|
+
flowchart LR
|
|
249
|
+
Human[Participant] -->|"sendMessage(text, senderId)"| Runtime(("Runtime"))
|
|
250
|
+
Agent[Participant] -->|"runLoop"| Runtime
|
|
251
|
+
Observer[Participant] -->|join| Runtime
|
|
252
|
+
Runtime -->|"SemanticEvent"| Human
|
|
253
|
+
Runtime -->|"SemanticEvent"| Agent
|
|
254
|
+
Runtime -->|"SemanticEvent"| Observer
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Fan-out is synchronous and does not await processors, so a slow listener never blocks producers or other listeners. Participants start receiving events as soon as they `join()`.
|
|
258
|
+
|
|
259
|
+
There are no built-in specifications — write the ones you need. Self versus others is a filter on `producerId`, not a second handler API.
|
|
264
260
|
|
|
265
261
|
```ts
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
262
|
+
import {
|
|
263
|
+
defineRuntime,
|
|
264
|
+
RuntimeState,
|
|
265
|
+
createAgent,
|
|
266
|
+
createHuman,
|
|
267
|
+
SituationSpecification,
|
|
268
|
+
Agent,
|
|
269
|
+
type SituationHandler,
|
|
270
|
+
type SituationContext,
|
|
271
|
+
} from "@mozaik-ai/core"
|
|
271
272
|
|
|
272
|
-
|
|
273
|
-
onLeft(): void {
|
|
274
|
-
console.log("I left the environment")
|
|
275
|
-
}
|
|
273
|
+
class AppState extends RuntimeState {}
|
|
276
274
|
|
|
277
|
-
|
|
278
|
-
onParticipantJoined(participant: Participant): void {
|
|
279
|
-
console.log(`${participant.constructor.name} joined`)
|
|
280
|
-
}
|
|
275
|
+
const { initializeRuntime, join, sendMessage, runLoop } = defineRuntime<AppState>()
|
|
281
276
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
277
|
+
initializeRuntime({ state: new AppState() })
|
|
278
|
+
|
|
279
|
+
class WhenOthersSendAMessage extends SituationSpecification {
|
|
280
|
+
isSatisfiedBy({ event, participant }: SituationContext): boolean {
|
|
281
|
+
return event.type === "message.sent" && event.producerId !== participant.getId()
|
|
285
282
|
}
|
|
286
283
|
}
|
|
287
|
-
```
|
|
288
284
|
|
|
289
|
-
|
|
285
|
+
const thinkOnMessage: SituationHandler = {
|
|
286
|
+
specification: new WhenOthersSendAMessage(),
|
|
287
|
+
processor: {
|
|
288
|
+
apply({ event, participant }) {
|
|
289
|
+
if (!(participant instanceof Agent)) return
|
|
290
|
+
const { message } = event.payload as { message: string }
|
|
291
|
+
runLoop(participant.getId(), message, {
|
|
292
|
+
model: "gpt-5.5",
|
|
293
|
+
context: participant.getMemory().getContext(),
|
|
294
|
+
tools: participant.getTools(),
|
|
295
|
+
})
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
}
|
|
290
299
|
|
|
291
|
-
|
|
300
|
+
const agent = createAgent({
|
|
301
|
+
name: "Assistant",
|
|
302
|
+
capabilities: ["inference"],
|
|
303
|
+
instruction: "You are a helpful teammate.",
|
|
304
|
+
tools: [],
|
|
305
|
+
handlers: [thinkOnMessage],
|
|
306
|
+
})
|
|
292
307
|
|
|
293
|
-
|
|
308
|
+
const human = createHuman({ name: "User", capabilities: [], handlers: [] })
|
|
294
309
|
|
|
295
|
-
|
|
310
|
+
join(human)
|
|
311
|
+
join(agent)
|
|
296
312
|
|
|
297
|
-
|
|
313
|
+
sendMessage("Hello", human.getId())
|
|
314
|
+
```
|
|
298
315
|
|
|
299
|
-
|
|
316
|
+
An observer is the same pattern with processors that only take side actions:
|
|
300
317
|
|
|
301
318
|
```ts
|
|
302
|
-
|
|
319
|
+
class WhenModelAnswers extends SituationSpecification {
|
|
320
|
+
isSatisfiedBy({ event }: SituationContext): boolean {
|
|
321
|
+
return event.type === "model.answer"
|
|
322
|
+
}
|
|
323
|
+
}
|
|
303
324
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
325
|
+
const transcript: SituationHandler = {
|
|
326
|
+
specification: new WhenModelAnswers(),
|
|
327
|
+
processor: {
|
|
328
|
+
apply({ event }) {
|
|
329
|
+
console.log("[model.answer]", event.producerId, event.payload)
|
|
330
|
+
},
|
|
331
|
+
},
|
|
307
332
|
}
|
|
333
|
+
|
|
334
|
+
const observer = createHuman({ name: "Transcript", capabilities: [], handlers: [transcript] })
|
|
335
|
+
join(observer)
|
|
308
336
|
```
|
|
309
337
|
|
|
310
|
-
|
|
338
|
+
Three things to note:
|
|
311
339
|
|
|
312
|
-
|
|
340
|
+
1. “Act on my own outputs” versus “observe others” is a specification filter on `event.producerId` versus `participant.getId()` — not a separate `onExternal*` API.
|
|
341
|
+
2. Do not `await` `runLoop` or `sendMessage` inside a processor. They return `void` and keep running in the background while the runtime keeps delivering events.
|
|
342
|
+
3. Behaviors compose by **reaction**, not orchestration. Add a second agent whose spec matches `model.answer` and you get a critique loop. Add a transcript observer and you get a UI stream. Neither change touches the existing participants.
|
|
313
343
|
|
|
314
|
-
|
|
344
|
+
---
|
|
315
345
|
|
|
316
|
-
|
|
317
|
-
import { BaseParticipant, Participant, AgenticError } from "@mozaik-ai/core"
|
|
346
|
+
## The agent loop
|
|
318
347
|
|
|
319
|
-
|
|
320
|
-
onError(error: AgenticError): void {
|
|
321
|
-
console.error("my handler threw:", error.message)
|
|
322
|
-
}
|
|
348
|
+
`runLoop(agentId, message, inferenceInput, interceptionHandler?)` drives one agent turn as a state machine. It starts at `context_update` and runs until `idle`. Tool execution and the follow-up inference live inside the loop — you do not call a separate function-call runner.
|
|
323
349
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
350
|
+
Each step: optionally intercept the pending transition, execute the state, then resolve the next transition.
|
|
351
|
+
|
|
352
|
+
```mermaid
|
|
353
|
+
flowchart TD
|
|
354
|
+
Start([runLoop]) --> CU[context_update]
|
|
355
|
+
CU --> INF[inference]
|
|
356
|
+
INF -->|function_call item| FC[function_call]
|
|
357
|
+
INF -->|assistant message| MM[model_message]
|
|
358
|
+
FC --> INF
|
|
359
|
+
MM --> Idle[idle]
|
|
328
360
|
```
|
|
329
361
|
|
|
330
|
-
|
|
362
|
+
| State | What it does |
|
|
363
|
+
| --------------------- | ------------------------------------------------------------------------------------------------ |
|
|
364
|
+
| `context_update` | Appends the user message to the agent's context. |
|
|
365
|
+
| `inference` | Calls the model and waits for the full response. |
|
|
366
|
+
| `inference_streaming` | Same call, streaming; each chunk is published as `inference.stream`. |
|
|
367
|
+
| `function_call` | Runs the matching tool, writes the call and output back into context, then returns to inference. |
|
|
368
|
+
| `model_message` | Publishes `model.answer`, then the loop goes `idle`. |
|
|
369
|
+
|
|
370
|
+
`runLoop` is fire-and-forget. Each invocation gets a unique `loopId`, so concurrent agents (or two loops on the same agent) do not share a cursor.
|
|
331
371
|
|
|
332
372
|
---
|
|
333
373
|
|
|
334
|
-
##
|
|
374
|
+
## Interception
|
|
335
375
|
|
|
336
|
-
|
|
376
|
+
An **`InterceptionHandler`** inspects or rewrites a loop transition **before** that state runs. Pass it as the optional fourth argument to `runLoop`:
|
|
337
377
|
|
|
338
378
|
```ts
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
ReasoningItem,
|
|
345
|
-
ModelMessageItem,
|
|
346
|
-
} from "@mozaik-ai/core"
|
|
347
|
-
|
|
348
|
-
export class TranscriptLogger extends BaseParticipant {
|
|
349
|
-
async onMessage(message: string): Promise<void> {
|
|
350
|
-
console.log("[message]", message)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async onExternalFunctionCall(source: Participant, item: FunctionCallItem): Promise<void> {
|
|
354
|
-
console.log(`[${source.constructor.name}] function_call`, item)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async onExternalFunctionCallOutput(source: Participant, item: FunctionCallOutputItem): Promise<void> {
|
|
358
|
-
console.log(`[${source.constructor.name}] function_call_output`, item)
|
|
359
|
-
}
|
|
379
|
+
interface InterceptionHandler {
|
|
380
|
+
isSatisfiedBy(transition: ExecutableTransition): boolean
|
|
381
|
+
handle(transition: ExecutableTransition): Promise<ExecutableTransition>
|
|
382
|
+
}
|
|
383
|
+
```
|
|
360
384
|
|
|
361
|
-
|
|
362
|
-
console.log(`[${source.constructor.name}] reasoning`, item)
|
|
363
|
-
}
|
|
385
|
+
When `isSatisfiedBy` is true, the loop publishes `interception.started`, awaits `handle` (which may change `nextStateId` and `input`), publishes `interception.finished`, then executes the returned transition.
|
|
364
386
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
387
|
+
```ts
|
|
388
|
+
import { type InterceptionHandler } from "@mozaik-ai/core"
|
|
389
|
+
|
|
390
|
+
const inspectFunctionCalls: InterceptionHandler = {
|
|
391
|
+
isSatisfiedBy(transition) {
|
|
392
|
+
return transition.nextStateId === "function_call"
|
|
393
|
+
},
|
|
394
|
+
async handle(transition) {
|
|
395
|
+
if (transition.nextStateId === "function_call") {
|
|
396
|
+
console.log("about to call", transition.input.call.name)
|
|
397
|
+
}
|
|
398
|
+
return transition
|
|
399
|
+
},
|
|
368
400
|
}
|
|
401
|
+
|
|
402
|
+
runLoop(agent.getId(), message, inferenceInput, inspectFunctionCalls)
|
|
369
403
|
```
|
|
370
404
|
|
|
405
|
+
Use it to steer loop execution by rewriting the next state — for example, swapping a tool call’s input — without putting that logic inside the agent’s situation handlers. That is how you keep a human in the loop, or another agent, to control what runs next.
|
|
406
|
+
|
|
371
407
|
---
|
|
372
408
|
|
|
373
409
|
## Examples
|
|
@@ -382,7 +418,7 @@ Working examples are available here: [mozaik-examples](https://github.com/jigjoy
|
|
|
382
418
|
|
|
383
419
|
```mermaid
|
|
384
420
|
flowchart LR
|
|
385
|
-
Conductor[Conductor] -->|"RunStart / LevelCompute / StorySpawn"| Bus(("Mozaik
|
|
421
|
+
Conductor[Conductor] -->|"RunStart / LevelCompute / StorySpawn"| Bus(("Mozaik Runtime"))
|
|
386
422
|
Factory[StoryFactory] -->|"spawn StoryAgent"| Bus
|
|
387
423
|
Story[StoryAgent] -->|"StoryResult / retries"| Bus
|
|
388
424
|
Librarian[Librarian] -->|"index exploration outputs"| Bus
|