@statelyai/agent 1.1.6 → 2.0.0-next.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 (54) hide show
  1. package/.changeset/light-hats-drive.md +9 -0
  2. package/.changeset/pre.json +10 -0
  3. package/.vscode/launch.json +6 -0
  4. package/CHANGELOG.md +10 -0
  5. package/dist/index.d.mts +262 -165
  6. package/dist/index.d.ts +262 -165
  7. package/dist/index.js +368 -263
  8. package/dist/index.mjs +371 -261
  9. package/examples/chatbot-alt.ts +57 -0
  10. package/examples/chatbot.ts +11 -16
  11. package/examples/cot.ts +25 -22
  12. package/examples/customer-service-sim.ts +107 -0
  13. package/examples/email.ts +14 -14
  14. package/examples/example.ts +5 -5
  15. package/examples/executor.ts +66 -0
  16. package/examples/goal.ts +11 -11
  17. package/examples/helpers/helpers.ts +26 -14
  18. package/examples/joke.ts +78 -75
  19. package/examples/jugs.ts +125 -0
  20. package/examples/multi.ts +4 -4
  21. package/examples/newspaper.ts +98 -104
  22. package/examples/number.ts +5 -4
  23. package/examples/raffle.ts +10 -11
  24. package/examples/river-crossing.ts +140 -0
  25. package/examples/sandbox.ts +1 -1
  26. package/examples/simple.ts +4 -2
  27. package/examples/summary.ts +121 -0
  28. package/examples/support.ts +5 -5
  29. package/examples/ticTacToe.ts +86 -45
  30. package/examples/todo.ts +6 -6
  31. package/examples/tutor.ts +13 -13
  32. package/examples/verify.ts +2 -2
  33. package/examples/weather.ts +5 -8
  34. package/examples/wiki.ts +26 -7
  35. package/examples/word.ts +15 -10
  36. package/package.json +13 -10
  37. package/readme.md +1 -1
  38. package/src/agent-experimental.ts +1 -1
  39. package/src/agent.test.ts +117 -228
  40. package/src/agent.ts +469 -81
  41. package/src/{decision.test.ts → decide.test.ts} +26 -50
  42. package/src/decide.ts +153 -0
  43. package/src/index.ts +1 -1
  44. package/src/middleware.ts +103 -0
  45. package/src/mockModel.ts +47 -0
  46. package/src/planners/shortestPathPlanner.ts +151 -13
  47. package/src/planners/simplePlanner.ts +57 -85
  48. package/src/strategies/chain-of-note.ts +6 -55
  49. package/src/text.ts +51 -144
  50. package/src/types.ts +172 -204
  51. package/src/utils.ts +37 -4
  52. package/src/adapters/vercel.ts +0 -7
  53. package/src/decision.ts +0 -84
  54. package/src/memory.ts +0 -25
@@ -1,61 +1,39 @@
1
1
  import { test, expect } from 'vitest';
2
- import { createAgent, fromDecision, type AIAdapter } from './';
2
+ import { createAgent, fromDecision } from '.';
3
3
  import { createActor, createMachine, waitFor } from 'xstate';
4
4
  import { z } from 'zod';
5
- import { GenerateTextResult } from 'ai';
5
+ import { LanguageModelV1CallOptions } from 'ai';
6
+ import { dummyResponseValues, MockLanguageModelV1 } from './mockModel';
6
7
 
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
- }
8
+ const doGenerate = async (params: LanguageModelV1CallOptions) => {
9
+ const keys =
10
+ params.mode.type === 'regular' ? params.mode.tools?.map((t) => t.name) : [];
19
11
 
20
12
  return {
21
- toolResults: [
13
+ ...dummyResponseValues,
14
+ finishReason: 'tool-calls',
15
+ toolCalls: [
22
16
  {
23
- result: {
24
- type: keys[0],
25
- },
17
+ toolCallType: 'function',
18
+ toolCallId: 'call-1',
19
+ toolName: keys![0],
20
+ args: `{ "type": "${keys?.[0]}" }`,
26
21
  },
27
22
  ],
28
- } as any as GenerateTextResult<any>;
23
+ } as any;
29
24
  };
30
25
 
31
26
  test('fromDecision() makes a decision', async () => {
27
+ const model = new MockLanguageModelV1({
28
+ doGenerate,
29
+ });
32
30
  const agent = createAgent({
33
31
  name: 'test',
34
- model: {} as any,
32
+ model,
35
33
  events: {
36
34
  doFirst: z.object({}),
37
35
  doSecond: z.object({}),
38
36
  },
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
37
  });
60
38
 
61
39
  const machine = createMachine({
@@ -91,17 +69,16 @@ test('fromDecision() makes a decision', async () => {
91
69
  });
92
70
 
93
71
  test('interacts with an actor', async () => {
72
+ const model = new MockLanguageModelV1({
73
+ doGenerate,
74
+ });
94
75
  const agent = createAgent({
95
76
  name: 'test',
96
- model: {} as any,
77
+ model,
97
78
  events: {
98
79
  doFirst: z.object({}),
99
80
  doSecond: z.object({}),
100
81
  },
101
- adapter: {
102
- generateText: mockToolDecision,
103
- streamText: {} as any,
104
- },
105
82
  });
106
83
 
107
84
  const machine = createMachine({
@@ -135,17 +112,16 @@ test('interacts with an actor', async () => {
135
112
  });
136
113
 
137
114
  test('interacts with an actor (late interaction)', async () => {
115
+ const model = new MockLanguageModelV1({
116
+ doGenerate,
117
+ });
138
118
  const agent = createAgent({
139
119
  name: 'test',
140
- model: {} as any,
120
+ model,
141
121
  events: {
142
122
  doFirst: z.object({}),
143
123
  doSecond: z.object({}),
144
124
  },
145
- adapter: {
146
- generateText: mockToolDecision,
147
- streamText: {} as any,
148
- },
149
125
  });
150
126
 
151
127
  const machine = createMachine({
package/src/decide.ts ADDED
@@ -0,0 +1,153 @@
1
+ import { AnyActor, AnyMachineSnapshot, fromPromise } from 'xstate';
2
+ import {
3
+ AnyAgent,
4
+ AgentDecideOptions,
5
+ AgentDecisionLogic,
6
+ AgentDecisionInput,
7
+ AgentPlanner,
8
+ AgentPlan,
9
+ EventsFromZodEventMapping,
10
+ AgentPlanInput,
11
+ TransitionData,
12
+ } from './types';
13
+ import { simplePlanner } from './planners/simplePlanner';
14
+ import { getTransitions } from './utils';
15
+ import { CoreTool, tool } from 'ai';
16
+
17
+ export async function agentDecide<T extends AnyAgent>(
18
+ agent: T,
19
+ options: AgentDecideOptions
20
+ ): Promise<AgentPlan<EventsFromZodEventMapping<T['events']>> | undefined> {
21
+ const resolvedOptions = {
22
+ ...agent.defaultOptions,
23
+ ...options,
24
+ };
25
+ const {
26
+ planner = simplePlanner as AgentPlanner<any>,
27
+ goal,
28
+ events = agent.events,
29
+ state,
30
+ machine,
31
+ model = agent.model,
32
+ ...otherPlanInput
33
+ } = resolvedOptions;
34
+
35
+ const plan = await planner(agent, {
36
+ model,
37
+ goal,
38
+ events,
39
+ state,
40
+ machine,
41
+ ...otherPlanInput,
42
+ });
43
+
44
+ if (plan?.nextEvent) {
45
+ agent.addPlan(plan);
46
+ await resolvedOptions.execute?.(plan.nextEvent);
47
+ }
48
+
49
+ return plan;
50
+ }
51
+
52
+ export function fromDecision(
53
+ agent: AnyAgent,
54
+ defaultInput?: AgentDecisionInput
55
+ ): AgentDecisionLogic<any> {
56
+ return fromPromise(async ({ input, self }) => {
57
+ const parentRef = self._parent;
58
+ if (!parentRef) {
59
+ return;
60
+ }
61
+
62
+ const snapshot = parentRef.getSnapshot() as AnyMachineSnapshot;
63
+ const inputObject = typeof input === 'string' ? { goal: input } : input;
64
+ const resolvedInput = {
65
+ ...defaultInput,
66
+ ...inputObject,
67
+ };
68
+ const state = {
69
+ value: snapshot.value,
70
+ context: resolvedInput.context,
71
+ };
72
+
73
+ const plan = await agentDecide(agent, {
74
+ machine: (parentRef as AnyActor).logic,
75
+ state,
76
+ execute: async (event) => {
77
+ parentRef.send(event);
78
+ },
79
+ ...resolvedInput,
80
+ });
81
+
82
+ return plan;
83
+ }) as AgentDecisionLogic<any>;
84
+ }
85
+
86
+ export function getToolMap<T extends AnyAgent>(
87
+ _agent: T,
88
+ input: AgentPlanInput<any>
89
+ ): Record<string, CoreTool<any, any>> | undefined {
90
+ // Get all of the possible next transitions
91
+ const transitions: TransitionData[] = input.machine
92
+ ? getTransitions(input.state, input.machine)
93
+ : Object.entries(input.events).map(([eventType, { description }]) => ({
94
+ eventType,
95
+ description,
96
+ }));
97
+
98
+ // Only keep the transitions that match the event types that are in the event mapping
99
+ // TODO: allow for custom filters
100
+ const filter = (eventType: string) =>
101
+ Object.keys(input.events).includes(eventType);
102
+
103
+ // Mapping of each event type (e.g. "mouse.click")
104
+ // to a valid function name (e.g. "mouse_click")
105
+ const functionNameMapping: Record<string, string> = {};
106
+
107
+ const toolTransitions = transitions
108
+ .filter((t) => {
109
+ return filter(t.eventType);
110
+ })
111
+ .map((t) => {
112
+ const name = t.eventType.replace(/\./g, '_');
113
+ functionNameMapping[name] = t.eventType;
114
+
115
+ return {
116
+ type: 'function',
117
+ eventType: t.eventType,
118
+ description: t.description,
119
+ name,
120
+ } as const;
121
+ });
122
+
123
+ // Convert the transition data to a tool map that the
124
+ // Vercel AI SDK can use
125
+ const toolMap: Record<string, CoreTool<any, any>> = {};
126
+ for (const toolTransitionData of toolTransitions) {
127
+ const toolZodType = input.events?.[toolTransitionData.eventType];
128
+
129
+ if (!toolZodType) {
130
+ continue;
131
+ }
132
+
133
+ toolMap[toolTransitionData.name] = tool({
134
+ description: toolZodType?.description ?? toolTransitionData.description,
135
+ parameters: toolZodType,
136
+ execute: async (params: Record<string, any>) => {
137
+ const event = {
138
+ type: toolTransitionData.eventType,
139
+ ...params,
140
+ };
141
+
142
+ return event;
143
+ },
144
+ });
145
+ }
146
+
147
+ if (!Object.keys(toolMap).length) {
148
+ // No valid transitions for the specified tools
149
+ return undefined;
150
+ }
151
+
152
+ return toolMap;
153
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { createAgent } from './agent';
2
2
  export { fromText, fromTextStream } from './text';
3
- export { fromDecision } from './decision';
3
+ export { fromDecision } from './decide';
4
4
  export * from './types';
@@ -0,0 +1,103 @@
1
+ import {
2
+ Experimental_LanguageModelV1Middleware as LanguageModelV1Middleware,
3
+ LanguageModelV1StreamPart,
4
+ } from 'ai';
5
+ import {
6
+ AnyAgent,
7
+ LanguageModelV1TextPart,
8
+ LanguageModelV1ToolCallPart,
9
+ } from './types';
10
+ import { randomId } from './utils';
11
+
12
+ export function createAgentMiddleware(agent: AnyAgent) {
13
+ const middleware: LanguageModelV1Middleware = {
14
+ transformParams: async ({ params }) => {
15
+ return params;
16
+ },
17
+ wrapGenerate: async ({ doGenerate, params }) => {
18
+ const id = randomId();
19
+
20
+ params.prompt.forEach((p) => {
21
+ agent.addMessage({
22
+ id,
23
+ ...p,
24
+ timestamp: Date.now(),
25
+ correlationId: params.providerMetadata
26
+ ?.correlationId as unknown as string,
27
+ parentCorrelationId: params.providerMetadata
28
+ ?.parentCorrelationId as unknown as string,
29
+ });
30
+ });
31
+
32
+ const result = await doGenerate();
33
+
34
+ return result;
35
+ },
36
+
37
+ wrapStream: async ({ doStream, params }) => {
38
+ const id = randomId();
39
+
40
+ params.prompt.forEach((message) => {
41
+ message.content;
42
+ agent.addMessage({
43
+ id,
44
+ ...message,
45
+ timestamp: Date.now(),
46
+ correlationId: params.providerMetadata
47
+ ?.correlationId as unknown as string,
48
+ parentCorrelationId: params.providerMetadata
49
+ ?.parentCorrelationId as unknown as string,
50
+ });
51
+ });
52
+
53
+ const { stream, ...rest } = await doStream();
54
+
55
+ let generatedText = '';
56
+
57
+ const transformStream = new TransformStream<
58
+ LanguageModelV1StreamPart,
59
+ LanguageModelV1StreamPart
60
+ >({
61
+ transform(chunk, controller) {
62
+ if (chunk.type === 'text-delta') {
63
+ generatedText += chunk.textDelta;
64
+ }
65
+
66
+ controller.enqueue(chunk);
67
+ },
68
+
69
+ flush() {
70
+ const content: (
71
+ | LanguageModelV1TextPart
72
+ | LanguageModelV1ToolCallPart
73
+ )[] = [];
74
+
75
+ if (generatedText) {
76
+ content.push({
77
+ type: 'text',
78
+ text: generatedText,
79
+ });
80
+ }
81
+
82
+ agent.addMessage({
83
+ id: randomId(),
84
+ timestamp: Date.now(),
85
+ role: 'assistant',
86
+ content,
87
+ responseId: id,
88
+ correlationId: params.providerMetadata
89
+ ?.correlationId as unknown as string,
90
+ parentCorrelationId: params.providerMetadata
91
+ ?.parentCorrelationId as unknown as string,
92
+ });
93
+ },
94
+ });
95
+
96
+ return {
97
+ stream: stream.pipeThrough(transformStream),
98
+ ...rest,
99
+ };
100
+ },
101
+ };
102
+ return middleware;
103
+ }
@@ -0,0 +1,47 @@
1
+ import { LanguageModelV1 } from 'ai';
2
+
3
+ export class MockLanguageModelV1 implements LanguageModelV1 {
4
+ readonly specificationVersion = 'v1';
5
+
6
+ readonly provider: LanguageModelV1['provider'];
7
+ readonly modelId: LanguageModelV1['modelId'];
8
+
9
+ doGenerate: LanguageModelV1['doGenerate'];
10
+ doStream: LanguageModelV1['doStream'];
11
+
12
+ readonly defaultObjectGenerationMode: LanguageModelV1['defaultObjectGenerationMode'];
13
+ readonly supportsStructuredOutputs: LanguageModelV1['supportsStructuredOutputs'];
14
+ constructor({
15
+ provider = 'mock-provider',
16
+ modelId = 'mock-model-id',
17
+ doGenerate = notImplemented,
18
+ doStream = notImplemented,
19
+ defaultObjectGenerationMode = undefined,
20
+ supportsStructuredOutputs = undefined,
21
+ }: {
22
+ provider?: LanguageModelV1['provider'];
23
+ modelId?: LanguageModelV1['modelId'];
24
+ doGenerate?: LanguageModelV1['doGenerate'];
25
+ doStream?: LanguageModelV1['doStream'];
26
+ defaultObjectGenerationMode?: LanguageModelV1['defaultObjectGenerationMode'];
27
+ supportsStructuredOutputs?: LanguageModelV1['supportsStructuredOutputs'];
28
+ } = {}) {
29
+ this.provider = provider;
30
+ this.modelId = modelId;
31
+ this.doGenerate = doGenerate;
32
+ this.doStream = doStream;
33
+
34
+ this.defaultObjectGenerationMode = defaultObjectGenerationMode;
35
+ this.supportsStructuredOutputs = supportsStructuredOutputs;
36
+ }
37
+ }
38
+
39
+ function notImplemented(): never {
40
+ throw new Error('Not implemented');
41
+ }
42
+
43
+ export const dummyResponseValues = {
44
+ rawCall: { rawPrompt: 'prompt', rawSettings: {} },
45
+ finishReason: 'stop' as const,
46
+ usage: { promptTokens: 10, completionTokens: 20 },
47
+ };
@@ -1,22 +1,160 @@
1
- import { Agent, AgentPlan, AgentPlanInput, AnyAgent } from '../types';
1
+ import { generateObject } from 'ai';
2
+ import { getToolMap } from '../decide';
3
+ import {
4
+ AgentPlan,
5
+ AgentPlanInput,
6
+ AgentStep,
7
+ AnyAgent,
8
+ CostFunction,
9
+ ObservedState,
10
+ } from '../types';
2
11
  import { getShortestPaths } from '@xstate/graph';
12
+ import { z } from 'zod';
13
+ import { zodToJsonSchema } from 'zod-to-json-schema';
14
+ import Ajv from 'ajv';
3
15
 
4
- export async function simplePlanner<T extends AnyAgent>(
16
+ const ajv = new Ajv();
17
+
18
+ function observedStatesEqual(state1: ObservedState, state2: ObservedState) {
19
+ // check state value && state context
20
+ return (
21
+ JSON.stringify(state1.value) === JSON.stringify(state2.value) &&
22
+ JSON.stringify(state1.context) === JSON.stringify(state2.context)
23
+ );
24
+ }
25
+
26
+ function trimSteps(steps: AgentStep<any>[], currentState: ObservedState) {
27
+ const index = steps.findIndex(
28
+ (step) => step.state && observedStatesEqual(step.state, currentState)
29
+ );
30
+
31
+ if (index === -1) {
32
+ return undefined;
33
+ }
34
+
35
+ return steps.slice(index + 1, steps.length);
36
+ }
37
+
38
+ export async function shortestPathPlanner<T extends AnyAgent>(
5
39
  agent: T,
6
40
  input: AgentPlanInput<any>
7
41
  ): 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;
42
+ const costFunction: CostFunction<any> =
43
+ input.costFunction ?? ((path) => path.weight ?? Infinity);
44
+ const existingPlan = agent
45
+ .getPlans()
46
+ .find((p) => p.planner === 'shortestPath' && p.goal === input.goal);
47
+
48
+ let paths = existingPlan?.paths;
49
+
50
+ if (existingPlan) {
51
+ console.log('Existing plan found');
52
+ }
53
+
54
+ if (!input.machine && !existingPlan) {
55
+ return;
56
+ }
57
+
58
+ if (input.machine && !existingPlan) {
59
+ const contextSchema = zodToJsonSchema(z.object(agent.context));
60
+ const result = await generateObject({
61
+ model: agent.model,
62
+ prompt: `
63
+ <goal>
64
+ ${input.goal}
65
+ </goal>
66
+ <contextSchema>
67
+ ${contextSchema}
68
+ </contextSchema>
69
+
70
+
71
+ Update the context JSON schema so that it validates the context to determine that it reaches the goal. Return the result as a diff.
72
+
73
+ The contextSchema properties must not change. Do not add or remove properties, or modify the name of the properties.
74
+ Use "const" for exact required values and define ranges/types for flexible conditions.
75
+
76
+ Examples:
77
+ 1. For "user is logged in with admin role":
78
+ {
79
+ "contextSchema": "{"type": "object", "properties": {"role": {"const": "admin"}, "lastLogin": {"type": "string"}}, "required": ["role"]}"
80
+ }
81
+
82
+ 2. For "score is above 100":
83
+ {
84
+ "contextSchema": "{"type": "object", "properties": {"score": {"type": "number", "minimum": 100}}, "required": ["score"]}"
85
+ }
86
+ `.trim(),
87
+ schema: z.object({
88
+ // valueSchema: z
89
+ // .string()
90
+ // .describe('The JSON Schema representing the goal state value'),
91
+ contextSchema: z
92
+ .object({
93
+ type: z.literal('object'),
94
+ properties: z.object(
95
+ Object.keys((contextSchema as any).properties).reduce(
96
+ (acc, key) => {
97
+ acc[key] = z.any();
98
+ return acc;
99
+ },
100
+ {} as any
101
+ )
102
+ ),
103
+ required: z.array(z.string()).optional(),
104
+ })
105
+ .describe('The JSON Schema representing the goal state context'),
106
+ }),
107
+ });
108
+
109
+ console.log(result.object);
110
+ const validateContext = ajv.compile(result.object.contextSchema);
111
+
112
+ const resolvedState = input.machine.resolveState({
113
+ ...input.state,
114
+ context: input.state.context ?? {},
115
+ });
116
+
117
+ paths = getShortestPaths(input.machine, {
118
+ fromState: resolvedState,
119
+ toState: (state) => {
120
+ const v = validateContext(state.context);
121
+ return v;
122
+ },
123
+ });
124
+ }
125
+
126
+ if (!paths) {
127
+ return undefined;
128
+ }
129
+
130
+ const trimmedPaths = paths
131
+ .map((path) => {
132
+ const trimmedSteps = trimSteps(path.steps, input.state);
133
+ if (!trimmedSteps) {
134
+ return undefined;
135
+ }
136
+ return {
137
+ ...path,
138
+ steps: trimmedSteps,
139
+ };
140
+ })
141
+ .filter((p): p is NonNullable<typeof p> => p !== undefined);
11
142
 
12
- // 2. Determine possible events that can occur
13
- void 0;
143
+ // Sort paths from least weight to most weight
144
+ const sortedPaths = trimmedPaths.sort(
145
+ (a, b) => costFunction(a) - costFunction(b)
146
+ );
14
147
 
15
- // 3. Get shortest paths from current state to
16
- // a state matching the criteria, using
17
- // possible events
18
- void 0;
148
+ const leastWeightPath = sortedPaths[0];
149
+ const nextStep = leastWeightPath?.steps[0];
19
150
 
20
- // 4. Return shortest path as a plan
21
- return null as any;
151
+ return {
152
+ planner: 'shortestPath',
153
+ episodeId: agent.episodeId,
154
+ goal: input.goal,
155
+ goalState: paths[0]?.state,
156
+ nextEvent: nextStep?.event,
157
+ paths,
158
+ timestamp: Date.now(),
159
+ };
22
160
  }