@link-assistant/agent 0.16.4 → 0.16.5
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/cli/model-config.js +147 -0
- package/src/index.js +11 -103
- package/src/provider/provider.ts +35 -7
- package/src/session/processor.ts +20 -0
- package/src/session/prompt.ts +59 -2
package/package.json
CHANGED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { getModelFromProcessArgv } from './argv.ts';
|
|
2
|
+
import { Log } from '../util/log.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse model config from argv. Supports "provider/model" or short "model" format.
|
|
6
|
+
* @param {object} argv - Parsed command line arguments
|
|
7
|
+
* @param {function} outputError - Function to output error messages
|
|
8
|
+
* @param {function} outputStatus - Function to output status messages
|
|
9
|
+
* @returns {Promise<{providerID: string, modelID: string}>}
|
|
10
|
+
*/
|
|
11
|
+
export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
12
|
+
// Safeguard: validate argv.model against process.argv to detect yargs/cache mismatch (#192, #196)
|
|
13
|
+
// This is critical because yargs under Bun may fail to parse --model correctly,
|
|
14
|
+
// returning the default value instead of the user's CLI argument.
|
|
15
|
+
const cliModelArg = getModelFromProcessArgv();
|
|
16
|
+
let modelArg = argv.model;
|
|
17
|
+
|
|
18
|
+
// ALWAYS prefer the CLI value over yargs when available (#196)
|
|
19
|
+
// The yargs default 'opencode/kimi-k2.5-free' can silently override user's --model argument
|
|
20
|
+
if (cliModelArg) {
|
|
21
|
+
if (cliModelArg !== modelArg) {
|
|
22
|
+
Log.Default.warn(() => ({
|
|
23
|
+
message: 'model argument mismatch detected - using CLI value',
|
|
24
|
+
yargsModel: modelArg,
|
|
25
|
+
cliModel: cliModelArg,
|
|
26
|
+
processArgv: process.argv.join(' '),
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
// Always use CLI value when available, even if it matches yargs
|
|
30
|
+
// This ensures we use the actual CLI argument, not a cached/default yargs value
|
|
31
|
+
modelArg = cliModelArg;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let providerID;
|
|
35
|
+
let modelID;
|
|
36
|
+
|
|
37
|
+
// Check if model includes explicit provider prefix
|
|
38
|
+
if (modelArg.includes('/')) {
|
|
39
|
+
// Explicit provider/model format - respect user's choice
|
|
40
|
+
const modelParts = modelArg.split('/');
|
|
41
|
+
providerID = modelParts[0];
|
|
42
|
+
modelID = modelParts.slice(1).join('/');
|
|
43
|
+
|
|
44
|
+
// Validate that providerID and modelID are not empty
|
|
45
|
+
// Do NOT fall back to defaults - if the user provided an invalid format, fail clearly (#196)
|
|
46
|
+
if (!providerID || !modelID) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Invalid model format: "${modelArg}". Expected "provider/model" format (e.g., "opencode/kimi-k2.5-free"). ` +
|
|
49
|
+
`Provider: "${providerID || '(empty)'}", Model: "${modelID || '(empty)'}".`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Log raw and parsed values to help diagnose model routing issues (#171)
|
|
54
|
+
Log.Default.info(() => ({
|
|
55
|
+
message: 'using explicit provider/model',
|
|
56
|
+
rawModel: modelArg,
|
|
57
|
+
providerID,
|
|
58
|
+
modelID,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// Validate that the model exists in the provider (#196)
|
|
62
|
+
// Without this check, a non-existent model silently proceeds and fails at API call time
|
|
63
|
+
// with confusing "reason: unknown" and zero tokens
|
|
64
|
+
try {
|
|
65
|
+
const { Provider } = await import('../provider/provider.ts');
|
|
66
|
+
const s = await Provider.state();
|
|
67
|
+
const provider = s.providers[providerID];
|
|
68
|
+
if (provider && !provider.info.models[modelID]) {
|
|
69
|
+
// Provider exists but model doesn't - warn and suggest alternatives
|
|
70
|
+
const availableModels = Object.keys(provider.info.models).slice(0, 5);
|
|
71
|
+
Log.Default.warn(() => ({
|
|
72
|
+
message:
|
|
73
|
+
'model not found in provider - will attempt anyway (provider may support unlisted models)',
|
|
74
|
+
providerID,
|
|
75
|
+
modelID,
|
|
76
|
+
availableModels,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
} catch (validationError) {
|
|
80
|
+
// Don't fail on validation errors - the model may still work
|
|
81
|
+
// This is a best-effort check
|
|
82
|
+
Log.Default.info(() => ({
|
|
83
|
+
message: 'skipping model existence validation',
|
|
84
|
+
reason: validationError?.message,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
// Short model name - resolve to appropriate provider
|
|
89
|
+
// Import Provider to use parseModelWithResolution
|
|
90
|
+
const { Provider } = await import('../provider/provider.ts');
|
|
91
|
+
const resolved = await Provider.parseModelWithResolution(modelArg);
|
|
92
|
+
providerID = resolved.providerID;
|
|
93
|
+
modelID = resolved.modelID;
|
|
94
|
+
|
|
95
|
+
Log.Default.info(() => ({
|
|
96
|
+
message: 'resolved short model name',
|
|
97
|
+
input: modelArg,
|
|
98
|
+
providerID,
|
|
99
|
+
modelID,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle --use-existing-claude-oauth option
|
|
104
|
+
// This reads OAuth credentials from ~/.claude/.credentials.json (Claude Code CLI)
|
|
105
|
+
// For new authentication, use: agent auth login (select Anthropic > Claude Pro/Max)
|
|
106
|
+
if (argv['use-existing-claude-oauth']) {
|
|
107
|
+
// Import ClaudeOAuth to check for credentials from Claude Code CLI
|
|
108
|
+
const { ClaudeOAuth } = await import('../auth/claude-oauth.ts');
|
|
109
|
+
const creds = await ClaudeOAuth.getCredentials();
|
|
110
|
+
|
|
111
|
+
if (!creds?.accessToken) {
|
|
112
|
+
const compactJson = argv['compact-json'] === true;
|
|
113
|
+
outputError(
|
|
114
|
+
{
|
|
115
|
+
errorType: 'AuthenticationError',
|
|
116
|
+
message:
|
|
117
|
+
'No Claude OAuth credentials found in ~/.claude/.credentials.json. Either authenticate with Claude Code CLI first, or use: agent auth login (select Anthropic > Claude Pro/Max)',
|
|
118
|
+
},
|
|
119
|
+
compactJson
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Set environment variable for the provider to use
|
|
125
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = creds.accessToken;
|
|
126
|
+
|
|
127
|
+
// If user specified the default model (opencode/kimi-k2.5-free), switch to claude-oauth
|
|
128
|
+
// If user explicitly specified kilo or another provider, warn but respect their choice
|
|
129
|
+
if (providerID === 'opencode' && modelID === 'kimi-k2.5-free') {
|
|
130
|
+
providerID = 'claude-oauth';
|
|
131
|
+
modelID = 'claude-sonnet-4-5';
|
|
132
|
+
} else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
|
|
133
|
+
// If user specified a different provider explicitly, warn them
|
|
134
|
+
const compactJson = argv['compact-json'] === true;
|
|
135
|
+
outputStatus(
|
|
136
|
+
{
|
|
137
|
+
type: 'warning',
|
|
138
|
+
message: `--use-existing-claude-oauth is set but model uses provider "${providerID}". Using specified provider.`,
|
|
139
|
+
},
|
|
140
|
+
compactJson
|
|
141
|
+
);
|
|
142
|
+
// Don't override - respect user's explicit provider choice
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { providerID, modelID };
|
|
147
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ setProcessName('agent');
|
|
|
5
5
|
import { Server } from './server/server.ts';
|
|
6
6
|
import { Instance } from './project/instance.ts';
|
|
7
7
|
import { Log } from './util/log.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { parseModelConfig } from './cli/model-config.js';
|
|
9
9
|
// Bus is used via createBusEventSubscription in event-handler.js
|
|
10
10
|
import { Session } from './session/index.ts';
|
|
11
11
|
import { SessionPrompt } from './session/prompt.ts';
|
|
@@ -135,106 +135,6 @@ function readStdinWithTimeout(timeout = null) {
|
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
/** Parse model config from argv. Supports "provider/model" or short "model" format. */
|
|
139
|
-
async function parseModelConfig(argv) {
|
|
140
|
-
// Safeguard: validate argv.model against process.argv to detect yargs/cache mismatch (#192)
|
|
141
|
-
const cliModelArg = getModelFromProcessArgv();
|
|
142
|
-
let modelArg = argv.model;
|
|
143
|
-
if (cliModelArg && cliModelArg !== modelArg) {
|
|
144
|
-
Log.Default.warn(() => ({
|
|
145
|
-
message: 'model argument mismatch detected - using CLI value',
|
|
146
|
-
yargsModel: modelArg,
|
|
147
|
-
cliModel: cliModelArg,
|
|
148
|
-
processArgv: process.argv.join(' '),
|
|
149
|
-
}));
|
|
150
|
-
modelArg = cliModelArg;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let providerID;
|
|
154
|
-
let modelID;
|
|
155
|
-
|
|
156
|
-
// Check if model includes explicit provider prefix
|
|
157
|
-
if (modelArg.includes('/')) {
|
|
158
|
-
// Explicit provider/model format - respect user's choice
|
|
159
|
-
const modelParts = modelArg.split('/');
|
|
160
|
-
providerID = modelParts[0];
|
|
161
|
-
modelID = modelParts.slice(1).join('/');
|
|
162
|
-
|
|
163
|
-
// Validate that providerID and modelID are not empty
|
|
164
|
-
if (!providerID || !modelID) {
|
|
165
|
-
providerID = providerID || 'opencode';
|
|
166
|
-
modelID = modelID || 'kimi-k2.5-free';
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Log raw and parsed values to help diagnose model routing issues (#171)
|
|
170
|
-
Log.Default.info(() => ({
|
|
171
|
-
message: 'using explicit provider/model',
|
|
172
|
-
rawModel: modelArg,
|
|
173
|
-
providerID,
|
|
174
|
-
modelID,
|
|
175
|
-
}));
|
|
176
|
-
} else {
|
|
177
|
-
// Short model name - resolve to appropriate provider
|
|
178
|
-
// Import Provider to use parseModelWithResolution
|
|
179
|
-
const { Provider } = await import('./provider/provider.ts');
|
|
180
|
-
const resolved = await Provider.parseModelWithResolution(modelArg);
|
|
181
|
-
providerID = resolved.providerID;
|
|
182
|
-
modelID = resolved.modelID;
|
|
183
|
-
|
|
184
|
-
Log.Default.info(() => ({
|
|
185
|
-
message: 'resolved short model name',
|
|
186
|
-
input: modelArg,
|
|
187
|
-
providerID,
|
|
188
|
-
modelID,
|
|
189
|
-
}));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Handle --use-existing-claude-oauth option
|
|
193
|
-
// This reads OAuth credentials from ~/.claude/.credentials.json (Claude Code CLI)
|
|
194
|
-
// For new authentication, use: agent auth login (select Anthropic > Claude Pro/Max)
|
|
195
|
-
if (argv['use-existing-claude-oauth']) {
|
|
196
|
-
// Import ClaudeOAuth to check for credentials from Claude Code CLI
|
|
197
|
-
const { ClaudeOAuth } = await import('./auth/claude-oauth.ts');
|
|
198
|
-
const creds = await ClaudeOAuth.getCredentials();
|
|
199
|
-
|
|
200
|
-
if (!creds?.accessToken) {
|
|
201
|
-
const compactJson = argv['compact-json'] === true;
|
|
202
|
-
outputError(
|
|
203
|
-
{
|
|
204
|
-
errorType: 'AuthenticationError',
|
|
205
|
-
message:
|
|
206
|
-
'No Claude OAuth credentials found in ~/.claude/.credentials.json. Either authenticate with Claude Code CLI first, or use: agent auth login (select Anthropic > Claude Pro/Max)',
|
|
207
|
-
},
|
|
208
|
-
compactJson
|
|
209
|
-
);
|
|
210
|
-
process.exit(1);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Set environment variable for the provider to use
|
|
214
|
-
process.env.CLAUDE_CODE_OAUTH_TOKEN = creds.accessToken;
|
|
215
|
-
|
|
216
|
-
// If user specified the default model (opencode/kimi-k2.5-free), switch to claude-oauth
|
|
217
|
-
// If user explicitly specified kilo or another provider, warn but respect their choice
|
|
218
|
-
if (providerID === 'opencode' && modelID === 'kimi-k2.5-free') {
|
|
219
|
-
providerID = 'claude-oauth';
|
|
220
|
-
modelID = 'claude-sonnet-4-5';
|
|
221
|
-
} else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
|
|
222
|
-
// If user specified a different provider explicitly, warn them
|
|
223
|
-
const compactJson = argv['compact-json'] === true;
|
|
224
|
-
outputStatus(
|
|
225
|
-
{
|
|
226
|
-
type: 'warning',
|
|
227
|
-
message: `--use-existing-claude-oauth is set but model uses provider "${providerID}". Using specified provider.`,
|
|
228
|
-
},
|
|
229
|
-
compactJson
|
|
230
|
-
);
|
|
231
|
-
// Don't override - respect user's explicit provider choice
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return { providerID, modelID };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
138
|
/**
|
|
239
139
|
* Read system message from files if specified
|
|
240
140
|
* @param {object} argv - Command line arguments
|
|
@@ -317,7 +217,11 @@ async function runAgentMode(argv, request) {
|
|
|
317
217
|
fn: async () => {
|
|
318
218
|
// Parse model config inside Instance.provide context
|
|
319
219
|
// This allows parseModelWithResolution to access the provider state
|
|
320
|
-
const { providerID, modelID } = await parseModelConfig(
|
|
220
|
+
const { providerID, modelID } = await parseModelConfig(
|
|
221
|
+
argv,
|
|
222
|
+
outputError,
|
|
223
|
+
outputStatus
|
|
224
|
+
);
|
|
321
225
|
|
|
322
226
|
if (argv.server) {
|
|
323
227
|
// SERVER MODE: Start server and communicate via HTTP
|
|
@@ -393,7 +297,11 @@ async function runContinuousAgentMode(argv) {
|
|
|
393
297
|
fn: async () => {
|
|
394
298
|
// Parse model config inside Instance.provide context
|
|
395
299
|
// This allows parseModelWithResolution to access the provider state
|
|
396
|
-
const { providerID, modelID } = await parseModelConfig(
|
|
300
|
+
const { providerID, modelID } = await parseModelConfig(
|
|
301
|
+
argv,
|
|
302
|
+
outputError,
|
|
303
|
+
outputStatus
|
|
304
|
+
);
|
|
397
305
|
|
|
398
306
|
if (argv.server) {
|
|
399
307
|
// SERVER MODE: Start server and communicate via HTTP
|
package/src/provider/provider.ts
CHANGED
|
@@ -1593,13 +1593,20 @@ export namespace Provider {
|
|
|
1593
1593
|
* Parse a model string that may or may not include a provider prefix.
|
|
1594
1594
|
* If no provider is specified, attempts to resolve the short model name to the appropriate provider.
|
|
1595
1595
|
*
|
|
1596
|
+
* IMPORTANT: If the model cannot be resolved, this function throws an error instead of
|
|
1597
|
+
* silently falling back to a default. This ensures the user gets their requested model
|
|
1598
|
+
* or a clear error explaining why it's not available.
|
|
1599
|
+
*
|
|
1596
1600
|
* Examples:
|
|
1597
1601
|
* - "kilo/glm-5-free" -> { providerID: "kilo", modelID: "glm-5-free" }
|
|
1598
1602
|
* - "glm-5-free" -> { providerID: "kilo", modelID: "glm-5-free" } (resolved)
|
|
1599
1603
|
* - "kimi-k2.5-free" -> { providerID: "opencode", modelID: "kimi-k2.5-free" } (resolved)
|
|
1604
|
+
* - "nonexistent-model" -> throws ModelNotFoundError
|
|
1600
1605
|
*
|
|
1601
1606
|
* @param model - Model string with or without provider prefix
|
|
1602
1607
|
* @returns Parsed provider ID and model ID
|
|
1608
|
+
* @throws ModelNotFoundError if the model cannot be found in any provider
|
|
1609
|
+
* @see https://github.com/link-assistant/agent/issues/194
|
|
1603
1610
|
*/
|
|
1604
1611
|
export async function parseModelWithResolution(
|
|
1605
1612
|
model: string
|
|
@@ -1616,15 +1623,36 @@ export namespace Provider {
|
|
|
1616
1623
|
return resolved;
|
|
1617
1624
|
}
|
|
1618
1625
|
|
|
1619
|
-
// Unable to resolve -
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1626
|
+
// Unable to resolve - fail with a clear error instead of silently falling back
|
|
1627
|
+
// This prevents the issue where user requests model X but gets model Y
|
|
1628
|
+
// See: https://github.com/link-assistant/agent/issues/194
|
|
1629
|
+
const s = await state();
|
|
1630
|
+
const availableModels: string[] = [];
|
|
1631
|
+
|
|
1632
|
+
// Collect some available models to suggest
|
|
1633
|
+
for (const [providerID, provider] of Object.entries(s.providers)) {
|
|
1634
|
+
for (const modelID of Object.keys(provider.info.models).slice(0, 3)) {
|
|
1635
|
+
availableModels.push(`${providerID}/${modelID}`);
|
|
1636
|
+
}
|
|
1637
|
+
if (availableModels.length >= 6) break;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const suggestion =
|
|
1641
|
+
availableModels.length > 0
|
|
1642
|
+
? `Model "${model}" not found in any provider. Available models include: ${availableModels.join(', ')}. Use --model provider/model-id format for explicit selection.`
|
|
1643
|
+
: `Model "${model}" not found. No providers are currently available. Check your API keys or authentication.`;
|
|
1644
|
+
|
|
1645
|
+
log.error(() => ({
|
|
1646
|
+
message: 'model not found - refusing to silently fallback',
|
|
1647
|
+
requestedModel: model,
|
|
1648
|
+
availableModels,
|
|
1623
1649
|
}));
|
|
1624
|
-
|
|
1625
|
-
|
|
1650
|
+
|
|
1651
|
+
throw new ModelNotFoundError({
|
|
1652
|
+
providerID: 'unknown',
|
|
1626
1653
|
modelID: model,
|
|
1627
|
-
|
|
1654
|
+
suggestion,
|
|
1655
|
+
});
|
|
1628
1656
|
}
|
|
1629
1657
|
|
|
1630
1658
|
/**
|
package/src/session/processor.ts
CHANGED
|
@@ -273,6 +273,26 @@ export namespace SessionProcessor {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
|
+
|
|
277
|
+
// CRITICAL FIX for issue #194: Infer finish reason from tool calls
|
|
278
|
+
// If finishReason is still undefined but we have pending tool calls,
|
|
279
|
+
// the model intended to make tool calls - infer 'tool-calls' as the reason.
|
|
280
|
+
// This prevents premature loop exit when providers don't return finishReason.
|
|
281
|
+
// See: https://github.com/link-assistant/agent/issues/194
|
|
282
|
+
if (rawFinishReason === undefined) {
|
|
283
|
+
const pendingToolCallCount = Object.keys(toolcalls).length;
|
|
284
|
+
if (pendingToolCallCount > 0) {
|
|
285
|
+
log.info(() => ({
|
|
286
|
+
message:
|
|
287
|
+
'inferred tool-calls finish reason from pending tool calls',
|
|
288
|
+
pendingToolCallCount,
|
|
289
|
+
providerID: input.providerID,
|
|
290
|
+
hint: 'Provider returned undefined finishReason but made tool calls',
|
|
291
|
+
}));
|
|
292
|
+
rawFinishReason = 'tool-calls';
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
276
296
|
const finishReason = Session.toFinishReason(rawFinishReason);
|
|
277
297
|
input.assistantMessage.finish = finishReason;
|
|
278
298
|
input.assistantMessage.cost += usage.cost;
|
package/src/session/prompt.ts
CHANGED
|
@@ -272,13 +272,70 @@ export namespace SessionPrompt {
|
|
|
272
272
|
throw new Error(
|
|
273
273
|
'No user message found in stream. This should never happen.'
|
|
274
274
|
);
|
|
275
|
+
|
|
276
|
+
// Check if we should exit the agentic loop
|
|
277
|
+
// We exit when: the assistant has a finish reason, it's not 'tool-calls',
|
|
278
|
+
// and the assistant message is newer than the last user message
|
|
275
279
|
if (
|
|
276
280
|
lastAssistant?.finish &&
|
|
277
281
|
lastAssistant.finish !== 'tool-calls' &&
|
|
278
282
|
lastUser.id < lastAssistant.id
|
|
279
283
|
) {
|
|
280
|
-
|
|
281
|
-
|
|
284
|
+
// SAFETY CHECK for issue #194: If finish reason is 'unknown', check for tool calls
|
|
285
|
+
// Some providers (e.g., Kimi K2.5 via OpenCode) return undefined finishReason
|
|
286
|
+
// which gets converted to 'unknown'. If there were tool calls made, we should
|
|
287
|
+
// continue the loop to execute them instead of prematurely exiting.
|
|
288
|
+
// See: https://github.com/link-assistant/agent/issues/194
|
|
289
|
+
if (lastAssistant.finish === 'unknown') {
|
|
290
|
+
// SAFETY CHECK for issue #196: Detect zero-token responses as provider failures
|
|
291
|
+
// When all tokens are 0 and finish reason is 'unknown', this indicates the provider
|
|
292
|
+
// returned an empty/error response (e.g., rate limit, model unavailable, API failure).
|
|
293
|
+
// Log a clear error message so the problem is visible in logs.
|
|
294
|
+
const tokens = lastAssistant.tokens;
|
|
295
|
+
if (
|
|
296
|
+
tokens.input === 0 &&
|
|
297
|
+
tokens.output === 0 &&
|
|
298
|
+
tokens.reasoning === 0
|
|
299
|
+
) {
|
|
300
|
+
log.error(() => ({
|
|
301
|
+
message:
|
|
302
|
+
'provider returned zero tokens with unknown finish reason - possible API failure',
|
|
303
|
+
sessionID,
|
|
304
|
+
finishReason: lastAssistant.finish,
|
|
305
|
+
tokens,
|
|
306
|
+
cost: lastAssistant.cost,
|
|
307
|
+
model: lastAssistant.model,
|
|
308
|
+
hint: 'This usually indicates the provider failed to process the request. Check provider status, model availability, and API keys.',
|
|
309
|
+
issue: 'https://github.com/link-assistant/agent/issues/196',
|
|
310
|
+
}));
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const lastAssistantParts = msgs.find(
|
|
315
|
+
(m) => m.info.id === lastAssistant.id
|
|
316
|
+
)?.parts;
|
|
317
|
+
const hasToolCalls = lastAssistantParts?.some(
|
|
318
|
+
(p) =>
|
|
319
|
+
p.type === 'tool' &&
|
|
320
|
+
(p.state.status === 'completed' || p.state.status === 'running')
|
|
321
|
+
);
|
|
322
|
+
if (hasToolCalls) {
|
|
323
|
+
log.info(() => ({
|
|
324
|
+
message:
|
|
325
|
+
'continuing loop despite unknown finish reason - tool calls detected',
|
|
326
|
+
sessionID,
|
|
327
|
+
finishReason: lastAssistant.finish,
|
|
328
|
+
hint: 'Provider returned undefined finishReason but made tool calls',
|
|
329
|
+
}));
|
|
330
|
+
// Don't break - continue the loop to handle tool call results
|
|
331
|
+
} else {
|
|
332
|
+
log.info(() => ({ message: 'exiting loop', sessionID }));
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
log.info(() => ({ message: 'exiting loop', sessionID }));
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
282
339
|
}
|
|
283
340
|
|
|
284
341
|
step++;
|