@statelyai/agent 1.1.6 → 2.0.0-next.1

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 (62) hide show
  1. package/.changeset/cyan-carpets-perform.md +5 -0
  2. package/.changeset/fast-donkeys-argue.md +5 -0
  3. package/.changeset/light-hats-drive.md +9 -0
  4. package/.changeset/old-jobs-check.md +5 -0
  5. package/.changeset/pre.json +13 -0
  6. package/.vscode/launch.json +6 -0
  7. package/CHANGELOG.md +22 -0
  8. package/dist/index.d.mts +262 -171
  9. package/dist/index.d.ts +262 -171
  10. package/dist/index.js +383 -274
  11. package/dist/index.mjs +386 -272
  12. package/examples/chatbot-alt.ts +57 -0
  13. package/examples/chatbot.ts +12 -17
  14. package/examples/cot.ts +26 -23
  15. package/examples/customer-service-sim.ts +107 -0
  16. package/examples/email.ts +37 -41
  17. package/examples/example.ts +6 -6
  18. package/examples/executor.ts +66 -0
  19. package/examples/goal.ts +12 -12
  20. package/examples/helpers/helpers.ts +26 -14
  21. package/examples/joke.ts +79 -76
  22. package/examples/jugs.ts +125 -0
  23. package/examples/multi.ts +5 -5
  24. package/examples/newspaper.ts +98 -104
  25. package/examples/number.ts +6 -5
  26. package/examples/raffle.ts +11 -12
  27. package/examples/river-crossing.ts +140 -0
  28. package/examples/sandbox.ts +1 -1
  29. package/examples/simple.ts +5 -3
  30. package/examples/summary.ts +121 -0
  31. package/examples/support.ts +6 -6
  32. package/examples/ticTacToe.ts +86 -45
  33. package/examples/todo.ts +7 -7
  34. package/examples/tutor.ts +15 -15
  35. package/examples/verify.ts +3 -3
  36. package/examples/weather.ts +6 -9
  37. package/examples/wiki.ts +27 -8
  38. package/examples/word.ts +16 -11
  39. package/package.json +16 -11
  40. package/readme.md +1 -1
  41. package/src/agent-experimental.ts +1 -1
  42. package/src/agent.test.ts +243 -214
  43. package/src/agent.ts +286 -95
  44. package/src/decide.test.ts +276 -0
  45. package/src/decide.ts +163 -0
  46. package/src/index.ts +1 -1
  47. package/src/middleware.ts +91 -0
  48. package/src/mockModel.ts +47 -0
  49. package/src/planners/shortestPath.test.ts +94 -0
  50. package/src/planners/shortestPath.ts +177 -0
  51. package/src/planners/simple.ts +105 -0
  52. package/src/strategies/chain-of-note.ts +6 -55
  53. package/src/text.ts +51 -144
  54. package/src/types.ts +187 -212
  55. package/src/utils.ts +48 -4
  56. package/vitest.config.ts +9 -3
  57. package/src/adapters/vercel.ts +0 -7
  58. package/src/decision.test.ts +0 -179
  59. package/src/decision.ts +0 -84
  60. package/src/memory.ts +0 -25
  61. package/src/planners/shortestPathPlanner.ts +0 -22
  62. package/src/planners/simplePlanner.ts +0 -139
@@ -0,0 +1,177 @@
1
+ import { generateObject } from 'ai';
2
+ import {
3
+ AgentPlan,
4
+ AgentPlanInput,
5
+ AgentPlanner,
6
+ AgentStep,
7
+ AnyAgent,
8
+ CostFunction,
9
+ ObservedState,
10
+ } from '../types';
11
+ import { getShortestPaths } from '@xstate/graph';
12
+ import { z } from 'zod';
13
+ import { zodToJsonSchema } from 'zod-to-json-schema';
14
+ import Ajv from 'ajv';
15
+ import { AnyMachineSnapshot } from 'xstate';
16
+
17
+ const ajv = new Ajv();
18
+
19
+ function observedStatesEqual(state1: ObservedState, state2: ObservedState) {
20
+ // check state value && state context
21
+ return (
22
+ JSON.stringify(state1.value) === JSON.stringify(state2.value) &&
23
+ JSON.stringify(state1.context) === JSON.stringify(state2.context)
24
+ );
25
+ }
26
+
27
+ function trimSteps(steps: AgentStep<any>[], currentState: ObservedState) {
28
+ const index = steps.findIndex(
29
+ (step) => step.state && observedStatesEqual(step.state, currentState)
30
+ );
31
+
32
+ if (index === -1) {
33
+ return undefined;
34
+ }
35
+
36
+ return steps.slice(index + 1, steps.length);
37
+ }
38
+
39
+ export function experimental_createShortestPathPlanner<
40
+ T extends AnyAgent
41
+ >(): AgentPlanner<T> {
42
+ return async function shortestPathPlanner<T extends AnyAgent>(
43
+ agent: T,
44
+ input: AgentPlanInput<any>
45
+ ): Promise<AgentPlan<any> | undefined> {
46
+ const costFunction: CostFunction<any> =
47
+ input.costFunction ?? ((path) => path.weight ?? Infinity);
48
+ const existingPlan = agent
49
+ .getPlans()
50
+ .find((p) => p.planner === 'shortestPath' && p.goal === input.goal);
51
+
52
+ let paths = existingPlan?.paths;
53
+
54
+ if (existingPlan) {
55
+ console.log('Existing plan found');
56
+ }
57
+
58
+ if (!input.machine && !existingPlan) {
59
+ return;
60
+ }
61
+
62
+ if (input.machine && !existingPlan) {
63
+ const contextSchema = zodToJsonSchema(z.object(agent.context));
64
+ const result = await generateObject({
65
+ model: agent.model,
66
+ system: input.system ?? agent.description,
67
+ prompt: `
68
+ <goal>
69
+ ${input.goal}
70
+ </goal>
71
+ <contextSchema>
72
+ ${contextSchema}
73
+ </contextSchema>
74
+
75
+
76
+ Update the context JSON schema so that it validates the context to determine that it reaches the goal. Return the result as a diff.
77
+
78
+ The contextSchema properties must not change. Do not add or remove properties, or modify the name of the properties.
79
+ Use "const" for exact required values and define ranges/types for flexible conditions.
80
+
81
+ Examples:
82
+ 1. For "user is logged in with admin role":
83
+ {
84
+ "contextSchema": "{"type": "object", "properties": {"role": {"const": "admin"}, "lastLogin": {"type": "string"}}, "required": ["role"]}"
85
+ }
86
+
87
+ 2. For "score is above 100":
88
+ {
89
+ "contextSchema": "{"type": "object", "properties": {"score": {"type": "number", "minimum": 100}}, "required": ["score"]}"
90
+ }
91
+
92
+ 3. For "fruits contain apple, orange, banana":
93
+ {
94
+ "type": "array",
95
+ "allOf": [
96
+ { "contains": { "const": "apple" } },
97
+ { "contains": { "const": "orange" } },
98
+ { "contains": { "const": "banana" } }
99
+ ]
100
+ }
101
+ `.trim(),
102
+ schema: z.object({
103
+ // valueSchema: z
104
+ // .string()
105
+ // .describe('The JSON Schema representing the goal state value'),
106
+ contextSchema: z
107
+ .object({
108
+ type: z.literal('object'),
109
+ properties: z.object(
110
+ Object.keys((contextSchema as any).properties).reduce(
111
+ (acc, key) => {
112
+ acc[key] = z.any();
113
+ return acc;
114
+ },
115
+ {} as any
116
+ )
117
+ ),
118
+ required: z.array(z.string()).optional(),
119
+ })
120
+ .describe('The JSON Schema representing the goal state context'),
121
+ }),
122
+ });
123
+
124
+ console.log(result.object);
125
+ const validateContext = ajv.compile(result.object.contextSchema);
126
+
127
+ const stateFilter = (state: AnyMachineSnapshot) => {
128
+ return validateContext(state.context);
129
+ };
130
+
131
+ const resolvedState = input.machine.resolveState({
132
+ ...input.state,
133
+ context: input.state.context ?? {},
134
+ });
135
+
136
+ paths = getShortestPaths(input.machine, {
137
+ fromState: resolvedState,
138
+ toState: stateFilter,
139
+ });
140
+ }
141
+
142
+ if (!paths) {
143
+ return undefined;
144
+ }
145
+
146
+ const trimmedPaths = paths
147
+ .map((path) => {
148
+ const trimmedSteps = trimSteps(path.steps, input.state);
149
+ if (!trimmedSteps) {
150
+ return undefined;
151
+ }
152
+ return {
153
+ ...path,
154
+ steps: trimmedSteps,
155
+ };
156
+ })
157
+ .filter((p): p is NonNullable<typeof p> => p !== undefined);
158
+
159
+ // Sort paths from least weight to most weight
160
+ const sortedPaths = trimmedPaths.sort(
161
+ (a, b) => costFunction(a) - costFunction(b)
162
+ );
163
+
164
+ const leastWeightPath = sortedPaths[0];
165
+ const nextStep = leastWeightPath?.steps[0];
166
+
167
+ return {
168
+ planner: 'shortestPath',
169
+ episodeId: agent.episodeId,
170
+ goal: input.goal,
171
+ goalState: paths[0]?.state,
172
+ nextEvent: nextStep?.event,
173
+ paths,
174
+ timestamp: Date.now(),
175
+ };
176
+ };
177
+ }
@@ -0,0 +1,105 @@
1
+ import { CoreMessage, generateText } from 'ai';
2
+ import { AgentPlan, AgentPlanInput, PromptTemplate, AnyAgent } from '../types';
3
+ import { randomId } from '../utils';
4
+ import { getNextSnapshot } from 'xstate';
5
+ import { defaultTextTemplate } from '../templates/defaultText';
6
+ import { getMessages } from '../text';
7
+ import { getToolMap } from '../decide';
8
+
9
+ const simplePlannerPromptTemplate: PromptTemplate<any> = (data) => {
10
+ return `
11
+ ${defaultTextTemplate(data)}
12
+
13
+ Make at most one tool call to achieve the above goal. If the goal cannot be achieved with any tool calls, do not make any tool call.
14
+ `.trim();
15
+ };
16
+
17
+ export async function simplePlanner<T extends AnyAgent>(
18
+ agent: T,
19
+ input: AgentPlanInput<any>
20
+ ): Promise<AgentPlan<any> | undefined> {
21
+ const toolMap = getToolMap(agent, input);
22
+
23
+ if (!toolMap) {
24
+ // No valid transitions for the specified tools
25
+ return undefined;
26
+ }
27
+
28
+ // Create a prompt with the given context and goal.
29
+ // The template is used to ensure that a single tool call at most is made.
30
+ const prompt = simplePlannerPromptTemplate({
31
+ context: input.state.context,
32
+ goal: input.goal,
33
+ });
34
+
35
+ const messages = await getMessages(agent, prompt, input);
36
+
37
+ const model = input.model ? agent.wrap(input.model) : agent.model;
38
+
39
+ const {
40
+ state,
41
+ machine,
42
+ previousPlan,
43
+ events,
44
+ goal,
45
+ model: _,
46
+ ...rest
47
+ } = input;
48
+
49
+ const machineState = input.machine
50
+ ? input.machine.resolveState({
51
+ ...input.state,
52
+ context: input.state.context,
53
+ })
54
+ : undefined;
55
+
56
+ const result = await generateText({
57
+ ...rest,
58
+ system: input.system ?? agent.description,
59
+ model,
60
+ messages,
61
+ tools: toolMap as any,
62
+ toolChoice: input.toolChoice ?? 'required',
63
+ });
64
+
65
+ result.response.messages.forEach((m) => {
66
+ const message: CoreMessage = m;
67
+
68
+ agent.addMessage({
69
+ ...message,
70
+ id: randomId(),
71
+ timestamp: Date.now(),
72
+ });
73
+ });
74
+
75
+ const singleResult = result.toolResults[0];
76
+
77
+ if (!singleResult) {
78
+ // TODO: retries?
79
+ console.warn('No tool call results returned');
80
+ return undefined;
81
+ }
82
+
83
+ return {
84
+ planner: 'simple',
85
+ goal: input.goal,
86
+ goalState: input.state,
87
+ nextEvent: singleResult.result,
88
+ episodeId: agent.episodeId,
89
+ timestamp: Date.now(),
90
+ paths: [
91
+ {
92
+ state: undefined,
93
+ steps: [
94
+ {
95
+ event: singleResult.result,
96
+ state:
97
+ machine && machineState
98
+ ? getNextSnapshot(machine, machineState, singleResult.result)
99
+ : undefined,
100
+ },
101
+ ],
102
+ },
103
+ ],
104
+ };
105
+ }
@@ -67,8 +67,8 @@ export const chainOfNote = setup({
67
67
  },
68
68
  }).createMachine({
69
69
  initial: 'searching',
70
- context: (x) => ({
71
- ...x.input,
70
+ context: ({ input }) => ({
71
+ ...input,
72
72
  searchResults: null,
73
73
  summaries: null,
74
74
  }),
@@ -76,8 +76,8 @@ export const chainOfNote = setup({
76
76
  searching: {
77
77
  invoke: {
78
78
  src: 'searchWiki',
79
- input: (x) => ({
80
- query: x.context.prompt,
79
+ input: ({ context }) => ({
80
+ query: context.prompt,
81
81
  }),
82
82
  onDone: {
83
83
  actions: assign({
@@ -90,8 +90,8 @@ export const chainOfNote = setup({
90
90
  extracting: {
91
91
  invoke: {
92
92
  src: 'extractSummaries',
93
- input: (x) => ({
94
- searchResult: x.context.searchResults!,
93
+ input: ({ context }) => ({
94
+ searchResult: context.searchResults!,
95
95
  }),
96
96
  onDone: {
97
97
  actions: assign({
@@ -104,52 +104,3 @@ export const chainOfNote = setup({
104
104
  generating: {},
105
105
  },
106
106
  });
107
-
108
- // export function chainOfNote() {
109
- // return {
110
- // generateText: async (x) => {
111
- // const passages = await wiki.search(x.prompt!, {
112
- // limit: 5,
113
- // });
114
-
115
- // const extracts = await Promise.all(
116
- // passages.results.map(async (p) => {
117
- // const summary = await wiki.summary(p.title);
118
- // return summary.extract;
119
- // })
120
- // );
121
- // x.agent?.addMessage({
122
- // content: x.prompt!,
123
- // id: Date.now() + '',
124
- // role: 'user',
125
- // timestamp: Date.now(),
126
- // });
127
- // const result = await generateText({
128
- // model: x.model,
129
- // system: `Task Description:
130
-
131
- // 1. Read the given question and five Wikipedia passages to gather relevant information.
132
-
133
- // 2. Write reading notes summarizing the key points from these passages.
134
-
135
- // 3. Discuss the relevance of the given question and Wikipedia passages.
136
-
137
- // 4. If some passages are relevant to the given question, provide a brief answer based on the passages.
138
-
139
- // 5. If no passage is relevant, direcly provide answer without considering the passages.
140
-
141
- // Passages: \n${extracts.join('\n')}`,
142
- // prompt: `${x.prompt!}`,
143
- // });
144
-
145
- // x.agent?.addMessage({
146
- // content: result.text,
147
- // id: Date.now() + '',
148
- // role: 'user',
149
- // timestamp: Date.now(),
150
- // });
151
-
152
- // return result;
153
- // },
154
- // } satisfies AgentStrategy;
155
- // }
package/src/text.ts CHANGED
@@ -1,9 +1,13 @@
1
- import type { CoreMessage, CoreTool, GenerateTextResult } from 'ai';
1
+ import {
2
+ generateText,
3
+ streamText,
4
+ type CoreMessage,
5
+ type CoreTool,
6
+ type GenerateTextResult,
7
+ } from 'ai';
2
8
  import {
3
9
  AgentGenerateTextOptions,
4
- AgentGenerateTextResult,
5
10
  AgentStreamTextOptions,
6
- AgentStreamTextResult,
7
11
  AnyAgent,
8
12
  } from './types';
9
13
  import { defaultTextTemplate } from './templates/defaultText';
@@ -15,7 +19,6 @@ import {
15
19
  fromPromise,
16
20
  toObserver,
17
21
  } from 'xstate';
18
- import { randomId } from './utils';
19
22
 
20
23
  /**
21
24
  * Gets an array of messages from the given prompt, based on the agent and options.
@@ -45,158 +48,39 @@ export async function getMessages(
45
48
  return messages;
46
49
  }
47
50
 
48
- export async function agentGenerateText<T extends AnyAgent>(
49
- agent: T,
50
- options: AgentGenerateTextOptions
51
- ): Promise<AgentGenerateTextResult> {
52
- const resolvedOptions = {
53
- ...agent.defaultOptions,
54
- ...options,
55
- correlationId: options.correlationId ?? randomId(),
56
- };
57
- // Generate a correlation ID if one is not provided
58
- const template = resolvedOptions.template ?? defaultTextTemplate;
59
- // TODO: check if messages was provided instead
60
- const id = randomId();
61
- const goal =
62
- typeof resolvedOptions.prompt === 'string'
63
- ? resolvedOptions.prompt
64
- : await resolvedOptions.prompt(agent);
65
-
66
- const promptWithContext = template({
67
- goal,
68
- context: resolvedOptions.context,
69
- });
70
-
71
- const messages = await getMessages(agent, promptWithContext, resolvedOptions);
72
-
73
- agent.addMessage({
74
- id,
75
- role: 'user',
76
- content: promptWithContext,
77
- timestamp: Date.now(),
78
- correlationId: resolvedOptions.correlationId,
79
- parentCorrelationId: resolvedOptions.parentCorrelationId,
80
- });
81
-
82
- const result = await agent.adapter.generateText({
83
- ...resolvedOptions,
84
- prompt: undefined,
85
- messages,
86
- });
87
-
88
- agent.addMessage({
89
- content: result.text,
90
- id,
91
- role: 'assistant',
92
- timestamp: Date.now(),
93
- responseId: id,
94
- result,
95
- correlationId: resolvedOptions.correlationId,
96
- parentCorrelationId: resolvedOptions.parentCorrelationId,
97
- });
98
-
99
- return {
100
- ...result,
101
- parentCorrelationId: resolvedOptions.parentCorrelationId,
102
- correlationId: resolvedOptions.correlationId,
103
- };
104
- }
105
-
106
- export async function agentStreamText(
107
- agent: AnyAgent,
108
- options: AgentStreamTextOptions
109
- ): Promise<AgentStreamTextResult> {
110
- const resolvedOptions = {
111
- ...agent.defaultOptions,
112
- ...options,
113
- correlationId: options.correlationId ?? randomId(),
114
- };
115
- const template = resolvedOptions.template ?? defaultTextTemplate;
116
-
117
- const id = randomId();
118
- const goal =
119
- typeof resolvedOptions.prompt === 'string'
120
- ? resolvedOptions.prompt
121
- : await resolvedOptions.prompt(agent);
122
-
123
- const promptWithContext = template({
124
- goal,
125
- context: resolvedOptions.context,
126
- });
127
-
128
- const messages = await getMessages(agent, promptWithContext, resolvedOptions);
129
-
130
- agent.addMessage({
131
- role: 'user',
132
- content: promptWithContext,
133
- id,
134
- timestamp: Date.now(),
135
- correlationId: resolvedOptions.correlationId,
136
- parentCorrelationId: resolvedOptions.parentCorrelationId,
137
- });
138
-
139
- const result = await agent.adapter.streamText({
140
- ...resolvedOptions,
141
- prompt: undefined,
142
- messages,
143
- onFinish: async (res) => {
144
- agent.addMessage({
145
- role: 'assistant',
146
- result: {
147
- text: res.text,
148
- finishReason: res.finishReason,
149
- logprobs: undefined,
150
- responseMessages: [],
151
- toolCalls: [],
152
- toolResults: [],
153
- usage: res.usage,
154
- warnings: res.warnings,
155
- rawResponse: res.rawResponse,
156
- roundtrips: [], // TODO: how do we get this information?,
157
- steps: res.steps,
158
- response: res.response,
159
- experimental_providerMetadata: res.experimental_providerMetadata,
160
- },
161
- content: res.text,
162
- id: randomId(),
163
- timestamp: Date.now(),
164
- responseId: id,
165
- correlationId: resolvedOptions.correlationId,
166
- parentCorrelationId: resolvedOptions.parentCorrelationId,
167
- });
168
- },
169
- });
170
-
171
- return {
172
- ...result,
173
- textStream: result.textStream,
174
- fullStream: result.fullStream,
175
- parentCorrelationId: resolvedOptions.parentCorrelationId,
176
- correlationId: resolvedOptions.correlationId,
177
- } as unknown as AgentStreamTextResult; // TODO: fix
178
- }
179
-
180
51
  export function fromTextStream<T extends AnyAgent>(
181
52
  agent: T,
182
- defaultOptions?: AgentStreamTextOptions
53
+ options?: AgentStreamTextOptions
183
54
  ): ObservableActorLogic<
184
55
  { textDelta: string },
185
56
  Omit<AgentStreamTextOptions, 'context'> & {
186
57
  context?: AgentStreamTextOptions['context'];
187
58
  }
188
59
  > {
60
+ const template = options?.template ?? defaultTextTemplate;
189
61
  return fromObservable(({ input }) => {
190
62
  const observers = new Set<Observer<{ textDelta: string }>>();
191
63
 
192
64
  // TODO: check if messages was provided instead
193
65
 
194
66
  (async () => {
195
- const result = await agentStreamText(agent, {
196
- ...defaultOptions,
197
- ...input,
67
+ const model = input.model ? agent.wrap(input.model) : agent.model;
68
+ const goal =
69
+ typeof input.prompt === 'string'
70
+ ? input.prompt
71
+ : await input.prompt(agent);
72
+ const promptWithContext = template({
73
+ goal,
198
74
  context: input.context,
199
75
  });
76
+ const messages = await getMessages(agent, promptWithContext, input);
77
+ const result = await streamText({
78
+ ...options,
79
+ ...input,
80
+ prompt: undefined, // overwritten by messages
81
+ model,
82
+ messages,
83
+ });
200
84
 
201
85
  for await (const part of result.fullStream) {
202
86
  if (part.type === 'text-delta') {
@@ -224,18 +108,41 @@ export function fromTextStream<T extends AnyAgent>(
224
108
 
225
109
  export function fromText<T extends AnyAgent>(
226
110
  agent: T,
227
- defaultOptions?: AgentGenerateTextOptions
111
+ options?: AgentGenerateTextOptions
228
112
  ): PromiseActorLogic<
229
113
  GenerateTextResult<Record<string, CoreTool<any, any>>>,
230
114
  Omit<AgentGenerateTextOptions, 'context'> & {
231
115
  context?: AgentGenerateTextOptions['context'];
232
116
  }
233
117
  > {
118
+ const resolvedOptions = {
119
+ ...agent.defaultOptions,
120
+ ...options,
121
+ };
122
+
123
+ const template = resolvedOptions.template ?? defaultTextTemplate;
124
+
234
125
  return fromPromise(async ({ input }) => {
235
- return await agentGenerateText(agent, {
236
- ...input,
237
- ...defaultOptions,
126
+ const goal =
127
+ typeof input.prompt === 'string'
128
+ ? input.prompt
129
+ : await input.prompt(agent);
130
+
131
+ const promptWithContext = template({
132
+ goal,
238
133
  context: input.context,
239
134
  });
135
+
136
+ const messages = await getMessages(agent, promptWithContext, input);
137
+
138
+ const model = input.model ? agent.wrap(input.model) : agent.model;
139
+
140
+ return await generateText({
141
+ ...input,
142
+ ...options,
143
+ prompt: undefined,
144
+ messages,
145
+ model,
146
+ });
240
147
  });
241
148
  }