@bibobonking/opensol 0.0.0-stage → 3.0.1

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 (114) hide show
  1. package/.github/workflows/build.yml +25 -0
  2. package/README.md +74 -2
  3. package/dist/assets/index-BmN7pBji.css +1 -0
  4. package/dist/assets/index-DgNDjkOz.js +301 -0
  5. package/dist/favicon.svg +15 -0
  6. package/dist/index.html +55 -0
  7. package/dist/preview-host.html +33 -0
  8. package/index.html +54 -0
  9. package/package.json +43 -4
  10. package/postcss.config.js +6 -0
  11. package/public/favicon.svg +15 -0
  12. package/public/preview-host.html +33 -0
  13. package/shared/plugin-builder.test.mjs +43 -0
  14. package/shared/plugin-package.d.mts +34 -0
  15. package/shared/plugin-package.mjs +197 -0
  16. package/shared/plugin-package.test.mjs +94 -0
  17. package/src/App.tsx +152 -0
  18. package/src/agent/adapters/anthropic.ts +233 -0
  19. package/src/agent/adapters/gemini.ts +225 -0
  20. package/src/agent/adapters/openai-responses.ts +215 -0
  21. package/src/agent/adapters/openai.ts +200 -0
  22. package/src/agent/adapters/shared.ts +96 -0
  23. package/src/agent/approval.ts +20 -0
  24. package/src/agent/compact.ts +234 -0
  25. package/src/agent/index.ts +36 -0
  26. package/src/agent/model.ts +20 -0
  27. package/src/agent/prompt.ts +107 -0
  28. package/src/agent/queue.test.mjs +111 -0
  29. package/src/agent/queue.ts +84 -0
  30. package/src/agent/subagent.ts +109 -0
  31. package/src/agent/tools.ts +541 -0
  32. package/src/components/ChatView.tsx +403 -0
  33. package/src/components/CommandPalette.tsx +416 -0
  34. package/src/components/Composer.tsx +678 -0
  35. package/src/components/ContextBar.tsx +58 -0
  36. package/src/components/DeployModal.tsx +512 -0
  37. package/src/components/ErrorBoundary.tsx +75 -0
  38. package/src/components/HomeDashboard.tsx +189 -0
  39. package/src/components/Logo.tsx +41 -0
  40. package/src/components/Markdown.tsx +232 -0
  41. package/src/components/MessageItem.tsx +655 -0
  42. package/src/components/ModelPicker.tsx +487 -0
  43. package/src/components/PixleSol.tsx +243 -0
  44. package/src/components/PluginBuilder.tsx +285 -0
  45. package/src/components/PluginManager.tsx +341 -0
  46. package/src/components/PreviewPane.tsx +166 -0
  47. package/src/components/SettingsModal.tsx +2052 -0
  48. package/src/components/Sidebar.tsx +504 -0
  49. package/src/components/Splash.tsx +104 -0
  50. package/src/components/ToolCallCard.tsx +177 -0
  51. package/src/components/TopBar.tsx +182 -0
  52. package/src/components/TrajectoryView.tsx +178 -0
  53. package/src/components/WorkspacePanel.tsx +710 -0
  54. package/src/components/ui.tsx +311 -0
  55. package/src/core/kernel.ts +45 -0
  56. package/src/core/plugins/tools/fs.ts +219 -0
  57. package/src/core/plugins/tools/git.ts +83 -0
  58. package/src/core/plugins/tools/grep.ts +98 -0
  59. package/src/core/plugins/tools/shell.ts +88 -0
  60. package/src/core/plugins/tools/subagent.ts +36 -0
  61. package/src/core/plugins/tools/web.ts +97 -0
  62. package/src/core/services/agent-loop.ts +417 -0
  63. package/src/core/services/approval.ts +66 -0
  64. package/src/core/services/compaction.ts +45 -0
  65. package/src/core/services/llm.ts +32 -0
  66. package/src/core/services/sessions.ts +47 -0
  67. package/src/core/services/subagents.ts +15 -0
  68. package/src/core/services/system-prompt.ts +177 -0
  69. package/src/core/services/tools.ts +135 -0
  70. package/src/core/services/ui.ts +109 -0
  71. package/src/core/types.ts +139 -0
  72. package/src/data/providers.ts +125 -0
  73. package/src/data/skills.ts +68 -0
  74. package/src/index.css +776 -0
  75. package/src/lib/boot.ts +94 -0
  76. package/src/lib/context.ts +75 -0
  77. package/src/lib/conversation.test.mjs +64 -0
  78. package/src/lib/conversation.ts +59 -0
  79. package/src/lib/daemon.ts +475 -0
  80. package/src/lib/hooks.ts +33 -0
  81. package/src/lib/permissions.ts +60 -0
  82. package/src/lib/pixlesol.test.mjs +51 -0
  83. package/src/lib/pixlesol.ts +55 -0
  84. package/src/lib/plugin-folder.test.mjs +145 -0
  85. package/src/lib/plugin-folder.ts +96 -0
  86. package/src/lib/reasoning.ts +87 -0
  87. package/src/lib/sse.ts +75 -0
  88. package/src/lib/themes.test.mjs +72 -0
  89. package/src/lib/themes.ts +52 -0
  90. package/src/lib/usage.test.mjs +72 -0
  91. package/src/lib/usage.ts +155 -0
  92. package/src/lib/utils.ts +73 -0
  93. package/src/main.tsx +16 -0
  94. package/src/plugins/api.test.mjs +77 -0
  95. package/src/plugins/api.ts +113 -0
  96. package/src/plugins/bridge.d.mts +1 -0
  97. package/src/plugins/bridge.mjs +26 -0
  98. package/src/plugins/bridge.test.mjs +17 -0
  99. package/src/plugins/index.ts +52 -0
  100. package/src/plugins/preview.test.mjs +18 -0
  101. package/src/plugins/preview.ts +34 -0
  102. package/src/plugins/runtime.ts +194 -0
  103. package/src/plugins/types.ts +40 -0
  104. package/src/store/chats.ts +278 -0
  105. package/src/store/models.ts +120 -0
  106. package/src/store/settings.ts +445 -0
  107. package/src/store/ui.ts +132 -0
  108. package/src/store/workspace.ts +299 -0
  109. package/src/types.ts +337 -0
  110. package/src/vite-env.d.ts +1 -0
  111. package/tailwind.config.js +83 -0
  112. package/tsconfig.json +22 -0
  113. package/tsconfig.tsbuildinfo +1 -0
  114. package/vite.config.ts +19 -0
@@ -0,0 +1,215 @@
1
+ import type { Message, ToolCall } from '../../types'
2
+ import { requestLLM } from '../../lib/daemon'
3
+ import { effortValues, normalizeEffort } from '../../lib/reasoning'
4
+ import { readSSE } from '../../lib/sse'
5
+ import { getToolSchemas } from '../tools'
6
+ import {
7
+ assistantHistory,
8
+ imageAttachments,
9
+ parseToolArgs,
10
+ toolResultText,
11
+ userText,
12
+ type AdapterRequest,
13
+ type AdapterResult,
14
+ } from './shared'
15
+
16
+ function buildInput(history: Message[]): Array<Record<string, unknown>> {
17
+ const input: Array<Record<string, unknown>> = []
18
+
19
+ for (const message of history) {
20
+ if (message.role === 'user') {
21
+ const images = imageAttachments(message)
22
+ const text = userText(message) || (images.length > 0 ? 'See the attached image.' : '')
23
+ if (!text.trim() && images.length === 0) continue
24
+ if (images.length === 0) {
25
+ input.push({ role: 'user', content: text })
26
+ continue
27
+ }
28
+ const content: Array<Record<string, unknown>> = [{ type: 'input_text', text }]
29
+ for (const image of images) {
30
+ content.push({ type: 'input_image', image_url: image.dataUrl, detail: 'auto' })
31
+ }
32
+ input.push({ role: 'user', content })
33
+ continue
34
+ }
35
+
36
+ if (message.role === 'assistant') {
37
+ if (!assistantHistory(message)) continue
38
+ if (message.content.trim()) {
39
+ input.push({ role: 'assistant', content: message.content })
40
+ }
41
+ for (const call of message.toolCalls) {
42
+ input.push({
43
+ type: 'function_call',
44
+ call_id: call.id,
45
+ name: call.name,
46
+ arguments: JSON.stringify(call.args),
47
+ })
48
+ }
49
+ continue
50
+ }
51
+
52
+ input.push({
53
+ type: 'function_call_output',
54
+ call_id: message.call.id,
55
+ output: toolResultText(message.result, message.status),
56
+ })
57
+ }
58
+
59
+ return input
60
+ }
61
+
62
+ interface PendingCall {
63
+ id: string
64
+ name: string
65
+ args: string
66
+ order: number
67
+ }
68
+
69
+ function itemRecord(value: unknown): Record<string, unknown> {
70
+ return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
71
+ }
72
+
73
+ export async function streamOpenAIResponses(request: AdapterRequest): Promise<AdapterResult> {
74
+ const { settings, model, history, system, mode, signal, handlers } = request
75
+ const modelId = settings.model.trim()
76
+ if (!modelId) throw new Error('Choose a model before sending a message.')
77
+ const tools = getToolSchemas(settings, mode)
78
+ const availableTools = new Set<string>(tools.map((tool) => tool.name))
79
+
80
+ const body: Record<string, unknown> = {
81
+ model: modelId,
82
+ instructions: system,
83
+ input: buildInput(history),
84
+ max_output_tokens: settings.maxTokens,
85
+ tool_choice: 'auto',
86
+ }
87
+ if (tools.length > 0) {
88
+ body.tools = tools.map((tool) => ({
89
+ type: 'function',
90
+ name: tool.name,
91
+ description: tool.description,
92
+ parameters: tool.parameters,
93
+ strict: false,
94
+ }))
95
+ }
96
+
97
+ const efforts = effortValues(model)
98
+ const effort = efforts.length > 0 ? normalizeEffort(settings.effort, model) : 'none'
99
+ if (effort !== 'none') {
100
+ body.reasoning = { effort }
101
+ }
102
+
103
+ const response = await requestLLM(settings.provider, 'openai-responses', modelId, body, signal)
104
+
105
+ let content = ''
106
+ let reasoning = ''
107
+ let usage: AdapterResult['usage'] = {}
108
+ const calls = new Map<string, PendingCall>()
109
+ let order = 0
110
+
111
+ const upsertCall = (key: string, item: Record<string, unknown>) => {
112
+ const existing = calls.get(key)
113
+ const id = typeof item.call_id === 'string' ? item.call_id : existing?.id || key
114
+ const name = typeof item.name === 'string' ? item.name : existing?.name || ''
115
+ const args = typeof item.arguments === 'string' ? item.arguments : existing?.args || ''
116
+ calls.set(key, { id, name, args, order: existing?.order ?? order++ })
117
+ }
118
+
119
+ for await (const data of readSSE(response)) {
120
+ if (data === '[DONE]') break
121
+ let event: Record<string, unknown>
122
+ try {
123
+ event = JSON.parse(data) as Record<string, unknown>
124
+ } catch {
125
+ continue
126
+ }
127
+ const type = typeof event.type === 'string' ? event.type : ''
128
+
129
+ if (type === 'error' || type === 'response.failed') {
130
+ const detail = itemRecord(event.response).error ?? event.error ?? event.message
131
+ const error = itemRecord(detail)
132
+ throw new Error(
133
+ typeof detail === 'string'
134
+ ? detail
135
+ : typeof error.message === 'string'
136
+ ? error.message
137
+ : 'The model request failed.',
138
+ )
139
+ }
140
+
141
+ if (type === 'response.output_text.delta' && typeof event.delta === 'string') {
142
+ content += event.delta
143
+ handlers.onText(event.delta)
144
+ continue
145
+ }
146
+
147
+ if (
148
+ (type === 'response.reasoning_summary_text.delta' ||
149
+ type === 'response.reasoning_text.delta') &&
150
+ typeof event.delta === 'string'
151
+ ) {
152
+ reasoning += event.delta
153
+ handlers.onReasoning(event.delta)
154
+ continue
155
+ }
156
+
157
+ if (type === 'response.output_item.added') {
158
+ const item = itemRecord(event.item)
159
+ if (item.type === 'function_call') {
160
+ const key = typeof item.id === 'string' ? item.id : typeof item.call_id === 'string' ? item.call_id : ''
161
+ if (key) upsertCall(key, item)
162
+ }
163
+ continue
164
+ }
165
+
166
+ if (type === 'response.function_call_arguments.delta') {
167
+ const key = typeof event.item_id === 'string' ? event.item_id : ''
168
+ const entry = key ? calls.get(key) : undefined
169
+ if (entry && typeof event.delta === 'string') entry.args += event.delta
170
+ continue
171
+ }
172
+
173
+ if (type === 'response.function_call_arguments.done') {
174
+ const key = typeof event.item_id === 'string' ? event.item_id : ''
175
+ const entry = key ? calls.get(key) : undefined
176
+ if (entry && typeof event.arguments === 'string') entry.args = event.arguments
177
+ continue
178
+ }
179
+
180
+ if (type === 'response.output_item.done') {
181
+ const item = itemRecord(event.item)
182
+ if (item.type === 'function_call') {
183
+ const key = typeof item.id === 'string' ? item.id : typeof item.call_id === 'string' ? item.call_id : ''
184
+ if (key) upsertCall(key, item)
185
+ }
186
+ continue
187
+ }
188
+
189
+ if (type === 'response.completed') {
190
+ const done = itemRecord(event.response)
191
+ const value = itemRecord(done.usage)
192
+ if (typeof value.input_tokens === 'number' || typeof value.output_tokens === 'number') {
193
+ usage = { input: Number(value.input_tokens) || undefined, output: Number(value.output_tokens) || undefined }
194
+ handlers.onUsage(usage)
195
+ }
196
+ }
197
+ }
198
+
199
+ const toolCalls: ToolCall[] = []
200
+ const pending = [...calls.entries()].sort((a, b) => a[1].order - b[1].order)
201
+ for (const [key, call] of pending) {
202
+ if (!call.name) continue
203
+ if (!availableTools.has(call.name)) {
204
+ throw new Error(`The model requested a tool that is not enabled: ${call.name}`)
205
+ }
206
+ toolCalls.push({
207
+ id: call.id || key,
208
+ name: call.name as ToolCall['name'],
209
+ args: parseToolArgs(call.args, call.name),
210
+ })
211
+ }
212
+ if (toolCalls.length > 0) handlers.onToolCalls(toolCalls)
213
+
214
+ return { content, reasoning, toolCalls, usage }
215
+ }
@@ -0,0 +1,200 @@
1
+ import type { Message, ToolCall } from '../../types'
2
+ import { requestLLM } from '../../lib/daemon'
3
+ import { effortValues, normalizeEffort } from '../../lib/reasoning'
4
+ import { readSSE } from '../../lib/sse'
5
+ import { getToolSchemas } from '../tools'
6
+ import {
7
+ assistantHistory,
8
+ imageAttachments,
9
+ parseToolArgs,
10
+ toolResultText,
11
+ userText,
12
+ type AdapterRequest,
13
+ type AdapterResult,
14
+ } from './shared'
15
+
16
+ type OpenAIMessage = Record<string, unknown>
17
+
18
+ function buildMessages(history: Message[]): OpenAIMessage[] {
19
+ const messages: OpenAIMessage[] = []
20
+
21
+ for (const message of history) {
22
+ if (message.role === 'user') {
23
+ const images = imageAttachments(message)
24
+ if (images.length > 0) {
25
+ const content: Array<Record<string, unknown>> = [
26
+ { type: 'text', text: userText(message) || 'See the attached image.' },
27
+ ]
28
+ for (const image of images) {
29
+ content.push({ type: 'image_url', image_url: { url: image.dataUrl, detail: 'auto' } })
30
+ }
31
+ messages.push({ role: 'user', content })
32
+ } else {
33
+ const text = userText(message)
34
+ if (text.trim()) messages.push({ role: 'user', content: text })
35
+ }
36
+ continue
37
+ }
38
+
39
+ if (message.role === 'assistant') {
40
+ if (!assistantHistory(message)) continue
41
+ const item: OpenAIMessage = {
42
+ role: 'assistant',
43
+ content: message.content || null,
44
+ }
45
+ if (message.toolCalls.length > 0) {
46
+ item.tool_calls = message.toolCalls.map((call) => ({
47
+ id: call.id,
48
+ type: 'function',
49
+ function: { name: call.name, arguments: JSON.stringify(call.args) },
50
+ }))
51
+ }
52
+ messages.push(item)
53
+ continue
54
+ }
55
+
56
+ messages.push({
57
+ role: 'tool',
58
+ tool_call_id: message.call.id,
59
+ content: toolResultText(message.result, message.status),
60
+ })
61
+ }
62
+
63
+ return messages
64
+ }
65
+
66
+ function usesCompletionTokens(provider: string, modelId: string) {
67
+ if (provider !== 'openai') return false
68
+ return /^(gpt-5|o[1-9]|codex)/i.test(modelId)
69
+ }
70
+
71
+ export async function streamOpenAI(request: AdapterRequest): Promise<AdapterResult> {
72
+ const { settings, model, history, system, mode, signal, handlers } = request
73
+ const modelId = settings.model.trim()
74
+ if (!modelId) throw new Error('Choose a model before sending a message.')
75
+ const tools = getToolSchemas(settings, mode)
76
+ const availableTools = new Set<string>(tools.map((tool) => tool.name))
77
+ const modelTools = tools.map((tool) => ({
78
+ type: 'function',
79
+ function: {
80
+ name: tool.name,
81
+ description: tool.description,
82
+ parameters: tool.parameters,
83
+ },
84
+ }))
85
+
86
+ const body: Record<string, unknown> = {
87
+ model: modelId,
88
+ messages: [{ role: 'system', content: system }, ...buildMessages(history)],
89
+ stream: true,
90
+ }
91
+ if (modelTools.length > 0) {
92
+ body.tools = modelTools
93
+ body.tool_choice = 'auto'
94
+ }
95
+ if (settings.provider === 'openai' || settings.provider === 'openrouter') {
96
+ body.stream_options = { include_usage: true }
97
+ }
98
+ if (usesCompletionTokens(settings.provider, modelId)) {
99
+ body.max_completion_tokens = settings.maxTokens
100
+ } else {
101
+ body.max_tokens = settings.maxTokens
102
+ }
103
+
104
+ const efforts = effortValues(model)
105
+ let reasoningEffort = efforts.length > 0 ? normalizeEffort(settings.effort, model) : ''
106
+ if (reasoningEffort === 'none') reasoningEffort = ''
107
+ if (reasoningEffort) {
108
+ if (settings.provider === 'openrouter') body.reasoning = { effort: reasoningEffort }
109
+ else body.reasoning_effort = reasoningEffort
110
+ }
111
+
112
+ const supportsTemperature = model ? model.temperature : !usesCompletionTokens(settings.provider, modelId)
113
+ if (!reasoningEffort && supportsTemperature) body.temperature = settings.temperature
114
+
115
+ const response = await requestLLM(settings.provider, 'openai-chat', modelId, body, signal)
116
+
117
+ let content = ''
118
+ let reasoning = ''
119
+ let usage: AdapterResult['usage'] = {}
120
+ const toolAccumulator = new Map<number, { id: string; name: string; arguments: string }>()
121
+
122
+ for await (const data of readSSE(response)) {
123
+ if (data === '[DONE]') break
124
+ let payload: Record<string, unknown>
125
+ try {
126
+ payload = JSON.parse(data) as Record<string, unknown>
127
+ } catch {
128
+ continue
129
+ }
130
+
131
+ if (payload.error) {
132
+ const error = payload.error as { message?: string } | string
133
+ throw new Error(typeof error === 'string' ? error : (error.message ?? 'The provider returned an error.'))
134
+ }
135
+
136
+ if (payload.usage && typeof payload.usage === 'object') {
137
+ const value = payload.usage as { prompt_tokens?: number; completion_tokens?: number }
138
+ usage = { input: value.prompt_tokens, output: value.completion_tokens }
139
+ handlers.onUsage(usage)
140
+ }
141
+
142
+ const choices = payload.choices as
143
+ | Array<{
144
+ delta?: {
145
+ content?: string
146
+ reasoning?: string
147
+ reasoning_content?: string
148
+ reasoning_details?: Array<{ text?: string }>
149
+ tool_calls?: Array<{
150
+ index?: number
151
+ id?: string
152
+ function?: { name?: string; arguments?: string }
153
+ }>
154
+ }
155
+ }>
156
+ | undefined
157
+ const delta = choices?.[0]?.delta
158
+ if (!delta) continue
159
+
160
+ if (typeof delta.content === 'string' && delta.content) {
161
+ content += delta.content
162
+ handlers.onText(delta.content)
163
+ }
164
+ const reasoningDelta =
165
+ typeof delta.reasoning_content === 'string'
166
+ ? delta.reasoning_content
167
+ : typeof delta.reasoning === 'string'
168
+ ? delta.reasoning
169
+ : (delta.reasoning_details ?? []).map((item) => item.text ?? '').join('')
170
+ if (reasoningDelta) {
171
+ reasoning += reasoningDelta
172
+ handlers.onReasoning(reasoningDelta)
173
+ }
174
+
175
+ for (const [fallbackIndex, call] of (delta.tool_calls ?? []).entries()) {
176
+ const index = typeof call.index === 'number' ? call.index : fallbackIndex
177
+ const existing = toolAccumulator.get(index) ?? { id: '', name: '', arguments: '' }
178
+ if (call.id) existing.id = call.id
179
+ if (call.function?.name) existing.name = call.function.name
180
+ if (call.function?.arguments) existing.arguments += call.function.arguments
181
+ toolAccumulator.set(index, existing)
182
+ }
183
+ }
184
+
185
+ const toolCalls: ToolCall[] = []
186
+ for (const [index, call] of [...toolAccumulator.entries()].sort((a, b) => a[0] - b[0])) {
187
+ if (!call.name) continue
188
+ if (!availableTools.has(call.name)) {
189
+ throw new Error(`The model requested a tool that is not enabled: ${call.name}`)
190
+ }
191
+ toolCalls.push({
192
+ id: call.id || `call_${Date.now().toString(36)}_${index}`,
193
+ name: call.name as ToolCall['name'],
194
+ args: parseToolArgs(call.arguments, call.name),
195
+ })
196
+ }
197
+ if (toolCalls.length > 0) handlers.onToolCalls(toolCalls)
198
+
199
+ return { content, reasoning, toolCalls, usage }
200
+ }
@@ -0,0 +1,96 @@
1
+ import type {
2
+ AgentMode,
3
+ AppSettings,
4
+ AssistantMessage,
5
+ Message,
6
+ ModelInfo,
7
+ StreamHandlers,
8
+ ToolCall,
9
+ Usage,
10
+ } from '../../types'
11
+
12
+ export interface AdapterRequest {
13
+ settings: AppSettings
14
+ model: ModelInfo | null
15
+ history: Message[]
16
+ system: string
17
+ mode: AgentMode
18
+ signal: AbortSignal
19
+ handlers: StreamHandlers
20
+ }
21
+
22
+ export interface AdapterResult {
23
+ content: string
24
+ reasoning: string
25
+ toolCalls: ToolCall[]
26
+ thinkingBlocks?: Array<{ text: string; signature?: string }>
27
+ usage: Usage
28
+ }
29
+
30
+ export const EMPTY_HANDLERS: StreamHandlers = {
31
+ onText: () => undefined,
32
+ onReasoning: () => undefined,
33
+ onToolCalls: () => undefined,
34
+ onUsage: () => undefined,
35
+ }
36
+
37
+ export function userText(message: Extract<Message, { role: 'user' }>): string {
38
+ const parts = [message.content]
39
+ for (const attachment of message.attachments ?? []) {
40
+ if (attachment.kind === 'text' && attachment.text) {
41
+ parts.push(`\n\n--- Attached file: ${attachment.name} ---\n${attachment.text}`)
42
+ }
43
+ }
44
+ return parts.filter(Boolean).join('')
45
+ }
46
+
47
+ export function assistantHistory(message: AssistantMessage): boolean {
48
+ return Boolean(message.content.trim() || message.toolCalls.length > 0)
49
+ }
50
+
51
+ export function imageAttachments(message: Extract<Message, { role: 'user' }>) {
52
+ return (message.attachments ?? []).filter(
53
+ (attachment) => attachment.kind === 'image' && attachment.dataUrl,
54
+ )
55
+ }
56
+
57
+ export function parseToolArgs(raw: string, toolName: string): Record<string, unknown> {
58
+ if (!raw.trim()) return {}
59
+ try {
60
+ const parsed = JSON.parse(raw) as unknown
61
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
62
+ return parsed as Record<string, unknown>
63
+ }
64
+ throw new Error('arguments must be a JSON object')
65
+ } catch (error) {
66
+ throw new Error(`The model returned invalid arguments for ${toolName}: ${(error as Error).message}`)
67
+ }
68
+ }
69
+
70
+ export function toolResultText(result: string | undefined, status: string): string {
71
+ if (status === 'done') return result ?? '(no output)'
72
+ if (status === 'denied') return 'The user denied this tool call.'
73
+ if (status === 'error') return `Tool error: ${result ?? 'unknown error'}`
74
+ if (status === 'awaiting') return 'The tool call was interrupted before approval.'
75
+ return result ?? '(no output)'
76
+ }
77
+
78
+ export function collectToolCalls(
79
+ candidates: Array<{ id: string; name: string; args: Record<string, unknown>; thoughtSignature?: string }>,
80
+ available: Set<string>,
81
+ ): ToolCall[] {
82
+ const calls: ToolCall[] = []
83
+ for (const candidate of candidates) {
84
+ if (!candidate.name) continue
85
+ if (!available.has(candidate.name)) {
86
+ throw new Error(`The model requested a tool that is not enabled: ${candidate.name}`)
87
+ }
88
+ calls.push({
89
+ id: candidate.id,
90
+ name: candidate.name as ToolCall['name'],
91
+ args: candidate.args,
92
+ ...(candidate.thoughtSignature ? { thoughtSignature: candidate.thoughtSignature } : {}),
93
+ })
94
+ }
95
+ return calls
96
+ }
@@ -0,0 +1,20 @@
1
+ import { kernel } from '../core/kernel'
2
+ import {
3
+ waitForApproval as coreWaitForApproval,
4
+ resolveApproval as coreResolveApproval,
5
+ } from '../core/services/approval'
6
+
7
+ export function waitForApproval(toolCallId: string, signal: AbortSignal): Promise<boolean> {
8
+ if (kernel.approval) {
9
+ return kernel.approval.waitForApproval(toolCallId, signal)
10
+ }
11
+ return coreWaitForApproval(toolCallId, signal)
12
+ }
13
+
14
+ export function resolveApproval(toolCallId: string, allow: boolean): void {
15
+ if (kernel.approval) {
16
+ kernel.approval.resolveApproval(toolCallId, allow)
17
+ } else {
18
+ coreResolveApproval(toolCallId, allow)
19
+ }
20
+ }