@looopy-ai/core 1.0.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.
- package/LICENSE +9 -0
- package/dist/core/agent.d.ts +53 -0
- package/dist/core/agent.js +416 -0
- package/dist/core/cleanup.d.ts +12 -0
- package/dist/core/cleanup.js +45 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/iteration.d.ts +5 -0
- package/dist/core/iteration.js +60 -0
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +31 -0
- package/dist/core/loop.d.ts +5 -0
- package/dist/core/loop.js +125 -0
- package/dist/core/tools.d.ts +4 -0
- package/dist/core/tools.js +78 -0
- package/dist/core/types.d.ts +30 -0
- package/dist/core/types.js +1 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +2 -0
- package/dist/events/utils.d.ts +250 -0
- package/dist/events/utils.js +263 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/spans/agent-turn.d.ts +31 -0
- package/dist/observability/spans/agent-turn.js +94 -0
- package/dist/observability/spans/index.d.ts +5 -0
- package/dist/observability/spans/index.js +5 -0
- package/dist/observability/spans/iteration.d.ts +14 -0
- package/dist/observability/spans/iteration.js +41 -0
- package/dist/observability/spans/llm-call.d.ts +14 -0
- package/dist/observability/spans/llm-call.js +50 -0
- package/dist/observability/spans/loop.d.ts +14 -0
- package/dist/observability/spans/loop.js +40 -0
- package/dist/observability/spans/tool.d.ts +14 -0
- package/dist/observability/spans/tool.js +44 -0
- package/dist/observability/tracing.d.ts +58 -0
- package/dist/observability/tracing.js +203 -0
- package/dist/providers/chat-completions/aggregate.d.ts +4 -0
- package/dist/providers/chat-completions/aggregate.js +152 -0
- package/dist/providers/chat-completions/content.d.ts +25 -0
- package/dist/providers/chat-completions/content.js +229 -0
- package/dist/providers/chat-completions/index.d.ts +4 -0
- package/dist/providers/chat-completions/index.js +4 -0
- package/dist/providers/chat-completions/streaming.d.ts +12 -0
- package/dist/providers/chat-completions/streaming.js +3 -0
- package/dist/providers/chat-completions/types.d.ts +39 -0
- package/dist/providers/chat-completions/types.js +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/litellm-provider.d.ts +43 -0
- package/dist/providers/litellm-provider.js +377 -0
- package/dist/server/event-buffer.d.ts +37 -0
- package/dist/server/event-buffer.js +116 -0
- package/dist/server/event-router.d.ts +31 -0
- package/dist/server/event-router.js +91 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +3 -0
- package/dist/server/sse.d.ts +60 -0
- package/dist/server/sse.js +159 -0
- package/dist/stores/artifacts/artifact-scheduler.d.ts +50 -0
- package/dist/stores/artifacts/artifact-scheduler.js +86 -0
- package/dist/stores/artifacts/index.d.ts +3 -0
- package/dist/stores/artifacts/index.js +3 -0
- package/dist/stores/artifacts/internal-event-artifact-store.d.ts +54 -0
- package/dist/stores/artifacts/internal-event-artifact-store.js +126 -0
- package/dist/stores/artifacts/memory-artifact-store.d.ts +52 -0
- package/dist/stores/artifacts/memory-artifact-store.js +268 -0
- package/dist/stores/filesystem/filesystem-agent-store.d.ts +18 -0
- package/dist/stores/filesystem/filesystem-agent-store.js +61 -0
- package/dist/stores/filesystem/filesystem-artifact-store.d.ts +59 -0
- package/dist/stores/filesystem/filesystem-artifact-store.js +325 -0
- package/dist/stores/filesystem/filesystem-context-store.d.ts +37 -0
- package/dist/stores/filesystem/filesystem-context-store.js +245 -0
- package/dist/stores/filesystem/filesystem-message-store.d.ts +28 -0
- package/dist/stores/filesystem/filesystem-message-store.js +149 -0
- package/dist/stores/filesystem/filesystem-task-state-store.d.ts +27 -0
- package/dist/stores/filesystem/filesystem-task-state-store.js +220 -0
- package/dist/stores/filesystem/index.d.ts +10 -0
- package/dist/stores/filesystem/index.js +5 -0
- package/dist/stores/index.d.ts +5 -0
- package/dist/stores/index.js +5 -0
- package/dist/stores/memory/memory-state-store.d.ts +15 -0
- package/dist/stores/memory/memory-state-store.js +55 -0
- package/dist/stores/messages/hybrid-message-store.d.ts +29 -0
- package/dist/stores/messages/hybrid-message-store.js +72 -0
- package/dist/stores/messages/index.d.ts +4 -0
- package/dist/stores/messages/index.js +4 -0
- package/dist/stores/messages/interfaces.d.ts +42 -0
- package/dist/stores/messages/interfaces.js +18 -0
- package/dist/stores/messages/mem0-message-store.d.ts +34 -0
- package/dist/stores/messages/mem0-message-store.js +218 -0
- package/dist/stores/messages/memory-message-store.d.ts +27 -0
- package/dist/stores/messages/memory-message-store.js +183 -0
- package/dist/tools/artifact-tools.d.ts +4 -0
- package/dist/tools/artifact-tools.js +277 -0
- package/dist/tools/client-tool-provider.d.ts +25 -0
- package/dist/tools/client-tool-provider.js +139 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/local-tools.d.ts +13 -0
- package/dist/tools/local-tools.js +70 -0
- package/dist/tools/mcp-client.d.ts +29 -0
- package/dist/tools/mcp-client.js +62 -0
- package/dist/tools/mcp-tool-provider.d.ts +22 -0
- package/dist/tools/mcp-tool-provider.js +86 -0
- package/dist/types/a2a.d.ts +36 -0
- package/dist/types/a2a.js +1 -0
- package/dist/types/agent.d.ts +14 -0
- package/dist/types/agent.js +1 -0
- package/dist/types/artifact.d.ts +126 -0
- package/dist/types/artifact.js +1 -0
- package/dist/types/context.d.ts +13 -0
- package/dist/types/context.js +1 -0
- package/dist/types/event.d.ts +360 -0
- package/dist/types/event.js +30 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.js +9 -0
- package/dist/types/llm.d.ts +24 -0
- package/dist/types/llm.js +1 -0
- package/dist/types/message.d.ts +9 -0
- package/dist/types/message.js +1 -0
- package/dist/types/state.d.ts +86 -0
- package/dist/types/state.js +1 -0
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/tools.js +53 -0
- package/dist/utils/error.d.ts +8 -0
- package/dist/utils/error.js +23 -0
- package/dist/utils/process-signals.d.ts +3 -0
- package/dist/utils/process-signals.js +67 -0
- package/package.json +54 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ArtifactScheduler } from '../stores';
|
|
3
|
+
import { localTools, tool } from './local-tools';
|
|
4
|
+
async function trackArtifactInState(taskId, artifactId, taskStateStore) {
|
|
5
|
+
const state = await taskStateStore.load(taskId);
|
|
6
|
+
if (!state)
|
|
7
|
+
return;
|
|
8
|
+
if (!state.artifactIds.includes(artifactId)) {
|
|
9
|
+
state.artifactIds.push(artifactId);
|
|
10
|
+
await taskStateStore.save(taskId, state);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function createArtifactTools(artifactStore, taskStateStore) {
|
|
14
|
+
const scheduledStore = new ArtifactScheduler(artifactStore);
|
|
15
|
+
return localTools([
|
|
16
|
+
tool('create_file_artifact', 'Create a new file artifact for streaming text or binary content. Use append_file_chunk to add content. Set override=true to replace existing artifact.', z.object({
|
|
17
|
+
artifactId: z
|
|
18
|
+
.string()
|
|
19
|
+
.describe('Unique identifier for the artifact (e.g., "report-2025", "analysis-results")'),
|
|
20
|
+
name: z.string().optional().describe('Human-readable name for the artifact'),
|
|
21
|
+
description: z.string().optional().describe('Description of the artifact content'),
|
|
22
|
+
mimeType: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.default('text/plain')
|
|
26
|
+
.describe('MIME type of the content (e.g., "text/plain", "text/markdown")'),
|
|
27
|
+
encoding: z
|
|
28
|
+
.enum(['utf-8', 'base64'])
|
|
29
|
+
.optional()
|
|
30
|
+
.default('utf-8')
|
|
31
|
+
.describe('Content encoding'),
|
|
32
|
+
override: z
|
|
33
|
+
.boolean()
|
|
34
|
+
.optional()
|
|
35
|
+
.default(false)
|
|
36
|
+
.describe('Set to true to replace an existing artifact with the same ID'),
|
|
37
|
+
}), async (params, context) => {
|
|
38
|
+
await scheduledStore.createFileArtifact({
|
|
39
|
+
artifactId: params.artifactId,
|
|
40
|
+
taskId: context.taskId,
|
|
41
|
+
contextId: context.contextId,
|
|
42
|
+
name: params.name,
|
|
43
|
+
description: params.description,
|
|
44
|
+
mimeType: params.mimeType,
|
|
45
|
+
encoding: params.encoding,
|
|
46
|
+
override: params.override,
|
|
47
|
+
});
|
|
48
|
+
await trackArtifactInState(context.taskId, params.artifactId, taskStateStore);
|
|
49
|
+
return {
|
|
50
|
+
artifactId: params.artifactId,
|
|
51
|
+
type: 'file',
|
|
52
|
+
status: 'building',
|
|
53
|
+
message: params.override
|
|
54
|
+
? 'File artifact reset. Use append_file_chunk to add content.'
|
|
55
|
+
: 'File artifact created. Use append_file_chunk to add content.',
|
|
56
|
+
};
|
|
57
|
+
}),
|
|
58
|
+
tool('append_file_chunk', 'Append a chunk of content to a file artifact. Call multiple times to stream content.', z.object({
|
|
59
|
+
artifactId: z.string().describe('The artifact ID to append to'),
|
|
60
|
+
content_chunk: z.string().describe('Content chunk to append to the file'),
|
|
61
|
+
isLastChunk: z
|
|
62
|
+
.boolean()
|
|
63
|
+
.optional()
|
|
64
|
+
.default(false)
|
|
65
|
+
.describe('Set to true on the final chunk to mark artifact as complete'),
|
|
66
|
+
}), async (params, context) => {
|
|
67
|
+
await scheduledStore.appendFileChunk(context.contextId, params.artifactId, params.content_chunk, {
|
|
68
|
+
isLastChunk: params.isLastChunk,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
artifactId: params.artifactId,
|
|
72
|
+
chunkAdded: true,
|
|
73
|
+
complete: params.isLastChunk,
|
|
74
|
+
message: params.isLastChunk
|
|
75
|
+
? 'Final chunk appended. Artifact is complete.'
|
|
76
|
+
: 'Chunk appended successfully.',
|
|
77
|
+
};
|
|
78
|
+
}),
|
|
79
|
+
tool('get_file_content', 'Get the complete content of a file artifact', z.object({
|
|
80
|
+
artifactId: z.string().describe('The artifact ID to retrieve'),
|
|
81
|
+
}), async (params, context) => {
|
|
82
|
+
const content = await scheduledStore.getFileContent(context.contextId, params.artifactId);
|
|
83
|
+
return {
|
|
84
|
+
artifactId: params.artifactId,
|
|
85
|
+
content,
|
|
86
|
+
};
|
|
87
|
+
}),
|
|
88
|
+
tool('create_data_artifact', 'Create a data artifact with structured JSON data. Set override=true to replace existing artifact.', z.object({
|
|
89
|
+
artifactId: z.string().describe('Unique identifier for the artifact'),
|
|
90
|
+
name: z.string().optional().describe('Human-readable name'),
|
|
91
|
+
description: z.string().optional().describe('Description of the data'),
|
|
92
|
+
data: z.record(z.string(), z.unknown()).describe('The structured data object'),
|
|
93
|
+
override: z
|
|
94
|
+
.boolean()
|
|
95
|
+
.optional()
|
|
96
|
+
.default(false)
|
|
97
|
+
.describe('Set to true to replace an existing artifact with the same ID'),
|
|
98
|
+
}), async (params, context) => {
|
|
99
|
+
await scheduledStore.createDataArtifact({
|
|
100
|
+
artifactId: params.artifactId,
|
|
101
|
+
taskId: context.taskId,
|
|
102
|
+
contextId: context.contextId,
|
|
103
|
+
name: params.name,
|
|
104
|
+
description: params.description,
|
|
105
|
+
override: params.override,
|
|
106
|
+
});
|
|
107
|
+
await scheduledStore.writeData(context.contextId, params.artifactId, params.data);
|
|
108
|
+
await trackArtifactInState(context.taskId, params.artifactId, taskStateStore);
|
|
109
|
+
return {
|
|
110
|
+
artifactId: params.artifactId,
|
|
111
|
+
type: 'data',
|
|
112
|
+
status: 'complete',
|
|
113
|
+
message: params.override
|
|
114
|
+
? 'Data artifact reset successfully.'
|
|
115
|
+
: 'Data artifact created successfully.',
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
tool('update_data_artifact', 'Update the data content of an existing data artifact', z.object({
|
|
119
|
+
artifactId: z.string().describe('The artifact ID to update'),
|
|
120
|
+
data: z.record(z.string(), z.unknown()).describe('The new data object'),
|
|
121
|
+
}), async (params, context) => {
|
|
122
|
+
await scheduledStore.writeData(context.contextId, params.artifactId, params.data);
|
|
123
|
+
return {
|
|
124
|
+
artifactId: params.artifactId,
|
|
125
|
+
type: 'data',
|
|
126
|
+
status: 'complete',
|
|
127
|
+
message: 'Data artifact updated successfully.',
|
|
128
|
+
};
|
|
129
|
+
}),
|
|
130
|
+
tool('get_data_content', 'Get the content of a data artifact', z.object({
|
|
131
|
+
artifactId: z.string().describe('The artifact ID to retrieve'),
|
|
132
|
+
}), async (params, context) => {
|
|
133
|
+
const data = await scheduledStore.getDataContent(context.contextId, params.artifactId);
|
|
134
|
+
return {
|
|
135
|
+
artifactId: params.artifactId,
|
|
136
|
+
data,
|
|
137
|
+
};
|
|
138
|
+
}),
|
|
139
|
+
tool('get_data_artifact', 'Get the data content of a data artifact', z.object({
|
|
140
|
+
artifactId: z.string().describe('The artifact ID to retrieve'),
|
|
141
|
+
}), async (params, context) => {
|
|
142
|
+
const data = await scheduledStore.getDataContent(context.contextId, params.artifactId);
|
|
143
|
+
return {
|
|
144
|
+
artifactId: params.artifactId,
|
|
145
|
+
data,
|
|
146
|
+
};
|
|
147
|
+
}),
|
|
148
|
+
tool('create_dataset_artifact', 'Create a dataset artifact for tabular data with a schema. Set override=true to replace existing artifact.', z.object({
|
|
149
|
+
artifactId: z.string().describe('Unique identifier for the dataset'),
|
|
150
|
+
name: z.string().optional().describe('Human-readable name'),
|
|
151
|
+
description: z.string().optional().describe('Description of the dataset'),
|
|
152
|
+
schema: z.object({
|
|
153
|
+
columns: z.array(z.object({
|
|
154
|
+
name: z.string(),
|
|
155
|
+
type: z.enum(['string', 'number', 'boolean', 'date', 'json']),
|
|
156
|
+
description: z.string().optional(),
|
|
157
|
+
})),
|
|
158
|
+
}),
|
|
159
|
+
override: z
|
|
160
|
+
.boolean()
|
|
161
|
+
.optional()
|
|
162
|
+
.default(false)
|
|
163
|
+
.describe('Set to true to replace an existing artifact with the same ID'),
|
|
164
|
+
}), async (params, context) => {
|
|
165
|
+
await scheduledStore.createDatasetArtifact({
|
|
166
|
+
artifactId: params.artifactId,
|
|
167
|
+
taskId: context.taskId,
|
|
168
|
+
contextId: context.contextId,
|
|
169
|
+
name: params.name,
|
|
170
|
+
description: params.description,
|
|
171
|
+
schema: params.schema,
|
|
172
|
+
override: params.override,
|
|
173
|
+
});
|
|
174
|
+
await trackArtifactInState(context.taskId, params.artifactId, taskStateStore);
|
|
175
|
+
return {
|
|
176
|
+
artifactId: params.artifactId,
|
|
177
|
+
type: 'dataset',
|
|
178
|
+
status: 'building',
|
|
179
|
+
message: params.override
|
|
180
|
+
? 'Dataset artifact reset. Use append_dataset_row(s) to add data.'
|
|
181
|
+
: 'Dataset artifact created. Use append_dataset_row(s) to add data.',
|
|
182
|
+
};
|
|
183
|
+
}),
|
|
184
|
+
tool('append_dataset_row', 'Append a single row to a dataset artifact', z.object({
|
|
185
|
+
artifactId: z.string().describe('The dataset artifact ID'),
|
|
186
|
+
row: z.record(z.string(), z.unknown()).describe('Row data matching the dataset schema'),
|
|
187
|
+
}), async (params, context) => {
|
|
188
|
+
await scheduledStore.appendDatasetBatch(context.contextId, params.artifactId, [params.row]);
|
|
189
|
+
return {
|
|
190
|
+
artifactId: params.artifactId,
|
|
191
|
+
rowAdded: true,
|
|
192
|
+
message: 'Row appended to dataset.',
|
|
193
|
+
};
|
|
194
|
+
}),
|
|
195
|
+
tool('append_dataset_rows', 'Append multiple rows to a dataset artifact', z.object({
|
|
196
|
+
artifactId: z.string().describe('The dataset artifact ID'),
|
|
197
|
+
rows: z.array(z.record(z.string(), z.unknown())).describe('Array of rows to append'),
|
|
198
|
+
isLastBatch: z.boolean().optional().describe('Set to true on the final batch'),
|
|
199
|
+
}), async (params, context) => {
|
|
200
|
+
await scheduledStore.appendDatasetBatch(context.contextId, params.artifactId, params.rows, {
|
|
201
|
+
isLastBatch: params.isLastBatch,
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
artifactId: params.artifactId,
|
|
205
|
+
rowsAdded: params.rows.length,
|
|
206
|
+
message: `${params.rows.length} rows appended to dataset.`,
|
|
207
|
+
};
|
|
208
|
+
}),
|
|
209
|
+
tool('get_dataset_rows', 'Get all rows from a dataset artifact', z.object({
|
|
210
|
+
artifactId: z.string().describe('The dataset artifact ID'),
|
|
211
|
+
}), async (params, context) => {
|
|
212
|
+
const rows = await scheduledStore.getDatasetRows(context.contextId, params.artifactId);
|
|
213
|
+
return {
|
|
214
|
+
artifactId: params.artifactId,
|
|
215
|
+
rows,
|
|
216
|
+
totalRows: rows.length,
|
|
217
|
+
};
|
|
218
|
+
}),
|
|
219
|
+
tool('list_artifacts', 'List all artifacts in the current context, optionally filtered by task', z.object({
|
|
220
|
+
taskId: z.string().optional().describe('Filter artifacts by task ID'),
|
|
221
|
+
}), async (params, context) => {
|
|
222
|
+
const artifactIds = await scheduledStore.listArtifacts(context.contextId, params.taskId);
|
|
223
|
+
const artifacts = await Promise.all(artifactIds.map((id) => scheduledStore.getArtifact(context.contextId, id)));
|
|
224
|
+
const validArtifacts = artifacts.filter((a) => a !== null);
|
|
225
|
+
return {
|
|
226
|
+
artifacts: validArtifacts.map((a) => ({
|
|
227
|
+
artifactId: a.artifactId,
|
|
228
|
+
type: a.type,
|
|
229
|
+
name: a.name,
|
|
230
|
+
taskId: a.taskId,
|
|
231
|
+
contextId: a.contextId,
|
|
232
|
+
createdAt: a.createdAt,
|
|
233
|
+
})),
|
|
234
|
+
totalCount: validArtifacts.length,
|
|
235
|
+
};
|
|
236
|
+
}),
|
|
237
|
+
tool('get_artifact', 'Get metadata for a specific artifact by ID', z.object({
|
|
238
|
+
artifactId: z.string().describe('The artifact ID to retrieve'),
|
|
239
|
+
}), async (params, context) => {
|
|
240
|
+
const artifact = await scheduledStore.getArtifact(context.contextId, params.artifactId);
|
|
241
|
+
if (!artifact) {
|
|
242
|
+
throw new Error(`Artifact not found: ${params.artifactId}`);
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
artifactId: artifact.artifactId,
|
|
246
|
+
type: artifact.type,
|
|
247
|
+
taskId: artifact.taskId,
|
|
248
|
+
contextId: artifact.contextId,
|
|
249
|
+
name: artifact.name,
|
|
250
|
+
description: artifact.description,
|
|
251
|
+
status: artifact.status,
|
|
252
|
+
createdAt: artifact.createdAt,
|
|
253
|
+
updatedAt: artifact.updatedAt,
|
|
254
|
+
...(artifact.type === 'file' && {
|
|
255
|
+
mimeType: artifact.mimeType,
|
|
256
|
+
encoding: artifact.encoding,
|
|
257
|
+
totalChunks: artifact.totalChunks,
|
|
258
|
+
totalSize: artifact.totalSize,
|
|
259
|
+
}),
|
|
260
|
+
...(artifact.type === 'dataset' && {
|
|
261
|
+
totalRows: artifact.totalSize,
|
|
262
|
+
schema: artifact.schema,
|
|
263
|
+
}),
|
|
264
|
+
};
|
|
265
|
+
}),
|
|
266
|
+
tool('delete_artifact', 'Delete an artifact by ID', z.object({
|
|
267
|
+
artifactId: z.string().describe('The artifact ID to delete'),
|
|
268
|
+
}), async (params, context) => {
|
|
269
|
+
await scheduledStore.deleteArtifact(context.contextId, params.artifactId);
|
|
270
|
+
return {
|
|
271
|
+
artifactId: params.artifactId,
|
|
272
|
+
deleted: true,
|
|
273
|
+
message: 'Artifact deleted successfully.',
|
|
274
|
+
};
|
|
275
|
+
}),
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExecutionContext } from '../types/context';
|
|
2
|
+
import { type ToolCall, type ToolDefinition, type ToolProvider, type ToolResult } from '../types/tools';
|
|
3
|
+
export interface ClientToolConfig {
|
|
4
|
+
tools: unknown;
|
|
5
|
+
onInputRequired: (toolCall: ToolCall, context: ExecutionContext) => Promise<ToolResult>;
|
|
6
|
+
}
|
|
7
|
+
export declare class ClientToolProvider implements ToolProvider {
|
|
8
|
+
private readonly tools;
|
|
9
|
+
private readonly toolNames;
|
|
10
|
+
private readonly onInputRequired;
|
|
11
|
+
constructor(config: ClientToolConfig);
|
|
12
|
+
getTools(): Promise<ToolDefinition[]>;
|
|
13
|
+
canHandle(toolName: string): boolean;
|
|
14
|
+
execute(toolCall: ToolCall, context: ExecutionContext): Promise<ToolResult>;
|
|
15
|
+
getTool(name: string): ToolDefinition | undefined;
|
|
16
|
+
validateToolArguments(toolCall: ToolCall): {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
errors?: string[];
|
|
19
|
+
};
|
|
20
|
+
private validateRequiredParams;
|
|
21
|
+
private validateUnknownParams;
|
|
22
|
+
private validateParamTypes;
|
|
23
|
+
private checkParamType;
|
|
24
|
+
private getJsonSchemaType;
|
|
25
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { validateToolDefinitions, } from '../types/tools';
|
|
2
|
+
export class ClientToolProvider {
|
|
3
|
+
tools;
|
|
4
|
+
toolNames;
|
|
5
|
+
onInputRequired;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
try {
|
|
8
|
+
this.tools = validateToolDefinitions(config.tools);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error instanceof Error) {
|
|
12
|
+
throw new Error(`Invalid client tool definitions: ${error.message}`);
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
this.toolNames = new Set(this.tools.map((t) => t.name));
|
|
17
|
+
if (this.toolNames.size !== this.tools.length) {
|
|
18
|
+
const names = this.tools.map((t) => t.name);
|
|
19
|
+
const duplicates = names.filter((name, index) => names.indexOf(name) !== index);
|
|
20
|
+
throw new Error(`Duplicate tool names: ${duplicates.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
this.onInputRequired = config.onInputRequired;
|
|
23
|
+
}
|
|
24
|
+
async getTools() {
|
|
25
|
+
return [...this.tools];
|
|
26
|
+
}
|
|
27
|
+
canHandle(toolName) {
|
|
28
|
+
return this.toolNames.has(toolName);
|
|
29
|
+
}
|
|
30
|
+
async execute(toolCall, context) {
|
|
31
|
+
if (!this.canHandle(toolCall.function.name)) {
|
|
32
|
+
return {
|
|
33
|
+
toolCallId: toolCall.id,
|
|
34
|
+
toolName: toolCall.function.name,
|
|
35
|
+
success: false,
|
|
36
|
+
result: null,
|
|
37
|
+
error: `Tool ${toolCall.function.name} not found in client tools`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
if (typeof toolCall.function.arguments !== 'object' || toolCall.function.arguments === null) {
|
|
42
|
+
return {
|
|
43
|
+
toolCallId: toolCall.id,
|
|
44
|
+
toolName: toolCall.function.name,
|
|
45
|
+
success: false,
|
|
46
|
+
result: null,
|
|
47
|
+
error: `Invalid tool arguments: must be an object.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const result = await this.onInputRequired(toolCall, context);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return {
|
|
55
|
+
toolCallId: toolCall.id,
|
|
56
|
+
toolName: toolCall.function.name,
|
|
57
|
+
success: false,
|
|
58
|
+
result: null,
|
|
59
|
+
error: error instanceof Error ? error.message : String(error),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
getTool(name) {
|
|
64
|
+
return this.tools.find((t) => t.name === name);
|
|
65
|
+
}
|
|
66
|
+
validateToolArguments(toolCall) {
|
|
67
|
+
const tool = this.getTool(toolCall.function.name);
|
|
68
|
+
if (!tool) {
|
|
69
|
+
return { valid: false, errors: [`Tool ${toolCall.function.name} not found`] };
|
|
70
|
+
}
|
|
71
|
+
if (typeof toolCall.function.arguments !== 'object' || toolCall.function.arguments === null) {
|
|
72
|
+
return {
|
|
73
|
+
valid: false,
|
|
74
|
+
errors: ['Arguments are not valid: must be an object.'],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const args = toolCall.function.arguments;
|
|
78
|
+
const errors = [];
|
|
79
|
+
this.validateRequiredParams(tool, args, errors);
|
|
80
|
+
this.validateUnknownParams(tool, args, errors);
|
|
81
|
+
this.validateParamTypes(tool, args, errors);
|
|
82
|
+
return errors.length > 0 ? { valid: false, errors } : { valid: true };
|
|
83
|
+
}
|
|
84
|
+
validateRequiredParams(tool, args, errors) {
|
|
85
|
+
const required = tool.parameters.required || [];
|
|
86
|
+
for (const param of required) {
|
|
87
|
+
if (!(param in args)) {
|
|
88
|
+
errors.push(`Missing required parameter: ${param}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
validateUnknownParams(tool, args, errors) {
|
|
93
|
+
if (tool.parameters.additionalProperties === false) {
|
|
94
|
+
const allowedParams = new Set(Object.keys(tool.parameters.properties));
|
|
95
|
+
for (const param of Object.keys(args)) {
|
|
96
|
+
if (!allowedParams.has(param)) {
|
|
97
|
+
errors.push(`Unknown parameter: ${param}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
validateParamTypes(tool, args, errors) {
|
|
103
|
+
for (const [paramName, paramValue] of Object.entries(args)) {
|
|
104
|
+
const schema = tool.parameters.properties[paramName];
|
|
105
|
+
if (!schema)
|
|
106
|
+
continue;
|
|
107
|
+
const type = schema.type;
|
|
108
|
+
if (!type)
|
|
109
|
+
continue;
|
|
110
|
+
const typeError = this.checkParamType(paramName, paramValue, type);
|
|
111
|
+
if (typeError) {
|
|
112
|
+
errors.push(typeError);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
checkParamType(paramName, paramValue, expectedType) {
|
|
117
|
+
const actualType = this.getJsonSchemaType(paramValue);
|
|
118
|
+
if (expectedType === 'integer') {
|
|
119
|
+
if (actualType !== 'number') {
|
|
120
|
+
return `Parameter ${paramName} has wrong type: expected integer, got ${actualType}`;
|
|
121
|
+
}
|
|
122
|
+
if (!Number.isInteger(paramValue)) {
|
|
123
|
+
return `Parameter ${paramName} must be an integer, got: ${paramValue}`;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (actualType !== expectedType) {
|
|
128
|
+
return `Parameter ${paramName} has wrong type: expected ${expectedType}, got ${actualType}`;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
getJsonSchemaType(value) {
|
|
133
|
+
if (Array.isArray(value))
|
|
134
|
+
return 'array';
|
|
135
|
+
if (value === null)
|
|
136
|
+
return 'null';
|
|
137
|
+
return typeof value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ExecutionContext } from '../types/context';
|
|
3
|
+
import type { ToolProvider } from '../types/tools';
|
|
4
|
+
type ToolHandler<TParams> = (params: TParams, context: ExecutionContext) => Promise<unknown> | unknown;
|
|
5
|
+
interface LocalToolDefinition<TSchema extends z.ZodObject> {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
schema: TSchema;
|
|
9
|
+
handler: ToolHandler<z.infer<TSchema>>;
|
|
10
|
+
}
|
|
11
|
+
export declare function tool<TSchema extends z.ZodObject>(name: string, description: string, schema: TSchema, handler: ToolHandler<z.infer<TSchema>>): LocalToolDefinition<TSchema>;
|
|
12
|
+
export declare function localTools(tools: LocalToolDefinition<z.ZodObject>[]): ToolProvider;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function tool(name, description, schema, handler) {
|
|
3
|
+
return { name, description, schema, handler };
|
|
4
|
+
}
|
|
5
|
+
const zodToJsonSchema = (schema) => {
|
|
6
|
+
const fullSchema = z.toJSONSchema(schema);
|
|
7
|
+
const { $schema: _$schema, ...jsonSchema } = fullSchema;
|
|
8
|
+
if (jsonSchema.type !== 'object' || !jsonSchema.properties) {
|
|
9
|
+
throw new Error('Tool parameters schema must be a Zod object schema');
|
|
10
|
+
}
|
|
11
|
+
return jsonSchema;
|
|
12
|
+
};
|
|
13
|
+
export function localTools(tools) {
|
|
14
|
+
const toolMap = new Map();
|
|
15
|
+
for (const tool of tools) {
|
|
16
|
+
if (toolMap.has(tool.name)) {
|
|
17
|
+
throw new Error(`Duplicate tool name: ${tool.name}`);
|
|
18
|
+
}
|
|
19
|
+
toolMap.set(tool.name, tool);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
getTools: async () => tools.map((t) => ({
|
|
23
|
+
name: t.name,
|
|
24
|
+
description: t.description,
|
|
25
|
+
parameters: zodToJsonSchema(t.schema),
|
|
26
|
+
})),
|
|
27
|
+
canHandle: (toolName) => toolMap.has(toolName),
|
|
28
|
+
execute: async (toolCall, context) => {
|
|
29
|
+
const toolDef = toolMap.get(toolCall.function.name);
|
|
30
|
+
if (!toolDef) {
|
|
31
|
+
return {
|
|
32
|
+
toolCallId: toolCall.id,
|
|
33
|
+
toolName: toolCall.function.name,
|
|
34
|
+
success: false,
|
|
35
|
+
result: null,
|
|
36
|
+
error: `Tool ${toolCall.function.name} not found`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const validatedParams = toolDef.schema.parse(toolCall.function.arguments);
|
|
41
|
+
const result = await toolDef.handler(validatedParams, context);
|
|
42
|
+
return {
|
|
43
|
+
toolCallId: toolCall.id,
|
|
44
|
+
toolName: toolCall.function.name,
|
|
45
|
+
success: true,
|
|
46
|
+
result,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof z.ZodError) {
|
|
51
|
+
return {
|
|
52
|
+
toolCallId: toolCall.id,
|
|
53
|
+
toolName: toolCall.function.name,
|
|
54
|
+
success: false,
|
|
55
|
+
result: null,
|
|
56
|
+
error: `Invalid arguments: ${error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
60
|
+
return {
|
|
61
|
+
toolCallId: toolCall.id,
|
|
62
|
+
toolName: toolCall.function.name,
|
|
63
|
+
success: false,
|
|
64
|
+
result: null,
|
|
65
|
+
error: err.message,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AuthContext } from '../types/context';
|
|
2
|
+
import type { FunctionParameters } from '../types/tools';
|
|
3
|
+
export interface MCPTool {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: FunctionParameters;
|
|
7
|
+
version?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MCPToolResponse {
|
|
10
|
+
result: unknown;
|
|
11
|
+
executionTime: number;
|
|
12
|
+
}
|
|
13
|
+
export interface MCPClientConfig {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
getHeaders: (authContext?: AuthContext) => Record<string, string>;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class MCPClient {
|
|
19
|
+
private readonly baseUrl;
|
|
20
|
+
private readonly timeout;
|
|
21
|
+
private readonly getHeaders;
|
|
22
|
+
constructor(config: MCPClientConfig);
|
|
23
|
+
listTools(): Promise<MCPTool[]>;
|
|
24
|
+
callTool(params: {
|
|
25
|
+
name: string;
|
|
26
|
+
arguments: Record<string, unknown>;
|
|
27
|
+
}, authContext?: AuthContext): Promise<MCPToolResponse>;
|
|
28
|
+
private request;
|
|
29
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const generateId = () => Math.random().toString(36).substring(2);
|
|
2
|
+
export class MCPClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
timeout;
|
|
5
|
+
getHeaders;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.baseUrl = config.baseUrl;
|
|
8
|
+
this.timeout = config.timeout || 30000;
|
|
9
|
+
this.getHeaders = config.getHeaders;
|
|
10
|
+
}
|
|
11
|
+
async listTools() {
|
|
12
|
+
const response = await this.request({
|
|
13
|
+
method: 'tools/list',
|
|
14
|
+
params: {},
|
|
15
|
+
});
|
|
16
|
+
return response.tools;
|
|
17
|
+
}
|
|
18
|
+
async callTool(params, authContext) {
|
|
19
|
+
return await this.request({
|
|
20
|
+
method: 'tools/call',
|
|
21
|
+
params,
|
|
22
|
+
}, authContext);
|
|
23
|
+
}
|
|
24
|
+
async request(req, authContext) {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(`${this.baseUrl}/rpc`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
...this.getHeaders(authContext),
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
jsonrpc: '2.0',
|
|
36
|
+
id: generateId(),
|
|
37
|
+
...req,
|
|
38
|
+
}),
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
});
|
|
41
|
+
clearTimeout(timeoutId);
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`MCP request failed with status ${response.status}: ${response.statusText}`);
|
|
44
|
+
}
|
|
45
|
+
const json = (await response.json());
|
|
46
|
+
if (json.error) {
|
|
47
|
+
throw new Error(`MCP error: ${json.error.message} (code: ${json.error.code})`);
|
|
48
|
+
}
|
|
49
|
+
if (json.result === undefined) {
|
|
50
|
+
throw new Error('Invalid MCP response: missing result');
|
|
51
|
+
}
|
|
52
|
+
return json.result;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
clearTimeout(timeoutId);
|
|
56
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
57
|
+
throw new Error(`MCP request timed out after ${this.timeout}ms`);
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AuthContext, ExecutionContext } from '../types/context';
|
|
2
|
+
import type { ToolCall, ToolDefinition, ToolProvider, ToolResult } from '../types/tools';
|
|
3
|
+
export interface MCPProviderConfig {
|
|
4
|
+
serverId: string;
|
|
5
|
+
serverUrl: string;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
getHeaders: (authContext?: AuthContext) => Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export declare const mcp: (config: MCPProviderConfig) => McpToolProvider;
|
|
10
|
+
export declare class McpToolProvider implements ToolProvider {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
private readonly client;
|
|
13
|
+
private toolCache;
|
|
14
|
+
private cacheExpiry;
|
|
15
|
+
private readonly cacheTTL;
|
|
16
|
+
private ongoingRequest;
|
|
17
|
+
constructor(config: MCPProviderConfig);
|
|
18
|
+
getTools(): Promise<ToolDefinition[]>;
|
|
19
|
+
canHandle(toolName: string): boolean;
|
|
20
|
+
execute(toolCall: ToolCall, context: ExecutionContext): Promise<ToolResult>;
|
|
21
|
+
private convertMCPTool;
|
|
22
|
+
}
|