thepopebot 1.2.76-beta.2 → 1.2.76-beta.21
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/README.md +3 -3
- package/api/CLAUDE.md +11 -4
- package/api/index.js +56 -18
- package/bin/CLAUDE.md +7 -4
- package/bin/cli.js +25 -45
- package/config/CLAUDE.md +23 -4
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/0022_organic_apocalypse.sql +16 -0
- package/drizzle/0023_needy_ender_wiggin.sql +1 -0
- package/drizzle/meta/0021_snapshot.json +639 -0
- package/drizzle/meta/0022_snapshot.json +743 -0
- package/drizzle/meta/0023_snapshot.json +750 -0
- package/drizzle/meta/_journal.json +21 -0
- package/lib/CLAUDE.md +2 -2
- package/lib/actions.js +9 -1
- package/lib/ai/CLAUDE.md +72 -57
- package/lib/ai/helper-llm.js +108 -0
- package/lib/ai/index.js +308 -438
- package/lib/ai/line-mappers.js +42 -24
- package/lib/ai/scope.js +26 -0
- package/lib/ai/sdk-adapters/CLAUDE.md +114 -0
- package/lib/ai/sdk-adapters/claude-code.js +120 -8
- package/lib/ai/system-prompt.js +34 -0
- package/lib/ai/workspace-setup.js +19 -35
- package/lib/channels/CLAUDE.md +14 -4
- package/lib/channels/base.js +6 -2
- package/lib/channels/commands/index.js +42 -0
- package/lib/channels/commands/session.js +53 -0
- package/lib/channels/commands/verify.js +18 -0
- package/lib/channels/telegram.js +79 -28
- package/lib/chat/CLAUDE.md +4 -4
- package/lib/chat/actions.js +270 -49
- package/lib/chat/api.js +185 -31
- package/lib/chat/components/CLAUDE.md +6 -2
- package/lib/chat/components/chat-input.js +77 -47
- package/lib/chat/components/chat-input.jsx +77 -40
- package/lib/chat/components/chat-page.js +2 -0
- package/lib/chat/components/chat-page.jsx +3 -0
- package/lib/chat/components/chat.js +62 -14
- package/lib/chat/components/chat.jsx +68 -10
- package/lib/chat/components/code-mode-toggle.js +141 -22
- package/lib/chat/components/code-mode-toggle.jsx +129 -20
- package/lib/chat/components/containers-page.js +58 -40
- package/lib/chat/components/containers-page.jsx +64 -25
- package/lib/chat/components/crons-page.js +17 -3
- package/lib/chat/components/crons-page.jsx +34 -6
- package/lib/chat/components/index.js +2 -2
- package/lib/chat/components/message.js +18 -3
- package/lib/chat/components/message.jsx +18 -3
- package/lib/chat/components/profile-page.js +182 -4
- package/lib/chat/components/profile-page.jsx +196 -1
- package/lib/chat/components/scope-picker.js +21 -0
- package/lib/chat/components/scope-picker.jsx +27 -0
- package/lib/chat/components/settings-chat-page.js +11 -11
- package/lib/chat/components/settings-chat-page.jsx +14 -18
- package/lib/chat/components/settings-coding-agents-page.js +110 -16
- package/lib/chat/components/settings-coding-agents-page.jsx +87 -3
- package/lib/chat/components/settings-github-page.js +5 -0
- package/lib/chat/components/settings-github-page.jsx +5 -0
- package/lib/chat/components/settings-layout.js +3 -3
- package/lib/chat/components/settings-layout.jsx +3 -3
- package/lib/chat/components/settings-secrets-layout.js +1 -2
- package/lib/chat/components/settings-secrets-layout.jsx +1 -2
- package/lib/chat/components/settings-secrets-page.js +180 -75
- package/lib/chat/components/settings-secrets-page.jsx +212 -66
- package/lib/chat/components/triggers-page.js +17 -3
- package/lib/chat/components/triggers-page.jsx +34 -6
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/chat/components/ui/dropdown-menu.js +23 -2
- package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
- package/lib/chat/telegram-profile.js +33 -0
- package/lib/cluster/CLAUDE.md +9 -3
- package/lib/code/CLAUDE.md +11 -3
- package/lib/code/actions.js +47 -8
- package/lib/code/terminal-view.js +31 -21
- package/lib/code/terminal-view.jsx +32 -23
- package/lib/config.js +15 -4
- package/lib/containers/CLAUDE.md +16 -6
- package/lib/db/CLAUDE.md +5 -2
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +8 -3
- package/lib/db/config.js +0 -1
- package/lib/db/index.js +12 -0
- package/lib/db/schema.js +24 -1
- package/lib/db/user-channels.js +129 -0
- package/lib/llm-providers.js +8 -0
- package/lib/maintenance.js +31 -21
- package/lib/tools/CLAUDE.md +12 -3
- package/lib/tools/assemblyai.js +17 -0
- package/lib/tools/create-agent-job.js +12 -8
- package/lib/tools/docker.js +34 -10
- package/lib/tools/github.js +34 -0
- package/lib/tools/telegram.js +106 -0
- package/lib/utils/render-md.js +44 -18
- package/package.json +8 -8
- package/setup/CLAUDE.md +11 -5
- package/setup/lib/providers.mjs +2 -1
- package/setup/lib/targets.mjs +13 -16
- package/setup/lib/telegram.mjs +8 -69
- package/templates/.env.example +0 -7
- package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
- package/templates/.gitignore.template +1 -3
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.template +29 -7
- package/templates/agent-job/CLAUDE.md.template +5 -3
- package/templates/agent-job/CRONS.json +16 -0
- package/templates/agent-job/SYSTEM.md +16 -11
- package/templates/agents/CLAUDE.md.template +17 -17
- package/templates/coding-workspace/CLAUDE.md.template +7 -0
- package/templates/data/CLAUDE.md.template +1 -1
- package/templates/docker-compose.custom.yml +1 -0
- package/templates/docker-compose.yml +1 -0
- package/templates/event-handler/CLAUDE.md.template +79 -0
- package/templates/event-handler/TRIGGERS.json +18 -2
- package/templates/skills/CLAUDE.md.template +20 -22
- package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/SKILL.md +2 -2
- package/lib/ai/agent.js +0 -65
- package/lib/ai/async-channel.js +0 -51
- package/lib/ai/model.js +0 -130
- package/lib/ai/tools.js +0 -164
- package/lib/tools/openai.js +0 -37
- package/setup/lib/telegram-verify.mjs +0 -63
- package/setup/setup-telegram.mjs +0 -260
- package/templates/agent-job/SOUL.md +0 -17
- /package/templates/{skills/active/.gitkeep → coding-workspace/SYSTEM.md} +0 -0
- /package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/agent-job-secrets.js +0 -0
- /package/templates/skills/{library/playwright-cli → playwright-cli}/SKILL.md +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { verifyCommand } from './verify.js';
|
|
2
|
+
import { sessionCommand } from './session.js';
|
|
3
|
+
|
|
4
|
+
const PRE_AUTH = {
|
|
5
|
+
verify: verifyCommand,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const POST_AUTH = {
|
|
9
|
+
session: sessionCommand,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function parse(text) {
|
|
13
|
+
const trimmed = (text || '').trim();
|
|
14
|
+
if (!trimmed.startsWith('/')) return null;
|
|
15
|
+
const [head, ...rest] = trimmed.slice(1).split(/\s+/);
|
|
16
|
+
return { name: head.toLowerCase(), args: rest };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Dispatch a pre-auth command. Returns { handled, reply, userId? } or null.
|
|
21
|
+
* Called when the incoming channel chat is not yet bound to a user.
|
|
22
|
+
* Only /verify runs here.
|
|
23
|
+
*/
|
|
24
|
+
export async function dispatchPreAuthCommand(normalized, ctx) {
|
|
25
|
+
const parsed = parse(normalized.text);
|
|
26
|
+
if (!parsed) return null;
|
|
27
|
+
const handler = PRE_AUTH[parsed.name];
|
|
28
|
+
if (!handler) return null;
|
|
29
|
+
return handler({ args: parsed.args, normalized, ctx });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Dispatch a post-auth command. Returns { handled, reply } or null.
|
|
34
|
+
* ctx must include { userId, channel, channelChatId }.
|
|
35
|
+
*/
|
|
36
|
+
export async function dispatchCommand(normalized, ctx) {
|
|
37
|
+
const parsed = parse(normalized.text);
|
|
38
|
+
if (!parsed) return null;
|
|
39
|
+
const handler = POST_AUTH[parsed.name];
|
|
40
|
+
if (!handler) return null;
|
|
41
|
+
return handler({ args: parsed.args, normalized, ctx });
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { setActiveThread } from '../../db/user-channels.js';
|
|
3
|
+
import { getChatById, getChatsByUser } from '../../db/chats.js';
|
|
4
|
+
|
|
5
|
+
function shortId(id) {
|
|
6
|
+
return id.replace(/-/g, '').slice(0, 8);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* /session → mint a new session UUID, mark it active
|
|
11
|
+
* /session list → list the user's 10 most-recent chats
|
|
12
|
+
* /session <id|short> → switch active to an existing chat (full uuid or 8-char short id)
|
|
13
|
+
*/
|
|
14
|
+
export async function sessionCommand({ args, ctx }) {
|
|
15
|
+
const sub = args[0];
|
|
16
|
+
|
|
17
|
+
if (!sub) {
|
|
18
|
+
const threadId = randomUUID();
|
|
19
|
+
setActiveThread(ctx.userId, ctx.channel, threadId);
|
|
20
|
+
return {
|
|
21
|
+
handled: true,
|
|
22
|
+
reply: `New session: ${shortId(threadId)}\nSend a message to begin.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (sub.toLowerCase() === 'list') {
|
|
27
|
+
const chats = getChatsByUser(ctx.userId).slice(0, 10);
|
|
28
|
+
if (!chats.length) {
|
|
29
|
+
return { handled: true, reply: 'No sessions yet. Send a message or use /session to start one.' };
|
|
30
|
+
}
|
|
31
|
+
const lines = chats.map((c) => `${shortId(c.id)} ${c.title}`);
|
|
32
|
+
return { handled: true, reply: `Recent sessions:\n${lines.join('\n')}` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const target = resolveChatId(sub, ctx.userId);
|
|
36
|
+
if (!target) {
|
|
37
|
+
return { handled: true, reply: `Session not found: ${sub}` };
|
|
38
|
+
}
|
|
39
|
+
setActiveThread(ctx.userId, ctx.channel, target.id);
|
|
40
|
+
return { handled: true, reply: `Switched to session: ${shortId(target.id)}\n${target.title}` };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveChatId(input, userId) {
|
|
44
|
+
// Full UUID match first
|
|
45
|
+
const direct = getChatById(input);
|
|
46
|
+
if (direct && direct.userId === userId) return direct;
|
|
47
|
+
// Short-id match (8-char prefix of UUID minus dashes)
|
|
48
|
+
if (input.length === 8) {
|
|
49
|
+
const chats = getChatsByUser(userId);
|
|
50
|
+
return chats.find((c) => shortId(c.id) === input.toLowerCase()) || null;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { redeemCode } from '../../db/user-channels.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* /verify <code>
|
|
5
|
+
* Pre-auth: binds the incoming channel chat to the user who issued the code.
|
|
6
|
+
*/
|
|
7
|
+
export async function verifyCommand({ args, ctx }) {
|
|
8
|
+
const [code] = args;
|
|
9
|
+
if (!code) {
|
|
10
|
+
return { handled: true, reply: 'Usage: /verify <code>' };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const { userId } = redeemCode(ctx.channel, code, ctx.channelChatId);
|
|
14
|
+
return { handled: true, reply: 'Linked. Send a message to start chatting.', userId };
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return { handled: true, reply: `Verification failed: ${err.message}` };
|
|
17
|
+
}
|
|
18
|
+
}
|
package/lib/channels/telegram.js
CHANGED
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
downloadFile,
|
|
5
5
|
reactToMessage,
|
|
6
6
|
startTypingIndicator,
|
|
7
|
+
formatToolCall,
|
|
7
8
|
} from '../tools/telegram.js';
|
|
8
|
-
import {
|
|
9
|
+
import { isAssemblyAIEnabled, transcribeAudio } from '../tools/assemblyai.js';
|
|
10
|
+
import { getConfig } from '../config.js';
|
|
9
11
|
|
|
10
12
|
class TelegramAdapter extends ChannelAdapter {
|
|
11
13
|
constructor(botToken) {
|
|
@@ -17,17 +19,21 @@ class TelegramAdapter extends ChannelAdapter {
|
|
|
17
19
|
* Parse a Telegram webhook update into normalized message data.
|
|
18
20
|
* Handles: text, voice/audio (transcribed), photos, documents.
|
|
19
21
|
* Returns null if the update should be ignored.
|
|
22
|
+
*
|
|
23
|
+
* Does NOT authorize the chat — auth is handled downstream by resolving
|
|
24
|
+
* `channelChatId` against `user_channels`. This adapter only validates
|
|
25
|
+
* that the request came from Telegram (webhook secret) and extracts the
|
|
26
|
+
* payload.
|
|
20
27
|
*/
|
|
21
28
|
async receive(request) {
|
|
22
|
-
const
|
|
29
|
+
const webhookSecret = getConfig('TELEGRAM_WEBHOOK_SECRET');
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
if (!TELEGRAM_WEBHOOK_SECRET) {
|
|
31
|
+
if (!webhookSecret) {
|
|
26
32
|
console.error('[telegram] TELEGRAM_WEBHOOK_SECRET not configured — rejecting webhook');
|
|
27
33
|
return null;
|
|
28
34
|
}
|
|
29
35
|
const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
|
|
30
|
-
if (headerSecret !==
|
|
36
|
+
if (headerSecret !== webhookSecret) {
|
|
31
37
|
return null;
|
|
32
38
|
}
|
|
33
39
|
|
|
@@ -40,31 +46,19 @@ class TelegramAdapter extends ChannelAdapter {
|
|
|
40
46
|
let text = message.text || null;
|
|
41
47
|
const attachments = [];
|
|
42
48
|
|
|
43
|
-
// Check for verification code — works even before TELEGRAM_CHAT_ID is set
|
|
44
|
-
if (TELEGRAM_VERIFICATION && text === TELEGRAM_VERIFICATION) {
|
|
45
|
-
await sendMessage(this.botToken, chatId, `Your chat ID:\n<code>${chatId}</code>`);
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Security: if no TELEGRAM_CHAT_ID configured, ignore all messages
|
|
50
|
-
if (!TELEGRAM_CHAT_ID) return null;
|
|
51
|
-
|
|
52
|
-
// Security: only accept messages from configured chat
|
|
53
|
-
if (chatId !== TELEGRAM_CHAT_ID) return null;
|
|
54
|
-
|
|
55
49
|
// Voice messages → transcribe to text
|
|
56
50
|
if (message.voice) {
|
|
57
|
-
if (!
|
|
51
|
+
if (!isAssemblyAIEnabled()) {
|
|
58
52
|
await sendMessage(
|
|
59
53
|
this.botToken,
|
|
60
54
|
chatId,
|
|
61
|
-
'Voice messages are not supported. Please set
|
|
55
|
+
'Voice messages are not supported. Please set ASSEMBLYAI_API_KEY to enable transcription.'
|
|
62
56
|
);
|
|
63
57
|
return null;
|
|
64
58
|
}
|
|
65
59
|
try {
|
|
66
|
-
const { buffer
|
|
67
|
-
text = await transcribeAudio(buffer
|
|
60
|
+
const { buffer } = await downloadFile(this.botToken, message.voice.file_id);
|
|
61
|
+
text = await transcribeAudio(buffer);
|
|
68
62
|
} catch (err) {
|
|
69
63
|
console.error('Failed to transcribe voice:', err);
|
|
70
64
|
await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your voice message.');
|
|
@@ -74,17 +68,17 @@ class TelegramAdapter extends ChannelAdapter {
|
|
|
74
68
|
|
|
75
69
|
// Audio messages → transcribe to text
|
|
76
70
|
if (message.audio && !text) {
|
|
77
|
-
if (!
|
|
71
|
+
if (!isAssemblyAIEnabled()) {
|
|
78
72
|
await sendMessage(
|
|
79
73
|
this.botToken,
|
|
80
74
|
chatId,
|
|
81
|
-
'Audio messages are not supported. Please set
|
|
75
|
+
'Audio messages are not supported. Please set ASSEMBLYAI_API_KEY to enable transcription.'
|
|
82
76
|
);
|
|
83
77
|
return null;
|
|
84
78
|
}
|
|
85
79
|
try {
|
|
86
|
-
const { buffer
|
|
87
|
-
text = await transcribeAudio(buffer
|
|
80
|
+
const { buffer } = await downloadFile(this.botToken, message.audio.file_id);
|
|
81
|
+
text = await transcribeAudio(buffer);
|
|
88
82
|
} catch (err) {
|
|
89
83
|
console.error('Failed to transcribe audio:', err);
|
|
90
84
|
await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your audio message.');
|
|
@@ -121,7 +115,8 @@ class TelegramAdapter extends ChannelAdapter {
|
|
|
121
115
|
if (!text && attachments.length === 0) return null;
|
|
122
116
|
|
|
123
117
|
return {
|
|
124
|
-
|
|
118
|
+
channel: 'telegram',
|
|
119
|
+
channelChatId: chatId,
|
|
125
120
|
text: text || '',
|
|
126
121
|
attachments,
|
|
127
122
|
metadata: { messageId: message.message_id, chatId },
|
|
@@ -136,8 +131,64 @@ class TelegramAdapter extends ChannelAdapter {
|
|
|
136
131
|
return startTypingIndicator(this.botToken, metadata.chatId);
|
|
137
132
|
}
|
|
138
133
|
|
|
139
|
-
async sendResponse(
|
|
140
|
-
await sendMessage(this.botToken,
|
|
134
|
+
async sendResponse(channelChatId, text, metadata) {
|
|
135
|
+
await sendMessage(this.botToken, channelChatId, text);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Consume a chatStream() async iterable and send progressive messages.
|
|
140
|
+
* - Text chunks accumulate and flush when a tool-call arrives or stream ends.
|
|
141
|
+
* - Each tool-call sends immediately as its own message.
|
|
142
|
+
* - Tool-results react to the tool-call message with ✅ (or ❌ on error).
|
|
143
|
+
*
|
|
144
|
+
* @param {string} chatId - Telegram chat ID
|
|
145
|
+
* @param {AsyncIterable} chunks - chatStream() output
|
|
146
|
+
*/
|
|
147
|
+
async streamChatResponse(chatId, chunks) {
|
|
148
|
+
let textBuffer = '';
|
|
149
|
+
// Map toolCallId → { telegramMessageId, hasCompleteArgs }
|
|
150
|
+
const toolMessages = new Map();
|
|
151
|
+
|
|
152
|
+
for await (const chunk of chunks) {
|
|
153
|
+
if (chunk.type === 'text') {
|
|
154
|
+
textBuffer += chunk.text;
|
|
155
|
+
} else if (chunk.type === 'tool-call') {
|
|
156
|
+
// Skip the first empty-args emission — wait for complete args
|
|
157
|
+
if (!chunk.args || Object.keys(chunk.args).length === 0) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Flush accumulated text before tool call
|
|
162
|
+
if (textBuffer.trim()) {
|
|
163
|
+
await sendMessage(this.botToken, chatId, textBuffer.trim());
|
|
164
|
+
textBuffer = '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Send tool call as its own message
|
|
168
|
+
const text = formatToolCall(chunk.toolName, chunk.args);
|
|
169
|
+
try {
|
|
170
|
+
const msg = await sendMessage(this.botToken, chatId, text);
|
|
171
|
+
toolMessages.set(chunk.toolCallId, msg.message_id);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error('[telegram] Failed to send tool call:', err.message);
|
|
174
|
+
}
|
|
175
|
+
} else if (chunk.type === 'tool-result') {
|
|
176
|
+
const messageId = toolMessages.get(chunk.toolCallId);
|
|
177
|
+
if (messageId) {
|
|
178
|
+
const emoji = chunk.result?.includes?.('error') || chunk.result?.includes?.('Error')
|
|
179
|
+
? '👎'
|
|
180
|
+
: '👍';
|
|
181
|
+
reactToMessage(this.botToken, chatId, messageId, emoji).catch(() => {});
|
|
182
|
+
toolMessages.delete(chunk.toolCallId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Skip: meta, result, thinking-*, unknown
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Flush remaining text
|
|
189
|
+
if (textBuffer.trim()) {
|
|
190
|
+
await sendMessage(this.botToken, chatId, textBuffer.trim());
|
|
191
|
+
}
|
|
141
192
|
}
|
|
142
193
|
|
|
143
194
|
get supportsStreaming() {
|
package/lib/chat/CLAUDE.md
CHANGED
|
@@ -38,10 +38,10 @@ export { getRepositoriesHandler as GET } from 'thepopebot/chat/api';
|
|
|
38
38
|
## Chat Streaming Flow
|
|
39
39
|
|
|
40
40
|
1. Client sends message via AI SDK `DefaultChatTransport` → `POST /stream/chat`
|
|
41
|
-
2. Handler validates session, extracts text + file attachments from message parts
|
|
42
|
-
3. Calls `chatStream()` from `lib/ai/` which handles DB persistence and LLM invocation
|
|
43
|
-
4. Streams response chunks (text deltas, tool calls, tool results) via `createUIMessageStream`
|
|
44
|
-
5. After first message, client calls `/chat/finalize-chat` to generate auto-title
|
|
41
|
+
2. Handler validates session, extracts text + file attachments from message parts. Images and PDFs pass through as vision content; text files are inlined into the prompt.
|
|
42
|
+
3. Calls `chatStream()` from `lib/ai/` which handles DB persistence and LLM invocation. Two paths: SDK adapter (in-process, e.g. Claude Agent SDK) or direct headless container (other agents).
|
|
43
|
+
4. Streams response chunks (text deltas, tool calls, tool results, thinking blocks) via `createUIMessageStream`. Tool call/tool result pairs and `{ type: 'error' }` chunks are persisted as JSON message parts.
|
|
44
|
+
5. After the first user message streams, the client calls `/chat/finalize-chat` to generate the auto-title (helper LLM with truncated-description fallback).
|
|
45
45
|
|
|
46
46
|
## Server Actions (actions.js)
|
|
47
47
|
|