stagent 0.1.0
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/LICENSE +191 -0
- package/README.md +399 -0
- package/components.json +21 -0
- package/dist/cli.js +171 -0
- package/drizzle.config.ts +12 -0
- package/next.config.mjs +15 -0
- package/package.json +114 -0
- package/postcss.config.mjs +8 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +13 -0
- package/public/readme/home-workspace.png +0 -0
- package/public/readme/inbox-approvals.png +0 -0
- package/public/readme/workflow-blueprints.png +0 -0
- package/public/stagent-s-128.png +0 -0
- package/public/stagent-s-64.png +0 -0
- package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
- package/src/app/api/blueprints/[id]/route.ts +39 -0
- package/src/app/api/blueprints/import/route.ts +68 -0
- package/src/app/api/blueprints/route.ts +29 -0
- package/src/app/api/command-palette/recent/route.ts +31 -0
- package/src/app/api/data/clear/route.ts +22 -0
- package/src/app/api/data/seed/route.ts +22 -0
- package/src/app/api/documents/[id]/file/route.ts +44 -0
- package/src/app/api/documents/[id]/route.ts +123 -0
- package/src/app/api/documents/route.ts +59 -0
- package/src/app/api/logs/stream/route.ts +101 -0
- package/src/app/api/notifications/[id]/route.ts +36 -0
- package/src/app/api/notifications/mark-all-read/route.ts +13 -0
- package/src/app/api/notifications/pending-approvals/route.ts +10 -0
- package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
- package/src/app/api/notifications/route.ts +34 -0
- package/src/app/api/permissions/route.ts +46 -0
- package/src/app/api/profiles/[id]/route.ts +79 -0
- package/src/app/api/profiles/[id]/test/route.ts +42 -0
- package/src/app/api/profiles/import/route.ts +108 -0
- package/src/app/api/profiles/route.ts +50 -0
- package/src/app/api/projects/[id]/route.ts +72 -0
- package/src/app/api/projects/route.ts +53 -0
- package/src/app/api/schedules/[id]/route.ts +185 -0
- package/src/app/api/schedules/route.ts +117 -0
- package/src/app/api/settings/budgets/route.ts +24 -0
- package/src/app/api/settings/openai/route.ts +24 -0
- package/src/app/api/settings/route.ts +21 -0
- package/src/app/api/settings/test/route.ts +26 -0
- package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
- package/src/app/api/tasks/[id]/execute/route.ts +90 -0
- package/src/app/api/tasks/[id]/logs/route.ts +95 -0
- package/src/app/api/tasks/[id]/output/route.ts +47 -0
- package/src/app/api/tasks/[id]/respond/route.ts +64 -0
- package/src/app/api/tasks/[id]/resume/route.ts +76 -0
- package/src/app/api/tasks/[id]/route.ts +77 -0
- package/src/app/api/tasks/assist/route.ts +35 -0
- package/src/app/api/tasks/route.ts +82 -0
- package/src/app/api/uploads/[id]/route.ts +81 -0
- package/src/app/api/uploads/cleanup/route.ts +7 -0
- package/src/app/api/uploads/route.ts +66 -0
- package/src/app/api/workflows/[id]/execute/route.ts +82 -0
- package/src/app/api/workflows/[id]/route.ts +133 -0
- package/src/app/api/workflows/[id]/status/route.ts +54 -0
- package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
- package/src/app/api/workflows/route.ts +61 -0
- package/src/app/apple-icon.tsx +31 -0
- package/src/app/costs/page.tsx +256 -0
- package/src/app/dashboard/page.tsx +44 -0
- package/src/app/documents/[id]/page.tsx +46 -0
- package/src/app/documents/page.tsx +45 -0
- package/src/app/error.tsx +26 -0
- package/src/app/global-error.tsx +23 -0
- package/src/app/globals.css +733 -0
- package/src/app/icon.tsx +30 -0
- package/src/app/inbox/loading.tsx +15 -0
- package/src/app/inbox/page.tsx +35 -0
- package/src/app/layout.tsx +78 -0
- package/src/app/manifest.ts +32 -0
- package/src/app/monitor/page.tsx +37 -0
- package/src/app/page.tsx +162 -0
- package/src/app/profiles/[id]/edit/page.tsx +39 -0
- package/src/app/profiles/[id]/page.tsx +33 -0
- package/src/app/profiles/new/page.tsx +22 -0
- package/src/app/profiles/page.tsx +19 -0
- package/src/app/projects/[id]/page.tsx +134 -0
- package/src/app/projects/loading.tsx +17 -0
- package/src/app/projects/page.tsx +32 -0
- package/src/app/schedules/[id]/page.tsx +47 -0
- package/src/app/schedules/page.tsx +18 -0
- package/src/app/settings/loading.tsx +24 -0
- package/src/app/settings/page.tsx +27 -0
- package/src/app/tasks/[id]/page.tsx +45 -0
- package/src/app/tasks/new/page.tsx +27 -0
- package/src/app/workflows/[id]/edit/page.tsx +66 -0
- package/src/app/workflows/[id]/page.tsx +37 -0
- package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
- package/src/app/workflows/blueprints/new/page.tsx +20 -0
- package/src/app/workflows/blueprints/page.tsx +11 -0
- package/src/app/workflows/new/page.tsx +36 -0
- package/src/app/workflows/page.tsx +18 -0
- package/src/components/charts/donut-ring.tsx +64 -0
- package/src/components/charts/mini-bar.tsx +75 -0
- package/src/components/charts/sparkline.tsx +107 -0
- package/src/components/costs/cost-dashboard.tsx +877 -0
- package/src/components/costs/cost-filters.tsx +179 -0
- package/src/components/dashboard/activity-feed.tsx +95 -0
- package/src/components/dashboard/greeting.tsx +30 -0
- package/src/components/dashboard/priority-queue.tsx +79 -0
- package/src/components/dashboard/quick-actions.tsx +62 -0
- package/src/components/dashboard/recent-projects.tsx +79 -0
- package/src/components/dashboard/stats-cards.tsx +114 -0
- package/src/components/documents/document-browser.tsx +235 -0
- package/src/components/documents/document-detail-view.tsx +367 -0
- package/src/components/documents/document-grid.tsx +78 -0
- package/src/components/documents/document-preview.tsx +68 -0
- package/src/components/documents/document-table.tsx +119 -0
- package/src/components/documents/document-upload-dialog.tsx +153 -0
- package/src/components/documents/types.ts +6 -0
- package/src/components/documents/utils.ts +57 -0
- package/src/components/monitoring/connection-indicator.tsx +14 -0
- package/src/components/monitoring/log-entry.tsx +79 -0
- package/src/components/monitoring/log-filters.tsx +57 -0
- package/src/components/monitoring/log-stream.tsx +144 -0
- package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
- package/src/components/monitoring/monitor-overview.tsx +119 -0
- package/src/components/notifications/failure-action.tsx +38 -0
- package/src/components/notifications/inbox-list.tsx +165 -0
- package/src/components/notifications/message-response.tsx +196 -0
- package/src/components/notifications/notification-item.tsx +250 -0
- package/src/components/notifications/pending-approval-host.tsx +478 -0
- package/src/components/notifications/permission-action.tsx +37 -0
- package/src/components/notifications/permission-response-actions.tsx +126 -0
- package/src/components/notifications/unread-badge.tsx +35 -0
- package/src/components/profiles/profile-browser.tsx +117 -0
- package/src/components/profiles/profile-card.tsx +78 -0
- package/src/components/profiles/profile-detail-view.tsx +564 -0
- package/src/components/profiles/profile-form-view.tsx +480 -0
- package/src/components/profiles/profile-import-dialog.tsx +113 -0
- package/src/components/projects/project-card.tsx +58 -0
- package/src/components/projects/project-create-dialog.tsx +140 -0
- package/src/components/projects/project-detail.tsx +68 -0
- package/src/components/projects/project-edit-dialog.tsx +219 -0
- package/src/components/projects/project-list.tsx +108 -0
- package/src/components/schedules/schedule-create-dialog.tsx +403 -0
- package/src/components/schedules/schedule-detail-view.tsx +274 -0
- package/src/components/schedules/schedule-list.tsx +242 -0
- package/src/components/schedules/schedule-status-badge.tsx +16 -0
- package/src/components/settings/api-key-form.tsx +141 -0
- package/src/components/settings/auth-config-section.tsx +141 -0
- package/src/components/settings/auth-method-selector.tsx +67 -0
- package/src/components/settings/auth-status-badge.tsx +40 -0
- package/src/components/settings/auth-status-dot.tsx +59 -0
- package/src/components/settings/budget-guardrails-section.tsx +842 -0
- package/src/components/settings/data-management-section.tsx +141 -0
- package/src/components/settings/openai-runtime-section.tsx +104 -0
- package/src/components/settings/permissions-section.tsx +91 -0
- package/src/components/shared/app-sidebar.tsx +123 -0
- package/src/components/shared/card-skeleton.tsx +42 -0
- package/src/components/shared/command-palette.tsx +250 -0
- package/src/components/shared/confirm-dialog.tsx +52 -0
- package/src/components/shared/empty-state.tsx +24 -0
- package/src/components/shared/error-state.tsx +32 -0
- package/src/components/shared/form-section-card.tsx +33 -0
- package/src/components/shared/section-heading.tsx +14 -0
- package/src/components/shared/stagent-logo.tsx +21 -0
- package/src/components/shared/theme-toggle.tsx +46 -0
- package/src/components/tasks/ai-assist-panel.tsx +210 -0
- package/src/components/tasks/content-preview.tsx +89 -0
- package/src/components/tasks/empty-board.tsx +12 -0
- package/src/components/tasks/file-upload.tsx +120 -0
- package/src/components/tasks/kanban-board.tsx +275 -0
- package/src/components/tasks/kanban-column.tsx +75 -0
- package/src/components/tasks/skeleton-board.tsx +21 -0
- package/src/components/tasks/task-attachments.tsx +114 -0
- package/src/components/tasks/task-card.tsx +101 -0
- package/src/components/tasks/task-create-panel.tsx +360 -0
- package/src/components/tasks/task-detail-view.tsx +356 -0
- package/src/components/ui/alert-dialog.tsx +196 -0
- package/src/components/ui/badge.tsx +50 -0
- package/src/components/ui/button.tsx +71 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +36 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/components/workflows/blueprint-editor.tsx +109 -0
- package/src/components/workflows/blueprint-gallery.tsx +155 -0
- package/src/components/workflows/blueprint-preview.tsx +240 -0
- package/src/components/workflows/loop-status-view.tsx +272 -0
- package/src/components/workflows/swarm-dashboard.tsx +185 -0
- package/src/components/workflows/workflow-form-view.tsx +1376 -0
- package/src/components/workflows/workflow-list.tsx +230 -0
- package/src/components/workflows/workflow-status-view.tsx +477 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/instrumentation.ts +7 -0
- package/src/lib/agents/claude-agent.ts +737 -0
- package/src/lib/agents/execution-manager.ts +27 -0
- package/src/lib/agents/profiles/assignment-validation.ts +75 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
- package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/compatibility.ts +109 -0
- package/src/lib/agents/profiles/registry.ts +293 -0
- package/src/lib/agents/profiles/test-runner.ts +18 -0
- package/src/lib/agents/profiles/test-types.ts +20 -0
- package/src/lib/agents/profiles/types.ts +43 -0
- package/src/lib/agents/router.ts +56 -0
- package/src/lib/agents/runtime/catalog.ts +85 -0
- package/src/lib/agents/runtime/claude-sdk.ts +12 -0
- package/src/lib/agents/runtime/claude.ts +370 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
- package/src/lib/agents/runtime/index.ts +167 -0
- package/src/lib/agents/runtime/openai-codex.ts +1089 -0
- package/src/lib/agents/runtime/task-assist-types.ts +8 -0
- package/src/lib/agents/runtime/types.ts +30 -0
- package/src/lib/constants/settings.ts +13 -0
- package/src/lib/constants/status-colors.ts +44 -0
- package/src/lib/constants/task-status.ts +49 -0
- package/src/lib/data/clear.ts +63 -0
- package/src/lib/data/seed-data/documents.ts +715 -0
- package/src/lib/data/seed-data/logs.ts +195 -0
- package/src/lib/data/seed-data/notifications.ts +141 -0
- package/src/lib/data/seed-data/profiles.ts +175 -0
- package/src/lib/data/seed-data/projects.ts +61 -0
- package/src/lib/data/seed-data/schedules.ts +108 -0
- package/src/lib/data/seed-data/tasks.ts +341 -0
- package/src/lib/data/seed-data/usage-ledger.ts +130 -0
- package/src/lib/data/seed-data/workflows.ts +213 -0
- package/src/lib/data/seed.ts +129 -0
- package/src/lib/db/index.ts +221 -0
- package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
- package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
- package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
- package/src/lib/db/migrations/0003_add_settings.sql +5 -0
- package/src/lib/db/migrations/0004_add_documents.sql +20 -0
- package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
- package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
- package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
- package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
- package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
- package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
- package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
- package/src/lib/db/migrations/meta/_journal.json +27 -0
- package/src/lib/db/schema.ts +227 -0
- package/src/lib/documents/cleanup.ts +50 -0
- package/src/lib/documents/context-builder.ts +75 -0
- package/src/lib/documents/output-scanner.ts +166 -0
- package/src/lib/documents/processor.ts +120 -0
- package/src/lib/documents/processors/image.ts +21 -0
- package/src/lib/documents/processors/office.ts +36 -0
- package/src/lib/documents/processors/pdf.ts +12 -0
- package/src/lib/documents/processors/spreadsheet.ts +18 -0
- package/src/lib/documents/processors/text.ts +8 -0
- package/src/lib/documents/registry.ts +25 -0
- package/src/lib/notifications/actionable.ts +108 -0
- package/src/lib/notifications/permissions.ts +169 -0
- package/src/lib/queries/chart-data.ts +184 -0
- package/src/lib/schedules/interval-parser.ts +110 -0
- package/src/lib/schedules/scheduler.ts +220 -0
- package/src/lib/settings/auth.ts +98 -0
- package/src/lib/settings/budget-guardrails.ts +590 -0
- package/src/lib/settings/helpers.ts +23 -0
- package/src/lib/settings/openai-auth.ts +80 -0
- package/src/lib/settings/permissions.ts +102 -0
- package/src/lib/usage/ledger.ts +489 -0
- package/src/lib/usage/pricing.ts +68 -0
- package/src/lib/utils/crypto.ts +90 -0
- package/src/lib/utils/format-timestamp.ts +46 -0
- package/src/lib/utils/session-cleanup.ts +26 -0
- package/src/lib/utils/stagent-paths.ts +18 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/blueprint.ts +43 -0
- package/src/lib/validators/profile.ts +64 -0
- package/src/lib/validators/project.ts +17 -0
- package/src/lib/validators/settings.ts +57 -0
- package/src/lib/validators/task.ts +30 -0
- package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
- package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
- package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
- package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
- package/src/lib/workflows/blueprints/instantiator.ts +131 -0
- package/src/lib/workflows/blueprints/registry.ts +128 -0
- package/src/lib/workflows/blueprints/template.ts +58 -0
- package/src/lib/workflows/blueprints/types.ts +38 -0
- package/src/lib/workflows/definition-validation.ts +121 -0
- package/src/lib/workflows/engine.ts +1113 -0
- package/src/lib/workflows/loop-executor.ts +270 -0
- package/src/lib/workflows/parallel.ts +55 -0
- package/src/lib/workflows/swarm.ts +97 -0
- package/src/lib/workflows/types.ts +112 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { tasks, projects, agentLogs, notifications } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { setExecution, removeExecution } from "./execution-manager";
|
|
6
|
+
import { MAX_RESUME_COUNT } from "@/lib/constants/task-status";
|
|
7
|
+
import { getAuthEnv, updateAuthStatus } from "@/lib/settings/auth";
|
|
8
|
+
import { buildDocumentContext } from "@/lib/documents/context-builder";
|
|
9
|
+
import {
|
|
10
|
+
buildTaskOutputInstructions,
|
|
11
|
+
prepareTaskOutputDirectory,
|
|
12
|
+
scanTaskOutputDocuments,
|
|
13
|
+
} from "@/lib/documents/output-scanner";
|
|
14
|
+
import { getProfile } from "./profiles/registry";
|
|
15
|
+
import { resolveProfileRuntimePayload } from "./profiles/compatibility";
|
|
16
|
+
import type { CanUseToolPolicy } from "./profiles/types";
|
|
17
|
+
import { buildClaudeSdkEnv } from "./runtime/claude-sdk";
|
|
18
|
+
import {
|
|
19
|
+
extractUsageSnapshot,
|
|
20
|
+
mergeUsageSnapshot,
|
|
21
|
+
recordUsageLedgerEntry,
|
|
22
|
+
resolveUsageActivityType,
|
|
23
|
+
type UsageActivityType,
|
|
24
|
+
type UsageSnapshot,
|
|
25
|
+
} from "@/lib/usage/ledger";
|
|
26
|
+
|
|
27
|
+
/** Typed representation of messages from the Agent SDK stream */
|
|
28
|
+
interface AgentStreamMessage {
|
|
29
|
+
type?: string;
|
|
30
|
+
subtype?: string;
|
|
31
|
+
session_id?: string;
|
|
32
|
+
api_key_source?: string;
|
|
33
|
+
event?: Record<string, unknown>;
|
|
34
|
+
message?: {
|
|
35
|
+
content?: Array<{ type: string; name?: string; input?: unknown }>;
|
|
36
|
+
};
|
|
37
|
+
result?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TaskUsageState extends UsageSnapshot {
|
|
41
|
+
activityType: UsageActivityType;
|
|
42
|
+
startedAt: Date;
|
|
43
|
+
taskId: string;
|
|
44
|
+
projectId?: string | null;
|
|
45
|
+
workflowId?: string | null;
|
|
46
|
+
scheduleId?: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ToolPermissionResponse {
|
|
50
|
+
behavior: "allow" | "deny";
|
|
51
|
+
updatedInput?: unknown;
|
|
52
|
+
message?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const inFlightPermissionRequests = new Map<
|
|
56
|
+
string,
|
|
57
|
+
Promise<ToolPermissionResponse>
|
|
58
|
+
>();
|
|
59
|
+
const settledPermissionRequests = new Map<string, ToolPermissionResponse>();
|
|
60
|
+
|
|
61
|
+
function buildAllowedToolPermissionResponse(
|
|
62
|
+
input: Record<string, unknown>
|
|
63
|
+
): ToolPermissionResponse {
|
|
64
|
+
return {
|
|
65
|
+
behavior: "allow",
|
|
66
|
+
updatedInput: input,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeToolPermissionResponse(
|
|
71
|
+
response: ToolPermissionResponse,
|
|
72
|
+
input: Record<string, unknown>
|
|
73
|
+
): ToolPermissionResponse {
|
|
74
|
+
if (response.behavior !== "allow" || response.updatedInput !== undefined) {
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...response,
|
|
80
|
+
updatedInput: input,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createTaskUsageState(
|
|
85
|
+
task: {
|
|
86
|
+
id: string;
|
|
87
|
+
projectId?: string | null;
|
|
88
|
+
workflowId?: string | null;
|
|
89
|
+
scheduleId?: string | null;
|
|
90
|
+
},
|
|
91
|
+
isResume = false
|
|
92
|
+
): TaskUsageState {
|
|
93
|
+
return {
|
|
94
|
+
taskId: task.id,
|
|
95
|
+
projectId: task.projectId ?? null,
|
|
96
|
+
workflowId: task.workflowId ?? null,
|
|
97
|
+
scheduleId: task.scheduleId ?? null,
|
|
98
|
+
activityType: resolveUsageActivityType({
|
|
99
|
+
workflowId: task.workflowId,
|
|
100
|
+
scheduleId: task.scheduleId,
|
|
101
|
+
isResume,
|
|
102
|
+
}),
|
|
103
|
+
startedAt: new Date(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function applyUsageSnapshot(state: TaskUsageState, source: unknown) {
|
|
108
|
+
Object.assign(state, mergeUsageSnapshot(state, extractUsageSnapshot(source)));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildPermissionCacheKey(
|
|
112
|
+
taskId: string,
|
|
113
|
+
toolName: string,
|
|
114
|
+
input: Record<string, unknown>
|
|
115
|
+
): string {
|
|
116
|
+
return `${taskId}::${toolName}::${JSON.stringify(input)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function clearPermissionCache(taskId: string) {
|
|
120
|
+
const prefix = `${taskId}::`;
|
|
121
|
+
|
|
122
|
+
for (const key of inFlightPermissionRequests.keys()) {
|
|
123
|
+
if (key.startsWith(prefix)) {
|
|
124
|
+
inFlightPermissionRequests.delete(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const key of settledPermissionRequests.keys()) {
|
|
129
|
+
if (key.startsWith(prefix)) {
|
|
130
|
+
settledPermissionRequests.delete(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function waitForToolPermissionResponse(
|
|
136
|
+
notificationId: string
|
|
137
|
+
): Promise<ToolPermissionResponse> {
|
|
138
|
+
const deadline = Date.now() + 55_000;
|
|
139
|
+
const pollInterval = 1500;
|
|
140
|
+
|
|
141
|
+
while (Date.now() < deadline) {
|
|
142
|
+
const [notification] = await db
|
|
143
|
+
.select()
|
|
144
|
+
.from(notifications)
|
|
145
|
+
.where(eq(notifications.id, notificationId));
|
|
146
|
+
|
|
147
|
+
if (notification?.response) {
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(notification.response) as ToolPermissionResponse;
|
|
150
|
+
} catch {
|
|
151
|
+
return { behavior: "deny", message: "Invalid response format" };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { behavior: "deny", message: "Permission request timed out" };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function finalizeTaskUsage(
|
|
162
|
+
state: TaskUsageState,
|
|
163
|
+
status: "completed" | "failed" | "cancelled"
|
|
164
|
+
) {
|
|
165
|
+
await recordUsageLedgerEntry({
|
|
166
|
+
taskId: state.taskId,
|
|
167
|
+
workflowId: state.workflowId ?? null,
|
|
168
|
+
scheduleId: state.scheduleId ?? null,
|
|
169
|
+
projectId: state.projectId ?? null,
|
|
170
|
+
activityType: state.activityType,
|
|
171
|
+
runtimeId: "claude-code",
|
|
172
|
+
providerId: "anthropic",
|
|
173
|
+
modelId: state.modelId ?? null,
|
|
174
|
+
inputTokens: state.inputTokens ?? null,
|
|
175
|
+
outputTokens: state.outputTokens ?? null,
|
|
176
|
+
totalTokens: state.totalTokens ?? null,
|
|
177
|
+
status,
|
|
178
|
+
startedAt: state.startedAt,
|
|
179
|
+
finishedAt: new Date(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Process the async message stream from the Agent SDK.
|
|
185
|
+
* Shared between executeClaudeTask and resumeClaudeTask to avoid duplication.
|
|
186
|
+
*/
|
|
187
|
+
async function processAgentStream(
|
|
188
|
+
taskId: string,
|
|
189
|
+
taskTitle: string,
|
|
190
|
+
response: AsyncIterable<Record<string, unknown>>,
|
|
191
|
+
abortController: AbortController,
|
|
192
|
+
agentProfileId = "general",
|
|
193
|
+
usageState: TaskUsageState
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
let sessionId: string | null = null;
|
|
196
|
+
let receivedResult = false;
|
|
197
|
+
|
|
198
|
+
for await (const raw of response) {
|
|
199
|
+
const message = raw as AgentStreamMessage;
|
|
200
|
+
applyUsageSnapshot(usageState, raw);
|
|
201
|
+
|
|
202
|
+
// Capture session ID from init message
|
|
203
|
+
if (
|
|
204
|
+
message.type === "system" &&
|
|
205
|
+
message.subtype === "init" &&
|
|
206
|
+
message.session_id
|
|
207
|
+
) {
|
|
208
|
+
sessionId = message.session_id;
|
|
209
|
+
await db
|
|
210
|
+
.update(tasks)
|
|
211
|
+
.set({ sessionId, updatedAt: new Date() })
|
|
212
|
+
.where(eq(tasks.id, taskId));
|
|
213
|
+
|
|
214
|
+
// Capture auth source from init message
|
|
215
|
+
if (message.api_key_source) {
|
|
216
|
+
updateAuthStatus(message.api_key_source as "db" | "env" | "oauth" | "unknown");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Update execution manager with sessionId
|
|
220
|
+
setExecution(taskId, {
|
|
221
|
+
abortController,
|
|
222
|
+
sessionId,
|
|
223
|
+
taskId,
|
|
224
|
+
startedAt: new Date(),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Log meaningful stream events
|
|
229
|
+
if (message.type === "stream_event" && message.event) {
|
|
230
|
+
const event = message.event;
|
|
231
|
+
const eventType = event.type as string;
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
eventType === "content_block_start" ||
|
|
235
|
+
eventType === "content_block_delta" ||
|
|
236
|
+
eventType === "message_start"
|
|
237
|
+
) {
|
|
238
|
+
await db.insert(agentLogs).values({
|
|
239
|
+
id: crypto.randomUUID(),
|
|
240
|
+
taskId,
|
|
241
|
+
agentType: agentProfileId,
|
|
242
|
+
event: eventType,
|
|
243
|
+
payload: JSON.stringify(event),
|
|
244
|
+
timestamp: new Date(),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle assistant messages (tool use starts)
|
|
250
|
+
if (message.type === "assistant" && message.message?.content) {
|
|
251
|
+
for (const block of message.message.content) {
|
|
252
|
+
if (block.type === "tool_use") {
|
|
253
|
+
await db.insert(agentLogs).values({
|
|
254
|
+
id: crypto.randomUUID(),
|
|
255
|
+
taskId,
|
|
256
|
+
agentType: agentProfileId,
|
|
257
|
+
event: "tool_start",
|
|
258
|
+
payload: JSON.stringify({
|
|
259
|
+
tool: block.name,
|
|
260
|
+
input: block.input,
|
|
261
|
+
}),
|
|
262
|
+
timestamp: new Date(),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle result — skip if task was cancelled mid-stream
|
|
269
|
+
if (message.type === "result" && "result" in raw) {
|
|
270
|
+
if (abortController.signal.aborted) {
|
|
271
|
+
await finalizeTaskUsage(usageState, "cancelled");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
receivedResult = true;
|
|
275
|
+
const resultText =
|
|
276
|
+
typeof message.result === "string"
|
|
277
|
+
? message.result
|
|
278
|
+
: JSON.stringify(message.result);
|
|
279
|
+
|
|
280
|
+
await db
|
|
281
|
+
.update(tasks)
|
|
282
|
+
.set({
|
|
283
|
+
status: "completed",
|
|
284
|
+
result: resultText,
|
|
285
|
+
updatedAt: new Date(),
|
|
286
|
+
})
|
|
287
|
+
.where(eq(tasks.id, taskId));
|
|
288
|
+
|
|
289
|
+
await db.insert(notifications).values({
|
|
290
|
+
id: crypto.randomUUID(),
|
|
291
|
+
taskId,
|
|
292
|
+
type: "task_completed",
|
|
293
|
+
title: `Task completed: ${taskTitle}`,
|
|
294
|
+
body: resultText.slice(0, 500),
|
|
295
|
+
createdAt: new Date(),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await db.insert(agentLogs).values({
|
|
299
|
+
id: crypto.randomUUID(),
|
|
300
|
+
taskId,
|
|
301
|
+
agentType: agentProfileId,
|
|
302
|
+
event: "completed",
|
|
303
|
+
payload: JSON.stringify({ result: resultText.slice(0, 1000) }),
|
|
304
|
+
timestamp: new Date(),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await scanTaskOutputDocuments(taskId);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
await db.insert(agentLogs).values({
|
|
311
|
+
id: crypto.randomUUID(),
|
|
312
|
+
taskId,
|
|
313
|
+
agentType: agentProfileId,
|
|
314
|
+
event: "output_scan_failed",
|
|
315
|
+
payload: JSON.stringify({
|
|
316
|
+
error: error instanceof Error ? error.message : String(error),
|
|
317
|
+
}),
|
|
318
|
+
timestamp: new Date(),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
await finalizeTaskUsage(usageState, "completed");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Safety net: if stream ended without a result frame, fail the task
|
|
327
|
+
// instead of leaving it stuck in "running" forever
|
|
328
|
+
if (!receivedResult) {
|
|
329
|
+
await db
|
|
330
|
+
.update(tasks)
|
|
331
|
+
.set({
|
|
332
|
+
status: "failed",
|
|
333
|
+
result: "Agent stream ended without producing a result",
|
|
334
|
+
updatedAt: new Date(),
|
|
335
|
+
})
|
|
336
|
+
.where(eq(tasks.id, taskId));
|
|
337
|
+
|
|
338
|
+
await db.insert(notifications).values({
|
|
339
|
+
id: crypto.randomUUID(),
|
|
340
|
+
taskId,
|
|
341
|
+
type: "task_failed",
|
|
342
|
+
title: `Task failed: ${taskTitle}`,
|
|
343
|
+
body: "Agent stream ended unexpectedly without a result",
|
|
344
|
+
createdAt: new Date(),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await finalizeTaskUsage(usageState, "failed");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function executeClaudeTask(taskId: string): Promise<void> {
|
|
352
|
+
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
|
|
353
|
+
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
354
|
+
const usageState = createTaskUsageState(task);
|
|
355
|
+
|
|
356
|
+
const abortController = new AbortController();
|
|
357
|
+
|
|
358
|
+
setExecution(taskId, {
|
|
359
|
+
abortController,
|
|
360
|
+
sessionId: null,
|
|
361
|
+
taskId,
|
|
362
|
+
startedAt: new Date(),
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
await prepareTaskOutputDirectory(taskId, { clearExisting: true });
|
|
367
|
+
const profile = getProfile(task.agentProfile ?? "general");
|
|
368
|
+
const payload = profile
|
|
369
|
+
? resolveProfileRuntimePayload(profile, "claude-code")
|
|
370
|
+
: null;
|
|
371
|
+
if (payload && !payload.supported) {
|
|
372
|
+
throw new Error(payload.reason ?? `Profile "${profile?.name}" is not supported on Claude Code`);
|
|
373
|
+
}
|
|
374
|
+
const systemPrompt = payload?.instructions ?? "";
|
|
375
|
+
const basePrompt = task.description || task.title;
|
|
376
|
+
const docContext = await buildDocumentContext(taskId);
|
|
377
|
+
const outputInstructions = buildTaskOutputInstructions(taskId);
|
|
378
|
+
const prompt = [systemPrompt, docContext, outputInstructions, basePrompt]
|
|
379
|
+
.filter(Boolean)
|
|
380
|
+
.join("\n\n");
|
|
381
|
+
|
|
382
|
+
// Resolve working directory: project's workingDirectory > process.cwd()
|
|
383
|
+
let cwd = process.cwd();
|
|
384
|
+
if (task.projectId) {
|
|
385
|
+
const [project] = await db
|
|
386
|
+
.select({ workingDirectory: projects.workingDirectory })
|
|
387
|
+
.from(projects)
|
|
388
|
+
.where(eq(projects.id, task.projectId));
|
|
389
|
+
if (project?.workingDirectory) {
|
|
390
|
+
cwd = project.workingDirectory;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const policyForTask = payload?.canUseToolPolicy;
|
|
395
|
+
const authEnv = await getAuthEnv();
|
|
396
|
+
const response = query({
|
|
397
|
+
prompt,
|
|
398
|
+
options: {
|
|
399
|
+
abortController,
|
|
400
|
+
includePartialMessages: true,
|
|
401
|
+
cwd,
|
|
402
|
+
env: buildClaudeSdkEnv(authEnv),
|
|
403
|
+
...(payload?.allowedTools && { allowedTools: payload.allowedTools }),
|
|
404
|
+
...(payload?.mcpServers &&
|
|
405
|
+
Object.keys(payload.mcpServers).length > 0 && {
|
|
406
|
+
mcpServers: payload.mcpServers,
|
|
407
|
+
}),
|
|
408
|
+
// @ts-expect-error Agent SDK canUseTool types are incomplete — our async handler is compatible at runtime
|
|
409
|
+
canUseTool: async (
|
|
410
|
+
toolName: string,
|
|
411
|
+
input: Record<string, unknown>
|
|
412
|
+
) => {
|
|
413
|
+
return handleToolPermission(taskId, toolName, input, policyForTask);
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await processAgentStream(
|
|
419
|
+
taskId,
|
|
420
|
+
task.title,
|
|
421
|
+
response as AsyncIterable<Record<string, unknown>>,
|
|
422
|
+
abortController,
|
|
423
|
+
task.agentProfile ?? "general",
|
|
424
|
+
usageState
|
|
425
|
+
);
|
|
426
|
+
} catch (error: unknown) {
|
|
427
|
+
await handleExecutionError(
|
|
428
|
+
taskId,
|
|
429
|
+
task.title,
|
|
430
|
+
error,
|
|
431
|
+
abortController,
|
|
432
|
+
task.agentProfile ?? "general",
|
|
433
|
+
usageState
|
|
434
|
+
);
|
|
435
|
+
} finally {
|
|
436
|
+
clearPermissionCache(taskId);
|
|
437
|
+
removeExecution(taskId);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function resumeClaudeTask(taskId: string): Promise<void> {
|
|
442
|
+
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
|
|
443
|
+
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
444
|
+
const usageState = createTaskUsageState(task, true);
|
|
445
|
+
|
|
446
|
+
if (!task.sessionId) {
|
|
447
|
+
throw new Error("No session to resume — use Retry instead");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (task.resumeCount >= MAX_RESUME_COUNT) {
|
|
451
|
+
throw new Error("Resume limit reached. Re-queue for fresh start.");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Increment resume count
|
|
455
|
+
await db
|
|
456
|
+
.update(tasks)
|
|
457
|
+
.set({ resumeCount: task.resumeCount + 1, updatedAt: new Date() })
|
|
458
|
+
.where(eq(tasks.id, taskId));
|
|
459
|
+
|
|
460
|
+
const abortController = new AbortController();
|
|
461
|
+
|
|
462
|
+
setExecution(taskId, {
|
|
463
|
+
abortController,
|
|
464
|
+
sessionId: task.sessionId,
|
|
465
|
+
taskId,
|
|
466
|
+
startedAt: new Date(),
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const profileId = task.agentProfile ?? "general";
|
|
470
|
+
|
|
471
|
+
await db.insert(agentLogs).values({
|
|
472
|
+
id: crypto.randomUUID(),
|
|
473
|
+
taskId,
|
|
474
|
+
agentType: profileId,
|
|
475
|
+
event: "session_resumed",
|
|
476
|
+
payload: JSON.stringify({
|
|
477
|
+
sessionId: task.sessionId,
|
|
478
|
+
resumeCount: task.resumeCount + 1,
|
|
479
|
+
profile: profileId,
|
|
480
|
+
}),
|
|
481
|
+
timestamp: new Date(),
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
await prepareTaskOutputDirectory(taskId);
|
|
486
|
+
const profile = getProfile(profileId);
|
|
487
|
+
const payload = profile
|
|
488
|
+
? resolveProfileRuntimePayload(profile, "claude-code")
|
|
489
|
+
: null;
|
|
490
|
+
if (payload && !payload.supported) {
|
|
491
|
+
throw new Error(payload.reason ?? `Profile "${profile?.name}" is not supported on Claude Code`);
|
|
492
|
+
}
|
|
493
|
+
const systemPrompt = payload?.instructions ?? "";
|
|
494
|
+
const basePrompt = task.description || task.title;
|
|
495
|
+
const docContext = await buildDocumentContext(taskId);
|
|
496
|
+
const outputInstructions = buildTaskOutputInstructions(taskId);
|
|
497
|
+
const prompt = [systemPrompt, docContext, outputInstructions, basePrompt]
|
|
498
|
+
.filter(Boolean)
|
|
499
|
+
.join("\n\n");
|
|
500
|
+
|
|
501
|
+
// Resolve working directory: project's workingDirectory > process.cwd()
|
|
502
|
+
let cwd = process.cwd();
|
|
503
|
+
if (task.projectId) {
|
|
504
|
+
const [project] = await db
|
|
505
|
+
.select({ workingDirectory: projects.workingDirectory })
|
|
506
|
+
.from(projects)
|
|
507
|
+
.where(eq(projects.id, task.projectId));
|
|
508
|
+
if (project?.workingDirectory) {
|
|
509
|
+
cwd = project.workingDirectory;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const policyForResume = payload?.canUseToolPolicy;
|
|
514
|
+
const authEnv = await getAuthEnv();
|
|
515
|
+
const response = query({
|
|
516
|
+
prompt,
|
|
517
|
+
options: {
|
|
518
|
+
resume: task.sessionId,
|
|
519
|
+
abortController,
|
|
520
|
+
includePartialMessages: true,
|
|
521
|
+
cwd,
|
|
522
|
+
env: buildClaudeSdkEnv(authEnv),
|
|
523
|
+
...(payload?.allowedTools && { allowedTools: payload.allowedTools }),
|
|
524
|
+
...(payload?.mcpServers &&
|
|
525
|
+
Object.keys(payload.mcpServers).length > 0 && {
|
|
526
|
+
mcpServers: payload.mcpServers,
|
|
527
|
+
}),
|
|
528
|
+
// @ts-expect-error Agent SDK canUseTool types are incomplete — our async handler is compatible at runtime
|
|
529
|
+
canUseTool: async (
|
|
530
|
+
toolName: string,
|
|
531
|
+
input: Record<string, unknown>
|
|
532
|
+
) => {
|
|
533
|
+
return handleToolPermission(taskId, toolName, input, policyForResume);
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
await processAgentStream(
|
|
539
|
+
taskId,
|
|
540
|
+
task.title,
|
|
541
|
+
response as AsyncIterable<Record<string, unknown>>,
|
|
542
|
+
abortController,
|
|
543
|
+
profileId,
|
|
544
|
+
usageState
|
|
545
|
+
);
|
|
546
|
+
} catch (error: unknown) {
|
|
547
|
+
const errorMessage =
|
|
548
|
+
error instanceof Error ? error.message : String(error);
|
|
549
|
+
|
|
550
|
+
// Detect session expiry from the SDK
|
|
551
|
+
if (
|
|
552
|
+
errorMessage.includes("session") &&
|
|
553
|
+
(errorMessage.includes("expired") || errorMessage.includes("not found"))
|
|
554
|
+
) {
|
|
555
|
+
await db
|
|
556
|
+
.update(tasks)
|
|
557
|
+
.set({
|
|
558
|
+
status: "failed",
|
|
559
|
+
result: "Session expired — re-queue for fresh start",
|
|
560
|
+
sessionId: null,
|
|
561
|
+
updatedAt: new Date(),
|
|
562
|
+
})
|
|
563
|
+
.where(eq(tasks.id, taskId));
|
|
564
|
+
|
|
565
|
+
await db.insert(notifications).values({
|
|
566
|
+
id: crypto.randomUUID(),
|
|
567
|
+
taskId,
|
|
568
|
+
type: "task_failed",
|
|
569
|
+
title: `Session expired: ${task.title}`,
|
|
570
|
+
body: "The agent session has expired. Re-queue this task for a fresh start.",
|
|
571
|
+
createdAt: new Date(),
|
|
572
|
+
});
|
|
573
|
+
await finalizeTaskUsage(usageState, "failed");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
await handleExecutionError(
|
|
578
|
+
taskId,
|
|
579
|
+
task.title,
|
|
580
|
+
error,
|
|
581
|
+
abortController,
|
|
582
|
+
profileId,
|
|
583
|
+
usageState
|
|
584
|
+
);
|
|
585
|
+
} finally {
|
|
586
|
+
clearPermissionCache(taskId);
|
|
587
|
+
removeExecution(taskId);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Shared error handler for both execute and resume paths.
|
|
593
|
+
*/
|
|
594
|
+
async function handleExecutionError(
|
|
595
|
+
taskId: string,
|
|
596
|
+
taskTitle: string,
|
|
597
|
+
error: unknown,
|
|
598
|
+
abortController: AbortController,
|
|
599
|
+
agentProfileId = "general",
|
|
600
|
+
usageState?: TaskUsageState
|
|
601
|
+
): Promise<void> {
|
|
602
|
+
const errorMessage =
|
|
603
|
+
error instanceof Error ? error.message : String(error);
|
|
604
|
+
|
|
605
|
+
if (abortController.signal.aborted) {
|
|
606
|
+
await db
|
|
607
|
+
.update(tasks)
|
|
608
|
+
.set({ status: "cancelled", updatedAt: new Date() })
|
|
609
|
+
.where(eq(tasks.id, taskId));
|
|
610
|
+
if (usageState) {
|
|
611
|
+
await finalizeTaskUsage(usageState, "cancelled");
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
await db
|
|
617
|
+
.update(tasks)
|
|
618
|
+
.set({
|
|
619
|
+
status: "failed",
|
|
620
|
+
result: errorMessage,
|
|
621
|
+
updatedAt: new Date(),
|
|
622
|
+
})
|
|
623
|
+
.where(eq(tasks.id, taskId));
|
|
624
|
+
|
|
625
|
+
await db.insert(notifications).values({
|
|
626
|
+
id: crypto.randomUUID(),
|
|
627
|
+
taskId,
|
|
628
|
+
type: "task_failed",
|
|
629
|
+
title: `Task failed: ${taskTitle}`,
|
|
630
|
+
body: errorMessage.slice(0, 500),
|
|
631
|
+
createdAt: new Date(),
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
await db.insert(agentLogs).values({
|
|
635
|
+
id: crypto.randomUUID(),
|
|
636
|
+
taskId,
|
|
637
|
+
agentType: agentProfileId,
|
|
638
|
+
event: "error",
|
|
639
|
+
payload: JSON.stringify({ error: errorMessage }),
|
|
640
|
+
timestamp: new Date(),
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
if (usageState) {
|
|
644
|
+
await finalizeTaskUsage(usageState, "failed");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Handle tool permission by inserting a notification and polling for response.
|
|
650
|
+
* Uses database polling pattern — the Inbox UI writes the response.
|
|
651
|
+
*/
|
|
652
|
+
async function handleToolPermission(
|
|
653
|
+
taskId: string,
|
|
654
|
+
toolName: string,
|
|
655
|
+
input: Record<string, unknown>,
|
|
656
|
+
canUseToolPolicy?: CanUseToolPolicy
|
|
657
|
+
): Promise<ToolPermissionResponse> {
|
|
658
|
+
const isQuestion = toolName === "AskUserQuestion";
|
|
659
|
+
|
|
660
|
+
// Layer 1: Profile-level canUseToolPolicy — fastest check, no I/O
|
|
661
|
+
if (!isQuestion && canUseToolPolicy) {
|
|
662
|
+
if (canUseToolPolicy.autoApprove?.includes(toolName)) {
|
|
663
|
+
return buildAllowedToolPermissionResponse(input);
|
|
664
|
+
}
|
|
665
|
+
if (canUseToolPolicy.autoDeny?.includes(toolName)) {
|
|
666
|
+
return { behavior: "deny", message: `Profile policy denies ${toolName}` };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Layer 2: Saved user permissions — skip notification for pre-approved tools
|
|
671
|
+
if (!isQuestion) {
|
|
672
|
+
const { isToolAllowed } = await import("@/lib/settings/permissions");
|
|
673
|
+
if (await isToolAllowed(toolName, input)) {
|
|
674
|
+
return buildAllowedToolPermissionResponse(input);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!isQuestion) {
|
|
679
|
+
const cacheKey = buildPermissionCacheKey(taskId, toolName, input);
|
|
680
|
+
const settledResponse = settledPermissionRequests.get(cacheKey);
|
|
681
|
+
if (settledResponse) {
|
|
682
|
+
return normalizeToolPermissionResponse(settledResponse, input);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const pendingRequest = inFlightPermissionRequests.get(cacheKey);
|
|
686
|
+
if (pendingRequest) {
|
|
687
|
+
return pendingRequest;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const requestPromise = (async () => {
|
|
691
|
+
const notificationId = crypto.randomUUID();
|
|
692
|
+
|
|
693
|
+
await db.insert(notifications).values({
|
|
694
|
+
id: notificationId,
|
|
695
|
+
taskId,
|
|
696
|
+
type: "permission_required",
|
|
697
|
+
title: `Permission required: ${toolName}`,
|
|
698
|
+
body: JSON.stringify(input).slice(0, 1000),
|
|
699
|
+
toolName,
|
|
700
|
+
toolInput: JSON.stringify(input),
|
|
701
|
+
createdAt: new Date(),
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
const response = normalizeToolPermissionResponse(
|
|
705
|
+
await waitForToolPermissionResponse(notificationId),
|
|
706
|
+
input
|
|
707
|
+
);
|
|
708
|
+
settledPermissionRequests.set(cacheKey, response);
|
|
709
|
+
return response;
|
|
710
|
+
})();
|
|
711
|
+
|
|
712
|
+
inFlightPermissionRequests.set(cacheKey, requestPromise);
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
return await requestPromise;
|
|
716
|
+
} finally {
|
|
717
|
+
inFlightPermissionRequests.delete(cacheKey);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const notificationId = crypto.randomUUID();
|
|
722
|
+
|
|
723
|
+
await db.insert(notifications).values({
|
|
724
|
+
id: notificationId,
|
|
725
|
+
taskId,
|
|
726
|
+
type: isQuestion ? "agent_message" : "permission_required",
|
|
727
|
+
title: isQuestion
|
|
728
|
+
? "Agent has a question"
|
|
729
|
+
: `Permission required: ${toolName}`,
|
|
730
|
+
body: JSON.stringify(input).slice(0, 1000),
|
|
731
|
+
toolName,
|
|
732
|
+
toolInput: JSON.stringify(input),
|
|
733
|
+
createdAt: new Date(),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
return waitForToolPermissionResponse(notificationId);
|
|
737
|
+
}
|