@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.
Files changed (72) 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/provider/custom.ts +73 -0
  64. package/src/runtime/provider/index.ts +2 -85
  65. package/src/runtime/provider/reasoning-builders.ts +280 -0
  66. package/src/runtime/provider/reasoning.ts +67 -264
  67. package/src/tools/adapter/events.ts +116 -0
  68. package/src/tools/adapter/execution.ts +160 -0
  69. package/src/tools/adapter/pending.ts +37 -0
  70. package/src/tools/adapter/persistence.ts +166 -0
  71. package/src/tools/adapter/results.ts +97 -0
  72. 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';
@@ -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
- const definition = cfg ? getProviderDefinition(cfg, provider) : undefined;
230
- const supportsReasoning =
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 === 'anthropic') {
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: true,
153
+ enabled: false,
353
154
  };
354
155
  }
355
156
 
356
- return {
357
- providerOptions: {},
358
- effectiveMaxOutputTokens: maxOutputTokens,
359
- enabled: false,
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
+ }