@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.
Files changed (74) hide show
  1. package/package.json +3 -3
  2. package/src/routes/auth/copilot.ts +699 -0
  3. package/src/routes/auth/oauth.ts +578 -0
  4. package/src/routes/auth/onboarding.ts +45 -0
  5. package/src/routes/auth/providers.ts +189 -0
  6. package/src/routes/auth/service.ts +167 -0
  7. package/src/routes/auth/state.ts +23 -0
  8. package/src/routes/auth/status.ts +203 -0
  9. package/src/routes/auth/wallet.ts +229 -0
  10. package/src/routes/auth.ts +12 -2080
  11. package/src/routes/config/models-service.ts +411 -0
  12. package/src/routes/config/models.ts +6 -426
  13. package/src/routes/config/providers-service.ts +237 -0
  14. package/src/routes/config/providers.ts +10 -242
  15. package/src/routes/files/handlers.ts +297 -0
  16. package/src/routes/files/service.ts +313 -0
  17. package/src/routes/files.ts +12 -608
  18. package/src/routes/git/commit-service.ts +207 -0
  19. package/src/routes/git/commit.ts +6 -220
  20. package/src/routes/git/remote-service.ts +116 -0
  21. package/src/routes/git/remote.ts +8 -115
  22. package/src/routes/git/staging-service.ts +111 -0
  23. package/src/routes/git/staging.ts +10 -205
  24. package/src/routes/mcp/auth.ts +338 -0
  25. package/src/routes/mcp/lifecycle.ts +263 -0
  26. package/src/routes/mcp/servers.ts +212 -0
  27. package/src/routes/mcp/service.ts +664 -0
  28. package/src/routes/mcp/state.ts +13 -0
  29. package/src/routes/mcp.ts +6 -1233
  30. package/src/routes/ottorouter/billing.ts +593 -0
  31. package/src/routes/ottorouter/service.ts +92 -0
  32. package/src/routes/ottorouter/topup.ts +301 -0
  33. package/src/routes/ottorouter/wallet.ts +370 -0
  34. package/src/routes/ottorouter.ts +6 -1319
  35. package/src/routes/research/service.ts +339 -0
  36. package/src/routes/research.ts +12 -390
  37. package/src/routes/sessions/crud.ts +563 -0
  38. package/src/routes/sessions/queue.ts +242 -0
  39. package/src/routes/sessions/retry.ts +121 -0
  40. package/src/routes/sessions/service.ts +768 -0
  41. package/src/routes/sessions/share.ts +434 -0
  42. package/src/routes/sessions.ts +8 -1977
  43. package/src/routes/skills/service.ts +221 -0
  44. package/src/routes/skills/spec.ts +309 -0
  45. package/src/routes/skills.ts +31 -909
  46. package/src/routes/terminals/service.ts +326 -0
  47. package/src/routes/terminals.ts +19 -295
  48. package/src/routes/tunnel/service.ts +217 -0
  49. package/src/routes/tunnel.ts +29 -219
  50. package/src/runtime/agent/registry-prompts.ts +147 -0
  51. package/src/runtime/agent/registry.ts +6 -124
  52. package/src/runtime/agent/runner-errors.ts +116 -0
  53. package/src/runtime/agent/runner-reminders.ts +45 -0
  54. package/src/runtime/agent/runner-setup-model.ts +75 -0
  55. package/src/runtime/agent/runner-setup-prompt.ts +185 -0
  56. package/src/runtime/agent/runner-setup-tools.ts +103 -0
  57. package/src/runtime/agent/runner-setup-utils.ts +21 -0
  58. package/src/runtime/agent/runner-setup.ts +54 -288
  59. package/src/runtime/agent/runner-telemetry.ts +112 -0
  60. package/src/runtime/agent/runner-text.ts +108 -0
  61. package/src/runtime/agent/runner-tool-observer.ts +86 -0
  62. package/src/runtime/agent/runner.ts +79 -378
  63. package/src/runtime/ask/service.ts +1 -0
  64. package/src/runtime/provider/custom.ts +73 -0
  65. package/src/runtime/provider/index.ts +6 -85
  66. package/src/runtime/provider/reasoning-builders.ts +280 -0
  67. package/src/runtime/provider/reasoning.ts +68 -264
  68. package/src/runtime/provider/xai.ts +8 -0
  69. package/src/tools/adapter/events.ts +116 -0
  70. package/src/tools/adapter/execution.ts +160 -0
  71. package/src/tools/adapter/pending.ts +37 -0
  72. package/src/tools/adapter/persistence.ts +166 -0
  73. package/src/tools/adapter/results.ts +97 -0
  74. 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
- const THINKING_BUDGET = 16000;
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
- function normalizeReasoningLevel(
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
- | null {
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
- const definition = cfg ? getProviderDefinition(cfg, provider) : undefined;
229
- const supportsReasoning =
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 === 'anthropic') {
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: true,
153
+ enabled: false,
352
154
  };
353
155
  }
354
156
 
355
- return {
356
- providerOptions: {},
357
- effectiveMaxOutputTokens: maxOutputTokens,
358
- enabled: false,
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
+ }