@mozaik-ai/core 3.12.0 → 3.13.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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ![npm downloads](https://img.shields.io/npm/dt/@mozaik-ai/core) ![npm downloads weekly](https://img.shields.io/npm/dw/@mozaik-ai/core) ![npm version](https://img.shields.io/npm/v/@mozaik-ai/core)
6
6
 
7
- In Mozaik, humans, agents, observers, and tools are all `Participant`s of the same `AgenticEnvironment`. Each participant runs **non-blocking** and produces events into the environment: plain-text **messages** for conversational input/output, and typed `ContextItem`s for model internals (function calls, function call outputs, reasoning, model messages). Every other participant sees those events in real time and can react, intercept, or stay silent — without any central scheduler.
7
+ Mozaik is built for **truly parallel, non-blocking agents**. It enables agents to communicate through semantic events, react to each other’s outputs, run asynchronously, and collaborate without being locked into a single sequential workflow.
8
8
 
9
9
  ---
10
10
 
@@ -30,11 +30,17 @@ pnpm add @mozaik-ai/core
30
30
 
31
31
  ## API Key Configuration
32
32
 
33
+ Mozaik picks a provider from the model name you pass to `runInference`, and each provider's SDK reads its credential from the environment. Set the keys for the providers you use:
34
+
33
35
  ```env
34
36
  # .env
35
37
  OPENAI_API_KEY=your-openai-key-here
38
+ ANTHROPIC_API_KEY=your-anthropic-key-here
39
+ GEMINI_API_KEY=your-gemini-key-here
36
40
  ```
37
41
 
42
+ DeepSeek models run through the OpenAI-compatible chat-completions endpoint, so they use an OpenAI-style credential and base URL (`OPENAI_API_KEY` / `OPENAI_BASE_URL`) pointed at DeepSeek.
43
+
38
44
  ---
39
45
 
40
46
  ## The agentic environment
@@ -58,14 +64,16 @@ OPENAI_API_KEY=your-openai-key-here
58
64
  | `onExternalModelMessage` | another agent's inference returns an assistant message |
59
65
  | `onInternalEvent` | its own inference emits a semantic stream event |
60
66
  | `onExternalEvent` | another participant emits a semantic stream event |
67
+ | `onError` | one of its own handlers throws |
68
+ | `onParticipantError` | another participant's handler throws |
61
69
 
62
- Every handler defaults to a no-op — override only the ones you care about.
70
+ Every handler defaults to a no-op on `BaseParticipant` — override only the ones you care about.
63
71
 
64
72
  ```mermaid
65
73
  flowchart LR
66
- Human[BaseHuman] -->|sendMessage| Env(("AgenticEnvironment"))
67
- Agent[BaseAgent] -->|"runInference / executeFunctionCall"| Env
68
- Observer[Custom Participant] -->|join| Env
74
+ Human[Participant] -->|"sendMessage(env, text, caller)"| Env(("AgenticEnvironment"))
75
+ Agent[Participant] -->|"runInference / executeFunctionCall"| Env
76
+ Observer[Participant] -->|join| Env
69
77
  Env -->|"onMessage / onExternal*"| Human
70
78
  Env -->|"onFunctionCall / onReasoning / …"| Agent
71
79
  Env -->|"onExternal*"| Observer
@@ -76,52 +84,58 @@ flowchart LR
76
84
 
77
85
  ## Non-blocking participants
78
86
 
79
- Mozaik ships three ready-to-use participants:
87
+ A participant is any subclass of `Participant`. Use `BaseParticipant` as a base when you only want to override a few handlers — every handler it defines is a no-op. The _role_ (human, agent, observer) is just which capability functions a participant calls and which handlers it overrides:
80
88
 
81
- | Participant | Role |
82
- | -------------- | -------------------------------------------------------------------------------- |
83
- | `BaseHuman` | Sends messages with `sendMessage(environment, text)` |
84
- | `BaseAgent` | Runs inference and function calls via `InferenceRunner` and `FunctionCallRunner` |
85
- | `BaseObserver` | Listens only; no inference |
89
+ | Role | How to build it |
90
+ | -------- | --------------------------------------------------------------------------- |
91
+ | Human | A participant that calls `sendMessage(environment, text, caller)` |
92
+ | Agent | A participant that calls `runInference(...)` and `executeFunctionCall(...)` |
93
+ | Observer | A participant that only overrides handlers and never runs inference |
86
94
 
87
95
  ```ts
88
96
  import {
89
97
  AgenticEnvironment,
90
- BaseAgent,
91
- BaseHuman,
92
- OpenAIInferenceRunner,
93
- DefaultFunctionCallRunner,
94
- Gpt54Mini,
98
+ BaseParticipant,
95
99
  ModelContext,
100
+ UserMessageItem,
101
+ runInference,
102
+ sendMessage,
96
103
  } from "@mozaik-ai/core"
97
104
 
98
105
  const environment = new AgenticEnvironment()
99
106
 
100
- const human = new BaseHuman()
101
- const agent = new BaseAgent(new OpenAIInferenceRunner(), new DefaultFunctionCallRunner())
102
- const observer = new BaseObserver()
107
+ const human = new BaseParticipant()
108
+
109
+ class Agent extends BaseParticipant {
110
+ private readonly context = ModelContext.create("demo")
111
+
112
+ async onMessage(message: string): Promise<void> {
113
+ this.context.addContextItem(UserMessageItem.create(message))
114
+ runInference({ model: "gpt-5.4", context: this.context, caller: this, environment })
115
+ }
116
+ }
117
+
118
+ const agent = new Agent()
119
+ const observer = new BaseParticipant()
103
120
 
104
121
  human.join(environment)
105
122
  agent.join(environment)
106
123
  observer.join(environment)
107
124
 
108
- environment.start()
109
-
110
- human.sendMessage(environment, "Hello")
125
+ sendMessage(environment, "Hello", human)
111
126
  ```
112
127
 
113
- The environment fans every item out to every subscriber synchronously and without awaiting them, so a slow listener never blocks producers or other listeners.
128
+ Participants react as soon as they `join()` agentic environment. The environment fans every item out to every subscriber synchronously and without awaiting them, so a slow listener never blocks producers or other listeners.
114
129
 
115
130
  ---
116
131
 
117
132
  ## Reactive agent
118
133
 
119
- A reactive agent extends `BaseAgent` and overrides the handlers it wants to react on. Each handler is already a no-op in the base class, so only the relevant ones need bodies:
134
+ A reactive agent extends `BaseParticipant` and overrides the handlers it wants to react on. Each handler is already a no-op in the base class, so only the relevant ones need bodies. Capabilities are the free functions `runInference` and `executeFunctionCall` — the participant passes itself as `caller`:
120
135
 
121
136
  ```ts
122
137
  import {
123
- BaseAgent,
124
- Participant,
138
+ BaseParticipant,
125
139
  UserMessageItem,
126
140
  FunctionCallItem,
127
141
  FunctionCallOutputItem,
@@ -129,38 +143,50 @@ import {
129
143
  ModelMessageItem,
130
144
  AgenticEnvironment,
131
145
  ModelContext,
132
- GenerativeModel,
133
- InferenceRunner,
134
- FunctionCallRunner,
146
+ ModelName,
147
+ Tool,
148
+ runInference,
149
+ executeFunctionCall,
135
150
  } from "@mozaik-ai/core"
136
151
 
137
- export class ReactiveAgent extends BaseAgent {
152
+ export class ReactiveAgent extends BaseParticipant {
138
153
  constructor(
139
- inferenceRunner: InferenceRunner,
140
- functionCallRunner: FunctionCallRunner,
141
154
  private readonly environment: AgenticEnvironment,
142
155
  private readonly context: ModelContext,
143
- private readonly model: GenerativeModel,
156
+ private readonly tools: Tool[] = [],
144
157
  ) {
145
- super(inferenceRunner, functionCallRunner)
158
+ super()
146
159
  }
147
160
 
148
161
  // A message from a human (or any other participant) → record it and think.
149
162
  async onMessage(message: string): Promise<void> {
150
163
  this.context.addContextItem(UserMessageItem.create(message))
151
- this.runInference(this.environment, this.context, this.model)
164
+ runInference({
165
+ model: 'gpt-5.5',
166
+ context: this.context,
167
+ tools: this.tools,
168
+ caller: this,
169
+ environment: this.environment,
170
+ })
152
171
  }
153
172
 
154
173
  // The agent just produced a function call → execute it.
155
174
  async onFunctionCall(item: FunctionCallItem): Promise<void> {
156
175
  this.context.addContextItem(item)
157
- this.executeFunctionCall(this.environment, item)
176
+ const tool = this.tools.find((t) => t.name === item.name)
177
+ if (tool) executeFunctionCall(this.environment, item, tool, this)
158
178
  }
159
179
 
160
180
  // The tool just produced an output → feed it back and run inference again.
161
181
  async onFunctionCallOutput(item: FunctionCallOutputItem): Promise<void> {
162
182
  this.context.addContextItem(item)
163
- this.runInference(this.environment, this.context, this.model)
183
+ runInference({
184
+ model: 'gpt-5.5',
185
+ context: this.context,
186
+ tools: this.tools,
187
+ caller: this,
188
+ environment: this.environment,
189
+ })
164
190
  }
165
191
 
166
192
  // Keep the local context in sync with model-emitted reasoning and replies.
@@ -177,22 +203,22 @@ export class ReactiveAgent extends BaseAgent {
177
203
  Three things to note:
178
204
 
179
205
  1. The split between self handlers and `onExternal*` handlers means a participant can encode "act on my own outputs" separately from "observe others", without inspecting `source` by hand.
180
- 2. The agent never `await`s its own capability calls inside the handlers — those methods are non-blocking, so the environment keeps delivering events while inference and tool execution run in the background.
206
+ 2. The agent never `await`s its capability calls inside the handlers — `runInference` and `executeFunctionCall` are fire-and-forget (they return `void`), so the environment keeps delivering events while inference and tool execution run in the background.
181
207
  3. Behaviors compose by **reaction**, not orchestration. Add a second agent that overrides `onExternalModelMessage` and you get a critique loop. Add a `TranscriptLogger` and you get a UI stream. Neither change touches the existing participants.
182
208
 
183
209
  ---
184
210
 
185
211
  ## Streaming and semantic events
186
212
 
187
- When inference runs with streaming enabled (`model.setStreaming(true)` on a model that supports it), the runner does not wait for the full response. As the provider emits chunks, `OpenAIInferenceRunner` yields **`SemanticEvent`** items (`type` + `data`) and the environment delivers each one to **every** joined participant immediately — the same fan-out as messages and context items. Participants react in real time by overriding the stream handlers; no participant needs to poll or share a callback.
213
+ When inference runs with streaming enabled (`streaming: true` on the `runInference` params, for a model that supports it), the runner does not wait for the full response. As the provider emits chunks, the endpoint yields **`SemanticEvent`** items (`type` + `data`) and the environment delivers each one to **every** joined participant immediately — the same fan-out as messages and context items. Participants react in real time by overriding the stream handlers; no participant needs to poll or share a callback.
188
214
 
189
215
  The producing participant receives `onInternalEvent`; everyone else receives `onExternalEvent(source, event)`:
190
216
 
191
217
  ```ts
192
- import { BaseAgent, Participant, SemanticEvent } from "@mozaik-ai/core"
218
+ import { BaseParticipant, Participant, SemanticEvent } from "@mozaik-ai/core"
193
219
 
194
220
  // Agent that runs streaming inference — can observe its own stream chunks.
195
- export class StreamingAgent extends BaseAgent {
221
+ export class StreamingAgent extends BaseParticipant {
196
222
  async onInternalEvent(event: SemanticEvent<unknown>): Promise<void> {
197
223
  if (event.type === "response.output_text.delta") {
198
224
  // e.g. keep a local buffer of partial output
@@ -201,20 +227,71 @@ export class StreamingAgent extends BaseAgent {
201
227
  }
202
228
 
203
229
  // Any other participant — UI, logger, second agent — reacts to another's stream.
204
- export class LiveTranscript extends Participant {
230
+ export class LiveTranscript extends BaseParticipant {
205
231
  async onExternalEvent(source: Participant, event: SemanticEvent<unknown>): Promise<void> {
206
232
  if (event.type === "response.output_text.delta") {
207
233
  const { delta } = event.data as { delta: string }
208
234
  process.stdout.write(delta)
209
235
  }
210
236
  }
211
-
212
- // Self-emitted stream events are unused for a pure observer.
213
- async onInternalEvent(): Promise<void> {}
214
237
  }
215
238
  ```
216
239
 
217
- Enable streaming on the model before calling `runInference` as usual. `setStreaming(true)` on a model without `supportStreaming` throws before the API is called.
240
+ Enable streaming by passing `streaming: true` to `runInference`:
241
+
242
+ ```ts
243
+ runInference({ model: "gpt-5.4", context, caller: this, environment, streaming: true })
244
+ ```
245
+
246
+ Requesting streaming for a model whose specification has `supportsStreaming: false` fails request validation before the API is called.
247
+
248
+ ---
249
+
250
+ ## Structured output
251
+
252
+ When you need the model to respond with a specific JSON shape instead of free-form text, use structured output. Pass a `structuredOutput` (a `StructuredOutputFormat`) on the `runInference` params and the provider will enforce the JSON Schema:
253
+
254
+ ```ts
255
+ import { runInference } from "@mozaik-ai/core"
256
+
257
+ runInference({
258
+ model: "gpt-5.4",
259
+ context,
260
+ caller: this,
261
+ environment,
262
+ structuredOutput: {
263
+ name: "weather",
264
+ schema: {
265
+ type: "object",
266
+ properties: {
267
+ city: { type: "string" },
268
+ temperature: { type: "number" },
269
+ condition: { type: "string" },
270
+ },
271
+ required: ["city", "temperature", "condition"],
272
+ additionalProperties: false,
273
+ },
274
+ strict: true,
275
+ },
276
+ })
277
+ ```
278
+
279
+ The response comes back as a `ModelMessageItem` with valid JSON in the text field — no new item type, consistent with OpenResponses.
280
+
281
+ Structured output works alongside tools and streaming. When streaming is enabled, partial JSON chunks arrive as `SemanticEvent`s and the final event contains the complete response.
282
+
283
+ To return to free-form text, simply omit `structuredOutput` on the next `runInference` call.
284
+
285
+ ### Provider support
286
+
287
+ | Provider | Models | Strict schema enforcement |
288
+ | --------- | --------------------------------------------------------------------- | ------------------------------------------------------ |
289
+ | OpenAI | gpt-5.4, gpt-5.4-mini, gpt-5.4-nano, gpt-5.5 | Yes |
290
+ | Anthropic | claude-opus-4-7, claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5 | Yes |
291
+ | Gemini | gemini-3.1-pro-preview, gemini-3.5-flash | Yes |
292
+ | DeepSeek | deepseek-v4-flash, deepseek-v4-pro | Not supported — use prompt-based JSON guidance instead |
293
+
294
+ Requesting structured output for a model whose specification has `supportsStructuredOutput: false` fails request validation before the API call.
218
295
 
219
296
  ---
220
297
 
@@ -223,24 +300,24 @@ Enable streaming on the model before calling `runInference` as usual. `setStream
223
300
  Every participant receives lifecycle notifications when it or others join/leave an environment:
224
301
 
225
302
  ```ts
226
- export class TeamAgent extends BaseAgent {
303
+ export class TeamAgent extends BaseParticipant {
227
304
  // Called when this participant joins an environment.
228
- onJoined(environment: AgenticEnvironment): void {
305
+ onJoined(): void {
229
306
  console.log("I joined the environment")
230
307
  }
231
308
 
232
309
  // Called when this participant leaves an environment.
233
- onLeft(environment: AgenticEnvironment): void {
310
+ onLeft(): void {
234
311
  console.log("I left the environment")
235
312
  }
236
313
 
237
314
  // Called when another participant joins the same environment.
238
- onParticipantJoined(participant: Participant, environment: AgenticEnvironment): void {
315
+ onParticipantJoined(participant: Participant): void {
239
316
  console.log(`${participant.constructor.name} joined`)
240
317
  }
241
318
 
242
319
  // Called when another participant leaves the same environment.
243
- onParticipantLeft(participant: Participant, environment: AgenticEnvironment): void {
320
+ onParticipantLeft(participant: Participant): void {
244
321
  console.log(`${participant.constructor.name} left`)
245
322
  }
246
323
  }
@@ -254,39 +331,77 @@ This lets participants react to membership changes — for example, an agent cou
254
331
 
255
332
  Participants can listen to external events and react by overriding methods like `onMessage`, `onExternalFunctionCall`, `onExternalFunctionCallOutput`, `onExternalReasoning`, and `onExternalModelMessage`.
256
333
 
334
+ ### Selective listening
335
+
336
+ By default a participant reacts to events from every other participant. To scope a participant so it only reacts to specific participant _types_, populate its `listens` list with those classes. When `listens` is non-empty, the environment only delivers external events whose source is an instance of one of the listed classes:
337
+
338
+ ```ts
339
+ import { BaseParticipant } from "@mozaik-ai/core"
340
+
341
+ export class Critic extends BaseParticipant {
342
+ // Only react to events produced by Writer participants.
343
+ protected listens = [Writer]
344
+ }
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Error handling
350
+
351
+ When any handler throws, the environment catches it and routes it as an `AgenticError` instead of crashing the run. The participant whose handler threw receives `onError(error)`; every other participant receives `onParticipantError(source, error)`. After its own `onError`, the failing participant is marked inactive in that environment so it stops receiving further events.
352
+
353
+ ```ts
354
+ import { BaseParticipant, Participant, AgenticError } from "@mozaik-ai/core"
355
+
356
+ export class ResilientAgent extends BaseParticipant {
357
+ onError(error: AgenticError): void {
358
+ console.error("my handler threw:", error.message)
359
+ }
360
+
361
+ onParticipantError(source: Participant, error: AgenticError): void {
362
+ console.warn(`${source.constructor.name} failed:`, error.message)
363
+ }
364
+ }
365
+ ```
366
+
367
+ `AgenticError` carries the originating participant (`getSource()`) and environment (`getEnvironment()`).
368
+
369
+ ---
370
+
257
371
  ## Passive observer
258
372
 
259
- You can create observers that don't run inference themselves but watch what's happening in the conversation and take side actions (logging, metrics, persistence, etc.). Subclass `Participant` and override only the handlers you care about:
373
+ You can create observers that don't run inference themselves but watch what's happening in the conversation and take side actions (logging, metrics, persistence, etc.). Subclass `BaseParticipant` and override only the handlers you care about — everything else stays a no-op:
260
374
 
261
375
  ```ts
262
- import { Participant, FunctionCallItem, FunctionCallOutputItem, ReasoningItem, ModelMessageItem } from "@mozaik-ai/core"
376
+ import {
377
+ BaseParticipant,
378
+ Participant,
379
+ FunctionCallItem,
380
+ FunctionCallOutputItem,
381
+ ReasoningItem,
382
+ ModelMessageItem,
383
+ } from "@mozaik-ai/core"
263
384
 
264
- export class TranscriptLogger extends Participant {
385
+ export class TranscriptLogger extends BaseParticipant {
265
386
  async onMessage(message: string): Promise<void> {
266
387
  console.log("[message]", message)
267
388
  }
268
389
 
269
390
  async onExternalFunctionCall(source: Participant, item: FunctionCallItem): Promise<void> {
270
- console.log(`[${source.constructor.name}] function_call`, item.toJSON())
391
+ console.log(`[${source.constructor.name}] function_call`, item)
271
392
  }
272
393
 
273
394
  async onExternalFunctionCallOutput(source: Participant, item: FunctionCallOutputItem): Promise<void> {
274
- console.log(`[${source.constructor.name}] function_call_output`, item.toJSON())
395
+ console.log(`[${source.constructor.name}] function_call_output`, item)
275
396
  }
276
397
 
277
398
  async onExternalReasoning(source: Participant, item: ReasoningItem): Promise<void> {
278
- console.log(`[${source.constructor.name}] reasoning`, item.toJSON())
399
+ console.log(`[${source.constructor.name}] reasoning`, item)
279
400
  }
280
401
 
281
402
  async onExternalModelMessage(source: Participant, item: ModelMessageItem): Promise<void> {
282
- console.log(`[${source.constructor.name}] model_message`, item.toJSON())
403
+ console.log(`[${source.constructor.name}] model_message`, item)
283
404
  }
284
-
285
- // Self-emitted handlers (onFunctionCall, onReasoning, …) can be no-ops for a pure observer.
286
- async onFunctionCall(): Promise<void> {}
287
- async onFunctionCallOutput(): Promise<void> {}
288
- async onReasoning(): Promise<void> {}
289
- async onModelMessage(): Promise<void> {}
290
405
  }
291
406
  ```
292
407
 
@@ -294,7 +409,7 @@ export class TranscriptLogger extends Participant {
294
409
 
295
410
  ## Context and models (reference)
296
411
 
297
- `ModelContext` is the ordered list of `ContextItem`s a `GenerativeModel` is asked to reason over. It is constructed and mutated explicitly — typically inside a participant in response to delivered items.
412
+ `ModelContext` is the ordered list of `ContextItem`s a model is asked to reason over. It is constructed and mutated explicitly — typically inside a participant in response to delivered items.
298
413
 
299
414
  ```ts
300
415
  import { ModelContext, DeveloperMessageItem, UserMessageItem, InMemoryModelContextRepository } from "@mozaik-ai/core"
@@ -309,55 +424,47 @@ await repo.save(context)
309
424
 
310
425
  Implement `ModelContextRepository` to plug in any storage backend.
311
426
 
312
- The default inference path is `OpenAIInferenceRunner`, which maps `ModelContext` to the OpenAI Responses API and back into typed `ContextItem`s (and `SemanticEvent`s when streaming). Bundled models: `Gpt54`, `Gpt54Mini`, `Gpt54Nano`, `Gpt55`.
427
+ A model is selected by its `ModelName` string. Mozaik resolves the name to a provider `Endpoint` and a `ModelSpecification` internally, maps the `ModelContext` to that provider's API, and returns typed `ContextItem`s (and `SemanticEvent`s when streaming). Bundled model names:
428
+
429
+ | Provider | `ModelName` values |
430
+ | --------- | ------------------------------------------------------------------------------------- |
431
+ | OpenAI | `"gpt-5.4"`, `"gpt-5.4-mini"`, `"gpt-5.4-nano"`, `"gpt-5.5"` |
432
+ | Anthropic | `"claude-haiku-4-5"`, `"claude-sonnet-4-6"`, `"claude-opus-4-7"`, `"claude-opus-4-8"` |
433
+ | Gemini | `"gemini-3.5-flash"`, `"gemini-3.1-pro-preview"` |
434
+ | DeepSeek | `"deepseek-v4-flash"`, `"deepseek-v4-pro"` |
435
+
436
+ You drive inference with the `runInference` capability; it streams the resulting items into the environment for participants to react to:
313
437
 
314
438
  ```ts
315
- import { OpenAIInferenceRunner, DefaultFunctionCallRunner, Gpt54, ModelContext } from "@mozaik-ai/core"
439
+ import { runInference, ModelContext } from "@mozaik-ai/core"
316
440
 
317
- const runner = new OpenAIInferenceRunner()
318
441
  const context = ModelContext.create("demo")
319
442
 
320
- for await (const item of runner.run(context, new Gpt54())) {
321
- // ReasoningItem | FunctionCallItem | ModelMessageItem | SemanticEvent
322
- }
443
+ runInference({ model: "gpt-5.4", context, caller: this, environment })
444
+ // → environment delivers ReasoningItem | FunctionCallItem | ModelMessageItem (and SemanticEvent when streaming)
323
445
  ```
324
446
 
325
447
  ---
326
448
 
327
- ## Overriding Generators
328
-
329
- Mozaik uses async generators for inference and function calls — that's what lets the system emit events incrementally so participants can react to them as they arrive. Swap any runner to change _how_ events are produced.
330
-
331
- Humans send text with `sendMessage(environment, message)`; other participants receive it via `onMessage`.
449
+ ### Tools
332
450
 
333
- ### Custom `InferenceRunner`
334
-
335
- An `InferenceRunner` can yield `ReasoningItem`, `FunctionCallItem`, `ModelMessageItem`, and `SemanticEvent` (when streaming).
451
+ A `Tool` is a function declaration with its own executor: `name`, `description`, JSON Schema `parameters`, `strict`, and an `invoke(args)` that runs the call. Pass tools on the `runInference` params; when the model emits a `FunctionCallItem`, run it with `executeFunctionCall`, which calls the matching tool's `invoke` and emits a `FunctionCallOutputItem`.
336
452
 
337
453
  ```ts
338
- import { InferenceRunner, ModelContext, GenerativeModel, OpenAIInferenceRunner } from "@mozaik-ai/core"
339
-
340
- // Use the bundled runner, or implement InferenceRunner for another provider.
341
- const runner: InferenceRunner = new OpenAIInferenceRunner()
342
- ```
343
-
344
- ### Custom `FunctionCallRunner`
345
-
346
- A `FunctionCallRunner` can only produce `FunctionCallOutputItem`.
347
-
348
- ```ts
349
- import { FunctionCallRunner, FunctionCallItem, FunctionCallOutputItem, Tool } from "@mozaik-ai/core"
350
-
351
- export class ToolRegistryFunctionCallRunner implements FunctionCallRunner {
352
- constructor(private readonly tools: Tool[]) {}
353
-
354
- async *run(call: FunctionCallItem, signal?: AbortSignal): AsyncIterable<FunctionCallOutputItem> {
355
- const tool = this.tools.find((t) => t.name === call.name)
356
- if (!tool) throw new Error(`Unknown tool: ${call.name}`)
357
-
358
- const result = await tool.invoke(JSON.parse(call.args))
359
- yield FunctionCallOutputItem.create(call.callId, JSON.stringify(result))
360
- }
454
+ import { Tool } from "@mozaik-ai/core"
455
+
456
+ const getWeather: Tool = {
457
+ type: "function",
458
+ name: "get_weather",
459
+ description: "Get the current weather for a city",
460
+ parameters: {
461
+ type: "object",
462
+ properties: { city: { type: "string" } },
463
+ required: ["city"],
464
+ additionalProperties: false,
465
+ },
466
+ strict: true,
467
+ invoke: async ({ city }: { city: string }) => ({ city, temperature: 21, condition: "sunny" }),
361
468
  }
362
469
  ```
363
470