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
package/lib/ai/index.js
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createChannel, mergeAsyncIterables } from './async-channel.js';
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
3
2
|
import { z } from 'zod';
|
|
4
|
-
import { getAgentChat, getCodeChat } from './agent.js';
|
|
5
|
-
import { createModel } from './model.js';
|
|
6
3
|
import path from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { callHelperLlm, callHelperLlmStructured } from './helper-llm.js';
|
|
7
6
|
import { PROJECT_ROOT } from '../paths.js';
|
|
8
7
|
import { render_md } from '../utils/render-md.js';
|
|
9
|
-
import {
|
|
8
|
+
import { buildCodingAgentSystemPrompt } from './system-prompt.js';
|
|
9
|
+
import { getChatById, createChat, updateChatTitle, linkChatToWorkspace, saveMessage } from '../db/chats.js';
|
|
10
10
|
import { getConfig } from '../config.js';
|
|
11
11
|
import { getSdkAdapter } from './sdk-adapters/index.js';
|
|
12
|
-
import { ensureWorkspaceRepo
|
|
12
|
+
import { ensureWorkspaceRepo } from './workspace-setup.js';
|
|
13
|
+
import { resolveAgentScope } from './scope.js';
|
|
13
14
|
import { readSessionId, writeSessionId } from './session-manager.js';
|
|
14
15
|
import { workspaceDir as getWorkspaceDir } from '../tools/docker.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Ensure a chat exists in the DB and save a message.
|
|
18
19
|
* Centralized so every channel gets persistence automatically.
|
|
19
|
-
*
|
|
20
|
-
* @param {string} threadId - Chat/thread ID
|
|
21
|
-
* @param {string} role - 'user' or 'assistant'
|
|
22
|
-
* @param {string} text - Message text
|
|
23
|
-
* @param {object} [options] - { userId, chatTitle }
|
|
24
20
|
*/
|
|
25
21
|
function persistMessage(threadId, role, text, options = {}) {
|
|
26
22
|
try {
|
|
@@ -34,98 +30,26 @@ function persistMessage(threadId, role, text, options = {}) {
|
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* @param {string} threadId - Conversation thread ID (from channel adapter)
|
|
41
|
-
* @param {string} message - User's message text
|
|
42
|
-
* @param {Array} [attachments=[]] - Normalized attachments from adapter
|
|
43
|
-
* @param {object} [options] - { userId, chatTitle } for DB persistence
|
|
44
|
-
* @returns {Promise<string>} AI response text
|
|
33
|
+
* Collect streamed text for channels that don't stream (e.g. Telegram one-shot).
|
|
34
|
+
* Delegates to chatStream — single source of truth.
|
|
45
35
|
*/
|
|
46
36
|
async function chat(threadId, message, attachments = [], options = {}) {
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
if (sdkAdapter) {
|
|
51
|
-
let fullText = '';
|
|
52
|
-
for await (const chunk of chatStream(threadId, message, attachments, options)) {
|
|
53
|
-
if (chunk.type === 'text') fullText += chunk.text;
|
|
54
|
-
}
|
|
55
|
-
return fullText;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Legacy LangGraph path
|
|
59
|
-
const agent = await getAgentChat();
|
|
60
|
-
|
|
61
|
-
// Save user message to DB
|
|
62
|
-
persistMessage(threadId, 'user', message || '[attachment]', options);
|
|
63
|
-
|
|
64
|
-
// Build content blocks: text + any image attachments as base64 vision
|
|
65
|
-
const content = [];
|
|
66
|
-
|
|
67
|
-
if (message) {
|
|
68
|
-
content.push({ type: 'text', text: message });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (const att of attachments) {
|
|
72
|
-
if (att.category === 'image') {
|
|
73
|
-
content.push({
|
|
74
|
-
type: 'image_url',
|
|
75
|
-
image_url: {
|
|
76
|
-
url: `data:${att.mimeType};base64,${att.data.toString('base64')}`,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
// Documents: future handling
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// If only text and no attachments, simplify to a string
|
|
84
|
-
const messageContent = content.length === 1 && content[0].type === 'text'
|
|
85
|
-
? content[0].text
|
|
86
|
-
: content;
|
|
87
|
-
|
|
88
|
-
const result = await agent.invoke(
|
|
89
|
-
{ messages: [new HumanMessage({ content: messageContent })] },
|
|
90
|
-
{ configurable: { thread_id: threadId } }
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const lastMessage = result.messages[result.messages.length - 1];
|
|
94
|
-
|
|
95
|
-
// LangChain message content can be a string or an array of content blocks
|
|
96
|
-
let response;
|
|
97
|
-
if (typeof lastMessage.content === 'string') {
|
|
98
|
-
response = lastMessage.content;
|
|
99
|
-
} else {
|
|
100
|
-
response = lastMessage.content
|
|
101
|
-
.filter((block) => block.type === 'text')
|
|
102
|
-
.map((block) => block.text)
|
|
103
|
-
.join('\n');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Save assistant response to DB
|
|
107
|
-
persistMessage(threadId, 'assistant', response, options);
|
|
108
|
-
|
|
109
|
-
// Auto-generate title for new chats
|
|
110
|
-
if (options.userId && message) {
|
|
111
|
-
autoTitle(threadId, message).catch(() => {});
|
|
37
|
+
let fullText = '';
|
|
38
|
+
for await (const chunk of chatStream(threadId, message, attachments, options)) {
|
|
39
|
+
if (chunk.type === 'text') fullText += chunk.text;
|
|
112
40
|
}
|
|
113
|
-
|
|
114
|
-
return response;
|
|
41
|
+
return fullText;
|
|
115
42
|
}
|
|
116
43
|
|
|
117
44
|
/**
|
|
118
|
-
* Process a chat message with streaming
|
|
45
|
+
* Process a chat message with streaming.
|
|
119
46
|
* Saves user and assistant messages to the DB automatically.
|
|
120
47
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* @param {object} [options] - { userId, chatTitle, skipUserPersist } for DB persistence
|
|
125
|
-
* @returns {AsyncIterableIterator<string>} Stream of text chunks
|
|
48
|
+
* Two paths share identical chunk shape and persistence patterns:
|
|
49
|
+
* - SDK path: in-process @anthropic-ai/claude-agent-sdk (claude-code only)
|
|
50
|
+
* - Direct path: headless Docker container running the configured coding agent
|
|
126
51
|
*/
|
|
127
52
|
async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
128
|
-
// Resolve agent and workspace context
|
|
129
53
|
const isCodeMode = !!options.codeMode;
|
|
130
54
|
const existingChat = getChatById(threadId);
|
|
131
55
|
let workspaceId = options.workspaceId;
|
|
@@ -133,369 +57,329 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
133
57
|
const branch = options.branch;
|
|
134
58
|
const codeModeType = options.codeModeType || 'plan';
|
|
135
59
|
|
|
60
|
+
// Resolve workspace — for existing chats, read scope from DB (client may not resend it after refresh)
|
|
61
|
+
let resolvedScope = options.scope || null;
|
|
62
|
+
|
|
136
63
|
if (!existingChat) {
|
|
137
|
-
// Create workspace if not already provided
|
|
138
64
|
if (!workspaceId) {
|
|
65
|
+
// Resolve repo + branch on the server. Agent mode uses configured GH_OWNER/GH_REPO
|
|
66
|
+
// and always detects the default branch. Code mode uses user-picked repo and falls
|
|
67
|
+
// back to detection only if no branch was provided.
|
|
68
|
+
let resolvedRepo = repo;
|
|
69
|
+
if (!isCodeMode) {
|
|
70
|
+
const ghOwner = getConfig('GH_OWNER');
|
|
71
|
+
const ghRepo = getConfig('GH_REPO');
|
|
72
|
+
if (ghOwner && ghRepo) resolvedRepo = `${ghOwner}/${ghRepo}`;
|
|
73
|
+
}
|
|
74
|
+
let resolvedBranch = branch;
|
|
75
|
+
if (resolvedRepo && (!isCodeMode || !resolvedBranch)) {
|
|
76
|
+
try {
|
|
77
|
+
const { getDefaultBranch } = await import('../tools/github.js');
|
|
78
|
+
const detected = await getDefaultBranch(resolvedRepo);
|
|
79
|
+
if (detected) resolvedBranch = detected;
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
139
83
|
const { createCodeWorkspace, updateFeatureBranch } = await import('../db/code-workspaces.js');
|
|
140
84
|
const workspace = createCodeWorkspace(options.userId || 'unknown', {
|
|
141
|
-
repo:
|
|
142
|
-
branch:
|
|
85
|
+
repo: resolvedRepo,
|
|
86
|
+
branch: resolvedBranch,
|
|
87
|
+
scope: resolvedScope,
|
|
143
88
|
});
|
|
144
89
|
workspaceId = workspace.id;
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
90
|
+
const branchMode = getConfig(isCodeMode ? 'CODE_MODE_BRANCH' : 'AGENT_MODE_BRANCH');
|
|
91
|
+
if (branchMode === 'dynamic') {
|
|
92
|
+
const { generateRandomName } = await import('../utils/random-name.js');
|
|
93
|
+
const shortId = workspaceId.replace(/-/g, '').slice(0, 8);
|
|
94
|
+
const featureBranch = `thepopebot/${generateRandomName()}-${shortId}`;
|
|
95
|
+
updateFeatureBranch(workspaceId, featureBranch);
|
|
96
|
+
} else {
|
|
97
|
+
// Default branch mode — featureBranch mirrors the working branch so
|
|
98
|
+
// downstream prompts never see an empty value.
|
|
99
|
+
if (resolvedBranch) updateFeatureBranch(workspaceId, resolvedBranch);
|
|
100
|
+
}
|
|
149
101
|
}
|
|
150
102
|
createChat(options.userId || 'unknown', 'New Chat', threadId, { chatMode: isCodeMode ? 'code' : 'agent' });
|
|
151
103
|
linkChatToWorkspace(threadId, workspaceId);
|
|
152
104
|
} else {
|
|
153
105
|
workspaceId = workspaceId || existingChat.codeWorkspaceId;
|
|
106
|
+
if (!resolvedScope && workspaceId) {
|
|
107
|
+
const { getCodeWorkspaceById } = await import('../db/code-workspaces.js');
|
|
108
|
+
const ws = getCodeWorkspaceById(workspaceId);
|
|
109
|
+
if (ws?.scope) resolvedScope = ws.scope;
|
|
110
|
+
}
|
|
154
111
|
}
|
|
155
112
|
|
|
156
|
-
//
|
|
157
|
-
const sdkAdapter = getSdkAdapter(getConfig('CODING_AGENT'));
|
|
158
|
-
const useSDK = !!sdkAdapter;
|
|
159
|
-
|
|
160
|
-
// Save user message to DB (skip on regeneration — message already exists)
|
|
113
|
+
// Save user message (skip on regeneration — message already exists)
|
|
161
114
|
if (!options.skipUserPersist) {
|
|
162
115
|
persistMessage(threadId, 'user', message || '[attachment]', options);
|
|
163
116
|
}
|
|
164
117
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// 3. System prompt (agent mode: SOUL.md + SYSTEM.md, code mode: none)
|
|
191
|
-
let systemPrompt = null;
|
|
192
|
-
if (!isCodeMode) {
|
|
193
|
-
const soulPath = path.join(PROJECT_ROOT, 'agent-job/SOUL.md');
|
|
194
|
-
const systemPath = path.join(PROJECT_ROOT, 'agent-job/SYSTEM.md');
|
|
195
|
-
try {
|
|
196
|
-
const soul = render_md(soulPath) || '';
|
|
197
|
-
const system = render_md(systemPath) || '';
|
|
198
|
-
systemPrompt = [soul, system].filter(Boolean).join('\n\n') || null;
|
|
199
|
-
} catch {
|
|
200
|
-
// Files may not exist — proceed without system prompt
|
|
201
|
-
}
|
|
118
|
+
const wsBaseDir = getWorkspaceDir(workspaceId);
|
|
119
|
+
const repoDir = path.join(wsBaseDir, 'workspace');
|
|
120
|
+
const codingAgent = getConfig('CODING_AGENT') || 'claude-code';
|
|
121
|
+
const sdkAdapter = getSdkAdapter(codingAgent);
|
|
122
|
+
|
|
123
|
+
// Read the resolved repo + branch from the workspace record (set at creation time).
|
|
124
|
+
const { getCodeWorkspaceById } = await import('../db/code-workspaces.js');
|
|
125
|
+
const workspace = getCodeWorkspaceById(workspaceId);
|
|
126
|
+
const featureBranch = workspace?.featureBranch;
|
|
127
|
+
|
|
128
|
+
// Resolve agent-mode-specific vs code-mode-specific values used by both paths
|
|
129
|
+
let effectiveRepo, effectiveBranch, systemPrompt, injectSecrets;
|
|
130
|
+
if (isCodeMode) {
|
|
131
|
+
effectiveRepo = workspace?.repo || repo;
|
|
132
|
+
effectiveBranch = workspace?.branch || branch;
|
|
133
|
+
systemPrompt = buildCodingAgentSystemPrompt('code');
|
|
134
|
+
injectSecrets = false;
|
|
135
|
+
} else {
|
|
136
|
+
const ghOwner = getConfig('GH_OWNER');
|
|
137
|
+
const ghRepo = getConfig('GH_REPO');
|
|
138
|
+
if (!ghOwner || !ghRepo) {
|
|
139
|
+
const msg = 'GH_OWNER or GH_REPO not configured';
|
|
140
|
+
yield { type: 'error', message: msg };
|
|
141
|
+
persistMessage(threadId, 'assistant', JSON.stringify({ type: 'error', message: msg }), options);
|
|
142
|
+
return;
|
|
202
143
|
}
|
|
144
|
+
effectiveRepo = `${ghOwner}/${ghRepo}`;
|
|
145
|
+
effectiveBranch = workspace?.branch || null;
|
|
146
|
+
const { skillsDir } = resolveAgentScope(repoDir, resolvedScope || null);
|
|
147
|
+
systemPrompt = buildCodingAgentSystemPrompt('agent', skillsDir, resolvedScope || null);
|
|
148
|
+
injectSecrets = true;
|
|
149
|
+
}
|
|
203
150
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
for await (const chunk of sdkAdapter({
|
|
210
|
-
prompt: message,
|
|
211
|
-
workspaceDir: repoDir,
|
|
212
|
-
systemPrompt,
|
|
213
|
-
sessionId,
|
|
214
|
-
permissionMode: codeModeType,
|
|
215
|
-
attachments,
|
|
216
|
-
})) {
|
|
217
|
-
// Write session ID on first meta chunk
|
|
218
|
-
if (chunk.type === 'meta' && chunk.sessionId) {
|
|
219
|
-
writeSessionId(wsBaseDir, chunk.sessionId);
|
|
220
|
-
}
|
|
151
|
+
// Workspace setup — visible to user as a `workspace` tool-call the first time
|
|
152
|
+
const needsSetup = !existsSync(path.join(repoDir, '.git'));
|
|
153
|
+
const setupToolCallId = `setup-${workspaceId.slice(0, 8)}`;
|
|
154
|
+
const setupArgs = { repo: effectiveRepo, branch: effectiveBranch, featureBranch };
|
|
221
155
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} else if (chunk.type === 'tool-call') {
|
|
226
|
-
// Flush accumulated text before tool call
|
|
227
|
-
if (pendingText) {
|
|
228
|
-
persistMessage(threadId, 'assistant', pendingText, options);
|
|
229
|
-
pendingText = '';
|
|
230
|
-
}
|
|
231
|
-
pendingToolCalls.set(chunk.toolCallId, { toolName: chunk.toolName, args: chunk.args });
|
|
232
|
-
} else if (chunk.type === 'tool-result') {
|
|
233
|
-
const tc = pendingToolCalls.get(chunk.toolCallId);
|
|
234
|
-
if (tc) {
|
|
235
|
-
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
236
|
-
type: 'tool-invocation',
|
|
237
|
-
toolCallId: chunk.toolCallId,
|
|
238
|
-
toolName: tc.toolName,
|
|
239
|
-
state: 'output-available',
|
|
240
|
-
input: tc.args,
|
|
241
|
-
output: chunk.result,
|
|
242
|
-
}), options);
|
|
243
|
-
pendingToolCalls.delete(chunk.toolCallId);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
156
|
+
if (needsSetup) {
|
|
157
|
+
yield { type: 'tool-call', toolCallId: setupToolCallId, toolName: 'workspace', args: setupArgs };
|
|
158
|
+
}
|
|
246
159
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
160
|
+
try {
|
|
161
|
+
const setupOutput = await ensureWorkspaceRepo({ workspaceDir: repoDir, repo: effectiveRepo, branch: effectiveBranch, featureBranch });
|
|
162
|
+
if (needsSetup) {
|
|
163
|
+
const result = setupOutput || `Workspace ready on ${featureBranch || effectiveBranch}`;
|
|
164
|
+
yield { type: 'tool-result', toolCallId: setupToolCallId, result };
|
|
165
|
+
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
166
|
+
type: 'tool-invocation',
|
|
167
|
+
toolCallId: setupToolCallId,
|
|
168
|
+
toolName: 'workspace',
|
|
169
|
+
state: 'output-available',
|
|
170
|
+
input: setupArgs,
|
|
171
|
+
output: result,
|
|
172
|
+
}), options);
|
|
257
173
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const msg = err.message || 'Workspace setup failed';
|
|
176
|
+
if (needsSetup) {
|
|
177
|
+
yield { type: 'tool-result', toolCallId: setupToolCallId, result: `Setup failed: ${msg}` };
|
|
262
178
|
}
|
|
263
|
-
|
|
179
|
+
yield { type: 'error', message: msg };
|
|
180
|
+
persistMessage(threadId, 'assistant', JSON.stringify({ type: 'error', message: msg }), options);
|
|
264
181
|
return;
|
|
265
182
|
}
|
|
266
183
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// Build content blocks: text + any image/PDF attachments as vision
|
|
274
|
-
const content = [];
|
|
275
|
-
|
|
276
|
-
if (message) {
|
|
277
|
-
content.push({ type: 'text', text: message });
|
|
184
|
+
// Fork on SDK availability
|
|
185
|
+
if (sdkAdapter) {
|
|
186
|
+
yield* streamViaSdk({ threadId, message, attachments, options, wsBaseDir, repoDir, resolvedScope, isCodeMode, codeModeType, systemPrompt, workspaceId, sdkAdapter });
|
|
187
|
+
} else {
|
|
188
|
+
yield* streamViaContainer({ threadId, message, options, codingAgent, workspaceId, featureBranch, effectiveRepo, effectiveBranch, systemPrompt, injectSecrets, codeModeType, resolvedScope });
|
|
278
189
|
}
|
|
279
190
|
|
|
280
|
-
for (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const url = att.dataUrl
|
|
284
|
-
? att.dataUrl
|
|
285
|
-
: `data:${att.mimeType};base64,${att.data.toString('base64')}`;
|
|
286
|
-
content.push({
|
|
287
|
-
type: 'image_url',
|
|
288
|
-
image_url: { url },
|
|
289
|
-
});
|
|
290
|
-
}
|
|
191
|
+
// Auto-generate title for new chats (runs once after either path completes)
|
|
192
|
+
if (options.userId && message) {
|
|
193
|
+
autoTitle(threadId, message).catch(() => {});
|
|
291
194
|
}
|
|
195
|
+
}
|
|
292
196
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
messageContent += `\n\n[chat mode: ${codeModeType}]`;
|
|
302
|
-
} else if (Array.isArray(messageContent)) {
|
|
303
|
-
const textBlock = messageContent.find(b => b.type === 'text');
|
|
304
|
-
if (textBlock) textBlock.text += `\n\n[chat mode: ${codeModeType}]`;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
197
|
+
/**
|
|
198
|
+
* SDK path — in-process @anthropic-ai/claude-agent-sdk.
|
|
199
|
+
* Used only when a registered SDK adapter exists for the active coding agent.
|
|
200
|
+
*/
|
|
201
|
+
async function* streamViaSdk({ threadId, message, attachments, options, wsBaseDir, repoDir, resolvedScope, isCodeMode, codeModeType, systemPrompt, workspaceId, sdkAdapter }) {
|
|
202
|
+
const chatMode = isCodeMode ? 'code' : 'agent';
|
|
203
|
+
const { workingDir } = resolveAgentScope(repoDir, resolvedScope);
|
|
204
|
+
const sessionId = readSessionId(wsBaseDir);
|
|
307
205
|
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
const streamCallback = (chunk) => {
|
|
311
|
-
if (chunk === null) sideChannel.done();
|
|
312
|
-
else sideChannel.push(chunk);
|
|
313
|
-
};
|
|
206
|
+
let pendingText = '';
|
|
207
|
+
const pendingToolCalls = new Map();
|
|
314
208
|
|
|
315
209
|
try {
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const indexToToolCallId = {}; // chunk index → tool_call_id
|
|
330
|
-
const toolCallArgsEmitted = new Set(); // tool_call_ids whose complete args have been yielded
|
|
331
|
-
|
|
332
|
-
// Headless container streaming state
|
|
333
|
-
const headlessPendingToolCalls = new Map();
|
|
334
|
-
let pendingText = ''; // channel text, flushed to DB at tool boundaries
|
|
335
|
-
let llmTextAccum = ''; // langgraph text (direct response or LLM follow-up after container)
|
|
336
|
-
|
|
337
|
-
// Tag helper so mergeAsyncIterables can tell the two sources apart.
|
|
338
|
-
// The LangGraph wrapper also closes sideChannel when the agent stream
|
|
339
|
-
// finishes — this prevents a deadlock when no tool calls streamCallback.
|
|
340
|
-
async function* tagged(iter, source) {
|
|
341
|
-
for await (const item of iter) yield { _src: source, item };
|
|
342
|
-
if (source === 'lg') sideChannel.done();
|
|
343
|
-
}
|
|
210
|
+
for await (const chunk of sdkAdapter({
|
|
211
|
+
prompt: message,
|
|
212
|
+
workspaceDir: workingDir,
|
|
213
|
+
systemPrompt,
|
|
214
|
+
sessionId,
|
|
215
|
+
permissionMode: codeModeType,
|
|
216
|
+
attachments,
|
|
217
|
+
workspaceId,
|
|
218
|
+
chatMode,
|
|
219
|
+
})) {
|
|
220
|
+
if (chunk.type === 'meta' && chunk.sessionId) {
|
|
221
|
+
writeSessionId(wsBaseDir, chunk.sessionId);
|
|
222
|
+
}
|
|
344
223
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
args: tc.args,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Accumulate raw tool call arg strings from streaming chunks
|
|
371
|
-
if (msg.tool_call_chunks?.length > 0) {
|
|
372
|
-
for (const c of msg.tool_call_chunks) {
|
|
373
|
-
if (c.id) {
|
|
374
|
-
indexToToolCallId[c.index] = c.id;
|
|
375
|
-
toolCallRawArgs[c.id] = (toolCallRawArgs[c.id] || '') + (c.args || '');
|
|
376
|
-
} else if (c.index != null && indexToToolCallId[c.index]) {
|
|
377
|
-
const id = indexToToolCallId[c.index];
|
|
378
|
-
toolCallRawArgs[id] = (toolCallRawArgs[id] || '') + (c.args || '');
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// Re-yield tool-call with complete args once the JSON is fully streamed
|
|
382
|
-
for (const c of msg.tool_call_chunks) {
|
|
383
|
-
const id = c.id || indexToToolCallId[c.index];
|
|
384
|
-
if (id && toolCallRawArgs[id] && !toolCallArgsEmitted.has(id)) {
|
|
385
|
-
try {
|
|
386
|
-
const parsed = JSON.parse(toolCallRawArgs[id]);
|
|
387
|
-
toolCallArgsEmitted.add(id);
|
|
388
|
-
const tc = pendingToolCalls.get(id);
|
|
389
|
-
if (tc) {
|
|
390
|
-
tc.args = parsed;
|
|
391
|
-
yield { type: 'tool-call', toolCallId: id, toolName: tc.toolName, args: parsed };
|
|
392
|
-
}
|
|
393
|
-
} catch {} // args not complete yet, keep accumulating
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Text content (wrapped in structured object)
|
|
399
|
-
let text = '';
|
|
400
|
-
if (typeof msg.content === 'string') {
|
|
401
|
-
text = msg.content;
|
|
402
|
-
} else if (Array.isArray(msg.content)) {
|
|
403
|
-
text = msg.content
|
|
404
|
-
.filter((b) => b.type === 'text' && b.text)
|
|
405
|
-
.map((b) => b.text)
|
|
406
|
-
.join('');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (text) {
|
|
410
|
-
llmTextAccum += text;
|
|
411
|
-
yield { type: 'text', text };
|
|
412
|
-
}
|
|
413
|
-
} else if (msgType === 'tool') {
|
|
414
|
-
// Parse complete args from accumulated raw fragments
|
|
415
|
-
const tc = pendingToolCalls.get(msg.tool_call_id);
|
|
416
|
-
const rawArgs = toolCallRawArgs[msg.tool_call_id];
|
|
417
|
-
let completeArgs;
|
|
418
|
-
try { completeArgs = rawArgs ? JSON.parse(rawArgs) : {}; } catch { completeArgs = {}; }
|
|
419
|
-
|
|
420
|
-
// Tool result — ToolMessage has tool_call_id and content
|
|
421
|
-
yield {
|
|
422
|
-
type: 'tool-result',
|
|
423
|
-
toolCallId: msg.tool_call_id,
|
|
424
|
-
toolName: tc?.toolName,
|
|
425
|
-
args: completeArgs,
|
|
426
|
-
result: msg.content,
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
// Save complete tool invocation as JSON
|
|
430
|
-
if (tc) {
|
|
431
|
-
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
432
|
-
type: 'tool-invocation',
|
|
433
|
-
toolCallId: msg.tool_call_id,
|
|
434
|
-
toolName: tc.toolName,
|
|
435
|
-
state: 'output-available',
|
|
436
|
-
input: completeArgs,
|
|
437
|
-
output: msg.content,
|
|
438
|
-
}), options);
|
|
439
|
-
pendingToolCalls.delete(msg.tool_call_id);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
// Skip other message types (human, system)
|
|
443
|
-
|
|
444
|
-
} else {
|
|
445
|
-
// ── Side channel: headless container chunks ───────────────────────
|
|
446
|
-
const chunk = item;
|
|
447
|
-
|
|
448
|
-
if (chunk.type === 'text') {
|
|
449
|
-
pendingText += chunk.text;
|
|
450
|
-
yield chunk;
|
|
451
|
-
} else if (chunk.type === 'tool-call') {
|
|
452
|
-
// Flush accumulated text before tool call
|
|
453
|
-
if (pendingText) {
|
|
454
|
-
persistMessage(threadId, 'assistant', pendingText, options);
|
|
455
|
-
pendingText = '';
|
|
456
|
-
}
|
|
457
|
-
headlessPendingToolCalls.set(chunk.toolCallId, { toolName: chunk.toolName, args: chunk.args });
|
|
458
|
-
yield chunk;
|
|
459
|
-
} else if (chunk.type === 'tool-result') {
|
|
460
|
-
// Enrich with args from matching tool-call (required by api.js tool-input-available update)
|
|
461
|
-
const htc = headlessPendingToolCalls.get(chunk.toolCallId);
|
|
462
|
-
const enriched = htc ? { ...chunk, args: htc.args, toolName: htc.toolName } : chunk;
|
|
463
|
-
yield enriched;
|
|
464
|
-
if (htc) {
|
|
465
|
-
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
466
|
-
type: 'tool-invocation',
|
|
467
|
-
toolCallId: chunk.toolCallId,
|
|
468
|
-
toolName: htc.toolName,
|
|
469
|
-
state: 'output-available',
|
|
470
|
-
input: htc.args,
|
|
471
|
-
output: chunk.result,
|
|
472
|
-
}), options);
|
|
473
|
-
headlessPendingToolCalls.delete(chunk.toolCallId);
|
|
474
|
-
}
|
|
475
|
-
} else {
|
|
476
|
-
// unknown/meta events pass through unchanged
|
|
477
|
-
yield chunk;
|
|
478
|
-
}
|
|
224
|
+
if (chunk.type === 'text') {
|
|
225
|
+
pendingText += chunk.text;
|
|
226
|
+
} else if (chunk.type === 'tool-call') {
|
|
227
|
+
if (pendingText) {
|
|
228
|
+
persistMessage(threadId, 'assistant', pendingText, options);
|
|
229
|
+
pendingText = '';
|
|
230
|
+
}
|
|
231
|
+
pendingToolCalls.set(chunk.toolCallId, { toolName: chunk.toolName, args: chunk.args });
|
|
232
|
+
} else if (chunk.type === 'tool-result') {
|
|
233
|
+
const tc = pendingToolCalls.get(chunk.toolCallId);
|
|
234
|
+
if (tc) {
|
|
235
|
+
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
236
|
+
type: 'tool-invocation',
|
|
237
|
+
toolCallId: chunk.toolCallId,
|
|
238
|
+
toolName: tc.toolName,
|
|
239
|
+
state: 'output-available',
|
|
240
|
+
input: tc.args,
|
|
241
|
+
output: chunk.result,
|
|
242
|
+
}), options);
|
|
243
|
+
pendingToolCalls.delete(chunk.toolCallId);
|
|
479
244
|
}
|
|
480
245
|
}
|
|
481
|
-
} finally {
|
|
482
|
-
// Ensure no dangling promise when tool was never called
|
|
483
|
-
sideChannel.done();
|
|
484
|
-
}
|
|
485
246
|
|
|
486
|
-
|
|
247
|
+
yield chunk;
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const msg = err.message || 'SDK stream failed';
|
|
251
|
+
console.error('[streamViaSdk] error:', err);
|
|
252
|
+
yield { type: 'error', message: msg };
|
|
253
|
+
persistMessage(threadId, 'assistant', JSON.stringify({ type: 'error', message: msg }), options);
|
|
254
|
+
} finally {
|
|
487
255
|
if (pendingText) {
|
|
488
256
|
persistMessage(threadId, 'assistant', pendingText, options);
|
|
489
257
|
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Direct headless path — spawn the coding agent in an ephemeral Docker container,
|
|
263
|
+
* stream its output via parseHeadlessStream, and yield normalized chunks.
|
|
264
|
+
*
|
|
265
|
+
* Replaces the former LangGraph React agent + `coding_agent` tool — there is no
|
|
266
|
+
* LLM layer between the user's message and the container. Multi-turn memory
|
|
267
|
+
* lives in the agent's own session files inside the volume-mounted workspace
|
|
268
|
+
* (see docker/coding-agent/CLAUDE.md § Session Tracking).
|
|
269
|
+
*/
|
|
270
|
+
async function* streamViaContainer({ threadId, message, options, codingAgent, workspaceId, featureBranch, effectiveRepo, effectiveBranch, systemPrompt, injectSecrets, codeModeType, resolvedScope }) {
|
|
271
|
+
const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
|
|
272
|
+
const mode = codeModeType === 'code' ? 'dangerous' : 'plan';
|
|
273
|
+
const { runHeadlessContainer, tailContainerLogs, waitForContainer, removeContainer } = await import('../tools/docker.js');
|
|
274
|
+
const { parseHeadlessStream } = await import('./headless-stream.js');
|
|
275
|
+
|
|
276
|
+
// Synthetic coding_agent wrapper — bracketing tool-call/tool-result pair so the
|
|
277
|
+
// user can see that the direct-container path ran (the SDK path has no wrapper).
|
|
278
|
+
// Inner container chunks stream as top-level parts alongside it; AI SDK parts
|
|
279
|
+
// are flat, so this is a visual anchor, not a container for nested content.
|
|
280
|
+
const wrapperId = `coding-agent-${randomUUID().slice(0, 8)}`;
|
|
281
|
+
const providerKeys = {
|
|
282
|
+
'claude-code': 'CODING_AGENT_CLAUDE_CODE_BACKEND',
|
|
283
|
+
'pi-coding-agent': 'CODING_AGENT_PI_PROVIDER',
|
|
284
|
+
'gemini-cli': 'CODING_AGENT_GEMINI_CLI_PROVIDER',
|
|
285
|
+
'codex-cli': 'CODING_AGENT_CODEX_CLI_PROVIDER',
|
|
286
|
+
'opencode': 'CODING_AGENT_OPENCODE_PROVIDER',
|
|
287
|
+
'kimi-cli': 'CODING_AGENT_KIMI_CLI_PROVIDER',
|
|
288
|
+
};
|
|
289
|
+
const backendApi = getConfig(providerKeys[codingAgent]) || 'anthropic';
|
|
290
|
+
const wrapperArgs = { prompt: message, codingAgent, backendApi };
|
|
291
|
+
|
|
292
|
+
yield { type: 'tool-call', toolCallId: wrapperId, toolName: 'coding_agent', args: wrapperArgs };
|
|
293
|
+
|
|
294
|
+
let pendingText = '';
|
|
295
|
+
const pendingToolCalls = new Map();
|
|
296
|
+
let started = false;
|
|
297
|
+
let wrapperClosed = false;
|
|
298
|
+
|
|
299
|
+
const closeWrapper = (result) => {
|
|
300
|
+
if (wrapperClosed) return null;
|
|
301
|
+
wrapperClosed = true;
|
|
302
|
+
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
303
|
+
type: 'tool-invocation',
|
|
304
|
+
toolCallId: wrapperId,
|
|
305
|
+
toolName: 'coding_agent',
|
|
306
|
+
state: 'output-available',
|
|
307
|
+
input: wrapperArgs,
|
|
308
|
+
output: result,
|
|
309
|
+
}), options);
|
|
310
|
+
return { type: 'tool-result', toolCallId: wrapperId, result };
|
|
311
|
+
};
|
|
490
312
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
313
|
+
try {
|
|
314
|
+
await runHeadlessContainer({
|
|
315
|
+
containerName,
|
|
316
|
+
repo: effectiveRepo,
|
|
317
|
+
branch: effectiveBranch,
|
|
318
|
+
featureBranch,
|
|
319
|
+
workspaceId,
|
|
320
|
+
taskPrompt: message,
|
|
321
|
+
mode,
|
|
322
|
+
systemPrompt,
|
|
323
|
+
injectSecrets,
|
|
324
|
+
scope: resolvedScope || undefined,
|
|
325
|
+
});
|
|
326
|
+
started = true;
|
|
327
|
+
|
|
328
|
+
const logStream = await tailContainerLogs(containerName);
|
|
329
|
+
|
|
330
|
+
for await (const chunk of parseHeadlessStream(logStream, codingAgent)) {
|
|
331
|
+
if (chunk.type === 'text') {
|
|
332
|
+
pendingText += chunk.text;
|
|
333
|
+
} else if (chunk.type === 'tool-call') {
|
|
334
|
+
if (pendingText) {
|
|
335
|
+
persistMessage(threadId, 'assistant', pendingText, options);
|
|
336
|
+
pendingText = '';
|
|
337
|
+
}
|
|
338
|
+
pendingToolCalls.set(chunk.toolCallId, { toolName: chunk.toolName, args: chunk.args });
|
|
339
|
+
} else if (chunk.type === 'tool-result') {
|
|
340
|
+
const tc = pendingToolCalls.get(chunk.toolCallId);
|
|
341
|
+
if (tc) {
|
|
342
|
+
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
343
|
+
type: 'tool-invocation',
|
|
344
|
+
toolCallId: chunk.toolCallId,
|
|
345
|
+
toolName: tc.toolName,
|
|
346
|
+
state: 'output-available',
|
|
347
|
+
input: tc.args,
|
|
348
|
+
output: chunk.result,
|
|
349
|
+
}), options);
|
|
350
|
+
pendingToolCalls.delete(chunk.toolCallId);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
yield chunk;
|
|
494
355
|
}
|
|
495
356
|
|
|
357
|
+
const exitCode = await waitForContainer(containerName);
|
|
358
|
+
await removeContainer(containerName);
|
|
359
|
+
|
|
360
|
+
const closeResult = exitCode === 0 ? 'Completed' : `Exited with code ${exitCode}`;
|
|
361
|
+
const closeChunk = closeWrapper(closeResult);
|
|
362
|
+
if (closeChunk) yield closeChunk;
|
|
363
|
+
|
|
364
|
+
if (exitCode !== 0) {
|
|
365
|
+
const msg = `Coding agent exited with code ${exitCode}`;
|
|
366
|
+
yield { type: 'error', message: msg };
|
|
367
|
+
persistMessage(threadId, 'assistant', JSON.stringify({ type: 'error', message: msg }), options);
|
|
368
|
+
}
|
|
496
369
|
} catch (err) {
|
|
497
|
-
|
|
498
|
-
|
|
370
|
+
const msg = err.message || 'Coding agent failed';
|
|
371
|
+
console.error('[streamViaContainer] error:', err);
|
|
372
|
+
const closeChunk = closeWrapper(`Error: ${msg}`);
|
|
373
|
+
if (closeChunk) yield closeChunk;
|
|
374
|
+
yield { type: 'error', message: msg };
|
|
375
|
+
persistMessage(threadId, 'assistant', JSON.stringify({ type: 'error', message: msg }), options);
|
|
376
|
+
if (started) {
|
|
377
|
+
try { await removeContainer(containerName); } catch {}
|
|
378
|
+
}
|
|
379
|
+
} finally {
|
|
380
|
+
if (pendingText) {
|
|
381
|
+
persistMessage(threadId, 'assistant', pendingText, options);
|
|
382
|
+
}
|
|
499
383
|
}
|
|
500
384
|
}
|
|
501
385
|
|
|
@@ -508,15 +392,16 @@ async function autoTitle(threadId, firstMessage) {
|
|
|
508
392
|
const chat = getChatById(threadId);
|
|
509
393
|
if (!chat || chat.title !== 'New Chat') return;
|
|
510
394
|
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
395
|
+
const result = await callHelperLlmStructured({
|
|
396
|
+
system: 'Title this chat in 2-5 words. Name the subject matter only. Never start with "User". Never describe what the user is doing — just the topic. Always produce a title, even for vague messages — infer the likely topic.',
|
|
397
|
+
user: firstMessage,
|
|
398
|
+
schema: z.object({ title: z.string() }),
|
|
399
|
+
maxTokens: 250,
|
|
400
|
+
});
|
|
401
|
+
const title = result?.title?.trim();
|
|
402
|
+
if (title) {
|
|
403
|
+
updateChatTitle(threadId, title);
|
|
404
|
+
return title;
|
|
520
405
|
}
|
|
521
406
|
} catch (err) {
|
|
522
407
|
console.error('[autoTitle] Failed to generate title:', err.message);
|
|
@@ -527,13 +412,9 @@ async function autoTitle(threadId, firstMessage) {
|
|
|
527
412
|
/**
|
|
528
413
|
* One-shot summarization with a different system prompt and no memory.
|
|
529
414
|
* Used for agent job completion summaries sent via GitHub webhook.
|
|
530
|
-
*
|
|
531
|
-
* @param {object} results - Agent job results from webhook payload
|
|
532
|
-
* @returns {Promise<string>} Summary text
|
|
533
415
|
*/
|
|
534
416
|
async function summarizeAgentJob(results) {
|
|
535
417
|
try {
|
|
536
|
-
const model = await createModel({ maxTokens: 1024 });
|
|
537
418
|
const summaryMdPath = path.join(PROJECT_ROOT, 'event-handler/SUMMARY.md');
|
|
538
419
|
const systemPrompt = render_md(summaryMdPath);
|
|
539
420
|
|
|
@@ -556,22 +437,11 @@ async function summarizeAgentJob(results) {
|
|
|
556
437
|
|
|
557
438
|
console.log(`[summarizeAgentJob] System prompt: ${systemPrompt.length} chars, user message: ${userMessage.length} chars`);
|
|
558
439
|
|
|
559
|
-
const
|
|
560
|
-
['system', systemPrompt],
|
|
561
|
-
['human', userMessage],
|
|
562
|
-
]);
|
|
563
|
-
|
|
564
|
-
const text =
|
|
565
|
-
typeof response.content === 'string'
|
|
566
|
-
? response.content
|
|
567
|
-
: response.content
|
|
568
|
-
.filter((block) => block.type === 'text')
|
|
569
|
-
.map((block) => block.text)
|
|
570
|
-
.join('\n');
|
|
440
|
+
const text = await callHelperLlm({ system: systemPrompt, user: userMessage, maxTokens: 1024 });
|
|
571
441
|
|
|
572
442
|
console.log(`[summarizeAgentJob] Result: ${text.length} chars — ${text.slice(0, 200)}`);
|
|
573
443
|
|
|
574
|
-
return text
|
|
444
|
+
return text || 'Agent job finished.';
|
|
575
445
|
} catch (err) {
|
|
576
446
|
console.error('[summarizeAgentJob] Failed to summarize agent job:', err);
|
|
577
447
|
return 'Agent job finished.';
|