@ottocode/server 0.1.228 → 0.1.231
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 +3 -3
- package/src/index.ts +1 -2
- package/src/openapi/paths/ask.ts +11 -0
- package/src/openapi/paths/config.ts +17 -0
- package/src/openapi/paths/messages.ts +6 -0
- package/src/openapi/schemas.ts +6 -0
- package/src/routes/ask.ts +8 -0
- package/src/routes/config/defaults.ts +13 -1
- package/src/routes/config/main.ts +7 -0
- package/src/routes/config/models.ts +2 -0
- package/src/routes/mcp.ts +62 -58
- package/src/routes/session-messages.ts +6 -1
- package/src/routes/session-stream.ts +46 -45
- package/src/routes/sessions.ts +4 -1
- package/src/routes/terminals.ts +15 -3
- package/src/routes/tunnel.ts +7 -3
- package/src/runtime/agent/runner-setup.ts +43 -34
- package/src/runtime/agent/runner.ts +171 -8
- package/src/runtime/ask/service.ts +16 -0
- package/src/runtime/debug/turn-dump.ts +330 -0
- package/src/runtime/message/history-builder.ts +99 -91
- package/src/runtime/message/service.ts +16 -2
- package/src/runtime/prompt/builder.ts +8 -6
- package/src/runtime/provider/reasoning.ts +291 -0
- package/src/runtime/session/queue.ts +2 -0
- package/src/runtime/tools/guards.ts +52 -4
- package/src/tools/adapter.ts +87 -8
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import {
|
|
2
|
+
catalog,
|
|
3
|
+
getModelNpmBinding,
|
|
4
|
+
getUnderlyingProviderKey,
|
|
5
|
+
modelSupportsReasoning,
|
|
6
|
+
type ProviderId,
|
|
7
|
+
type ReasoningLevel,
|
|
8
|
+
} from '@ottocode/sdk';
|
|
9
|
+
|
|
10
|
+
const THINKING_BUDGET = 16000;
|
|
11
|
+
|
|
12
|
+
export type ReasoningConfigResult = {
|
|
13
|
+
providerOptions: Record<string, unknown>;
|
|
14
|
+
effectiveMaxOutputTokens: number | undefined;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function normalizeReasoningLevel(
|
|
19
|
+
level: ReasoningLevel | undefined,
|
|
20
|
+
): Exclude<ReasoningLevel, 'xhigh'> {
|
|
21
|
+
if (!level) return 'high';
|
|
22
|
+
if (level === 'xhigh') return 'high';
|
|
23
|
+
return level;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toAnthropicEffort(
|
|
27
|
+
level: ReasoningLevel | undefined,
|
|
28
|
+
): 'low' | 'medium' | 'high' | 'max' {
|
|
29
|
+
switch (level) {
|
|
30
|
+
case 'minimal':
|
|
31
|
+
case 'low':
|
|
32
|
+
return 'low';
|
|
33
|
+
case 'medium':
|
|
34
|
+
return 'medium';
|
|
35
|
+
case 'max':
|
|
36
|
+
case 'xhigh':
|
|
37
|
+
return 'max';
|
|
38
|
+
case 'high':
|
|
39
|
+
default:
|
|
40
|
+
return 'high';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toOpenAIEffort(
|
|
45
|
+
level: ReasoningLevel | undefined,
|
|
46
|
+
): 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' {
|
|
47
|
+
switch (level) {
|
|
48
|
+
case 'minimal':
|
|
49
|
+
return 'minimal';
|
|
50
|
+
case 'low':
|
|
51
|
+
return 'low';
|
|
52
|
+
case 'medium':
|
|
53
|
+
return 'medium';
|
|
54
|
+
case 'max':
|
|
55
|
+
case 'xhigh':
|
|
56
|
+
return 'xhigh';
|
|
57
|
+
case 'high':
|
|
58
|
+
default:
|
|
59
|
+
return 'high';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toGoogleThinkingLevel(
|
|
64
|
+
level: ReasoningLevel | undefined,
|
|
65
|
+
): 'minimal' | 'low' | 'medium' | 'high' {
|
|
66
|
+
switch (level) {
|
|
67
|
+
case 'minimal':
|
|
68
|
+
return 'minimal';
|
|
69
|
+
case 'low':
|
|
70
|
+
return 'low';
|
|
71
|
+
case 'medium':
|
|
72
|
+
return 'medium';
|
|
73
|
+
case 'max':
|
|
74
|
+
case 'xhigh':
|
|
75
|
+
case 'high':
|
|
76
|
+
default:
|
|
77
|
+
return 'high';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toThinkingBudget(
|
|
82
|
+
level: ReasoningLevel | undefined,
|
|
83
|
+
maxOutputTokens: number | undefined,
|
|
84
|
+
): number {
|
|
85
|
+
const cap = maxOutputTokens
|
|
86
|
+
? Math.max(maxOutputTokens, THINKING_BUDGET)
|
|
87
|
+
: THINKING_BUDGET;
|
|
88
|
+
switch (level) {
|
|
89
|
+
case 'minimal':
|
|
90
|
+
return Math.min(2048, cap);
|
|
91
|
+
case 'low':
|
|
92
|
+
return Math.min(4096, cap);
|
|
93
|
+
case 'medium':
|
|
94
|
+
return Math.min(8192, cap);
|
|
95
|
+
case 'max':
|
|
96
|
+
case 'xhigh':
|
|
97
|
+
return Math.min(24000, cap);
|
|
98
|
+
case 'high':
|
|
99
|
+
default:
|
|
100
|
+
return Math.min(16000, cap);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toCamelCaseKey(value: string): string {
|
|
105
|
+
return value
|
|
106
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
107
|
+
.trim()
|
|
108
|
+
.split(/\s+/)
|
|
109
|
+
.map((segment, index) => {
|
|
110
|
+
const lower = segment.toLowerCase();
|
|
111
|
+
if (index === 0) return lower;
|
|
112
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
113
|
+
})
|
|
114
|
+
.join('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getOpenAICompatibleProviderOptionKeys(provider: ProviderId): string[] {
|
|
118
|
+
const entry = catalog[provider];
|
|
119
|
+
const keys = new Set<string>(['openaiCompatible', toCamelCaseKey(provider)]);
|
|
120
|
+
if (entry?.label) {
|
|
121
|
+
keys.add(toCamelCaseKey(entry.label));
|
|
122
|
+
}
|
|
123
|
+
return Array.from(keys).filter(Boolean);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildSharedProviderOptions(
|
|
127
|
+
provider: ProviderId,
|
|
128
|
+
options: Record<string, unknown>,
|
|
129
|
+
): Record<string, unknown> {
|
|
130
|
+
const keys = getOpenAICompatibleProviderOptionKeys(provider);
|
|
131
|
+
return Object.fromEntries(keys.map((key) => [key, options]));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function usesAdaptiveAnthropicThinking(model: string): boolean {
|
|
135
|
+
const lower = model.toLowerCase();
|
|
136
|
+
return (
|
|
137
|
+
lower.includes('claude-opus-4-6') ||
|
|
138
|
+
lower.includes('claude-opus-4.6') ||
|
|
139
|
+
lower.includes('claude-sonnet-4-6') ||
|
|
140
|
+
lower.includes('claude-sonnet-4.6')
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getReasoningProviderTarget(
|
|
145
|
+
provider: ProviderId,
|
|
146
|
+
model: string,
|
|
147
|
+
):
|
|
148
|
+
| 'anthropic'
|
|
149
|
+
| 'openai'
|
|
150
|
+
| 'google'
|
|
151
|
+
| 'openai-compatible'
|
|
152
|
+
| 'openrouter'
|
|
153
|
+
| null {
|
|
154
|
+
if (provider === 'openrouter') return 'openrouter';
|
|
155
|
+
if (
|
|
156
|
+
provider === 'moonshot' ||
|
|
157
|
+
provider === 'zai' ||
|
|
158
|
+
provider === 'zai-coding'
|
|
159
|
+
) {
|
|
160
|
+
return 'openai-compatible';
|
|
161
|
+
}
|
|
162
|
+
if (provider === 'minimax') return 'anthropic';
|
|
163
|
+
|
|
164
|
+
const npmBinding = getModelNpmBinding(provider, model);
|
|
165
|
+
if (npmBinding === '@ai-sdk/anthropic') return 'anthropic';
|
|
166
|
+
if (npmBinding === '@ai-sdk/openai') return 'openai';
|
|
167
|
+
if (npmBinding === '@ai-sdk/google') return 'google';
|
|
168
|
+
if (npmBinding === '@ai-sdk/openai-compatible') return 'openai-compatible';
|
|
169
|
+
if (npmBinding === '@openrouter/ai-sdk-provider') return 'openrouter';
|
|
170
|
+
|
|
171
|
+
const underlyingProvider = getUnderlyingProviderKey(provider, model);
|
|
172
|
+
if (underlyingProvider === 'anthropic') return 'anthropic';
|
|
173
|
+
if (underlyingProvider === 'openai') return 'openai';
|
|
174
|
+
if (underlyingProvider === 'google') return 'google';
|
|
175
|
+
if (underlyingProvider === 'openai-compatible') return 'openai-compatible';
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function buildReasoningConfig(args: {
|
|
180
|
+
provider: ProviderId;
|
|
181
|
+
model: string;
|
|
182
|
+
reasoningText?: boolean;
|
|
183
|
+
reasoningLevel?: ReasoningLevel;
|
|
184
|
+
maxOutputTokens: number | undefined;
|
|
185
|
+
}): ReasoningConfigResult {
|
|
186
|
+
const { provider, model, reasoningText, reasoningLevel, maxOutputTokens } =
|
|
187
|
+
args;
|
|
188
|
+
if (!reasoningText || !modelSupportsReasoning(provider, model)) {
|
|
189
|
+
return {
|
|
190
|
+
providerOptions: {},
|
|
191
|
+
effectiveMaxOutputTokens: maxOutputTokens,
|
|
192
|
+
enabled: false,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const reasoningTarget = getReasoningProviderTarget(provider, model);
|
|
197
|
+
if (reasoningTarget === 'anthropic') {
|
|
198
|
+
if (usesAdaptiveAnthropicThinking(model)) {
|
|
199
|
+
return {
|
|
200
|
+
providerOptions: {
|
|
201
|
+
anthropic: {
|
|
202
|
+
thinking: { type: 'adaptive' },
|
|
203
|
+
effort: toAnthropicEffort(reasoningLevel),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
effectiveMaxOutputTokens: maxOutputTokens,
|
|
207
|
+
enabled: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const thinkingBudget = toThinkingBudget(reasoningLevel, maxOutputTokens);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
providerOptions: {
|
|
215
|
+
anthropic: {
|
|
216
|
+
thinking: { type: 'enabled', budgetTokens: thinkingBudget },
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
effectiveMaxOutputTokens:
|
|
220
|
+
maxOutputTokens && maxOutputTokens > thinkingBudget
|
|
221
|
+
? maxOutputTokens - thinkingBudget
|
|
222
|
+
: maxOutputTokens,
|
|
223
|
+
enabled: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (reasoningTarget === 'openai') {
|
|
228
|
+
return {
|
|
229
|
+
providerOptions: {
|
|
230
|
+
openai: {
|
|
231
|
+
reasoningEffort: toOpenAIEffort(reasoningLevel),
|
|
232
|
+
reasoningSummary: 'auto',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
effectiveMaxOutputTokens: maxOutputTokens,
|
|
236
|
+
enabled: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (reasoningTarget === 'google') {
|
|
241
|
+
const isGemini3 = model.includes('gemini-3');
|
|
242
|
+
return {
|
|
243
|
+
providerOptions: {
|
|
244
|
+
google: {
|
|
245
|
+
thinkingConfig: isGemini3
|
|
246
|
+
? {
|
|
247
|
+
thinkingLevel: toGoogleThinkingLevel(reasoningLevel),
|
|
248
|
+
includeThoughts: true,
|
|
249
|
+
}
|
|
250
|
+
: {
|
|
251
|
+
thinkingBudget: toThinkingBudget(
|
|
252
|
+
reasoningLevel,
|
|
253
|
+
maxOutputTokens,
|
|
254
|
+
),
|
|
255
|
+
includeThoughts: true,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
effectiveMaxOutputTokens: maxOutputTokens,
|
|
260
|
+
enabled: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (reasoningTarget === 'openrouter') {
|
|
265
|
+
return {
|
|
266
|
+
providerOptions: {
|
|
267
|
+
openrouter: {
|
|
268
|
+
reasoning: { effort: normalizeReasoningLevel(reasoningLevel) },
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
effectiveMaxOutputTokens: maxOutputTokens,
|
|
272
|
+
enabled: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (reasoningTarget === 'openai-compatible') {
|
|
277
|
+
return {
|
|
278
|
+
providerOptions: buildSharedProviderOptions(provider, {
|
|
279
|
+
reasoningEffort: normalizeReasoningLevel(reasoningLevel),
|
|
280
|
+
}),
|
|
281
|
+
effectiveMaxOutputTokens: maxOutputTokens,
|
|
282
|
+
enabled: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
providerOptions: {},
|
|
288
|
+
effectiveMaxOutputTokens: maxOutputTokens,
|
|
289
|
+
enabled: false,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ProviderName } from '../provider/index.ts';
|
|
2
2
|
import { publish } from '../../events/bus.ts';
|
|
3
3
|
import type { ToolApprovalMode } from '../tools/approval.ts';
|
|
4
|
+
import type { ReasoningLevel } from '@ottocode/sdk';
|
|
4
5
|
|
|
5
6
|
export type RunOpts = {
|
|
6
7
|
sessionId: string;
|
|
@@ -12,6 +13,7 @@ export type RunOpts = {
|
|
|
12
13
|
oneShot?: boolean;
|
|
13
14
|
userContext?: string;
|
|
14
15
|
reasoningText?: boolean;
|
|
16
|
+
reasoningLevel?: ReasoningLevel;
|
|
15
17
|
abortSignal?: AbortSignal;
|
|
16
18
|
isCompactCommand?: boolean;
|
|
17
19
|
compactionContext?: string;
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import { resolve as resolvePath } from 'node:path';
|
|
2
|
+
|
|
1
3
|
export type GuardAction =
|
|
2
4
|
| { type: 'block'; reason: string }
|
|
3
5
|
| { type: 'approve'; reason: string }
|
|
4
6
|
| { type: 'allow' };
|
|
5
7
|
|
|
6
|
-
export
|
|
8
|
+
export type GuardContext = {
|
|
9
|
+
projectRoot?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function guardToolCall(
|
|
13
|
+
toolName: string,
|
|
14
|
+
args: unknown,
|
|
15
|
+
context: GuardContext = {},
|
|
16
|
+
): GuardAction {
|
|
7
17
|
const a = (args ?? {}) as Record<string, unknown>;
|
|
8
18
|
|
|
9
19
|
switch (toolName) {
|
|
@@ -12,7 +22,7 @@ export function guardToolCall(toolName: string, args: unknown): GuardAction {
|
|
|
12
22
|
case 'terminal':
|
|
13
23
|
return guardTerminal(a);
|
|
14
24
|
case 'read':
|
|
15
|
-
return guardReadPath(String(a.path ?? ''));
|
|
25
|
+
return guardReadPath(String(a.path ?? ''), context.projectRoot);
|
|
16
26
|
case 'write':
|
|
17
27
|
case 'edit':
|
|
18
28
|
case 'multiedit':
|
|
@@ -118,7 +128,42 @@ const SENSITIVE_READ_PATHS: Array<{ pattern: RegExp; reason: string }> = [
|
|
|
118
128
|
{ pattern: /^~?\/?\.docker\/config\.json$/, reason: 'Docker credentials' },
|
|
119
129
|
];
|
|
120
130
|
|
|
121
|
-
function
|
|
131
|
+
function normalizeForComparison(value: string): string {
|
|
132
|
+
const withForwardSlashes = value.replace(/\\/g, '/');
|
|
133
|
+
return process.platform === 'win32'
|
|
134
|
+
? withForwardSlashes.toLowerCase()
|
|
135
|
+
: withForwardSlashes;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function expandTilde(path: string): string {
|
|
139
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
140
|
+
if (!home) return path;
|
|
141
|
+
if (path === '~') return home;
|
|
142
|
+
if (path.startsWith('~/')) return `${home}/${path.slice(2)}`;
|
|
143
|
+
return path;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isAbsoluteLike(path: string): boolean {
|
|
147
|
+
return (
|
|
148
|
+
path.startsWith('/') || path.startsWith('~') || /^[A-Za-z]:[\\/]/.test(path)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isPathInProject(path: string, projectRoot?: string): boolean {
|
|
153
|
+
if (!projectRoot || !isAbsoluteLike(path)) return false;
|
|
154
|
+
const root = resolvePath(projectRoot);
|
|
155
|
+
const target = resolvePath(expandTilde(path));
|
|
156
|
+
const rootNorm = (() => {
|
|
157
|
+
const normalized = normalizeForComparison(root);
|
|
158
|
+
if (normalized === '/') return '/';
|
|
159
|
+
return normalized.replace(/[\\/]+$/, '');
|
|
160
|
+
})();
|
|
161
|
+
const targetNorm = normalizeForComparison(target);
|
|
162
|
+
const rootWithSlash = rootNorm === '/' ? '/' : `${rootNorm}/`;
|
|
163
|
+
return targetNorm === rootNorm || targetNorm.startsWith(rootWithSlash);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function guardReadPath(path: string, projectRoot?: string): GuardAction {
|
|
122
167
|
if (!path) return { type: 'allow' };
|
|
123
168
|
const p = path.trim();
|
|
124
169
|
|
|
@@ -128,7 +173,10 @@ function guardReadPath(path: string): GuardAction {
|
|
|
128
173
|
for (const { pattern, reason } of SENSITIVE_READ_PATHS) {
|
|
129
174
|
if (pattern.test(p)) return { type: 'approve', reason };
|
|
130
175
|
}
|
|
131
|
-
if (p
|
|
176
|
+
if (isPathInProject(p, projectRoot)) {
|
|
177
|
+
return { type: 'allow' };
|
|
178
|
+
}
|
|
179
|
+
if (isAbsoluteLike(p)) {
|
|
132
180
|
return { type: 'approve', reason: 'Reading path outside project root' };
|
|
133
181
|
}
|
|
134
182
|
return { type: 'allow' };
|
package/src/tools/adapter.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
requestApproval,
|
|
19
19
|
} from '../runtime/tools/approval.ts';
|
|
20
20
|
import { guardToolCall } from '../runtime/tools/guards.ts';
|
|
21
|
+
import { debugLog } from '../runtime/debug/index.ts';
|
|
21
22
|
|
|
22
23
|
export type { ToolAdapterContext } from '../runtime/tools/context.ts';
|
|
23
24
|
|
|
@@ -55,6 +56,37 @@ function getPendingQueue(
|
|
|
55
56
|
return queue;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
function extractToolCallId(options: unknown): string | undefined {
|
|
60
|
+
return (options as { toolCallId?: string } | undefined)?.toolCallId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEFAULT_TRACED_TOOL_INPUTS = new Set(['write', 'apply_patch']);
|
|
64
|
+
|
|
65
|
+
function shouldTraceToolInput(name: string): boolean {
|
|
66
|
+
const raw = process.env.OTTO_DEBUG_TOOL_INPUT?.trim();
|
|
67
|
+
if (!raw) return false;
|
|
68
|
+
const normalized = raw.toLowerCase();
|
|
69
|
+
if (['1', 'true', 'yes', 'on', 'all'].includes(normalized)) {
|
|
70
|
+
return DEFAULT_TRACED_TOOL_INPUTS.has(name);
|
|
71
|
+
}
|
|
72
|
+
const tokens = raw
|
|
73
|
+
.split(/[\s,]+/)
|
|
74
|
+
.map((token) => token.trim().toLowerCase())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
return tokens.includes('all') || tokens.includes(name.toLowerCase());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function summarizeTraceValue(value: unknown, max = 160): string {
|
|
80
|
+
try {
|
|
81
|
+
const json = JSON.stringify(value);
|
|
82
|
+
if (typeof json === 'string') {
|
|
83
|
+
return json.length > max ? `${json.slice(0, max)}…` : json;
|
|
84
|
+
}
|
|
85
|
+
} catch {}
|
|
86
|
+
const fallback = String(value);
|
|
87
|
+
return fallback.length > max ? `${fallback.slice(0, max)}…` : fallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
58
90
|
function unwrapDoubleWrappedArgs(
|
|
59
91
|
input: unknown,
|
|
60
92
|
expectedName: string,
|
|
@@ -199,12 +231,18 @@ export function adaptTools(
|
|
|
199
231
|
...base,
|
|
200
232
|
...(providerOptions ? { providerOptions } : {}),
|
|
201
233
|
async onInputStart(options: unknown) {
|
|
234
|
+
const sdkCallId = extractToolCallId(options);
|
|
202
235
|
const queue = getPendingQueue(pendingCalls, name);
|
|
203
236
|
queue.push({
|
|
204
|
-
callId: crypto.randomUUID(),
|
|
237
|
+
callId: sdkCallId || crypto.randomUUID(),
|
|
205
238
|
startTs: Date.now(),
|
|
206
239
|
stepIndex: ctx.stepIndex,
|
|
207
240
|
});
|
|
241
|
+
if (shouldTraceToolInput(name)) {
|
|
242
|
+
debugLog(
|
|
243
|
+
`[TOOL_INPUT_TRACE][adapter] onInputStart tool=${name} callId=${sdkCallId ?? queue[queue.length - 1]?.callId ?? 'unknown'} step=${ctx.stepIndex}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
208
246
|
if (typeof base.onInputStart === 'function')
|
|
209
247
|
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
210
248
|
await base.onInputStart(options as any);
|
|
@@ -212,8 +250,14 @@ export function adaptTools(
|
|
|
212
250
|
async onInputDelta(options: unknown) {
|
|
213
251
|
const delta = (options as { inputTextDelta?: string } | undefined)
|
|
214
252
|
?.inputTextDelta;
|
|
253
|
+
const sdkCallId = extractToolCallId(options);
|
|
215
254
|
const queue = pendingCalls.get(name);
|
|
216
255
|
const meta = queue?.length ? queue[queue.length - 1] : undefined;
|
|
256
|
+
if (shouldTraceToolInput(name)) {
|
|
257
|
+
debugLog(
|
|
258
|
+
`[TOOL_INPUT_TRACE][adapter] onInputDelta tool=${name} callId=${sdkCallId ?? meta?.callId ?? 'unknown'} step=${meta?.stepIndex ?? ctx.stepIndex} delta=${summarizeTraceValue(delta ?? '')}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
217
261
|
// Stream tool argument deltas as events if needed
|
|
218
262
|
publish({
|
|
219
263
|
type: 'tool.delta',
|
|
@@ -233,21 +277,30 @@ export function adaptTools(
|
|
|
233
277
|
},
|
|
234
278
|
async onInputAvailable(options: unknown) {
|
|
235
279
|
const args = (options as { input?: unknown } | undefined)?.input;
|
|
280
|
+
const sdkCallId = extractToolCallId(options);
|
|
236
281
|
const queue = getPendingQueue(pendingCalls, name);
|
|
237
282
|
let meta = queue.length ? queue[queue.length - 1] : undefined;
|
|
238
283
|
if (!meta) {
|
|
239
284
|
meta = {
|
|
240
|
-
callId: crypto.randomUUID(),
|
|
285
|
+
callId: sdkCallId || crypto.randomUUID(),
|
|
241
286
|
startTs: Date.now(),
|
|
242
287
|
stepIndex: ctx.stepIndex,
|
|
243
288
|
};
|
|
244
289
|
queue.push(meta);
|
|
245
290
|
}
|
|
291
|
+
if (sdkCallId && meta.callId !== sdkCallId) {
|
|
292
|
+
meta.callId = sdkCallId;
|
|
293
|
+
}
|
|
246
294
|
meta.stepIndex = ctx.stepIndex;
|
|
247
295
|
meta.args = args;
|
|
248
296
|
const callId = meta.callId;
|
|
249
297
|
const callPartId = crypto.randomUUID();
|
|
250
298
|
const startTs = meta.startTs;
|
|
299
|
+
if (shouldTraceToolInput(name)) {
|
|
300
|
+
debugLog(
|
|
301
|
+
`[TOOL_INPUT_TRACE][adapter] onInputAvailable tool=${name} callId=${callId} step=${ctx.stepIndex} input=${summarizeTraceValue(args)}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
251
304
|
|
|
252
305
|
if (
|
|
253
306
|
!firstToolCallReported &&
|
|
@@ -340,7 +393,9 @@ export function adaptTools(
|
|
|
340
393
|
args,
|
|
341
394
|
);
|
|
342
395
|
}
|
|
343
|
-
const guard = guardToolCall(name, args
|
|
396
|
+
const guard = guardToolCall(name, args, {
|
|
397
|
+
projectRoot: ctx.projectRoot,
|
|
398
|
+
});
|
|
344
399
|
if (guard.type === 'block') {
|
|
345
400
|
meta.blocked = true;
|
|
346
401
|
meta.blockReason = guard.reason;
|
|
@@ -360,10 +415,11 @@ export function adaptTools(
|
|
|
360
415
|
},
|
|
361
416
|
async execute(input: ToolExecuteInput, options: ToolExecuteOptions) {
|
|
362
417
|
input = unwrapDoubleWrappedArgs(input, name);
|
|
418
|
+
const sdkCallId = extractToolCallId(options);
|
|
363
419
|
const queue = pendingCalls.get(name);
|
|
364
420
|
const meta = queue?.shift();
|
|
365
421
|
if (queue && queue.length === 0) pendingCalls.delete(name);
|
|
366
|
-
const callIdFromQueue = meta?.callId;
|
|
422
|
+
const callIdFromQueue = sdkCallId || meta?.callId;
|
|
367
423
|
const startTsFromQueue = meta?.startTs;
|
|
368
424
|
const stepIndexForEvent = meta?.stepIndex ?? ctx.stepIndex;
|
|
369
425
|
|
|
@@ -462,23 +518,46 @@ export function adaptTools(
|
|
|
462
518
|
// If tool returns an async iterable, stream deltas while accumulating
|
|
463
519
|
if (res && typeof res === 'object' && Symbol.asyncIterator in res) {
|
|
464
520
|
const chunks: unknown[] = [];
|
|
521
|
+
let streamedResult: unknown = null;
|
|
465
522
|
for await (const chunk of res as AsyncIterable<unknown>) {
|
|
466
523
|
chunks.push(chunk);
|
|
524
|
+
if (chunk && typeof chunk === 'object' && 'result' in chunk) {
|
|
525
|
+
streamedResult = (chunk as { result: unknown }).result;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const delta =
|
|
529
|
+
typeof chunk === 'string'
|
|
530
|
+
? chunk
|
|
531
|
+
: chunk &&
|
|
532
|
+
typeof chunk === 'object' &&
|
|
533
|
+
'delta' in chunk &&
|
|
534
|
+
typeof (chunk as { delta?: unknown }).delta === 'string'
|
|
535
|
+
? ((chunk as { delta: string }).delta ?? '')
|
|
536
|
+
: null;
|
|
537
|
+
if (!delta) continue;
|
|
538
|
+
const channel =
|
|
539
|
+
chunk &&
|
|
540
|
+
typeof chunk === 'object' &&
|
|
541
|
+
'channel' in chunk &&
|
|
542
|
+
typeof (chunk as { channel?: unknown }).channel === 'string'
|
|
543
|
+
? ((chunk as { channel: string }).channel ?? 'output')
|
|
544
|
+
: 'output';
|
|
467
545
|
publish({
|
|
468
546
|
type: 'tool.delta',
|
|
469
547
|
sessionId: ctx.sessionId,
|
|
470
548
|
payload: {
|
|
471
549
|
name,
|
|
472
|
-
channel
|
|
473
|
-
delta
|
|
550
|
+
channel,
|
|
551
|
+
delta,
|
|
474
552
|
stepIndex: stepIndexForEvent,
|
|
475
553
|
callId: callIdFromQueue,
|
|
476
554
|
messageId: ctx.messageId,
|
|
477
555
|
},
|
|
478
556
|
});
|
|
479
557
|
}
|
|
480
|
-
|
|
481
|
-
|
|
558
|
+
result =
|
|
559
|
+
streamedResult ??
|
|
560
|
+
(chunks.length > 0 ? chunks[chunks.length - 1] : null);
|
|
482
561
|
} else {
|
|
483
562
|
// Await promise or passthrough value
|
|
484
563
|
result = await Promise.resolve(res as ToolExecuteReturn);
|