thepopebot 1.2.74-beta.21 → 1.2.74-beta.23
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/lib/ai/agent.js +10 -11
- package/lib/ai/headless-stream.js +26 -37
- package/lib/ai/index.js +3 -16
- package/lib/ai/tools.js +4 -4
- package/lib/chat/components/code-mode-toggle.js +2 -2
- package/lib/chat/components/code-mode-toggle.jsx +2 -2
- package/lib/chat/components/message.js +3 -1
- package/lib/chat/components/message.jsx +3 -1
- package/lib/code/actions.js +2 -3
- package/package.json +2 -1
package/lib/ai/agent.js
CHANGED
|
@@ -3,12 +3,16 @@ import { SystemMessage } from '@langchain/core/messages';
|
|
|
3
3
|
import { createModel } from './model.js';
|
|
4
4
|
import { createJobTool, getJobStatusTool, getSystemTechnicalSpecsTool, getSkillBuildingGuideTool, getSkillDetailsTool, createStartHeadlessCodingTool, createGetRepositoryDetailsTool } from './tools.js';
|
|
5
5
|
import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
|
|
6
|
-
import { jobPlanningMd, codePlanningMd,
|
|
6
|
+
import { jobPlanningMd, codePlanningMd, thepopebotDb } from '../paths.js';
|
|
7
7
|
import { render_md } from '../utils/render-md.js';
|
|
8
8
|
import { createWebSearchTool, getProvider } from './web-search.js';
|
|
9
9
|
|
|
10
10
|
let _agent = null;
|
|
11
11
|
|
|
12
|
+
let _currentCodeModeType = 'plan';
|
|
13
|
+
export function setCurrentCodeModeType(mode) { _currentCodeModeType = mode; }
|
|
14
|
+
export function getCurrentCodeModeType() { return _currentCodeModeType; }
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Get or create the LangGraph job agent singleton.
|
|
14
18
|
* Uses createReactAgent which handles the tool loop automatically.
|
|
@@ -49,19 +53,16 @@ const _codeAgents = new Map();
|
|
|
49
53
|
/**
|
|
50
54
|
* Get or create a code agent for a specific chat/workspace.
|
|
51
55
|
* Each code chat gets its own agent with unique start_coding tool bindings.
|
|
52
|
-
* Agents are keyed by chatId+mode so switching modes creates a new agent with the right prompt.
|
|
53
56
|
* @param {object} context
|
|
54
57
|
* @param {string} context.repo - GitHub repo
|
|
55
58
|
* @param {string} context.branch - Git branch
|
|
56
59
|
* @param {string} context.workspaceId - Pre-created workspace row ID
|
|
57
60
|
* @param {string} context.chatId - Chat thread ID
|
|
58
|
-
* @param {string} [context.codeModeType='plan'] - 'plan' or 'code'
|
|
59
61
|
* @returns {Promise<object>} LangGraph agent
|
|
60
62
|
*/
|
|
61
|
-
export async function getCodeAgent({ repo, branch, workspaceId, chatId
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return _codeAgents.get(cacheKey);
|
|
63
|
+
export async function getCodeAgent({ repo, branch, workspaceId, chatId }) {
|
|
64
|
+
if (_codeAgents.has(chatId)) {
|
|
65
|
+
return _codeAgents.get(chatId);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const model = await createModel();
|
|
@@ -78,15 +79,13 @@ export async function getCodeAgent({ repo, branch, workspaceId, chatId, codeMode
|
|
|
78
79
|
|
|
79
80
|
const checkpointer = SqliteSaver.fromConnString(thepopebotDb);
|
|
80
81
|
|
|
81
|
-
const promptMd = codeModeType === 'code' ? codeExecutionMd : codePlanningMd;
|
|
82
|
-
|
|
83
82
|
const agent = createReactAgent({
|
|
84
83
|
llm: model,
|
|
85
84
|
tools,
|
|
86
85
|
checkpointSaver: checkpointer,
|
|
87
|
-
prompt: (state) => [new SystemMessage(render_md(
|
|
86
|
+
prompt: (state) => [new SystemMessage(render_md(codePlanningMd)), ...state.messages],
|
|
88
87
|
});
|
|
89
88
|
|
|
90
|
-
_codeAgents.set(
|
|
89
|
+
_codeAgents.set(chatId, agent);
|
|
91
90
|
return agent;
|
|
92
91
|
}
|
|
@@ -1,55 +1,43 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
import split2 from 'split2';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Parse Docker container logs from a headless Claude Code container
|
|
3
6
|
* running with --output-format stream-json.
|
|
4
7
|
*
|
|
5
8
|
* Three layers:
|
|
6
|
-
* 1. Docker multiplexed frame
|
|
7
|
-
* 2. NDJSON line
|
|
9
|
+
* 1. Docker multiplexed frame decoder (Transform stream)
|
|
10
|
+
* 2. split2 for reliable NDJSON line splitting
|
|
8
11
|
* 3. Claude Code stream-json → chat event mapper
|
|
9
12
|
*
|
|
10
13
|
* @param {import('http').IncomingMessage} dockerLogStream - Raw Docker log stream
|
|
11
14
|
* @yields {{ type: string, text?: string, toolCallId?: string, toolName?: string, args?: object, result?: string }}
|
|
12
15
|
*/
|
|
13
16
|
export async function* parseHeadlessStream(dockerLogStream) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const streamType = frameBuf[0];
|
|
26
|
-
if (streamType === 1) { // stdout only
|
|
27
|
-
decoded += frameBuf.slice(8, 8 + size).toString('utf8');
|
|
17
|
+
// Layer 1: Docker frame decoder
|
|
18
|
+
const frameDecoder = new Transform({
|
|
19
|
+
transform(chunk, encoding, callback) {
|
|
20
|
+
this._buf = this._buf ? Buffer.concat([this._buf, chunk]) : chunk;
|
|
21
|
+
while (this._buf.length >= 8) {
|
|
22
|
+
const size = this._buf.readUInt32BE(4);
|
|
23
|
+
if (this._buf.length < 8 + size) break;
|
|
24
|
+
if (this._buf[0] === 1) { // stdout only
|
|
25
|
+
this.push(this._buf.slice(8, 8 + size));
|
|
26
|
+
}
|
|
27
|
+
this._buf = this._buf.slice(8 + size);
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
callback();
|
|
30
30
|
}
|
|
31
|
+
});
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Layer 2: NDJSON line splitter
|
|
35
|
-
lineBuf += decoded;
|
|
36
|
-
const lines = lineBuf.split('\n');
|
|
37
|
-
lineBuf = lines.pop(); // keep incomplete last piece
|
|
38
|
-
|
|
39
|
-
for (const line of lines) {
|
|
40
|
-
const trimmed = line.trim();
|
|
41
|
-
if (!trimmed) continue;
|
|
42
|
-
|
|
43
|
-
// Layer 3: Event mapper
|
|
44
|
-
for (const event of mapLine(trimmed)) {
|
|
45
|
-
yield event;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
33
|
+
// Layer 2: split2 for reliable line splitting
|
|
34
|
+
const lines = dockerLogStream.pipe(frameDecoder).pipe(split2());
|
|
49
35
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
// Layer 3: map each complete line to chat events
|
|
37
|
+
for await (const line of lines) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed) continue;
|
|
40
|
+
for (const event of mapLine(trimmed)) {
|
|
53
41
|
yield event;
|
|
54
42
|
}
|
|
55
43
|
}
|
|
@@ -65,6 +53,7 @@ export function mapLine(line) {
|
|
|
65
53
|
try {
|
|
66
54
|
parsed = JSON.parse(line);
|
|
67
55
|
} catch {
|
|
56
|
+
console.warn('[headless-stream] JSON parse failed, length:', line.length, 'preview:', line.slice(0, 120));
|
|
68
57
|
// Non-JSON lines (NO_CHANGES, MERGE_SUCCESS, AGENT_FAILED, etc.)
|
|
69
58
|
return [{ type: 'text', text: `\n${line}\n` }];
|
|
70
59
|
}
|
package/lib/ai/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { getJobAgent, getCodeAgent } from './agent.js';
|
|
3
|
+
import { getJobAgent, getCodeAgent, setCurrentCodeModeType } from './agent.js';
|
|
4
4
|
import { createModel } from './model.js';
|
|
5
5
|
import { jobSummaryMd } from '../paths.js';
|
|
6
6
|
import { render_md } from '../utils/render-md.js';
|
|
@@ -134,12 +134,12 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
134
134
|
workspaceId = existingChat.codeWorkspaceId;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
setCurrentCodeModeType(options.codeModeType || 'plan');
|
|
137
138
|
agent = await getCodeAgent({
|
|
138
139
|
repo: options.repo,
|
|
139
140
|
branch: options.branch,
|
|
140
141
|
workspaceId,
|
|
141
142
|
chatId: threadId,
|
|
142
|
-
codeModeType: options.codeModeType || 'plan',
|
|
143
143
|
});
|
|
144
144
|
} else {
|
|
145
145
|
agent = await getJobAgent();
|
|
@@ -194,14 +194,6 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
194
194
|
|
|
195
195
|
if (msgType === 'ai') {
|
|
196
196
|
// Debug: log web search content blocks to see actual shape
|
|
197
|
-
if (Array.isArray(msg.content)) {
|
|
198
|
-
for (const block of msg.content) {
|
|
199
|
-
if (block.type === 'server_tool_use' || block.type === 'server_tool_call' || block.type === 'web_search_tool_result') {
|
|
200
|
-
console.log(`[chatStream] ${block.type}:`, JSON.stringify(block));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
197
|
// Tool calls — AIMessage.tool_calls is an array of { id, name, args }
|
|
206
198
|
if (msg.tool_calls?.length > 0) {
|
|
207
199
|
for (const tc of msg.tool_calls) {
|
|
@@ -274,7 +266,7 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
274
266
|
// Phase 2: Stream headless container output live
|
|
275
267
|
if (headlessContainer) {
|
|
276
268
|
try {
|
|
277
|
-
const { tailContainerLogs, waitForContainer, removeContainer
|
|
269
|
+
const { tailContainerLogs, waitForContainer, removeContainer } =
|
|
278
270
|
await import('../tools/docker.js');
|
|
279
271
|
const { parseHeadlessStream } = await import('./headless-stream.js');
|
|
280
272
|
|
|
@@ -317,11 +309,6 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
317
309
|
await removeContainer(headlessContainer.containerName);
|
|
318
310
|
|
|
319
311
|
if (exitCode === 0) {
|
|
320
|
-
const existingChat = getChatById(threadId);
|
|
321
|
-
const wsId = existingChat?.codeWorkspaceId;
|
|
322
|
-
if (wsId) {
|
|
323
|
-
await removeCodeWorkspaceVolume(wsId);
|
|
324
|
-
}
|
|
325
312
|
const completionMsg = '\n\nTask completed and merged back to base branch.';
|
|
326
313
|
yield { type: 'text', text: completionMsg };
|
|
327
314
|
fullText += completionMsg;
|
package/lib/ai/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
import { createJob } from '../tools/create-job.js';
|
|
6
6
|
import { getJobStatus } from '../tools/github.js';
|
|
7
7
|
import { claudeMd, skillGuidePath, skillsDir } from '../paths.js';
|
|
8
|
+
import { getCurrentCodeModeType } from './agent.js';
|
|
8
9
|
|
|
9
10
|
const createJobTool = tool(
|
|
10
11
|
async ({ job_description }) => {
|
|
@@ -276,7 +277,7 @@ function createGetRepositoryDetailsTool({ repo, branch }) {
|
|
|
276
277
|
*/
|
|
277
278
|
function createStartHeadlessCodingTool({ repo, branch, workspaceId }) {
|
|
278
279
|
return tool(
|
|
279
|
-
async ({ task_description
|
|
280
|
+
async ({ task_description }) => {
|
|
280
281
|
try {
|
|
281
282
|
const { randomUUID } = await import('crypto');
|
|
282
283
|
const containerName = `code-headless-${randomUUID().slice(0, 8)}`;
|
|
@@ -285,6 +286,8 @@ function createStartHeadlessCodingTool({ repo, branch, workspaceId }) {
|
|
|
285
286
|
const workspace = getCodeWorkspaceById(workspaceId);
|
|
286
287
|
const featureBranch = workspace?.featureBranch || `thepopebot/new-chat-${workspaceId.replace(/-/g, '').slice(0, 8)}`;
|
|
287
288
|
|
|
289
|
+
const plan = getCurrentCodeModeType() === 'plan';
|
|
290
|
+
|
|
288
291
|
const { runHeadlessCodeContainer } = await import('../tools/docker.js');
|
|
289
292
|
|
|
290
293
|
await runHeadlessCodeContainer({
|
|
@@ -315,9 +318,6 @@ function createStartHeadlessCodingTool({ repo, branch, workspaceId }) {
|
|
|
315
318
|
task_description: z.string().describe(
|
|
316
319
|
'Detailed description of the coding task. Include context, requirements, and any specific instructions.'
|
|
317
320
|
),
|
|
318
|
-
plan: z.boolean().default(true).describe(
|
|
319
|
-
'Defaults to true (plan permission mode — read-only, no writes). Set to false to run in dangerous mode with full write access for code execution.'
|
|
320
|
-
),
|
|
321
321
|
}),
|
|
322
322
|
}
|
|
323
323
|
);
|
|
@@ -131,7 +131,7 @@ function CodeModeToggle({
|
|
|
131
131
|
}
|
|
132
132
|
),
|
|
133
133
|
enabled && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
134
|
-
/* @__PURE__ */ jsx("div", { className: "w-full sm:w-auto sm:min-w-[
|
|
134
|
+
/* @__PURE__ */ jsx("div", { className: "w-full sm:w-auto sm:min-w-[240px] sm:max-w-[240px]", children: /* @__PURE__ */ jsx(
|
|
135
135
|
Combobox,
|
|
136
136
|
{
|
|
137
137
|
options: repoOptions,
|
|
@@ -142,7 +142,7 @@ function CodeModeToggle({
|
|
|
142
142
|
highlight: !repo && !loadingRepos
|
|
143
143
|
}
|
|
144
144
|
) }),
|
|
145
|
-
/* @__PURE__ */ jsx("div", { className: cn("w-full sm:w-auto sm:min-w-[
|
|
145
|
+
/* @__PURE__ */ jsx("div", { className: cn("w-full sm:w-auto sm:min-w-[200px] sm:max-w-[200px]", !repo && "opacity-50 pointer-events-none"), children: /* @__PURE__ */ jsx(
|
|
146
146
|
Combobox,
|
|
147
147
|
{
|
|
148
148
|
options: branchOptions,
|
|
@@ -173,7 +173,7 @@ export function CodeModeToggle({
|
|
|
173
173
|
{/* Repo/branch pickers — inline, both always visible */}
|
|
174
174
|
{enabled && (
|
|
175
175
|
<>
|
|
176
|
-
<div className="w-full sm:w-auto sm:min-w-[
|
|
176
|
+
<div className="w-full sm:w-auto sm:min-w-[240px] sm:max-w-[240px]">
|
|
177
177
|
<Combobox
|
|
178
178
|
options={repoOptions}
|
|
179
179
|
value={repo}
|
|
@@ -183,7 +183,7 @@ export function CodeModeToggle({
|
|
|
183
183
|
highlight={!repo && !loadingRepos}
|
|
184
184
|
/>
|
|
185
185
|
</div>
|
|
186
|
-
<div className={cn("w-full sm:w-auto sm:min-w-[
|
|
186
|
+
<div className={cn("w-full sm:w-auto sm:min-w-[200px] sm:max-w-[200px]", !repo && "opacity-50 pointer-events-none")}>
|
|
187
187
|
<Combobox
|
|
188
188
|
options={branchOptions}
|
|
189
189
|
value={branch}
|
|
@@ -280,7 +280,9 @@ function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
|
|
|
280
280
|
text ? /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap break-words", children: text }) : null
|
|
281
281
|
] }) : /* @__PURE__ */ jsx(Fragment, { children: message.parts?.length > 0 ? message.parts.map((part, i) => {
|
|
282
282
|
if (part.type === "text") {
|
|
283
|
-
|
|
283
|
+
const prevPart = message.parts[i - 1];
|
|
284
|
+
const afterTool = prevPart?.type?.startsWith("tool-");
|
|
285
|
+
return /* @__PURE__ */ jsx(Streamdown, { className: afterTool ? "mt-3" : void 0, mode: isLoading ? "streaming" : "static", linkSafety, children: part.text }, i);
|
|
284
286
|
}
|
|
285
287
|
if (part.type === "file") {
|
|
286
288
|
if (part.mediaType?.startsWith("image/")) {
|
|
@@ -331,7 +331,9 @@ export function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
|
|
|
331
331
|
{message.parts?.length > 0 ? (
|
|
332
332
|
message.parts.map((part, i) => {
|
|
333
333
|
if (part.type === 'text') {
|
|
334
|
-
|
|
334
|
+
const prevPart = message.parts[i - 1];
|
|
335
|
+
const afterTool = prevPart?.type?.startsWith('tool-');
|
|
336
|
+
return <Streamdown key={i} className={afterTool ? 'mt-3' : undefined} mode={isLoading ? 'streaming' : 'static'} linkSafety={linkSafety}>{part.text}</Streamdown>;
|
|
335
337
|
}
|
|
336
338
|
if (part.type === 'file') {
|
|
337
339
|
if (part.mediaType?.startsWith('image/')) {
|
package/lib/code/actions.js
CHANGED
|
@@ -322,7 +322,7 @@ export async function closeInteractiveMode(id, isClean) {
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
try {
|
|
325
|
-
const { execInContainer, removeContainer
|
|
325
|
+
const { execInContainer, removeContainer } = await import('../tools/docker.js');
|
|
326
326
|
|
|
327
327
|
// Only fetch git data and inject context when the session is clean
|
|
328
328
|
let commits = '';
|
|
@@ -344,9 +344,8 @@ export async function closeInteractiveMode(id, isClean) {
|
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
// Destroy container
|
|
347
|
+
// Destroy container (volume preserved for reuse)
|
|
348
348
|
await removeContainer(workspace.containerName);
|
|
349
|
-
await removeCodeWorkspaceVolume(id);
|
|
350
349
|
clearWorkspaceSessions(id);
|
|
351
350
|
updateContainerName(id, null);
|
|
352
351
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thepopebot",
|
|
3
|
-
"version": "1.2.74-beta.
|
|
3
|
+
"version": "1.2.74-beta.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
|
|
6
6
|
"bin": {
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"lucide-react": "^0.400.0",
|
|
91
91
|
"node-cron": "^3.0.3",
|
|
92
92
|
"open": "^10.0.0",
|
|
93
|
+
"split2": "^4.2.0",
|
|
93
94
|
"streamdown": "^2.2.0",
|
|
94
95
|
"tailwind-merge": "^3.0.0",
|
|
95
96
|
"uuid": "^9.0.0",
|