@statelyai/agent 0.0.8 → 0.1.0

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 (50) hide show
  1. package/.vscode/launch.json +12 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/index.d.mts +3 -0
  4. package/dist/index.d.ts +286 -44
  5. package/dist/index.js +695 -1225
  6. package/dist/index.mjs +7 -0
  7. package/examples/chatbot.ts +79 -0
  8. package/examples/cot.ts +91 -0
  9. package/examples/email.ts +118 -0
  10. package/examples/example.ts +81 -0
  11. package/examples/goal.ts +94 -0
  12. package/examples/joke.ts +98 -84
  13. package/examples/multi.ts +103 -0
  14. package/examples/newspaper.ts +324 -0
  15. package/examples/number.ts +102 -0
  16. package/examples/raffle.ts +105 -0
  17. package/examples/simple.ts +39 -0
  18. package/examples/support.ts +147 -0
  19. package/examples/ticTacToe.ts +77 -77
  20. package/examples/todo.ts +132 -0
  21. package/examples/tutor.ts +100 -0
  22. package/examples/verify.ts +120 -0
  23. package/examples/weather.ts +42 -45
  24. package/examples/wiki.ts +30 -0
  25. package/examples/word.ts +168 -0
  26. package/package.json +17 -12
  27. package/readme.md +9 -38
  28. package/src/adapters/vercel.ts +7 -0
  29. package/src/agent-experimental.ts +221 -0
  30. package/src/agent.test.ts +187 -0
  31. package/src/agent.ts +260 -6
  32. package/src/decision.test.ts +179 -0
  33. package/src/decision.ts +83 -0
  34. package/src/index.ts +3 -2
  35. package/src/memory.ts +25 -0
  36. package/src/planners/shortestPathPlanner.ts +22 -0
  37. package/src/planners/simplePlanner.ts +126 -0
  38. package/src/schemas.ts +9 -20
  39. package/src/strategies/chain-of-note.ts +155 -0
  40. package/src/templates/defaultText.ts +18 -0
  41. package/src/templates/defaultToolCall.ts +10 -0
  42. package/src/text.ts +232 -0
  43. package/src/types.ts +363 -46
  44. package/src/utils.ts +13 -72
  45. package/tsconfig.json +1 -1
  46. package/examples/multiAgentCollaboration.ts +0 -0
  47. package/examples/numberGuesser.ts +0 -101
  48. package/examples/wordGuesser.ts +0 -144
  49. package/src/adapter.test.ts +0 -217
  50. package/src/adapters/openai.ts +0 -303
package/examples/joke.ts CHANGED
@@ -1,58 +1,10 @@
1
- import OpenAI from 'openai';
2
- import { assign, fromCallback, fromPromise, log, setup } from 'xstate';
3
- import { createAgent, createOpenAIAdapter, defineEvents } from '../src';
1
+ import { assign, createActor, fromCallback, log, setup } from 'xstate';
2
+ import { createAgent, fromDecision } from '../src';
4
3
  import { loadingAnimation } from './helpers/loader';
5
4
  import { z } from 'zod';
5
+ import { openai } from '@ai-sdk/openai';
6
+ import { getFromTerminal } from './helpers/helpers';
6
7
 
7
- const openai = new OpenAI({
8
- apiKey: process.env.OPENAI_API_KEY,
9
- });
10
-
11
- const events = defineEvents({
12
- askForTopic: z.object({
13
- topic: z.string().describe('The topic for the joke'),
14
- }),
15
- tellJoke: z.object({
16
- joke: z.string().describe('The joke text'),
17
- }),
18
- endJokes: z.object({}).describe('End the jokes'),
19
-
20
- rateJoke: z.object({
21
- rating: z.number().min(1).max(10),
22
- explanation: z.string(),
23
- }),
24
- });
25
-
26
- const adapter = createOpenAIAdapter(openai, {
27
- model: 'gpt-3.5-turbo-1106',
28
- });
29
-
30
- const getJokeCompletion = adapter.fromEvent(
31
- (topic: string) => `Tell me a joke about ${topic}.`
32
- );
33
-
34
- const rateJoke = adapter.fromEvent(
35
- (joke: string) => `Rate this joke on a scale of 1 to 10: ${joke}`
36
- );
37
-
38
- const getTopic = fromPromise(async () => {
39
- const topic = await new Promise<string>((res) => {
40
- console.log('Give me a joke topic:');
41
- const listener = (data: Buffer) => {
42
- const result = data.toString().trim();
43
- process.stdin.off('data', listener);
44
- res(result);
45
- };
46
- process.stdin.on('data', listener);
47
- });
48
-
49
- return topic;
50
- });
51
-
52
- const decide = adapter.fromEvent(
53
- (lastRating: number) =>
54
- `Choose what to do next, given the previous rating of the joke: ${lastRating}`
55
- );
56
8
  export function getRandomFunnyPhrase() {
57
9
  const funnyPhrases = [
58
10
  'Concocting chuckles...',
@@ -92,10 +44,32 @@ const loader = fromCallback(({ input }: { input: string }) => {
92
44
  };
93
45
  });
94
46
 
95
- const jokeMachine = setup({
96
- schemas: {
97
- events: events.schemas,
47
+ const agent = createAgent({
48
+ name: 'joke-teller',
49
+ model: openai('gpt-4-turbo'),
50
+ events: {
51
+ askForTopic: z.object({
52
+ topic: z.string().describe('The topic for the joke'),
53
+ }),
54
+ 'agent.tellJoke': z.object({
55
+ joke: z.string().describe('The joke text'),
56
+ }),
57
+ 'agent.endJokes': z.object({}).describe('End the jokes'),
58
+ 'agent.rateJoke': z.object({
59
+ rating: z.number().min(1).max(10),
60
+ explanation: z.string(),
61
+ }),
62
+ 'agent.continue': z.object({}).describe('Continue'),
63
+ 'agent.markAsIrrelevant': z
64
+ .object({
65
+ explanation: z.string(),
66
+ })
67
+ .describe('Explains why the joke was irrelevant'),
68
+ 'agent.markAsRelevant': z.object({}).describe('The joke was relevant'),
98
69
  },
70
+ });
71
+
72
+ const jokeMachine = setup({
99
73
  types: {
100
74
  context: {} as {
101
75
  topic: string;
@@ -104,14 +78,12 @@ const jokeMachine = setup({
104
78
  lastRating: number | null;
105
79
  loader: string | null;
106
80
  },
107
- events: events.types,
81
+ events: agent.eventTypes,
108
82
  },
109
83
  actors: {
110
- getJokeCompletion,
111
- getTopic,
112
- rateJoke,
113
- decide,
84
+ agent: fromDecision(agent),
114
85
  loader,
86
+ getFromTerminal,
115
87
  },
116
88
  }).createMachine({
117
89
  id: 'joke',
@@ -126,7 +98,8 @@ const jokeMachine = setup({
126
98
  states: {
127
99
  waitingForTopic: {
128
100
  invoke: {
129
- src: 'getTopic',
101
+ src: 'getFromTerminal',
102
+ input: 'Give me a joke topic.',
130
103
  onDone: {
131
104
  actions: assign({
132
105
  topic: ({ event }) => event.output,
@@ -138,8 +111,13 @@ const jokeMachine = setup({
138
111
  tellingJoke: {
139
112
  invoke: [
140
113
  {
141
- src: 'getJokeCompletion',
142
- input: ({ context }) => context.topic,
114
+ src: 'agent',
115
+ input: ({ context }) => ({
116
+ context: {
117
+ topic: context.topic,
118
+ },
119
+ goal: `Tell me a joke about the topic. Do not make any joke that is not relevant to the topic.`,
120
+ }),
143
121
  },
144
122
  {
145
123
  src: 'loader',
@@ -147,10 +125,36 @@ const jokeMachine = setup({
147
125
  },
148
126
  ],
149
127
  on: {
150
- tellJoke: {
151
- actions: assign({
152
- jokes: ({ context, event }) => [...context.jokes, event.joke],
153
- }),
128
+ 'agent.tellJoke': {
129
+ actions: [
130
+ assign({
131
+ jokes: ({ context, event }) => [...context.jokes, event.joke],
132
+ }),
133
+ log((x) => x.event.joke),
134
+ ],
135
+ target: 'relevance',
136
+ },
137
+ },
138
+ },
139
+ relevance: {
140
+ invoke: {
141
+ src: 'agent',
142
+ input: (x) => ({
143
+ context: {
144
+ topic: x.context.topic,
145
+ lastJoke: x.context.jokes[x.context.jokes.length - 1],
146
+ },
147
+ goal: 'An irrelevant joke has no reference to the topic. If the last joke is completely irrelevant to the topic, ask for a new joke topic. Otherwise, continue.',
148
+ }),
149
+ },
150
+ on: {
151
+ 'agent.markAsIrrelevant': {
152
+ actions: log((x) => 'Irrelevant joke: ' + x.event.explanation),
153
+ target: 'waitingForTopic',
154
+ description: 'Continue',
155
+ },
156
+ 'agent.markAsRelevant': {
157
+ actions: log('Joke was relevant'),
154
158
  target: 'rateJoke',
155
159
  },
156
160
  },
@@ -158,8 +162,13 @@ const jokeMachine = setup({
158
162
  rateJoke: {
159
163
  invoke: [
160
164
  {
161
- src: 'rateJoke',
162
- input: ({ context }) => context.jokes[context.jokes.length - 1]!,
165
+ src: 'agent',
166
+ input: ({ context }) => ({
167
+ context: {
168
+ jokes: context.jokes,
169
+ },
170
+ goal: `Rate the last joke on a scale of 1 to 10.`,
171
+ }),
163
172
  },
164
173
  {
165
174
  src: 'loader',
@@ -167,18 +176,28 @@ const jokeMachine = setup({
167
176
  },
168
177
  ],
169
178
  on: {
170
- rateJoke: {
171
- actions: assign({
172
- lastRating: ({ event }) => event.rating,
173
- }),
179
+ 'agent.rateJoke': {
180
+ actions: [
181
+ assign({
182
+ lastRating: ({ event }) => event.rating,
183
+ }),
184
+ log(
185
+ ({ event }) => `Rating: ${event.rating}\n\n${event.explanation}`
186
+ ),
187
+ ],
174
188
  target: 'decide',
175
189
  },
176
190
  },
177
191
  },
178
192
  decide: {
179
193
  invoke: {
180
- src: 'decide',
181
- input: ({ context }) => context.lastRating!,
194
+ src: 'agent',
195
+ input: ({ context }) => ({
196
+ context: {
197
+ lastRating: context.lastRating,
198
+ },
199
+ goal: `Choose what to do next, given the previous rating of the joke.`,
200
+ }),
182
201
  },
183
202
  on: {
184
203
  askForTopic: {
@@ -187,7 +206,7 @@ const jokeMachine = setup({
187
206
  description:
188
207
  'Ask for a new topic, because the last joke rated 6 or lower',
189
208
  },
190
- endJokes: {
209
+ 'agent.endJokes': {
191
210
  target: 'end',
192
211
  actions: log('That joke was good enough. Goodbye!'),
193
212
  description: 'End the jokes, since the last joke rated 7 or higher',
@@ -203,11 +222,6 @@ const jokeMachine = setup({
203
222
  },
204
223
  });
205
224
 
206
- const agent = createAgent(jokeMachine, {
207
- inspect: (ev) => {
208
- if (ev.type === '@xstate.event') {
209
- console.log(`\n${ev.actorRef.id}`, ev.event);
210
- }
211
- },
212
- });
213
- agent.start();
225
+ const actor = createActor(jokeMachine);
226
+
227
+ actor.start();
@@ -0,0 +1,103 @@
1
+ import { createAgent, fromDecision } from '../src';
2
+ import { z } from 'zod';
3
+ import { assign, createActor, log, setup } from 'xstate';
4
+ import { getFromTerminal } from './helpers/helpers';
5
+ import { openai } from '@ai-sdk/openai';
6
+
7
+ const agent = createAgent({
8
+ name: 'multi',
9
+ model: openai('gpt-4-1106-preview'),
10
+ events: {
11
+ 'agent.respond': z.object({
12
+ response: z.string().describe('The response from the agent'),
13
+ }),
14
+ },
15
+ });
16
+
17
+ const machine = setup({
18
+ types: {
19
+ context: {} as {
20
+ topic: string | null;
21
+ discourse: string[];
22
+ },
23
+ },
24
+ actors: {
25
+ getFromTerminal,
26
+ agent: fromDecision(agent),
27
+ },
28
+ }).createMachine({
29
+ initial: 'asking',
30
+ context: {
31
+ topic: null,
32
+ discourse: [],
33
+ },
34
+ states: {
35
+ asking: {
36
+ invoke: {
37
+ src: 'getFromTerminal',
38
+ input: 'What is the question?',
39
+ onDone: {
40
+ actions: assign({
41
+ topic: ({ event }) => event.output,
42
+ }),
43
+ target: 'positiveResponse',
44
+ },
45
+ },
46
+ },
47
+ positiveResponse: {
48
+ invoke: {
49
+ src: 'agent',
50
+ input: ({ context }) => ({
51
+ context,
52
+ goal: 'Debate the topic, and take the positive position. Respond directly to the last message of the discourse. Keep it short.',
53
+ }),
54
+ },
55
+ on: {
56
+ 'agent.respond': {
57
+ actions: [
58
+ assign({
59
+ discourse: ({ context, event }) =>
60
+ context.discourse.concat(event.response),
61
+ }),
62
+ log(({ event }) => event.response),
63
+ ],
64
+ target: 'negativeResponse',
65
+ },
66
+ },
67
+ },
68
+ negativeResponse: {
69
+ invoke: {
70
+ src: 'agent',
71
+ input: ({ context }) => ({
72
+ model: openai('gpt-3.5-turbo-16k-0613'),
73
+ context,
74
+ goal: 'Debate the topic, and take the negative position. Respond directly to the last message of the discourse. Keep it short.',
75
+ }),
76
+ },
77
+ on: {
78
+ 'agent.respond': {
79
+ actions: [
80
+ assign({
81
+ discourse: ({ context, event }) =>
82
+ context.discourse.concat(event.response),
83
+ }),
84
+ log(({ event }) => event.response),
85
+ ],
86
+ target: 'positiveResponse',
87
+ },
88
+ },
89
+ always: {
90
+ guard: ({ context }) => context.discourse.length >= 5,
91
+ target: 'debateOver',
92
+ },
93
+ },
94
+ debateOver: {
95
+ type: 'final',
96
+ },
97
+ },
98
+ exit: () => {
99
+ process.exit();
100
+ },
101
+ });
102
+
103
+ createActor(machine).start();
@@ -0,0 +1,324 @@
1
+ // Based on GPT Newspaper:
2
+ // https://github.com/assafelovic/gpt-newspaper
3
+ // https://gist.github.com/TheGreatBonnie/58dc21ebbeeb8cbb08df665db762738c
4
+
5
+ import { TavilySearchAPIRetriever } from '@langchain/community/retrievers/tavily_search_api';
6
+ import { ChatOpenAI } from '@langchain/openai';
7
+ import { HumanMessage, SystemMessage } from '@langchain/core/messages';
8
+ import { assign, createActor, fromPromise, setup } from 'xstate';
9
+
10
+ interface AgentState {
11
+ topic: string;
12
+ searchResults?: string;
13
+ article?: string;
14
+ critique?: string;
15
+ revisionCount: number;
16
+ }
17
+
18
+ function model() {
19
+ return new ChatOpenAI({
20
+ temperature: 0,
21
+ modelName: 'gpt-4-1106-preview',
22
+ openAIApiKey: process.env.OPENAI_API_KEY,
23
+ });
24
+ }
25
+
26
+ async function search({ topic }: Pick<AgentState, 'topic'>): Promise<string> {
27
+ const retriever = new TavilySearchAPIRetriever({
28
+ k: 10,
29
+ apiKey: process.env.TAVILY_API_KEY,
30
+ });
31
+ // let topic = state.agentState.topic;
32
+ // must be at least 5 characters long
33
+ if (topic.length < 5) {
34
+ topic = 'topic: ' + topic;
35
+ }
36
+ const docs = await retriever.invoke(topic);
37
+ return JSON.stringify(docs);
38
+ }
39
+
40
+ async function curate(
41
+ input: Pick<AgentState, 'topic' | 'searchResults'>
42
+ ): Promise<string> {
43
+ const response = await model().invoke(
44
+ [
45
+ new SystemMessage(
46
+ `You are a personal newspaper editor.
47
+ Your sole task is to return a list of URLs of the 5 most relevant articles for the provided topic or query as a JSON list of strings
48
+ in this format:
49
+ {
50
+ urls: ["url1", "url2", "url3", "url4", "url5"]
51
+ }
52
+ .`.replace(/\s+/g, ' ')
53
+ ),
54
+ new HumanMessage(
55
+ `Today's date is ${new Date().toLocaleDateString('en-GB')}.
56
+ Topic or Query: ${input.topic}
57
+
58
+ Here is a list of articles:
59
+ ${input.searchResults}`.replace(/\s+/g, ' ')
60
+ ),
61
+ ],
62
+ {
63
+ response_format: {
64
+ type: 'json_object',
65
+ },
66
+ }
67
+ );
68
+ const urls = JSON.parse(response.content as string).urls;
69
+ const searchResults = JSON.parse(input.searchResults!);
70
+ const newSearchResults = searchResults.filter((result: any) => {
71
+ return urls.includes(result.metadata.source);
72
+ });
73
+ return JSON.stringify(newSearchResults);
74
+ }
75
+
76
+ async function critique(
77
+ input: Pick<AgentState, 'article' | 'critique'>
78
+ ): Promise<string | undefined> {
79
+ let feedbackInstructions = '';
80
+ if (input.critique) {
81
+ feedbackInstructions =
82
+ `The writer has revised the article based on your previous critique: ${input.critique}
83
+ The writer might have left feedback for you encoded between <FEEDBACK> tags.
84
+ The feedback is only for you to see and will be removed from the final article.
85
+ `.replace(/\s+/g, ' ');
86
+ }
87
+ const response = await model().invoke([
88
+ new SystemMessage(
89
+ `You are a personal newspaper writing critique. Your sole purpose is to provide short feedback on a written
90
+ article so the writer will know what to fix.
91
+ Today's date is ${new Date().toLocaleDateString('en-GB')}
92
+ Your task is to provide a really short feedback on the article only if necessary.
93
+ if you think the article is good, please return [DONE].
94
+ you can provide feedback on the revised article or just
95
+ return [DONE] if you think the article is good.
96
+ Please return a string of your critique or [DONE].`.replace(/\s+/g, ' ')
97
+ ),
98
+ new HumanMessage(
99
+ `${feedbackInstructions}
100
+ This is the article: ${input.article}`
101
+ ),
102
+ ]);
103
+ const content = response.content as string;
104
+ console.log('critique:', content);
105
+ return content.includes('[DONE]') ? undefined : content;
106
+ }
107
+
108
+ async function write(
109
+ input: Pick<AgentState, 'searchResults' | 'topic'>
110
+ ): Promise<string> {
111
+ const response = await model().invoke([
112
+ new SystemMessage(
113
+ `You are a personal newspaper writer. Your sole purpose is to write a well-written article about a
114
+ topic using a list of articles. Write 5 paragraphs in markdown.`.replace(
115
+ /\s+/g,
116
+ ' '
117
+ )
118
+ ),
119
+ new HumanMessage(
120
+ `Today's date is ${new Date().toLocaleDateString('en-GB')}.
121
+ Your task is to write a critically acclaimed article for me about the provided query or
122
+ topic based on the sources.
123
+ Here is a list of articles: ${input.searchResults}
124
+ This is the topic: ${input.topic}
125
+ Please return a well-written article based on the provided information.`.replace(
126
+ /\s+/g,
127
+ ' '
128
+ )
129
+ ),
130
+ ]);
131
+ const content = response.content as string;
132
+ return content;
133
+ }
134
+
135
+ async function revise(
136
+ input: Pick<AgentState, 'article' | 'critique'>
137
+ ): Promise<string> {
138
+ const response = await model().invoke([
139
+ new SystemMessage(
140
+ `You are a personal newspaper editor. Your sole purpose is to edit a well-written article about a
141
+ topic based on given critique.`.replace(/\s+/g, ' ')
142
+ ),
143
+ new HumanMessage(
144
+ `Your task is to edit the article based on the critique given.
145
+ This is the article: ${input.article}
146
+ This is the critique: ${input.critique}
147
+ Please return the edited article based on the critique given.
148
+ You may leave feedback about the critique encoded between <FEEDBACK> tags like this:
149
+ <FEEDBACK> here goes the feedback ...</FEEDBACK>`.replace(/\s+/g, ' ')
150
+ ),
151
+ ]);
152
+ const content = response.content as string;
153
+ return content;
154
+ }
155
+
156
+ const machine = setup({
157
+ types: {
158
+ context: {} as AgentState,
159
+ },
160
+ actors: {
161
+ search: fromPromise(({ input }: { input: Pick<AgentState, 'topic'> }) => {
162
+ return search(input);
163
+ }),
164
+ curate: fromPromise(
165
+ ({ input }: { input: Pick<AgentState, 'topic' | 'searchResults'> }) => {
166
+ return curate(input);
167
+ }
168
+ ),
169
+ critique: fromPromise(
170
+ ({ input }: { input: Pick<AgentState, 'article' | 'critique'> }) => {
171
+ return critique(input);
172
+ }
173
+ ),
174
+ write: fromPromise(
175
+ ({ input }: { input: Pick<AgentState, 'searchResults' | 'topic'> }) => {
176
+ return write(input);
177
+ }
178
+ ),
179
+ revise: fromPromise(
180
+ ({ input }: { input: Pick<AgentState, 'article' | 'critique'> }) => {
181
+ return revise(input);
182
+ }
183
+ ),
184
+ },
185
+ }).createMachine({
186
+ context: {
187
+ topic: 'Orlando',
188
+ revisionCount: 0,
189
+ },
190
+ initial: 'search',
191
+ states: {
192
+ search: {
193
+ invoke: {
194
+ src: 'search',
195
+ input: ({ context }) => ({
196
+ topic: context.topic,
197
+ }),
198
+ onDone: {
199
+ actions: assign({
200
+ searchResults: ({ event }) => event.output,
201
+ }),
202
+ target: 'curate',
203
+ },
204
+ },
205
+ },
206
+ curate: {
207
+ invoke: {
208
+ src: 'curate',
209
+ input: ({ context }) => ({
210
+ topic: context.topic,
211
+ searchResults: context.searchResults!,
212
+ }),
213
+ onDone: {
214
+ actions: assign({
215
+ searchResults: ({ event }) => event.output,
216
+ }),
217
+ target: 'write',
218
+ },
219
+ },
220
+ },
221
+ write: {
222
+ invoke: {
223
+ src: 'write',
224
+ input: ({ context }) => ({
225
+ topic: context.topic,
226
+ searchResults: context.searchResults!,
227
+ }),
228
+ onDone: {
229
+ actions: assign({
230
+ article: ({ event }) => event.output,
231
+ }),
232
+ target: 'critique',
233
+ },
234
+ },
235
+ },
236
+ critique: {
237
+ invoke: {
238
+ src: 'critique',
239
+ input: ({ context }) => ({
240
+ article: context.article!,
241
+ critique: context.critique,
242
+ }),
243
+ onDone: [
244
+ {
245
+ guard: ({ event }) => event.output === undefined,
246
+ target: 'done',
247
+ },
248
+ {
249
+ actions: assign({
250
+ article: ({ event }) => event.output,
251
+ }),
252
+ target: 'revise',
253
+ },
254
+ ],
255
+ },
256
+ },
257
+ revise: {
258
+ always: {
259
+ guard: ({ context }) => context.revisionCount > 3,
260
+ target: 'done',
261
+ },
262
+ entry: assign({
263
+ revisionCount: ({ context }) => context.revisionCount + 1,
264
+ }),
265
+ invoke: {
266
+ src: 'revise',
267
+ input: ({ context }) => ({
268
+ article: context.article!,
269
+ critique: context.critique,
270
+ }),
271
+ onDone: {
272
+ actions: assign({
273
+ article: ({ event }) => event.output,
274
+ }),
275
+ target: 'revise',
276
+ reenter: true,
277
+ },
278
+ },
279
+ },
280
+ done: {
281
+ type: 'final',
282
+ },
283
+ },
284
+ output: ({ context }) => context.article,
285
+ });
286
+
287
+ const actor = createActor(machine, {
288
+ // inspect: (inspEv) => {
289
+ // if (inspEv.type === '@xstate.event') {
290
+ // console.log(JSON.stringify(inspEv.event, null, 2));
291
+ // }
292
+ // },
293
+ });
294
+
295
+ actor.subscribe({
296
+ next: (s) => {
297
+ console.log('State:', s.value);
298
+ console.log(
299
+ 'Context:',
300
+ JSON.stringify(
301
+ s.context,
302
+ (k, v) => {
303
+ if (typeof v === 'string') {
304
+ // truncate if longer than 50 chars
305
+ return v.length > 50 ? `${v.slice(0, 50)}...` : v;
306
+ }
307
+ return v;
308
+ },
309
+ 2
310
+ )
311
+ );
312
+ },
313
+ complete: () => {
314
+ console.log(actor.getSnapshot().output);
315
+ },
316
+ error: (err) => {
317
+ console.error(err);
318
+ },
319
+ });
320
+
321
+ actor.start();
322
+
323
+ // keep the process alive by invoking a promise that never resolves
324
+ new Promise(() => {});