@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.
Files changed (127) 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/server/README.md +75 -0
  14. package/server/daemon.mjs +1082 -0
  15. package/server/lib/checkpoints.mjs +124 -0
  16. package/server/lib/checkpoints.test.mjs +110 -0
  17. package/server/lib/deploy.mjs +436 -0
  18. package/server/lib/http.mjs +237 -0
  19. package/server/lib/plugins.mjs +321 -0
  20. package/server/lib/plugins.test.mjs +140 -0
  21. package/server/lib/providers.mjs +594 -0
  22. package/server/lib/store.mjs +491 -0
  23. package/server/lib/tools.mjs +990 -0
  24. package/server/opensol.mjs +202 -0
  25. package/server/package.json +42 -0
  26. package/shared/plugin-builder.test.mjs +43 -0
  27. package/shared/plugin-package.d.mts +34 -0
  28. package/shared/plugin-package.mjs +197 -0
  29. package/shared/plugin-package.test.mjs +94 -0
  30. package/src/App.tsx +152 -0
  31. package/src/agent/adapters/anthropic.ts +233 -0
  32. package/src/agent/adapters/gemini.ts +225 -0
  33. package/src/agent/adapters/openai-responses.ts +215 -0
  34. package/src/agent/adapters/openai.ts +200 -0
  35. package/src/agent/adapters/shared.ts +96 -0
  36. package/src/agent/approval.ts +20 -0
  37. package/src/agent/compact.ts +234 -0
  38. package/src/agent/index.ts +36 -0
  39. package/src/agent/model.ts +20 -0
  40. package/src/agent/prompt.ts +107 -0
  41. package/src/agent/queue.test.mjs +111 -0
  42. package/src/agent/queue.ts +84 -0
  43. package/src/agent/subagent.ts +109 -0
  44. package/src/agent/tools.ts +541 -0
  45. package/src/components/ChatView.tsx +403 -0
  46. package/src/components/CommandPalette.tsx +416 -0
  47. package/src/components/Composer.tsx +678 -0
  48. package/src/components/ContextBar.tsx +58 -0
  49. package/src/components/DeployModal.tsx +512 -0
  50. package/src/components/ErrorBoundary.tsx +75 -0
  51. package/src/components/HomeDashboard.tsx +189 -0
  52. package/src/components/Logo.tsx +41 -0
  53. package/src/components/Markdown.tsx +232 -0
  54. package/src/components/MessageItem.tsx +655 -0
  55. package/src/components/ModelPicker.tsx +487 -0
  56. package/src/components/PixleSol.tsx +243 -0
  57. package/src/components/PluginBuilder.tsx +285 -0
  58. package/src/components/PluginManager.tsx +341 -0
  59. package/src/components/PreviewPane.tsx +166 -0
  60. package/src/components/SettingsModal.tsx +2052 -0
  61. package/src/components/Sidebar.tsx +504 -0
  62. package/src/components/Splash.tsx +104 -0
  63. package/src/components/ToolCallCard.tsx +177 -0
  64. package/src/components/TopBar.tsx +182 -0
  65. package/src/components/TrajectoryView.tsx +178 -0
  66. package/src/components/WorkspacePanel.tsx +710 -0
  67. package/src/components/ui.tsx +311 -0
  68. package/src/core/kernel.ts +45 -0
  69. package/src/core/plugins/tools/fs.ts +219 -0
  70. package/src/core/plugins/tools/git.ts +83 -0
  71. package/src/core/plugins/tools/grep.ts +98 -0
  72. package/src/core/plugins/tools/shell.ts +88 -0
  73. package/src/core/plugins/tools/subagent.ts +36 -0
  74. package/src/core/plugins/tools/web.ts +97 -0
  75. package/src/core/services/agent-loop.ts +417 -0
  76. package/src/core/services/approval.ts +66 -0
  77. package/src/core/services/compaction.ts +45 -0
  78. package/src/core/services/llm.ts +32 -0
  79. package/src/core/services/sessions.ts +47 -0
  80. package/src/core/services/subagents.ts +15 -0
  81. package/src/core/services/system-prompt.ts +177 -0
  82. package/src/core/services/tools.ts +135 -0
  83. package/src/core/services/ui.ts +109 -0
  84. package/src/core/types.ts +139 -0
  85. package/src/data/providers.ts +125 -0
  86. package/src/data/skills.ts +68 -0
  87. package/src/index.css +776 -0
  88. package/src/lib/boot.ts +94 -0
  89. package/src/lib/context.ts +75 -0
  90. package/src/lib/conversation.test.mjs +64 -0
  91. package/src/lib/conversation.ts +59 -0
  92. package/src/lib/daemon.ts +475 -0
  93. package/src/lib/hooks.ts +33 -0
  94. package/src/lib/permissions.ts +60 -0
  95. package/src/lib/pixlesol.test.mjs +51 -0
  96. package/src/lib/pixlesol.ts +55 -0
  97. package/src/lib/plugin-folder.test.mjs +145 -0
  98. package/src/lib/plugin-folder.ts +96 -0
  99. package/src/lib/reasoning.ts +87 -0
  100. package/src/lib/sse.ts +75 -0
  101. package/src/lib/themes.test.mjs +72 -0
  102. package/src/lib/themes.ts +52 -0
  103. package/src/lib/usage.test.mjs +72 -0
  104. package/src/lib/usage.ts +155 -0
  105. package/src/lib/utils.ts +73 -0
  106. package/src/main.tsx +16 -0
  107. package/src/plugins/api.test.mjs +77 -0
  108. package/src/plugins/api.ts +113 -0
  109. package/src/plugins/bridge.d.mts +1 -0
  110. package/src/plugins/bridge.mjs +26 -0
  111. package/src/plugins/bridge.test.mjs +17 -0
  112. package/src/plugins/index.ts +52 -0
  113. package/src/plugins/preview.test.mjs +18 -0
  114. package/src/plugins/preview.ts +34 -0
  115. package/src/plugins/runtime.ts +194 -0
  116. package/src/plugins/types.ts +40 -0
  117. package/src/store/chats.ts +278 -0
  118. package/src/store/models.ts +120 -0
  119. package/src/store/settings.ts +445 -0
  120. package/src/store/ui.ts +132 -0
  121. package/src/store/workspace.ts +299 -0
  122. package/src/types.ts +337 -0
  123. package/src/vite-env.d.ts +1 -0
  124. package/tailwind.config.js +83 -0
  125. package/tsconfig.json +22 -0
  126. package/tsconfig.tsbuildinfo +1 -0
  127. package/vite.config.ts +19 -0
@@ -0,0 +1,417 @@
1
+ import { Service, type Context } from 'cordis'
2
+ import type { AssistantMessage, ToolMessage } from '../../types'
3
+ import { activeModelId, useSettings } from '../../store/settings'
4
+ import { createId } from '../../store/chats'
5
+ import { findModel } from '../../store/models'
6
+ import { useRuntime } from '../../store/ui'
7
+ import { getProviderName } from '../../data/providers'
8
+ import { describeRequestError } from '../../lib/sse'
9
+ import type { AdapterResult } from '../../agent/adapters/shared'
10
+ import { createTurnQueue, lastUserMessageId } from '../../agent/queue'
11
+ import type { StepContext, StepResult, TurnResult } from '../types'
12
+
13
+ const MAX_ROUNDS = 16
14
+ const MAX_RETRIES = 2
15
+ /** Provider hiccups worth another try: overload, rate limits, 5xx and dropped connections. */
16
+ const RETRYABLE = /\b(429|500|502|503|504|529)\b|rate.?limit|overloaded|temporar|timed out|could not be reached|lost the connection|failed to fetch|networkerror/i
17
+
18
+ function sleep(ms: number, signal: AbortSignal): Promise<void> {
19
+ return new Promise((resolve) => {
20
+ const finish = () => {
21
+ window.clearTimeout(timer)
22
+ signal.removeEventListener('abort', finish)
23
+ resolve()
24
+ }
25
+ const timer = window.setTimeout(finish, ms)
26
+ signal.addEventListener('abort', finish, { once: true })
27
+ })
28
+ }
29
+
30
+ function isAbortError(error: unknown) {
31
+ return (
32
+ (error instanceof DOMException && error.name === 'AbortError') ||
33
+ (error instanceof Error && error.name === 'AbortError')
34
+ )
35
+ }
36
+
37
+ function truncate(value: string, limit = 40_000) {
38
+ if (value.length <= limit) return value
39
+ return `${value.slice(0, limit)}\n… truncated (${value.length - limit} more characters)`
40
+ }
41
+
42
+ export class AgentLoopService extends Service {
43
+ static provide = 'agentLoop'
44
+ static inject = ['sessions', 'systemPrompt', 'tools', 'llm', 'approval', 'compaction', 'subagents']
45
+
46
+ private controllers = new Map<string, AbortController>()
47
+ private queue = createTurnQueue((sessionId, turnId) => this.runTurn(sessionId, turnId))
48
+
49
+ constructor(ctx: Context) {
50
+ super(ctx, 'agentLoop')
51
+ }
52
+
53
+ isBusy(sessionId: string): boolean {
54
+ return this.queue.isBusy(sessionId)
55
+ }
56
+
57
+ stopRun(sessionId: string): void {
58
+ this.queue.clear(sessionId)
59
+ this.controllers.get(sessionId)?.abort()
60
+ void this.ctx.events.serial('agent/turn-stopping', sessionId)
61
+ }
62
+
63
+ stopAllRuns(): void {
64
+ this.queue.clearAll()
65
+ this.controllers.forEach((controller, sessionId) => {
66
+ controller.abort()
67
+ void this.ctx.events.serial('agent/turn-stopping', sessionId)
68
+ })
69
+ }
70
+
71
+ runAgent(sessionId: string, userMessageId?: string): void {
72
+ const session = this.ctx.sessions.current(sessionId)
73
+ if (!session) return
74
+ const turnId = userMessageId ?? lastUserMessageId(session.messages)
75
+ if (!turnId) return
76
+ void this.queue.push(sessionId, turnId)
77
+ }
78
+
79
+ regenerateLastTurn(sessionId: string): boolean {
80
+ const session = this.ctx.sessions.current(sessionId)
81
+ if (!session || this.queue.isBusy(sessionId)) return false
82
+ let lastUser = -1
83
+ for (let index = session.messages.length - 1; index >= 0; index -= 1) {
84
+ if (session.messages[index].role === 'user') {
85
+ lastUser = index
86
+ break
87
+ }
88
+ }
89
+ if (lastUser < 0) return false
90
+ const userMessageId = session.messages[lastUser].id
91
+ this.ctx.sessions.updateSession(sessionId, {
92
+ messages: session.messages.slice(0, lastUser + 1),
93
+ })
94
+ this.runAgent(sessionId, userMessageId)
95
+ return true
96
+ }
97
+
98
+ private async runTurn(sessionId: string, userMessageId: string): Promise<void> {
99
+ const session = this.ctx.sessions.current(sessionId)
100
+ if (!session) return
101
+ if (!this.ctx.sessions.turnHistory(session.messages, userMessageId)) return
102
+
103
+ const controller = new AbortController()
104
+ this.controllers.set(sessionId, controller)
105
+ useRuntime.getState().setStreaming(sessionId, true)
106
+ const settings = useSettings.getState()
107
+ const providerName = getProviderName(settings.provider)
108
+ const mode = session.mode ?? settings.mode
109
+ const startedAt = Date.now()
110
+ let totalRounds = 0
111
+ let turnError: string | undefined
112
+
113
+ try {
114
+ rounds: for (let round = 0; round < MAX_ROUNDS; round += 1) {
115
+ totalRounds = round + 1
116
+ if (controller.signal.aborted) break
117
+ let current = this.ctx.sessions.current(sessionId)
118
+ if (!current) break
119
+
120
+ // Auto-compaction check before round 0
121
+ // A prompt queued behind this turn would be dropped by the rewrite, so wait until it is the last one.
122
+ if (round === 0 && lastUserMessageId(current.messages) === userMessageId) {
123
+ const compacted = await this.ctx.compaction.checkAutoCompact(sessionId)
124
+ if (compacted) {
125
+ // Compaction rewrites the whole transcript, so the original user message
126
+ // is gone: continue from the handoff message and the fresh session state.
127
+ const refreshed = this.ctx.sessions.current(sessionId)
128
+ if (!refreshed) break
129
+ const handoffId = lastUserMessageId(refreshed.messages)
130
+ if (!handoffId) break
131
+ userMessageId = handoffId
132
+ current = refreshed
133
+ }
134
+ }
135
+
136
+ const history = this.ctx.sessions.turnHistory(current.messages, userMessageId)
137
+ if (!history) break
138
+
139
+ const assistantId = createId('msg')
140
+ const assistant: AssistantMessage = {
141
+ id: assistantId,
142
+ role: 'assistant',
143
+ content: '',
144
+ reasoning: '',
145
+ toolCalls: [],
146
+ round,
147
+ model: activeModelId(settings),
148
+ provider: settings.provider,
149
+ createdAt: Date.now(),
150
+ }
151
+ this.ctx.sessions.addMessage(sessionId, assistant)
152
+
153
+ let streamedContent = ''
154
+ let streamedReasoning = ''
155
+ let flushTimer: number | undefined
156
+ let usage: AdapterResult['usage'] = {}
157
+
158
+ const flush = () => {
159
+ if (flushTimer !== undefined) return
160
+ flushTimer = window.setTimeout(() => {
161
+ flushTimer = undefined
162
+ this.ctx.sessions.updateMessage(sessionId, assistantId, {
163
+ content: streamedContent,
164
+ reasoning: streamedReasoning,
165
+ })
166
+ }, 40)
167
+ }
168
+
169
+ const stepContext: StepContext = {
170
+ sessionId,
171
+ round,
172
+ history,
173
+ settings,
174
+ mode,
175
+ signal: controller.signal,
176
+ assistantId,
177
+ }
178
+
179
+ // agent/pre-step waterfall
180
+ await this.ctx.events.waterfall('agent/pre-step', stepContext, () => undefined)
181
+
182
+ let result: AdapterResult
183
+ let attempt = 0
184
+ for (;;) {
185
+ try {
186
+ result = await this.ctx.llm.stream({
187
+ settings,
188
+ model: findModel(settings.provider, activeModelId(settings)),
189
+ history,
190
+ system: this.ctx.systemPrompt.assemble(settings, mode),
191
+ mode,
192
+ signal: controller.signal,
193
+ handlers: {
194
+ onText: (delta) => {
195
+ streamedContent += delta
196
+ flush()
197
+ },
198
+ onReasoning: (delta) => {
199
+ streamedReasoning += delta
200
+ flush()
201
+ },
202
+ onToolCalls: () => undefined,
203
+ onUsage: (value) => {
204
+ usage = value
205
+ },
206
+ },
207
+ })
208
+ break
209
+ } catch (error) {
210
+ const stopped = isAbortError(error) || controller.signal.aborted
211
+ // Only retry when nothing reached the screen yet, so a retry never duplicates text.
212
+ const retryable =
213
+ !stopped &&
214
+ attempt < MAX_RETRIES &&
215
+ !streamedContent &&
216
+ !streamedReasoning &&
217
+ RETRYABLE.test(error instanceof Error ? error.message : '')
218
+ if (retryable) {
219
+ attempt += 1
220
+ window.dispatchEvent(
221
+ new CustomEvent('maxcode:notice', {
222
+ detail: {
223
+ message: `${providerName} is busy. Retrying (${attempt}/${MAX_RETRIES})…`,
224
+ tone: 'info',
225
+ },
226
+ }),
227
+ )
228
+ await sleep(1200 * attempt * attempt, controller.signal)
229
+ if (controller.signal.aborted) continue
230
+ continue
231
+ }
232
+ if (flushTimer !== undefined) window.clearTimeout(flushTimer)
233
+ turnError = stopped ? undefined : describeRequestError(error, providerName)
234
+ this.ctx.sessions.updateMessage(sessionId, assistantId, {
235
+ content: streamedContent,
236
+ reasoning: streamedReasoning,
237
+ stopped,
238
+ durationMs: Date.now() - startedAt,
239
+ error: turnError,
240
+ })
241
+ await this.ctx.events.waterfall('agent/request-error', error, stepContext, () => undefined)
242
+ break rounds
243
+ }
244
+ }
245
+
246
+ if (flushTimer !== undefined) window.clearTimeout(flushTimer)
247
+ this.ctx.sessions.updateMessage(sessionId, assistantId, {
248
+ content: result.content,
249
+ reasoning: result.reasoning,
250
+ thinkingBlocks: result.thinkingBlocks,
251
+ toolCalls: result.toolCalls,
252
+ usage: usage.input || usage.output ? usage : result.usage,
253
+ durationMs: Date.now() - startedAt,
254
+ stopped: controller.signal.aborted || undefined,
255
+ })
256
+ this.ctx.sessions.updateSession(sessionId, {
257
+ provider: settings.provider,
258
+ model: activeModelId(settings),
259
+ mode,
260
+ })
261
+
262
+ if (controller.signal.aborted) break
263
+ if (result.toolCalls.length === 0) {
264
+ const stepResult: StepResult = {
265
+ sessionId,
266
+ round,
267
+ assistant,
268
+ toolResults: [],
269
+ stopped: controller.signal.aborted,
270
+ }
271
+ this.ctx.events.emit('agent/step-finish', stepResult)
272
+ break
273
+ }
274
+
275
+ const toolMessages: ToolMessage[] = []
276
+
277
+ for (const call of result.toolCalls) {
278
+ if (controller.signal.aborted) break
279
+ const toolSetting = settings.tools[call.name]
280
+ if (!toolSetting?.enabled) {
281
+ const message: ToolMessage = {
282
+ id: createId('tool'),
283
+ role: 'tool',
284
+ call,
285
+ status: 'error',
286
+ result: 'This tool is disabled in Settings → Tools.',
287
+ round,
288
+ createdAt: Date.now(),
289
+ }
290
+ this.ctx.sessions.addMessage(sessionId, message)
291
+ toolMessages.push(message)
292
+ continue
293
+ }
294
+
295
+ const toolMessageId = createId('tool')
296
+ const awaiting = toolSetting.approval === 'ask'
297
+ const toolMessage: ToolMessage = {
298
+ id: toolMessageId,
299
+ role: 'tool',
300
+ call,
301
+ status: awaiting ? 'awaiting' : 'running',
302
+ round,
303
+ createdAt: Date.now(),
304
+ }
305
+ this.ctx.sessions.addMessage(sessionId, toolMessage)
306
+
307
+ const toolStarted = Date.now()
308
+ try {
309
+ const output =
310
+ call.name === 'subagent'
311
+ ? {
312
+ output: truncate(
313
+ await this.ctx.subagents.run(
314
+ typeof call.args.task === 'string' ? call.args.task : '',
315
+ typeof call.args.context === 'string' ? call.args.context : '',
316
+ controller.signal,
317
+ ),
318
+ ),
319
+ isError: false,
320
+ }
321
+ : await this.ctx.tools.execute(
322
+ call.name,
323
+ call.args,
324
+ settings,
325
+ mode,
326
+ controller.signal,
327
+ call.id,
328
+ userMessageId,
329
+ )
330
+ const finalToolMessage: ToolMessage = {
331
+ ...toolMessage,
332
+ status: output.isError ? 'error' : 'done',
333
+ result: output.output,
334
+ durationMs: Date.now() - toolStarted,
335
+ }
336
+ this.ctx.sessions.updateMessage(sessionId, toolMessageId, finalToolMessage)
337
+ toolMessages.push(finalToolMessage)
338
+ } catch (error) {
339
+ const stopped = isAbortError(error)
340
+ const finalToolMessage: ToolMessage = {
341
+ ...toolMessage,
342
+ status: 'error',
343
+ result: stopped ? 'Stopped.' : error instanceof Error ? error.message : 'The tool failed.',
344
+ durationMs: Date.now() - toolStarted,
345
+ }
346
+ this.ctx.sessions.updateMessage(sessionId, toolMessageId, finalToolMessage)
347
+ toolMessages.push(finalToolMessage)
348
+ if (stopped) break
349
+ }
350
+ }
351
+
352
+ const stepResult: StepResult = {
353
+ sessionId,
354
+ round,
355
+ assistant,
356
+ toolResults: toolMessages,
357
+ stopped: controller.signal.aborted,
358
+ }
359
+ this.ctx.events.emit('agent/step-finish', stepResult)
360
+
361
+ if (controller.signal.aborted) {
362
+ const latest = this.ctx.sessions.current(sessionId)
363
+ if (latest) {
364
+ const recorded = new Set(
365
+ latest.messages
366
+ .filter((message): message is ToolMessage => message.role === 'tool')
367
+ .map((message) => message.call.id),
368
+ )
369
+ for (const call of result.toolCalls) {
370
+ if (recorded.has(call.id)) continue
371
+ this.ctx.sessions.addMessage(sessionId, {
372
+ id: createId('tool'),
373
+ role: 'tool',
374
+ call,
375
+ status: 'error',
376
+ result: 'Stopped before the tool ran.',
377
+ round,
378
+ createdAt: Date.now(),
379
+ })
380
+ }
381
+ }
382
+ break
383
+ }
384
+ }
385
+
386
+ const finalSession = this.ctx.sessions.current(sessionId)
387
+ if (!controller.signal.aborted && finalSession) {
388
+ const last = finalSession.messages[finalSession.messages.length - 1]
389
+ if (last?.role === 'tool') {
390
+ const message: AssistantMessage = {
391
+ id: createId('msg'),
392
+ role: 'assistant',
393
+ content: '',
394
+ reasoning: '',
395
+ toolCalls: [],
396
+ round: MAX_ROUNDS,
397
+ createdAt: Date.now(),
398
+ error: `Stopped after ${MAX_ROUNDS} tool rounds. Send another message to continue.`,
399
+ }
400
+ this.ctx.sessions.addMessage(sessionId, message)
401
+ }
402
+ }
403
+ } finally {
404
+ this.controllers.delete(sessionId)
405
+ useRuntime.getState().setStreaming(sessionId, false)
406
+ const turnResult: TurnResult = {
407
+ sessionId,
408
+ userMessageId,
409
+ rounds: totalRounds,
410
+ durationMs: Date.now() - startedAt,
411
+ stopped: controller.signal.aborted,
412
+ error: turnError,
413
+ }
414
+ this.ctx.events.emit('agent/turn-finish', turnResult)
415
+ }
416
+ }
417
+ }
@@ -0,0 +1,66 @@
1
+ import { Service, type Context } from 'cordis'
2
+ import type { ToolExecutionContext, ToolPreExecuteResult } from '../types'
3
+
4
+ interface PendingApproval {
5
+ toolCallId: string
6
+ resolve: (allow: boolean) => void
7
+ cleanup: () => void
8
+ }
9
+
10
+ const pendingApprovals = new Map<string, PendingApproval>()
11
+
12
+ export class ApprovalService extends Service {
13
+ static provide = 'approval'
14
+ static inject = ['tools']
15
+
16
+ constructor(ctx: Context) {
17
+ super(ctx, 'approval')
18
+
19
+ // Attach to tools/pre-execute waterfall gate
20
+ this.ctx.on('tools/pre-execute', async (exec: ToolExecutionContext, next: () => Promise<ToolPreExecuteResult>) => {
21
+ const toolSetting = exec.settings.tools[exec.name]
22
+ if (toolSetting?.approval === 'ask' && exec.callId) {
23
+ const approved = await this.waitForApproval(exec.callId, exec.signal)
24
+ if (!approved) {
25
+ return {
26
+ decision: 'deny',
27
+ reason: exec.signal.aborted ? 'Stopped before the tool ran.' : 'You denied this tool call.',
28
+ }
29
+ }
30
+ }
31
+ return next()
32
+ })
33
+ }
34
+
35
+ waitForApproval(toolCallId: string, signal: AbortSignal): Promise<boolean> {
36
+ return waitForApproval(toolCallId, signal)
37
+ }
38
+
39
+ resolveApproval(toolCallId: string, allow: boolean): void {
40
+ resolveApproval(toolCallId, allow)
41
+ }
42
+ }
43
+
44
+ export function waitForApproval(toolCallId: string, signal: AbortSignal): Promise<boolean> {
45
+ if (signal.aborted) return Promise.resolve(false)
46
+ return new Promise<boolean>((resolve) => {
47
+ const finish = (allow: boolean) => {
48
+ const entry = pendingApprovals.get(toolCallId)
49
+ if (!entry) return
50
+ entry.cleanup()
51
+ pendingApprovals.delete(toolCallId)
52
+ resolve(allow)
53
+ }
54
+ const onAbort = () => finish(false)
55
+ signal.addEventListener('abort', onAbort, { once: true })
56
+ pendingApprovals.set(toolCallId, {
57
+ toolCallId,
58
+ resolve: finish,
59
+ cleanup: () => signal.removeEventListener('abort', onAbort),
60
+ })
61
+ })
62
+ }
63
+
64
+ export function resolveApproval(toolCallId: string, allow: boolean): void {
65
+ pendingApprovals.get(toolCallId)?.resolve(allow)
66
+ }
@@ -0,0 +1,45 @@
1
+ import { Service, type Context } from 'cordis'
2
+ import { compactSession, isCompacting } from '../../agent/compact'
3
+ import { contextUsageRatio, AUTO_COMPACT_THRESHOLD } from '../../lib/context'
4
+ import { findModel } from '../../store/models'
5
+ import { activeModelId, useSettings } from '../../store/settings'
6
+ import { useChats } from '../../store/chats'
7
+
8
+ export class CompactionService extends Service {
9
+ static provide = 'compaction'
10
+ static inject = ['sessions', 'llm']
11
+
12
+ constructor(ctx: Context) {
13
+ super(ctx, 'compaction')
14
+ }
15
+
16
+ isCompacting(sessionId: string): boolean {
17
+ return isCompacting(sessionId)
18
+ }
19
+
20
+ async compact(sessionId: string): Promise<boolean> {
21
+ return await compactSession(sessionId)
22
+ }
23
+
24
+ async checkAutoCompact(sessionId: string): Promise<boolean> {
25
+ const session = useChats.getState().sessions.find((item) => item.id === sessionId)
26
+ if (!session || session.messages.length < 4 || this.isCompacting(sessionId)) return false
27
+
28
+ const settings = useSettings.getState()
29
+ const modelInfo = findModel(settings.provider, activeModelId(settings))
30
+ const ratio = contextUsageRatio(session.messages, modelInfo)
31
+
32
+ if (ratio >= AUTO_COMPACT_THRESHOLD) {
33
+ window.dispatchEvent(
34
+ new CustomEvent('maxcode:notice', {
35
+ detail: {
36
+ message: `Context at ${Math.round(ratio * 100)}% — compacting session before next request…`,
37
+ tone: 'info',
38
+ },
39
+ }),
40
+ )
41
+ return await this.compact(sessionId)
42
+ }
43
+ return false
44
+ }
45
+ }
@@ -0,0 +1,32 @@
1
+ import { Service, type Context } from 'cordis'
2
+ import type { AdapterResult } from '../../agent/adapters/shared'
3
+ import { streamAnthropic } from '../../agent/adapters/anthropic'
4
+ import { streamGemini } from '../../agent/adapters/gemini'
5
+ import { streamOpenAI } from '../../agent/adapters/openai'
6
+ import { streamOpenAIResponses } from '../../agent/adapters/openai-responses'
7
+ import { apiKindFor } from '../../store/models'
8
+ import type { ModelRequest } from '../types'
9
+
10
+ export class LlmService extends Service {
11
+ static provide = 'llm'
12
+
13
+ constructor(ctx: Context) {
14
+ super(ctx, 'llm')
15
+ }
16
+
17
+ async stream(request: ModelRequest): Promise<AdapterResult> {
18
+ return await this.ctx.events.waterfall('agent/request', request, async () => {
19
+ const kind = apiKindFor(request.settings.provider, request.settings.model, request.model)
20
+ switch (kind) {
21
+ case 'anthropic':
22
+ return await streamAnthropic(request)
23
+ case 'gemini':
24
+ return await streamGemini(request)
25
+ case 'openai-responses':
26
+ return await streamOpenAIResponses(request)
27
+ default:
28
+ return await streamOpenAI(request)
29
+ }
30
+ })
31
+ }
32
+ }
@@ -0,0 +1,47 @@
1
+ import { Service, type Context } from 'cordis'
2
+ import type { ChatSession, Message } from '../../types'
3
+ import { useChats } from '../../store/chats'
4
+ import { turnHistory as extractTurnHistory } from '../../agent/queue'
5
+
6
+ export class SessionService extends Service {
7
+ static provide = 'sessions'
8
+
9
+ constructor(ctx: Context) {
10
+ super(ctx, 'sessions')
11
+ }
12
+
13
+ current(sessionId: string): ChatSession | undefined {
14
+ return useChats.getState().sessions.find((s) => s.id === sessionId)
15
+ }
16
+
17
+ all(): ChatSession[] {
18
+ return useChats.getState().sessions
19
+ }
20
+
21
+ getMessages(sessionId: string): Message[] {
22
+ return this.current(sessionId)?.messages ?? []
23
+ }
24
+
25
+ addMessage(sessionId: string, message: Message): void {
26
+ useChats.getState().addMessage(sessionId, message)
27
+ void this.flush(sessionId)
28
+ }
29
+
30
+ updateMessage(sessionId: string, messageId: string, patch: Partial<Message>): void {
31
+ useChats.getState().updateMessage(sessionId, messageId, patch)
32
+ void this.flush(sessionId)
33
+ }
34
+
35
+ updateSession(sessionId: string, patch: Partial<ChatSession>): void {
36
+ useChats.getState().updateSession(sessionId, patch)
37
+ void this.flush(sessionId)
38
+ }
39
+
40
+ turnHistory(messages: Message[], userMessageId: string): Message[] | null {
41
+ return extractTurnHistory(messages, userMessageId)
42
+ }
43
+
44
+ async flush(sessionId: string): Promise<void> {
45
+ await this.ctx.events.parallel('session/flush', sessionId).catch(() => undefined)
46
+ }
47
+ }
@@ -0,0 +1,15 @@
1
+ import { Service, type Context } from 'cordis'
2
+ import { runSubagent } from '../../agent/subagent'
3
+
4
+ export class SubagentService extends Service {
5
+ static provide = 'subagents'
6
+ static inject = ['tools', 'llm', 'systemPrompt']
7
+
8
+ constructor(ctx: Context) {
9
+ super(ctx, 'subagents')
10
+ }
11
+
12
+ async run(task: string, context: string, signal: AbortSignal): Promise<string> {
13
+ return await runSubagent(task, context, signal)
14
+ }
15
+ }