@statelyai/agent 2.0.0-next.0 → 2.0.0-next.2

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 (51) hide show
  1. package/.changeset/cyan-carpets-perform.md +5 -0
  2. package/.changeset/fast-donkeys-argue.md +5 -0
  3. package/.changeset/grumpy-dolphins-think.md +17 -0
  4. package/.changeset/old-jobs-check.md +5 -0
  5. package/.changeset/old-teachers-tap.md +5 -0
  6. package/.changeset/pre.json +7 -1
  7. package/.changeset/smart-yaks-pull.md +23 -0
  8. package/CHANGELOG.md +52 -0
  9. package/dist/index.d.mts +69 -66
  10. package/dist/index.d.ts +69 -66
  11. package/dist/index.js +111 -79
  12. package/dist/index.mjs +114 -82
  13. package/examples/chatbot-alt.ts +1 -1
  14. package/examples/chatbot.ts +3 -3
  15. package/examples/cot.ts +7 -25
  16. package/examples/customer-service-sim.ts +7 -7
  17. package/examples/email.ts +37 -35
  18. package/examples/example.ts +3 -3
  19. package/examples/goal.ts +3 -3
  20. package/examples/joke.ts +3 -3
  21. package/examples/jugs.ts +5 -8
  22. package/examples/learn-from-feedback.ts +100 -0
  23. package/examples/multi.ts +1 -1
  24. package/examples/number.ts +3 -3
  25. package/examples/raffle.ts +3 -3
  26. package/examples/river-crossing.ts +5 -8
  27. package/examples/simple.ts +14 -11
  28. package/examples/summary.ts +3 -6
  29. package/examples/support.ts +43 -39
  30. package/examples/ticTacToe.ts +48 -6
  31. package/examples/todo.ts +3 -3
  32. package/examples/tutor.ts +4 -4
  33. package/examples/verify.ts +3 -3
  34. package/examples/weather-agent.ts +141 -0
  35. package/examples/weather.ts +24 -24
  36. package/examples/wiki.ts +1 -1
  37. package/examples/word.ts +9 -7
  38. package/package.json +6 -3
  39. package/src/agent.test.ts +161 -19
  40. package/src/agent.ts +66 -250
  41. package/src/decide.test.ts +172 -3
  42. package/src/decide.ts +50 -30
  43. package/src/middleware.ts +2 -14
  44. package/src/strategies/chainOfThought.ts +48 -0
  45. package/src/strategies/shortestPath.test.ts +91 -0
  46. package/src/{planners/shortestPathPlanner.ts → strategies/shortestPath.ts} +32 -19
  47. package/src/{planners/simplePlanner.ts → strategies/simple.ts} +28 -24
  48. package/src/types.ts +75 -34
  49. package/src/utils.ts +23 -0
  50. package/vitest.config.ts +9 -3
  51. package/src/strategies/chain-of-note.ts +0 -106
package/src/agent.ts CHANGED
@@ -2,7 +2,6 @@ import {
2
2
  Actor,
3
3
  ActorRefLike,
4
4
  AnyEventObject,
5
- AnyStateMachine,
6
5
  EventObject,
7
6
  fromTransition,
8
7
  Subscription,
@@ -11,7 +10,7 @@ import { ZodContextMapping, ZodEventMapping } from './schemas';
11
10
  import {
12
11
  AgentLogic,
13
12
  AgentMessage,
14
- AgentPlanner,
13
+ AgentStrategy,
15
14
  EventsFromZodEventMapping,
16
15
  GenerateTextOptions,
17
16
  AgentLongTermMemory,
@@ -23,14 +22,17 @@ import {
23
22
  AgentFeedback,
24
23
  AgentMessageInput,
25
24
  AgentFeedbackInput,
26
- AgentPlan,
25
+ AgentDecision,
27
26
  Compute,
28
27
  AgentDecisionInput,
29
28
  AgentDecideOptions,
29
+ AnyAgent,
30
+ EventsFromAgent,
31
+ AgentInteractInput,
30
32
  } from './types';
31
- import { simplePlanner } from './planners/simplePlanner';
33
+ import { simpleStrategy } from './strategies/simple';
32
34
  import { agentDecide } from './decide';
33
- import { getMachineHash, isActorRef, randomId } from './utils';
35
+ import { getMachineHash, isActorRef, isMachineActor, randomId } from './utils';
34
36
  import {
35
37
  experimental_wrapLanguageModel,
36
38
  LanguageModel,
@@ -68,17 +70,19 @@ export const agentLogic: AgentLogic<AnyEventObject> = fromTransition(
68
70
  });
69
71
  break;
70
72
  }
71
- case 'agent.plan': {
72
- state.plans.push(event.plan);
73
+ case 'agent.decision': {
74
+ state.decisions.push(event.decision);
73
75
  emit({
74
- type: 'plan',
75
- // @ts-ignore TODO: fix types in XState
76
- plan: event.plan,
76
+ type: 'decision',
77
+ decision: event.decision,
77
78
  });
78
79
  break;
79
80
  }
80
- default:
81
+ default: {
82
+ // unrecognized
83
+ console.warn('Unrecognized event', event);
81
84
  break;
85
+ }
82
86
  }
83
87
  return state;
84
88
  },
@@ -87,7 +91,7 @@ export const agentLogic: AgentLogic<AnyEventObject> = fromTransition(
87
91
  feedback: [],
88
92
  messages: [],
89
93
  observations: [],
90
- plans: [],
94
+ decisions: [],
91
95
  } as AgentMemoryContext)
92
96
  );
93
97
 
@@ -98,22 +102,19 @@ export function createAgent<
98
102
  TContext = ContextFromZodContextMapping<TContextSchema>
99
103
  >({
100
104
  id,
101
- name,
102
- description,
105
+ description: description,
103
106
  model,
104
107
  events,
105
108
  context,
106
- planner = simplePlanner as AgentPlanner<Agent<TContextSchema, TEventSchemas>>,
107
- stringify = JSON.stringify,
108
- getMemory,
109
+ episodeId,
110
+ strategy = simpleStrategy,
109
111
  logic = agentLogic as AgentLogic<TEvents>,
110
- ...generateTextOptions
111
112
  }: {
112
113
  /**
113
114
  * The unique identifier for the agent.
114
115
  *
115
116
  * This should be the same across all sessions of a specific agent, as it can be
116
- * used to retrieve memory for this agent.
117
+ * used to retrieve memory for previous episodes of this agent.
117
118
  *
118
119
  * @example
119
120
  * ```ts
@@ -124,10 +125,6 @@ export function createAgent<
124
125
  * ```
125
126
  */
126
127
  id?: string;
127
- /**
128
- * The name of the agent
129
- */
130
- name?: string;
131
128
  /**
132
129
  * A description of the role of the agent
133
130
  */
@@ -138,7 +135,7 @@ export function createAgent<
138
135
  */
139
136
  events: TEventSchemas;
140
137
  context?: TContextSchema;
141
- planner?: AgentPlanner<Agent<TContextSchema, TEventSchemas>>;
138
+ strategy?: AgentStrategy<Agent<TContextSchema, TEventSchemas>>;
142
139
  stringify?: typeof JSON.stringify;
143
140
  /**
144
141
  * A function that retrieves the agent's long term memory
@@ -150,210 +147,19 @@ export function createAgent<
150
147
  * Agent logic
151
148
  */
152
149
  logic?: AgentLogic<TEvents>;
153
- } & GenerateTextOptions): Agent<TContextSchema, TEventSchemas> {
150
+ model: LanguageModel;
151
+ episodeId?: string;
152
+ }): Agent<TContextSchema, TEventSchemas> {
154
153
  return new Agent({
155
154
  id,
156
155
  context,
157
156
  events,
158
- name,
159
157
  description,
160
- planner,
158
+ strategy: strategy,
161
159
  model,
162
160
  logic,
161
+ episodeId,
163
162
  }) as any;
164
- // const agent = createActor(logic) as unknown as Agent<TContext, TEvents>;
165
- // agent.events = events;
166
- // agent.model = model;
167
- // agent.name = name;
168
- // agent.description = description;
169
- // agent.defaultOptions = { ...generateTextOptions, model };
170
- // agent.memory = getMemory ? getMemory(agent) : undefined;
171
-
172
- // agent.onMessage = (callback) => {
173
- // agent.on('message', (ev) => callback(ev.message));
174
- // };
175
-
176
- // agent.decide = (opts) => {
177
- // return agentDecide(agent, opts);
178
- // };
179
-
180
- // agent.addMessage = (messageInput) => {
181
- // const message = {
182
- // ...messageInput,
183
- // id: messageInput.id ?? randomId(),
184
- // timestamp: messageInput.timestamp ?? Date.now(),
185
- // sessionId: agent.sessionId,
186
- // } satisfies AgentMessage;
187
- // agent.send({
188
- // type: 'agent.message',
189
- // message,
190
- // });
191
-
192
- // return message;
193
- // };
194
- // agent.getMessages = () => agent.getSnapshot().context.messages;
195
-
196
- // agent.addFeedback = (feedbackInput) => {
197
- // const feedback = {
198
- // ...feedbackInput,
199
- // attributes: { ...feedbackInput.attributes },
200
- // reward: feedbackInput.reward ?? 0,
201
- // timestamp: feedbackInput.timestamp ?? Date.now(),
202
- // sessionId: agent.sessionId,
203
- // } satisfies AgentFeedback;
204
- // agent.send({
205
- // type: 'agent.feedback',
206
- // feedback,
207
- // });
208
- // return feedback;
209
- // };
210
- // agent.getFeedback = () => agent.getSnapshot().context.feedback;
211
-
212
- // agent.addObservation = (observationInput) => {
213
- // const { prevState, event, state } = observationInput;
214
- // const observedState = { context: state.context, value: state.value };
215
- // const observedPrevState = prevState
216
- // ? {
217
- // context: prevState.context,
218
- // value: prevState.value,
219
- // }
220
- // : undefined;
221
- // const observation = {
222
- // prevState: observedPrevState,
223
- // event,
224
- // state: observedState,
225
- // id: observationInput.id ?? randomId(),
226
- // sessionId: agent.sessionId,
227
- // timestamp: observationInput.timestamp ?? Date.now(),
228
- // machineHash: observationInput.machine
229
- // ? getMachineHash(observationInput.machine)
230
- // : undefined,
231
- // } satisfies AgentObservation<any>;
232
-
233
- // agent.send({
234
- // type: 'agent.observe',
235
- // observation,
236
- // });
237
-
238
- // return observation;
239
- // };
240
- // agent.getObservations = () => agent.getSnapshot().context.observations;
241
-
242
- // agent.addPlan = (plan) => {
243
- // agent.send({
244
- // type: 'agent.plan',
245
- // plan,
246
- // });
247
- // };
248
- // agent.getPlans = () => agent.getSnapshot().context.plans;
249
-
250
- // agent.interact = ((actorRef, getInput) => {
251
- // let prevState: ObservedState | undefined = undefined;
252
- // let subscribed = true;
253
-
254
- // async function handleObservation(observationInput: AgentObservationInput) {
255
- // const observation = agent.addObservation(observationInput);
256
-
257
- // const input = getInput?.(observation);
258
-
259
- // if (input) {
260
- // await agentDecide(agent, {
261
- // machine: actorRef.src as AnyStateMachine,
262
- // state: observation.state,
263
- // execute: async (event) => {
264
- // actorRef.send(event);
265
- // },
266
- // ...input,
267
- // });
268
- // }
269
-
270
- // prevState = observationInput.state;
271
- // }
272
-
273
- // // Inspect system, but only observe specified actor
274
- // const sub = actorRef.system.inspect({
275
- // next: async (inspEvent) => {
276
- // if (
277
- // !subscribed ||
278
- // inspEvent.actorRef !== actorRef ||
279
- // inspEvent.type !== '@xstate.snapshot'
280
- // ) {
281
- // return;
282
- // }
283
-
284
- // const observationInput = {
285
- // event: inspEvent.event,
286
- // prevState,
287
- // state: inspEvent.snapshot as any,
288
- // machine: (actorRef as any).src,
289
- // } satisfies AgentObservationInput;
290
-
291
- // await handleObservation(observationInput);
292
- // },
293
- // });
294
-
295
- // // If actor already started, interact with current state
296
- // if ((actorRef as any)._processingStatus === 1) {
297
- // handleObservation({
298
- // prevState: undefined,
299
- // event: { type: '' }, // TODO: unknown events?
300
- // state: actorRef.getSnapshot(),
301
- // machine: (actorRef as any).src,
302
- // });
303
- // }
304
-
305
- // return {
306
- // unsubscribe: () => {
307
- // sub.unsubscribe();
308
- // subscribed = false;
309
- // },
310
- // };
311
- // }) as typeof agent.interact;
312
-
313
- // agent.observe = (actorRef) => {
314
- // let prevState: ObservedState = actorRef.getSnapshot();
315
-
316
- // const sub = actorRef.system.inspect({
317
- // next: async (inspEvent) => {
318
- // if (
319
- // inspEvent.actorRef !== actorRef ||
320
- // inspEvent.type !== '@xstate.snapshot'
321
- // ) {
322
- // return;
323
- // }
324
-
325
- // const observationInput = {
326
- // event: inspEvent.event,
327
- // prevState,
328
- // state: inspEvent.snapshot as any,
329
- // machine: (actorRef as any).src,
330
- // } satisfies AgentObservationInput;
331
-
332
- // prevState = observationInput.state;
333
-
334
- // agent.addObservation(observationInput);
335
- // },
336
- // });
337
-
338
- // return sub;
339
- // };
340
-
341
- // agent.types = {} as any;
342
-
343
- // agent.wrap = (modelToWrap) =>
344
- // experimental_wrapLanguageModel({
345
- // model: modelToWrap,
346
- // middleware: createAgentMiddleware(agent),
347
- // });
348
-
349
- // agent.model = experimental_wrapLanguageModel({
350
- // model,
351
- // middleware: createAgentMiddleware(agent),
352
- // });
353
-
354
- // agent.start();
355
-
356
- // return agent;
357
163
  }
358
164
 
359
165
  export class Agent<
@@ -374,14 +180,14 @@ export class Agent<
374
180
  public description?: string;
375
181
  public events: TEventSchemas;
376
182
  public context?: TContextSchema;
377
- public planner?: AgentPlanner<Agent<TContextSchema, TEventSchemas>>;
378
- public types: {
379
- events: TEvents;
380
- context: Compute<TContext>;
381
- };
183
+ public strategy: AgentStrategy<Agent<TContextSchema, TEventSchemas>>;
184
+ // public types: {
185
+ // events: TEvents;
186
+ // context: Compute<TContext>;
187
+ // };
382
188
  public model: LanguageModel;
383
189
  public memory: AgentLongTermMemory | undefined;
384
- public defaultOptions: any; // todo
190
+ public defaultOptions: AgentDecideOptions<AnyAgent> | undefined; // todo
385
191
 
386
192
  constructor({
387
193
  logic = agentLogic as AgentLogic<TEvents>,
@@ -391,7 +197,8 @@ export class Agent<
391
197
  model,
392
198
  events,
393
199
  context,
394
- planner = simplePlanner,
200
+ episodeId,
201
+ strategy = simpleStrategy,
395
202
  }: {
396
203
  logic: AgentLogic<TEvents>;
397
204
  id?: string;
@@ -400,17 +207,18 @@ export class Agent<
400
207
  model: GenerateTextOptions['model'];
401
208
  events: TEventSchemas;
402
209
  context?: TContextSchema;
403
- planner?: AgentPlanner<Agent<TContextSchema, TEventSchemas>>;
210
+ strategy?: AgentStrategy<Agent<TContextSchema, TEventSchemas>>;
211
+ episodeId?: string;
404
212
  }) {
405
213
  super(logic);
406
214
  this.model = model;
407
- this.episodeId = id ?? randomId();
215
+ this.episodeId = episodeId ?? randomId('episode-');
408
216
  this.name = name;
409
217
  this.description = description;
410
218
  this.events = events;
411
219
  this.context = context;
412
- this.planner = planner;
413
- this.types = {} as any;
220
+ this.strategy = strategy;
221
+ this.id = id ?? randomId();
414
222
 
415
223
  this.start();
416
224
  }
@@ -422,6 +230,13 @@ export class Agent<
422
230
  return this.on('message', (ev) => fn(ev.message));
423
231
  }
424
232
 
233
+ /**
234
+ * Called whenever the agent (LLM assistant) receives some feedback.
235
+ */
236
+ public onFeedback(fn: (feedback: AgentFeedback) => void) {
237
+ return this.on('feedback', (ev) => fn(ev.feedback));
238
+ }
239
+
425
240
  /**
426
241
  * Retrieves messages from the agent's short-term (local) memory.
427
242
  */
@@ -448,7 +263,6 @@ export class Agent<
448
263
  const feedback = {
449
264
  ...feedbackInput,
450
265
  attributes: { ...feedbackInput.attributes },
451
- reward: feedbackInput.reward ?? 0,
452
266
  timestamp: feedbackInput.timestamp ?? Date.now(),
453
267
  episodeId: this.episodeId,
454
268
  } satisfies AgentFeedback;
@@ -497,17 +311,17 @@ export class Agent<
497
311
  return this.getSnapshot().context.observations;
498
312
  }
499
313
 
500
- public addPlan(plan: AgentPlan<TEvents>) {
314
+ public addDecision(decision: AgentDecision<TEvents>) {
501
315
  this.send({
502
- type: 'agent.plan',
503
- plan,
316
+ type: 'agent.decision',
317
+ decision,
504
318
  });
505
319
  }
506
320
  /**
507
321
  * Retrieves strategies from the agent's short-term (local) memory.
508
322
  */
509
- public getPlans() {
510
- return this.getSnapshot().context.plans;
323
+ public getDecisions() {
324
+ return this.getSnapshot().context.decisions;
511
325
  }
512
326
 
513
327
  /**
@@ -558,15 +372,16 @@ export class Agent<
558
372
  actorRef: TActor,
559
373
  getInput: (
560
374
  observation: AgentObservation<TActor>
561
- ) => AgentDecisionInput | undefined
375
+ ) => AgentInteractInput<this> | void
562
376
  ): Subscription;
563
377
  public interact<TActor extends ActorRefLike>(
564
378
  actorRef: TActor,
565
379
  getInput?: (
566
380
  observation: AgentObservation<TActor>
567
- ) => AgentDecisionInput | undefined
381
+ ) => AgentInteractInput<this> | void
568
382
  ): Subscription {
569
- const actorRefCheck = isActorRef(actorRef);
383
+ const actorRefCheck = isActorRef(actorRef) && actorRef.src;
384
+ const machine = isMachineActor(actorRef) ? actorRef.src : undefined;
570
385
 
571
386
  let prevState: ObservedState | undefined = undefined;
572
387
  let subscribed = true;
@@ -579,16 +394,15 @@ export class Agent<
579
394
  const input = getInput?.(observation);
580
395
 
581
396
  if (input) {
582
- await agentDecide(agent, {
583
- machine: actorRefCheck
584
- ? (actorRef.src as AnyStateMachine)
585
- : undefined,
397
+ const res = await agentDecide(agent, {
398
+ machine,
586
399
  state: observation.state,
587
- execute: async (event) => {
588
- actorRef.send(event);
589
- },
590
400
  ...input,
591
401
  });
402
+
403
+ if (res?.nextEvent) {
404
+ actorRef.send(res.nextEvent);
405
+ }
592
406
  }
593
407
 
594
408
  prevState = observationInput.state;
@@ -622,7 +436,7 @@ export class Agent<
622
436
  if ((actorRef as any)._processingStatus === 1) {
623
437
  handleObservation({
624
438
  prevState: undefined,
625
- event: { type: '' }, // TODO: unknown events?
439
+ event: undefined,
626
440
  state: actorRef.getSnapshot(),
627
441
  machine: (actorRef as any).src,
628
442
  });
@@ -675,14 +489,16 @@ export class Agent<
675
489
  }
676
490
 
677
491
  /**
678
- * Resolves with an `AgentPlan` based on the information provided in the `options`, including:
492
+ * Resolves with an `AgentDecision` based on the information provided in the `options`, including:
679
493
  *
680
494
  * - The `goal` for the agent to achieve
681
495
  * - The observed current `state`
682
496
  * - The `machine` (e.g. a state machine) that specifies what can happen next
683
497
  * - Additional `context`
684
498
  */
685
- public decide(opts: AgentDecideOptions) {
499
+ public async decide(
500
+ opts: AgentDecideOptions<this>
501
+ ): Promise<AgentDecision<EventsFromAgent<this>> | undefined> {
686
502
  return agentDecide(this, opts);
687
503
  }
688
504
  }
@@ -28,7 +28,7 @@ test('fromDecision() makes a decision', async () => {
28
28
  doGenerate,
29
29
  });
30
30
  const agent = createAgent({
31
- name: 'test',
31
+ id: 'test',
32
32
  model,
33
33
  events: {
34
34
  doFirst: z.object({}),
@@ -73,7 +73,7 @@ test('interacts with an actor', async () => {
73
73
  doGenerate,
74
74
  });
75
75
  const agent = createAgent({
76
- name: 'test',
76
+ id: 'test',
77
77
  model,
78
78
  events: {
79
79
  doFirst: z.object({}),
@@ -116,7 +116,7 @@ test('interacts with an actor (late interaction)', async () => {
116
116
  doGenerate,
117
117
  });
118
118
  const agent = createAgent({
119
- name: 'test',
119
+ id: 'test',
120
120
  model,
121
121
  events: {
122
122
  doFirst: z.object({}),
@@ -153,3 +153,172 @@ test('interacts with an actor (late interaction)', async () => {
153
153
 
154
154
  expect(actor.getSnapshot().value).toBe('third');
155
155
  });
156
+
157
+ test('agent.decide() makes a decision based on goal and state (simple strategy)', async () => {
158
+ const model = new MockLanguageModelV1({
159
+ doGenerate,
160
+ });
161
+
162
+ const agent = createAgent({
163
+ id: 'test',
164
+ model,
165
+ events: {
166
+ MOVE: z.object({}),
167
+ },
168
+ });
169
+
170
+ const decision = await agent.decide({
171
+ goal: 'Make the best move',
172
+ state: {
173
+ value: 'playing',
174
+ context: {
175
+ board: [0, 0, 0],
176
+ },
177
+ },
178
+ machine: createMachine({
179
+ initial: 'playing',
180
+ states: {
181
+ playing: {
182
+ on: {
183
+ MOVE: 'next',
184
+ },
185
+ },
186
+ next: {},
187
+ },
188
+ }),
189
+ });
190
+
191
+ expect(decision).toBeDefined();
192
+ expect(decision!.nextEvent).toEqual(
193
+ expect.objectContaining({
194
+ type: 'MOVE',
195
+ })
196
+ );
197
+ });
198
+
199
+ test.each([
200
+ [undefined, true],
201
+ [undefined, false],
202
+ [3, true],
203
+ [3, false],
204
+ ])(
205
+ 'agent.decide() retries if a decision is not made (%i attempts, succeed: %s)',
206
+ async (maxAttempts, succeed) => {
207
+ let attempts = 0;
208
+ const doGenerateWithRetry = async (params: LanguageModelV1CallOptions) => {
209
+ const keys =
210
+ params.mode.type === 'regular'
211
+ ? params.mode.tools?.map((t) => t.name)
212
+ : [];
213
+
214
+ console.log('try', attempts, 'max', maxAttempts);
215
+
216
+ const toolCalls =
217
+ succeed && attempts++ === (maxAttempts ?? 2) - 1
218
+ ? [
219
+ {
220
+ toolCallType: 'function',
221
+ toolCallId: 'call-1',
222
+ toolName: keys![0],
223
+ args: `{ "type": "${keys?.[0]}" }`,
224
+ },
225
+ ]
226
+ : [];
227
+
228
+ return {
229
+ ...dummyResponseValues,
230
+ finishReason: 'tool-calls',
231
+ toolCalls,
232
+ } as any;
233
+ };
234
+ const model = new MockLanguageModelV1({
235
+ doGenerate: doGenerateWithRetry,
236
+ });
237
+
238
+ const agent = createAgent({
239
+ id: 'test',
240
+ model,
241
+ events: {
242
+ MOVE: z.object({}),
243
+ },
244
+ });
245
+
246
+ const decision = await agent.decide({
247
+ goal: 'Make the best move',
248
+ state: {
249
+ value: 'playing',
250
+ },
251
+ machine: createMachine({
252
+ initial: 'playing',
253
+ states: {
254
+ playing: {
255
+ on: {
256
+ MOVE: 'win',
257
+ },
258
+ },
259
+ win: {},
260
+ },
261
+ }),
262
+ maxAttempts,
263
+ });
264
+
265
+ if (!succeed) {
266
+ expect(decision).toBeUndefined();
267
+ } else {
268
+ expect(decision).toBeDefined();
269
+ expect(decision!.nextEvent).toEqual(
270
+ expect.objectContaining({
271
+ type: 'MOVE',
272
+ })
273
+ );
274
+ }
275
+ }
276
+ );
277
+
278
+ test.each([['MOVE'], ['FORFEIT']] as const)(
279
+ 'agent.decide() respects allowedEvents constraint (event: %s)',
280
+ async (allowedEventType) => {
281
+ const model = new MockLanguageModelV1({
282
+ doGenerate: async (params: LanguageModelV1CallOptions) => {
283
+ const keys =
284
+ params.mode.type === 'regular'
285
+ ? params.mode.tools?.map((t) => t.name)
286
+ : [];
287
+
288
+ return {
289
+ ...dummyResponseValues,
290
+ finishReason: 'tool-calls',
291
+ toolCalls: [
292
+ {
293
+ toolCallType: 'function',
294
+ toolCallId: 'call-1',
295
+ toolName: keys![0],
296
+ args: `{ "type": "${keys?.[0]}" }`,
297
+ },
298
+ ],
299
+ } as any;
300
+ },
301
+ });
302
+
303
+ const agent = createAgent({
304
+ id: 'test',
305
+ model,
306
+ events: {
307
+ MOVE: z.object({}),
308
+ SKIP: z.object({}),
309
+ FORFEIT: z.object({}),
310
+ },
311
+ });
312
+
313
+ const decision = await agent.decide({
314
+ goal: 'Make the best move',
315
+ state: {
316
+ value: 'playing',
317
+ context: {},
318
+ },
319
+ allowedEvents: [allowedEventType],
320
+ });
321
+
322
+ expect(decision?.nextEvent?.type).toEqual(allowedEventType);
323
+ }
324
+ );