@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
@@ -1,11 +1,11 @@
1
1
  import { assign, createActor, setup, log } from 'xstate';
2
2
  import { fromTerminal } from './helpers/helpers';
3
- import { createAgent, fromDecision } from '../src';
3
+ import { createAgent, EventsFromAgent, fromDecision } from '../src';
4
4
  import { z } from 'zod';
5
5
  import { openai } from '@ai-sdk/openai';
6
6
 
7
7
  const agent = createAgent({
8
- name: 'verifier',
8
+ id: 'verifier',
9
9
  model: openai('gpt-3.5-turbo-16k-0613'),
10
10
  events: {
11
11
  'agent.validateAnswer': z.object({
@@ -35,7 +35,7 @@ const machine = setup({
35
35
  answer: string | null;
36
36
  validation: string | null;
37
37
  },
38
- events: agent.types.events,
38
+ events: {} as EventsFromAgent<typeof agent>,
39
39
  },
40
40
  actors: {
41
41
  getFromTerminal: fromTerminal,
@@ -0,0 +1,141 @@
1
+ import { z } from 'zod';
2
+ import { createAgent } from '../src';
3
+ import { assign, createActor, fromPromise, setup } from 'xstate';
4
+ // import { anthropic } from '@ai-sdk/anthropic';
5
+ import { fromTerminal } from './helpers/helpers';
6
+ import { openai } from '@ai-sdk/openai';
7
+
8
+ // Create the weather agent
9
+ const agent = createAgent({
10
+ id: 'weather-agent',
11
+ model: openai('gpt-4o'),
12
+ events: {
13
+ 'weather.check': z.object({
14
+ location: z.string().describe('The location to check weather for'),
15
+ }),
16
+ 'weather.report': z.object({
17
+ temperature: z.string(),
18
+ conditions: z.string(),
19
+ }),
20
+ 'agent.respond': z.object({
21
+ response: z.string(),
22
+ }),
23
+ },
24
+ });
25
+
26
+ // Create the weather tool/service
27
+ const getWeather = (location: string) => {
28
+ if (
29
+ location.toLowerCase().includes('sf') ||
30
+ location.toLowerCase().includes('san francisco')
31
+ ) {
32
+ return {
33
+ temperature: '60 degrees',
34
+ conditions: 'foggy',
35
+ };
36
+ }
37
+ return {
38
+ temperature: '90 degrees',
39
+ conditions: 'sunny',
40
+ };
41
+ };
42
+
43
+ // Create the state machine
44
+ const machine = setup({
45
+ types: {
46
+ context: {} as {
47
+ input: string | null;
48
+ location: string | null;
49
+ weather: any;
50
+ },
51
+ },
52
+ actors: {
53
+ getFromTerminal: fromTerminal,
54
+ getWeather: fromPromise(async ({ input }: { input: string }) => {
55
+ return getWeather(input);
56
+ }),
57
+ },
58
+ }).createMachine({
59
+ context: {
60
+ input: null,
61
+ location: null,
62
+ weather: null,
63
+ },
64
+ initial: 'user',
65
+ states: {
66
+ user: {
67
+ invoke: {
68
+ src: 'getFromTerminal',
69
+ input: 'Ask me about the weather!',
70
+ onDone: {
71
+ actions: assign({
72
+ input: ({ event }) => event.output,
73
+ }),
74
+ target: 'processing',
75
+ },
76
+ },
77
+ },
78
+ processing: {
79
+ entry: () => console.log('Processing...'),
80
+ on: {
81
+ 'weather.check': {
82
+ actions: assign({
83
+ input: ({ event }) => event.location,
84
+ }),
85
+ target: 'checking',
86
+ },
87
+ 'agent.respond': {
88
+ actions: ({ event }) => console.log(event.response),
89
+ target: 'responded',
90
+ },
91
+ },
92
+ },
93
+ checking: {
94
+ entry: ({ event }) => console.log('Checking weather...', event),
95
+ invoke: {
96
+ src: 'getWeather',
97
+ input: ({ context }) => context.input!,
98
+ onDone: {
99
+ actions: assign({
100
+ weather: ({ event }) => event.output,
101
+ }),
102
+ target: 'responding',
103
+ },
104
+ },
105
+ },
106
+ responding: {
107
+ on: {
108
+ 'agent.respond': {
109
+ actions: ({ event }) => console.log(event.response),
110
+ target: 'responded',
111
+ },
112
+ },
113
+ },
114
+ responded: {
115
+ after: {
116
+ 1000: { target: 'user' },
117
+ },
118
+ },
119
+ },
120
+ });
121
+
122
+ // Create and start the actor
123
+ const actor = createActor(machine).start();
124
+
125
+ agent.interact(actor, (obs) => {
126
+ if (obs.state.matches('processing')) {
127
+ return {
128
+ goal: 'Determine if user is asking about weather and for which location. If so, get the weather. Otherwise, respond to the user.',
129
+ context: obs.state.context,
130
+ messages: agent.getMessages(),
131
+ };
132
+ }
133
+
134
+ if (obs.state.matches('responding')) {
135
+ return {
136
+ goal: 'Provide a natural response about the weather in ${context.location}',
137
+ context: obs.state.context,
138
+ messages: agent.getMessages(),
139
+ };
140
+ }
141
+ });
@@ -1,4 +1,4 @@
1
- import { createAgent, fromDecision } from '../src';
1
+ import { createAgent, EventsFromAgent, fromDecision } from '../src';
2
2
  import { assign, createActor, fromPromise, log, setup } from 'xstate';
3
3
  import { fromTerminal } from './helpers/helpers';
4
4
  import { z } from 'zod';
@@ -49,7 +49,7 @@ const getWeather = fromPromise(async ({ input }: { input: string }) => {
49
49
  });
50
50
 
51
51
  const agent = createAgent({
52
- name: 'weather',
52
+ id: 'weather',
53
53
  model: openai('gpt-4o-mini'),
54
54
  events: {
55
55
  'agent.getWeather': z.object({
@@ -75,10 +75,10 @@ const machine = setup({
75
75
  types: {
76
76
  context: {} as {
77
77
  location: string;
78
- history: string[];
79
78
  count: number;
79
+ result: string | null;
80
80
  },
81
- events: agent.types.events,
81
+ events: {} as EventsFromAgent<typeof agent>,
82
82
  },
83
83
  actors: {
84
84
  agent: fromDecision(agent),
@@ -90,7 +90,7 @@ const machine = setup({
90
90
  context: {
91
91
  location: '',
92
92
  count: 0,
93
- history: [],
93
+ result: null,
94
94
  },
95
95
  states: {
96
96
  getLocation: {
@@ -111,15 +111,6 @@ const machine = setup({
111
111
  },
112
112
  decide: {
113
113
  entry: log('Deciding...'),
114
- invoke: {
115
- src: 'agent',
116
- input: ({ context }) => ({
117
- context: {
118
- location: context.location,
119
- },
120
- goal: `Decide what to do based on the given location, which may or may not be a location`,
121
- }),
122
- },
123
114
  on: {
124
115
  'agent.getWeather': {
125
116
  actions: log(({ event }) => event),
@@ -138,6 +129,7 @@ const machine = setup({
138
129
  log(({ event }) => event.output),
139
130
  assign({
140
131
  count: ({ context }) => context.count + 1,
132
+ result: ({ event }) => event.output,
141
133
  }),
142
134
  ],
143
135
  target: 'reportWeather',
@@ -145,13 +137,6 @@ const machine = setup({
145
137
  },
146
138
  },
147
139
  reportWeather: {
148
- invoke: {
149
- src: 'agent',
150
- input: ({ context }) => ({
151
- goal: 'Report the weather', // TODO
152
- context,
153
- }),
154
- },
155
140
  on: {
156
141
  'agent.reportWeather': {
157
142
  actions: log(({ event }) => event),
@@ -169,7 +154,22 @@ const machine = setup({
169
154
  });
170
155
 
171
156
  const actor = createActor(machine);
172
- actor.subscribe((s) => {
173
- console.log(s.value);
174
- });
175
157
  actor.start();
158
+
159
+ agent.interact(actor, (obs) => {
160
+ if (obs.state.matches('decide')) {
161
+ return {
162
+ goal: `Decide what to do based on the given location, which may or may not be a location`,
163
+ context: {
164
+ location: obs.state.context.location,
165
+ },
166
+ };
167
+ }
168
+
169
+ if (obs.state.matches('reportWeather')) {
170
+ return {
171
+ goal: `Report the weather for the given location`,
172
+ context: obs.state.context,
173
+ };
174
+ }
175
+ });
package/examples/wiki.ts CHANGED
@@ -4,7 +4,7 @@ import { openai } from '@ai-sdk/openai';
4
4
  import { CoreMessage, generateText, streamText } from 'ai';
5
5
 
6
6
  const agent = createAgent({
7
- name: 'wiki',
7
+ id: 'wiki',
8
8
  model: openai('gpt-4o-mini'),
9
9
  events: {
10
10
  provideAnswer: z.object({
package/examples/word.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  import { assign, createActor, log, setup } from 'xstate';
2
2
  import { fromTerminal } from './helpers/helpers';
3
- import { createAgent, fromDecision } from '../src';
3
+ import {
4
+ ContextFromAgent,
5
+ createAgent,
6
+ fromDecision,
7
+ TypesFromAgent,
8
+ } from '../src';
4
9
  import { z } from 'zod';
5
10
  import { openai } from '@ai-sdk/openai';
6
11
 
7
12
  const agent = createAgent({
8
- name: 'word',
13
+ id: 'word',
9
14
  model: openai('gpt-4o-mini'),
10
15
  context: {
11
16
  word: z.string().nullable().describe('The word to guess'),
@@ -36,13 +41,10 @@ const context = {
36
41
  word: null,
37
42
  guessedWord: null,
38
43
  lettersGuessed: [],
39
- } satisfies typeof agent.types.context;
44
+ } satisfies ContextFromAgent<typeof agent>;
40
45
 
41
46
  const wordGuesserMachine = setup({
42
- types: {
43
- context: agent.types.context,
44
- events: agent.types.events,
45
- },
47
+ types: {} as TypesFromAgent<typeof agent>,
46
48
  actors: {
47
49
  agent: fromDecision(agent),
48
50
  getFromTerminal: fromTerminal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/agent",
3
- "version": "2.0.0-next.0",
3
+ "version": "2.0.0-next.2",
4
4
  "description": "Stateful agents that make decisions based on finite-state machine models",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -15,6 +15,7 @@
15
15
  "author": "David Khourshid <david@stately.ai>",
16
16
  "license": "MIT",
17
17
  "devDependencies": {
18
+ "@ai-sdk/anthropic": "^0.0.54",
18
19
  "@ai-sdk/openai": "^0.0.40",
19
20
  "@changesets/changelog-github": "^0.5.0",
20
21
  "@changesets/cli": "^2.27.9",
@@ -24,6 +25,7 @@
24
25
  "@tavily/core": "^0.0.2",
25
26
  "@types/node": "^20.17.1",
26
27
  "@types/object-hash": "^3.0.6",
28
+ "@vitest/coverage-v8": "^2.1.4",
27
29
  "dotenv": "^16.4.5",
28
30
  "json-schema-to-ts": "^3.1.1",
29
31
  "ts-node": "^10.9.2",
@@ -38,7 +40,7 @@
38
40
  },
39
41
  "dependencies": {
40
42
  "@xstate/graph": "^2.0.1",
41
- "ai": "^3.4.20",
43
+ "ai": "^3.4.31",
42
44
  "ajv": "^8.17.1",
43
45
  "object-hash": "^3.0.0",
44
46
  "xstate": "^5.18.2",
@@ -52,6 +54,7 @@
52
54
  "example": "ts-node examples/helpers/runner.ts",
53
55
  "changeset": "changeset",
54
56
  "release": "changeset publish",
55
- "version": "changeset version"
57
+ "version": "changeset version",
58
+ "coverage": "vitest run --coverage"
56
59
  }
57
60
  }
package/src/agent.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { test, expect, vi } from 'vitest';
2
- import { createAgent } from './';
2
+ import { createAgent, TypesFromAgent } from './';
3
3
  import { createActor, createMachine } from 'xstate';
4
4
  import { LanguageModelV1CallOptions } from 'ai';
5
5
  import { z } from 'zod';
@@ -7,7 +7,7 @@ import { dummyResponseValues, MockLanguageModelV1 } from './mockModel';
7
7
 
8
8
  test('an agent has the expected interface', () => {
9
9
  const agent = createAgent({
10
- name: 'test',
10
+ id: 'test',
11
11
  events: {},
12
12
  model: new MockLanguageModelV1(),
13
13
  });
@@ -17,12 +17,12 @@ test('an agent has the expected interface', () => {
17
17
  expect(agent.addMessage).toBeDefined();
18
18
  expect(agent.addObservation).toBeDefined();
19
19
  expect(agent.addFeedback).toBeDefined();
20
- expect(agent.addPlan).toBeDefined();
20
+ expect(agent.addDecision).toBeDefined();
21
21
 
22
22
  expect(agent.getMessages).toBeDefined();
23
23
  expect(agent.getObservations).toBeDefined();
24
24
  expect(agent.getFeedback).toBeDefined();
25
- expect(agent.getPlans).toBeDefined();
25
+ expect(agent.getDecisions).toBeDefined();
26
26
 
27
27
  expect(agent.interact).toBeDefined();
28
28
  });
@@ -31,7 +31,7 @@ test('agent.addMessage() adds to message history', () => {
31
31
  const model = new MockLanguageModelV1();
32
32
 
33
33
  const agent = createAgent({
34
- name: 'test',
34
+ id: 'test',
35
35
  events: {},
36
36
  model,
37
37
  });
@@ -65,7 +65,7 @@ test('agent.addMessage() adds to message history', () => {
65
65
 
66
66
  test('agent.addFeedback() adds to feedback', () => {
67
67
  const agent = createAgent({
68
- name: 'test',
68
+ id: 'test',
69
69
  events: {},
70
70
  model: {} as any,
71
71
  });
@@ -106,7 +106,7 @@ test('agent.addFeedback() adds to feedback', () => {
106
106
 
107
107
  test('agent.addObservation() adds to observations', () => {
108
108
  const agent = createAgent({
109
- name: 'test',
109
+ id: 'test',
110
110
  events: {},
111
111
  model: {} as any,
112
112
  });
@@ -132,7 +132,7 @@ test('agent.addObservation() adds to observations', () => {
132
132
 
133
133
  test('agent.addObservation() adds to observations (initial state)', () => {
134
134
  const agent = createAgent({
135
- name: 'test',
135
+ id: 'test',
136
136
  events: {},
137
137
  model: {} as any,
138
138
  });
@@ -154,7 +154,7 @@ test('agent.addObservation() adds to observations (initial state)', () => {
154
154
 
155
155
  test('agent.addObservation() adds to observations with machine hash', () => {
156
156
  const agent = createAgent({
157
- name: 'test',
157
+ id: 'test',
158
158
  events: {},
159
159
  model: {} as any,
160
160
  });
@@ -194,7 +194,7 @@ test('agent.addObservation() adds to observations with machine hash', () => {
194
194
 
195
195
  test('agent.addFeedback() adds to feedback (with observation)', () => {
196
196
  const agent = createAgent({
197
- name: 'test',
197
+ id: 'test',
198
198
  events: {},
199
199
  model: {} as any,
200
200
  });
@@ -251,7 +251,7 @@ test('agent.interact() observes machine actors (no 2nd arg)', () => {
251
251
  });
252
252
 
253
253
  const agent = createAgent({
254
- name: 'test',
254
+ id: 'test',
255
255
  events: {},
256
256
  model: {} as any,
257
257
  });
@@ -289,7 +289,7 @@ test('agent.interact() observes machine actors (no 2nd arg)', () => {
289
289
  test('You can listen for feedback events', () => {
290
290
  const fn = vi.fn();
291
291
  const agent = createAgent({
292
- name: 'test',
292
+ id: 'test',
293
293
  events: {},
294
294
  model: {} as any,
295
295
  });
@@ -307,7 +307,7 @@ test('You can listen for feedback events', () => {
307
307
  expect(fn).toHaveBeenCalled();
308
308
  });
309
309
 
310
- test('You can listen for plan events', async () => {
310
+ test('You can listen for decision events', async () => {
311
311
  const fn = vi.fn();
312
312
  const model = new MockLanguageModelV1({
313
313
  doGenerate: async (params: LanguageModelV1CallOptions) => {
@@ -332,14 +332,14 @@ test('You can listen for plan events', async () => {
332
332
  });
333
333
 
334
334
  const agent = createAgent({
335
- name: 'test',
335
+ id: 'test',
336
336
  model,
337
337
  events: {
338
338
  WIN: z.object({}),
339
339
  },
340
340
  });
341
341
 
342
- agent.on('plan', fn);
342
+ agent.on('decision', fn);
343
343
 
344
344
  await agent.decide({
345
345
  goal: 'Win the game',
@@ -364,7 +364,7 @@ test('You can listen for plan events', async () => {
364
364
 
365
365
  expect(fn).toHaveBeenCalledWith(
366
366
  expect.objectContaining({
367
- plan: expect.objectContaining({
367
+ decision: expect.objectContaining({
368
368
  nextEvent: {
369
369
  type: 'WIN',
370
370
  },
@@ -386,10 +386,152 @@ test('agent.types provides context and event types', () => {
386
386
  },
387
387
  });
388
388
 
389
- agent.types satisfies { context: any; events: any };
389
+ let types = {} as TypesFromAgent<typeof agent>;
390
390
 
391
- agent.types.context satisfies { score: number };
391
+ types satisfies { context: any; events: any };
392
+
393
+ types.context satisfies { score: number };
392
394
 
393
395
  // @ts-expect-error
394
- agent.types.context satisfies { score: string };
396
+ types.context satisfies { score: string };
397
+ });
398
+
399
+ test('It allows unrecognized events', () => {
400
+ const agent = createAgent({
401
+ model: {} as any,
402
+ events: {},
403
+ context: {},
404
+ });
405
+
406
+ expect(() => {
407
+ agent.send({
408
+ // @ts-expect-error
409
+ type: 'unrecognized',
410
+ });
411
+ }).not.toThrow();
412
+ });
413
+
414
+ test('You can listen for message events', () => {
415
+ const fn = vi.fn();
416
+ const agent = createAgent({
417
+ id: 'test',
418
+ events: {},
419
+ model: {} as any,
420
+ });
421
+
422
+ agent.onMessage(fn);
423
+
424
+ const message = {
425
+ role: 'user' as const,
426
+ content: [{ type: 'text' as const, text: 'test message' }],
427
+ };
428
+
429
+ agent.addMessage(message);
430
+
431
+ expect(fn).toHaveBeenCalledWith(
432
+ expect.objectContaining({
433
+ role: 'user',
434
+ content: [{ type: 'text', text: 'test message' }],
435
+ episodeId: expect.any(String),
436
+ timestamp: expect.any(Number),
437
+ })
438
+ );
439
+ });
440
+
441
+ test('agent.getDecisions() returns decisions from context', () => {
442
+ const agent = createAgent({
443
+ id: 'test',
444
+ events: {},
445
+ model: {} as any,
446
+ strategy: async (agent) => {
447
+ return {
448
+ episodeId: agent.episodeId,
449
+ strategy: 'test-strategy',
450
+ goal: '',
451
+ goalState: undefined,
452
+ paths: [
453
+ {
454
+ state: undefined,
455
+ steps: [],
456
+ },
457
+ ],
458
+ nextEvent: undefined,
459
+ timestamp: Date.now(),
460
+ };
461
+ },
462
+ });
463
+
464
+ const decisions = agent.getDecisions();
465
+
466
+ expect(decisions).toBeDefined();
467
+ expect(Array.isArray(decisions)).toBe(true);
468
+ });
469
+
470
+ test('Event listeners can be unsubscribed', () => {
471
+ const fn = vi.fn();
472
+ const agent = createAgent({
473
+ id: 'test',
474
+ events: {},
475
+ model: {} as any,
476
+ });
477
+
478
+ const subscription = agent.on('message', fn);
479
+
480
+ agent.addMessage({
481
+ role: 'user',
482
+ content: [{ type: 'text', text: 'first message' }],
483
+ });
484
+
485
+ expect(fn).toHaveBeenCalledTimes(1);
486
+
487
+ subscription.unsubscribe();
488
+
489
+ agent.addMessage({
490
+ role: 'user',
491
+ content: [{ type: 'text', text: 'second message' }],
492
+ });
493
+
494
+ expect(fn).toHaveBeenCalledTimes(1); // Still only called once
495
+ });
496
+
497
+ test('agent.observe() adds observations from actor snapshots', () => {
498
+ const machine = createMachine({
499
+ initial: 'idle',
500
+ states: {
501
+ idle: {
502
+ on: { START: 'running' },
503
+ },
504
+ running: {},
505
+ },
506
+ });
507
+
508
+ const agent = createAgent({
509
+ id: 'test',
510
+ events: {},
511
+ model: {} as any,
512
+ });
513
+
514
+ const actor = createActor(machine);
515
+ const subscription = agent.observe(actor);
516
+
517
+ actor.start();
518
+ actor.send({ type: 'START' });
519
+
520
+ expect(agent.getObservations()).toContainEqual(
521
+ expect.objectContaining({
522
+ state: expect.objectContaining({ value: 'idle' }),
523
+ machineHash: expect.any(String),
524
+ })
525
+ );
526
+
527
+ expect(agent.getObservations()).toContainEqual(
528
+ expect.objectContaining({
529
+ prevState: expect.objectContaining({ value: 'idle' }),
530
+ event: { type: 'START' },
531
+ state: expect.objectContaining({ value: 'running' }),
532
+ machineHash: expect.any(String),
533
+ })
534
+ );
535
+
536
+ subscription.unsubscribe();
395
537
  });