@statelyai/agent 0.0.7 → 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 +47 -0
  3. package/dist/index.d.mts +3 -0
  4. package/dist/index.d.ts +282 -71
  5. package/dist/index.js +4389 -183
  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 +117 -110
  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 +89 -124
  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 +65 -47
  24. package/examples/wiki.ts +30 -0
  25. package/examples/word.ts +168 -0
  26. package/package.json +18 -11
  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 +13 -38
  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 -50
  45. package/tsconfig.json +1 -1
  46. package/examples/multiAgentCollaboration.ts +0 -0
  47. package/examples/numberGuesser.ts +0 -128
  48. package/examples/wordGuesser.ts +0 -156
  49. package/src/adapter.test.ts +0 -217
  50. package/src/adapters/openai.ts +0 -298
package/src/agent.ts CHANGED
@@ -1,8 +1,262 @@
1
- import { ActorOptions, AnyStateMachine, createActor } from 'xstate';
1
+ import {
2
+ AnyEventObject,
3
+ AnyStateMachine,
4
+ createActor,
5
+ EventObject,
6
+ fromTransition,
7
+ Observer,
8
+ toObserver,
9
+ } from 'xstate';
10
+ import { ZodEventMapping } from './schemas';
11
+ import {
12
+ Agent,
13
+ AgentLogic,
14
+ AgentMessageHistory,
15
+ AgentPlanner,
16
+ EventsFromZodEventMapping,
17
+ GenerateTextOptions,
18
+ AgentLongTermMemory,
19
+ AIAdapter,
20
+ ObservedState,
21
+ AgentObservationInput,
22
+ AgentMemoryContext,
23
+ } from './types';
24
+ import { simplePlanner } from './planners/simplePlanner';
25
+ import { randomUUID } from 'crypto';
26
+ import { agentGenerateText, agentStreamText } from './text';
27
+ import { agentDecide } from './decision';
28
+ import { vercelAdapter } from './adapters/vercel';
2
29
 
3
- export function createAgent<T extends AnyStateMachine>(
4
- ...args: Parameters<typeof createActor<T>>
5
- ) {
6
- const [machine, options] = args;
7
- return createActor(machine, options);
30
+ export const agentLogic: AgentLogic<AnyEventObject> = fromTransition(
31
+ (state, event, { emit }) => {
32
+ switch (event.type) {
33
+ case 'agent.feedback': {
34
+ state.feedback.push(event.feedback);
35
+ emit({
36
+ type: 'feedback',
37
+ // @ts-ignore TODO: fix types in XState
38
+ feedback: event.feedback,
39
+ });
40
+ break;
41
+ }
42
+ case 'agent.observe': {
43
+ state.observations.push(event.observation);
44
+ emit({
45
+ type: 'observation',
46
+ // @ts-ignore TODO: fix types in XState
47
+ observation: event.observation,
48
+ });
49
+ break;
50
+ }
51
+ case 'agent.message': {
52
+ state.messages.push(event.message);
53
+ emit({
54
+ type: 'message',
55
+ // @ts-ignore TODO: fix types in XState
56
+ message: event.message,
57
+ });
58
+ break;
59
+ }
60
+ case 'agent.plan': {
61
+ state.plans.push(event.plan);
62
+ emit({
63
+ type: 'plan',
64
+ // @ts-ignore TODO: fix types in XState
65
+ plan: event.plan,
66
+ });
67
+ break;
68
+ }
69
+ default:
70
+ break;
71
+ }
72
+ return state;
73
+ },
74
+ {
75
+ feedback: [],
76
+ messages: [],
77
+ observations: [],
78
+ plans: [],
79
+ } as AgentMemoryContext
80
+ );
81
+
82
+ export function createAgent<
83
+ const TEventSchemas extends ZodEventMapping,
84
+ TEvents extends EventObject = EventsFromZodEventMapping<TEventSchemas>
85
+ >({
86
+ name,
87
+ description,
88
+ model,
89
+ events,
90
+ planner = simplePlanner as AgentPlanner<Agent<TEvents>>,
91
+ stringify = JSON.stringify,
92
+ getMemory,
93
+ logic = agentLogic as AgentLogic<TEvents>,
94
+ adapter = vercelAdapter,
95
+ ...generateTextOptions
96
+ }: {
97
+ /**
98
+ * The name of the agent
99
+ */
100
+ name: string;
101
+ /**
102
+ * A description of the role of the agent
103
+ */
104
+ description?: string;
105
+ /**
106
+ * Events that the agent can cause (send) in an environment
107
+ * that the agent knows about.
108
+ */
109
+ events: TEventSchemas;
110
+ planner?: AgentPlanner<Agent<TEvents>>;
111
+ stringify?: typeof JSON.stringify;
112
+ /**
113
+ * A function that retrieves the agent's long term memory
114
+ */
115
+ getMemory?: (agent: Agent<any>) => AgentLongTermMemory;
116
+ /**
117
+ * Agent logic
118
+ */
119
+ logic?: AgentLogic<TEvents>;
120
+ adapter?: AIAdapter;
121
+ } & GenerateTextOptions): Agent<TEvents> {
122
+ const messageHistoryListeners: Observer<AgentMessageHistory>[] = [];
123
+
124
+ const agent = createActor(logic) as unknown as Agent<TEvents>;
125
+ agent.events = events;
126
+ agent.model = model;
127
+ agent.name = name;
128
+ agent.description = description;
129
+ agent.adapter = adapter;
130
+ agent.defaultOptions = { ...generateTextOptions, model };
131
+ agent.select = (selector) => {
132
+ return selector(agent.getSnapshot().context);
133
+ };
134
+ agent.memory = getMemory ? getMemory(agent) : undefined;
135
+
136
+ agent.onMessage = (callback) => {
137
+ messageHistoryListeners.push(toObserver(callback));
138
+ };
139
+
140
+ agent.decide = (opts) => {
141
+ return agentDecide(agent, opts);
142
+ };
143
+
144
+ agent.addMessage = (messageInput) => {
145
+ const message = {
146
+ ...messageInput,
147
+ id: messageInput.id ?? randomUUID(),
148
+ timestamp: messageInput.timestamp ?? Date.now(),
149
+ sessionId: agent.sessionId,
150
+ };
151
+ agent.send({
152
+ type: 'agent.message',
153
+ message,
154
+ });
155
+
156
+ return message;
157
+ };
158
+
159
+ agent.generateText = (opts) => agentGenerateText(agent, opts);
160
+
161
+ agent.streamText = (opts) => agentStreamText(agent, opts);
162
+
163
+ agent.addFeedback = (feedbackInput) => {
164
+ const feedback = {
165
+ ...feedbackInput,
166
+ timestamp: feedbackInput.timestamp ?? Date.now(),
167
+ sessionId: agent.sessionId,
168
+ };
169
+ agent.send({
170
+ type: 'agent.feedback',
171
+ feedback,
172
+ });
173
+ return feedback;
174
+ };
175
+
176
+ agent.addObservation = (observationInput) => {
177
+ const observation = {
178
+ ...observationInput,
179
+ id: observationInput.id ?? randomUUID(),
180
+ sessionId: agent.sessionId,
181
+ timestamp: observationInput.timestamp ?? Date.now(),
182
+ };
183
+
184
+ agent.send({
185
+ type: 'agent.observe',
186
+ observation,
187
+ });
188
+
189
+ return observation;
190
+ };
191
+
192
+ agent.addPlan = (plan) => {
193
+ agent.send({
194
+ type: 'agent.plan',
195
+ plan,
196
+ });
197
+ };
198
+
199
+ agent.interact = (actorRef, getInput) => {
200
+ let prevState: ObservedState | undefined = undefined;
201
+ let subscribed = true;
202
+
203
+ async function handleObservation(observationInput: AgentObservationInput) {
204
+ const observation = agent.addObservation(observationInput);
205
+
206
+ const input = getInput?.(observation);
207
+
208
+ if (input) {
209
+ await agentDecide(agent, {
210
+ machine: actorRef.src as AnyStateMachine,
211
+ state: observation.state,
212
+ execute: async (event) => {
213
+ actorRef.send(event);
214
+ },
215
+ ...input,
216
+ });
217
+ }
218
+
219
+ prevState = observationInput.state;
220
+ }
221
+
222
+ // Inspect system, but only observe specified actor
223
+ actorRef.system.inspect({
224
+ next: async (inspEvent) => {
225
+ if (
226
+ !subscribed ||
227
+ inspEvent.actorRef !== actorRef ||
228
+ inspEvent.type !== '@xstate.snapshot'
229
+ ) {
230
+ return;
231
+ }
232
+
233
+ const observationInput = {
234
+ event: inspEvent.event,
235
+ prevState,
236
+ state: inspEvent.snapshot as any,
237
+ };
238
+
239
+ await handleObservation(observationInput);
240
+ },
241
+ });
242
+
243
+ // If actor already started, interact with current state
244
+ if ((actorRef as any)._processingStatus === 1) {
245
+ handleObservation({
246
+ prevState: undefined,
247
+ event: { type: '' }, // TODO: unknown events?
248
+ state: actorRef.getSnapshot(),
249
+ });
250
+ }
251
+
252
+ return {
253
+ unsubscribe: () => {
254
+ subscribed = false;
255
+ }, // TODO: make this actually unsubscribe
256
+ };
257
+ };
258
+
259
+ agent.start();
260
+
261
+ return agent;
8
262
  }
@@ -0,0 +1,179 @@
1
+ import { test, expect } from 'vitest';
2
+ import { createAgent, fromDecision, type AIAdapter } from './';
3
+ import { createActor, createMachine, waitFor } from 'xstate';
4
+ import { z } from 'zod';
5
+ import { GenerateTextResult } from 'ai';
6
+
7
+ const mockToolDecision: AIAdapter['generateText'] = async (arg) => {
8
+ const keys = Object.keys(arg.tools!);
9
+
10
+ if (keys.length > 1) {
11
+ throw new Error('Expected only 1 choice');
12
+ }
13
+
14
+ if (keys.length === 0) {
15
+ return {
16
+ toolResults: [],
17
+ } as any as GenerateTextResult<any>;
18
+ }
19
+
20
+ return {
21
+ toolResults: [
22
+ {
23
+ result: {
24
+ type: keys[0],
25
+ },
26
+ },
27
+ ],
28
+ } as any as GenerateTextResult<any>;
29
+ };
30
+
31
+ test('fromDecision() makes a decision', async () => {
32
+ const agent = createAgent({
33
+ name: 'test',
34
+ model: {} as any,
35
+ events: {
36
+ doFirst: z.object({}),
37
+ doSecond: z.object({}),
38
+ },
39
+ adapter: {
40
+ generateText: async (arg) => {
41
+ const keys = Object.keys(arg.tools!);
42
+
43
+ if (keys.length !== 1) {
44
+ throw new Error('Expected only 1 choice');
45
+ }
46
+
47
+ return {
48
+ toolResults: [
49
+ {
50
+ result: {
51
+ type: keys[0],
52
+ },
53
+ },
54
+ ],
55
+ } as any as GenerateTextResult<any>;
56
+ },
57
+ streamText: {} as any,
58
+ },
59
+ });
60
+
61
+ const machine = createMachine({
62
+ initial: 'first',
63
+ states: {
64
+ first: {
65
+ invoke: {
66
+ src: fromDecision(agent),
67
+ },
68
+ on: {
69
+ doFirst: 'second',
70
+ },
71
+ },
72
+ second: {
73
+ invoke: {
74
+ src: fromDecision(agent),
75
+ },
76
+ on: {
77
+ doSecond: 'third',
78
+ },
79
+ },
80
+ third: {},
81
+ },
82
+ });
83
+
84
+ const actor = createActor(machine);
85
+
86
+ actor.start();
87
+
88
+ await waitFor(actor, (s) => s.matches('third'));
89
+
90
+ expect(actor.getSnapshot().value).toBe('third');
91
+ });
92
+
93
+ test('interacts with an actor', async () => {
94
+ const agent = createAgent({
95
+ name: 'test',
96
+ model: {} as any,
97
+ events: {
98
+ doFirst: z.object({}),
99
+ doSecond: z.object({}),
100
+ },
101
+ adapter: {
102
+ generateText: mockToolDecision,
103
+ streamText: {} as any,
104
+ },
105
+ });
106
+
107
+ const machine = createMachine({
108
+ initial: 'first',
109
+ states: {
110
+ first: {
111
+ on: {
112
+ doFirst: 'second',
113
+ },
114
+ },
115
+ second: {
116
+ on: {
117
+ doSecond: 'third',
118
+ },
119
+ },
120
+ third: {},
121
+ },
122
+ });
123
+
124
+ const actor = createActor(machine);
125
+
126
+ agent.interact(actor, () => ({
127
+ goal: 'Some goal',
128
+ }));
129
+
130
+ actor.start();
131
+
132
+ await waitFor(actor, (s) => s.matches('third'));
133
+
134
+ expect(actor.getSnapshot().value).toBe('third');
135
+ });
136
+
137
+ test('interacts with an actor (late interaction)', async () => {
138
+ const agent = createAgent({
139
+ name: 'test',
140
+ model: {} as any,
141
+ events: {
142
+ doFirst: z.object({}),
143
+ doSecond: z.object({}),
144
+ },
145
+ adapter: {
146
+ generateText: mockToolDecision,
147
+ streamText: {} as any,
148
+ },
149
+ });
150
+
151
+ const machine = createMachine({
152
+ initial: 'first',
153
+ states: {
154
+ first: {
155
+ on: {
156
+ doFirst: 'second',
157
+ },
158
+ },
159
+ second: {
160
+ on: {
161
+ doSecond: 'third',
162
+ },
163
+ },
164
+ third: {},
165
+ },
166
+ });
167
+
168
+ const actor = createActor(machine);
169
+
170
+ actor.start();
171
+
172
+ agent.interact(actor, () => ({
173
+ goal: 'Some goal',
174
+ }));
175
+
176
+ await waitFor(actor, (s) => s.matches('third'));
177
+
178
+ expect(actor.getSnapshot().value).toBe('third');
179
+ });
@@ -0,0 +1,83 @@
1
+ import { AnyMachineSnapshot, fromPromise } from 'xstate';
2
+ import {
3
+ Agent,
4
+ AgentDecideOptions,
5
+ AgentDecisionLogic,
6
+ AgentDecisionInput,
7
+ AgentPlanner,
8
+ } from './types';
9
+ import { simplePlanner } from './planners/simplePlanner';
10
+
11
+ export async function agentDecide<T extends Agent<any>>(
12
+ agent: T,
13
+ options: AgentDecideOptions
14
+ ) {
15
+ const resolvedOptions = {
16
+ ...agent.defaultOptions,
17
+ ...options,
18
+ };
19
+ const {
20
+ planner = simplePlanner as AgentPlanner<any>,
21
+ goal,
22
+ events = agent.events,
23
+ state,
24
+ machine,
25
+ model = agent.model,
26
+ ...otherPlanInput
27
+ } = resolvedOptions;
28
+
29
+ const plan = await planner(agent, {
30
+ model,
31
+ goal,
32
+ events,
33
+ state,
34
+ machine,
35
+ ...otherPlanInput,
36
+ });
37
+
38
+ if (plan?.nextEvent) {
39
+ agent.addPlan(plan);
40
+ await resolvedOptions.execute?.(plan.nextEvent);
41
+ }
42
+
43
+ return plan;
44
+ }
45
+
46
+ export function fromDecision(
47
+ agent: Agent<any>,
48
+ defaultInput?: AgentDecisionInput
49
+ ) {
50
+ return fromPromise(async ({ input, self }) => {
51
+ const parentRef = self._parent;
52
+ if (!parentRef) {
53
+ return;
54
+ }
55
+
56
+ const snapshot = parentRef.getSnapshot() as AnyMachineSnapshot;
57
+ const inputObject = typeof input === 'string' ? { goal: input } : input;
58
+ const resolvedInput = {
59
+ ...defaultInput,
60
+ ...inputObject,
61
+ };
62
+ const contextToInclude =
63
+ resolvedInput.context === true
64
+ ? // include entire context
65
+ parentRef.getSnapshot().context
66
+ : resolvedInput.context;
67
+ const state = {
68
+ value: snapshot.value,
69
+ context: contextToInclude,
70
+ };
71
+
72
+ const plan = await agentDecide(agent, {
73
+ machine: parentRef.src as any,
74
+ state,
75
+ execute: async (event) => {
76
+ parentRef.send(event);
77
+ },
78
+ ...resolvedInput,
79
+ });
80
+
81
+ return plan;
82
+ }) as AgentDecisionLogic<any>;
83
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { createSchemas } from './schemas';
2
1
  export { createAgent } from './agent';
3
- export { createOpenAIAdapter } from './adapters/openai';
2
+ export { fromText, fromTextStream, agentGenerateText } from './text';
3
+ export { fromDecision, agentDecide } from './decision';
4
+ export * from './types';
package/src/memory.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { AgentMemory, AgentMemoryContext } from './types';
2
+
3
+ export function createAgentMemory(): AgentMemory {
4
+ const storage = {
5
+ sessions: {} as Record<string, AgentMemoryContext>,
6
+ };
7
+
8
+ return {
9
+ append: async (sessionId, key, item) => {
10
+ storage.sessions[sessionId] =
11
+ storage.sessions[sessionId] ||
12
+ ({
13
+ observations: [],
14
+ messages: [],
15
+ plans: [],
16
+ feedback: [],
17
+ } satisfies AgentMemoryContext);
18
+
19
+ storage.sessions[sessionId]![key].push(item as any);
20
+ },
21
+ getAll: async (sessionId, key) => {
22
+ return storage.sessions[sessionId]?.[key];
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,22 @@
1
+ import { Agent, AgentPlan, AgentPlanInput } from '../types';
2
+ import { getShortestPaths } from '@xstate/graph';
3
+
4
+ export async function simplePlanner<T extends Agent<any>>(
5
+ agent: T,
6
+ input: AgentPlanInput<any>
7
+ ): Promise<AgentPlan<any> | undefined> {
8
+ // 1. Determine goal state criteria
9
+ // e.g. a state where the agent has won a game
10
+ void 0;
11
+
12
+ // 2. Determine possible events that can occur
13
+ void 0;
14
+
15
+ // 3. Get shortest paths from current state to
16
+ // a state matching the criteria, using
17
+ // possible events
18
+ void 0;
19
+
20
+ // 4. Return shortest path as a plan
21
+ return null as any;
22
+ }
@@ -0,0 +1,126 @@
1
+ import { CoreTool, tool } from 'ai';
2
+ import {
3
+ Agent,
4
+ AgentPlan,
5
+ AgentPlanInput,
6
+ ObservedState,
7
+ PromptTemplate,
8
+ TransitionData,
9
+ } from '../types';
10
+ import { getAllTransitions } from '../utils';
11
+ import { AnyStateMachine } from 'xstate';
12
+ import { z } from 'zod';
13
+ import { defaultTextTemplate } from '../templates/defaultText';
14
+
15
+ function getTransitions(
16
+ state: ObservedState,
17
+ machine: AnyStateMachine
18
+ ): TransitionData[] {
19
+ if (!machine) {
20
+ return [];
21
+ }
22
+
23
+ const resolvedState = machine.resolveState(state);
24
+ return getAllTransitions(resolvedState);
25
+ }
26
+
27
+ const simplePlannerPromptTemplate: PromptTemplate<any> = (data) => {
28
+ return `
29
+ ${defaultTextTemplate(data)}
30
+
31
+ Only make a single tool call to achieve the above goal.
32
+ `.trim();
33
+ };
34
+
35
+ export async function simplePlanner<T extends Agent<any>>(
36
+ agent: T,
37
+ input: AgentPlanInput<any>
38
+ ): Promise<AgentPlan<any> | undefined> {
39
+ // Get all of the possible next transitions
40
+ const transitions: TransitionData[] = input.machine
41
+ ? getTransitions(input.state, input.machine)
42
+ : Object.entries(input.events).map(([eventType, { description }]) => ({
43
+ eventType,
44
+ description,
45
+ }));
46
+
47
+ // Only keep the transitions that match the event types that are in the event mapping
48
+ // TODO: allow for custom filters
49
+ const filter = (eventType: string) =>
50
+ Object.keys(input.events).includes(eventType);
51
+
52
+ // Mapping of each event type (e.g. "mouse.click")
53
+ // to a valid function name (e.g. "mouse_click")
54
+ const functionNameMapping: Record<string, string> = {};
55
+
56
+ const toolTransitions = transitions
57
+ .filter((t) => {
58
+ return filter(t.eventType);
59
+ })
60
+ .map((t) => {
61
+ const name = t.eventType.replace(/\./g, '_');
62
+ functionNameMapping[name] = t.eventType;
63
+
64
+ return {
65
+ type: 'function',
66
+ eventType: t.eventType,
67
+ description: t.description,
68
+ name,
69
+ } as const;
70
+ });
71
+
72
+ // Convert the transition data to a tool map that the
73
+ // Vercel AI SDK can use
74
+ const toolMap: Record<string, CoreTool<any, any>> = {};
75
+ for (const toolTransitionData of toolTransitions) {
76
+ const toolZodType = input.events?.[toolTransitionData.eventType];
77
+
78
+ toolMap[toolTransitionData.name] = tool({
79
+ description: toolZodType?.description ?? toolTransitionData.description,
80
+ parameters: toolZodType ?? z.object({}),
81
+ execute: async (params) => {
82
+ const event = {
83
+ type: toolTransitionData.eventType,
84
+ ...params,
85
+ };
86
+
87
+ return event;
88
+ },
89
+ });
90
+ }
91
+
92
+ // Create a prompt with the given context and goal.
93
+ // The template is used to ensure that a single tool call is made.
94
+ const prompt = simplePlannerPromptTemplate({
95
+ context: input.state.context,
96
+ goal: input.goal,
97
+ });
98
+
99
+ const result = await agent.generateText({
100
+ prompt,
101
+ tools: toolMap,
102
+ toolChoice: 'required',
103
+ ...input,
104
+ });
105
+
106
+ const singleResult = result.toolResults[0];
107
+
108
+ if (!singleResult) {
109
+ // TODO: retries?
110
+ console.warn('No tool call results returned');
111
+ return undefined;
112
+ }
113
+
114
+ return {
115
+ goal: input.goal,
116
+ state: input.state,
117
+ steps: [
118
+ {
119
+ event: singleResult.result,
120
+ },
121
+ ],
122
+ nextEvent: singleResult.result,
123
+ sessionId: agent.sessionId,
124
+ timestamp: Date.now(),
125
+ };
126
+ }