@link-assistant/agent 0.26.9 → 0.26.10
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/package.json +1 -1
- package/src/session/message-v2.ts +8 -1
- package/src/session/processor.ts +3 -7
- package/src/session/prompt.ts +32 -26
- package/src/tool/bash.ts +37 -9
- package/src/tool/batch.ts +6 -8
- package/src/tool/grep.ts +1 -1
- package/src/tool/process-result.ts +18 -0
- package/src/tool/tool.ts +46 -9
package/package.json
CHANGED
|
@@ -338,6 +338,8 @@ export namespace MessageV2 {
|
|
|
338
338
|
status: z.literal('error'),
|
|
339
339
|
input: z.record(z.string(), z.any()),
|
|
340
340
|
error: z.string(),
|
|
341
|
+
title: z.string().optional(),
|
|
342
|
+
attachments: FilePart.array().optional(),
|
|
341
343
|
metadata: z.record(z.string(), z.any()).optional(),
|
|
342
344
|
time: z.object({
|
|
343
345
|
start: z.number(),
|
|
@@ -734,7 +736,10 @@ export namespace MessageV2 {
|
|
|
734
736
|
type: 'step-start',
|
|
735
737
|
});
|
|
736
738
|
if (part.type === 'tool') {
|
|
737
|
-
if (
|
|
739
|
+
if (
|
|
740
|
+
part.state.status === 'completed' ||
|
|
741
|
+
part.state.status === 'error'
|
|
742
|
+
) {
|
|
738
743
|
if (part.state.attachments?.length) {
|
|
739
744
|
result.push({
|
|
740
745
|
id: Identifier.ascending('message'),
|
|
@@ -753,6 +758,8 @@ export namespace MessageV2 {
|
|
|
753
758
|
],
|
|
754
759
|
});
|
|
755
760
|
}
|
|
761
|
+
}
|
|
762
|
+
if (part.state.status === 'completed') {
|
|
756
763
|
assistantMessage.parts.push({
|
|
757
764
|
type: ('tool-' + part.tool) as `tool-${string}`,
|
|
758
765
|
state: 'output-available',
|
package/src/session/processor.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { SessionStatus } from './status';
|
|
|
19
19
|
import { config, isVerbose } from '../config/config';
|
|
20
20
|
import { SessionCompaction } from './compaction';
|
|
21
21
|
import { SSEUsageExtractor } from '../util/sse-usage-extractor';
|
|
22
|
+
import { Tool } from '../tool/tool';
|
|
22
23
|
|
|
23
24
|
export namespace SessionProcessor {
|
|
24
25
|
const DOOM_LOOP_THRESHOLD = 3;
|
|
@@ -233,18 +234,13 @@ export namespace SessionProcessor {
|
|
|
233
234
|
if (match && match.state.status === 'running') {
|
|
234
235
|
await Session.updatePart({
|
|
235
236
|
...match,
|
|
236
|
-
state: {
|
|
237
|
-
status: 'completed',
|
|
237
|
+
state: Tool.toState(value.output, {
|
|
238
238
|
input: value.input,
|
|
239
|
-
output: value.output.output,
|
|
240
|
-
metadata: value.output.metadata,
|
|
241
|
-
title: value.output.title,
|
|
242
239
|
time: {
|
|
243
240
|
start: match.state.time.start,
|
|
244
241
|
end: Date.now(),
|
|
245
242
|
},
|
|
246
|
-
|
|
247
|
-
},
|
|
243
|
+
}),
|
|
248
244
|
});
|
|
249
245
|
|
|
250
246
|
delete toolcalls[value.toolCallId];
|
package/src/session/prompt.ts
CHANGED
|
@@ -48,6 +48,8 @@ import { fn } from '../util/fn';
|
|
|
48
48
|
import { SessionProcessor } from './processor';
|
|
49
49
|
import { TaskTool } from '../tool/task';
|
|
50
50
|
import { SessionStatus } from './status';
|
|
51
|
+
import { Tool } from '../tool/tool';
|
|
52
|
+
import { processResult } from '../tool/process-result';
|
|
51
53
|
|
|
52
54
|
export namespace SessionPrompt {
|
|
53
55
|
const log = Log.create({ service: 'session.prompt' });
|
|
@@ -1198,12 +1200,7 @@ export namespace SessionPrompt {
|
|
|
1198
1200
|
});
|
|
1199
1201
|
return result;
|
|
1200
1202
|
},
|
|
1201
|
-
toModelOutput
|
|
1202
|
-
return {
|
|
1203
|
-
type: 'text',
|
|
1204
|
-
value: result.output,
|
|
1205
|
-
};
|
|
1206
|
-
},
|
|
1203
|
+
toModelOutput: Tool.toModelOutput,
|
|
1207
1204
|
});
|
|
1208
1205
|
}
|
|
1209
1206
|
|
|
@@ -1260,14 +1257,10 @@ export namespace SessionPrompt {
|
|
|
1260
1257
|
output: textParts.join('\n\n'),
|
|
1261
1258
|
attachments,
|
|
1262
1259
|
content: result.content, // directly return content to preserve ordering when outputting to model
|
|
1260
|
+
isError: result.isError,
|
|
1263
1261
|
};
|
|
1264
1262
|
};
|
|
1265
|
-
item.toModelOutput =
|
|
1266
|
-
return {
|
|
1267
|
-
type: 'text',
|
|
1268
|
-
value: result.output,
|
|
1269
|
-
};
|
|
1270
|
-
};
|
|
1263
|
+
item.toModelOutput = Tool.toModelOutput;
|
|
1271
1264
|
tools[key] = item;
|
|
1272
1265
|
}
|
|
1273
1266
|
return tools;
|
|
@@ -1723,20 +1716,29 @@ export namespace SessionPrompt {
|
|
|
1723
1716
|
msg.time.completed = Date.now();
|
|
1724
1717
|
await Session.updateMessage(msg);
|
|
1725
1718
|
if (part.state.status === 'running') {
|
|
1726
|
-
part.state =
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
...
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1719
|
+
part.state = Tool.toState(
|
|
1720
|
+
{
|
|
1721
|
+
title: '',
|
|
1722
|
+
...processResult({
|
|
1723
|
+
output,
|
|
1724
|
+
exit: proc.exitCode,
|
|
1725
|
+
signal: proc.signalCode,
|
|
1726
|
+
}),
|
|
1727
|
+
metadata: {
|
|
1728
|
+
output,
|
|
1729
|
+
exit: proc.exitCode,
|
|
1730
|
+
signal: proc.signalCode,
|
|
1731
|
+
description: '',
|
|
1732
|
+
},
|
|
1737
1733
|
},
|
|
1738
|
-
|
|
1739
|
-
|
|
1734
|
+
{
|
|
1735
|
+
time: {
|
|
1736
|
+
...part.state.time,
|
|
1737
|
+
end: Date.now(),
|
|
1738
|
+
},
|
|
1739
|
+
input: part.state.input,
|
|
1740
|
+
}
|
|
1741
|
+
);
|
|
1740
1742
|
await Session.updatePart(part);
|
|
1741
1743
|
}
|
|
1742
1744
|
return { info: msg, parts: [part] };
|
|
@@ -1794,7 +1796,11 @@ export namespace SessionPrompt {
|
|
|
1794
1796
|
const results = await Promise.all(
|
|
1795
1797
|
shell.map(async ([, cmd]) => {
|
|
1796
1798
|
try {
|
|
1797
|
-
|
|
1799
|
+
const result = await $`${{ raw: cmd }}`.nothrow();
|
|
1800
|
+
return processResult({
|
|
1801
|
+
output: result.text(),
|
|
1802
|
+
exit: result.exitCode,
|
|
1803
|
+
}).output;
|
|
1798
1804
|
} catch (error) {
|
|
1799
1805
|
return `Error executing command: ${error instanceof Error ? error.message : String(error)}`;
|
|
1800
1806
|
}
|
package/src/tool/bash.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { $ } from 'bun';
|
|
|
11
11
|
import { Filesystem } from '../util/filesystem';
|
|
12
12
|
import { Permission } from '../permission';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
|
+
import { processResult } from './process-result';
|
|
14
15
|
|
|
15
16
|
const MAX_OUTPUT_LENGTH = 30_000;
|
|
16
17
|
const DEFAULT_TIMEOUT = 1 * 60 * 1000;
|
|
@@ -154,10 +155,11 @@ export const BashTool = Tool.define('bash', {
|
|
|
154
155
|
let timedOut = false;
|
|
155
156
|
let aborted = false;
|
|
156
157
|
let exited = false;
|
|
158
|
+
let closed = false;
|
|
157
159
|
|
|
158
160
|
const killTree = async () => {
|
|
159
161
|
const pid = proc.pid;
|
|
160
|
-
if (!pid ||
|
|
162
|
+
if (!pid || closed) {
|
|
161
163
|
return;
|
|
162
164
|
}
|
|
163
165
|
|
|
@@ -175,7 +177,9 @@ export const BashTool = Tool.define('bash', {
|
|
|
175
177
|
try {
|
|
176
178
|
process.kill(-pid, 'SIGTERM');
|
|
177
179
|
await Bun.sleep(SIGKILL_TIMEOUT_MS);
|
|
178
|
-
|
|
180
|
+
// The shell may exit before a child that ignores SIGTERM. The
|
|
181
|
+
// process group still needs SIGKILL until its output pipes close.
|
|
182
|
+
if (!closed) {
|
|
179
183
|
process.kill(-pid, 'SIGKILL');
|
|
180
184
|
}
|
|
181
185
|
} catch (_e) {
|
|
@@ -187,11 +191,6 @@ export const BashTool = Tool.define('bash', {
|
|
|
187
191
|
}
|
|
188
192
|
};
|
|
189
193
|
|
|
190
|
-
if (ctx.abort.aborted) {
|
|
191
|
-
aborted = true;
|
|
192
|
-
await killTree();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
194
|
const abortHandler = () => {
|
|
196
195
|
aborted = true;
|
|
197
196
|
void killTree();
|
|
@@ -204,7 +203,7 @@ export const BashTool = Tool.define('bash', {
|
|
|
204
203
|
void killTree();
|
|
205
204
|
}, timeout);
|
|
206
205
|
|
|
207
|
-
|
|
206
|
+
const completion = new Promise<void>((resolve, reject) => {
|
|
208
207
|
const cleanup = () => {
|
|
209
208
|
clearTimeout(timeoutTimer);
|
|
210
209
|
ctx.abort.removeEventListener('abort', abortHandler);
|
|
@@ -212,6 +211,12 @@ export const BashTool = Tool.define('bash', {
|
|
|
212
211
|
|
|
213
212
|
proc.once('exit', () => {
|
|
214
213
|
exited = true;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// close follows exit after stdout/stderr have drained. Install these
|
|
217
|
+
// listeners before killing an already-aborted process.
|
|
218
|
+
proc.once('close', () => {
|
|
219
|
+
closed = true;
|
|
215
220
|
cleanup();
|
|
216
221
|
resolve();
|
|
217
222
|
});
|
|
@@ -223,6 +228,9 @@ export const BashTool = Tool.define('bash', {
|
|
|
223
228
|
});
|
|
224
229
|
});
|
|
225
230
|
|
|
231
|
+
if (ctx.abort.aborted) abortHandler();
|
|
232
|
+
await completion;
|
|
233
|
+
|
|
226
234
|
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
227
235
|
output = output.slice(0, MAX_OUTPUT_LENGTH);
|
|
228
236
|
output += '\n\n(Output was truncated due to length limit)';
|
|
@@ -236,14 +244,34 @@ export const BashTool = Tool.define('bash', {
|
|
|
236
244
|
output += '\n\n(Command was aborted)';
|
|
237
245
|
}
|
|
238
246
|
|
|
247
|
+
const result = processResult({
|
|
248
|
+
output,
|
|
249
|
+
exit: proc.exitCode,
|
|
250
|
+
signal: proc.signalCode,
|
|
251
|
+
timedOut,
|
|
252
|
+
aborted,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
log.debug(() => ({
|
|
256
|
+
message: 'command finished',
|
|
257
|
+
exit: proc.exitCode,
|
|
258
|
+
signal: proc.signalCode,
|
|
259
|
+
timedOut,
|
|
260
|
+
aborted,
|
|
261
|
+
isError: result.isError,
|
|
262
|
+
}));
|
|
263
|
+
|
|
239
264
|
return {
|
|
240
265
|
title: params.command,
|
|
241
266
|
metadata: {
|
|
242
267
|
output,
|
|
243
268
|
exit: proc.exitCode,
|
|
269
|
+
signal: proc.signalCode,
|
|
270
|
+
timedOut,
|
|
271
|
+
aborted,
|
|
244
272
|
description: params.description,
|
|
245
273
|
},
|
|
246
|
-
|
|
274
|
+
...result,
|
|
247
275
|
};
|
|
248
276
|
},
|
|
249
277
|
});
|
package/src/tool/batch.ts
CHANGED
|
@@ -101,21 +101,18 @@ export const BatchTool = Tool.define('batch', async () => {
|
|
|
101
101
|
type: 'tool',
|
|
102
102
|
tool: call.tool,
|
|
103
103
|
callID: partID,
|
|
104
|
-
state: {
|
|
105
|
-
status: 'completed',
|
|
104
|
+
state: Tool.toState(result, {
|
|
106
105
|
input: call.parameters,
|
|
107
|
-
output: result.output,
|
|
108
|
-
title: result.title,
|
|
109
|
-
metadata: result.metadata,
|
|
110
|
-
attachments: result.attachments,
|
|
111
106
|
time: {
|
|
112
107
|
start: callStartTime,
|
|
113
108
|
end: Date.now(),
|
|
114
109
|
},
|
|
115
|
-
},
|
|
110
|
+
}),
|
|
116
111
|
});
|
|
117
112
|
|
|
118
|
-
return
|
|
113
|
+
return result.isError
|
|
114
|
+
? { success: false as const, tool: call.tool, error: result.output }
|
|
115
|
+
: { success: true as const, tool: call.tool, result };
|
|
119
116
|
} catch (error) {
|
|
120
117
|
await Session.updatePart({
|
|
121
118
|
id: partID,
|
|
@@ -179,6 +176,7 @@ export const BatchTool = Tool.define('batch', async () => {
|
|
|
179
176
|
return {
|
|
180
177
|
title: `Batch execution (${successfulCalls}/${results.length} successful)`,
|
|
181
178
|
output: outputMessage,
|
|
179
|
+
isError: failedCalls > 0,
|
|
182
180
|
attachments: results
|
|
183
181
|
.filter((result) => result.success)
|
|
184
182
|
.flatMap((r) => r.result.attachments ?? []),
|
package/src/tool/grep.ts
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Keep process status in the text sent to every provider, including those
|
|
2
|
+
* whose tool protocol has no separate error flag. */
|
|
3
|
+
export function processResult(input: {
|
|
4
|
+
output: string;
|
|
5
|
+
exit: number | null;
|
|
6
|
+
signal?: string | null;
|
|
7
|
+
timedOut?: boolean;
|
|
8
|
+
aborted?: boolean;
|
|
9
|
+
}) {
|
|
10
|
+
const isError = input.exit !== 0 || !!input.timedOut || !!input.aborted;
|
|
11
|
+
const status = input.signal ? ` (signal ${input.signal})` : '';
|
|
12
|
+
return {
|
|
13
|
+
output: isError
|
|
14
|
+
? `Exit code ${input.exit ?? 'unavailable'}${status}\n${input.output}`
|
|
15
|
+
: input.output,
|
|
16
|
+
isError,
|
|
17
|
+
};
|
|
18
|
+
}
|
package/src/tool/tool.ts
CHANGED
|
@@ -1,11 +1,56 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
import type { MessageV2 } from '../session/message-v2';
|
|
3
|
+
import type { ToolResultOutput } from 'ai';
|
|
3
4
|
|
|
4
5
|
export namespace Tool {
|
|
5
6
|
interface Metadata {
|
|
6
7
|
[key: string]: any;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
export interface Result<M extends Metadata = Metadata> {
|
|
11
|
+
title: string;
|
|
12
|
+
metadata: M;
|
|
13
|
+
output: string;
|
|
14
|
+
isError?: boolean;
|
|
15
|
+
attachments?: MessageV2.FilePart[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// AI SDK 7 passes an options object containing the execution output.
|
|
19
|
+
export function toModelOutput({
|
|
20
|
+
output: result,
|
|
21
|
+
}: {
|
|
22
|
+
output: Pick<Result, 'output' | 'isError'>;
|
|
23
|
+
}): ToolResultOutput {
|
|
24
|
+
return {
|
|
25
|
+
type: result.isError ? 'error-text' : 'text',
|
|
26
|
+
value: result.output,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toState(
|
|
31
|
+
result: Result,
|
|
32
|
+
input: { input: Record<string, any>; time: { start: number; end: number } }
|
|
33
|
+
): MessageV2.ToolStateCompleted | MessageV2.ToolStateError {
|
|
34
|
+
if (result.isError) {
|
|
35
|
+
return {
|
|
36
|
+
...input,
|
|
37
|
+
status: 'error',
|
|
38
|
+
error: result.output,
|
|
39
|
+
metadata: result.metadata,
|
|
40
|
+
title: result.title,
|
|
41
|
+
attachments: result.attachments,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
...input,
|
|
46
|
+
status: 'completed',
|
|
47
|
+
output: result.output,
|
|
48
|
+
title: result.title,
|
|
49
|
+
metadata: result.metadata,
|
|
50
|
+
attachments: result.attachments,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
9
54
|
export type Context<M extends Metadata = Metadata> = {
|
|
10
55
|
sessionID: string;
|
|
11
56
|
messageID: string;
|
|
@@ -23,15 +68,7 @@ export namespace Tool {
|
|
|
23
68
|
init: () => Promise<{
|
|
24
69
|
description: string;
|
|
25
70
|
parameters: Parameters;
|
|
26
|
-
execute(
|
|
27
|
-
args: z.infer<Parameters>,
|
|
28
|
-
ctx: Context
|
|
29
|
-
): Promise<{
|
|
30
|
-
title: string;
|
|
31
|
-
metadata: M;
|
|
32
|
-
output: string;
|
|
33
|
-
attachments?: MessageV2.FilePart[];
|
|
34
|
-
}>;
|
|
71
|
+
execute(args: z.infer<Parameters>, ctx: Context): Promise<Result<M>>;
|
|
35
72
|
formatValidationError?(error: z.ZodError): string;
|
|
36
73
|
}>;
|
|
37
74
|
}
|