@dxos/assistant 0.7.4-df37807 → 0.7.5-feature-compute.4d9d99a

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 (96) hide show
  1. package/dist/lib/browser/{chunk-AKP45LQH.mjs → chunk-LVLSJQB3.mjs} +506 -357
  2. package/dist/lib/browser/chunk-LVLSJQB3.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +17 -17
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +10 -3
  6. package/dist/lib/browser/testing/index.mjs.map +4 -4
  7. package/dist/lib/node/{chunk-G6CGWEMO.cjs → chunk-SHOSAIOD.cjs} +507 -362
  8. package/dist/lib/node/chunk-SHOSAIOD.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +42 -42
  10. package/dist/lib/node/index.cjs.map +1 -1
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +18 -12
  13. package/dist/lib/node/testing/index.cjs.map +4 -4
  14. package/dist/lib/node-esm/{chunk-WUPUPEFB.mjs → chunk-ZLBTRSUY.mjs} +506 -357
  15. package/dist/lib/node-esm/chunk-ZLBTRSUY.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +17 -17
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +10 -3
  19. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  20. package/dist/types/src/ai-service/client.d.ts +15 -32
  21. package/dist/types/src/ai-service/client.d.ts.map +1 -1
  22. package/dist/types/src/ai-service/index.d.ts +2 -1
  23. package/dist/types/src/ai-service/index.d.ts.map +1 -1
  24. package/dist/types/src/ai-service/stream.d.ts +34 -0
  25. package/dist/types/src/ai-service/stream.d.ts.map +1 -0
  26. package/dist/types/src/ai-service/stream.test.d.ts +2 -0
  27. package/dist/types/src/ai-service/stream.test.d.ts.map +1 -0
  28. package/dist/types/src/ai-service/types/index.d.ts +3 -0
  29. package/dist/types/src/ai-service/types/index.d.ts.map +1 -0
  30. package/dist/types/src/ai-service/types/message.d.ts +191 -0
  31. package/dist/types/src/ai-service/types/message.d.ts.map +1 -0
  32. package/dist/types/src/ai-service/types/schema.d.ts +175 -0
  33. package/dist/types/src/ai-service/types/schema.d.ts.map +1 -0
  34. package/dist/types/src/ai-service/util/index.d.ts +2 -0
  35. package/dist/types/src/ai-service/util/index.d.ts.map +1 -0
  36. package/dist/types/src/ai-service/{sse.d.ts → util/sse.d.ts} +5 -0
  37. package/dist/types/src/ai-service/util/sse.d.ts.map +1 -0
  38. package/dist/types/src/conversation/conversation.d.ts +54 -4
  39. package/dist/types/src/conversation/conversation.d.ts.map +1 -1
  40. package/dist/types/src/conversation/types.d.ts +35 -4
  41. package/dist/types/src/conversation/types.d.ts.map +1 -1
  42. package/dist/types/src/cypher/combinators.d.ts.map +1 -1
  43. package/dist/types/src/cypher/parjs.d.ts +1 -1
  44. package/dist/types/src/cypher/parjs.d.ts.map +1 -1
  45. package/dist/types/src/cypher/schema.d.ts.map +1 -1
  46. package/dist/types/src/testing/artifacts.test.d.ts +1 -0
  47. package/dist/types/src/testing/artifacts.test.d.ts.map +1 -0
  48. package/dist/types/src/testing/data.d.ts +2 -0
  49. package/dist/types/src/testing/data.d.ts.map +1 -0
  50. package/dist/types/src/testing/defs.d.ts +5 -0
  51. package/dist/types/src/testing/defs.d.ts.map +1 -0
  52. package/dist/types/src/testing/index.d.ts +1 -0
  53. package/dist/types/src/testing/index.d.ts.map +1 -1
  54. package/dist/types/src/testing/test-data.d.ts +1 -1
  55. package/dist/types/src/testing/test-data.d.ts.map +1 -1
  56. package/package.json +20 -18
  57. package/src/ai-service/client.test.ts +25 -26
  58. package/src/ai-service/client.ts +29 -177
  59. package/src/ai-service/index.ts +2 -1
  60. package/src/ai-service/stream.test.ts +186 -0
  61. package/src/ai-service/stream.ts +181 -0
  62. package/src/ai-service/types/index.ts +6 -0
  63. package/src/ai-service/types/message.ts +147 -0
  64. package/src/ai-service/types/schema.ts +128 -0
  65. package/src/ai-service/util/index.ts +5 -0
  66. package/src/ai-service/{sse.ts → util/sse.ts} +5 -0
  67. package/src/bin/chat.ts +6 -9
  68. package/src/conversation/conversation.test.ts +10 -14
  69. package/src/conversation/conversation.ts +109 -14
  70. package/src/conversation/types.ts +47 -6
  71. package/src/cypher/combinators.test.ts +3 -3
  72. package/src/cypher/combinators.ts +0 -1
  73. package/src/cypher/execution-plan.test.ts +5 -3
  74. package/src/cypher/parjs-shims.d.ts +4 -0
  75. package/src/cypher/parjs.ts +2 -2
  76. package/src/cypher/query-executor.test.ts +1 -1
  77. package/src/cypher/schema.ts +0 -1
  78. package/src/index.ts +1 -1
  79. package/src/testing/artifacts.test.ts +182 -0
  80. package/src/testing/data.ts +13 -0
  81. package/src/testing/defs.ts +8 -0
  82. package/src/testing/index.ts +1 -0
  83. package/src/testing/query.test.ts +9 -9
  84. package/src/testing/test-data.ts +1 -1
  85. package/vendor/parjs-browser.mjs +4 -0
  86. package/vendor/parjs-default.mjs +8 -0
  87. package/dist/lib/browser/chunk-AKP45LQH.mjs.map +0 -7
  88. package/dist/lib/node/chunk-G6CGWEMO.cjs.map +0 -7
  89. package/dist/lib/node-esm/chunk-WUPUPEFB.mjs.map +0 -7
  90. package/dist/types/src/ai-service/schema.d.ts +0 -296
  91. package/dist/types/src/ai-service/schema.d.ts.map +0 -1
  92. package/dist/types/src/ai-service/sse.d.ts.map +0 -1
  93. package/dist/types/src/sanity.test.d.ts +0 -2
  94. package/dist/types/src/sanity.test.d.ts.map +0 -1
  95. package/src/ai-service/schema.ts +0 -260
  96. package/src/sanity.test.ts +0 -9
@@ -0,0 +1,147 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Schema as S } from '@effect/schema';
6
+
7
+ import { EchoObject, ObjectId } from '@dxos/echo-schema';
8
+ import { SpaceId } from '@dxos/keys';
9
+
10
+ // TODO(dmaretskyi): Dedupe package with dxos/edge.
11
+ // TODO(dmaretskyi): Extract IDs to protocols.
12
+
13
+ export const SpaceIdSchema: S.Schema<SpaceId, string> = S.String.pipe(S.filter(SpaceId.isValid));
14
+
15
+ export const Space = S.Struct({
16
+ id: SpaceIdSchema,
17
+ });
18
+
19
+ export interface Space extends S.Schema.Type<typeof Space> {}
20
+
21
+ /**
22
+ * Thread.
23
+ */
24
+ export const Thread = S.Struct({
25
+ id: ObjectId,
26
+ spaceId: SpaceIdSchema,
27
+ });
28
+
29
+ export interface Thread extends S.Schema.Type<typeof Thread> {}
30
+
31
+ export const MessageRole = S.String.pipe(S.filter((role) => role === 'user' || role === 'assistant'));
32
+ export type MessageRole = S.Schema.Type<typeof MessageRole>;
33
+
34
+ /**
35
+ * Tagged block.
36
+ */
37
+ export const TagContentBlock = S.Struct({
38
+ type: S.Literal('tag'),
39
+ tag: S.String,
40
+ text: S.String,
41
+ }).pipe(S.mutable);
42
+
43
+ export type TagContentBlock = S.Schema.Type<typeof TagContentBlock>;
44
+
45
+ /**
46
+ * Text.
47
+ */
48
+ export const TextContentBlock = S.Struct({
49
+ type: S.Literal('text'),
50
+ text: S.String,
51
+ }).pipe(S.mutable);
52
+
53
+ export type TextContentBlock = S.Schema.Type<typeof TextContentBlock>;
54
+
55
+ /**
56
+ * Image.
57
+ */
58
+ export const ImageSource = S.Struct({
59
+ type: S.Literal('base64'),
60
+ media_type: S.Literal('image/jpeg', 'image/png', 'image/gif', 'image/webp'),
61
+ data: S.String,
62
+ }).pipe(S.mutable);
63
+
64
+ export type ImageSource = S.Schema.Type<typeof ImageSource>;
65
+
66
+ export const ImageContentBlock = S.Struct({
67
+ type: S.Literal('image'),
68
+ id: S.optional(S.String),
69
+ source: S.optional(ImageSource),
70
+ }).pipe(S.mutable);
71
+
72
+ export type ImageContentBlock = S.Schema.Type<typeof ImageContentBlock>;
73
+
74
+ /**
75
+ * Tool use.
76
+ */
77
+ export const ToolUseContentBlock = S.Struct({
78
+ type: S.Literal('tool_use'),
79
+
80
+ /**
81
+ * Opaque service-defined ID of the tool invocation.
82
+ * Used to match the tool result block.
83
+ */
84
+ id: S.String,
85
+
86
+ /**
87
+ * Tool name.
88
+ */
89
+ name: S.String,
90
+
91
+ input: S.Unknown,
92
+
93
+ /**
94
+ * Used to accumulate the partial tool input JSON in streaming mode.
95
+ */
96
+ // TODO(burdon): Remove?
97
+ inputJson: S.optional(S.String),
98
+ }).pipe(S.mutable);
99
+
100
+ export const ToolResultContentBlock = S.Struct({
101
+ type: S.Literal('tool_result'),
102
+ toolUseId: S.String,
103
+ content: S.String,
104
+ isError: S.optional(S.Boolean),
105
+ });
106
+
107
+ /**
108
+ * Content union.
109
+ */
110
+ export const MessageContentBlock = S.Union(
111
+ TagContentBlock,
112
+ TextContentBlock,
113
+ ImageContentBlock,
114
+ ToolUseContentBlock,
115
+ ToolResultContentBlock,
116
+ );
117
+
118
+ export type MessageContentBlock = S.Schema.Type<typeof MessageContentBlock>;
119
+
120
+ /**
121
+ * Message.
122
+ */
123
+ const MessageSchema = S.Struct({
124
+ id: ObjectId,
125
+ threadId: S.optional(ObjectId),
126
+ spaceId: S.optional(SpaceIdSchema),
127
+
128
+ /**
129
+ * ID of the message from the foreign provider.
130
+ */
131
+ // TODO(dmaretskyi): Should be in meta/keys.
132
+ foreignId: S.optional(S.String),
133
+
134
+ // TODO(dmaretskyi): Figure out how to deal with those.
135
+ // created: S.optional(S.DateFromString),
136
+ // updated: S.optional(S.DateFromString),
137
+
138
+ role: MessageRole,
139
+
140
+ // TODO(burdon): Rename blocks?
141
+ content: S.Array(MessageContentBlock).pipe(S.mutable),
142
+ });
143
+
144
+ // TODO(burdon): Reconcile with Chat/Message types.
145
+ export const Message = MessageSchema.pipe(EchoObject('dxos.org/type/Message', '0.1.0'));
146
+
147
+ export type Message = S.Schema.Type<typeof Message>;
@@ -0,0 +1,128 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Schema as S } from '@effect/schema';
6
+
7
+ import { JsonSchemaType, ObjectId } from '@dxos/echo-schema';
8
+
9
+ import { Message, type MessageContentBlock, SpaceIdSchema } from './message';
10
+
11
+ export const LLMModel = S.Literal(
12
+ '@hf/nousresearch/hermes-2-pro-mistral-7b',
13
+ '@anthropic/claude-3-5-sonnet-20241022',
14
+ '@anthropic/claude-3-5-haiku-20241022',
15
+ '@ollama/llama-3-2-3b',
16
+ '@ollama/llama-3-1-nemotron-70b-instruct',
17
+ '@ollama/llama-3-1-nemotron-mini-4b-instruct',
18
+ '@cf/deepseek-ai/deepseek-r1-distill-qwen-32b',
19
+ );
20
+ export type LLMModel = S.Schema.Type<typeof LLMModel>;
21
+
22
+ export const ToolTypes = Object.freeze({
23
+ // TODO(dmaretskyi): Not implemented yet.
24
+ // DatabaseQuery: 'database_query',
25
+ TextToImage: 'text_to_image',
26
+ });
27
+
28
+ export const LLMTool = S.Struct({
29
+ name: S.String,
30
+
31
+ /**
32
+ * If the tool is implemented by the service a type should be provided.
33
+ * For user-implemented tools, this field should be omitted.
34
+ * See {@link ToolTypes} for the list of supported types.
35
+ */
36
+ type: S.optional(S.String),
37
+
38
+ /**
39
+ * Required for user-implemented tools.
40
+ */
41
+ description: S.optional(S.String),
42
+
43
+ /**
44
+ * Input schema for the tool in the JSON Schema format.
45
+ * Required for user-implemented tools.
46
+ */
47
+ parameters: S.optional(JsonSchemaType),
48
+
49
+ /**
50
+ * Tool-specific options.
51
+ */
52
+ options: S.optional(S.Any),
53
+
54
+ /**
55
+ * Javascript function to execute the tool.
56
+ */
57
+ execute: S.optional(S.Any),
58
+ });
59
+
60
+ export interface LLMTool extends S.Schema.Type<typeof LLMTool> {}
61
+
62
+ export const GenerateRequest = S.Struct({
63
+ model: LLMModel,
64
+
65
+ spaceId: S.optional(SpaceIdSchema),
66
+ threadId: S.optional(ObjectId),
67
+
68
+ /**
69
+ * History of messages to include in the context window.
70
+ */
71
+ history: S.optional(S.Array(Message)),
72
+
73
+ /**
74
+ * System instructions to the LLM.
75
+ */
76
+ // TODO(dmaretskyi): Should this be part of the thread?.
77
+ systemPrompt: S.optional(S.String),
78
+
79
+ /**
80
+ * Tools available for the LLM.
81
+ */
82
+ tools: S.optional(S.Array(LLMTool).pipe(S.mutable)),
83
+ });
84
+
85
+ export type GenerateRequest = S.Schema.Type<typeof GenerateRequest>;
86
+
87
+ export type LLMStopReason = 'tool_use' | 'end_turn';
88
+
89
+ // TODO(dmaretskyi): Effect schema.
90
+ // TODO(dmaretskyi): Rename GenerationStreamEvent.
91
+ export type ResultStreamEvent =
92
+ | {
93
+ // TODO(dmaretskyi): Normalize types to our schema.
94
+ type: 'message_start';
95
+ message: Omit<Message, 'spaceId' | 'threadId'>;
96
+ }
97
+ | {
98
+ type: 'message_delta';
99
+ delta: {
100
+ stopReason: LLMStopReason;
101
+ };
102
+ }
103
+ | {
104
+ type: 'message_stop';
105
+ }
106
+ | {
107
+ type: 'content_block_start';
108
+ index: number;
109
+ content: MessageContentBlock;
110
+ }
111
+ | {
112
+ type: 'content_block_delta';
113
+ index: number;
114
+ delta:
115
+ | {
116
+ type: 'text_delta';
117
+ text: string;
118
+ }
119
+ | {
120
+ partial_json: string;
121
+
122
+ type: 'input_json_delta';
123
+ };
124
+ }
125
+ | {
126
+ type: 'content_block_stop';
127
+ index: number;
128
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './sse';
@@ -3,6 +3,11 @@
3
3
  // Copied from anthropic SDK source
4
4
  //
5
5
 
6
+ /**
7
+ * Server sent events (SSE) iterator.
8
+ * @param response
9
+ * @param controller
10
+ */
6
11
  export async function* iterSSEMessages(
7
12
  response: Response,
8
13
  controller: AbortController,
package/src/bin/chat.ts CHANGED
@@ -2,11 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { writeFileSync } from 'fs';
5
6
  import inquirer from 'inquirer';
6
7
 
8
+ import { ObjectId } from '@dxos/echo-schema';
7
9
  import { SpaceId } from '@dxos/keys';
8
10
 
9
- import { AIServiceClientImpl, ObjectId, ToolTypes } from '../ai-service';
11
+ import { AIServiceClientImpl, ToolTypes } from '../ai-service';
10
12
  import { runLLM, createUserMessage } from '../conversation';
11
13
  import {
12
14
  createLogger,
@@ -17,16 +19,11 @@ import {
17
19
  Org,
18
20
  Project,
19
21
  Task,
22
+ AI_SERVICE_ENDPOINT,
20
23
  } from '../testing';
21
- import { readFileSync, writeFileSync } from 'fs';
22
-
23
- // TODO(burdon): Move out of src?
24
-
25
- const ENDPOINT = 'http://localhost:8787';
26
- // const ENDPOINT = 'https://ai-service.dxos.workers.dev';
27
24
 
28
25
  const client = new AIServiceClientImpl({
29
- endpoint: ENDPOINT,
26
+ endpoint: AI_SERVICE_ENDPOINT.LOCAL,
30
27
  });
31
28
 
32
29
  const dataSource = createTestData();
@@ -46,7 +43,7 @@ while (true) {
46
43
  message: 'Enter a message:',
47
44
  },
48
45
  ]);
49
- await client.insertMessages([createUserMessage(spaceId, threadId, prompt.message)]);
46
+ await client.appendMessages([createUserMessage(spaceId, threadId, prompt.message)]);
50
47
 
51
48
  await runLLM({
52
49
  model: '@anthropic/claude-3-5-sonnet-20241022',
@@ -5,37 +5,35 @@
5
5
  import { Schema as S } from '@effect/schema';
6
6
  import { describe, test } from 'vitest';
7
7
 
8
+ import { ObjectId } from '@dxos/echo-schema';
8
9
  import { SpaceId } from '@dxos/keys';
9
10
  import { log } from '@dxos/log';
10
11
 
11
12
  import { runLLM, type ConversationEvent } from './conversation';
12
13
  import { createUserMessage, defineTool, LLMToolResult } from './types';
13
- import { AIServiceClientImpl, ObjectId } from '../ai-service';
14
-
15
- const ENDPOINT = 'http://localhost:8787';
14
+ import { AIServiceClientImpl } from '../ai-service';
15
+ import { AI_SERVICE_ENDPOINT } from '../testing';
16
16
 
17
17
  describe.skip('Conversation tests', () => {
18
18
  const client = new AIServiceClientImpl({
19
- endpoint: ENDPOINT,
19
+ endpoint: AI_SERVICE_ENDPOINT.LOCAL,
20
20
  });
21
21
 
22
22
  test('hello', async ({ expect }) => {
23
23
  const spaceId = SpaceId.random();
24
24
  const threadId = ObjectId.random();
25
25
 
26
- await client.insertMessages([createUserMessage(spaceId, threadId, 'Hello, how are you?')]);
27
26
  const result = await runLLM({
28
27
  model: '@anthropic/claude-3-5-sonnet-20241022',
29
- spaceId,
30
- threadId,
28
+ history: [createUserMessage(spaceId, threadId, 'Hello, how are you?')],
31
29
  tools: [],
32
30
  client,
33
31
  logger: messageLogger,
34
32
  });
35
- log.info('result', { result });
33
+ log('result', { result });
36
34
  });
37
35
 
38
- test('tool call', async ({ expect }) => {
36
+ test.only('tool call', async ({ expect }) => {
39
37
  const custodian = defineTool({
40
38
  name: 'custodian',
41
39
  description: 'Custodian can tell you the password if you say the magic word',
@@ -54,21 +52,19 @@ describe.skip('Conversation tests', () => {
54
52
  const spaceId = SpaceId.random();
55
53
  const threadId = ObjectId.random();
56
54
 
57
- await client.insertMessages([createUserMessage(spaceId, threadId, 'What is the password? Ask the custodian.')]);
58
55
  const result = await runLLM({
59
56
  model: '@anthropic/claude-3-5-sonnet-20241022',
60
- spaceId,
61
- threadId,
57
+ history: [createUserMessage(spaceId, threadId, 'What is the password? Ask the custodian.')],
62
58
  tools: [custodian],
63
59
  client,
64
60
  logger: messageLogger,
65
61
  });
66
- log.info('result', { result });
62
+ log('result', { result });
67
63
  });
68
64
 
69
65
  const messageLogger = (event: ConversationEvent) => {
70
66
  if (event.type === 'message') {
71
- log.info('message', { message: event.message });
67
+ log('message', { message: event.message });
72
68
  }
73
69
  };
74
70
  });
@@ -2,13 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { createStatic, ObjectId } from '@dxos/echo-schema';
5
6
  import { invariant } from '@dxos/invariant';
6
7
  import { type SpaceId } from '@dxos/keys';
7
8
  import { log } from '@dxos/log';
8
9
 
9
- import { type LLMToolDefinition } from './types';
10
- import { ObjectId, type LLMTool } from '../ai-service';
11
- import { type AIServiceClient, type Message, type ResultStreamEvent, type LLMModel } from '../ai-service';
10
+ import { LLMToolResult } from './types';
11
+ import { type LLMTool, Message, type AIServiceClient, type ResultStreamEvent, type LLMModel } from '../ai-service';
12
12
 
13
13
  export type CreateLLMConversationParams = {
14
14
  model: LLMModel;
@@ -18,11 +18,13 @@ export type CreateLLMConversationParams = {
18
18
  */
19
19
  system?: string;
20
20
 
21
- spaceId: SpaceId;
22
- threadId: ObjectId;
21
+ spaceId?: SpaceId;
22
+ threadId?: ObjectId;
23
23
 
24
24
  tools: LLMTool[];
25
25
 
26
+ history?: Message[];
27
+
26
28
  client: AIServiceClient;
27
29
 
28
30
  logger?: (event: ConversationEvent) => void;
@@ -37,29 +39,33 @@ export type ConversationEvent =
37
39
 
38
40
  export const runLLM = async (params: CreateLLMConversationParams) => {
39
41
  let conversationResult: any = null;
42
+ const history = params.history ?? [];
40
43
 
41
44
  const generate = async () => {
42
45
  log('llm generate', { tools: params.tools });
43
46
  const beginTs = Date.now();
44
- const result = await params.client.generate({
47
+ const stream = await params.client.generate({
45
48
  model: params.model,
46
49
  spaceId: params.spaceId,
47
50
  threadId: params.threadId,
51
+ history,
48
52
  systemPrompt: params.system,
49
53
  tools: params.tools as any,
50
54
  });
51
55
 
52
- for await (const event of result) {
56
+ for await (const event of stream) {
53
57
  params.logger?.(event);
54
58
  }
55
- const messages = await result.complete();
59
+ const messages = await stream.complete();
56
60
  const message = messages.at(-1);
57
61
 
58
62
  log('llm result', { time: Date.now() - beginTs, message });
59
63
  invariant(message);
60
64
  params.logger?.({ type: 'message', message });
61
- await params.client.insertMessages(
62
- messages.map((msg) => ({ ...msg, content: msg.content.filter((c) => c.type !== 'image') })),
65
+ history.push(
66
+ ...messages.map(
67
+ (message): Message => ({ ...message, content: message.content.filter((block) => block.type !== 'image') }),
68
+ ),
63
69
  );
64
70
 
65
71
  const isToolUse = message.content.at(-1)?.type === 'tool_use';
@@ -67,7 +73,7 @@ export const runLLM = async (params: CreateLLMConversationParams) => {
67
73
  const toolCalls = message.content.filter((c) => c.type === 'tool_use');
68
74
  invariant(toolCalls.length === 1);
69
75
  const toolCall = toolCalls[0];
70
- const tool = params.tools.find((t) => t.name === toolCall.name);
76
+ const tool = params.tools.find((tool) => tool.name === toolCall.name);
71
77
  if (!tool) {
72
78
  throw new Error(`Tool not found: ${toolCall.name}`);
73
79
  }
@@ -75,7 +81,7 @@ export const runLLM = async (params: CreateLLMConversationParams) => {
75
81
  const toolResult = await tool.execute(toolCall.input, {});
76
82
  switch (toolResult.kind) {
77
83
  case 'error': {
78
- log('tool error', { message: toolResult.message });
84
+ log.warn('tool error', { message: toolResult.message });
79
85
  const resultMessage: Message = {
80
86
  id: ObjectId.random(),
81
87
  spaceId: params.spaceId,
@@ -91,10 +97,11 @@ export const runLLM = async (params: CreateLLMConversationParams) => {
91
97
  ],
92
98
  };
93
99
  params.logger?.({ type: 'message', message: resultMessage });
94
- await params.client.insertMessages([resultMessage]);
100
+ history.push(resultMessage);
95
101
 
96
102
  return true;
97
103
  }
104
+
98
105
  case 'success': {
99
106
  log('tool success', { result: toolResult.result });
100
107
  const resultMessage: Message = {
@@ -111,10 +118,11 @@ export const runLLM = async (params: CreateLLMConversationParams) => {
111
118
  ],
112
119
  };
113
120
  params.logger?.({ type: 'message', message: resultMessage });
114
- await params.client.insertMessages([resultMessage]);
121
+ history.push(resultMessage);
115
122
 
116
123
  return true;
117
124
  }
125
+
118
126
  case 'break': {
119
127
  log('tool break', { result: toolResult.result });
120
128
  conversationResult = toolResult.result;
@@ -130,5 +138,92 @@ export const runLLM = async (params: CreateLLMConversationParams) => {
130
138
 
131
139
  return {
132
140
  result: conversationResult,
141
+ history,
133
142
  };
134
143
  };
144
+
145
+ export const isToolUse = (message: Message) => message.content.at(-1)?.type === 'tool_use';
146
+
147
+ type RunToolsOptions = {
148
+ message: Message;
149
+ tools: LLMTool[];
150
+ extensions?: LLMToolContextExtensions;
151
+ };
152
+
153
+ export type RunToolsResult =
154
+ | {
155
+ type: 'continue';
156
+ message: Message;
157
+ }
158
+ | {
159
+ type: 'break';
160
+ result: unknown;
161
+ };
162
+
163
+ export const runTools = async ({ message, tools, extensions }: RunToolsOptions): Promise<RunToolsResult> => {
164
+ const toolCalls = message.content.filter((block) => block.type === 'tool_use');
165
+ invariant(toolCalls.length === 1);
166
+ const toolCall = toolCalls[0];
167
+ const tool = tools.find((tool) => tool.name === toolCall.name);
168
+ if (!tool) {
169
+ throw new Error(`Tool not found: ${toolCall.name}`);
170
+ }
171
+
172
+ let toolResult: LLMToolResult;
173
+ try {
174
+ toolResult = await tool.execute(toolCall.input, { extensions });
175
+ } catch (error: any) {
176
+ log('tool error', { error });
177
+ toolResult = LLMToolResult.Error(error.message);
178
+ }
179
+
180
+ switch (toolResult.kind) {
181
+ case 'error': {
182
+ log('tool error', { message: toolResult.message });
183
+ const resultMessage = createStatic(Message, {
184
+ role: 'user',
185
+ content: [
186
+ {
187
+ type: 'tool_result',
188
+ toolUseId: toolCall.id,
189
+ content: toolResult.message,
190
+ isError: true,
191
+ },
192
+ ],
193
+ });
194
+
195
+ // TODO(burdon): Retry count?
196
+ return {
197
+ type: 'continue',
198
+ message: resultMessage,
199
+ };
200
+ }
201
+
202
+ case 'success': {
203
+ log('tool success', { result: toolResult.result });
204
+ const resultMessage = createStatic(Message, {
205
+ role: 'user',
206
+ content: [
207
+ {
208
+ type: 'tool_result',
209
+ toolUseId: toolCall.id,
210
+ content: typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result),
211
+ },
212
+ ],
213
+ });
214
+
215
+ return {
216
+ type: 'continue',
217
+ message: resultMessage,
218
+ };
219
+ }
220
+
221
+ case 'break': {
222
+ log('tool break', { result: toolResult.result });
223
+ return {
224
+ type: 'break',
225
+ result: toolResult.result,
226
+ };
227
+ }
228
+ }
229
+ };