thepopebot 1.2.75-beta.10 → 1.2.75-beta.12
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 +1 -1
- package/bin/CLAUDE.md +1 -1
- package/bin/cli.js +2 -2
- package/bin/managed-paths.js +2 -5
- package/config/CLAUDE.md +1 -29
- package/lib/CLAUDE.md +3 -3
- package/lib/ai/CLAUDE.md +24 -3
- package/lib/ai/agent.js +8 -5
- package/lib/ai/headless-stream.js +3 -0
- package/lib/ai/index.js +8 -33
- package/lib/ai/line-mappers.js +72 -9
- package/lib/ai/tools.js +12 -16
- package/lib/chat/actions.js +6 -5
- package/lib/chat/components/crons-page.js +1 -1
- package/lib/chat/components/crons-page.jsx +1 -1
- package/lib/chat/components/message.js +3 -2
- package/lib/chat/components/message.jsx +3 -2
- package/lib/chat/components/triggers-page.js +1 -1
- package/lib/chat/components/triggers-page.jsx +1 -1
- package/lib/cluster/actions.js +4 -4
- package/lib/cluster/execute.js +3 -1
- package/lib/cron.js +3 -3
- package/lib/db/index.js +3 -1
- package/lib/paths.js +1 -38
- package/lib/tools/docker.js +3 -1
- package/lib/triggers.js +4 -3
- package/lib/utils/render-md.js +3 -1
- package/package.json +1 -1
- package/templates/CLAUDE.md +3 -2
- package/templates/agent-job/CLAUDE.md.template +34 -0
- package/templates/{config → agent-job}/CRONS.json +1 -1
- package/templates/docker-compose.custom.yml +3 -5
- package/templates/docker-compose.yml +3 -5
- package/templates/CLAUDE.md.template +0 -367
- package/templates/README.md +0 -75
- package/templates/config/CLAUDE.md.template +0 -40
- package/templates/cron/CLAUDE.md.template +0 -24
- package/templates/docs/CLAUDE.md.template +0 -12
- package/templates/docs/CLI.md +0 -59
- package/templates/docs/CLUSTERS.md +0 -151
- package/templates/docs/CONFIGURATION.md +0 -147
- package/templates/docs/CRONS_AND_TRIGGERS.md +0 -132
- package/templates/docs/GETTING_STARTED.md +0 -64
- package/templates/docs/SECURITY.md +0 -61
- package/templates/docs/SKILLS.md +0 -114
- package/templates/docs/UPGRADING.md +0 -92
- package/templates/triggers/.gitkeep +0 -0
- package/templates/triggers/CLAUDE.md.template +0 -41
- /package/templates/{config → agent-job}/HEARTBEAT.md +0 -0
- /package/templates/{config/agent-job → agent-job}/SOUL.md +0 -0
- /package/templates/{config/agent-job/AGENT_JOB.md → agent-job/SYSTEM.md} +0 -0
- /package/templates/{cron/.gitkeep → event-handler/CLAUDE.md.template} +0 -0
- /package/templates/{config/agent-job → event-handler}/SUMMARY.md +0 -0
- /package/templates/{config → event-handler}/TRIGGERS.json +0 -0
- /package/templates/{config → event-handler}/agent-chat/SYSTEM.md +0 -0
- /package/templates/{config/cluster → event-handler/clusters}/ROLE.md +0 -0
- /package/templates/{config/cluster → event-handler/clusters}/SYSTEM.md +0 -0
- /package/templates/{config → event-handler}/code-chat/SYSTEM.md +0 -0
- /package/templates/{config → event-handler}/litellm/main.yaml +0 -0
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ The wizard walks you through everything:
|
|
|
106
106
|
- **Web Chat**: Visit your APP_URL to chat with your agent, create jobs, upload files
|
|
107
107
|
- **Telegram** (optional): Run `npm run setup-telegram` to connect a Telegram bot
|
|
108
108
|
- **Webhook**: Send a POST to `/api/create-agent-job` with your API key to create jobs programmatically
|
|
109
|
-
- **Cron**: Edit `
|
|
109
|
+
- **Cron**: Edit `agent-job/CRONS.json` to schedule recurring jobs
|
|
110
110
|
|
|
111
111
|
### Chat vs Agent LLM
|
|
112
112
|
|
package/bin/CLAUDE.md
CHANGED
|
@@ -21,7 +21,7 @@ Entry point: `cli.js` (invoked via `npx thepopebot <command>`).
|
|
|
21
21
|
|
|
22
22
|
`managed-paths.js` defines files auto-synced by `init`. These are overwritten on every init/upgrade — users should not edit them.
|
|
23
23
|
|
|
24
|
-
**Managed paths**: `.github/workflows/`, `docker-compose.yml`, `.dockerignore`, `.gitignore`, `CLAUDE.md`, `
|
|
24
|
+
**Managed paths**: `.github/workflows/`, `docker-compose.yml`, `.dockerignore`, `.gitignore`, `agent-job/CLAUDE.md`, `event-handler/CLAUDE.md`, `skills/CLAUDE.md`.
|
|
25
25
|
|
|
26
26
|
`isManaged(relPath)` — returns true if a path is managed (exact match or directory prefix).
|
|
27
27
|
|
package/bin/cli.js
CHANGED
|
@@ -385,7 +385,7 @@ function reset(filePath) {
|
|
|
385
385
|
console.log(` ${destPath(file)}`);
|
|
386
386
|
}
|
|
387
387
|
console.log('\nUsage: thepopebot reset <file>');
|
|
388
|
-
console.log('Example: thepopebot reset
|
|
388
|
+
console.log('Example: thepopebot reset agent-job/SOUL.md\n');
|
|
389
389
|
return;
|
|
390
390
|
}
|
|
391
391
|
|
|
@@ -442,7 +442,7 @@ function diff(filePath) {
|
|
|
442
442
|
console.log(' All files match package templates.');
|
|
443
443
|
}
|
|
444
444
|
console.log('\nUsage: thepopebot diff <file>');
|
|
445
|
-
console.log('Example: thepopebot diff
|
|
445
|
+
console.log('Example: thepopebot diff agent-job/SOUL.md\n');
|
|
446
446
|
return;
|
|
447
447
|
}
|
|
448
448
|
|
package/bin/managed-paths.js
CHANGED
|
@@ -8,12 +8,9 @@ export const MANAGED_PATHS = [
|
|
|
8
8
|
'docker-compose.yml',
|
|
9
9
|
'.dockerignore',
|
|
10
10
|
'.gitignore',
|
|
11
|
-
'CLAUDE.md',
|
|
12
|
-
'
|
|
11
|
+
'agent-job/CLAUDE.md',
|
|
12
|
+
'event-handler/CLAUDE.md',
|
|
13
13
|
'skills/CLAUDE.md',
|
|
14
|
-
'cron/CLAUDE.md',
|
|
15
|
-
'triggers/CLAUDE.md',
|
|
16
|
-
'docs/',
|
|
17
14
|
];
|
|
18
15
|
|
|
19
16
|
export function isManaged(relPath) {
|
package/config/CLAUDE.md
CHANGED
|
@@ -1,32 +1,4 @@
|
|
|
1
|
-
# config/ —
|
|
2
|
-
|
|
3
|
-
## Directory Structure
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
config/
|
|
7
|
-
├── agent-chat/
|
|
8
|
-
│ └── SYSTEM.md # Agent chat system prompt (supports {{skills}}, {{datetime}})
|
|
9
|
-
├── code-chat/
|
|
10
|
-
│ └── SYSTEM.md # Code workspace system prompt
|
|
11
|
-
├── agent-job/
|
|
12
|
-
│ ├── SOUL.md # Agent personality/identity (used by Docker agent)
|
|
13
|
-
│ ├── AGENT_JOB.md # Agent runtime environment docs (used by Docker agent)
|
|
14
|
-
│ └── SUMMARY.md # Prompt for summarizing completed jobs
|
|
15
|
-
├── cluster/
|
|
16
|
-
│ ├── SYSTEM.md # Cluster worker system prompt
|
|
17
|
-
│ └── ROLE.md # Per-role prompt template for cluster workers
|
|
18
|
-
├── HEARTBEAT.md # Self-monitoring behavior (cron task prompt)
|
|
19
|
-
├── CRONS.json # Scheduled job definitions
|
|
20
|
-
└── TRIGGERS.json # Webhook trigger definitions
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Markdown File Includes
|
|
24
|
-
|
|
25
|
-
Markdown files in `config/` support includes and built-in variables, powered by `lib/utils/render-md.js`.
|
|
26
|
-
|
|
27
|
-
- **File includes**: `{{ filepath.md }}` — resolves relative to project root, recursive with circular detection. Missing files are left as-is.
|
|
28
|
-
- **`{{datetime}}`** — Current ISO timestamp.
|
|
29
|
-
- **`{{skills}}`** — Dynamic bullet list of active skill descriptions from `skills/active/*/SKILL.md` frontmatter. Never hardcode skill names — this is resolved at runtime.
|
|
1
|
+
# config/ — Next.js Config Wrapper
|
|
30
2
|
|
|
31
3
|
## Next.js Config Wrapper (index.js)
|
|
32
4
|
|
package/lib/CLAUDE.md
CHANGED
|
@@ -12,16 +12,16 @@ If the task needs to *think*, use `agent`. If it just needs to *do*, use `comman
|
|
|
12
12
|
|
|
13
13
|
**Agent**: Creates a Docker Agent job via `createAgentJob()`. Pushes an `agent-job/*` branch and launches a local Docker container. The `job` string is the LLM task prompt. Agent backend selected via `agent_backend` in `agent-job.config.json`.
|
|
14
14
|
|
|
15
|
-
**Command**: Runs a shell command on the event handler. Working directory:
|
|
15
|
+
**Command**: Runs a shell command on the event handler. Working directory: project root.
|
|
16
16
|
|
|
17
17
|
**Webhook**: Makes an HTTP request. `GET` skips the body; `POST` (default) sends `{ ...vars }` or `{ ...vars, data: <payload> }`.
|
|
18
18
|
|
|
19
19
|
## Cron Jobs
|
|
20
20
|
|
|
21
|
-
Defined in `
|
|
21
|
+
Defined in `agent-job/CRONS.json`, loaded by `lib/cron.js` at startup via `node-cron`. Each entry has `name`, `schedule` (cron expression), `type` (`agent`/`command`/`webhook`), and the corresponding action fields (`job`, `command`, or `url`/`method`/`headers`/`vars`). Set `enabled: false` to disable. Agent-type entries support optional `llm_provider` and `llm_model` fields to override the default LLM (passed to Docker agent via `agent-job.config.json`).
|
|
22
22
|
|
|
23
23
|
## Webhook Triggers
|
|
24
24
|
|
|
25
|
-
Defined in `
|
|
25
|
+
Defined in `event-handler/TRIGGERS.json`, loaded by `lib/triggers.js`. Each trigger watches an endpoint path (`watch_path`) and fires an array of actions (fire-and-forget, after auth, before route handler). Actions use the same `type`/`job`/`command`/`url` fields as cron jobs, including optional `llm_provider`/`llm_model` overrides.
|
|
26
26
|
|
|
27
27
|
Template tokens in `job` and `command` strings: `{{body}}`, `{{body.field}}`, `{{query}}`, `{{query.field}}`, `{{headers}}`, `{{headers.field}}`.
|
package/lib/ai/CLAUDE.md
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
Two agent singletons, both using `createReactAgent` from `@langchain/langgraph/prebuilt` with `SqliteSaver` for conversation memory:
|
|
6
6
|
|
|
7
7
|
**Agent Chat** — singleton via `getAgentChat()`:
|
|
8
|
-
- System prompt: `
|
|
8
|
+
- System prompt: `event-handler/agent-chat/SYSTEM.md` (rendered fresh each invocation via `render_md()`)
|
|
9
9
|
- Tools: `agent_job`, `coding_agent`
|
|
10
10
|
- Call `resetAgentChats()` to clear both singletons (required if hot-reloading)
|
|
11
11
|
|
|
12
12
|
**Code Chat** — singleton via `getCodeChat()`:
|
|
13
|
-
- System prompt: `
|
|
13
|
+
- System prompt: `event-handler/code-chat/SYSTEM.md` (rendered fresh each invocation)
|
|
14
14
|
- Tools: `coding_agent` (reads repo/branch/workspace from `runtime.configurable`)
|
|
15
15
|
|
|
16
16
|
## Adding a New Tool
|
|
@@ -73,7 +73,28 @@ Three-layer parser for Claude Code agents running in headless Docker containers:
|
|
|
73
73
|
3. **Event mapper** (`mapLine()`) — Converts each line to chat events:
|
|
74
74
|
- `assistant` messages: `text` blocks → `{ type: 'text' }`, `tool_use` blocks → `{ type: 'tool-call' }`
|
|
75
75
|
- `user` messages: `tool_result` blocks → `{ type: 'tool-result' }` (priority: stdout > string content > array)
|
|
76
|
-
- `result` messages: → `{ type: 'text'
|
|
76
|
+
- `result` messages: → `{ type: 'text' }` (final summary from the agent)
|
|
77
77
|
- Non-JSON lines (e.g. `NO_CHANGES`, `AGENT_FAILED`): wrapped as plain text events
|
|
78
78
|
|
|
79
79
|
`parseHeadlessStream(dockerLogStream)` is an async generator consuming `http.IncomingMessage`. `mapLine()` is also reused by `lib/cluster/stream.js` for worker log parsing.
|
|
80
|
+
|
|
81
|
+
### Tool Return Format
|
|
82
|
+
|
|
83
|
+
The `coding_agent` tool (in `tools.js`) returns the **full container session** as a flat JSON array. This becomes the ToolMessage in LangGraph's checkpoint, giving the LLM complete context on the current turn. The array contains:
|
|
84
|
+
|
|
85
|
+
- `{ type: 'meta', codingAgent, backendApi }` — first event, agent identity
|
|
86
|
+
- `{ type: 'text', text }` — agent text output
|
|
87
|
+
- `{ type: 'tool-call', toolCallId, toolName, args }` — agent tool invocations
|
|
88
|
+
- `{ type: 'tool-result', toolCallId, result }` — tool execution results
|
|
89
|
+
- `{ type: 'exit', exitCode }` — last event, container exit status
|
|
90
|
+
|
|
91
|
+
On error before streaming starts: `[{ type: 'error', message }]`.
|
|
92
|
+
|
|
93
|
+
### Adding a New Agent Mapper (line-mappers.js)
|
|
94
|
+
|
|
95
|
+
Each coding agent CLI has its own mapper function (`mapClaudeCodeLine`, `mapPiLine`, `mapGeminiLine`, `mapCodexLine`, `mapOpenCodeLine`, `mapKimiLine`). When adding a new agent:
|
|
96
|
+
|
|
97
|
+
1. Create `mapXxxLine(parsed)` in `line-mappers.js` that returns an array of `{ type, ... }` events
|
|
98
|
+
2. Register it in `headless-stream.js`: add to imports, re-exports, and the `mapperMap` object
|
|
99
|
+
3. Map the agent's JSON output to three event types: `{ type: 'text', text }`, `{ type: 'tool-call', toolCallId, toolName, args }`, `{ type: 'tool-result', toolCallId, result }`
|
|
100
|
+
4. Return `[{ type: 'skip' }]` for noise events (session init, rate limits, etc.) to suppress them without triggering the unknown fallback
|
package/lib/ai/agent.js
CHANGED
|
@@ -3,7 +3,8 @@ import { SystemMessage } from '@langchain/core/messages';
|
|
|
3
3
|
import { createModel } from './model.js';
|
|
4
4
|
import { agentJobTool, agentChatCodingTool, codeChatCodingTool } from './tools.js';
|
|
5
5
|
import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
|
|
6
|
-
import
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { PROJECT_ROOT } from '../paths.js';
|
|
7
8
|
import { render_md } from '../utils/render-md.js';
|
|
8
9
|
|
|
9
10
|
// Singletons on globalThis to survive Next.js webpack chunk duplication.
|
|
@@ -20,13 +21,14 @@ export async function getAgentChat() {
|
|
|
20
21
|
const model = await createModel();
|
|
21
22
|
const tools = [agentJobTool, agentChatCodingTool];
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
+
const dbPath = process.env.DATABASE_PATH || path.join(PROJECT_ROOT, 'data/db/thepopebot.sqlite');
|
|
25
|
+
const checkpointer = SqliteSaver.fromConnString(dbPath);
|
|
24
26
|
|
|
25
27
|
globalThis.__popebotAgentChat = createReactAgent({
|
|
26
28
|
llm: model,
|
|
27
29
|
tools,
|
|
28
30
|
checkpointSaver: checkpointer,
|
|
29
|
-
prompt: (state) => [new SystemMessage(render_md(
|
|
31
|
+
prompt: (state) => [new SystemMessage(render_md(path.join(PROJECT_ROOT, 'event-handler/agent-chat/SYSTEM.md'))), ...state.messages],
|
|
30
32
|
});
|
|
31
33
|
}
|
|
32
34
|
return globalThis.__popebotAgentChat;
|
|
@@ -41,13 +43,14 @@ export async function getCodeChat() {
|
|
|
41
43
|
const model = await createModel();
|
|
42
44
|
const tools = [codeChatCodingTool];
|
|
43
45
|
|
|
44
|
-
const
|
|
46
|
+
const dbPath = process.env.DATABASE_PATH || path.join(PROJECT_ROOT, 'data/db/thepopebot.sqlite');
|
|
47
|
+
const checkpointer = SqliteSaver.fromConnString(dbPath);
|
|
45
48
|
|
|
46
49
|
globalThis.__popebotCodeChat = createReactAgent({
|
|
47
50
|
llm: model,
|
|
48
51
|
tools,
|
|
49
52
|
checkpointSaver: checkpointer,
|
|
50
|
-
prompt: (state) => [new SystemMessage(render_md(
|
|
53
|
+
prompt: (state) => [new SystemMessage(render_md(path.join(PROJECT_ROOT, 'event-handler/code-chat/SYSTEM.md'))), ...state.messages],
|
|
51
54
|
});
|
|
52
55
|
}
|
|
53
56
|
return globalThis.__popebotCodeChat;
|
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
mapGeminiLine,
|
|
11
11
|
mapCodexLine,
|
|
12
12
|
mapOpenCodeLine,
|
|
13
|
+
mapKimiLine,
|
|
13
14
|
} from './line-mappers.js';
|
|
14
15
|
|
|
15
16
|
import {
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
mapGeminiLine,
|
|
20
21
|
mapCodexLine,
|
|
21
22
|
mapOpenCodeLine,
|
|
23
|
+
mapKimiLine,
|
|
22
24
|
} from './line-mappers.js';
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -41,6 +43,7 @@ export async function* parseHeadlessStream(dockerLogStream, codingAgent = 'claud
|
|
|
41
43
|
'gemini-cli': mapGeminiLine,
|
|
42
44
|
'codex-cli': mapCodexLine,
|
|
43
45
|
'opencode': mapOpenCodeLine,
|
|
46
|
+
'kimi-cli': mapKimiLine,
|
|
44
47
|
};
|
|
45
48
|
const mapper = mapperMap[codingAgent] || mapClaudeCodeLine;
|
|
46
49
|
|
package/lib/ai/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { HumanMessage
|
|
1
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
2
2
|
import { createChannel, mergeAsyncIterables } from './async-channel.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { getAgentChat, getCodeChat } from './agent.js';
|
|
5
5
|
import { createModel } from './model.js';
|
|
6
|
-
import
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { PROJECT_ROOT } from '../paths.js';
|
|
7
8
|
import { render_md } from '../utils/render-md.js';
|
|
8
9
|
import { getChatById, createChat, saveMessage, updateChatTitle, linkChatToWorkspace } from '../db/chats.js';
|
|
9
10
|
|
|
@@ -192,7 +193,6 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
192
193
|
{ configurable: { thread_id: threadId, workspaceId, repo, branch, codeModeType, streamCallback }, streamMode: 'messages' }
|
|
193
194
|
);
|
|
194
195
|
|
|
195
|
-
let fullText = '';
|
|
196
196
|
const toolCallNames = {};
|
|
197
197
|
const pendingToolCalls = new Map();
|
|
198
198
|
|
|
@@ -205,11 +205,9 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
205
205
|
const toolCallArgsEmitted = new Set(); // tool_call_ids whose complete args have been yielded
|
|
206
206
|
|
|
207
207
|
// Headless container streaming state
|
|
208
|
-
const memoryParts = [];
|
|
209
208
|
const headlessPendingToolCalls = new Map();
|
|
210
209
|
let pendingText = ''; // channel text, flushed to DB at tool boundaries
|
|
211
210
|
let llmTextAccum = ''; // langgraph text (direct response or LLM follow-up after container)
|
|
212
|
-
let resultSummary = '';
|
|
213
211
|
|
|
214
212
|
// Tag helper so mergeAsyncIterables can tell the two sources apart.
|
|
215
213
|
// The LangGraph wrapper also closes sideChannel when the agent stream
|
|
@@ -284,7 +282,6 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
284
282
|
}
|
|
285
283
|
|
|
286
284
|
if (text) {
|
|
287
|
-
fullText += text;
|
|
288
285
|
llmTextAccum += text;
|
|
289
286
|
yield { type: 'text', text };
|
|
290
287
|
}
|
|
@@ -321,20 +318,9 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
321
318
|
|
|
322
319
|
} else {
|
|
323
320
|
// ── Side channel: headless container chunks ───────────────────────
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// Result summary: skip if duplicate, otherwise ensure it starts on a new line
|
|
327
|
-
if (chunk._resultSummary && chunk.type === 'text') {
|
|
328
|
-
resultSummary = chunk._resultSummary;
|
|
329
|
-
if (pendingText.trim() && chunk.text.trim() === pendingText.trim()) {
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
chunk = { ...chunk, text: '\n\n' + chunk.text };
|
|
333
|
-
}
|
|
321
|
+
const chunk = item;
|
|
334
322
|
|
|
335
323
|
if (chunk.type === 'text') {
|
|
336
|
-
fullText += chunk.text;
|
|
337
|
-
memoryParts.push(chunk.text);
|
|
338
324
|
pendingText += chunk.text;
|
|
339
325
|
yield chunk;
|
|
340
326
|
} else if (chunk.type === 'tool-call') {
|
|
@@ -343,7 +329,6 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
343
329
|
persistMessage(threadId, 'assistant', pendingText, options);
|
|
344
330
|
pendingText = '';
|
|
345
331
|
}
|
|
346
|
-
memoryParts.push('[tool-call] ' + chunk.toolName + ': ' + JSON.stringify(chunk.args));
|
|
347
332
|
headlessPendingToolCalls.set(chunk.toolCallId, { toolName: chunk.toolName, args: chunk.args });
|
|
348
333
|
yield chunk;
|
|
349
334
|
} else if (chunk.type === 'tool-result') {
|
|
@@ -351,7 +336,6 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
351
336
|
const htc = headlessPendingToolCalls.get(chunk.toolCallId);
|
|
352
337
|
const enriched = htc ? { ...chunk, args: htc.args, toolName: htc.toolName } : chunk;
|
|
353
338
|
yield enriched;
|
|
354
|
-
memoryParts.push('[tool-result] ' + chunk.result);
|
|
355
339
|
if (htc) {
|
|
356
340
|
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
357
341
|
type: 'tool-invocation',
|
|
@@ -364,11 +348,9 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
364
348
|
headlessPendingToolCalls.delete(chunk.toolCallId);
|
|
365
349
|
}
|
|
366
350
|
} else {
|
|
367
|
-
// unknown events pass through unchanged
|
|
351
|
+
// unknown/meta events pass through unchanged
|
|
368
352
|
yield chunk;
|
|
369
353
|
}
|
|
370
|
-
|
|
371
|
-
if (chunk._resultSummary) resultSummary = chunk._resultSummary;
|
|
372
354
|
}
|
|
373
355
|
}
|
|
374
356
|
} finally {
|
|
@@ -386,14 +368,6 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
386
368
|
persistMessage(threadId, 'assistant', llmTextAccum, options);
|
|
387
369
|
}
|
|
388
370
|
|
|
389
|
-
// Inject full headless conversation detail into LangGraph memory for follow-up turns
|
|
390
|
-
if (memoryParts.length > 0) {
|
|
391
|
-
await agent.updateState(
|
|
392
|
-
{ configurable: { thread_id: threadId } },
|
|
393
|
-
{ messages: [new AIMessage(memoryParts.join('\n'))] }
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
371
|
} catch (err) {
|
|
398
372
|
console.error('[chatStream] error:', err);
|
|
399
373
|
throw err;
|
|
@@ -435,10 +409,11 @@ async function autoTitle(threadId, firstMessage) {
|
|
|
435
409
|
async function summarizeAgentJob(results) {
|
|
436
410
|
try {
|
|
437
411
|
const model = await createModel({ maxTokens: 1024 });
|
|
438
|
-
const
|
|
412
|
+
const summaryMdPath = path.join(PROJECT_ROOT, 'event-handler/SUMMARY.md');
|
|
413
|
+
const systemPrompt = render_md(summaryMdPath);
|
|
439
414
|
|
|
440
415
|
if (!systemPrompt) {
|
|
441
|
-
console.error(`[summarizeAgentJob] Empty system prompt —
|
|
416
|
+
console.error(`[summarizeAgentJob] Empty system prompt — event-handler/SUMMARY.md not found or empty at: ${summaryMdPath}`);
|
|
442
417
|
}
|
|
443
418
|
|
|
444
419
|
const userMessage = [
|
package/lib/ai/line-mappers.js
CHANGED
|
@@ -107,7 +107,7 @@ export function mapClaudeCodeLine(parsed) {
|
|
|
107
107
|
// User messages without tool_result (e.g. subagent prompts) — skip
|
|
108
108
|
if (events.length === 0) return [{ type: 'skip' }];
|
|
109
109
|
} else if (type === 'result' && result) {
|
|
110
|
-
events.push({ type: 'text', text: result
|
|
110
|
+
events.push({ type: 'text', text: result });
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
return events;
|
|
@@ -176,7 +176,7 @@ export function mapPiLine(parsed) {
|
|
|
176
176
|
.map(b => b.text)
|
|
177
177
|
.join('');
|
|
178
178
|
if (text) {
|
|
179
|
-
events.push({ type: 'text', text
|
|
179
|
+
events.push({ type: 'text', text });
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
}
|
|
@@ -233,7 +233,7 @@ export function mapGeminiLine(parsed) {
|
|
|
233
233
|
const stats = parsed.stats;
|
|
234
234
|
if (stats) {
|
|
235
235
|
const summary = `Completed (${stats.total_tokens || 0} tokens, ${stats.tool_calls || 0} tool calls, ${((stats.duration_ms || 0) / 1000).toFixed(1)}s)`;
|
|
236
|
-
events.push({ type: 'text', text: summary
|
|
236
|
+
events.push({ type: 'text', text: summary });
|
|
237
237
|
}
|
|
238
238
|
return events.length ? events : [{ type: 'skip' }];
|
|
239
239
|
} else if (type === 'error') {
|
|
@@ -245,6 +245,74 @@ export function mapGeminiLine(parsed) {
|
|
|
245
245
|
return events;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
// Kimi CLI: OpenAI-style function calling JSON
|
|
250
|
+
//
|
|
251
|
+
// Event shapes (from real output):
|
|
252
|
+
// role: "assistant", content: [], tool_calls: [{ type: "function", id, function: { name, arguments } }]
|
|
253
|
+
// role: "assistant", content: "text..." (with or without tool_calls)
|
|
254
|
+
// role: "tool", content: "...", tool_call_id: "..."
|
|
255
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Map a Kimi CLI line to chat events.
|
|
259
|
+
* @param {object} parsed - Parsed JSON object
|
|
260
|
+
* @returns {Array<object>}
|
|
261
|
+
*/
|
|
262
|
+
export function mapKimiLine(parsed) {
|
|
263
|
+
const events = [];
|
|
264
|
+
const { role, content, tool_calls, tool_call_id } = parsed;
|
|
265
|
+
|
|
266
|
+
if (role === 'assistant') {
|
|
267
|
+
// Text content — can be a string or an array of blocks
|
|
268
|
+
if (typeof content === 'string' && content) {
|
|
269
|
+
events.push({ type: 'text', text: content });
|
|
270
|
+
} else if (Array.isArray(content)) {
|
|
271
|
+
for (const block of content) {
|
|
272
|
+
if (typeof block === 'string' && block) {
|
|
273
|
+
events.push({ type: 'text', text: block });
|
|
274
|
+
} else if (block?.type === 'text' && block.text) {
|
|
275
|
+
events.push({ type: 'text', text: block.text });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Tool calls — OpenAI function calling format
|
|
281
|
+
if (Array.isArray(tool_calls)) {
|
|
282
|
+
for (const tc of tool_calls) {
|
|
283
|
+
if (tc.type === 'function' && tc.function) {
|
|
284
|
+
let args = {};
|
|
285
|
+
try {
|
|
286
|
+
args = typeof tc.function.arguments === 'string'
|
|
287
|
+
? JSON.parse(tc.function.arguments)
|
|
288
|
+
: tc.function.arguments || {};
|
|
289
|
+
} catch { /* leave as empty object */ }
|
|
290
|
+
events.push({
|
|
291
|
+
type: 'tool-call',
|
|
292
|
+
toolCallId: tc.id || '',
|
|
293
|
+
toolName: tc.function.name || '',
|
|
294
|
+
args,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Assistant message with only empty content and no tool calls — skip
|
|
301
|
+
if (events.length === 0) return [{ type: 'skip' }];
|
|
302
|
+
} else if (role === 'tool') {
|
|
303
|
+
const resultText = typeof content === 'string' ? content :
|
|
304
|
+
Array.isArray(content) ? content.map(b => (typeof b === 'string' ? b : b?.text || '')).join('') :
|
|
305
|
+
JSON.stringify(content);
|
|
306
|
+
events.push({
|
|
307
|
+
type: 'tool-result',
|
|
308
|
+
toolCallId: tool_call_id || '',
|
|
309
|
+
result: resultText,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return events;
|
|
314
|
+
}
|
|
315
|
+
|
|
248
316
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
317
|
// Codex CLI: --json
|
|
250
318
|
//
|
|
@@ -312,7 +380,7 @@ export function mapCodexLine(parsed) {
|
|
|
312
380
|
const usage = parsed.usage;
|
|
313
381
|
if (usage) {
|
|
314
382
|
const summary = `Completed (${usage.input_tokens || 0} input, ${usage.output_tokens || 0} output tokens)`;
|
|
315
|
-
events.push({ type: 'text', text: summary
|
|
383
|
+
events.push({ type: 'text', text: summary });
|
|
316
384
|
}
|
|
317
385
|
return events.length ? events : [{ type: 'skip' }];
|
|
318
386
|
} else if (type === 'turn.failed') {
|
|
@@ -350,11 +418,6 @@ export function mapOpenCodeLine(parsed) {
|
|
|
350
418
|
// Text output — part.text contains the assistant's response
|
|
351
419
|
if (type === 'text' && part?.text) {
|
|
352
420
|
events.push({ type: 'text', text: part.text });
|
|
353
|
-
// If step_finish follows with reason "stop", this is the final answer
|
|
354
|
-
// We mark it as result summary so it gets captured in LangGraph memory
|
|
355
|
-
if (part.text.length > 50) {
|
|
356
|
-
events[events.length - 1]._resultSummary = part.text;
|
|
357
|
-
}
|
|
358
421
|
}
|
|
359
422
|
|
|
360
423
|
// Tool use — OpenCode emits a single event with completed state (input + output)
|
package/lib/ai/tools.js
CHANGED
|
@@ -55,7 +55,7 @@ const agentChatCodingTool = tool(
|
|
|
55
55
|
const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
|
|
56
56
|
|
|
57
57
|
const { runHeadlessContainer, tailContainerLogs, waitForContainer, removeContainer } = await import('../tools/docker.js');
|
|
58
|
-
await runHeadlessContainer({
|
|
58
|
+
const { backendApi } = await runHeadlessContainer({
|
|
59
59
|
containerName,
|
|
60
60
|
repo,
|
|
61
61
|
branch: 'main',
|
|
@@ -66,14 +66,14 @@ const agentChatCodingTool = tool(
|
|
|
66
66
|
injectSecrets: true,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
const chunks = [{ type: 'meta', codingAgent, backendApi }];
|
|
69
70
|
const streamCallback = runtime.configurable.streamCallback;
|
|
70
71
|
const { parseHeadlessStream } = await import('./headless-stream.js');
|
|
71
72
|
|
|
72
73
|
const logStream = await tailContainerLogs(containerName);
|
|
73
|
-
let resultSummary = '';
|
|
74
74
|
|
|
75
75
|
for await (const chunk of parseHeadlessStream(logStream, codingAgent)) {
|
|
76
|
-
|
|
76
|
+
chunks.push(chunk);
|
|
77
77
|
streamCallback?.(chunk);
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -81,14 +81,12 @@ const agentChatCodingTool = tool(
|
|
|
81
81
|
await removeContainer(containerName);
|
|
82
82
|
streamCallback?.(null);
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
return resultSummary || 'Task completed successfully.';
|
|
84
|
+
chunks.push({ type: 'exit', exitCode });
|
|
85
|
+
return JSON.stringify(chunks);
|
|
88
86
|
} catch (err) {
|
|
89
87
|
console.error('[coding_agent] Failed:', err);
|
|
90
88
|
runtime.configurable.streamCallback?.(null);
|
|
91
|
-
return
|
|
89
|
+
return JSON.stringify([{ type: 'error', message: err.message }]);
|
|
92
90
|
}
|
|
93
91
|
},
|
|
94
92
|
{
|
|
@@ -122,20 +120,20 @@ const codeChatCodingTool = tool(
|
|
|
122
120
|
const codingAgent = getConfig('CODING_AGENT') || 'claude-code';
|
|
123
121
|
const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
|
|
124
122
|
|
|
125
|
-
await runHeadlessContainer({
|
|
123
|
+
const { backendApi } = await runHeadlessContainer({
|
|
126
124
|
containerName, repo, branch, featureBranch, workspaceId,
|
|
127
125
|
taskPrompt: prompt,
|
|
128
126
|
mode,
|
|
129
127
|
});
|
|
130
128
|
|
|
129
|
+
const chunks = [{ type: 'meta', codingAgent, backendApi }];
|
|
131
130
|
const streamCallback = runtime.configurable.streamCallback;
|
|
132
131
|
const { parseHeadlessStream } = await import('./headless-stream.js');
|
|
133
132
|
|
|
134
133
|
const logStream = await tailContainerLogs(containerName);
|
|
135
|
-
let resultSummary = '';
|
|
136
134
|
|
|
137
135
|
for await (const chunk of parseHeadlessStream(logStream, codingAgent)) {
|
|
138
|
-
|
|
136
|
+
chunks.push(chunk);
|
|
139
137
|
streamCallback?.(chunk);
|
|
140
138
|
}
|
|
141
139
|
|
|
@@ -143,14 +141,12 @@ const codeChatCodingTool = tool(
|
|
|
143
141
|
await removeContainer(containerName);
|
|
144
142
|
streamCallback?.(null);
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
return resultSummary || 'Task completed successfully.';
|
|
144
|
+
chunks.push({ type: 'exit', exitCode });
|
|
145
|
+
return JSON.stringify(chunks);
|
|
150
146
|
} catch (err) {
|
|
151
147
|
console.error('[coding_agent] Failed:', err);
|
|
152
148
|
runtime.configurable.streamCallback?.(null);
|
|
153
|
-
return
|
|
149
|
+
return JSON.stringify([{ type: 'error', message: err.message }]);
|
|
154
150
|
}
|
|
155
151
|
},
|
|
156
152
|
{
|
package/lib/chat/actions.js
CHANGED
|
@@ -556,12 +556,13 @@ export async function getRunnersStatus(page = 1) {
|
|
|
556
556
|
*/
|
|
557
557
|
export async function getRunnersConfig() {
|
|
558
558
|
await requireAuth();
|
|
559
|
-
const {
|
|
559
|
+
const { PROJECT_ROOT } = await import('../paths.js');
|
|
560
560
|
const fs = await import('fs');
|
|
561
|
+
const path = await import('path');
|
|
561
562
|
let crons = [];
|
|
562
563
|
let triggers = [];
|
|
563
|
-
try { crons = JSON.parse(fs.readFileSync(
|
|
564
|
-
try { triggers = JSON.parse(fs.readFileSync(
|
|
564
|
+
try { crons = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'agent-job/CRONS.json'), 'utf8')); } catch {}
|
|
565
|
+
try { triggers = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'event-handler/TRIGGERS.json'), 'utf8')); } catch {}
|
|
565
566
|
return { crons, triggers };
|
|
566
567
|
}
|
|
567
568
|
|
|
@@ -985,8 +986,8 @@ async function syncLitellmConfig() {
|
|
|
985
986
|
try {
|
|
986
987
|
const fs = await import('fs');
|
|
987
988
|
const path = await import('path');
|
|
988
|
-
const {
|
|
989
|
-
const litellmDir = path.default.join(
|
|
989
|
+
const { PROJECT_ROOT } = await import('../paths.js');
|
|
990
|
+
const litellmDir = path.default.join(PROJECT_ROOT, 'event-handler/litellm');
|
|
990
991
|
if (!fs.default.existsSync(litellmDir)) return;
|
|
991
992
|
|
|
992
993
|
const { getConfig } = await import('../config.js');
|
|
@@ -152,7 +152,7 @@ function CronsPage() {
|
|
|
152
152
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1", children: "No cron jobs configured" }),
|
|
153
153
|
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground max-w-sm", children: [
|
|
154
154
|
"Add scheduled jobs by editing ",
|
|
155
|
-
/* @__PURE__ */ jsx("span", { className: "font-mono", children: "
|
|
155
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono", children: "agent-job/CRONS.json" }),
|
|
156
156
|
" in your project."
|
|
157
157
|
] })
|
|
158
158
|
] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
@@ -216,7 +216,7 @@ export function CronsPage() {
|
|
|
216
216
|
</div>
|
|
217
217
|
<p className="text-sm font-medium mb-1">No cron jobs configured</p>
|
|
218
218
|
<p className="text-xs text-muted-foreground max-w-sm">
|
|
219
|
-
Add scheduled jobs by editing <span className="font-mono">
|
|
219
|
+
Add scheduled jobs by editing <span className="font-mono">agent-job/CRONS.json</span> in your project.
|
|
220
220
|
</p>
|
|
221
221
|
</div>
|
|
222
222
|
) : (
|
|
@@ -153,8 +153,9 @@ function ToolCall({ part, className }) {
|
|
|
153
153
|
isDone && (() => {
|
|
154
154
|
try {
|
|
155
155
|
const o = typeof part.output === "string" ? JSON.parse(part.output) : part.output;
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const meta = Array.isArray(o) ? o.find((e) => e.type === "meta") : o;
|
|
157
|
+
if (meta?.codingAgent || meta?.backendApi) {
|
|
158
|
+
return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: [meta.codingAgent, meta.backendApi].filter(Boolean).join(" \xB7 ") });
|
|
158
159
|
}
|
|
159
160
|
} catch {
|
|
160
161
|
}
|
|
@@ -157,10 +157,11 @@ function ToolCall({ part, className }) {
|
|
|
157
157
|
{isDone && (() => {
|
|
158
158
|
try {
|
|
159
159
|
const o = typeof part.output === 'string' ? JSON.parse(part.output) : part.output;
|
|
160
|
-
|
|
160
|
+
const meta = Array.isArray(o) ? o.find(e => e.type === 'meta') : o;
|
|
161
|
+
if (meta?.codingAgent || meta?.backendApi) {
|
|
161
162
|
return (
|
|
162
163
|
<span className="text-xs text-muted-foreground">
|
|
163
|
-
{[
|
|
164
|
+
{[meta.codingAgent, meta.backendApi].filter(Boolean).join(' · ')}
|
|
164
165
|
</span>
|
|
165
166
|
);
|
|
166
167
|
}
|
|
@@ -133,7 +133,7 @@ function TriggersPage() {
|
|
|
133
133
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1", children: "No triggers configured" }),
|
|
134
134
|
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground max-w-sm", children: [
|
|
135
135
|
"Add webhook triggers by editing ",
|
|
136
|
-
/* @__PURE__ */ jsx("span", { className: "font-mono", children: "
|
|
136
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono", children: "event-handler/TRIGGERS.json" }),
|
|
137
137
|
" in your project."
|
|
138
138
|
] })
|
|
139
139
|
] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
@@ -193,7 +193,7 @@ export function TriggersPage() {
|
|
|
193
193
|
</div>
|
|
194
194
|
<p className="text-sm font-medium mb-1">No triggers configured</p>
|
|
195
195
|
<p className="text-xs text-muted-foreground max-w-sm">
|
|
196
|
-
Add webhook triggers by editing <span className="font-mono">
|
|
196
|
+
Add webhook triggers by editing <span className="font-mono">event-handler/TRIGGERS.json</span> in your project.
|
|
197
197
|
</p>
|
|
198
198
|
</div>
|
|
199
199
|
) : (
|