@illuma-ai/agents 1.4.0-alpha.2 → 1.4.0-alpha.4
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/dist/cjs/main.cjs +37 -27
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/providers/tools-server/ToolsServerCapabilityProvider.cjs +4 -0
- package/dist/cjs/providers/tools-server/ToolsServerCapabilityProvider.cjs.map +1 -1
- package/dist/cjs/providers/types.cjs.map +1 -1
- package/dist/cjs/tools/artifacts/schema.cjs +86 -0
- package/dist/cjs/tools/artifacts/schema.cjs.map +1 -0
- package/dist/cjs/tools/artifacts/tool.cjs +219 -0
- package/dist/cjs/tools/artifacts/tool.cjs.map +1 -0
- package/dist/cjs/tools/fileSearch/formatter.cjs +2 -4
- package/dist/cjs/tools/fileSearch/formatter.cjs.map +1 -1
- package/dist/cjs/tools/fileSearch/ragClient.cjs +4 -6
- package/dist/cjs/tools/fileSearch/ragClient.cjs.map +1 -1
- package/dist/cjs/tools/fileSearch/schema.cjs.map +1 -1
- package/dist/cjs/tools/fileSearch/tool.cjs.map +1 -1
- package/dist/esm/main.mjs +2 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/providers/tools-server/ToolsServerCapabilityProvider.mjs +4 -0
- package/dist/esm/providers/tools-server/ToolsServerCapabilityProvider.mjs.map +1 -1
- package/dist/esm/providers/types.mjs.map +1 -1
- package/dist/esm/tools/artifacts/schema.mjs +79 -0
- package/dist/esm/tools/artifacts/schema.mjs.map +1 -0
- package/dist/esm/tools/artifacts/tool.mjs +213 -0
- package/dist/esm/tools/artifacts/tool.mjs.map +1 -0
- package/dist/esm/tools/fileSearch/formatter.mjs +2 -4
- package/dist/esm/tools/fileSearch/formatter.mjs.map +1 -1
- package/dist/esm/tools/fileSearch/ragClient.mjs +4 -6
- package/dist/esm/tools/fileSearch/ragClient.mjs.map +1 -1
- package/dist/esm/tools/fileSearch/schema.mjs.map +1 -1
- package/dist/esm/tools/fileSearch/tool.mjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/types.d.ts +14 -0
- package/dist/types/tools/artifacts/index.d.ts +3 -0
- package/dist/types/tools/artifacts/schema.d.ts +63 -0
- package/dist/types/tools/artifacts/tool.d.ts +16 -0
- package/dist/types/tools/artifacts/types.d.ts +127 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/providers/tools-server/ToolsServerCapabilityProvider.ts +8 -0
- package/src/providers/types.ts +17 -0
- package/src/tools/artifacts/__tests__/tool.test.ts +259 -0
- package/src/tools/artifacts/index.ts +33 -0
- package/src/tools/artifacts/schema.ts +99 -0
- package/src/tools/artifacts/tool.ts +289 -0
- package/src/tools/artifacts/types.ts +162 -0
- package/src/tools/fileSearch/__tests__/tool.test.ts +20 -10
- package/src/tools/fileSearch/formatter.ts +5 -7
- package/src/tools/fileSearch/ragClient.ts +6 -10
- package/src/tools/fileSearch/schema.ts +2 -2
- package/src/tools/fileSearch/tool.ts +6 -6
- package/src/tools/fileSearch/types.ts +4 -2
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the artifact_tool + content_reader library factories.
|
|
3
|
+
*
|
|
4
|
+
* The factories are thin dispatchers — their job is:
|
|
5
|
+
* 1. Resolve scope from runnableConfig (error if missing)
|
|
6
|
+
* 2. Optionally self-heal content_id via the resolver
|
|
7
|
+
* 3. Validate per-action required args
|
|
8
|
+
* 4. Route to the runtime's handler, propagate errors
|
|
9
|
+
*
|
|
10
|
+
* We verify each of those paths here with mock handlers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createArtifactTool, createContentReaderTool } from '../tool';
|
|
14
|
+
import type {
|
|
15
|
+
ArtifactWriteHandlers,
|
|
16
|
+
ContentReadHandlers,
|
|
17
|
+
ArtifactToolScope,
|
|
18
|
+
ArtifactToolResult,
|
|
19
|
+
ContentIdResolver,
|
|
20
|
+
} from '../types';
|
|
21
|
+
|
|
22
|
+
function makeScope(): ArtifactToolScope {
|
|
23
|
+
return { conversationId: 'conv-1', userId: 'user-1' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeWriteHandlers(): ArtifactWriteHandlers & {
|
|
27
|
+
_calls: Array<{ action: string; args: unknown; scope: ArtifactToolScope }>;
|
|
28
|
+
} {
|
|
29
|
+
const calls: Array<{
|
|
30
|
+
action: string;
|
|
31
|
+
args: unknown;
|
|
32
|
+
scope: ArtifactToolScope;
|
|
33
|
+
}> = [];
|
|
34
|
+
return {
|
|
35
|
+
_calls: calls,
|
|
36
|
+
write: jest.fn(async (args, scope): Promise<ArtifactToolResult> => {
|
|
37
|
+
calls.push({ action: 'write', args, scope });
|
|
38
|
+
return ['wrote it', { id: 'new-id' }];
|
|
39
|
+
}),
|
|
40
|
+
edit: jest.fn(async (args, scope): Promise<ArtifactToolResult> => {
|
|
41
|
+
calls.push({ action: 'edit', args, scope });
|
|
42
|
+
return ['edited', {}];
|
|
43
|
+
}),
|
|
44
|
+
verify: jest.fn(async (args, scope): Promise<ArtifactToolResult> => {
|
|
45
|
+
calls.push({ action: 'verify', args, scope });
|
|
46
|
+
return ['verified', {}];
|
|
47
|
+
}),
|
|
48
|
+
delete: jest.fn(async (args, scope): Promise<ArtifactToolResult> => {
|
|
49
|
+
calls.push({ action: 'delete', args, scope });
|
|
50
|
+
return ['deleted', {}];
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeReadHandlers(): ContentReadHandlers & {
|
|
56
|
+
_calls: Array<{ action: string; args: unknown; scope: ArtifactToolScope }>;
|
|
57
|
+
} {
|
|
58
|
+
const calls: Array<{
|
|
59
|
+
action: string;
|
|
60
|
+
args: unknown;
|
|
61
|
+
scope: ArtifactToolScope;
|
|
62
|
+
}> = [];
|
|
63
|
+
return {
|
|
64
|
+
_calls: calls,
|
|
65
|
+
read: jest.fn(async (args, scope): Promise<ArtifactToolResult> => {
|
|
66
|
+
calls.push({ action: 'read', args, scope });
|
|
67
|
+
return ['read-output', {}];
|
|
68
|
+
}),
|
|
69
|
+
search: jest.fn(async (args, scope): Promise<ArtifactToolResult> => {
|
|
70
|
+
calls.push({ action: 'search', args, scope });
|
|
71
|
+
return ['search-output', {}];
|
|
72
|
+
}),
|
|
73
|
+
list: jest.fn(async (args, scope): Promise<ArtifactToolResult> => {
|
|
74
|
+
calls.push({ action: 'list', args, scope });
|
|
75
|
+
return ['list-output', {}];
|
|
76
|
+
}),
|
|
77
|
+
info: jest.fn(async (args, scope): Promise<ArtifactToolResult> => {
|
|
78
|
+
calls.push({ action: 'info', args, scope });
|
|
79
|
+
return ['info-output', {}];
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('createArtifactTool', () => {
|
|
85
|
+
it('returns a scope error when getScope returns null', async () => {
|
|
86
|
+
const handlers = makeWriteHandlers();
|
|
87
|
+
const t = createArtifactTool({
|
|
88
|
+
handlers,
|
|
89
|
+
getScope: () => null,
|
|
90
|
+
});
|
|
91
|
+
const result = await t.invoke({ action: 'write', content: 'x' });
|
|
92
|
+
const text = Array.isArray(result) ? String(result[0]) : String(result);
|
|
93
|
+
expect(text).toMatch(/no conversation context/i);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('dispatches write to handlers.write with resolved args + scope', async () => {
|
|
97
|
+
const handlers = makeWriteHandlers();
|
|
98
|
+
const t = createArtifactTool({
|
|
99
|
+
handlers,
|
|
100
|
+
getScope: makeScope,
|
|
101
|
+
});
|
|
102
|
+
await t.invoke({ action: 'write', content: 'hello', name: 'doc.md' });
|
|
103
|
+
expect(handlers.write).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(handlers._calls[0].args).toEqual(
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
action: 'write',
|
|
107
|
+
content: 'hello',
|
|
108
|
+
name: 'doc.md',
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
expect(handlers._calls[0].scope).toEqual(
|
|
112
|
+
expect.objectContaining({ conversationId: 'conv-1' })
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects write without content', async () => {
|
|
117
|
+
const handlers = makeWriteHandlers();
|
|
118
|
+
const t = createArtifactTool({
|
|
119
|
+
handlers,
|
|
120
|
+
getScope: makeScope,
|
|
121
|
+
});
|
|
122
|
+
const result = await t.invoke({ action: 'write' });
|
|
123
|
+
const text = Array.isArray(result) ? String(result[0]) : String(result);
|
|
124
|
+
expect(text).toMatch(/write requires content/i);
|
|
125
|
+
expect(handlers.write).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('rejects edit/verify/delete without content_id', async () => {
|
|
129
|
+
const handlers = makeWriteHandlers();
|
|
130
|
+
const t = createArtifactTool({
|
|
131
|
+
handlers,
|
|
132
|
+
getScope: makeScope,
|
|
133
|
+
});
|
|
134
|
+
const e = await t.invoke({ action: 'edit', old_str: 'a', new_str: 'b' });
|
|
135
|
+
expect(String(Array.isArray(e) ? e[0] : e)).toMatch(
|
|
136
|
+
/edit requires content_id/i
|
|
137
|
+
);
|
|
138
|
+
const v = await t.invoke({ action: 'verify' });
|
|
139
|
+
expect(String(Array.isArray(v) ? v[0] : v)).toMatch(
|
|
140
|
+
/verify requires content_id/i
|
|
141
|
+
);
|
|
142
|
+
const d = await t.invoke({ action: 'delete' });
|
|
143
|
+
expect(String(Array.isArray(d) ? d[0] : d)).toMatch(
|
|
144
|
+
/delete requires content_id/i
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('applies the resolver before dispatching to handler', async () => {
|
|
149
|
+
const handlers = makeWriteHandlers();
|
|
150
|
+
const resolver: ContentIdResolver = {
|
|
151
|
+
resolve: jest.fn(async (id) => ({
|
|
152
|
+
resolvedId: `canonical:${id}`,
|
|
153
|
+
resolvedName: 'X',
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
const t = createArtifactTool({
|
|
157
|
+
handlers,
|
|
158
|
+
getScope: makeScope,
|
|
159
|
+
resolver,
|
|
160
|
+
});
|
|
161
|
+
await t.invoke({
|
|
162
|
+
action: 'edit',
|
|
163
|
+
content_id: 'nickname',
|
|
164
|
+
old_str: 'a',
|
|
165
|
+
new_str: 'b',
|
|
166
|
+
});
|
|
167
|
+
expect(handlers._calls[0].args).toEqual(
|
|
168
|
+
expect.objectContaining({ content_id: 'canonical:nickname' })
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('catches handler exceptions and returns them as tool errors', async () => {
|
|
173
|
+
const handlers = makeWriteHandlers();
|
|
174
|
+
handlers.write = jest.fn(async () => {
|
|
175
|
+
throw new Error('s3 unavailable');
|
|
176
|
+
});
|
|
177
|
+
const t = createArtifactTool({
|
|
178
|
+
handlers,
|
|
179
|
+
getScope: makeScope,
|
|
180
|
+
});
|
|
181
|
+
const result = await t.invoke({ action: 'write', content: 'x' });
|
|
182
|
+
const text = Array.isArray(result) ? String(result[0]) : String(result);
|
|
183
|
+
expect(text).toMatch(/s3 unavailable/);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('honors descriptionOverride for host-specific guidance', async () => {
|
|
187
|
+
const handlers = makeWriteHandlers();
|
|
188
|
+
const t = createArtifactTool({
|
|
189
|
+
handlers,
|
|
190
|
+
getScope: makeScope,
|
|
191
|
+
descriptionOverride: 'HOST-BRAND-GUIDE',
|
|
192
|
+
});
|
|
193
|
+
expect(t.description).toBe('HOST-BRAND-GUIDE');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('createContentReaderTool', () => {
|
|
198
|
+
it('dispatches list with no content_id required', async () => {
|
|
199
|
+
const handlers = makeReadHandlers();
|
|
200
|
+
const t = createContentReaderTool({
|
|
201
|
+
handlers,
|
|
202
|
+
getScope: makeScope,
|
|
203
|
+
});
|
|
204
|
+
await t.invoke({ action: 'list' });
|
|
205
|
+
expect(handlers.list).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('rejects read/info/search without content_id', async () => {
|
|
209
|
+
const handlers = makeReadHandlers();
|
|
210
|
+
const t = createContentReaderTool({
|
|
211
|
+
handlers,
|
|
212
|
+
getScope: makeScope,
|
|
213
|
+
});
|
|
214
|
+
const r = await t.invoke({ action: 'read' });
|
|
215
|
+
expect(String(Array.isArray(r) ? r[0] : r)).toMatch(
|
|
216
|
+
/read requires content_id/i
|
|
217
|
+
);
|
|
218
|
+
const i = await t.invoke({ action: 'info' });
|
|
219
|
+
expect(String(Array.isArray(i) ? i[0] : i)).toMatch(
|
|
220
|
+
/info requires content_id/i
|
|
221
|
+
);
|
|
222
|
+
const s = await t.invoke({ action: 'search', pattern: 'p' });
|
|
223
|
+
expect(String(Array.isArray(s) ? s[0] : s)).toMatch(
|
|
224
|
+
/search requires content_id/i
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('rejects search without pattern even when content_id is given', async () => {
|
|
229
|
+
const handlers = makeReadHandlers();
|
|
230
|
+
const t = createContentReaderTool({
|
|
231
|
+
handlers,
|
|
232
|
+
getScope: makeScope,
|
|
233
|
+
});
|
|
234
|
+
const result = await t.invoke({ action: 'search', content_id: 'x' });
|
|
235
|
+
const text = Array.isArray(result) ? String(result[0]) : String(result);
|
|
236
|
+
expect(text).toMatch(/search requires pattern/i);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('passes read pagination args through to the handler', async () => {
|
|
240
|
+
const handlers = makeReadHandlers();
|
|
241
|
+
const t = createContentReaderTool({
|
|
242
|
+
handlers,
|
|
243
|
+
getScope: makeScope,
|
|
244
|
+
});
|
|
245
|
+
await t.invoke({
|
|
246
|
+
action: 'read',
|
|
247
|
+
content_id: 'c-1',
|
|
248
|
+
start_line: 10,
|
|
249
|
+
end_line: 50,
|
|
250
|
+
});
|
|
251
|
+
expect(handlers._calls[0].args).toEqual(
|
|
252
|
+
expect.objectContaining({
|
|
253
|
+
content_id: 'c-1',
|
|
254
|
+
start_line: 10,
|
|
255
|
+
end_line: 50,
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createArtifactTool,
|
|
3
|
+
createContentReaderTool,
|
|
4
|
+
ARTIFACT_TOOL_NAME,
|
|
5
|
+
CONTENT_READER_NAME,
|
|
6
|
+
ARTIFACT_WRITE_ACTIONS,
|
|
7
|
+
CONTENT_READ_ACTIONS,
|
|
8
|
+
} from './tool';
|
|
9
|
+
export {
|
|
10
|
+
artifactToolSchema,
|
|
11
|
+
contentReaderSchema,
|
|
12
|
+
type ArtifactToolInput,
|
|
13
|
+
type ContentReaderInput,
|
|
14
|
+
} from './schema';
|
|
15
|
+
export type {
|
|
16
|
+
ArtifactToolScope,
|
|
17
|
+
ArtifactToolResult,
|
|
18
|
+
ArtifactToolLogger,
|
|
19
|
+
ArtifactToolBaseConfig,
|
|
20
|
+
ArtifactToolConfig,
|
|
21
|
+
ContentReaderToolConfig,
|
|
22
|
+
ArtifactWriteHandlers,
|
|
23
|
+
ContentReadHandlers,
|
|
24
|
+
ContentIdResolver,
|
|
25
|
+
WriteArgs,
|
|
26
|
+
EditArgs,
|
|
27
|
+
VerifyArgs,
|
|
28
|
+
DeleteArgs,
|
|
29
|
+
ReadArgs,
|
|
30
|
+
SearchArgs,
|
|
31
|
+
ListArgs,
|
|
32
|
+
InfoArgs,
|
|
33
|
+
} from './types';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const ARTIFACT_WRITE_ACTIONS = [
|
|
4
|
+
'write',
|
|
5
|
+
'edit',
|
|
6
|
+
'verify',
|
|
7
|
+
'delete',
|
|
8
|
+
] as const;
|
|
9
|
+
export const CONTENT_READ_ACTIONS = ['read', 'search', 'list', 'info'] as const;
|
|
10
|
+
|
|
11
|
+
export const artifactToolSchema = z.object({
|
|
12
|
+
action: z
|
|
13
|
+
.enum(ARTIFACT_WRITE_ACTIONS)
|
|
14
|
+
.describe(
|
|
15
|
+
'Authoring action: write (create/overwrite), edit (str_replace), verify (syntax check), delete (remove).'
|
|
16
|
+
),
|
|
17
|
+
content_id: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe(
|
|
21
|
+
'ID of the artifact entry. Required for edit/verify/delete; optional for write (supply to overwrite an existing entry).'
|
|
22
|
+
),
|
|
23
|
+
|
|
24
|
+
// write
|
|
25
|
+
content: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Full file content (required for write action).'),
|
|
29
|
+
name: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
'Filename for the new entry (required when creating). MUST include the correct file extension — the extension drives the preview template.'
|
|
34
|
+
),
|
|
35
|
+
|
|
36
|
+
// edit (str_replace)
|
|
37
|
+
old_str: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('Exact string to find and replace (required for edit).'),
|
|
41
|
+
new_str: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Replacement string (required for edit).'),
|
|
45
|
+
replace_all: z
|
|
46
|
+
.boolean()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(
|
|
49
|
+
'edit: when true, replaces every occurrence of old_str. When false (default) and old_str matches more than one location, the edit is refused.'
|
|
50
|
+
),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const contentReaderSchema = z.object({
|
|
54
|
+
action: z
|
|
55
|
+
.enum(CONTENT_READ_ACTIONS)
|
|
56
|
+
.describe(
|
|
57
|
+
'Read-only action: read (lines), search (regex), list (all entries), info (metadata).'
|
|
58
|
+
),
|
|
59
|
+
content_id: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe(
|
|
63
|
+
'ID of the content entry. Required for read/search/info. Omit for list.'
|
|
64
|
+
),
|
|
65
|
+
|
|
66
|
+
// read pagination
|
|
67
|
+
start_line: z.number().optional().describe('1-based start line for reading.'),
|
|
68
|
+
end_line: z.number().optional().describe('1-based end line (inclusive).'),
|
|
69
|
+
|
|
70
|
+
// search
|
|
71
|
+
pattern: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Regex pattern (required for search).'),
|
|
75
|
+
flags: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Regex flags (e.g., "i" for case-insensitive).'),
|
|
79
|
+
context: z
|
|
80
|
+
.number()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Lines of context around each match.'),
|
|
83
|
+
|
|
84
|
+
// shared pagination
|
|
85
|
+
offset: z
|
|
86
|
+
.number()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Offset for read or search pagination.'),
|
|
89
|
+
limit: z
|
|
90
|
+
.number()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Max lines (read) or matches (search).'),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const ARTIFACT_TOOL_NAME = 'artifact_tool';
|
|
96
|
+
export const CONTENT_READER_NAME = 'content_reader';
|
|
97
|
+
|
|
98
|
+
export type ArtifactToolInput = z.infer<typeof artifactToolSchema>;
|
|
99
|
+
export type ContentReaderInput = z.infer<typeof contentReaderSchema>;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifact_tool + content_reader library factories.
|
|
3
|
+
*
|
|
4
|
+
* The library owns the LangChain wiring (schema, description, response
|
|
5
|
+
* shape) and the action dispatch; the runtime supplies a handler bundle
|
|
6
|
+
* matching the `ArtifactWriteHandlers` / `ContentReadHandlers` interface.
|
|
7
|
+
*
|
|
8
|
+
* This keeps 800+ LOC of host-specific handler logic (S3 adapters, file
|
|
9
|
+
* model CRUD, syntax checkers, line utils) out of the library while
|
|
10
|
+
* still centralizing the tool surface every runtime shares.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
14
|
+
import {
|
|
15
|
+
artifactToolSchema,
|
|
16
|
+
contentReaderSchema,
|
|
17
|
+
ARTIFACT_TOOL_NAME,
|
|
18
|
+
CONTENT_READER_NAME,
|
|
19
|
+
} from './schema';
|
|
20
|
+
import type {
|
|
21
|
+
ArtifactToolConfig,
|
|
22
|
+
ContentReaderToolConfig,
|
|
23
|
+
ArtifactToolScope,
|
|
24
|
+
ArtifactToolResult,
|
|
25
|
+
WriteArgs,
|
|
26
|
+
EditArgs,
|
|
27
|
+
VerifyArgs,
|
|
28
|
+
DeleteArgs,
|
|
29
|
+
ReadArgs,
|
|
30
|
+
SearchArgs,
|
|
31
|
+
ListArgs,
|
|
32
|
+
InfoArgs,
|
|
33
|
+
ContentIdResolver,
|
|
34
|
+
} from './types';
|
|
35
|
+
|
|
36
|
+
const DEFAULT_ARTIFACT_DESCRIPTION = `Author content artifacts that render live in the host's preview panel — this tool does NOT produce downloadable files (use execute_code for those).
|
|
37
|
+
|
|
38
|
+
Actions:
|
|
39
|
+
- write: Create a new artifact. Write a COMPLETE file in one call. The \`name\` field MUST include the file extension — it routes the preview (.tsx, .html, .mmd, .svg, .csv, .json, .dot, .md, .drawio).
|
|
40
|
+
- verify: Check an artifact for syntax errors. REQUIRED as the next step after every write/edit on code files — do not render until verify passes.
|
|
41
|
+
- edit: Surgical string replacement — provide old_str (exact match) and new_str. Works on all file types.
|
|
42
|
+
- delete: Remove an artifact and its backing file.
|
|
43
|
+
|
|
44
|
+
Artifacts are persisted by the host. No manual save needed.`;
|
|
45
|
+
|
|
46
|
+
const DEFAULT_CONTENT_READER_DESCRIPTION = `Read and navigate stored content — artifacts authored by artifact_tool, large tool results auto-cached by the host, uploaded file attachments, and code blocks.
|
|
47
|
+
|
|
48
|
+
Read-only surface. Use write/edit tools (artifact_tool, execute_code) to mutate.
|
|
49
|
+
|
|
50
|
+
Actions:
|
|
51
|
+
- read: Return line ranges from a specific content_id.
|
|
52
|
+
- search: Regex search across a specific content_id with paginated matches.
|
|
53
|
+
- list: Enumerate every content entry currently stored.
|
|
54
|
+
- info: Metadata (size, kind, creation time) for a specific content_id.`;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Optional content_id self-healing — if the runtime supplies a resolver,
|
|
58
|
+
* we pre-resolve the ID on every action that takes one so nicknames
|
|
59
|
+
* (e.g., "Dashboard") map to canonical IDs.
|
|
60
|
+
*/
|
|
61
|
+
async function resolveContentIdIfPresent(
|
|
62
|
+
resolver: ContentIdResolver | undefined,
|
|
63
|
+
id: string | undefined,
|
|
64
|
+
scope: ArtifactToolScope,
|
|
65
|
+
logger?: { debug: (msg: string) => void }
|
|
66
|
+
): Promise<string | undefined> {
|
|
67
|
+
if (!resolver || !id) return id;
|
|
68
|
+
const out = await resolver.resolve(id, scope);
|
|
69
|
+
if (!out) return id;
|
|
70
|
+
if (out.resolvedId !== id) {
|
|
71
|
+
logger?.debug(
|
|
72
|
+
`[artifact] resolved "${id}" → "${out.resolvedId}"${out.resolvedName ? ` ("${out.resolvedName}")` : ''}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return out.resolvedId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Writer tool (artifact_tool) ─────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export function createArtifactTool(
|
|
81
|
+
config: ArtifactToolConfig
|
|
82
|
+
): DynamicStructuredTool {
|
|
83
|
+
const { handlers, getScope, resolver, logger, descriptionOverride } = config;
|
|
84
|
+
|
|
85
|
+
return tool(
|
|
86
|
+
async (rawInput, runnableConfig): Promise<ArtifactToolResult> => {
|
|
87
|
+
const scope = getScope(runnableConfig);
|
|
88
|
+
if (!scope) {
|
|
89
|
+
logger?.warn('[artifact_tool] no scope resolved from runnableConfig');
|
|
90
|
+
return ['Error: No conversation context available', {}];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const input = rawInput as {
|
|
94
|
+
action: 'write' | 'edit' | 'verify' | 'delete';
|
|
95
|
+
content_id?: string;
|
|
96
|
+
content?: string;
|
|
97
|
+
name?: string;
|
|
98
|
+
old_str?: string;
|
|
99
|
+
new_str?: string;
|
|
100
|
+
replace_all?: boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const resolvedContentId = await resolveContentIdIfPresent(
|
|
104
|
+
resolver,
|
|
105
|
+
input.content_id,
|
|
106
|
+
scope,
|
|
107
|
+
logger
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const started = Date.now();
|
|
111
|
+
try {
|
|
112
|
+
switch (input.action) {
|
|
113
|
+
case 'write': {
|
|
114
|
+
const args: WriteArgs = {
|
|
115
|
+
action: 'write',
|
|
116
|
+
content_id: resolvedContentId,
|
|
117
|
+
content: input.content ?? '',
|
|
118
|
+
name: input.name,
|
|
119
|
+
};
|
|
120
|
+
if (!args.content) return ['Error: write requires content', {}];
|
|
121
|
+
return await handlers.write(args, scope);
|
|
122
|
+
}
|
|
123
|
+
case 'edit': {
|
|
124
|
+
if (!resolvedContentId)
|
|
125
|
+
return ['Error: edit requires content_id', {}];
|
|
126
|
+
const args: EditArgs = {
|
|
127
|
+
action: 'edit',
|
|
128
|
+
content_id: resolvedContentId,
|
|
129
|
+
old_str: input.old_str ?? '',
|
|
130
|
+
new_str: input.new_str ?? '',
|
|
131
|
+
replace_all: input.replace_all,
|
|
132
|
+
};
|
|
133
|
+
if (!args.old_str) return ['Error: edit requires old_str', {}];
|
|
134
|
+
return await handlers.edit(args, scope);
|
|
135
|
+
}
|
|
136
|
+
case 'verify': {
|
|
137
|
+
if (!resolvedContentId)
|
|
138
|
+
return ['Error: verify requires content_id', {}];
|
|
139
|
+
const args: VerifyArgs = {
|
|
140
|
+
action: 'verify',
|
|
141
|
+
content_id: resolvedContentId,
|
|
142
|
+
};
|
|
143
|
+
return await handlers.verify(args, scope);
|
|
144
|
+
}
|
|
145
|
+
case 'delete': {
|
|
146
|
+
if (!resolvedContentId)
|
|
147
|
+
return ['Error: delete requires content_id', {}];
|
|
148
|
+
const args: DeleteArgs = {
|
|
149
|
+
action: 'delete',
|
|
150
|
+
content_id: resolvedContentId,
|
|
151
|
+
};
|
|
152
|
+
return await handlers.delete(args, scope);
|
|
153
|
+
}
|
|
154
|
+
default:
|
|
155
|
+
return [
|
|
156
|
+
`Unknown action: ${(input as { action: string }).action}`,
|
|
157
|
+
{},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
162
|
+
logger?.error('[artifact_tool] handler threw', {
|
|
163
|
+
action: input.action,
|
|
164
|
+
contentId: resolvedContentId,
|
|
165
|
+
error: e.message,
|
|
166
|
+
elapsed: `${Date.now() - started}ms`,
|
|
167
|
+
});
|
|
168
|
+
return [`Error: ${e.message}`, {}];
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: ARTIFACT_TOOL_NAME,
|
|
173
|
+
responseFormat: 'content_and_artifact',
|
|
174
|
+
description: descriptionOverride ?? DEFAULT_ARTIFACT_DESCRIPTION,
|
|
175
|
+
schema: artifactToolSchema,
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Reader tool (content_reader) ────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
export function createContentReaderTool(
|
|
183
|
+
config: ContentReaderToolConfig
|
|
184
|
+
): DynamicStructuredTool {
|
|
185
|
+
const { handlers, getScope, resolver, logger, descriptionOverride } = config;
|
|
186
|
+
|
|
187
|
+
return tool(
|
|
188
|
+
async (rawInput, runnableConfig): Promise<ArtifactToolResult> => {
|
|
189
|
+
const scope = getScope(runnableConfig);
|
|
190
|
+
if (!scope) {
|
|
191
|
+
logger?.warn('[content_reader] no scope resolved from runnableConfig');
|
|
192
|
+
return ['Error: No conversation context available', {}];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const input = rawInput as {
|
|
196
|
+
action: 'read' | 'search' | 'list' | 'info';
|
|
197
|
+
content_id?: string;
|
|
198
|
+
start_line?: number;
|
|
199
|
+
end_line?: number;
|
|
200
|
+
pattern?: string;
|
|
201
|
+
flags?: string;
|
|
202
|
+
context?: number;
|
|
203
|
+
offset?: number;
|
|
204
|
+
limit?: number;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const resolvedContentId = await resolveContentIdIfPresent(
|
|
208
|
+
resolver,
|
|
209
|
+
input.content_id,
|
|
210
|
+
scope,
|
|
211
|
+
logger
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const started = Date.now();
|
|
215
|
+
try {
|
|
216
|
+
switch (input.action) {
|
|
217
|
+
case 'read': {
|
|
218
|
+
if (!resolvedContentId)
|
|
219
|
+
return ['Error: read requires content_id', {}];
|
|
220
|
+
const args: ReadArgs = {
|
|
221
|
+
action: 'read',
|
|
222
|
+
content_id: resolvedContentId,
|
|
223
|
+
start_line: input.start_line,
|
|
224
|
+
end_line: input.end_line,
|
|
225
|
+
offset: input.offset,
|
|
226
|
+
limit: input.limit,
|
|
227
|
+
};
|
|
228
|
+
return await handlers.read(args, scope);
|
|
229
|
+
}
|
|
230
|
+
case 'search': {
|
|
231
|
+
if (!resolvedContentId)
|
|
232
|
+
return ['Error: search requires content_id', {}];
|
|
233
|
+
if (!input.pattern) return ['Error: search requires pattern', {}];
|
|
234
|
+
const args: SearchArgs = {
|
|
235
|
+
action: 'search',
|
|
236
|
+
content_id: resolvedContentId,
|
|
237
|
+
pattern: input.pattern,
|
|
238
|
+
flags: input.flags,
|
|
239
|
+
context: input.context,
|
|
240
|
+
offset: input.offset,
|
|
241
|
+
limit: input.limit,
|
|
242
|
+
};
|
|
243
|
+
return await handlers.search(args, scope);
|
|
244
|
+
}
|
|
245
|
+
case 'list': {
|
|
246
|
+
const args: ListArgs = { action: 'list' };
|
|
247
|
+
return await handlers.list(args, scope);
|
|
248
|
+
}
|
|
249
|
+
case 'info': {
|
|
250
|
+
if (!resolvedContentId)
|
|
251
|
+
return ['Error: info requires content_id', {}];
|
|
252
|
+
const args: InfoArgs = {
|
|
253
|
+
action: 'info',
|
|
254
|
+
content_id: resolvedContentId,
|
|
255
|
+
};
|
|
256
|
+
return await handlers.info(args, scope);
|
|
257
|
+
}
|
|
258
|
+
default:
|
|
259
|
+
return [
|
|
260
|
+
`Unknown action: ${(input as { action: string }).action}`,
|
|
261
|
+
{},
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
266
|
+
logger?.error('[content_reader] handler threw', {
|
|
267
|
+
action: input.action,
|
|
268
|
+
contentId: resolvedContentId,
|
|
269
|
+
error: e.message,
|
|
270
|
+
elapsed: `${Date.now() - started}ms`,
|
|
271
|
+
});
|
|
272
|
+
return [`Error: ${e.message}`, {}];
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: CONTENT_READER_NAME,
|
|
277
|
+
responseFormat: 'content_and_artifact',
|
|
278
|
+
description: descriptionOverride ?? DEFAULT_CONTENT_READER_DESCRIPTION,
|
|
279
|
+
schema: contentReaderSchema,
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export {
|
|
285
|
+
ARTIFACT_TOOL_NAME,
|
|
286
|
+
CONTENT_READER_NAME,
|
|
287
|
+
ARTIFACT_WRITE_ACTIONS,
|
|
288
|
+
CONTENT_READ_ACTIONS,
|
|
289
|
+
} from './schema';
|