@kafca/agentdock 0.1.44 → 0.1.46

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 (126) hide show
  1. package/README.md +76 -11
  2. package/bin/agentdock.mjs +5 -5
  3. package/dist/renderer/assets/{Badge-Qk9qHovH.js → Badge-BIesaDYG.js} +1 -1
  4. package/dist/renderer/assets/Chat-CpataoDh.js +2 -0
  5. package/dist/renderer/assets/{ChatMarkdown-BKdW8OON.js → ChatMarkdown-_Mdmltgv.js} +1 -1
  6. package/dist/renderer/assets/Config-Pe5-zKZk.js +21 -0
  7. package/dist/renderer/assets/CronList-BnaOSPT8.js +1 -0
  8. package/dist/renderer/assets/Dashboard-D05l8GKE.js +26 -0
  9. package/dist/renderer/assets/{EmptyState-kD9yAwtw.js → EmptyState-BDBavDll.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeDetail-BAMCLrjz.js → KnowledgeDetail-DvR798l_.js} +2 -2
  11. package/dist/renderer/assets/KnowledgeHome-r2ZuvU8-.js +11 -0
  12. package/dist/renderer/assets/Logs-BaljtmAV.js +1 -0
  13. package/dist/renderer/assets/{Modal-DwY4lecS.js → Modal-CXhD_OWR.js} +1 -1
  14. package/dist/renderer/assets/MonitorList-D7tyipbx.js +1 -0
  15. package/dist/renderer/assets/{Page-CnB9ePsb.js → Page-KUuFYgjg.js} +1 -1
  16. package/dist/renderer/assets/ProjectDetail-Ddq94-yu.js +11 -0
  17. package/dist/renderer/assets/ProjectList-CbdHd5Ev.js +1 -0
  18. package/dist/renderer/assets/{Select-zfoksLGB.js → Select-Djc8BXMC.js} +1 -1
  19. package/dist/renderer/assets/SessionChat-DOy0iKQR.js +1 -0
  20. package/dist/renderer/assets/SessionList-DHgl1s1D.js +1 -0
  21. package/dist/renderer/assets/ThreadChat-I7dYibDc.js +23 -0
  22. package/dist/renderer/assets/Workspace-DTocqY-F.js +19 -0
  23. package/dist/renderer/assets/{arrow-left-CNEAuElg.js → arrow-left-CXyya3hP.js} +1 -1
  24. package/dist/renderer/assets/{book-open-BNJMhNI-.js → book-open-9n4Kxc7E.js} +1 -1
  25. package/dist/renderer/assets/bot-C9HBk5XH.js +6 -0
  26. package/dist/renderer/assets/{check-BnclClRU.js → check-CutSg9a5.js} +1 -1
  27. package/dist/renderer/assets/{chevron-down-Bh53e0O-.js → chevron-down-DrydR4i3.js} +1 -1
  28. package/dist/renderer/assets/{circle-ClZTWCWO.js → circle-BEjtahw6.js} +1 -1
  29. package/dist/renderer/assets/desktop-DATgk_ka.js +1 -0
  30. package/dist/renderer/assets/{desktop-C6q6wBae.js → desktop-JQkhNOF4.js} +5 -5
  31. package/dist/renderer/assets/en-CmAr_0e6.js +1 -0
  32. package/dist/renderer/assets/es-Cs9x52ca.js +1 -0
  33. package/dist/renderer/assets/{funnel-_5hsz0xI.js → funnel-BhqZpS4f.js} +1 -1
  34. package/dist/renderer/assets/{heart-u4Q1aOqj.js → heart-Dp8Vs-7_.js} +1 -1
  35. package/dist/renderer/assets/{index-BNFx4uYv.js → index-BKpv0P0s.js} +2 -2
  36. package/dist/renderer/assets/index-CO4fGqmv.css +11 -0
  37. package/dist/renderer/assets/{index-B36eowj-.js → index-cI3pWBnN.js} +36 -36
  38. package/dist/renderer/assets/ja-DtvtBRTW.js +1 -0
  39. package/dist/renderer/assets/{pencil-DLh9QNmT.js → pencil-ywcJzeOo.js} +1 -1
  40. package/dist/renderer/assets/{play-BDTk9Scm.js → play-AtN492Cx.js} +1 -1
  41. package/dist/renderer/assets/{plus-BSc8POsz.js → plus-BEj14BBV.js} +1 -1
  42. package/dist/renderer/assets/projects-Clar8lgb.js +1 -0
  43. package/dist/renderer/assets/save-D6ov4CNv.js +11 -0
  44. package/dist/renderer/assets/{search-CniPk7Ey.js → search-C3bvw7AR.js} +1 -1
  45. package/dist/renderer/assets/{server-GsXyQAZm.js → server-C1Uyedyx.js} +1 -1
  46. package/dist/renderer/assets/{session-utils-BDxzgTBC.js → session-utils-DpRwJ6Nh.js} +1 -1
  47. package/dist/renderer/assets/{sessions-CLcKA1Tt.js → sessions-BWmAppwK.js} +1 -1
  48. package/dist/renderer/assets/{shield-check-4i0E3hmf.js → shield-check-CVx1mEs2.js} +1 -1
  49. package/dist/renderer/assets/{trash-2-DhtOCsO5.js → trash-2-o-heXOgs.js} +1 -1
  50. package/dist/renderer/assets/user-dPOzUqIc.js +6 -0
  51. package/dist/renderer/assets/{wifi-off-BHPzPY_B.js → wifi-off-Fz5c3_x_.js} +1 -1
  52. package/dist/renderer/assets/zh-CcJ9vBZj.js +1 -0
  53. package/dist/renderer/assets/zh-TW-elI72AC1.js +1 -0
  54. package/dist/renderer/index.html +2 -2
  55. package/dist-electron/electron/main.js +26 -1
  56. package/dist-electron/packages/core-sdk/src/index.js +28 -0
  57. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +111 -62
  58. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +80 -7
  59. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-transport.js +78 -5
  60. package/dist-electron/services/local-ai-core/src/acp/store/external-store.js +98 -0
  61. package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +33 -0
  62. package/dist-electron/services/local-ai-core/src/acp/store/model-provider-store.js +88 -0
  63. package/dist-electron/services/local-ai-core/src/acp/store/schema.js +43 -0
  64. package/dist-electron/services/local-ai-core/src/agents/pi/launch.js +2 -119
  65. package/dist-electron/services/local-ai-core/src/agents/pi/provider-materializer.js +148 -0
  66. package/dist-electron/services/local-ai-core/src/channel/lark/inbound.js +55 -1
  67. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +65 -10
  68. package/dist-electron/services/local-ai-core/src/channel/shared/session-command-runtime.js +4 -2
  69. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +139 -29
  70. package/dist-electron/services/local-ai-core/src/execution/agent-execution-backend.js +56 -0
  71. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +18 -57
  72. package/dist-electron/services/local-ai-core/src/kernel/local-core-errors.js +255 -0
  73. package/dist-electron/services/local-ai-core/src/plugins/builtin/catalog.js +78 -0
  74. package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +3 -1
  75. package/dist-electron/services/local-ai-core/src/router/workspace-router.js +64 -9
  76. package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +5 -2
  77. package/dist-electron/services/local-ai-core/src/runtime/config-migration.js +152 -0
  78. package/dist-electron/services/local-ai-core/src/runtime/deployment-diagnostics.js +175 -0
  79. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +330 -3
  80. package/dist-electron/services/local-ai-core/src/runtime/local-core-runtime-state.js +11 -2
  81. package/dist-electron/services/local-ai-core/src/runtime/provider-config-migration.js +32 -0
  82. package/dist-electron/services/local-ai-core/src/runtime/runtime-detection-service.js +76 -8
  83. package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +62 -0
  84. package/dist-electron/services/local-ai-core/src/runtime/server.js +708 -4
  85. package/dist-electron/services/local-ai-core/src/runtime/standalone.js +13 -3
  86. package/dist-electron/services/local-ai-core/src/sandbox/opensandbox-client.js +113 -0
  87. package/dist-electron/services/local-ai-core/src/sandbox/sandbox-config.js +273 -0
  88. package/dist-electron/services/local-ai-core/src/sandbox/sandbox-manager.js +215 -0
  89. package/dist-electron/services/local-ai-core/src/sandbox/sandbox-stdio-proxy.js +188 -0
  90. package/dist-electron/services/local-ai-core/src/thread/slash-command-registry.js +3 -0
  91. package/dist-electron/services/local-ai-core/src/thread/thread-command-service.js +32 -0
  92. package/dist-electron/services/local-ai-core/src/thread/thread-slash-command-dispatcher.js +71 -0
  93. package/dist-electron/shared/desktop.js +85 -1
  94. package/dist-electron/tests/contracts/architecture-docs.test.js +83 -0
  95. package/dist-electron/tests/electron/plugin-kernel.test.js +225 -18
  96. package/dist-electron/tests/electron/runtime-detection-service.test.js +57 -5
  97. package/dist-electron/tests/electron/sandbox-config.test.js +541 -0
  98. package/dist-electron/tests/electron/workspace-task-store.test.js +257 -0
  99. package/dist-electron/tests/integration/lark-gateway.test.js +139 -0
  100. package/dist-electron/tests/integration/local-core-refactor.test.js +183 -20
  101. package/dist-electron/tests/integration/local-core-routes.test.js +34 -0
  102. package/package.json +2 -3
  103. package/dist/renderer/assets/Chat-BK-fm0Xr.js +0 -2
  104. package/dist/renderer/assets/Config-B93CQGvY.js +0 -16
  105. package/dist/renderer/assets/CronList-Z3TvSRKP.js +0 -1
  106. package/dist/renderer/assets/Dashboard-Ch3QkWwf.js +0 -26
  107. package/dist/renderer/assets/KnowledgeHome-ChvjOfrc.js +0 -11
  108. package/dist/renderer/assets/Logs-Cv0nth3j.js +0 -1
  109. package/dist/renderer/assets/MonitorList-JEvMoWUm.js +0 -1
  110. package/dist/renderer/assets/ProjectDetail-DJa3sMmU.js +0 -11
  111. package/dist/renderer/assets/ProjectList-D6CXumR_.js +0 -1
  112. package/dist/renderer/assets/SessionChat-DUTh4WoM.js +0 -1
  113. package/dist/renderer/assets/SessionList-D9o-lUst.js +0 -1
  114. package/dist/renderer/assets/ThreadChat-BYXlT-Kc.js +0 -23
  115. package/dist/renderer/assets/Workspace-BrhSwRQ-.js +0 -14
  116. package/dist/renderer/assets/desktop-C5vBT0yM.js +0 -1
  117. package/dist/renderer/assets/en-D2SkCrhG.js +0 -1
  118. package/dist/renderer/assets/es-C8UBPhxM.js +0 -1
  119. package/dist/renderer/assets/index-CaQZSRbg.css +0 -11
  120. package/dist/renderer/assets/ja-ChF4vbBL.js +0 -1
  121. package/dist/renderer/assets/plug-BG0JHW6B.js +0 -6
  122. package/dist/renderer/assets/projects-DwfZwxDW.js +0 -1
  123. package/dist/renderer/assets/save-D5AO_G6a.js +0 -6
  124. package/dist/renderer/assets/user-BtC_pVXV.js +0 -11
  125. package/dist/renderer/assets/zh-DIcdHycM.js +0 -1
  126. package/dist/renderer/assets/zh-TW-BtRwreY_.js +0 -1
@@ -3,11 +3,288 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalAiCoreServer = void 0;
4
4
  const node_http_1 = require("node:http");
5
5
  const server_routes_js_1 = require("./server-routes.js");
6
+ const local_core_errors_js_1 = require("../kernel/local-core-errors.js");
7
+ class OpenAiChatCompletionStreamAdapter {
8
+ options;
9
+ created = Math.floor(Date.now() / 1000);
10
+ completionId;
11
+ assistantPreviewContentByHandle = new Map();
12
+ thoughtContentByHandle = new Map();
13
+ emittedMessageIds = new Set();
14
+ closed = false;
15
+ roleSent = false;
16
+ constructor(options) {
17
+ this.options = options;
18
+ this.completionId = `chatcmpl_${sanitizeOpenAiId(options.runId)}`;
19
+ }
20
+ start() {
21
+ this.options.response.writeHead(200, {
22
+ 'Content-Type': 'text/event-stream',
23
+ 'Cache-Control': 'no-cache, no-transform',
24
+ Connection: 'keep-alive',
25
+ });
26
+ this.sendChunk({ role: 'assistant' }, {
27
+ event: 'run_started',
28
+ kind: 'assistant',
29
+ run_id: this.options.runId,
30
+ });
31
+ this.roleSent = true;
32
+ }
33
+ handleBridgeEvent(event) {
34
+ if (this.closed || String(event.replyCtx || '') !== this.options.runId) {
35
+ return;
36
+ }
37
+ if (!this.roleSent) {
38
+ this.start();
39
+ }
40
+ if (event.type === 'typing_stop') {
41
+ this.finish('stop', { event: 'run_finished', kind: 'status' });
42
+ return;
43
+ }
44
+ if ((event.type === 'status' || event.type === 'card') && event.error) {
45
+ this.error(event.error, 'runtime_error');
46
+ return;
47
+ }
48
+ if (event.type === 'buttons' && event.bridgeKind === 'permission') {
49
+ this.error('Unexpected permission request for yolo OpenAI chat run.', 'unexpected_permission_request');
50
+ return;
51
+ }
52
+ if (event.type === 'preview_start' || event.type === 'update_message') {
53
+ this.handlePreviewEvent(event);
54
+ return;
55
+ }
56
+ if (event.type === 'reply') {
57
+ this.handleReplyEvent(event);
58
+ return;
59
+ }
60
+ if (event.type === 'status' || event.type === 'card') {
61
+ this.sendChunk({}, {
62
+ event: event.type,
63
+ kind: event.bridgeKind || 'status',
64
+ run_id: this.options.runId,
65
+ content: event.content,
66
+ ok: event.ok,
67
+ card: event.card,
68
+ });
69
+ }
70
+ }
71
+ replayMessage(input) {
72
+ if (this.closed || this.emittedMessageIds.has(input.id)) {
73
+ return;
74
+ }
75
+ this.emittedMessageIds.add(input.id);
76
+ const event = {
77
+ type: 'reply',
78
+ replyCtx: this.options.runId,
79
+ messageId: input.id,
80
+ content: input.content,
81
+ bridgeKind: input.bridgeKind,
82
+ bridgeStatus: input.bridgeStatus,
83
+ toolCall: input.toolCall,
84
+ };
85
+ this.handleReplyEvent(event);
86
+ }
87
+ finish(finishReason = 'stop', agentdock = {}) {
88
+ if (this.closed) {
89
+ return;
90
+ }
91
+ const chunk = this.baseChunk({}, {
92
+ run_id: this.options.runId,
93
+ ...agentdock,
94
+ }, finishReason);
95
+ this.options.response.write(createOpenAiSseData(chunk));
96
+ this.options.response.write(createOpenAiDone());
97
+ this.closed = true;
98
+ this.options.onClose();
99
+ this.options.response.end();
100
+ }
101
+ error(message, code = 'runtime_error') {
102
+ if (this.closed) {
103
+ return;
104
+ }
105
+ const chunk = this.baseChunk({}, {
106
+ run_id: this.options.runId,
107
+ event: 'error',
108
+ kind: 'status',
109
+ });
110
+ chunk.error = {
111
+ message,
112
+ type: code,
113
+ code,
114
+ };
115
+ this.options.response.write(createOpenAiSseData(chunk));
116
+ this.options.response.write(createOpenAiDone());
117
+ this.closed = true;
118
+ this.options.onClose();
119
+ this.options.response.end();
120
+ }
121
+ handlePreviewEvent(event) {
122
+ const content = String(event.content || '');
123
+ const handle = String(event.previewHandle || event.messageId || event.replyCtx || '');
124
+ if (!content || !handle) {
125
+ return;
126
+ }
127
+ if (event.bridgeKind === 'thought') {
128
+ const prior = this.thoughtContentByHandle.get(handle) || '';
129
+ const delta = diffAccumulatedText(prior, content);
130
+ this.thoughtContentByHandle.set(handle, content);
131
+ if (!delta) {
132
+ return;
133
+ }
134
+ this.sendProgressChunk(this.progressText('thinking', delta), {
135
+ event: 'thought_delta',
136
+ kind: 'thought',
137
+ run_id: this.options.runId,
138
+ thought: {
139
+ content,
140
+ delta,
141
+ preview_handle: handle,
142
+ },
143
+ });
144
+ return;
145
+ }
146
+ if (event.bridgeKind && event.bridgeKind !== 'assistant') {
147
+ return;
148
+ }
149
+ const prior = this.assistantPreviewContentByHandle.get(handle) || '';
150
+ const delta = diffAccumulatedText(prior, content);
151
+ this.assistantPreviewContentByHandle.set(handle, content);
152
+ if (!delta) {
153
+ return;
154
+ }
155
+ this.sendChunk({ content: delta }, {
156
+ event: 'assistant_delta',
157
+ kind: 'assistant',
158
+ run_id: this.options.runId,
159
+ preview_handle: handle,
160
+ });
161
+ }
162
+ handleReplyEvent(event) {
163
+ const content = String(event.content || '');
164
+ if (event.messageId) {
165
+ if (this.emittedMessageIds.has(event.messageId)) {
166
+ return;
167
+ }
168
+ this.emittedMessageIds.add(event.messageId);
169
+ }
170
+ if (event.bridgeKind === 'tool') {
171
+ this.sendProgressChunk(this.formatToolProgressContent(event), {
172
+ event: 'tool_update',
173
+ kind: 'tool',
174
+ run_id: this.options.runId,
175
+ message_id: event.messageId,
176
+ tool: event.toolCall || {
177
+ output: content,
178
+ },
179
+ });
180
+ return;
181
+ }
182
+ if (event.bridgeKind === 'plan') {
183
+ this.sendProgressChunk(this.progressText('plan', content), {
184
+ event: 'plan_update',
185
+ kind: 'plan',
186
+ run_id: this.options.runId,
187
+ message_id: event.messageId,
188
+ plan: {
189
+ content,
190
+ },
191
+ });
192
+ return;
193
+ }
194
+ if (event.bridgeKind === 'thought') {
195
+ this.sendProgressChunk(this.progressText('thinking', content), {
196
+ event: 'thought_message',
197
+ kind: 'thought',
198
+ run_id: this.options.runId,
199
+ message_id: event.messageId,
200
+ thought: {
201
+ content,
202
+ delta: content,
203
+ },
204
+ });
205
+ return;
206
+ }
207
+ if (event.bridgeKind === 'status' || event.bridgeKind === 'permission') {
208
+ this.sendProgressChunk(this.progressText(event.bridgeKind, content), {
209
+ event: event.bridgeKind === 'permission' ? 'permission_required' : 'status',
210
+ kind: event.bridgeKind,
211
+ run_id: this.options.runId,
212
+ message_id: event.messageId,
213
+ content,
214
+ bridge_status: event.bridgeStatus,
215
+ });
216
+ return;
217
+ }
218
+ if (content) {
219
+ this.sendChunk({ content }, {
220
+ event: 'assistant_message',
221
+ kind: 'assistant',
222
+ run_id: this.options.runId,
223
+ message_id: event.messageId,
224
+ });
225
+ }
226
+ }
227
+ sendProgressChunk(content, agentdock) {
228
+ this.sendChunk(this.options.progressMode === 'content' && content ? { content } : {}, agentdock);
229
+ }
230
+ sendChunk(delta, agentdock, finishReason = null) {
231
+ if (this.closed) {
232
+ return;
233
+ }
234
+ this.options.response.write(createOpenAiSseData(this.baseChunk(delta, agentdock, finishReason)));
235
+ }
236
+ baseChunk(delta, agentdock, finishReason = null) {
237
+ return {
238
+ id: this.completionId,
239
+ object: 'chat.completion.chunk',
240
+ created: this.created,
241
+ model: this.options.model,
242
+ choices: [
243
+ {
244
+ index: 0,
245
+ delta,
246
+ finish_reason: finishReason,
247
+ },
248
+ ],
249
+ agentdock,
250
+ };
251
+ }
252
+ progressText(kind, content) {
253
+ return content ? `\n\n[${kind}]\n${content}` : '';
254
+ }
255
+ formatToolProgressContent(event) {
256
+ const tool = event.toolCall;
257
+ const name = tool?.name || tool?.label || 'tool';
258
+ const status = tool?.status || '';
259
+ const output = tool?.output || tool?.detail || event.content || '';
260
+ return output ? `\n\n[tool:${name}${status ? ` ${status}` : ''}]\n${output}` : '';
261
+ }
262
+ }
6
263
  function json(res, statusCode, data, ok = true, error) {
7
264
  res.statusCode = statusCode;
8
265
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
9
266
  res.end(JSON.stringify(ok ? { ok: true, data } : { ok: false, error }));
10
267
  }
268
+ function rawJson(res, statusCode, data) {
269
+ res.statusCode = statusCode;
270
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
271
+ res.end(JSON.stringify(data));
272
+ }
273
+ function openAiJsonError(res, statusCode, message, code = 'invalid_request_error') {
274
+ rawJson(res, statusCode, {
275
+ error: {
276
+ message,
277
+ type: code,
278
+ code,
279
+ },
280
+ });
281
+ }
282
+ function jsonError(res, statusCode, error) {
283
+ const info = (0, local_core_errors_js_1.toLocalCoreErrorInfo)(error);
284
+ res.statusCode = statusCode;
285
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
286
+ res.end(JSON.stringify((0, local_core_errors_js_1.errorInfoToHttpBody)(info)));
287
+ }
11
288
  async function readJsonBody(req) {
12
289
  const body = await readRawBody(req);
13
290
  if (!body.length) {
@@ -34,24 +311,174 @@ function setCorsHeaders(req, res) {
34
311
  function createSseEvent(name, payload) {
35
312
  return `event: ${name}\ndata: ${JSON.stringify(payload)}\n\n`;
36
313
  }
314
+ function createOpenAiSseData(payload) {
315
+ return `data: ${JSON.stringify(payload)}\n\n`;
316
+ }
317
+ function createOpenAiDone() {
318
+ return 'data: [DONE]\n\n';
319
+ }
320
+ function sanitizeOpenAiId(value) {
321
+ return String(value || '')
322
+ .replace(/[^A-Za-z0-9_-]/g, '_')
323
+ .slice(0, 120) || 'run';
324
+ }
325
+ function diffAccumulatedText(previous, next) {
326
+ if (!next) {
327
+ return '';
328
+ }
329
+ if (!previous) {
330
+ return next;
331
+ }
332
+ if (next.startsWith(previous)) {
333
+ return next.slice(previous.length);
334
+ }
335
+ if (previous === next) {
336
+ return '';
337
+ }
338
+ return next;
339
+ }
340
+ function isTerminalAgentTaskStatus(status) {
341
+ return status === 'completed' || status === 'failed' || status === 'cancelled';
342
+ }
343
+ function extractMetadataValue(metadata, key) {
344
+ const value = metadata[key];
345
+ return String(value === undefined || value === null ? '' : value).trim();
346
+ }
347
+ function parseOpenAiChatCompletionRequest(input) {
348
+ const metadata = input.metadata && typeof input.metadata === 'object' && !Array.isArray(input.metadata)
349
+ ? input.metadata
350
+ : {};
351
+ const userId = extractMetadataValue(metadata, 'user_id') || String(input.user || '').trim();
352
+ const projectId = extractMetadataValue(metadata, 'project_id');
353
+ if (!userId) {
354
+ throw new Error('metadata.user_id is required.');
355
+ }
356
+ if (!projectId) {
357
+ throw new Error('metadata.project_id is required.');
358
+ }
359
+ const unsupported = [
360
+ input.tools !== undefined ? 'tools' : '',
361
+ input.tool_choice !== undefined ? 'tool_choice' : '',
362
+ input.response_format !== undefined ? 'response_format' : '',
363
+ input.audio !== undefined ? 'audio' : '',
364
+ input.logprobs !== undefined ? 'logprobs' : '',
365
+ ].filter(Boolean);
366
+ if (unsupported.length > 0) {
367
+ throw new Error(`Unsupported OpenAI chat field(s): ${unsupported.join(', ')}.`);
368
+ }
369
+ if (input.n !== undefined && Number(input.n) !== 1) {
370
+ throw new Error('Only n=1 is supported.');
371
+ }
372
+ const messages = Array.isArray(input.messages) ? input.messages : [];
373
+ if (messages.length === 0) {
374
+ throw new Error('messages must contain at least one text message.');
375
+ }
376
+ const prompt = messages
377
+ .map((message) => formatOpenAiMessage(message))
378
+ .filter(Boolean)
379
+ .join('\n\n');
380
+ if (!prompt.trim()) {
381
+ throw new Error('messages must contain text content.');
382
+ }
383
+ const progressMode = extractMetadataValue(metadata, 'agentdock_progress_mode') === 'content'
384
+ ? 'content'
385
+ : 'extension';
386
+ const model = String(input.model || extractMetadataValue(metadata, 'model') || '').trim();
387
+ const externalRun = {
388
+ user_id: userId,
389
+ external_project_id: projectId,
390
+ external_thread_id: extractMetadataValue(metadata, 'thread_id') || undefined,
391
+ display_name: extractMetadataValue(metadata, 'display_name') || undefined,
392
+ agent_type: extractMetadataValue(metadata, 'agent_type') || 'pi',
393
+ provider_id: extractMetadataValue(metadata, 'provider_id') || undefined,
394
+ model: model || undefined,
395
+ title: extractMetadataValue(metadata, 'title') || undefined,
396
+ metadata: {
397
+ ...metadata,
398
+ openai_compatible: true,
399
+ },
400
+ prompt,
401
+ permission_mode: 'bypassPermissions',
402
+ runtime_env: {
403
+ AGENTDOCK_OPENAI_COMPAT: '1',
404
+ },
405
+ };
406
+ return {
407
+ externalRun,
408
+ model: model || externalRun.agent_type || 'agentdock',
409
+ stream: Boolean(input.stream),
410
+ progressMode,
411
+ };
412
+ }
413
+ function formatOpenAiMessage(message) {
414
+ const role = String(message?.role || 'user').trim() || 'user';
415
+ const content = extractOpenAiMessageText(message?.content);
416
+ if (!content.trim()) {
417
+ return '';
418
+ }
419
+ if (role === 'user') {
420
+ return content;
421
+ }
422
+ return `[${role}]\n${content}`;
423
+ }
424
+ function extractOpenAiMessageText(content) {
425
+ if (typeof content === 'string') {
426
+ return content;
427
+ }
428
+ if (content === null || content === undefined) {
429
+ return '';
430
+ }
431
+ if (!Array.isArray(content)) {
432
+ throw new Error('Only text message content is supported.');
433
+ }
434
+ return content.map((part) => {
435
+ const type = String(part?.type || 'text');
436
+ if (type !== 'text') {
437
+ throw new Error(`Only text message parts are supported; received ${type}.`);
438
+ }
439
+ return String(part?.text || '');
440
+ }).join('');
441
+ }
442
+ function collectAssistantMessagesForRun(snapshot) {
443
+ const thread = snapshot.thread;
444
+ const startedAt = Date.parse(snapshot.task?.startedAt || snapshot.task?.createdAt || '');
445
+ if (!thread || !Number.isFinite(startedAt)) {
446
+ return [];
447
+ }
448
+ return (thread.messages || []).filter((message) => {
449
+ if (message.role !== 'assistant') {
450
+ return false;
451
+ }
452
+ const messageAt = Date.parse(message.timestamp || '');
453
+ return Number.isFinite(messageAt) && messageAt >= startedAt;
454
+ });
455
+ }
37
456
  class LocalAiCoreServer {
38
457
  bindings;
39
458
  host;
40
459
  port;
41
460
  sseClients = new Set();
42
461
  heartbeatTimers = new Map();
462
+ externalReplayTimers = new Map();
463
+ externalRunSseClients = new Map();
464
+ openAiRunStreams = new Map();
43
465
  server = (0, node_http_1.createServer)((req, res) => {
44
466
  void this.handleRequest(req, res);
45
467
  });
46
468
  constructor(bindings, options = {}) {
47
469
  this.bindings = bindings;
48
470
  this.host = options.host || '127.0.0.1';
49
- this.port = options.port || 9831;
471
+ this.port = options.port ?? 9831;
50
472
  this.bindings.on('runtime', (runtime) => {
51
473
  this.broadcast({ type: 'runtime.updated', runtime });
52
474
  });
53
475
  this.bindings.on('bridge', (bridge) => {
54
476
  this.broadcast({ type: 'stream.updated', stream: bridge });
477
+ if (bridge.replyCtx) {
478
+ const runId = String(bridge.replyCtx);
479
+ this.broadcastExternalRunStream(runId, bridge);
480
+ this.broadcastOpenAiRunStream(runId, bridge);
481
+ }
55
482
  if (bridge.sessionKey) {
56
483
  const threadId = this.findThreadIdFromSessionKey(bridge.sessionKey);
57
484
  this.broadcast({
@@ -99,6 +526,22 @@ class LocalAiCoreServer {
99
526
  clearInterval(timer);
100
527
  }
101
528
  this.heartbeatTimers.clear();
529
+ for (const timer of this.externalReplayTimers.values()) {
530
+ clearInterval(timer);
531
+ }
532
+ this.externalReplayTimers.clear();
533
+ for (const clients of this.externalRunSseClients.values()) {
534
+ for (const client of clients) {
535
+ client.end();
536
+ }
537
+ }
538
+ this.externalRunSseClients.clear();
539
+ for (const adapters of this.openAiRunStreams.values()) {
540
+ for (const adapter of adapters) {
541
+ adapter.finish('stop', { event: 'server_stopped', kind: 'status' });
542
+ }
543
+ }
544
+ this.openAiRunStreams.clear();
102
545
  await new Promise((resolve, reject) => {
103
546
  this.server.close((error) => {
104
547
  if (error) {
@@ -124,10 +567,10 @@ class LocalAiCoreServer {
124
567
  await this.handleParsedRoute(route, req, res, url);
125
568
  return;
126
569
  }
127
- json(res, 404, null, false, `Unknown route: ${path}`);
570
+ jsonError(res, 404, new Error(`Unknown route: ${path}`));
128
571
  }
129
572
  catch (error) {
130
- json(res, 500, null, false, error instanceof Error ? error.message : String(error));
573
+ jsonError(res, 500, error);
131
574
  }
132
575
  }
133
576
  async handleParsedRoute(route, req, res, url) {
@@ -329,6 +772,38 @@ class LocalAiCoreServer {
329
772
  case 'workspace-registry.delete':
330
773
  json(res, 200, await this.bindings.deleteWorkspaceRegistryEntry(route.workspaceId));
331
774
  return;
775
+ case 'providers.list':
776
+ json(res, 200, await this.bindings.listModelProviders());
777
+ return;
778
+ case 'providers.create': {
779
+ const body = await readJsonBody(req);
780
+ json(res, 200, await this.bindings.createModelProvider(body));
781
+ return;
782
+ }
783
+ case 'provider.update': {
784
+ const body = await readJsonBody(req);
785
+ json(res, 200, await this.bindings.updateModelProvider(route.providerId, body));
786
+ return;
787
+ }
788
+ case 'provider.delete':
789
+ json(res, 200, await this.bindings.deleteModelProvider(route.providerId));
790
+ return;
791
+ case 'external.project.ensure': {
792
+ const body = await readJsonBody(req);
793
+ json(res, 200, await this.bindings.ensureExternalProject(body));
794
+ return;
795
+ }
796
+ case 'external.run.create': {
797
+ const body = await readJsonBody(req);
798
+ json(res, 200, await this.bindings.createExternalRun(body));
799
+ return;
800
+ }
801
+ case 'external.run.events':
802
+ await this.attachExternalRunSseClient(route.runId, res);
803
+ return;
804
+ case 'openai.chat.completions':
805
+ await this.handleOpenAiChatCompletions(req, res);
806
+ return;
332
807
  case 'workspace-security.get':
333
808
  json(res, 200, await this.bindings.getWorkspaceSecuritySettings(route.workspaceId));
334
809
  return;
@@ -474,6 +949,15 @@ class LocalAiCoreServer {
474
949
  case 'capabilities.snapshot':
475
950
  json(res, 200, await this.bindings.getCapabilitySnapshot());
476
951
  return;
952
+ case 'diagnostics.errors':
953
+ json(res, 200, { errors: await this.bindings.listDiagnosticErrors() });
954
+ return;
955
+ case 'diagnostics.doctor':
956
+ json(res, 200, await this.bindings.runDiagnosticsDoctor());
957
+ return;
958
+ case 'diagnostics.deployment':
959
+ json(res, 200, await this.bindings.runDeploymentDiagnostics());
960
+ return;
477
961
  case 'plugins.diagnostics':
478
962
  json(res, 200, await this.bindings.getPluginDiagnostics());
479
963
  return;
@@ -499,7 +983,7 @@ class LocalAiCoreServer {
499
983
  case 'platform.qrcode.status': {
500
984
  const ticket = String(url.searchParams.get('ticket') || '');
501
985
  if (!ticket) {
502
- json(res, 400, null, false, 'Missing ticket parameter');
986
+ jsonError(res, 400, new Error('Missing ticket parameter'));
503
987
  return;
504
988
  }
505
989
  json(res, 200, await this.bindings.checkChannelQrCodeStatus(route.platform, route.workspaceId, ticket, this.channelInstanceId(url)));
@@ -560,12 +1044,232 @@ class LocalAiCoreServer {
560
1044
  this.sseClients.delete(res);
561
1045
  });
562
1046
  }
1047
+ async attachExternalRunSseClient(runId, res) {
1048
+ res.writeHead(200, {
1049
+ 'Content-Type': 'text/event-stream',
1050
+ 'Cache-Control': 'no-cache, no-transform',
1051
+ Connection: 'keep-alive',
1052
+ });
1053
+ res.write(': connected\n\n');
1054
+ const clients = this.externalRunSseClients.get(runId) || new Set();
1055
+ clients.add(res);
1056
+ this.externalRunSseClients.set(runId, clients);
1057
+ const replayedMessageIds = new Set();
1058
+ const heartbeat = setInterval(() => {
1059
+ res.write(': heartbeat\n\n');
1060
+ }, 15000);
1061
+ this.heartbeatTimers.set(res, heartbeat);
1062
+ const replayTimer = setInterval(() => {
1063
+ void this.bindings.getExternalRunSnapshot(runId)
1064
+ .then((snapshot) => this.replayExternalRunMessages(res, runId, snapshot, replayedMessageIds))
1065
+ .catch(() => undefined);
1066
+ }, 1000);
1067
+ this.externalReplayTimers.set(res, replayTimer);
1068
+ res.on('close', () => {
1069
+ clearInterval(heartbeat);
1070
+ clearInterval(replayTimer);
1071
+ this.heartbeatTimers.delete(res);
1072
+ this.externalReplayTimers.delete(res);
1073
+ clients.delete(res);
1074
+ if (clients.size === 0) {
1075
+ this.externalRunSseClients.delete(runId);
1076
+ }
1077
+ });
1078
+ const snapshot = await this.bindings.getExternalRunSnapshot(runId);
1079
+ res.write(createSseEvent('external.run.snapshot', {
1080
+ type: 'external.run.snapshot',
1081
+ snapshot,
1082
+ }));
1083
+ this.replayExternalRunMessages(res, runId, snapshot, replayedMessageIds);
1084
+ }
1085
+ async handleOpenAiChatCompletions(req, res) {
1086
+ let parsed;
1087
+ try {
1088
+ parsed = parseOpenAiChatCompletionRequest(await readJsonBody(req));
1089
+ }
1090
+ catch (error) {
1091
+ openAiJsonError(res, 400, error instanceof Error ? error.message : String(error));
1092
+ return;
1093
+ }
1094
+ if (parsed.stream) {
1095
+ await this.handleOpenAiStreamingChatCompletion(parsed, res);
1096
+ return;
1097
+ }
1098
+ await this.handleOpenAiNonStreamingChatCompletion(parsed, res);
1099
+ }
1100
+ async handleOpenAiStreamingChatCompletion(parsed, res) {
1101
+ try {
1102
+ const created = await this.bindings.createExternalRun(parsed.externalRun);
1103
+ const adapters = this.openAiRunStreams.get(created.run_id) || new Set();
1104
+ const adapter = new OpenAiChatCompletionStreamAdapter({
1105
+ runId: created.run_id,
1106
+ model: parsed.model,
1107
+ response: res,
1108
+ progressMode: parsed.progressMode,
1109
+ onClose: () => {
1110
+ adapters.delete(adapter);
1111
+ if (adapters.size === 0) {
1112
+ this.openAiRunStreams.delete(created.run_id);
1113
+ }
1114
+ },
1115
+ });
1116
+ adapters.add(adapter);
1117
+ this.openAiRunStreams.set(created.run_id, adapters);
1118
+ adapter.start();
1119
+ res.on('close', () => {
1120
+ adapters.delete(adapter);
1121
+ if (adapters.size === 0) {
1122
+ this.openAiRunStreams.delete(created.run_id);
1123
+ }
1124
+ });
1125
+ const snapshot = await this.bindings.getExternalRunSnapshot(created.run_id);
1126
+ this.replayOpenAiRunMessages(adapter, snapshot);
1127
+ if (isTerminalAgentTaskStatus(snapshot.task?.status)) {
1128
+ adapter.finish('stop', { event: 'run_finished', kind: 'status' });
1129
+ }
1130
+ }
1131
+ catch (error) {
1132
+ openAiJsonError(res, 500, error instanceof Error ? error.message : String(error), 'agentdock_run_error');
1133
+ }
1134
+ }
1135
+ async handleOpenAiNonStreamingChatCompletion(parsed, res) {
1136
+ let created;
1137
+ try {
1138
+ created = await this.bindings.createExternalRun(parsed.externalRun);
1139
+ }
1140
+ catch (error) {
1141
+ openAiJsonError(res, 500, error instanceof Error ? error.message : String(error), 'agentdock_run_error');
1142
+ return;
1143
+ }
1144
+ const started = Date.now();
1145
+ const timeoutMs = 10 * 60 * 1000;
1146
+ let snapshot = await this.bindings.getExternalRunSnapshot(created.run_id);
1147
+ while (!isTerminalAgentTaskStatus(snapshot.task?.status) && Date.now() - started < timeoutMs) {
1148
+ await new Promise((resolve) => setTimeout(resolve, 500));
1149
+ snapshot = await this.bindings.getExternalRunSnapshot(created.run_id);
1150
+ }
1151
+ if (!isTerminalAgentTaskStatus(snapshot.task?.status)) {
1152
+ openAiJsonError(res, 504, 'OpenAI-compatible chat completion timed out waiting for the agent run.', 'run_timeout');
1153
+ return;
1154
+ }
1155
+ if (snapshot.task?.status === 'failed') {
1156
+ openAiJsonError(res, 500, snapshot.task.error || 'Agent run failed.', 'agentdock_run_failed');
1157
+ return;
1158
+ }
1159
+ if (snapshot.task?.status === 'cancelled') {
1160
+ openAiJsonError(res, 500, snapshot.task.error || 'Agent run was cancelled.', 'agentdock_run_cancelled');
1161
+ return;
1162
+ }
1163
+ const messages = collectAssistantMessagesForRun(snapshot);
1164
+ const finalContent = messages
1165
+ .filter((message) => message.kind === 'final' || !message.bridgeKind || message.bridgeKind === 'assistant')
1166
+ .map((message) => message.content)
1167
+ .filter(Boolean)
1168
+ .join('\n\n');
1169
+ const response = {
1170
+ id: `chatcmpl_${sanitizeOpenAiId(created.run_id)}`,
1171
+ object: 'chat.completion',
1172
+ created: Math.floor(Date.now() / 1000),
1173
+ model: parsed.model,
1174
+ choices: [
1175
+ {
1176
+ index: 0,
1177
+ message: {
1178
+ role: 'assistant',
1179
+ content: finalContent,
1180
+ },
1181
+ finish_reason: 'stop',
1182
+ },
1183
+ ],
1184
+ agentdock: {
1185
+ run_id: created.run_id,
1186
+ workspace_id: created.workspace_id,
1187
+ thread_id: created.thread_id,
1188
+ task_id: created.task_id,
1189
+ events: messages
1190
+ .filter((message) => message.bridgeKind && message.bridgeKind !== 'assistant')
1191
+ .map((message) => ({
1192
+ event: `${message.bridgeKind}_update`,
1193
+ kind: message.bridgeKind,
1194
+ content: message.content,
1195
+ tool: message.toolCall,
1196
+ })),
1197
+ },
1198
+ };
1199
+ rawJson(res, 200, response);
1200
+ }
563
1201
  broadcast(event) {
564
1202
  const payload = createSseEvent(event.type, event);
565
1203
  for (const client of this.sseClients) {
566
1204
  client.write(payload);
567
1205
  }
568
1206
  }
1207
+ broadcastExternalRunStream(runId, stream) {
1208
+ const clients = this.externalRunSseClients.get(runId);
1209
+ if (!clients?.size) {
1210
+ return;
1211
+ }
1212
+ const payload = createSseEvent('external.run.stream', {
1213
+ type: 'external.run.stream',
1214
+ runId,
1215
+ stream,
1216
+ });
1217
+ for (const client of clients) {
1218
+ client.write(payload);
1219
+ }
1220
+ }
1221
+ broadcastOpenAiRunStream(runId, stream) {
1222
+ const adapters = this.openAiRunStreams.get(runId);
1223
+ if (!adapters?.size) {
1224
+ return;
1225
+ }
1226
+ for (const adapter of adapters) {
1227
+ adapter.handleBridgeEvent(stream);
1228
+ }
1229
+ }
1230
+ replayOpenAiRunMessages(adapter, snapshot) {
1231
+ for (const message of collectAssistantMessagesForRun(snapshot)) {
1232
+ adapter.replayMessage({
1233
+ id: message.id,
1234
+ content: message.content,
1235
+ bridgeKind: message.bridgeKind,
1236
+ bridgeStatus: message.bridgeStatus,
1237
+ toolCall: message.toolCall,
1238
+ });
1239
+ }
1240
+ }
1241
+ replayExternalRunMessages(res, runId, snapshot, replayedMessageIds) {
1242
+ const thread = snapshot.thread;
1243
+ const startedAt = Date.parse(snapshot.task?.startedAt || snapshot.task?.createdAt || '');
1244
+ if (!thread || !Number.isFinite(startedAt)) {
1245
+ return;
1246
+ }
1247
+ for (const message of thread.messages || []) {
1248
+ if (message.role !== 'assistant') {
1249
+ continue;
1250
+ }
1251
+ const messageAt = Date.parse(message.timestamp || '');
1252
+ if (!Number.isFinite(messageAt) || messageAt < startedAt) {
1253
+ continue;
1254
+ }
1255
+ if (replayedMessageIds.has(message.id)) {
1256
+ continue;
1257
+ }
1258
+ replayedMessageIds.add(message.id);
1259
+ res.write(createSseEvent('external.run.stream', {
1260
+ type: 'external.run.stream',
1261
+ runId,
1262
+ stream: {
1263
+ type: message.kind === 'final' ? 'reply' : 'update_message',
1264
+ sessionKey: thread.bridgeSessionKey,
1265
+ replyCtx: runId,
1266
+ content: message.content,
1267
+ bridgeKind: message.bridgeKind,
1268
+ bridgeStatus: message.bridgeStatus,
1269
+ },
1270
+ }));
1271
+ }
1272
+ }
569
1273
  async runtimeDetectionResponse() {
570
1274
  return {
571
1275
  runtimes: await this.bindings.listInstalledAgentRuntimes(),