@bibobonking/opensol 0.0.0-stage → 3.0.2
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/.github/workflows/build.yml +25 -0
- package/README.md +74 -2
- package/dist/assets/index-BmN7pBji.css +1 -0
- package/dist/assets/index-DgNDjkOz.js +301 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +55 -0
- package/dist/preview-host.html +33 -0
- package/index.html +54 -0
- package/package.json +43 -4
- package/postcss.config.js +6 -0
- package/public/favicon.svg +15 -0
- package/public/preview-host.html +33 -0
- package/server/README.md +75 -0
- package/server/daemon.mjs +1082 -0
- package/server/lib/checkpoints.mjs +124 -0
- package/server/lib/checkpoints.test.mjs +110 -0
- package/server/lib/deploy.mjs +436 -0
- package/server/lib/http.mjs +237 -0
- package/server/lib/plugins.mjs +321 -0
- package/server/lib/plugins.test.mjs +140 -0
- package/server/lib/providers.mjs +594 -0
- package/server/lib/store.mjs +491 -0
- package/server/lib/tools.mjs +990 -0
- package/server/opensol.mjs +202 -0
- package/server/package.json +42 -0
- package/shared/plugin-builder.test.mjs +43 -0
- package/shared/plugin-package.d.mts +34 -0
- package/shared/plugin-package.mjs +197 -0
- package/shared/plugin-package.test.mjs +94 -0
- package/src/App.tsx +152 -0
- package/src/agent/adapters/anthropic.ts +233 -0
- package/src/agent/adapters/gemini.ts +225 -0
- package/src/agent/adapters/openai-responses.ts +215 -0
- package/src/agent/adapters/openai.ts +200 -0
- package/src/agent/adapters/shared.ts +96 -0
- package/src/agent/approval.ts +20 -0
- package/src/agent/compact.ts +234 -0
- package/src/agent/index.ts +36 -0
- package/src/agent/model.ts +20 -0
- package/src/agent/prompt.ts +107 -0
- package/src/agent/queue.test.mjs +111 -0
- package/src/agent/queue.ts +84 -0
- package/src/agent/subagent.ts +109 -0
- package/src/agent/tools.ts +541 -0
- package/src/components/ChatView.tsx +403 -0
- package/src/components/CommandPalette.tsx +416 -0
- package/src/components/Composer.tsx +678 -0
- package/src/components/ContextBar.tsx +58 -0
- package/src/components/DeployModal.tsx +512 -0
- package/src/components/ErrorBoundary.tsx +75 -0
- package/src/components/HomeDashboard.tsx +189 -0
- package/src/components/Logo.tsx +41 -0
- package/src/components/Markdown.tsx +232 -0
- package/src/components/MessageItem.tsx +655 -0
- package/src/components/ModelPicker.tsx +487 -0
- package/src/components/PixleSol.tsx +243 -0
- package/src/components/PluginBuilder.tsx +285 -0
- package/src/components/PluginManager.tsx +341 -0
- package/src/components/PreviewPane.tsx +166 -0
- package/src/components/SettingsModal.tsx +2052 -0
- package/src/components/Sidebar.tsx +504 -0
- package/src/components/Splash.tsx +104 -0
- package/src/components/ToolCallCard.tsx +177 -0
- package/src/components/TopBar.tsx +182 -0
- package/src/components/TrajectoryView.tsx +178 -0
- package/src/components/WorkspacePanel.tsx +710 -0
- package/src/components/ui.tsx +311 -0
- package/src/core/kernel.ts +45 -0
- package/src/core/plugins/tools/fs.ts +219 -0
- package/src/core/plugins/tools/git.ts +83 -0
- package/src/core/plugins/tools/grep.ts +98 -0
- package/src/core/plugins/tools/shell.ts +88 -0
- package/src/core/plugins/tools/subagent.ts +36 -0
- package/src/core/plugins/tools/web.ts +97 -0
- package/src/core/services/agent-loop.ts +417 -0
- package/src/core/services/approval.ts +66 -0
- package/src/core/services/compaction.ts +45 -0
- package/src/core/services/llm.ts +32 -0
- package/src/core/services/sessions.ts +47 -0
- package/src/core/services/subagents.ts +15 -0
- package/src/core/services/system-prompt.ts +177 -0
- package/src/core/services/tools.ts +135 -0
- package/src/core/services/ui.ts +109 -0
- package/src/core/types.ts +139 -0
- package/src/data/providers.ts +125 -0
- package/src/data/skills.ts +68 -0
- package/src/index.css +776 -0
- package/src/lib/boot.ts +94 -0
- package/src/lib/context.ts +75 -0
- package/src/lib/conversation.test.mjs +64 -0
- package/src/lib/conversation.ts +59 -0
- package/src/lib/daemon.ts +475 -0
- package/src/lib/hooks.ts +33 -0
- package/src/lib/permissions.ts +60 -0
- package/src/lib/pixlesol.test.mjs +51 -0
- package/src/lib/pixlesol.ts +55 -0
- package/src/lib/plugin-folder.test.mjs +145 -0
- package/src/lib/plugin-folder.ts +96 -0
- package/src/lib/reasoning.ts +87 -0
- package/src/lib/sse.ts +75 -0
- package/src/lib/themes.test.mjs +72 -0
- package/src/lib/themes.ts +52 -0
- package/src/lib/usage.test.mjs +72 -0
- package/src/lib/usage.ts +155 -0
- package/src/lib/utils.ts +73 -0
- package/src/main.tsx +16 -0
- package/src/plugins/api.test.mjs +77 -0
- package/src/plugins/api.ts +113 -0
- package/src/plugins/bridge.d.mts +1 -0
- package/src/plugins/bridge.mjs +26 -0
- package/src/plugins/bridge.test.mjs +17 -0
- package/src/plugins/index.ts +52 -0
- package/src/plugins/preview.test.mjs +18 -0
- package/src/plugins/preview.ts +34 -0
- package/src/plugins/runtime.ts +194 -0
- package/src/plugins/types.ts +40 -0
- package/src/store/chats.ts +278 -0
- package/src/store/models.ts +120 -0
- package/src/store/settings.ts +445 -0
- package/src/store/ui.ts +132 -0
- package/src/store/workspace.ts +299 -0
- package/src/types.ts +337 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +83 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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
|
+
}
|