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.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/bin/CLAUDE.md +1 -1
  3. package/bin/cli.js +2 -2
  4. package/bin/managed-paths.js +2 -5
  5. package/config/CLAUDE.md +1 -29
  6. package/lib/CLAUDE.md +3 -3
  7. package/lib/ai/CLAUDE.md +24 -3
  8. package/lib/ai/agent.js +8 -5
  9. package/lib/ai/headless-stream.js +3 -0
  10. package/lib/ai/index.js +8 -33
  11. package/lib/ai/line-mappers.js +72 -9
  12. package/lib/ai/tools.js +12 -16
  13. package/lib/chat/actions.js +6 -5
  14. package/lib/chat/components/crons-page.js +1 -1
  15. package/lib/chat/components/crons-page.jsx +1 -1
  16. package/lib/chat/components/message.js +3 -2
  17. package/lib/chat/components/message.jsx +3 -2
  18. package/lib/chat/components/triggers-page.js +1 -1
  19. package/lib/chat/components/triggers-page.jsx +1 -1
  20. package/lib/cluster/actions.js +4 -4
  21. package/lib/cluster/execute.js +3 -1
  22. package/lib/cron.js +3 -3
  23. package/lib/db/index.js +3 -1
  24. package/lib/paths.js +1 -38
  25. package/lib/tools/docker.js +3 -1
  26. package/lib/triggers.js +4 -3
  27. package/lib/utils/render-md.js +3 -1
  28. package/package.json +1 -1
  29. package/templates/CLAUDE.md +3 -2
  30. package/templates/agent-job/CLAUDE.md.template +34 -0
  31. package/templates/{config → agent-job}/CRONS.json +1 -1
  32. package/templates/docker-compose.custom.yml +3 -5
  33. package/templates/docker-compose.yml +3 -5
  34. package/templates/CLAUDE.md.template +0 -367
  35. package/templates/README.md +0 -75
  36. package/templates/config/CLAUDE.md.template +0 -40
  37. package/templates/cron/CLAUDE.md.template +0 -24
  38. package/templates/docs/CLAUDE.md.template +0 -12
  39. package/templates/docs/CLI.md +0 -59
  40. package/templates/docs/CLUSTERS.md +0 -151
  41. package/templates/docs/CONFIGURATION.md +0 -147
  42. package/templates/docs/CRONS_AND_TRIGGERS.md +0 -132
  43. package/templates/docs/GETTING_STARTED.md +0 -64
  44. package/templates/docs/SECURITY.md +0 -61
  45. package/templates/docs/SKILLS.md +0 -114
  46. package/templates/docs/UPGRADING.md +0 -92
  47. package/templates/triggers/.gitkeep +0 -0
  48. package/templates/triggers/CLAUDE.md.template +0 -41
  49. /package/templates/{config → agent-job}/HEARTBEAT.md +0 -0
  50. /package/templates/{config/agent-job → agent-job}/SOUL.md +0 -0
  51. /package/templates/{config/agent-job/AGENT_JOB.md → agent-job/SYSTEM.md} +0 -0
  52. /package/templates/{cron/.gitkeep → event-handler/CLAUDE.md.template} +0 -0
  53. /package/templates/{config/agent-job → event-handler}/SUMMARY.md +0 -0
  54. /package/templates/{config → event-handler}/TRIGGERS.json +0 -0
  55. /package/templates/{config → event-handler}/agent-chat/SYSTEM.md +0 -0
  56. /package/templates/{config/cluster → event-handler/clusters}/ROLE.md +0 -0
  57. /package/templates/{config/cluster → event-handler/clusters}/SYSTEM.md +0 -0
  58. /package/templates/{config → event-handler}/code-chat/SYSTEM.md +0 -0
  59. /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 `config/CRONS.json` to schedule recurring jobs
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`, `config/CLAUDE.md`, `skills/CLAUDE.md`, `cron/CLAUDE.md`, `triggers/CLAUDE.md`, `docs/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 config/SOUL.md\n');
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 config/SOUL.md\n');
445
+ console.log('Example: thepopebot diff agent-job/SOUL.md\n');
446
446
  return;
447
447
  }
448
448
 
@@ -8,12 +8,9 @@ export const MANAGED_PATHS = [
8
8
  'docker-compose.yml',
9
9
  '.dockerignore',
10
10
  '.gitignore',
11
- 'CLAUDE.md',
12
- 'config/CLAUDE.md',
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/ — Configuration Files
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: `cron/` for crons, `triggers/` for triggers.
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 `config/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`).
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 `config/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.
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: `config/agent-chat/SYSTEM.md` (rendered fresh each invocation via `render_md()`)
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: `config/code-chat/SYSTEM.md` (rendered fresh each invocation)
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', _resultSummary }` (injected into LangGraph memory)
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 { agentJobPlanningMd, codePlanningMd, thepopebotDb } from '../paths.js';
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 checkpointer = SqliteSaver.fromConnString(thepopebotDb);
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(agentJobPlanningMd)), ...state.messages],
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 checkpointer = SqliteSaver.fromConnString(thepopebotDb);
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(codePlanningMd)), ...state.messages],
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, AIMessage } from '@langchain/core/messages';
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 { agentJobSummaryMd } from '../paths.js';
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
- let chunk = item;
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 systemPrompt = render_md(agentJobSummaryMd);
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 — agent-job/SUMMARY.md not found or empty at: ${agentJobSummaryMd}`);
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 = [
@@ -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, _resultSummary: 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, _resultSummary: 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, _resultSummary: 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, _resultSummary: 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
- if (chunk._resultSummary) resultSummary = chunk._resultSummary;
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
- if (exitCode !== 0) {
85
- return `Task exited with errors (exit code ${exitCode}).`;
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 `Failed to run coding agent: ${err.message}`;
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
- if (chunk._resultSummary) resultSummary = chunk._resultSummary;
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
- if (exitCode !== 0) {
147
- return `Task exited with errors (exit code ${exitCode}).`;
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 `Failed to run coding agent: ${err.message}`;
149
+ return JSON.stringify([{ type: 'error', message: err.message }]);
154
150
  }
155
151
  },
156
152
  {
@@ -556,12 +556,13 @@ export async function getRunnersStatus(page = 1) {
556
556
  */
557
557
  export async function getRunnersConfig() {
558
558
  await requireAuth();
559
- const { cronsFile, triggersFile } = await import('../paths.js');
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(cronsFile, 'utf8')); } catch {}
564
- try { triggers = JSON.parse(fs.readFileSync(triggersFile, 'utf8')); } catch {}
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 { configDir } = await import('../paths.js');
989
- const litellmDir = path.default.join(configDir, 'litellm');
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: "config/CRONS.json" }),
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">config/CRONS.json</span> in your project.
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
- if (o?.codingAgent || o?.backendApi) {
157
- return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: [o.codingAgent, o.backendApi].filter(Boolean).join(" \xB7 ") });
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
- if (o?.codingAgent || o?.backendApi) {
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
- {[o.codingAgent, o.backendApi].filter(Boolean).join(' · ')}
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: "config/TRIGGERS.json" }),
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">config/TRIGGERS.json</span> in your project.
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
  ) : (