production: reminder cron, dashboard overhaul, shadcn components, setup wizard
- /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
This commit is contained in:
198
lib/agent-worker.ts
Normal file
198
lib/agent-worker.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user