thepopebot 1.2.76-beta.16 → 1.2.76-beta.19

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 (54) hide show
  1. package/lib/CLAUDE.md +2 -2
  2. package/lib/ai/CLAUDE.md +72 -57
  3. package/lib/ai/helper-llm.js +108 -0
  4. package/lib/ai/index.js +294 -453
  5. package/lib/ai/line-mappers.js +42 -24
  6. package/lib/ai/sdk-adapters/CLAUDE.md +4 -3
  7. package/lib/ai/workspace-setup.js +3 -2
  8. package/lib/channels/CLAUDE.md +1 -1
  9. package/lib/channels/telegram.js +9 -9
  10. package/lib/chat/actions.js +60 -22
  11. package/lib/chat/api.js +34 -17
  12. package/lib/chat/components/CLAUDE.md +6 -2
  13. package/lib/chat/components/chat-input.js +2 -17
  14. package/lib/chat/components/chat-input.jsx +3 -17
  15. package/lib/chat/components/chat-page.js +2 -0
  16. package/lib/chat/components/chat-page.jsx +3 -0
  17. package/lib/chat/components/chat.js +16 -7
  18. package/lib/chat/components/chat.jsx +19 -6
  19. package/lib/chat/components/code-mode-toggle.js +33 -10
  20. package/lib/chat/components/code-mode-toggle.jsx +27 -9
  21. package/lib/chat/components/crons-page.js +17 -3
  22. package/lib/chat/components/crons-page.jsx +34 -6
  23. package/lib/chat/components/index.js +1 -1
  24. package/lib/chat/components/message.js +15 -0
  25. package/lib/chat/components/message.jsx +15 -0
  26. package/lib/chat/components/settings-chat-page.js +11 -11
  27. package/lib/chat/components/settings-chat-page.jsx +14 -18
  28. package/lib/chat/components/settings-coding-agents-page.js +109 -15
  29. package/lib/chat/components/settings-coding-agents-page.jsx +85 -1
  30. package/lib/chat/components/settings-secrets-layout.js +1 -1
  31. package/lib/chat/components/settings-secrets-layout.jsx +1 -1
  32. package/lib/chat/components/triggers-page.js +17 -3
  33. package/lib/chat/components/triggers-page.jsx +34 -6
  34. package/lib/chat/components/ui/dropdown-menu.js +23 -2
  35. package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
  36. package/lib/code/terminal-view.js +21 -1
  37. package/lib/code/terminal-view.jsx +16 -1
  38. package/lib/config.js +8 -2
  39. package/lib/db/index.js +12 -0
  40. package/lib/llm-providers.js +7 -0
  41. package/lib/tools/CLAUDE.md +2 -2
  42. package/lib/tools/assemblyai.js +17 -0
  43. package/lib/tools/create-agent-job.js +9 -8
  44. package/lib/tools/github.js +18 -0
  45. package/package.json +6 -7
  46. package/setup/lib/targets.mjs +1 -1
  47. package/templates/agent-job/CLAUDE.md.template +2 -1
  48. package/templates/agent-job/CRONS.json +16 -0
  49. package/templates/event-handler/TRIGGERS.json +18 -2
  50. package/lib/ai/agent.js +0 -65
  51. package/lib/ai/async-channel.js +0 -51
  52. package/lib/ai/model.js +0 -130
  53. package/lib/ai/tools.js +0 -184
  54. package/lib/tools/openai.js +0 -37
package/lib/CLAUDE.md CHANGED
@@ -18,10 +18,10 @@ If the task needs to *think*, use `agent`. If it just needs to *do*, use `comman
18
18
 
19
19
  ## Cron Jobs
20
20
 
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`, `llm_model`, and `scope` fields. `scope` sets the agent's working directory to a subdirectory (e.g., `"scope": "agents/gary-vee"`) — the system prompt and skills resolve from that scope.
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 `agent_backend`, `llm_model`, and `scope` fields. `agent_backend` picks which coding agent runs the job (e.g. `claude-code`, `codex-cli`); `llm_model` overrides the model within that agent. `scope` sets the agent's working directory to a subdirectory (e.g., `"scope": "agents/gary-vee"`) — the system prompt and skills resolve from that scope.
22
22
 
23
23
  ## Webhook Triggers
24
24
 
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`/`scope` 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 `agent_backend`/`llm_model`/`scope` 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
@@ -1,44 +1,79 @@
1
1
  # lib/ai/ — LLM Integration
2
2
 
3
- ## Agent Types
3
+ ## Architecture
4
4
 
5
- Two agent singletons, both using `createReactAgent` from `@langchain/langgraph/prebuilt` with `SqliteSaver` for conversation memory:
5
+ Every chat message flows through `chatStream()` in `index.js`. After workspace setup, it forks on whether a registered SDK adapter exists for the active coding agent:
6
6
 
7
- **Agent Chat** — singleton via `getAgentChat()`:
8
- - System prompt: `event-handler/agent-chat/SYSTEM.md` (rendered fresh each invocation via `render_md()`)
9
- - Tools: `agent_job`, `coding_agent`
10
- - Call `resetAgentChats()` to clear both singletons (required if hot-reloading)
7
+ - **SDK path** (`streamViaSdk`) in-process `@anthropic-ai/claude-agent-sdk` via `sdk-adapters/claude-code.js`. Used only when `CODING_AGENT=claude-code`.
8
+ - **Direct path** (`streamViaContainer`) spawns the configured coding agent in an ephemeral headless Docker container via `runHeadlessContainer()`. Streams output through `parseHeadlessStream()`. Used for every agent without an SDK adapter (pi, codex, gemini, opencode, kimi).
11
9
 
12
- **Code Chat** singleton via `getCodeChat()`:
13
- - System prompt: `event-handler/code-chat/SYSTEM.md` (rendered fresh each invocation)
14
- - Tools: `coding_agent` (reads repo/branch/workspace from `runtime.configurable`)
10
+ Both paths yield the same normalized chunk shape and use the same DB persistence pattern. There is no LangGraph React agent and no intermediate LLM between the user's message and the agent.
15
11
 
16
- ## Adding a New Tool
12
+ ## Multi-Turn Memory
17
13
 
18
- 1. Define in `tools.js` with Zod schema (use `tool()` from `@langchain/core/tools`)
19
- 2. Add to the agent's tools array in `agent.js`
20
- 3. Call `resetAgentChats()` if the agent needs to pick up the new tool without restart
14
+ Neither path persists conversation context at the LangChain/LangGraph layer — that layer is gone. Memory lives where the coding agent naturally keeps it:
15
+
16
+ - **SDK path** — session ID captured from the SDK's `meta` chunk and written via `session-manager.js` (`{workspaceBaseDir}/.claude-ttyd-sessions/7681`). Passed back into the SDK on the next turn.
17
+ - **Direct path** — `runHeadlessContainer()` passes `CONTINUE_SESSION=1` into the container. Each agent's `run.sh` reads its own port-keyed session file and resumes natively (see `docker/coding-agent/CLAUDE.md` § Session Tracking).
21
18
 
22
19
  ## Chat Modes
23
20
 
24
- Two primary chat modes stored in `chats.chatMode`:
21
+ `chats.chatMode` is either `'agent'` or `'code'`:
22
+
23
+ - **Agent mode** (`chatMode: 'agent'`) — repo/branch defaulted from `GH_OWNER`/`GH_REPO`, `main` branch, agent job secrets injected, system prompt built from `event-handler/agent-chat/SYSTEM.md` with scope-resolved skills.
24
+ - **Code mode** (`chatMode: 'code'`) — user-selected repo/branch, no secret injection, system prompt from `event-handler/code-chat/SYSTEM.md`.
25
+
26
+ Per-chat sub-mode via `codeModeType`:
27
+ - **plan** — `PERMISSION=plan` (read-only).
28
+ - **code** — `PERMISSION=code` (write/dangerous).
29
+
30
+ The "job" sub-mode is no longer wired — a skill will replace autonomous job dispatch.
31
+
32
+ ## Chunk Shape
33
+
34
+ `chatStream()` yields normalized chunks consumed by `lib/chat/api.js`:
35
+
36
+ - `{ type: 'text', text }`
37
+ - `{ type: 'tool-call', toolCallId, toolName, args }`
38
+ - `{ type: 'tool-result', toolCallId, result }`
39
+ - `{ type: 'error', message }` — surfaced to the UI as a red message and persisted for refresh
40
+ - `{ type: 'meta', ... }`, `{ type: 'result', ... }` — internal, not emitted to client
41
+ - `{ type: 'thinking-start' | 'thinking' | 'thinking-end' }` — SDK path only
42
+
43
+ ## Workspace Setup
44
+
45
+ `ensureWorkspaceRepo()` (workspace-setup.js) is called before either path runs. It clones the repo, sets git identity, and checks out/creates the feature branch on the host — agent-agnostic. The container's `2_clone.sh` is a no-op when `.git` already exists.
46
+
47
+ On the first message in a new chat, `chatStream` yields a visible `tool-call`/`tool-result` pair with `toolName: 'workspace'` so the setup appears in the UI.
25
48
 
26
- **Agent mode** (`chatMode: 'agent'`) — Tools: `agent_job`, `coding_agent`. Three sub-modes selected per-chat via `codeModeType` (stored in client localStorage):
27
- - **plan** — `coding_agent` runs in read-only permission mode
28
- - **code** — `coding_agent` runs in write (dangerous) permission mode
29
- - **job** — `agent_job` dispatches autonomous Docker container task
49
+ ## Helper LLM (`helper-llm.js`)
30
50
 
31
- **Code mode** (`chatMode: 'code'`) Tool: `coding_agent` only (operates on user's selected repo). Sub-modes: plan and code (no job).
51
+ Small one-shot completions used by the event handler itself. Independent of the coding agent it has its own provider/model selection at `/admin/event-handler/helper-llm`.
32
52
 
33
- The `[chat mode: X]` suffix is appended to user messages in `index.js` so the LLM knows which tool to invoke. `codeModeType` flows through `runtime.configurable` to tools, which map it to Docker's `PERMISSION` env var (`plan` or `code`).
53
+ **Callers:**
54
+ - `autoTitle()` (chat title — 2-5 word title for "New Chat")
55
+ - `summarizeAgentJob()` (webhook-triggered PR merge summary)
56
+ - `generateAgentJobTitle()` (~10 word title for an agent job)
34
57
 
35
- ## Model Resolution
58
+ **API:**
59
+ - `callHelperLlm({system, user, maxTokens})` → returns trimmed text (uses AI SDK `generateText`)
60
+ - `callHelperLlmStructured({system, user, schema, maxTokens})` → returns parsed object (uses AI SDK `generateObject`); throws on schema/parse failure (callers catch and fall back)
36
61
 
37
- `createModel()` in `model.js` resolves provider/model at agent creation time (singleton for chat agent). Provider determined by `LLM_PROVIDER` config, model by `LLM_MODEL`. Changing these requires restart (or `resetAgentChats()`).
62
+ **Provider resolution.** Reads `LLM_PROVIDER` and `LLM_MODEL` from config and builds the right AI SDK adapter:
63
+
64
+ | Provider slug | AI SDK adapter |
65
+ |---|---|
66
+ | `anthropic` | `@ai-sdk/anthropic` |
67
+ | `openai` | `@ai-sdk/openai` |
68
+ | `google` | `@ai-sdk/google` |
69
+ | built-in OpenAI-compatible (`deepseek`, `mistral`, `xai`, `kimi`, `openrouter`, `nvidia`) | `@ai-sdk/openai-compatible` with each provider's `baseUrl` from `BUILTIN_PROVIDERS` |
70
+ | custom user-added | `@ai-sdk/openai-compatible` with the custom provider's `baseUrl` and `apiKey` |
71
+
72
+ The AI SDK handles per-provider quirks (max-token param naming, thinking/reasoning block stripping, structured output via the right native mechanism per provider). Helper LLM has no LangChain dependency.
38
73
 
39
74
  ### LLM Providers
40
75
 
41
- Source of truth: `lib/llm-providers.js` (`BUILTIN_PROVIDERS`). Each provider declares credentials, available models, and capability flags (`chat`, `codingAgent`) that gate which models appear in which UI contexts.
76
+ Source of truth: `lib/llm-providers.js` (`BUILTIN_PROVIDERS`).
42
77
 
43
78
  | Provider | `LLM_PROVIDER` | Default Model | Required Key |
44
79
  |----------|----------------|---------------|-------------|
@@ -52,49 +87,29 @@ Source of truth: `lib/llm-providers.js` (`BUILTIN_PROVIDERS`). Each provider dec
52
87
  | Kimi | `kimi` | `kimi-k2.5` | `MOONSHOT_API_KEY` |
53
88
  | OpenRouter | `openrouter` | (user-specified) | `OPENROUTER_API_KEY` |
54
89
 
55
- All credentials are stored in the settings DB (encrypted), not `.env`. Configured via `/admin/event-handler/llms` (credentials) and `/admin/event-handler/chat` (model selection).
56
-
57
- **Custom providers**: Users can add OpenAI-compatible providers via the admin UI. Stored as `type: 'llm_provider'` in the settings table. Resolved in `model.js` via `getCustomProvider()`.
58
-
59
- `LLM_MAX_TOKENS` defaults to 4096.
60
-
61
- > **Google model compatibility note:** `gemini-2.5-pro` and `gemini-3.*` models require `thought_signature` round-tripping that `@langchain/google-genai` doesn't support. Auto-falls back to `gemini-2.5-flash` with a warning (issue #201).
90
+ All credentials are stored in the settings DB (encrypted). `LLM_MAX_TOKENS` defaults to 4096.
62
91
 
63
- ## Chat Streaming
64
-
65
- `chatStream()` in `index.js` yields chunks: `{ type: 'text', content }`, `{ type: 'tool-call', name, args }`, `{ type: 'tool-result', name, result }`. Called by `lib/chat/api.js` (the `/stream/chat` endpoint).
92
+ **Custom providers**: users can add OpenAI-compatible providers via `/admin/event-handler/llms`. Stored as `type: 'llm_provider'` in the settings table. Resolved at call time via `getCustomProvider()` in `helper-llm.js`.
66
93
 
67
94
  ## Headless Stream Parser (headless-stream.js)
68
95
 
69
- Three-layer parser for Claude Code agents running in headless Docker containers:
96
+ Three-layer parser consumed by the direct path:
70
97
 
71
- 1. **Docker frame decoder** — Parses 8-byte multiplexed stream headers (type + size), extracts stdout frames, discards stderr. Buffers incomplete frames across chunks.
72
- 2. **NDJSON splitter** — Accumulates decoded UTF-8, splits on newlines. Holds incomplete trailing lines for next chunk.
73
- 3. **Event mapper** (`mapLine()`) — Converts each line to chat events:
98
+ 1. **Docker frame decoder** — parses 8-byte multiplexed stream headers (type + size), extracts stdout frames, discards stderr.
99
+ 2. **NDJSON splitter** — accumulates decoded UTF-8 and splits on newlines.
100
+ 3. **Event mapper** (`mapLine()`) — converts each line to chat events:
74
101
  - `assistant` messages: `text` blocks → `{ type: 'text' }`, `tool_use` blocks → `{ type: 'tool-call' }`
75
- - `user` messages: `tool_result` blocks → `{ type: 'tool-result' }` (priority: stdout > string content > array)
76
- - `result` messages: → `{ type: 'text' }` (final summary from the agent)
102
+ - `user` messages: `tool_result` blocks → `{ type: 'tool-result' }` (priority: stdout > string > array)
103
+ - `result` messages: → `{ type: 'text' }` (final summary)
77
104
  - Non-JSON lines (e.g. `NO_CHANGES`, `AGENT_FAILED`): wrapped as plain text events
78
105
 
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 }]`.
106
+ `mapLine()` is also reused by `lib/cluster/stream.js` for worker log parsing.
92
107
 
93
108
  ### Adding a New Agent Mapper (line-mappers.js)
94
109
 
95
- Each coding agent CLI has its own mapper function (`mapClaudeCodeLine`, `mapPiLine`, `mapGeminiLine`, `mapCodexLine`, `mapOpenCodeLine`, `mapKimiLine`). When adding a new agent:
110
+ Each coding agent CLI has its own mapper (`mapClaudeCodeLine`, `mapPiLine`, `mapGeminiLine`, `mapCodexLine`, `mapOpenCodeLine`, `mapKimiLine`). To add one:
96
111
 
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
112
+ 1. Create `mapXxxLine(parsed)` in `line-mappers.js` that returns an array of `{ type, ... }` events.
113
+ 2. Register it in `headless-stream.js`: imports, re-exports, and the `mapperMap` object.
114
+ 3. Map the agent's JSON output to the chunk shape above.
115
+ 4. Return `[{ type: 'skip' }]` for noise events to suppress them without triggering the unknown fallback.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Helper LLM — small one-shot completions used by the event handler itself
3
+ * (chat titles, agent-job summaries, agent-job titles). Independent of the
4
+ * coding agent and the streaming chat path.
5
+ *
6
+ * Provider/model is set at /admin/event-handler/helper-llm and stored as
7
+ * LLM_PROVIDER / LLM_MODEL config keys. Credentials live in the same settings
8
+ * DB used by /admin/event-handler/llms.
9
+ */
10
+
11
+ import { generateText, generateObject } from 'ai';
12
+ import { createAnthropic } from '@ai-sdk/anthropic';
13
+ import { createOpenAI } from '@ai-sdk/openai';
14
+ import { createGoogleGenerativeAI } from '@ai-sdk/google';
15
+ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
16
+ import { getConfig } from '../config.js';
17
+ import { getCustomProvider } from '../db/config.js';
18
+ import { BUILTIN_PROVIDERS } from '../llm-providers.js';
19
+
20
+ /**
21
+ * Build the active LanguageModelV2 instance for helper LLM calls.
22
+ * Reads LLM_PROVIDER + LLM_MODEL from config and selects the right adapter.
23
+ *
24
+ * @returns {import('ai').LanguageModelV2}
25
+ */
26
+ function resolveModel() {
27
+ const slug = getConfig('LLM_PROVIDER');
28
+ const modelName = getConfig('LLM_MODEL');
29
+ if (!slug) throw new Error('LLM_PROVIDER not configured');
30
+ if (!modelName) throw new Error('LLM_MODEL not configured');
31
+
32
+ if (slug === 'anthropic') {
33
+ return createAnthropic({ apiKey: getConfig('ANTHROPIC_API_KEY') })(modelName);
34
+ }
35
+ if (slug === 'google') {
36
+ return createGoogleGenerativeAI({ apiKey: getConfig('GOOGLE_API_KEY') })(modelName);
37
+ }
38
+ if (slug === 'openai') {
39
+ return createOpenAI({ apiKey: getConfig('OPENAI_API_KEY') })(modelName);
40
+ }
41
+
42
+ // Built-in OpenAI-compatible providers (deepseek, mistral, xai, kimi, openrouter, nvidia)
43
+ const builtin = BUILTIN_PROVIDERS[slug];
44
+ if (builtin) {
45
+ if (!builtin.baseUrl) throw new Error(`Provider ${slug} has no baseUrl`);
46
+ return createOpenAICompatible({
47
+ name: slug,
48
+ baseURL: builtin.baseUrl,
49
+ apiKey: getConfig(builtin.credentials[0].key),
50
+ })(modelName);
51
+ }
52
+
53
+ // Custom user-added OpenAI-compatible provider
54
+ const custom = getCustomProvider(slug);
55
+ if (custom) {
56
+ return createOpenAICompatible({
57
+ name: slug,
58
+ baseURL: custom.baseUrl,
59
+ apiKey: custom.apiKey || 'not-needed',
60
+ })(modelName);
61
+ }
62
+
63
+ throw new Error(`Unknown LLM provider: ${slug}`);
64
+ }
65
+
66
+ /**
67
+ * Plain-text helper LLM call. Returns the trimmed text.
68
+ *
69
+ * @param {object} args
70
+ * @param {string} args.system - System prompt
71
+ * @param {string} args.user - User prompt
72
+ * @param {number} args.maxTokens - Max output tokens
73
+ * @returns {Promise<string>}
74
+ */
75
+ export async function callHelperLlm({ system, user, maxTokens }) {
76
+ const model = resolveModel();
77
+ const { text } = await generateText({
78
+ model,
79
+ system,
80
+ prompt: user,
81
+ maxOutputTokens: maxTokens,
82
+ });
83
+ return (text || '').trim();
84
+ }
85
+
86
+ /**
87
+ * Structured helper LLM call. Returns the parsed object matching the schema.
88
+ * Throws if the response can't be parsed or fails schema validation —
89
+ * callers catch and fall back as appropriate.
90
+ *
91
+ * @param {object} args
92
+ * @param {string} args.system - System prompt
93
+ * @param {string} args.user - User prompt
94
+ * @param {import('zod').ZodTypeAny} args.schema - Zod schema for the output
95
+ * @param {number} args.maxTokens - Max output tokens
96
+ * @returns {Promise<unknown>}
97
+ */
98
+ export async function callHelperLlmStructured({ system, user, schema, maxTokens }) {
99
+ const model = resolveModel();
100
+ const { object } = await generateObject({
101
+ model,
102
+ system,
103
+ prompt: user,
104
+ schema,
105
+ maxOutputTokens: maxTokens,
106
+ });
107
+ return object;
108
+ }