@ottocode/server 0.1.265 → 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/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +2 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +67 -264
- 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';
|
|
@@ -210,6 +81,45 @@ function getReasoningProviderTarget(
|
|
|
210
81
|
return null;
|
|
211
82
|
}
|
|
212
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
|
+
|
|
213
123
|
export function buildReasoningConfig(args: {
|
|
214
124
|
cfg?: OttoConfig;
|
|
215
125
|
provider: ProviderId;
|
|
@@ -226,16 +136,8 @@ export function buildReasoningConfig(args: {
|
|
|
226
136
|
reasoningLevel,
|
|
227
137
|
maxOutputTokens,
|
|
228
138
|
} = args;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
definition?.compatibility === 'ollama'
|
|
232
|
-
? true
|
|
233
|
-
: definition?.source === 'custom'
|
|
234
|
-
? true
|
|
235
|
-
: provider === 'ottorouter'
|
|
236
|
-
? true
|
|
237
|
-
: modelSupportsReasoning(provider, model);
|
|
238
|
-
if (!reasoningText || !supportsReasoning) {
|
|
139
|
+
|
|
140
|
+
if (!reasoningText || !isReasoningSupported({ cfg, provider, model })) {
|
|
239
141
|
return {
|
|
240
142
|
providerOptions: {},
|
|
241
143
|
effectiveMaxOutputTokens: maxOutputTokens,
|
|
@@ -244,118 +146,19 @@ export function buildReasoningConfig(args: {
|
|
|
244
146
|
}
|
|
245
147
|
|
|
246
148
|
const reasoningTarget = getReasoningProviderTarget(provider, model, cfg);
|
|
247
|
-
if (reasoningTarget
|
|
248
|
-
if (usesAdaptiveAnthropicThinking(model)) {
|
|
249
|
-
const thinking = isClaudeOpus47(model)
|
|
250
|
-
? { type: 'adaptive', display: 'summarized' }
|
|
251
|
-
: { type: 'adaptive' };
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
providerOptions: {
|
|
255
|
-
anthropic: {
|
|
256
|
-
thinking,
|
|
257
|
-
effort: toAnthropicEffort(model, reasoningLevel),
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
261
|
-
enabled: true,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const thinkingBudget = toThinkingBudget(reasoningLevel, maxOutputTokens);
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
providerOptions: {
|
|
269
|
-
anthropic: {
|
|
270
|
-
thinking: { type: 'enabled', budgetTokens: thinkingBudget },
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
effectiveMaxOutputTokens:
|
|
274
|
-
maxOutputTokens && maxOutputTokens > thinkingBudget
|
|
275
|
-
? maxOutputTokens - thinkingBudget
|
|
276
|
-
: maxOutputTokens,
|
|
277
|
-
enabled: true,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (reasoningTarget === 'openai') {
|
|
149
|
+
if (!reasoningTarget) {
|
|
282
150
|
return {
|
|
283
|
-
providerOptions: {
|
|
284
|
-
openai: {
|
|
285
|
-
reasoningEffort: toOpenAIEffort(reasoningLevel),
|
|
286
|
-
reasoningSummary: 'auto',
|
|
287
|
-
},
|
|
288
|
-
},
|
|
289
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
290
|
-
enabled: true,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (reasoningTarget === 'google') {
|
|
295
|
-
const isGemini3 = model.includes('gemini-3');
|
|
296
|
-
return {
|
|
297
|
-
providerOptions: {
|
|
298
|
-
google: {
|
|
299
|
-
thinkingConfig: isGemini3
|
|
300
|
-
? {
|
|
301
|
-
thinkingLevel: toGoogleThinkingLevel(reasoningLevel),
|
|
302
|
-
includeThoughts: true,
|
|
303
|
-
}
|
|
304
|
-
: {
|
|
305
|
-
thinkingBudget: toThinkingBudget(
|
|
306
|
-
reasoningLevel,
|
|
307
|
-
maxOutputTokens,
|
|
308
|
-
),
|
|
309
|
-
includeThoughts: true,
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
314
|
-
enabled: true,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (reasoningTarget === 'ollama') {
|
|
319
|
-
return {
|
|
320
|
-
providerOptions: {
|
|
321
|
-
ollama: {
|
|
322
|
-
think: true,
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
326
|
-
enabled: true,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (reasoningTarget === 'openrouter') {
|
|
331
|
-
return {
|
|
332
|
-
providerOptions: {
|
|
333
|
-
openrouter: {
|
|
334
|
-
reasoning: { effort: normalizeReasoningLevel(reasoningLevel) },
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
effectiveMaxOutputTokens: maxOutputTokens,
|
|
338
|
-
enabled: true,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (reasoningTarget === 'openai-compatible') {
|
|
343
|
-
return {
|
|
344
|
-
providerOptions: buildSharedProviderOptions(
|
|
345
|
-
provider,
|
|
346
|
-
{
|
|
347
|
-
reasoningEffort: normalizeReasoningLevel(reasoningLevel),
|
|
348
|
-
},
|
|
349
|
-
cfg,
|
|
350
|
-
),
|
|
151
|
+
providerOptions: {},
|
|
351
152
|
effectiveMaxOutputTokens: maxOutputTokens,
|
|
352
|
-
enabled:
|
|
153
|
+
enabled: false,
|
|
353
154
|
};
|
|
354
155
|
}
|
|
355
156
|
|
|
356
|
-
return {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
157
|
+
return buildTargetReasoningConfig(reasoningTarget, {
|
|
158
|
+
cfg,
|
|
159
|
+
provider,
|
|
160
|
+
model,
|
|
161
|
+
reasoningLevel,
|
|
162
|
+
maxOutputTokens,
|
|
163
|
+
});
|
|
361
164
|
}
|
|
@@ -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
|
+
}
|