@link-assistant/agent 0.6.3 → 0.8.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/package.json +9 -9
- package/src/auth/plugins.ts +538 -5
- package/src/cli/continuous-mode.js +188 -18
- package/src/cli/event-handler.js +99 -0
- package/src/config/config.ts +51 -0
- package/src/index.js +82 -157
- package/src/mcp/index.ts +125 -0
- package/src/provider/google-cloudcode.ts +384 -0
- package/src/session/message-v2.ts +30 -1
- package/src/session/processor.ts +21 -2
- package/src/session/prompt.ts +23 -1
- package/src/session/retry.ts +18 -0
- package/EXAMPLES.md +0 -462
- package/LICENSE +0 -24
- package/MODELS.md +0 -143
- package/README.md +0 -616
- package/TOOLS.md +0 -154
|
@@ -10,6 +10,7 @@ import { Session } from '../session/index.ts';
|
|
|
10
10
|
import { SessionPrompt } from '../session/prompt.ts';
|
|
11
11
|
import { createEventHandler } from '../json-standard/index.ts';
|
|
12
12
|
import { createContinuousStdinReader } from './input-queue.js';
|
|
13
|
+
import { Log } from '../util/log.ts';
|
|
13
14
|
|
|
14
15
|
// Shared error tracking
|
|
15
16
|
let hasError = false;
|
|
@@ -42,6 +43,155 @@ function outputStatus(status, compact = false) {
|
|
|
42
43
|
console.error(json);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// Logger for resume operations
|
|
47
|
+
const log = Log.create({ service: 'resume' });
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the session to use based on --resume, --continue, and --no-fork options.
|
|
51
|
+
* Returns the session ID to use, handling forking as needed.
|
|
52
|
+
* @param {object} argv - Command line arguments
|
|
53
|
+
* @param {boolean} compactJson - Whether to use compact JSON output
|
|
54
|
+
* @returns {Promise<{sessionID: string, wasResumed: boolean, wasForked: boolean} | null>} - Session info or null if new session should be created
|
|
55
|
+
* @export
|
|
56
|
+
*/
|
|
57
|
+
export async function resolveResumeSession(argv, compactJson) {
|
|
58
|
+
const resumeSessionID = argv.resume;
|
|
59
|
+
const shouldContinue = argv.continue === true;
|
|
60
|
+
const noFork = argv['no-fork'] === true;
|
|
61
|
+
|
|
62
|
+
// If neither --resume nor --continue is specified, return null to create new session
|
|
63
|
+
if (!resumeSessionID && !shouldContinue) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let targetSessionID = resumeSessionID;
|
|
68
|
+
|
|
69
|
+
// If --continue is specified, find the most recent session
|
|
70
|
+
if (shouldContinue && !targetSessionID) {
|
|
71
|
+
let mostRecentSession = null;
|
|
72
|
+
let mostRecentTime = 0;
|
|
73
|
+
|
|
74
|
+
for await (const session of Session.list()) {
|
|
75
|
+
// Skip child sessions (those with parentID) - find top-level sessions only
|
|
76
|
+
if (session.parentID) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (session.time.updated > mostRecentTime) {
|
|
81
|
+
mostRecentTime = session.time.updated;
|
|
82
|
+
mostRecentSession = session;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!mostRecentSession) {
|
|
87
|
+
outputStatus(
|
|
88
|
+
{
|
|
89
|
+
type: 'error',
|
|
90
|
+
errorType: 'SessionNotFound',
|
|
91
|
+
message:
|
|
92
|
+
'No existing sessions found to continue. Start a new session first.',
|
|
93
|
+
},
|
|
94
|
+
compactJson
|
|
95
|
+
);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
targetSessionID = mostRecentSession.id;
|
|
100
|
+
log.info(() => ({
|
|
101
|
+
message: 'Found most recent session to continue',
|
|
102
|
+
sessionID: targetSessionID,
|
|
103
|
+
title: mostRecentSession.title,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Verify the session exists
|
|
108
|
+
let existingSession;
|
|
109
|
+
try {
|
|
110
|
+
existingSession = await Session.get(targetSessionID);
|
|
111
|
+
} catch (_error) {
|
|
112
|
+
// Session.get throws an error when the session doesn't exist
|
|
113
|
+
outputStatus(
|
|
114
|
+
{
|
|
115
|
+
type: 'error',
|
|
116
|
+
errorType: 'SessionNotFound',
|
|
117
|
+
message: `Session not found: ${targetSessionID}`,
|
|
118
|
+
},
|
|
119
|
+
compactJson
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!existingSession) {
|
|
125
|
+
outputStatus(
|
|
126
|
+
{
|
|
127
|
+
type: 'error',
|
|
128
|
+
errorType: 'SessionNotFound',
|
|
129
|
+
message: `Session not found: ${targetSessionID}`,
|
|
130
|
+
},
|
|
131
|
+
compactJson
|
|
132
|
+
);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
log.info(() => ({
|
|
137
|
+
message: 'Resuming session',
|
|
138
|
+
sessionID: targetSessionID,
|
|
139
|
+
title: existingSession.title,
|
|
140
|
+
noFork,
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
// If --no-fork is specified, continue in the same session
|
|
144
|
+
if (noFork) {
|
|
145
|
+
outputStatus(
|
|
146
|
+
{
|
|
147
|
+
type: 'status',
|
|
148
|
+
mode: 'resume',
|
|
149
|
+
message: `Continuing session without forking: ${targetSessionID}`,
|
|
150
|
+
sessionID: targetSessionID,
|
|
151
|
+
title: existingSession.title,
|
|
152
|
+
forked: false,
|
|
153
|
+
},
|
|
154
|
+
compactJson
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
sessionID: targetSessionID,
|
|
159
|
+
wasResumed: true,
|
|
160
|
+
wasForked: false,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Fork the session to a new UUID (default behavior)
|
|
165
|
+
const forkedSession = await Session.fork({
|
|
166
|
+
sessionID: targetSessionID,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
outputStatus(
|
|
170
|
+
{
|
|
171
|
+
type: 'status',
|
|
172
|
+
mode: 'resume',
|
|
173
|
+
message: `Forked session ${targetSessionID} to new session: ${forkedSession.id}`,
|
|
174
|
+
originalSessionID: targetSessionID,
|
|
175
|
+
sessionID: forkedSession.id,
|
|
176
|
+
title: forkedSession.title,
|
|
177
|
+
forked: true,
|
|
178
|
+
},
|
|
179
|
+
compactJson
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
log.info(() => ({
|
|
183
|
+
message: 'Forked session',
|
|
184
|
+
originalSessionID: targetSessionID,
|
|
185
|
+
newSessionID: forkedSession.id,
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
sessionID: forkedSession.id,
|
|
190
|
+
wasResumed: true,
|
|
191
|
+
wasForked: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
45
195
|
/**
|
|
46
196
|
* Run server mode with continuous stdin input
|
|
47
197
|
* Keeps the session alive and processes messages as they arrive
|
|
@@ -64,20 +214,30 @@ export async function runContinuousServerMode(
|
|
|
64
214
|
let stdinReader = null;
|
|
65
215
|
|
|
66
216
|
try {
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
217
|
+
// Check if we should resume an existing session
|
|
218
|
+
const resumeInfo = await resolveResumeSession(argv, compactJson);
|
|
219
|
+
|
|
220
|
+
let sessionID;
|
|
221
|
+
|
|
222
|
+
if (resumeInfo) {
|
|
223
|
+
// Use the resumed/forked session
|
|
224
|
+
sessionID = resumeInfo.sessionID;
|
|
225
|
+
} else {
|
|
226
|
+
// Create a new session
|
|
227
|
+
const createRes = await fetch(
|
|
228
|
+
`http://${server.hostname}:${server.port}/session`,
|
|
229
|
+
{
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
body: JSON.stringify({}),
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
const session = await createRes.json();
|
|
236
|
+
sessionID = session.id;
|
|
78
237
|
|
|
79
|
-
|
|
80
|
-
|
|
238
|
+
if (!sessionID) {
|
|
239
|
+
throw new Error('Failed to create session');
|
|
240
|
+
}
|
|
81
241
|
}
|
|
82
242
|
|
|
83
243
|
// Create event handler for the selected JSON standard
|
|
@@ -275,11 +435,21 @@ export async function runContinuousDirectMode(
|
|
|
275
435
|
let stdinReader = null;
|
|
276
436
|
|
|
277
437
|
try {
|
|
278
|
-
//
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
438
|
+
// Check if we should resume an existing session
|
|
439
|
+
const resumeInfo = await resolveResumeSession(argv, compactJson);
|
|
440
|
+
|
|
441
|
+
let sessionID;
|
|
442
|
+
|
|
443
|
+
if (resumeInfo) {
|
|
444
|
+
// Use the resumed/forked session
|
|
445
|
+
sessionID = resumeInfo.sessionID;
|
|
446
|
+
} else {
|
|
447
|
+
// Create a new session directly
|
|
448
|
+
const session = await Session.createNext({
|
|
449
|
+
directory: process.cwd(),
|
|
450
|
+
});
|
|
451
|
+
sessionID = session.id;
|
|
452
|
+
}
|
|
283
453
|
|
|
284
454
|
// Create event handler for the selected JSON standard
|
|
285
455
|
const eventHandler = createEventHandler(jsonStandard, sessionID);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared event handler logic for bus events.
|
|
3
|
+
* Used by both single-message mode (index.js) and continuous mode (continuous-mode.js).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Bus } from '../bus/index.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a subscription for session bus events that outputs events in the selected JSON format.
|
|
10
|
+
* Returns an object with the unsubscribe function and a promise that resolves when session becomes idle.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} options - Configuration options
|
|
13
|
+
* @param {string} options.sessionID - The session ID to filter events
|
|
14
|
+
* @param {object} options.eventHandler - The event handler (from createEventHandler)
|
|
15
|
+
* @param {function} options.onError - Callback when error occurs (sets hasError flag)
|
|
16
|
+
* @returns {{ unsub: function, idlePromise: Promise<void> }}
|
|
17
|
+
*/
|
|
18
|
+
export function createBusEventSubscription({
|
|
19
|
+
sessionID,
|
|
20
|
+
eventHandler,
|
|
21
|
+
onError,
|
|
22
|
+
}) {
|
|
23
|
+
let idleResolve;
|
|
24
|
+
const idlePromise = new Promise((resolve) => {
|
|
25
|
+
idleResolve = resolve;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const unsub = Bus.subscribeAll((event) => {
|
|
29
|
+
// Output events in selected JSON format
|
|
30
|
+
if (event.type === 'message.part.updated') {
|
|
31
|
+
const part = event.properties.part;
|
|
32
|
+
if (part.sessionID !== sessionID) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Output different event types
|
|
37
|
+
if (part.type === 'step-start') {
|
|
38
|
+
eventHandler.output({
|
|
39
|
+
type: 'step_start',
|
|
40
|
+
timestamp: Date.now(),
|
|
41
|
+
sessionID,
|
|
42
|
+
part,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (part.type === 'step-finish') {
|
|
47
|
+
eventHandler.output({
|
|
48
|
+
type: 'step_finish',
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
sessionID,
|
|
51
|
+
part,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (part.type === 'text' && part.time?.end) {
|
|
56
|
+
eventHandler.output({
|
|
57
|
+
type: 'text',
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
sessionID,
|
|
60
|
+
part,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (part.type === 'tool' && part.state.status === 'completed') {
|
|
65
|
+
eventHandler.output({
|
|
66
|
+
type: 'tool_use',
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
sessionID,
|
|
69
|
+
part,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle session idle to know when to stop
|
|
75
|
+
if (
|
|
76
|
+
event.type === 'session.idle' &&
|
|
77
|
+
event.properties.sessionID === sessionID
|
|
78
|
+
) {
|
|
79
|
+
idleResolve();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle errors
|
|
83
|
+
if (event.type === 'session.error') {
|
|
84
|
+
const props = event.properties;
|
|
85
|
+
if (props.sessionID !== sessionID || !props.error) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
onError();
|
|
89
|
+
eventHandler.output({
|
|
90
|
+
type: 'error',
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
sessionID,
|
|
93
|
+
error: props.error,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return { unsub, idlePromise };
|
|
99
|
+
}
|
package/src/config/config.ts
CHANGED
|
@@ -432,6 +432,20 @@ export namespace Config {
|
|
|
432
432
|
.describe(
|
|
433
433
|
'Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.'
|
|
434
434
|
),
|
|
435
|
+
tool_call_timeout: z
|
|
436
|
+
.number()
|
|
437
|
+
.int()
|
|
438
|
+
.positive()
|
|
439
|
+
.optional()
|
|
440
|
+
.describe(
|
|
441
|
+
'Default timeout in ms for MCP tool execution. Defaults to 120000 (2 minutes) if not specified. Set per-tool overrides in tool_timeouts.'
|
|
442
|
+
),
|
|
443
|
+
tool_timeouts: z
|
|
444
|
+
.record(z.string(), z.number().int().positive())
|
|
445
|
+
.optional()
|
|
446
|
+
.describe(
|
|
447
|
+
'Per-tool timeout overrides in ms. Keys are tool names (e.g., "browser_run_code": 300000 for 5 minutes).'
|
|
448
|
+
),
|
|
435
449
|
})
|
|
436
450
|
.strict()
|
|
437
451
|
.meta({
|
|
@@ -458,6 +472,20 @@ export namespace Config {
|
|
|
458
472
|
.describe(
|
|
459
473
|
'Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.'
|
|
460
474
|
),
|
|
475
|
+
tool_call_timeout: z
|
|
476
|
+
.number()
|
|
477
|
+
.int()
|
|
478
|
+
.positive()
|
|
479
|
+
.optional()
|
|
480
|
+
.describe(
|
|
481
|
+
'Default timeout in ms for MCP tool execution. Defaults to 120000 (2 minutes) if not specified. Set per-tool overrides in tool_timeouts.'
|
|
482
|
+
),
|
|
483
|
+
tool_timeouts: z
|
|
484
|
+
.record(z.string(), z.number().int().positive())
|
|
485
|
+
.optional()
|
|
486
|
+
.describe(
|
|
487
|
+
'Per-tool timeout overrides in ms. Keys are tool names (e.g., "browser_run_code": 300000 for 5 minutes).'
|
|
488
|
+
),
|
|
461
489
|
})
|
|
462
490
|
.strict()
|
|
463
491
|
.meta({
|
|
@@ -846,6 +874,29 @@ export namespace Config {
|
|
|
846
874
|
.record(z.string(), Mcp)
|
|
847
875
|
.optional()
|
|
848
876
|
.describe('MCP (Model Context Protocol) server configurations'),
|
|
877
|
+
mcp_defaults: z
|
|
878
|
+
.object({
|
|
879
|
+
tool_call_timeout: z
|
|
880
|
+
.number()
|
|
881
|
+
.int()
|
|
882
|
+
.positive()
|
|
883
|
+
.optional()
|
|
884
|
+
.describe(
|
|
885
|
+
'Global default timeout in ms for MCP tool execution. Defaults to 120000 (2 minutes). Can be overridden per-server or per-tool.'
|
|
886
|
+
),
|
|
887
|
+
max_tool_call_timeout: z
|
|
888
|
+
.number()
|
|
889
|
+
.int()
|
|
890
|
+
.positive()
|
|
891
|
+
.optional()
|
|
892
|
+
.describe(
|
|
893
|
+
'Maximum allowed timeout in ms for MCP tool execution. Defaults to 600000 (10 minutes). Tool timeouts will be capped at this value.'
|
|
894
|
+
),
|
|
895
|
+
})
|
|
896
|
+
.optional()
|
|
897
|
+
.describe(
|
|
898
|
+
'Global default settings for MCP tool call timeouts. Can be overridden at the server level.'
|
|
899
|
+
),
|
|
849
900
|
formatter: z
|
|
850
901
|
.union([
|
|
851
902
|
z.literal(false),
|