@ottocode/server 0.1.264 → 0.1.266
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/routes/auth/copilot.ts +699 -0
- package/src/routes/auth/oauth.ts +578 -0
- package/src/routes/auth/onboarding.ts +45 -0
- package/src/routes/auth/providers.ts +189 -0
- package/src/routes/auth/service.ts +167 -0
- package/src/routes/auth/state.ts +23 -0
- package/src/routes/auth/status.ts +203 -0
- package/src/routes/auth/wallet.ts +229 -0
- package/src/routes/auth.ts +12 -2080
- package/src/routes/config/models-service.ts +411 -0
- package/src/routes/config/models.ts +6 -426
- package/src/routes/config/providers-service.ts +237 -0
- package/src/routes/config/providers.ts +10 -242
- package/src/routes/files/handlers.ts +297 -0
- package/src/routes/files/service.ts +313 -0
- package/src/routes/files.ts +12 -608
- package/src/routes/git/commit-service.ts +207 -0
- package/src/routes/git/commit.ts +6 -220
- package/src/routes/git/remote-service.ts +116 -0
- package/src/routes/git/remote.ts +8 -115
- package/src/routes/git/staging-service.ts +111 -0
- package/src/routes/git/staging.ts +10 -205
- package/src/routes/mcp/auth.ts +338 -0
- package/src/routes/mcp/lifecycle.ts +263 -0
- package/src/routes/mcp/servers.ts +212 -0
- package/src/routes/mcp/service.ts +664 -0
- package/src/routes/mcp/state.ts +13 -0
- package/src/routes/mcp.ts +6 -1233
- package/src/routes/ottorouter/billing.ts +593 -0
- package/src/routes/ottorouter/service.ts +92 -0
- package/src/routes/ottorouter/topup.ts +301 -0
- package/src/routes/ottorouter/wallet.ts +370 -0
- package/src/routes/ottorouter.ts +6 -1319
- package/src/routes/research/service.ts +339 -0
- package/src/routes/research.ts +12 -390
- package/src/routes/sessions/crud.ts +563 -0
- package/src/routes/sessions/queue.ts +242 -0
- package/src/routes/sessions/retry.ts +121 -0
- package/src/routes/sessions/service.ts +768 -0
- package/src/routes/sessions/share.ts +434 -0
- package/src/routes/sessions.ts +8 -1977
- package/src/routes/skills/service.ts +221 -0
- package/src/routes/skills/spec.ts +309 -0
- package/src/routes/skills.ts +31 -909
- package/src/routes/terminals/service.ts +326 -0
- package/src/routes/terminals.ts +19 -295
- package/src/routes/tunnel/service.ts +217 -0
- package/src/routes/tunnel.ts +29 -219
- package/src/runtime/agent/registry-prompts.ts +147 -0
- package/src/runtime/agent/registry.ts +6 -124
- package/src/runtime/agent/runner-errors.ts +116 -0
- package/src/runtime/agent/runner-reminders.ts +45 -0
- package/src/runtime/agent/runner-setup-model.ts +75 -0
- package/src/runtime/agent/runner-setup-prompt.ts +185 -0
- package/src/runtime/agent/runner-setup-tools.ts +103 -0
- package/src/runtime/agent/runner-setup-utils.ts +21 -0
- package/src/runtime/agent/runner-setup.ts +54 -288
- package/src/runtime/agent/runner-telemetry.ts +112 -0
- package/src/runtime/agent/runner-text.ts +108 -0
- package/src/runtime/agent/runner-tool-observer.ts +86 -0
- package/src/runtime/agent/runner.ts +79 -378
- package/src/runtime/ask/service.ts +1 -0
- package/src/runtime/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +6 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +68 -264
- package/src/runtime/provider/xai.ts +8 -0
- package/src/tools/adapter/events.ts +116 -0
- package/src/tools/adapter/execution.ts +160 -0
- package/src/tools/adapter/pending.ts +37 -0
- package/src/tools/adapter/persistence.ts +166 -0
- package/src/tools/adapter/results.ts +97 -0
- package/src/tools/adapter.ts +124 -451
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
catalog,
|
|
3
2
|
getConfiguredProviderFamily,
|
|
4
3
|
getProviderDefinition,
|
|
5
4
|
getModelNpmBinding,
|
|
6
5
|
getUnderlyingProviderKey,
|
|
7
|
-
isBuiltInProviderId,
|
|
8
6
|
modelSupportsReasoning,
|
|
9
7
|
type OttoConfig,
|
|
10
8
|
type ProviderId,
|
|
11
9
|
type ReasoningLevel,
|
|
12
10
|
} from '@ottocode/sdk';
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import {
|
|
12
|
+
buildAnthropicReasoningOptions,
|
|
13
|
+
buildGoogleReasoningOptions,
|
|
14
|
+
buildOllamaReasoningOptions,
|
|
15
|
+
buildOpenAICompatibleReasoningOptions,
|
|
16
|
+
buildOpenAIReasoningOptions,
|
|
17
|
+
buildOpenRouterReasoningOptions,
|
|
18
|
+
} from './reasoning-builders.ts';
|
|
15
19
|
|
|
16
20
|
export type ReasoningConfigResult = {
|
|
17
21
|
providerOptions: Record<string, unknown>;
|
|
@@ -19,152 +23,19 @@ export type ReasoningConfigResult = {
|
|
|
19
23
|
enabled: boolean;
|
|
20
24
|
};
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
level: ReasoningLevel | undefined,
|
|
24
|
-
): Exclude<ReasoningLevel, 'xhigh'> {
|
|
25
|
-
if (!level) return 'high';
|
|
26
|
-
if (level === 'xhigh') return 'high';
|
|
27
|
-
return level;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function toAnthropicEffort(
|
|
31
|
-
model: string,
|
|
32
|
-
level: ReasoningLevel | undefined,
|
|
33
|
-
): 'low' | 'medium' | 'high' | 'xhigh' | 'max' {
|
|
34
|
-
switch (level) {
|
|
35
|
-
case 'minimal':
|
|
36
|
-
case 'low':
|
|
37
|
-
return 'low';
|
|
38
|
-
case 'medium':
|
|
39
|
-
return 'medium';
|
|
40
|
-
case 'max':
|
|
41
|
-
return 'max';
|
|
42
|
-
case 'xhigh':
|
|
43
|
-
return isClaudeOpus47(model) ? 'xhigh' : 'max';
|
|
44
|
-
default:
|
|
45
|
-
return 'high';
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function toOpenAIEffort(
|
|
50
|
-
level: ReasoningLevel | undefined,
|
|
51
|
-
): 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' {
|
|
52
|
-
switch (level) {
|
|
53
|
-
case 'minimal':
|
|
54
|
-
return 'minimal';
|
|
55
|
-
case 'low':
|
|
56
|
-
return 'low';
|
|
57
|
-
case 'medium':
|
|
58
|
-
return 'medium';
|
|
59
|
-
case 'max':
|
|
60
|
-
case 'xhigh':
|
|
61
|
-
return 'xhigh';
|
|
62
|
-
default:
|
|
63
|
-
return 'high';
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function toGoogleThinkingLevel(
|
|
68
|
-
level: ReasoningLevel | undefined,
|
|
69
|
-
): 'minimal' | 'low' | 'medium' | 'high' {
|
|
70
|
-
switch (level) {
|
|
71
|
-
case 'minimal':
|
|
72
|
-
return 'minimal';
|
|
73
|
-
case 'low':
|
|
74
|
-
return 'low';
|
|
75
|
-
case 'medium':
|
|
76
|
-
return 'medium';
|
|
77
|
-
default:
|
|
78
|
-
return 'high';
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function toThinkingBudget(
|
|
83
|
-
level: ReasoningLevel | undefined,
|
|
84
|
-
maxOutputTokens: number | undefined,
|
|
85
|
-
): number {
|
|
86
|
-
const cap = maxOutputTokens
|
|
87
|
-
? Math.max(maxOutputTokens, THINKING_BUDGET)
|
|
88
|
-
: THINKING_BUDGET;
|
|
89
|
-
switch (level) {
|
|
90
|
-
case 'minimal':
|
|
91
|
-
return Math.min(2048, cap);
|
|
92
|
-
case 'low':
|
|
93
|
-
return Math.min(4096, cap);
|
|
94
|
-
case 'medium':
|
|
95
|
-
return Math.min(8192, cap);
|
|
96
|
-
case 'max':
|
|
97
|
-
case 'xhigh':
|
|
98
|
-
return Math.min(24000, cap);
|
|
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(
|
|
118
|
-
provider: ProviderId,
|
|
119
|
-
cfg?: OttoConfig,
|
|
120
|
-
): string[] {
|
|
121
|
-
const definition = cfg ? getProviderDefinition(cfg, provider) : undefined;
|
|
122
|
-
const entry = isBuiltInProviderId(provider) ? catalog[provider] : undefined;
|
|
123
|
-
const keys = new Set<string>(['openaiCompatible', toCamelCaseKey(provider)]);
|
|
124
|
-
const label = definition?.label ?? entry?.label;
|
|
125
|
-
if (label) {
|
|
126
|
-
keys.add(toCamelCaseKey(label));
|
|
127
|
-
}
|
|
128
|
-
return Array.from(keys).filter(Boolean);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function buildSharedProviderOptions(
|
|
132
|
-
provider: ProviderId,
|
|
133
|
-
options: Record<string, unknown>,
|
|
134
|
-
cfg?: OttoConfig,
|
|
135
|
-
): Record<string, unknown> {
|
|
136
|
-
const keys = getOpenAICompatibleProviderOptionKeys(provider, cfg);
|
|
137
|
-
return Object.fromEntries(keys.map((key) => [key, options]));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function isClaudeOpus47(model: string): boolean {
|
|
141
|
-
const lower = model.toLowerCase();
|
|
142
|
-
return lower.includes('claude-opus-4-7') || lower.includes('claude-opus-4.7');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function usesAdaptiveAnthropicThinking(model: string): boolean {
|
|
146
|
-
const lower = model.toLowerCase();
|
|
147
|
-
return (
|
|
148
|
-
isClaudeOpus47(model) ||
|
|
149
|
-
lower.includes('claude-opus-4-6') ||
|
|
150
|
-
lower.includes('claude-opus-4.6') ||
|
|
151
|
-
lower.includes('claude-sonnet-4-6') ||
|
|
152
|
-
lower.includes('claude-sonnet-4.6')
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function getReasoningProviderTarget(
|
|
157
|
-
provider: ProviderId,
|
|
158
|
-
model: string,
|
|
159
|
-
cfg?: OttoConfig,
|
|
160
|
-
):
|
|
26
|
+
type ReasoningProviderTarget =
|
|
161
27
|
| 'anthropic'
|
|
162
28
|
| 'openai'
|
|
163
29
|
| 'google'
|
|
164
30
|
| 'ollama'
|
|
165
31
|
| 'openai-compatible'
|
|
166
|
-
| 'openrouter'
|
|
167
|
-
|
|
32
|
+
| 'openrouter';
|
|
33
|
+
|
|
34
|
+
function getReasoningProviderTarget(
|
|
35
|
+
provider: ProviderId,
|
|
36
|
+
model: string,
|
|
37
|
+
cfg?: OttoConfig,
|
|
38
|
+
): ReasoningProviderTarget | null {
|
|
168
39
|
const definition = cfg ? getProviderDefinition(cfg, provider) : undefined;
|
|
169
40
|
if (definition?.source === 'custom') {
|
|
170
41
|
if (definition.compatibility === 'anthropic') return 'anthropic';
|
|
@@ -190,6 +61,7 @@ function getReasoningProviderTarget(
|
|
|
190
61
|
const npmBinding = getModelNpmBinding(provider, model);
|
|
191
62
|
if (npmBinding === '@ai-sdk/anthropic') return 'anthropic';
|
|
192
63
|
if (npmBinding === '@ai-sdk/openai') return 'openai';
|
|
64
|
+
if (npmBinding === '@ai-sdk/xai') return 'openai';
|
|
193
65
|
if (npmBinding === '@ai-sdk/google') return 'google';
|
|
194
66
|
if (npmBinding === 'ai-sdk-ollama') return 'ollama';
|
|
195
67
|
if (npmBinding === '@ai-sdk/openai-compatible') return 'openai-compatible';
|
|
@@ -209,6 +81,45 @@ function getReasoningProviderTarget(
|
|
|
209
81
|
return null;
|
|
210
82
|
}
|
|
211
83
|
|
|
84
|
+
function isReasoningSupported(args: {
|
|
85
|
+
cfg?: OttoConfig;
|
|
86
|
+
provider: ProviderId;
|
|
87
|
+
model: string;
|
|
88
|
+
}): boolean {
|
|
89
|
+
const { cfg, provider, model } = args;
|
|
90
|
+
const definition = cfg ? getProviderDefinition(cfg, provider) : undefined;
|
|
91
|
+
if (definition?.compatibility === 'ollama') return true;
|
|
92
|
+
if (definition?.source === 'custom') return true;
|
|
93
|
+
if (provider === 'ottorouter') return true;
|
|
94
|
+
return modelSupportsReasoning(provider, model);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildTargetReasoningConfig(
|
|
98
|
+
target: ReasoningProviderTarget,
|
|
99
|
+
args: {
|
|
100
|
+
cfg?: OttoConfig;
|
|
101
|
+
provider: ProviderId;
|
|
102
|
+
model: string;
|
|
103
|
+
reasoningLevel?: ReasoningLevel;
|
|
104
|
+
maxOutputTokens: number | undefined;
|
|
105
|
+
},
|
|
106
|
+
): ReasoningConfigResult {
|
|
107
|
+
switch (target) {
|
|
108
|
+
case 'anthropic':
|
|
109
|
+
return buildAnthropicReasoningOptions(args);
|
|
110
|
+
case 'openai':
|
|
111
|
+
return buildOpenAIReasoningOptions(args);
|
|
112
|
+
case 'google':
|
|
113
|
+
return buildGoogleReasoningOptions(args);
|
|
114
|
+
case 'ollama':
|
|
115
|
+
return buildOllamaReasoningOptions(args);
|
|
116
|
+
case 'openrouter':
|
|
117
|
+
return buildOpenRouterReasoningOptions(args);
|
|
118
|
+
case 'openai-compatible':
|
|
119
|
+
return buildOpenAICompatibleReasoningOptions(args);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
212
123
|
export function buildReasoningConfig(args: {
|
|
213
124
|
cfg?: OttoConfig;
|
|
214
125
|
provider: ProviderId;
|
|
@@ -225,16 +136,8 @@ export function buildReasoningConfig(args: {
|
|
|
225
136
|
reasoningLevel,
|
|
226
137
|
maxOutputTokens,
|
|
227
138
|
} = args;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
definition?.compatibility === 'ollama'
|
|
231
|
-
? true
|
|
232
|
-
: definition?.source === 'custom'
|
|
233
|
-
? true
|
|
234
|
-
: provider === 'ottorouter'
|
|
235
|
-
? true
|
|
236
|
-
: modelSupportsReasoning(provider, model);
|
|
237
|
-
if (!reasoningText || !supportsReasoning) {
|
|
139
|
+
|
|
140
|
+
if (!reasoningText || !isReasoningSupported({ cfg, provider, model })) {
|
|
238
141
|
return {
|
|
239
142
|
providerOptions: {},
|
|
240
143
|
effectiveMaxOutputTokens: maxOutputTokens,
|
|
@@ -243,118 +146,19 @@ export function buildReasoningConfig(args: {
|
|
|
243
146
|
}
|
|
244
147
|
|
|
245
148
|
const reasoningTarget = getReasoningProviderTarget(provider, model, cfg);
|
|
246
|
-
if (reasoningTarget
|
|
247
|
-
if (usesAdaptiveAnthropicThinking(model)) {
|
|
248
|
-
const thinking = isClaudeOpus47(model)
|
|
249
|
-
? { type: 'adaptive', display: 'summarized' }
|
|
250
|
-
: { type: 'adaptive' };
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
providerOptions: {
|
|
254
|
-
anthropic: {
|
|
255
|
-
thinking,
|
|
256
|
-
effort: toAnthropicEffort(model, reasoningLevel),
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
260
|
-
enabled: true,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const thinkingBudget = toThinkingBudget(reasoningLevel, maxOutputTokens);
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
providerOptions: {
|
|
268
|
-
anthropic: {
|
|
269
|
-
thinking: { type: 'enabled', budgetTokens: thinkingBudget },
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
effectiveMaxOutputTokens:
|
|
273
|
-
maxOutputTokens && maxOutputTokens > thinkingBudget
|
|
274
|
-
? maxOutputTokens - thinkingBudget
|
|
275
|
-
: maxOutputTokens,
|
|
276
|
-
enabled: true,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (reasoningTarget === 'openai') {
|
|
149
|
+
if (!reasoningTarget) {
|
|
281
150
|
return {
|
|
282
|
-
providerOptions: {
|
|
283
|
-
openai: {
|
|
284
|
-
reasoningEffort: toOpenAIEffort(reasoningLevel),
|
|
285
|
-
reasoningSummary: 'auto',
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
289
|
-
enabled: true,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (reasoningTarget === 'google') {
|
|
294
|
-
const isGemini3 = model.includes('gemini-3');
|
|
295
|
-
return {
|
|
296
|
-
providerOptions: {
|
|
297
|
-
google: {
|
|
298
|
-
thinkingConfig: isGemini3
|
|
299
|
-
? {
|
|
300
|
-
thinkingLevel: toGoogleThinkingLevel(reasoningLevel),
|
|
301
|
-
includeThoughts: true,
|
|
302
|
-
}
|
|
303
|
-
: {
|
|
304
|
-
thinkingBudget: toThinkingBudget(
|
|
305
|
-
reasoningLevel,
|
|
306
|
-
maxOutputTokens,
|
|
307
|
-
),
|
|
308
|
-
includeThoughts: true,
|
|
309
|
-
},
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
313
|
-
enabled: true,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (reasoningTarget === 'ollama') {
|
|
318
|
-
return {
|
|
319
|
-
providerOptions: {
|
|
320
|
-
ollama: {
|
|
321
|
-
think: true,
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
325
|
-
enabled: true,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (reasoningTarget === 'openrouter') {
|
|
330
|
-
return {
|
|
331
|
-
providerOptions: {
|
|
332
|
-
openrouter: {
|
|
333
|
-
reasoning: { effort: normalizeReasoningLevel(reasoningLevel) },
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
337
|
-
enabled: true,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (reasoningTarget === 'openai-compatible') {
|
|
342
|
-
return {
|
|
343
|
-
providerOptions: buildSharedProviderOptions(
|
|
344
|
-
provider,
|
|
345
|
-
{
|
|
346
|
-
reasoningEffort: normalizeReasoningLevel(reasoningLevel),
|
|
347
|
-
},
|
|
348
|
-
cfg,
|
|
349
|
-
),
|
|
151
|
+
providerOptions: {},
|
|
350
152
|
effectiveMaxOutputTokens: maxOutputTokens,
|
|
351
|
-
enabled:
|
|
153
|
+
enabled: false,
|
|
352
154
|
};
|
|
353
155
|
}
|
|
354
156
|
|
|
355
|
-
return {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
157
|
+
return buildTargetReasoningConfig(reasoningTarget, {
|
|
158
|
+
cfg,
|
|
159
|
+
provider,
|
|
160
|
+
model,
|
|
161
|
+
reasoningLevel,
|
|
162
|
+
maxOutputTokens,
|
|
163
|
+
});
|
|
360
164
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OttoConfig } from '@ottocode/sdk';
|
|
2
|
+
import { getAuth, createXaiModel } from '@ottocode/sdk';
|
|
3
|
+
|
|
4
|
+
export async function getXaiInstance(cfg: OttoConfig, model: string) {
|
|
5
|
+
const auth = await getAuth('xai', cfg.projectRoot);
|
|
6
|
+
const apiKey = auth?.type === 'api' ? auth.key : undefined;
|
|
7
|
+
return createXaiModel(model, { apiKey });
|
|
8
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { logger } from '@ottocode/sdk';
|
|
2
|
+
import { publish } from '../../events/bus.ts';
|
|
3
|
+
import type { ToolAdapterContext } from '../../runtime/tools/context.ts';
|
|
4
|
+
|
|
5
|
+
export type ToolResultContent = {
|
|
6
|
+
name: string;
|
|
7
|
+
result: unknown;
|
|
8
|
+
callId?: string;
|
|
9
|
+
artifact?: unknown;
|
|
10
|
+
args?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function publishToolCall(
|
|
14
|
+
ctx: ToolAdapterContext,
|
|
15
|
+
args: {
|
|
16
|
+
name: string;
|
|
17
|
+
input: unknown;
|
|
18
|
+
callId: string;
|
|
19
|
+
stepIndex?: number;
|
|
20
|
+
},
|
|
21
|
+
): void {
|
|
22
|
+
publish({
|
|
23
|
+
type: 'tool.call',
|
|
24
|
+
sessionId: ctx.sessionId,
|
|
25
|
+
payload: {
|
|
26
|
+
name: args.name,
|
|
27
|
+
args: args.input,
|
|
28
|
+
callId: args.callId,
|
|
29
|
+
stepIndex: args.stepIndex,
|
|
30
|
+
messageId: ctx.messageId,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function logToolCall(
|
|
36
|
+
ctx: ToolAdapterContext,
|
|
37
|
+
args: { name: string; callId?: string; stepIndex?: number },
|
|
38
|
+
): void {
|
|
39
|
+
logger.debug(`[tools] call ${args.name}`, {
|
|
40
|
+
sessionId: ctx.sessionId,
|
|
41
|
+
messageId: ctx.messageId,
|
|
42
|
+
toolName: args.name,
|
|
43
|
+
callId: args.callId,
|
|
44
|
+
stepIndex: args.stepIndex,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function publishToolDelta(
|
|
49
|
+
ctx: ToolAdapterContext,
|
|
50
|
+
args: {
|
|
51
|
+
name: string;
|
|
52
|
+
channel: string;
|
|
53
|
+
delta: unknown;
|
|
54
|
+
stepIndex?: number;
|
|
55
|
+
callId?: string;
|
|
56
|
+
},
|
|
57
|
+
): void {
|
|
58
|
+
publish({
|
|
59
|
+
type: 'tool.delta',
|
|
60
|
+
sessionId: ctx.sessionId,
|
|
61
|
+
payload: {
|
|
62
|
+
name: args.name,
|
|
63
|
+
channel: args.channel,
|
|
64
|
+
delta: args.delta,
|
|
65
|
+
stepIndex: args.stepIndex,
|
|
66
|
+
callId: args.callId,
|
|
67
|
+
messageId: ctx.messageId,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function publishToolResult(
|
|
73
|
+
ctx: ToolAdapterContext,
|
|
74
|
+
content: ToolResultContent,
|
|
75
|
+
stepIndex?: number,
|
|
76
|
+
): void {
|
|
77
|
+
publish({
|
|
78
|
+
type: 'tool.result',
|
|
79
|
+
sessionId: ctx.sessionId,
|
|
80
|
+
payload: { ...content, stepIndex },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function logToolResult(
|
|
85
|
+
ctx: ToolAdapterContext,
|
|
86
|
+
args: { name: string; callId?: string; stepIndex?: number },
|
|
87
|
+
): void {
|
|
88
|
+
logger.debug(`[tools] result ${args.name}`, {
|
|
89
|
+
sessionId: ctx.sessionId,
|
|
90
|
+
messageId: ctx.messageId,
|
|
91
|
+
toolName: args.name,
|
|
92
|
+
callId: args.callId,
|
|
93
|
+
stepIndex: args.stepIndex,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function publishPlanUpdated(
|
|
98
|
+
ctx: ToolAdapterContext,
|
|
99
|
+
result: unknown,
|
|
100
|
+
): void {
|
|
101
|
+
try {
|
|
102
|
+
const resultValue = result as
|
|
103
|
+
| { items?: unknown; note?: unknown }
|
|
104
|
+
| undefined;
|
|
105
|
+
if (resultValue && Array.isArray(resultValue.items)) {
|
|
106
|
+
publish({
|
|
107
|
+
type: 'plan.updated',
|
|
108
|
+
sessionId: ctx.sessionId,
|
|
109
|
+
payload: {
|
|
110
|
+
items: resultValue.items,
|
|
111
|
+
note: resultValue.note,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { Tool } from 'ai';
|
|
2
|
+
import type { ToolAdapterContext } from '../../runtime/tools/context.ts';
|
|
3
|
+
import { getCwd, joinRelative, setCwd } from '../../runtime/utils/cwd.ts';
|
|
4
|
+
import { publishToolDelta } from './events.ts';
|
|
5
|
+
|
|
6
|
+
type ToolExecuteSignature = Tool['execute'] extends (
|
|
7
|
+
input: infer Input,
|
|
8
|
+
options: infer Options,
|
|
9
|
+
) => infer Result
|
|
10
|
+
? { input: Input; options: Options; result: Result }
|
|
11
|
+
: { input: unknown; options: unknown; result: unknown };
|
|
12
|
+
|
|
13
|
+
type ToolExecuteInput = ToolExecuteSignature['input'];
|
|
14
|
+
type ToolExecuteOptions = ToolExecuteSignature['options'] extends never
|
|
15
|
+
? undefined
|
|
16
|
+
: ToolExecuteSignature['options'];
|
|
17
|
+
type ToolExecuteReturn = ToolExecuteSignature['result'];
|
|
18
|
+
|
|
19
|
+
export function executeBaseTool(
|
|
20
|
+
ctx: ToolAdapterContext,
|
|
21
|
+
args: {
|
|
22
|
+
base: Tool;
|
|
23
|
+
name: string;
|
|
24
|
+
input: ToolExecuteInput;
|
|
25
|
+
options: ToolExecuteOptions;
|
|
26
|
+
},
|
|
27
|
+
): ToolExecuteReturn | { cwd: string } | null | undefined {
|
|
28
|
+
const cwd = getCwd(ctx.sessionId);
|
|
29
|
+
const { base, name, input, options } = args;
|
|
30
|
+
|
|
31
|
+
if (name === 'pwd') {
|
|
32
|
+
return { cwd };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (name === 'cd') {
|
|
36
|
+
const next = joinRelative(
|
|
37
|
+
cwd,
|
|
38
|
+
String((input as Record<string, unknown>)?.path ?? '.'),
|
|
39
|
+
);
|
|
40
|
+
setCwd(ctx.sessionId, next);
|
|
41
|
+
return { cwd: next };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
['read', 'write', 'ls', 'tree'].includes(name) &&
|
|
46
|
+
typeof (input as Record<string, unknown>)?.path === 'string'
|
|
47
|
+
) {
|
|
48
|
+
const rel = joinRelative(
|
|
49
|
+
cwd,
|
|
50
|
+
String((input as Record<string, unknown>).path),
|
|
51
|
+
);
|
|
52
|
+
const nextInput = {
|
|
53
|
+
...(input as Record<string, unknown>),
|
|
54
|
+
path: rel,
|
|
55
|
+
} as ToolExecuteInput;
|
|
56
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
57
|
+
return base.execute?.(nextInput, options as any);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (name === 'shell' || name === 'bash') {
|
|
61
|
+
const needsCwd =
|
|
62
|
+
!input || typeof (input as Record<string, unknown>).cwd !== 'string';
|
|
63
|
+
const nextInput = needsCwd
|
|
64
|
+
? ({
|
|
65
|
+
...(input as Record<string, unknown>),
|
|
66
|
+
cwd,
|
|
67
|
+
} as ToolExecuteInput)
|
|
68
|
+
: input;
|
|
69
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
70
|
+
return base.execute?.(nextInput, options as any);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
74
|
+
return base.execute?.(input, options as any);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getStringDelta(chunk: unknown): string | null {
|
|
78
|
+
if (typeof chunk === 'string') return chunk;
|
|
79
|
+
if (
|
|
80
|
+
chunk &&
|
|
81
|
+
typeof chunk === 'object' &&
|
|
82
|
+
'delta' in chunk &&
|
|
83
|
+
typeof (chunk as { delta?: unknown }).delta === 'string'
|
|
84
|
+
) {
|
|
85
|
+
return (chunk as { delta: string }).delta ?? '';
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getStringChannel(chunk: unknown): string {
|
|
91
|
+
if (
|
|
92
|
+
chunk &&
|
|
93
|
+
typeof chunk === 'object' &&
|
|
94
|
+
'channel' in chunk &&
|
|
95
|
+
typeof (chunk as { channel?: unknown }).channel === 'string'
|
|
96
|
+
) {
|
|
97
|
+
return (chunk as { channel: string }).channel ?? 'output';
|
|
98
|
+
}
|
|
99
|
+
return 'output';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getTerminalId(chunk: unknown): string | null {
|
|
103
|
+
if (
|
|
104
|
+
chunk &&
|
|
105
|
+
typeof chunk === 'object' &&
|
|
106
|
+
'terminalId' in chunk &&
|
|
107
|
+
typeof (chunk as { terminalId?: unknown }).terminalId === 'string'
|
|
108
|
+
) {
|
|
109
|
+
return (chunk as { terminalId: string }).terminalId;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function consumeToolStream(
|
|
115
|
+
ctx: ToolAdapterContext,
|
|
116
|
+
args: {
|
|
117
|
+
stream: AsyncIterable<unknown>;
|
|
118
|
+
name: string;
|
|
119
|
+
stepIndex?: number;
|
|
120
|
+
callId?: string;
|
|
121
|
+
},
|
|
122
|
+
): Promise<unknown> {
|
|
123
|
+
const chunks: unknown[] = [];
|
|
124
|
+
let streamedResult: unknown = null;
|
|
125
|
+
|
|
126
|
+
for await (const chunk of args.stream) {
|
|
127
|
+
chunks.push(chunk);
|
|
128
|
+
if (chunk && typeof chunk === 'object' && 'result' in chunk) {
|
|
129
|
+
streamedResult = (chunk as { result: unknown }).result;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const terminalId = getTerminalId(chunk);
|
|
134
|
+
if (terminalId) {
|
|
135
|
+
publishToolDelta(ctx, {
|
|
136
|
+
name: args.name,
|
|
137
|
+
channel: 'terminal',
|
|
138
|
+
delta: terminalId,
|
|
139
|
+
stepIndex: args.stepIndex,
|
|
140
|
+
callId: args.callId,
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const delta = getStringDelta(chunk);
|
|
146
|
+
if (!delta) continue;
|
|
147
|
+
|
|
148
|
+
publishToolDelta(ctx, {
|
|
149
|
+
name: args.name,
|
|
150
|
+
channel: getStringChannel(chunk),
|
|
151
|
+
delta,
|
|
152
|
+
stepIndex: args.stepIndex,
|
|
153
|
+
callId: args.callId,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
streamedResult ?? (chunks.length > 0 ? chunks[chunks.length - 1] : null)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type PendingCallMeta = {
|
|
2
|
+
callId: string;
|
|
3
|
+
startTs: number;
|
|
4
|
+
stepIndex?: number;
|
|
5
|
+
args?: unknown;
|
|
6
|
+
approvalPromise?: Promise<boolean>;
|
|
7
|
+
blocked?: boolean;
|
|
8
|
+
blockReason?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function getPendingQueue(
|
|
12
|
+
map: Map<string, PendingCallMeta[]>,
|
|
13
|
+
name: string,
|
|
14
|
+
): PendingCallMeta[] {
|
|
15
|
+
let queue = map.get(name);
|
|
16
|
+
if (!queue) {
|
|
17
|
+
queue = [];
|
|
18
|
+
map.set(name, queue);
|
|
19
|
+
}
|
|
20
|
+
return queue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function shiftPendingCall(
|
|
24
|
+
map: Map<string, PendingCallMeta[]>,
|
|
25
|
+
name: string,
|
|
26
|
+
): PendingCallMeta | undefined {
|
|
27
|
+
const queue = map.get(name);
|
|
28
|
+
const meta = queue?.shift();
|
|
29
|
+
if (queue && queue.length === 0) {
|
|
30
|
+
map.delete(name);
|
|
31
|
+
}
|
|
32
|
+
return meta;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function extractToolCallId(options: unknown): string | undefined {
|
|
36
|
+
return (options as { toolCallId?: string } | undefined)?.toolCallId;
|
|
37
|
+
}
|