@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.
- package/.vscode/launch.json +12 -1
- package/CHANGELOG.md +18 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +286 -44
- package/dist/index.js +695 -1225
- package/dist/index.mjs +7 -0
- package/examples/chatbot.ts +79 -0
- package/examples/cot.ts +91 -0
- package/examples/email.ts +118 -0
- package/examples/example.ts +81 -0
- package/examples/goal.ts +94 -0
- package/examples/joke.ts +98 -84
- package/examples/multi.ts +103 -0
- package/examples/newspaper.ts +324 -0
- package/examples/number.ts +102 -0
- package/examples/raffle.ts +105 -0
- package/examples/simple.ts +39 -0
- package/examples/support.ts +147 -0
- package/examples/ticTacToe.ts +77 -77
- package/examples/todo.ts +132 -0
- package/examples/tutor.ts +100 -0
- package/examples/verify.ts +120 -0
- package/examples/weather.ts +42 -45
- package/examples/wiki.ts +30 -0
- package/examples/word.ts +168 -0
- package/package.json +17 -12
- package/readme.md +9 -38
- package/src/adapters/vercel.ts +7 -0
- package/src/agent-experimental.ts +221 -0
- package/src/agent.test.ts +187 -0
- package/src/agent.ts +260 -6
- package/src/decision.test.ts +179 -0
- package/src/decision.ts +83 -0
- package/src/index.ts +3 -2
- package/src/memory.ts +25 -0
- package/src/planners/shortestPathPlanner.ts +22 -0
- package/src/planners/simplePlanner.ts +126 -0
- package/src/schemas.ts +9 -20
- package/src/strategies/chain-of-note.ts +155 -0
- package/src/templates/defaultText.ts +18 -0
- package/src/templates/defaultToolCall.ts +10 -0
- package/src/text.ts +232 -0
- package/src/types.ts +363 -46
- package/src/utils.ts +13 -72
- package/tsconfig.json +1 -1
- package/examples/multiAgentCollaboration.ts +0 -0
- package/examples/numberGuesser.ts +0 -101
- package/examples/wordGuesser.ts +0 -144
- package/src/adapter.test.ts +0 -217
- package/src/adapters/openai.ts +0 -303
package/src/agent.ts
CHANGED
|
@@ -1,8 +1,262 @@
|
|
|
1
|
-
import {
|
|
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
|
|
4
|
-
|
|
5
|
-
) {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
});
|
package/src/decision.ts
ADDED
|
@@ -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 { defineEvents as defineEvents } from './schemas';
|
|
2
1
|
export { createAgent } from './agent';
|
|
3
|
-
export {
|
|
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
|
+
}
|