thepopebot 1.2.75 → 1.2.76-beta.10
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/api/index.js +22 -8
- package/bin/cli.js +27 -2
- package/bin/sync.js +5 -1
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/meta/_journal.json +7 -0
- package/lib/ai/index.js +138 -3
- package/lib/ai/model.js +6 -0
- package/lib/ai/sdk-adapters/CLAUDE.md +113 -0
- package/lib/ai/sdk-adapters/claude-code.js +268 -0
- package/lib/ai/sdk-adapters/index.js +13 -0
- package/lib/ai/session-manager.js +33 -0
- package/lib/ai/system-prompt.js +16 -0
- package/lib/ai/tools.js +5 -2
- package/lib/ai/workspace-setup.js +143 -0
- package/lib/channels/telegram.js +78 -7
- package/lib/chat/actions.js +223 -10
- package/lib/chat/api.js +117 -15
- package/lib/chat/components/chat-input.js +78 -33
- package/lib/chat/components/chat-input.jsx +74 -23
- package/lib/chat/components/chat.js +27 -5
- package/lib/chat/components/chat.jsx +27 -3
- package/lib/chat/components/chats-page.js +3 -12
- package/lib/chat/components/chats-page.jsx +4 -7
- package/lib/chat/components/code-mode-toggle.js +110 -14
- package/lib/chat/components/code-mode-toggle.jsx +104 -13
- package/lib/chat/components/index.js +1 -1
- package/lib/chat/components/message.js +3 -3
- package/lib/chat/components/message.jsx +3 -3
- package/lib/chat/components/settings-chat-page.js +7 -117
- package/lib/chat/components/settings-chat-page.jsx +13 -132
- package/lib/chat/components/settings-secrets-layout.js +1 -1
- package/lib/chat/components/settings-secrets-layout.jsx +1 -1
- package/lib/chat/components/settings-secrets-page.js +274 -75
- package/lib/chat/components/settings-secrets-page.jsx +327 -65
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/code/actions.js +26 -6
- package/lib/code/terminal-view.js +36 -9
- package/lib/code/terminal-view.jsx +42 -10
- package/lib/config.js +12 -1
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +6 -2
- package/lib/db/schema.js +1 -0
- package/lib/llm-providers.js +8 -0
- package/lib/maintenance.js +31 -21
- package/lib/tools/docker.js +24 -9
- package/lib/tools/github.js +16 -0
- package/lib/tools/telegram.js +115 -0
- package/lib/utils/render-md.js +1 -1
- package/package.json +2 -1
- package/setup/lib/telegram.mjs +9 -69
- package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
- package/templates/.gitignore.template +1 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.template +2 -1
- package/templates/agent-job/CLAUDE.md.template +1 -2
- package/templates/agent-job/SYSTEM.md +2 -2
- package/templates/coding-workspace/CLAUDE.md.template +7 -0
- package/templates/coding-workspace/SYSTEM.md +0 -0
- package/templates/agent-job/SOUL.md +0 -17
package/api/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createAgentJob } from '../lib/tools/create-agent-job.js';
|
|
|
3
3
|
import { setWebhook } from '../lib/tools/telegram.js';
|
|
4
4
|
import { getAgentJobStatus, fetchAgentJobLog } from '../lib/tools/github.js';
|
|
5
5
|
import { getTelegramAdapter } from '../lib/channels/index.js';
|
|
6
|
-
import { chat, summarizeAgentJob } from '../lib/ai/index.js';
|
|
6
|
+
import { chat, chatStream, summarizeAgentJob } from '../lib/ai/index.js';
|
|
7
7
|
import { createNotification } from '../lib/db/notifications.js';
|
|
8
8
|
import { loadTriggers } from '../lib/triggers.js';
|
|
9
9
|
import { verifyApiKey } from '../lib/db/api-keys.js';
|
|
@@ -211,19 +211,33 @@ async function handleTelegramWebhook(request) {
|
|
|
211
211
|
/**
|
|
212
212
|
* Process a normalized message through the AI layer with channel UX.
|
|
213
213
|
* Message persistence is handled centrally by the AI layer.
|
|
214
|
+
*
|
|
215
|
+
* Uses chatStream() for progressive tool-call rendering when the adapter
|
|
216
|
+
* supports it (Telegram: sends each tool call as a message, reacts on completion).
|
|
217
|
+
* Falls back to chat() for adapters without streamChatResponse.
|
|
214
218
|
*/
|
|
215
219
|
async function processChannelMessage(adapter, normalized) {
|
|
216
220
|
await adapter.acknowledge(normalized.metadata);
|
|
217
221
|
const stopIndicator = adapter.startProcessingIndicator(normalized.metadata);
|
|
218
222
|
|
|
219
223
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
if (adapter.streamChatResponse) {
|
|
225
|
+
const chunks = chatStream(
|
|
226
|
+
normalized.threadId,
|
|
227
|
+
normalized.text,
|
|
228
|
+
normalized.attachments,
|
|
229
|
+
{ userId: 'telegram', chatTitle: 'Telegram' }
|
|
230
|
+
);
|
|
231
|
+
await adapter.streamChatResponse(normalized.metadata.chatId, chunks);
|
|
232
|
+
} else {
|
|
233
|
+
const response = await chat(
|
|
234
|
+
normalized.threadId,
|
|
235
|
+
normalized.text,
|
|
236
|
+
normalized.attachments,
|
|
237
|
+
{ userId: 'telegram', chatTitle: 'Telegram' }
|
|
238
|
+
);
|
|
239
|
+
await adapter.sendResponse(normalized.threadId, response, normalized.metadata);
|
|
240
|
+
}
|
|
227
241
|
} catch (err) {
|
|
228
242
|
console.error('Failed to process message with AI:', err);
|
|
229
243
|
await adapter
|
package/bin/cli.js
CHANGED
|
@@ -305,6 +305,31 @@ async function init() {
|
|
|
305
305
|
console.log(' Created .claude/skills → ../skills/active');
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Create .codex/skills → ../skills/active symlink
|
|
309
|
+
const codexSkillsLink = path.join(cwd, '.codex', 'skills');
|
|
310
|
+
if (!fs.existsSync(codexSkillsLink)) {
|
|
311
|
+
fs.mkdirSync(path.dirname(codexSkillsLink), { recursive: true });
|
|
312
|
+
createDirLink('../skills/active', codexSkillsLink);
|
|
313
|
+
console.log(' Created .codex/skills → ../skills/active');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Create .gemini/skills → ../skills/active symlink
|
|
317
|
+
const geminiSkillsLink = path.join(cwd, '.gemini', 'skills');
|
|
318
|
+
if (!fs.existsSync(geminiSkillsLink)) {
|
|
319
|
+
fs.mkdirSync(path.dirname(geminiSkillsLink), { recursive: true });
|
|
320
|
+
createDirLink('../skills/active', geminiSkillsLink);
|
|
321
|
+
console.log(' Created .gemini/skills → ../skills/active');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Create .kimi/skills → ../skills/active symlink
|
|
325
|
+
const kimiSkillsLink = path.join(cwd, '.kimi', 'skills');
|
|
326
|
+
if (!fs.existsSync(kimiSkillsLink)) {
|
|
327
|
+
fs.mkdirSync(path.dirname(kimiSkillsLink), { recursive: true });
|
|
328
|
+
createDirLink('../skills/active', kimiSkillsLink);
|
|
329
|
+
console.log(' Created .kimi/skills → ../skills/active');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
308
333
|
// Report backed-up files
|
|
309
334
|
if (backedUp.length > 0) {
|
|
310
335
|
console.log(`\n Backed up ${backedUp.length} file(s) to ${path.relative(cwd, backupDir)}/`);
|
|
@@ -420,7 +445,7 @@ function reset(filePath) {
|
|
|
420
445
|
console.log(` ${destPath(file)}`);
|
|
421
446
|
}
|
|
422
447
|
console.log('\nUsage: thepopebot reset <file>');
|
|
423
|
-
console.log('Example: thepopebot reset agent-job/
|
|
448
|
+
console.log('Example: thepopebot reset agent-job/SYSTEM.md\n');
|
|
424
449
|
return;
|
|
425
450
|
}
|
|
426
451
|
|
|
@@ -502,7 +527,7 @@ function diff(filePath) {
|
|
|
502
527
|
console.log(' All files match package templates.');
|
|
503
528
|
}
|
|
504
529
|
console.log('\nUsage: thepopebot diff <file>');
|
|
505
|
-
console.log('Example: thepopebot diff agent-job/
|
|
530
|
+
console.log('Example: thepopebot diff agent-job/SYSTEM.md\n');
|
|
506
531
|
return;
|
|
507
532
|
}
|
|
508
533
|
|
package/bin/sync.js
CHANGED
|
@@ -271,10 +271,13 @@ function buildDockerImage(projectPath) {
|
|
|
271
271
|
const version = pkg.version;
|
|
272
272
|
const imageTag = `stephengpope/thepopebot:event-handler-${version}`;
|
|
273
273
|
|
|
274
|
-
// Copy web/ to project for Docker build context
|
|
274
|
+
// Copy web/ and docker/ to project for Docker build context
|
|
275
275
|
const webSrc = path.join(PACKAGE_DIR, 'web');
|
|
276
276
|
const webDest = path.join(projectPath, 'web');
|
|
277
|
+
const dockerSrc = path.join(PACKAGE_DIR, 'docker');
|
|
278
|
+
const dockerDest = path.join(projectPath, 'docker');
|
|
277
279
|
fs.cpSync(webSrc, webDest, { recursive: true });
|
|
280
|
+
fs.cpSync(dockerSrc, dockerDest, { recursive: true });
|
|
278
281
|
|
|
279
282
|
try {
|
|
280
283
|
execSync(`docker build -f - -t ${imageTag} .`, {
|
|
@@ -284,6 +287,7 @@ function buildDockerImage(projectPath) {
|
|
|
284
287
|
});
|
|
285
288
|
} finally {
|
|
286
289
|
fs.rmSync(webDest, { recursive: true, force: true });
|
|
290
|
+
fs.rmSync(dockerDest, { recursive: true, force: true });
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
// Clean up dangling images from previous builds
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `code_workspaces` ADD `coding_agent` text;
|
|
@@ -148,6 +148,13 @@
|
|
|
148
148
|
"when": 1774327178886,
|
|
149
149
|
"tag": "0020_natural_fabian_cortez",
|
|
150
150
|
"breakpoints": true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"idx": 21,
|
|
154
|
+
"version": "6",
|
|
155
|
+
"when": 1775865600000,
|
|
156
|
+
"tag": "0021_coding_agent_workspace",
|
|
157
|
+
"breakpoints": true
|
|
151
158
|
}
|
|
152
159
|
]
|
|
153
160
|
}
|
package/lib/ai/index.js
CHANGED
|
@@ -4,9 +4,16 @@ import { z } from 'zod';
|
|
|
4
4
|
import { getAgentChat, getCodeChat } from './agent.js';
|
|
5
5
|
import { createModel } from './model.js';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
7
8
|
import { PROJECT_ROOT } from '../paths.js';
|
|
8
9
|
import { render_md } from '../utils/render-md.js';
|
|
10
|
+
import { buildCodingAgentSystemPrompt } from './system-prompt.js';
|
|
9
11
|
import { getChatById, createChat, saveMessage, updateChatTitle, linkChatToWorkspace } from '../db/chats.js';
|
|
12
|
+
import { getConfig } from '../config.js';
|
|
13
|
+
import { getSdkAdapter } from './sdk-adapters/index.js';
|
|
14
|
+
import { ensureWorkspaceRepo, ensureSkills } from './workspace-setup.js';
|
|
15
|
+
import { readSessionId, writeSessionId } from './session-manager.js';
|
|
16
|
+
import { workspaceDir as getWorkspaceDir } from '../tools/docker.js';
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
19
|
* Ensure a chat exists in the DB and save a message.
|
|
@@ -39,6 +46,18 @@ function persistMessage(threadId, role, text, options = {}) {
|
|
|
39
46
|
* @returns {Promise<string>} AI response text
|
|
40
47
|
*/
|
|
41
48
|
async function chat(threadId, message, attachments = [], options = {}) {
|
|
49
|
+
// SDK path: delegate to chatStream and collect text
|
|
50
|
+
const sdkAdapter = getSdkAdapter(getConfig('CODING_AGENT'));
|
|
51
|
+
|
|
52
|
+
if (sdkAdapter) {
|
|
53
|
+
let fullText = '';
|
|
54
|
+
for await (const chunk of chatStream(threadId, message, attachments, options)) {
|
|
55
|
+
if (chunk.type === 'text') fullText += chunk.text;
|
|
56
|
+
}
|
|
57
|
+
return fullText;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Legacy LangGraph path
|
|
42
61
|
const agent = await getAgentChat();
|
|
43
62
|
|
|
44
63
|
// Save user message to DB
|
|
@@ -136,15 +155,131 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
136
155
|
workspaceId = workspaceId || existingChat.codeWorkspaceId;
|
|
137
156
|
}
|
|
138
157
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
158
|
+
// ── SDK path: direct in-process SDK call (no LangGraph, no Docker) ──
|
|
159
|
+
const sdkAdapter = getSdkAdapter(getConfig('CODING_AGENT'));
|
|
160
|
+
const useSDK = !!sdkAdapter;
|
|
142
161
|
|
|
143
162
|
// Save user message to DB (skip on regeneration — message already exists)
|
|
144
163
|
if (!options.skipUserPersist) {
|
|
145
164
|
persistMessage(threadId, 'user', message || '[attachment]', options);
|
|
146
165
|
}
|
|
147
166
|
|
|
167
|
+
if (useSDK) {
|
|
168
|
+
// 1. Workspace setup (idempotent — skips if .git exists)
|
|
169
|
+
const wsBaseDir = getWorkspaceDir(workspaceId);
|
|
170
|
+
const repoDir = path.join(wsBaseDir, 'workspace');
|
|
171
|
+
|
|
172
|
+
const { getCodeWorkspaceById } = await import('../db/code-workspaces.js');
|
|
173
|
+
const workspace = getCodeWorkspaceById(workspaceId);
|
|
174
|
+
const featureBranch = workspace?.featureBranch;
|
|
175
|
+
|
|
176
|
+
const needsSetup = !existsSync(path.join(repoDir, '.git'));
|
|
177
|
+
const setupToolCallId = `setup-${workspaceId.slice(0, 8)}`;
|
|
178
|
+
const setupArgs = { repo, branch, featureBranch };
|
|
179
|
+
|
|
180
|
+
if (needsSetup) {
|
|
181
|
+
yield { type: 'tool-call', toolCallId: setupToolCallId, toolName: 'workspace', args: setupArgs };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const setupOutput = await ensureWorkspaceRepo({ workspaceDir: repoDir, repo, branch, featureBranch });
|
|
186
|
+
ensureSkills(repoDir, isCodeMode ? 'code' : 'agent');
|
|
187
|
+
if (needsSetup) {
|
|
188
|
+
const result = setupOutput || `Workspace ready on ${featureBranch || branch}`;
|
|
189
|
+
yield { type: 'tool-result', toolCallId: setupToolCallId, result };
|
|
190
|
+
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
191
|
+
type: 'tool-invocation',
|
|
192
|
+
toolCallId: setupToolCallId,
|
|
193
|
+
toolName: 'workspace',
|
|
194
|
+
state: 'output-available',
|
|
195
|
+
input: setupArgs,
|
|
196
|
+
output: result,
|
|
197
|
+
}), options);
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (needsSetup) {
|
|
201
|
+
yield { type: 'tool-result', toolCallId: setupToolCallId, result: `Setup failed: ${err.message}` };
|
|
202
|
+
}
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 2. Session continuity
|
|
207
|
+
const sessionId = readSessionId(wsBaseDir);
|
|
208
|
+
|
|
209
|
+
// 3. System prompt
|
|
210
|
+
const systemPrompt = buildCodingAgentSystemPrompt(isCodeMode ? 'code' : 'agent');
|
|
211
|
+
|
|
212
|
+
// 4. Stream from SDK adapter
|
|
213
|
+
let pendingText = '';
|
|
214
|
+
const pendingToolCalls = new Map();
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
for await (const chunk of sdkAdapter({
|
|
218
|
+
prompt: message,
|
|
219
|
+
workspaceDir: repoDir,
|
|
220
|
+
systemPrompt,
|
|
221
|
+
sessionId,
|
|
222
|
+
permissionMode: codeModeType,
|
|
223
|
+
attachments,
|
|
224
|
+
workspaceId,
|
|
225
|
+
chatMode: isCodeMode ? 'code' : 'agent',
|
|
226
|
+
})) {
|
|
227
|
+
// Write session ID on first meta chunk
|
|
228
|
+
if (chunk.type === 'meta' && chunk.sessionId) {
|
|
229
|
+
writeSessionId(wsBaseDir, chunk.sessionId);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// DB persistence
|
|
233
|
+
if (chunk.type === 'text') {
|
|
234
|
+
pendingText += chunk.text;
|
|
235
|
+
} else if (chunk.type === 'tool-call') {
|
|
236
|
+
// Flush accumulated text before tool call
|
|
237
|
+
if (pendingText) {
|
|
238
|
+
persistMessage(threadId, 'assistant', pendingText, options);
|
|
239
|
+
pendingText = '';
|
|
240
|
+
}
|
|
241
|
+
pendingToolCalls.set(chunk.toolCallId, { toolName: chunk.toolName, args: chunk.args });
|
|
242
|
+
} else if (chunk.type === 'tool-result') {
|
|
243
|
+
const tc = pendingToolCalls.get(chunk.toolCallId);
|
|
244
|
+
if (tc) {
|
|
245
|
+
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
246
|
+
type: 'tool-invocation',
|
|
247
|
+
toolCallId: chunk.toolCallId,
|
|
248
|
+
toolName: tc.toolName,
|
|
249
|
+
state: 'output-available',
|
|
250
|
+
input: tc.args,
|
|
251
|
+
output: chunk.result,
|
|
252
|
+
}), options);
|
|
253
|
+
pendingToolCalls.delete(chunk.toolCallId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
yield chunk;
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error('[chatStream] error:', err);
|
|
261
|
+
throw err;
|
|
262
|
+
} finally {
|
|
263
|
+
// Flush remaining text
|
|
264
|
+
if (pendingText) {
|
|
265
|
+
persistMessage(threadId, 'assistant', pendingText, options);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Auto-generate title for new chats
|
|
270
|
+
if (options.userId && message) {
|
|
271
|
+
autoTitle(threadId, message).catch(() => {});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Legacy path: LangGraph + Docker (non-claude-code agents + job mode) ──
|
|
278
|
+
|
|
279
|
+
const agent = isCodeMode
|
|
280
|
+
? await getCodeChat()
|
|
281
|
+
: await getAgentChat();
|
|
282
|
+
|
|
148
283
|
// Build content blocks: text + any image/PDF attachments as vision
|
|
149
284
|
const content = [];
|
|
150
285
|
|
package/lib/ai/model.js
CHANGED
|
@@ -118,6 +118,12 @@ export async function createModel(options = {}) {
|
|
|
118
118
|
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
119
119
|
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://openrouter.ai/api/v1' } });
|
|
120
120
|
}
|
|
121
|
+
case 'nvidia': {
|
|
122
|
+
const { ChatOpenAI } = await import('@langchain/openai');
|
|
123
|
+
const apiKey = getConfig('NVIDIA_API_KEY');
|
|
124
|
+
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
125
|
+
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://integrate.api.nvidia.com/v1' } });
|
|
126
|
+
}
|
|
121
127
|
default:
|
|
122
128
|
throw new Error(`Unknown LLM provider: ${provider}`);
|
|
123
129
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# lib/ai/sdk-adapters/ — SDK Adapter System
|
|
2
|
+
|
|
3
|
+
In-process SDK adapters that replace the legacy LangGraph + Docker path for chat. Each adapter wraps a coding agent's SDK and yields a unified chunk stream consumed by `chatStream()` in `lib/ai/index.js`.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Browser → POST /stream/chat (api.js)
|
|
9
|
+
→ chatStream() (index.js)
|
|
10
|
+
→ getSdkAdapter() returns adapter function or null
|
|
11
|
+
→ if adapter: workspace setup → SDK adapter streaming → DB persistence
|
|
12
|
+
→ if null: falls back to legacy LangGraph/Docker path
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The adapter is a pure stream translator — it receives a prompt and options, calls the SDK, and yields normalized chunks. Everything else (workspace setup, DB persistence, session continuity, system prompts) is handled by `chatStream()` in `index.js`.
|
|
16
|
+
|
|
17
|
+
## Existing Adapter
|
|
18
|
+
|
|
19
|
+
| File | Agent | SDK |
|
|
20
|
+
|------|-------|-----|
|
|
21
|
+
| `claude-code.js` | `claude-code` | `@anthropic-ai/claude-agent-sdk` |
|
|
22
|
+
|
|
23
|
+
## Adding a New SDK Adapter
|
|
24
|
+
|
|
25
|
+
### 1. Create the adapter file
|
|
26
|
+
|
|
27
|
+
Create `{agent-name}.js` in this directory. Export a single async generator function:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
export async function* myAgentStream({ prompt, workspaceDir, systemPrompt, sessionId, permissionMode, attachments }) {
|
|
31
|
+
// ... call the SDK, yield chunks
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Required chunk types to yield
|
|
36
|
+
|
|
37
|
+
The adapter MUST yield these chunk types for `chatStream()` and `api.js` to work correctly:
|
|
38
|
+
|
|
39
|
+
| Chunk | Shape | When | Purpose |
|
|
40
|
+
|-------|-------|------|---------|
|
|
41
|
+
| `meta` | `{ type: 'meta', sessionId: string }` | First event | Session ID for continuity across messages. `chatStream()` writes this to disk via `writeSessionId()` so subsequent messages resume the session. |
|
|
42
|
+
| `text` | `{ type: 'text', text: string }` | Text output | Streamed to UI as deltas. Accumulated by `chatStream()` and flushed to DB as assistant messages at tool boundaries and stream end. |
|
|
43
|
+
| `tool-call` | `{ type: 'tool-call', toolCallId: string, toolName: string, args: object }` | Tool invocation starts | Triggers tool UI in the browser. May be yielded twice: once at start with `args: {}`, once at `content_block_stop` with complete args. `chatStream()` tracks these in `pendingToolCalls` for pairing with results. |
|
|
44
|
+
| `tool-result` | `{ type: 'tool-result', toolCallId: string, result: string }` | Tool completes | Paired with the matching `tool-call` by `toolCallId`. `chatStream()` persists the pair as a `tool-invocation` JSON message in the DB. |
|
|
45
|
+
| `result` | `{ type: 'result', text: string, cost?: number, duration?: number, subtype?: string }` | Stream ends | Final summary. Logged by `chatStream()`, not persisted or sent to UI. |
|
|
46
|
+
|
|
47
|
+
Optional:
|
|
48
|
+
| `unknown` | `{ type: 'unknown', raw: any }` | Unrecognized events | `api.js` renders these as collapsible boxes in the UI. Use for debugging unhandled SDK events. |
|
|
49
|
+
|
|
50
|
+
### 3. Register in index.js
|
|
51
|
+
|
|
52
|
+
Add the import and mapping in `getSdkAdapter()`:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { myAgentStream } from './my-agent.js';
|
|
56
|
+
|
|
57
|
+
export function getSdkAdapter(agentType) {
|
|
58
|
+
if (agentType === 'claude-code') return claudeCodeStream;
|
|
59
|
+
if (agentType === 'my-agent') return myAgentStream;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The `agentType` string comes from the `CODING_AGENT` config value set in the admin UI.
|
|
65
|
+
|
|
66
|
+
### 4. Auth resolution
|
|
67
|
+
|
|
68
|
+
Use `buildAgentAuthEnv(agentType)` from `lib/tools/docker.js` to get credentials from the settings DB. This returns `{ env: string[], backendApi: string }` where `env` is an array of `KEY=value` strings. Parse them into an env object:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { buildAgentAuthEnv } from '../../tools/docker.js';
|
|
72
|
+
|
|
73
|
+
const env = { ...process.env };
|
|
74
|
+
const { env: authEnvPairs } = buildAgentAuthEnv('my-agent');
|
|
75
|
+
for (const pair of authEnvPairs) {
|
|
76
|
+
const eqIdx = pair.indexOf('=');
|
|
77
|
+
if (eqIdx > 0) env[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The agent's auth config (API keys, OAuth tokens, provider selection) is managed in the admin UI at `/admin/event-handler/coding-agents` and stored in the settings DB. `buildAgentAuthEnv()` reads it — you don't need to access the settings DB directly.
|
|
82
|
+
|
|
83
|
+
### 5. Function parameters
|
|
84
|
+
|
|
85
|
+
| Param | Type | Description |
|
|
86
|
+
|-------|------|-------------|
|
|
87
|
+
| `prompt` | `string` | User message text |
|
|
88
|
+
| `workspaceDir` | `string` | Absolute path to git repo root (the SDK should execute here) |
|
|
89
|
+
| `systemPrompt` | `string\|null` | System prompt for agent mode (null in code mode) |
|
|
90
|
+
| `sessionId` | `string\|null` | Previous session ID to resume (null on first message) |
|
|
91
|
+
| `permissionMode` | `string` | `'plan'` (read-only) or `'code'` (read-write). Map to the SDK's equivalent permission concept. |
|
|
92
|
+
| `attachments` | `Array` | Image attachments: `{ category: 'image', mimeType, dataUrl }` |
|
|
93
|
+
|
|
94
|
+
### 6. Session continuity contract
|
|
95
|
+
|
|
96
|
+
Multi-turn conversation works via session IDs:
|
|
97
|
+
|
|
98
|
+
1. First message: `sessionId` param is `null`. Adapter yields `{ type: 'meta', sessionId: '<new-id>' }`.
|
|
99
|
+
2. `chatStream()` writes the session ID to `{workspaceBaseDir}/.claude-ttyd-sessions/7681`.
|
|
100
|
+
3. Next message: `sessionId` param contains the saved ID. Adapter passes it to the SDK's resume mechanism.
|
|
101
|
+
|
|
102
|
+
If the SDK doesn't support session resume, the adapter can ignore `sessionId` — but multi-turn context will be lost between messages.
|
|
103
|
+
|
|
104
|
+
## What the adapter does NOT handle
|
|
105
|
+
|
|
106
|
+
These are managed by `chatStream()` in `index.js` — adapters should not duplicate them:
|
|
107
|
+
|
|
108
|
+
- **Workspace git setup** — `ensureWorkspaceRepo()` clones/checkouts before the adapter is called
|
|
109
|
+
- **DB persistence** — `chatStream()` saves user messages, assistant text, and tool invocations
|
|
110
|
+
- **Chat creation** — `chatStream()` creates the chat and workspace DB records
|
|
111
|
+
- **Auto-titling** — `chatStream()` generates a title after the first message
|
|
112
|
+
- **System prompt loading** — `chatStream()` calls `buildCodingAgentSystemPrompt()` and passes the result as `systemPrompt`
|
|
113
|
+
- **Skill activation** — `ensureSkills()` runs before the adapter is called
|