- /api/cron/reminders: processes pending reminders every 15min, sends WhatsApp with email fallback - /api/cron/overdue: marks overdue pledges daily (7d deferred, 14d immediate) - /api/pledges: GET handler with filtering, search, pagination, sort by dueDate - Dashboard overview: stats, collection progress bar, needs attention, upcoming payments - Dashboard pledges: proper table with status tabs, search, actions, pagination - New shadcn components: Table, Tabs, DropdownMenu, Progress - Setup wizard: 4-step onboarding (org → bank → event → QR code) - Settings API: PUT handler for org create/update - Org resolver: single-tenant fallback to first org - Cron jobs installed: reminders every 15min, overdue check at 6am - Auto-generates installment dates when not provided - HOSTNAME=0.0.0.0 in compose for multi-network binding
199 lines
6.1 KiB
TypeScript
199 lines
6.1 KiB
TypeScript
import Anthropic from "@anthropic-ai/sdk";
|
|
import { toolDefs, executeTool } from "./tools.js";
|
|
import * as store from "./store.js";
|
|
import * as tg from "./telegram.js";
|
|
|
|
const client = new Anthropic();
|
|
const MODEL = "claude-sonnet-4-20250514";
|
|
const MAX_TURNS = 50;
|
|
|
|
// Map of agentId -> resolve function for when user replies
|
|
const waitingForUser = new Map<number, (response: string) => void>();
|
|
|
|
export function isWaitingForUser(agentId: number): boolean {
|
|
return waitingForUser.has(agentId);
|
|
}
|
|
|
|
export function resolveUserResponse(agentId: number, response: string): void {
|
|
const resolve = waitingForUser.get(agentId);
|
|
if (resolve) {
|
|
waitingForUser.delete(agentId);
|
|
resolve(response);
|
|
}
|
|
}
|
|
|
|
function buildSystemPrompt(task: string): string {
|
|
return `You are an autonomous coding agent. You have been assigned a specific task.
|
|
|
|
YOUR TASK:
|
|
${task}
|
|
|
|
GUIDELINES:
|
|
- Work independently to complete the task
|
|
- Use the bash tool for running commands, git, etc.
|
|
- Use read_file, write_file, edit_file for file operations
|
|
- When you're done, call the "done" tool with a summary
|
|
- If you need user input or a decision, call "ask_user"
|
|
- Be efficient — don't explain what you're about to do, just do it
|
|
- If something fails, try to fix it yourself before asking the user
|
|
|
|
WORKING DIRECTORY: ${process.cwd()}`;
|
|
}
|
|
|
|
export async function runAgent(agentId: number): Promise<void> {
|
|
const agent = store.getAgent(agentId);
|
|
if (!agent) return;
|
|
|
|
store.updateAgent(agentId, { status: "working" });
|
|
store.addLog(agentId, "system", `Agent started: ${agent.task}`);
|
|
|
|
const messages: Anthropic.MessageParam[] = [
|
|
{ role: "user", content: agent.task },
|
|
];
|
|
|
|
let turns = 0;
|
|
|
|
try {
|
|
while (turns < MAX_TURNS) {
|
|
turns++;
|
|
|
|
const response = await client.messages.create({
|
|
model: MODEL,
|
|
max_tokens: 8096,
|
|
system: buildSystemPrompt(agent.task),
|
|
tools: toolDefs as any,
|
|
messages,
|
|
});
|
|
|
|
// Collect assistant content
|
|
const assistantContent = response.content;
|
|
messages.push({ role: "assistant", content: assistantContent });
|
|
|
|
// Log text blocks (no Telegram notification — reduces noise)
|
|
for (const block of assistantContent) {
|
|
if (block.type === "text" && block.text.trim()) {
|
|
store.addLog(agentId, "assistant", block.text);
|
|
}
|
|
}
|
|
|
|
// If no tool use, we're done
|
|
if (response.stop_reason !== "tool_use") {
|
|
store.updateAgent(agentId, {
|
|
status: "done",
|
|
summary: "Completed (no more actions)",
|
|
});
|
|
store.addLog(agentId, "system", "Agent finished (end_turn)");
|
|
await tg.send(
|
|
`✅ *Agent #${agentId}* finished.\nTask: ${agent.task}`,
|
|
agent.chat_id,
|
|
{ reply_to: agent.thread_msg_id || undefined }
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Process tool calls
|
|
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
|
|
for (const block of assistantContent) {
|
|
if (block.type !== "tool_use") continue;
|
|
|
|
const toolName = block.name;
|
|
const toolInput = block.input as Record<string, unknown>;
|
|
|
|
store.addLog(
|
|
agentId,
|
|
"tool",
|
|
`${toolName}: ${JSON.stringify(toolInput).slice(0, 500)}`
|
|
);
|
|
|
|
if (toolName === "ask_user") {
|
|
// Pause and wait for user response
|
|
store.updateAgent(agentId, { status: "waiting" });
|
|
await tg.send(
|
|
`❓ *Agent #${agentId}* needs your input:\n\n${toolInput.question}`,
|
|
agent.chat_id,
|
|
{
|
|
reply_to: agent.thread_msg_id || undefined,
|
|
keyboard: [
|
|
[{ text: "💬 Reply", callback_data: `talk_${agentId}` }],
|
|
],
|
|
}
|
|
);
|
|
store.addLog(agentId, "system", `Waiting for user: ${toolInput.question}`);
|
|
|
|
// Wait for user response
|
|
const userResponse = await new Promise<string>((resolve) => {
|
|
waitingForUser.set(agentId, resolve);
|
|
});
|
|
|
|
store.updateAgent(agentId, { status: "working" });
|
|
store.addLog(agentId, "user", `User replied: ${userResponse}`);
|
|
|
|
toolResults.push({
|
|
type: "tool_result",
|
|
tool_use_id: block.id,
|
|
content: `User responded: ${userResponse}`,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (toolName === "done") {
|
|
const summary = (toolInput.summary as string) || "Task completed";
|
|
store.updateAgent(agentId, { status: "done", summary });
|
|
store.addLog(agentId, "system", `Done: ${summary}`);
|
|
await tg.send(
|
|
`✅ *Agent #${agentId}* completed!\n\n*Summary:* ${summary}\n*Task:* ${agent.task}`,
|
|
agent.chat_id,
|
|
{ reply_to: agent.thread_msg_id || undefined }
|
|
);
|
|
|
|
toolResults.push({
|
|
type: "tool_result",
|
|
tool_use_id: block.id,
|
|
content: summary,
|
|
});
|
|
// Push tool results and stop
|
|
messages.push({ role: "user", content: toolResults });
|
|
return;
|
|
}
|
|
|
|
// Execute the tool
|
|
const { result } = executeTool(toolName, toolInput);
|
|
store.addLog(
|
|
agentId,
|
|
"tool_result",
|
|
`${toolName} → ${result.slice(0, 500)}`
|
|
);
|
|
|
|
toolResults.push({
|
|
type: "tool_result",
|
|
tool_use_id: block.id,
|
|
content: result,
|
|
});
|
|
}
|
|
|
|
messages.push({ role: "user", content: toolResults });
|
|
}
|
|
|
|
// Hit max turns
|
|
store.updateAgent(agentId, {
|
|
status: "done",
|
|
summary: `Stopped after ${MAX_TURNS} turns`,
|
|
});
|
|
await tg.send(
|
|
`⚠️ *Agent #${agentId}* hit max turns (${MAX_TURNS}). Task: ${agent.task}`,
|
|
agent.chat_id,
|
|
{ reply_to: agent.thread_msg_id || undefined }
|
|
);
|
|
} catch (e: any) {
|
|
console.error(`[agent ${agentId}] error:`, e);
|
|
store.updateAgent(agentId, { status: "error", error: e.message });
|
|
store.addLog(agentId, "error", e.message);
|
|
await tg.send(
|
|
`❌ *Agent #${agentId}* error:\n${e.message?.slice(0, 500)}`,
|
|
agent.chat_id,
|
|
{ reply_to: agent.thread_msg_id || undefined }
|
|
);
|
|
}
|
|
}
|