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,1089 @@
|
|
|
1
|
+
import { eq, sql } from "drizzle-orm";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { agentLogs, notifications, projects, tasks } from "@/lib/db/schema";
|
|
4
|
+
import {
|
|
5
|
+
getExecution,
|
|
6
|
+
removeExecution,
|
|
7
|
+
setExecution,
|
|
8
|
+
} from "@/lib/agents/execution-manager";
|
|
9
|
+
import { getProfile } from "@/lib/agents/profiles/registry";
|
|
10
|
+
import { resolveProfileRuntimePayload } from "@/lib/agents/profiles/compatibility";
|
|
11
|
+
import { buildDocumentContext } from "@/lib/documents/context-builder";
|
|
12
|
+
import {
|
|
13
|
+
buildTaskOutputInstructions,
|
|
14
|
+
prepareTaskOutputDirectory,
|
|
15
|
+
scanTaskOutputDocuments,
|
|
16
|
+
} from "@/lib/documents/output-scanner";
|
|
17
|
+
import {
|
|
18
|
+
getOpenAIApiKey,
|
|
19
|
+
updateOpenAIAuthStatus,
|
|
20
|
+
} from "@/lib/settings/openai-auth";
|
|
21
|
+
import { isToolAllowed } from "@/lib/settings/permissions";
|
|
22
|
+
import { getRuntimeCatalogEntry } from "./catalog";
|
|
23
|
+
import { CodexAppServerClient } from "./codex-app-server-client";
|
|
24
|
+
import type {
|
|
25
|
+
AgentRuntimeAdapter,
|
|
26
|
+
RuntimeConnectionResult,
|
|
27
|
+
TaskAssistInput,
|
|
28
|
+
} from "./types";
|
|
29
|
+
import type { TaskAssistResponse } from "./task-assist-types";
|
|
30
|
+
import {
|
|
31
|
+
extractUsageSnapshot,
|
|
32
|
+
mergeUsageSnapshot,
|
|
33
|
+
recordUsageLedgerEntry,
|
|
34
|
+
resolveUsageActivityType,
|
|
35
|
+
type UsageActivityType,
|
|
36
|
+
type UsageSnapshot,
|
|
37
|
+
} from "@/lib/usage/ledger";
|
|
38
|
+
|
|
39
|
+
interface JsonRpcLikeRequest {
|
|
40
|
+
id: number;
|
|
41
|
+
method: string;
|
|
42
|
+
params?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface JsonRpcLikeNotification {
|
|
46
|
+
method: string;
|
|
47
|
+
params?: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface TaskExecutionContext {
|
|
51
|
+
task: typeof tasks.$inferSelect;
|
|
52
|
+
profileId: string;
|
|
53
|
+
instructions: string;
|
|
54
|
+
prompt: string;
|
|
55
|
+
cwd: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface NotificationResponse {
|
|
59
|
+
behavior: "allow" | "deny";
|
|
60
|
+
message?: string;
|
|
61
|
+
updatedInput?: {
|
|
62
|
+
answers?: Record<string, string | string[]>;
|
|
63
|
+
};
|
|
64
|
+
alwaysAllow?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface CodexUserQuestion {
|
|
68
|
+
id: string;
|
|
69
|
+
header: string;
|
|
70
|
+
question: string;
|
|
71
|
+
options: Array<{ label: string; description: string }>;
|
|
72
|
+
multiSelect: boolean;
|
|
73
|
+
isSecret?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface AssistTurnOptions {
|
|
77
|
+
prompt: string;
|
|
78
|
+
developerInstructions: string;
|
|
79
|
+
cwd: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface TaskUsageState extends UsageSnapshot {
|
|
83
|
+
activityType: UsageActivityType;
|
|
84
|
+
startedAt: Date;
|
|
85
|
+
taskId: string;
|
|
86
|
+
projectId?: string | null;
|
|
87
|
+
workflowId?: string | null;
|
|
88
|
+
scheduleId?: string | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const TASK_ASSIST_SYSTEM_PROMPT = `You are an AI task definition assistant.
|
|
92
|
+
Return a single JSON object matching the provided schema.
|
|
93
|
+
Do not wrap the JSON in markdown or code fences.
|
|
94
|
+
Keep the reasoning concise and operational.`;
|
|
95
|
+
|
|
96
|
+
const TASK_ASSIST_OUTPUT_SCHEMA = {
|
|
97
|
+
type: "object",
|
|
98
|
+
additionalProperties: false,
|
|
99
|
+
required: [
|
|
100
|
+
"improvedDescription",
|
|
101
|
+
"breakdown",
|
|
102
|
+
"recommendedPattern",
|
|
103
|
+
"complexity",
|
|
104
|
+
"needsCheckpoint",
|
|
105
|
+
"reasoning",
|
|
106
|
+
],
|
|
107
|
+
properties: {
|
|
108
|
+
improvedDescription: { type: "string" },
|
|
109
|
+
breakdown: {
|
|
110
|
+
type: "array",
|
|
111
|
+
items: {
|
|
112
|
+
type: "object",
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
required: ["title", "description"],
|
|
115
|
+
properties: {
|
|
116
|
+
title: { type: "string" },
|
|
117
|
+
description: { type: "string" },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
recommendedPattern: {
|
|
122
|
+
type: "string",
|
|
123
|
+
enum: ["single", "sequence", "planner-executor", "checkpoint"],
|
|
124
|
+
},
|
|
125
|
+
complexity: {
|
|
126
|
+
type: "string",
|
|
127
|
+
enum: ["simple", "moderate", "complex"],
|
|
128
|
+
},
|
|
129
|
+
needsCheckpoint: { type: "boolean" },
|
|
130
|
+
reasoning: { type: "string" },
|
|
131
|
+
},
|
|
132
|
+
} as const;
|
|
133
|
+
|
|
134
|
+
async function resolveTaskExecutionContext(
|
|
135
|
+
taskId: string,
|
|
136
|
+
options: { resume?: boolean } = {}
|
|
137
|
+
): Promise<TaskExecutionContext> {
|
|
138
|
+
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
|
|
139
|
+
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
140
|
+
|
|
141
|
+
const profileId = task.agentProfile ?? "general";
|
|
142
|
+
const profile = getProfile(profileId);
|
|
143
|
+
const payload = profile
|
|
144
|
+
? resolveProfileRuntimePayload(profile, "openai-codex-app-server")
|
|
145
|
+
: null;
|
|
146
|
+
if (payload && !payload.supported) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
payload.reason ??
|
|
149
|
+
`Profile "${profile?.name}" is not supported on OpenAI Codex App Server`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const docContext = await buildDocumentContext(taskId);
|
|
153
|
+
await prepareTaskOutputDirectory(taskId, {
|
|
154
|
+
clearExisting: !options.resume,
|
|
155
|
+
});
|
|
156
|
+
const outputInstructions = buildTaskOutputInstructions(taskId);
|
|
157
|
+
const prompt = [docContext, outputInstructions, task.description || task.title]
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.join("\n\n");
|
|
160
|
+
|
|
161
|
+
let cwd = process.cwd();
|
|
162
|
+
if (task.projectId) {
|
|
163
|
+
const [project] = await db
|
|
164
|
+
.select({ workingDirectory: projects.workingDirectory })
|
|
165
|
+
.from(projects)
|
|
166
|
+
.where(eq(projects.id, task.projectId));
|
|
167
|
+
|
|
168
|
+
if (project?.workingDirectory) {
|
|
169
|
+
cwd = project.workingDirectory;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
task,
|
|
175
|
+
profileId,
|
|
176
|
+
instructions: payload?.instructions ?? "",
|
|
177
|
+
prompt,
|
|
178
|
+
cwd,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function createTaskUsageState(
|
|
183
|
+
task: {
|
|
184
|
+
id: string;
|
|
185
|
+
projectId?: string | null;
|
|
186
|
+
workflowId?: string | null;
|
|
187
|
+
scheduleId?: string | null;
|
|
188
|
+
},
|
|
189
|
+
isResume = false
|
|
190
|
+
): TaskUsageState {
|
|
191
|
+
return {
|
|
192
|
+
taskId: task.id,
|
|
193
|
+
projectId: task.projectId ?? null,
|
|
194
|
+
workflowId: task.workflowId ?? null,
|
|
195
|
+
scheduleId: task.scheduleId ?? null,
|
|
196
|
+
activityType: resolveUsageActivityType({
|
|
197
|
+
workflowId: task.workflowId,
|
|
198
|
+
scheduleId: task.scheduleId,
|
|
199
|
+
isResume,
|
|
200
|
+
}),
|
|
201
|
+
startedAt: new Date(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function applyUsageSnapshot(state: UsageSnapshot, source: unknown) {
|
|
206
|
+
Object.assign(state, mergeUsageSnapshot(state, extractUsageSnapshot(source)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function finalizeTaskUsage(
|
|
210
|
+
state: TaskUsageState,
|
|
211
|
+
status: "completed" | "failed" | "cancelled"
|
|
212
|
+
) {
|
|
213
|
+
await recordUsageLedgerEntry({
|
|
214
|
+
taskId: state.taskId,
|
|
215
|
+
workflowId: state.workflowId ?? null,
|
|
216
|
+
scheduleId: state.scheduleId ?? null,
|
|
217
|
+
projectId: state.projectId ?? null,
|
|
218
|
+
activityType: state.activityType,
|
|
219
|
+
runtimeId: "openai-codex-app-server",
|
|
220
|
+
providerId: "openai",
|
|
221
|
+
modelId: state.modelId ?? null,
|
|
222
|
+
inputTokens: state.inputTokens ?? null,
|
|
223
|
+
outputTokens: state.outputTokens ?? null,
|
|
224
|
+
totalTokens: state.totalTokens ?? null,
|
|
225
|
+
status,
|
|
226
|
+
startedAt: state.startedAt,
|
|
227
|
+
finishedAt: new Date(),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function buildTurnInput(prompt: string) {
|
|
232
|
+
return [
|
|
233
|
+
{
|
|
234
|
+
type: "text" as const,
|
|
235
|
+
text: prompt,
|
|
236
|
+
text_elements: [],
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
242
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
243
|
+
? (value as Record<string, unknown>)
|
|
244
|
+
: null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function asString(value: unknown): string | null {
|
|
248
|
+
return typeof value === "string" ? value : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseQuestions(value: unknown): CodexUserQuestion[] {
|
|
252
|
+
if (!Array.isArray(value)) return [];
|
|
253
|
+
|
|
254
|
+
const questions: CodexUserQuestion[] = [];
|
|
255
|
+
|
|
256
|
+
value.forEach((entry, index) => {
|
|
257
|
+
const question = asRecord(entry);
|
|
258
|
+
if (!question) return;
|
|
259
|
+
|
|
260
|
+
const id = asString(question.id) ?? `question-${index + 1}`;
|
|
261
|
+
const header = asString(question.header) ?? "Question";
|
|
262
|
+
const text = asString(question.question);
|
|
263
|
+
if (!text) return;
|
|
264
|
+
|
|
265
|
+
const optionsValue = Array.isArray(question.options) ? question.options : [];
|
|
266
|
+
const options = optionsValue
|
|
267
|
+
.map((option) => {
|
|
268
|
+
const parsed = asRecord(option);
|
|
269
|
+
if (!parsed) return null;
|
|
270
|
+
const label = asString(parsed.label);
|
|
271
|
+
if (!label) return null;
|
|
272
|
+
return {
|
|
273
|
+
label,
|
|
274
|
+
description: asString(parsed.description) ?? "",
|
|
275
|
+
};
|
|
276
|
+
})
|
|
277
|
+
.filter(
|
|
278
|
+
(option): option is { label: string; description: string } =>
|
|
279
|
+
option !== null
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
questions.push({
|
|
283
|
+
id,
|
|
284
|
+
header,
|
|
285
|
+
question: text,
|
|
286
|
+
options,
|
|
287
|
+
multiSelect: false,
|
|
288
|
+
isSecret: Boolean(question.isSecret),
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return questions;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function extractThreadId(params: Record<string, unknown>): string | null {
|
|
296
|
+
const thread = asRecord(params.thread);
|
|
297
|
+
return asString(thread?.id) ?? asString(params.threadId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function extractTurnId(params: Record<string, unknown>): string | null {
|
|
301
|
+
const turn = asRecord(params.turn);
|
|
302
|
+
return asString(turn?.id) ?? asString(params.turnId);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function extractTurnStatus(params: Record<string, unknown>) {
|
|
306
|
+
const turn = asRecord(params.turn);
|
|
307
|
+
const error = asRecord(turn?.error);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
status: asString(turn?.status),
|
|
311
|
+
errorMessage: asString(error?.message),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function extractJsonObject(text: string): TaskAssistResponse {
|
|
316
|
+
const trimmed = text.trim();
|
|
317
|
+
const jsonMatch = trimmed.match(/\{[\s\S]*\}/);
|
|
318
|
+
if (!jsonMatch) {
|
|
319
|
+
throw new Error("Codex did not return valid JSON for task assist");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return JSON.parse(jsonMatch[0]) as TaskAssistResponse;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function insertLog(taskId: string, event: string, payload: unknown) {
|
|
326
|
+
await db.insert(agentLogs).values({
|
|
327
|
+
id: crypto.randomUUID(),
|
|
328
|
+
taskId,
|
|
329
|
+
agentType: "openai-codex-app-server",
|
|
330
|
+
event,
|
|
331
|
+
payload: JSON.stringify(payload),
|
|
332
|
+
timestamp: new Date(),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function markTaskCompleted(
|
|
337
|
+
taskId: string,
|
|
338
|
+
title: string,
|
|
339
|
+
resultText: string
|
|
340
|
+
) {
|
|
341
|
+
await db
|
|
342
|
+
.update(tasks)
|
|
343
|
+
.set({
|
|
344
|
+
status: "completed",
|
|
345
|
+
result: resultText,
|
|
346
|
+
updatedAt: new Date(),
|
|
347
|
+
})
|
|
348
|
+
.where(eq(tasks.id, taskId));
|
|
349
|
+
|
|
350
|
+
await db.insert(notifications).values({
|
|
351
|
+
id: crypto.randomUUID(),
|
|
352
|
+
taskId,
|
|
353
|
+
type: "task_completed",
|
|
354
|
+
title: `Task completed: ${title}`,
|
|
355
|
+
body: resultText.slice(0, 500),
|
|
356
|
+
createdAt: new Date(),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
await scanTaskOutputDocuments(taskId);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
await insertLog(taskId, "output_scan_failed", {
|
|
363
|
+
error: error instanceof Error ? error.message : String(error),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function markTaskFailed(taskId: string, title: string, message: string) {
|
|
369
|
+
await db
|
|
370
|
+
.update(tasks)
|
|
371
|
+
.set({
|
|
372
|
+
status: "failed",
|
|
373
|
+
result: message,
|
|
374
|
+
updatedAt: new Date(),
|
|
375
|
+
})
|
|
376
|
+
.where(eq(tasks.id, taskId));
|
|
377
|
+
|
|
378
|
+
await db.insert(notifications).values({
|
|
379
|
+
id: crypto.randomUUID(),
|
|
380
|
+
taskId,
|
|
381
|
+
type: "task_failed",
|
|
382
|
+
title: `Task failed: ${title}`,
|
|
383
|
+
body: message.slice(0, 500),
|
|
384
|
+
createdAt: new Date(),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function markTaskCancelled(taskId: string) {
|
|
389
|
+
await db
|
|
390
|
+
.update(tasks)
|
|
391
|
+
.set({
|
|
392
|
+
status: "cancelled",
|
|
393
|
+
updatedAt: new Date(),
|
|
394
|
+
})
|
|
395
|
+
.where(eq(tasks.id, taskId));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function waitForNotificationResponse(
|
|
399
|
+
notificationId: string
|
|
400
|
+
): Promise<NotificationResponse> {
|
|
401
|
+
const deadline = Date.now() + 55_000;
|
|
402
|
+
const pollInterval = 1_500;
|
|
403
|
+
|
|
404
|
+
while (Date.now() < deadline) {
|
|
405
|
+
const [notification] = await db
|
|
406
|
+
.select()
|
|
407
|
+
.from(notifications)
|
|
408
|
+
.where(eq(notifications.id, notificationId));
|
|
409
|
+
|
|
410
|
+
if (notification?.response) {
|
|
411
|
+
try {
|
|
412
|
+
return JSON.parse(notification.response) as NotificationResponse;
|
|
413
|
+
} catch {
|
|
414
|
+
return {
|
|
415
|
+
behavior: "deny",
|
|
416
|
+
message: "Invalid response format",
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
behavior: "deny",
|
|
426
|
+
message: "Request timed out",
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function createApprovalNotification(
|
|
431
|
+
taskId: string,
|
|
432
|
+
title: string,
|
|
433
|
+
toolName: string,
|
|
434
|
+
toolInput: Record<string, unknown>
|
|
435
|
+
) {
|
|
436
|
+
if (await isToolAllowed(toolName, toolInput)) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const notificationId = crypto.randomUUID();
|
|
441
|
+
await db.insert(notifications).values({
|
|
442
|
+
id: notificationId,
|
|
443
|
+
taskId,
|
|
444
|
+
type: "permission_required",
|
|
445
|
+
title,
|
|
446
|
+
body: JSON.stringify(toolInput).slice(0, 1000),
|
|
447
|
+
toolName,
|
|
448
|
+
toolInput: JSON.stringify(toolInput),
|
|
449
|
+
createdAt: new Date(),
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return notificationId;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function createQuestionNotification(
|
|
456
|
+
taskId: string,
|
|
457
|
+
questions: CodexUserQuestion[]
|
|
458
|
+
) {
|
|
459
|
+
const notificationId = crypto.randomUUID();
|
|
460
|
+
|
|
461
|
+
await db.insert(notifications).values({
|
|
462
|
+
id: notificationId,
|
|
463
|
+
taskId,
|
|
464
|
+
type: "agent_message",
|
|
465
|
+
title: "Agent has a question",
|
|
466
|
+
body: questions.map((question) => question.question).join("\n").slice(0, 1000),
|
|
467
|
+
toolName: "AskUserQuestion",
|
|
468
|
+
toolInput: JSON.stringify({ questions }),
|
|
469
|
+
createdAt: new Date(),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return notificationId;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function handleServerRequest(
|
|
476
|
+
client: CodexAppServerClient,
|
|
477
|
+
taskId: string,
|
|
478
|
+
request: JsonRpcLikeRequest
|
|
479
|
+
) {
|
|
480
|
+
const params = asRecord(request.params) ?? {};
|
|
481
|
+
|
|
482
|
+
switch (request.method) {
|
|
483
|
+
case "item/commandExecution/requestApproval": {
|
|
484
|
+
const toolInput = {
|
|
485
|
+
command: asString(params.command),
|
|
486
|
+
cwd: asString(params.cwd),
|
|
487
|
+
reason: asString(params.reason),
|
|
488
|
+
approvalId: asString(params.approvalId),
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const notificationId = await createApprovalNotification(
|
|
492
|
+
taskId,
|
|
493
|
+
"Permission required: Command execution",
|
|
494
|
+
"Bash",
|
|
495
|
+
toolInput
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
if (!notificationId) {
|
|
499
|
+
client.respond(request.id, { decision: "acceptForSession" });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const response = await waitForNotificationResponse(notificationId);
|
|
504
|
+
client.respond(request.id, {
|
|
505
|
+
decision:
|
|
506
|
+
response.behavior === "allow"
|
|
507
|
+
? response.alwaysAllow
|
|
508
|
+
? "acceptForSession"
|
|
509
|
+
: "accept"
|
|
510
|
+
: "decline",
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
case "item/fileChange/requestApproval": {
|
|
516
|
+
const toolInput = {
|
|
517
|
+
reason: asString(params.reason),
|
|
518
|
+
grantRoot: asString(params.grantRoot),
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const notificationId = await createApprovalNotification(
|
|
522
|
+
taskId,
|
|
523
|
+
"Permission required: File change",
|
|
524
|
+
"Write",
|
|
525
|
+
toolInput
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
if (!notificationId) {
|
|
529
|
+
client.respond(request.id, { decision: "acceptForSession" });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const response = await waitForNotificationResponse(notificationId);
|
|
534
|
+
client.respond(request.id, {
|
|
535
|
+
decision:
|
|
536
|
+
response.behavior === "allow"
|
|
537
|
+
? response.alwaysAllow
|
|
538
|
+
? "acceptForSession"
|
|
539
|
+
: "accept"
|
|
540
|
+
: "decline",
|
|
541
|
+
});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
case "item/tool/requestUserInput": {
|
|
546
|
+
const questions = parseQuestions(params.questions);
|
|
547
|
+
const notificationId = await createQuestionNotification(taskId, questions);
|
|
548
|
+
const response = await waitForNotificationResponse(notificationId);
|
|
549
|
+
const answers = response.updatedInput?.answers ?? {};
|
|
550
|
+
|
|
551
|
+
client.respond(request.id, {
|
|
552
|
+
answers: Object.fromEntries(
|
|
553
|
+
Object.entries(answers).map(([questionId, answer]) => [
|
|
554
|
+
questionId,
|
|
555
|
+
{
|
|
556
|
+
answers: Array.isArray(answer)
|
|
557
|
+
? answer.map(String)
|
|
558
|
+
: [String(answer ?? "")],
|
|
559
|
+
},
|
|
560
|
+
])
|
|
561
|
+
),
|
|
562
|
+
});
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
case "item/tool/call": {
|
|
567
|
+
client.respond(request.id, {
|
|
568
|
+
success: false,
|
|
569
|
+
contentItems: [
|
|
570
|
+
{
|
|
571
|
+
type: "inputText",
|
|
572
|
+
text: "Dynamic tool calls are not supported by Stagent's Codex runtime yet.",
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
default:
|
|
580
|
+
client.reject(
|
|
581
|
+
request.id,
|
|
582
|
+
`Unsupported Codex server request: ${request.method}`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function initializeOpenAIClient(
|
|
588
|
+
client: CodexAppServerClient,
|
|
589
|
+
apiKey: string
|
|
590
|
+
) {
|
|
591
|
+
await client.request("initialize", {
|
|
592
|
+
clientInfo: {
|
|
593
|
+
name: "Stagent",
|
|
594
|
+
version: "0.1.0",
|
|
595
|
+
},
|
|
596
|
+
capabilities: null,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
await client.request("account/login/start", {
|
|
600
|
+
type: "apiKey",
|
|
601
|
+
apiKey,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function runAssistTurn({
|
|
606
|
+
prompt,
|
|
607
|
+
developerInstructions,
|
|
608
|
+
cwd,
|
|
609
|
+
}: AssistTurnOptions): Promise<{ text: string; usage: UsageSnapshot }> {
|
|
610
|
+
const { apiKey, source } = await getOpenAIApiKey();
|
|
611
|
+
if (!apiKey) {
|
|
612
|
+
throw new Error("OpenAI API key is not configured");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
let client: CodexAppServerClient | null = null;
|
|
616
|
+
let text = "";
|
|
617
|
+
let usage: UsageSnapshot = {};
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
client = await CodexAppServerClient.connect({
|
|
621
|
+
cwd,
|
|
622
|
+
env: { OPENAI_API_KEY: apiKey },
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
client.onNotification = (notification: JsonRpcLikeNotification) => {
|
|
626
|
+
if (notification.method !== "item/agentMessage/delta") return;
|
|
627
|
+
const delta = asString(asRecord(notification.params)?.delta);
|
|
628
|
+
if (delta) {
|
|
629
|
+
text += delta;
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
await initializeOpenAIClient(client, apiKey);
|
|
634
|
+
await updateOpenAIAuthStatus(source);
|
|
635
|
+
|
|
636
|
+
const threadResponse = (await client.request("thread/start", {
|
|
637
|
+
cwd,
|
|
638
|
+
approvalPolicy: "never",
|
|
639
|
+
sandbox: "workspace-write",
|
|
640
|
+
serviceName: "stagent",
|
|
641
|
+
developerInstructions,
|
|
642
|
+
experimentalRawEvents: false,
|
|
643
|
+
ephemeral: true,
|
|
644
|
+
})) as { thread: { id: string } };
|
|
645
|
+
|
|
646
|
+
const completion = new Promise<void>((resolve, reject) => {
|
|
647
|
+
client!.onNotification = (notification: JsonRpcLikeNotification) => {
|
|
648
|
+
const params = asRecord(notification.params) ?? {};
|
|
649
|
+
applyUsageSnapshot(usage, params);
|
|
650
|
+
|
|
651
|
+
if (notification.method === "item/agentMessage/delta") {
|
|
652
|
+
const delta = asString(params.delta);
|
|
653
|
+
if (delta) {
|
|
654
|
+
text += delta;
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (notification.method === "turn/completed") {
|
|
660
|
+
const { status, errorMessage } = extractTurnStatus(params);
|
|
661
|
+
|
|
662
|
+
if (status === "completed") {
|
|
663
|
+
resolve();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
reject(new Error(errorMessage || `Codex assist turn ended with status ${status}`));
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
await client.request("turn/start", {
|
|
673
|
+
threadId: threadResponse.thread.id,
|
|
674
|
+
input: buildTurnInput(prompt),
|
|
675
|
+
approvalPolicy: "never",
|
|
676
|
+
outputSchema: TASK_ASSIST_OUTPUT_SCHEMA,
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
await completion;
|
|
680
|
+
|
|
681
|
+
return { text: text.trim(), usage };
|
|
682
|
+
} finally {
|
|
683
|
+
if (client) {
|
|
684
|
+
await client.close();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function executeOpenAICodexTask(
|
|
690
|
+
taskId: string,
|
|
691
|
+
options: { resume?: boolean } = {}
|
|
692
|
+
): Promise<void> {
|
|
693
|
+
const { task, profileId, instructions, prompt, cwd } =
|
|
694
|
+
await resolveTaskExecutionContext(taskId, options);
|
|
695
|
+
const { apiKey, source } = await getOpenAIApiKey();
|
|
696
|
+
|
|
697
|
+
if (!apiKey) {
|
|
698
|
+
throw new Error("OpenAI API key is not configured");
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const abortController = new AbortController();
|
|
702
|
+
let client: CodexAppServerClient | null = null;
|
|
703
|
+
let threadId = task.sessionId;
|
|
704
|
+
let turnId: string | null = null;
|
|
705
|
+
let agentOutput = "";
|
|
706
|
+
let settled = false;
|
|
707
|
+
let resolveCompletion: (() => void) | null = null;
|
|
708
|
+
let rejectCompletion: ((error: Error) => void) | null = null;
|
|
709
|
+
const usageState = createTaskUsageState(task, Boolean(task.sessionId));
|
|
710
|
+
|
|
711
|
+
const settle = async (work: () => Promise<void>) => {
|
|
712
|
+
if (settled) return;
|
|
713
|
+
settled = true;
|
|
714
|
+
await work();
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
client = await CodexAppServerClient.connect({
|
|
719
|
+
cwd,
|
|
720
|
+
env: { OPENAI_API_KEY: apiKey },
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
client.onProcessError = (error) => {
|
|
724
|
+
if (settled) return;
|
|
725
|
+
void settle(async () => {
|
|
726
|
+
await markTaskFailed(taskId, task.title, error.message);
|
|
727
|
+
await insertLog(taskId, "failed", { error: error.message });
|
|
728
|
+
await finalizeTaskUsage(usageState, "failed");
|
|
729
|
+
});
|
|
730
|
+
rejectCompletion?.(error);
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
client.onRequest = (request: JsonRpcLikeRequest) => {
|
|
734
|
+
void handleServerRequest(client!, taskId, request).catch((error) => {
|
|
735
|
+
client?.reject(
|
|
736
|
+
request.id,
|
|
737
|
+
error instanceof Error ? error.message : String(error)
|
|
738
|
+
);
|
|
739
|
+
});
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const completion = new Promise<void>((resolve, reject) => {
|
|
743
|
+
resolveCompletion = resolve;
|
|
744
|
+
rejectCompletion = (error: Error) => reject(error);
|
|
745
|
+
client!.onNotification = (notification: JsonRpcLikeNotification) => {
|
|
746
|
+
void (async () => {
|
|
747
|
+
const params = asRecord(notification.params) ?? {};
|
|
748
|
+
applyUsageSnapshot(usageState, params);
|
|
749
|
+
|
|
750
|
+
switch (notification.method) {
|
|
751
|
+
case "thread/started": {
|
|
752
|
+
const startedThreadId = extractThreadId(params);
|
|
753
|
+
if (!startedThreadId) return;
|
|
754
|
+
threadId = startedThreadId;
|
|
755
|
+
await db
|
|
756
|
+
.update(tasks)
|
|
757
|
+
.set({ sessionId: threadId, updatedAt: new Date() })
|
|
758
|
+
.where(eq(tasks.id, taskId));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
case "turn/started": {
|
|
763
|
+
turnId = extractTurnId(params);
|
|
764
|
+
setExecution(taskId, {
|
|
765
|
+
abortController,
|
|
766
|
+
sessionId: threadId,
|
|
767
|
+
taskId,
|
|
768
|
+
startedAt: new Date(),
|
|
769
|
+
interrupt: async () => {
|
|
770
|
+
if (client && threadId && turnId) {
|
|
771
|
+
await client.request("turn/interrupt", { threadId, turnId });
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
cleanup: async () => {
|
|
775
|
+
if (client) {
|
|
776
|
+
await client.close();
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
metadata: {
|
|
780
|
+
runtimeId: "openai-codex-app-server",
|
|
781
|
+
threadId,
|
|
782
|
+
turnId,
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
await insertLog(taskId, "turn_started", { threadId, turnId });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
case "item/agentMessage/delta": {
|
|
790
|
+
const delta = asString(params.delta) ?? "";
|
|
791
|
+
agentOutput += delta;
|
|
792
|
+
await insertLog(taskId, "agent_message_delta", {
|
|
793
|
+
threadId,
|
|
794
|
+
turnId,
|
|
795
|
+
itemId: asString(params.itemId),
|
|
796
|
+
delta,
|
|
797
|
+
});
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
case "item/commandExecution/outputDelta": {
|
|
802
|
+
await insertLog(taskId, "command_output_delta", {
|
|
803
|
+
threadId,
|
|
804
|
+
turnId,
|
|
805
|
+
itemId: asString(params.itemId),
|
|
806
|
+
delta: asString(params.delta),
|
|
807
|
+
});
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
case "item/plan/delta": {
|
|
812
|
+
await insertLog(taskId, "plan_delta", {
|
|
813
|
+
threadId,
|
|
814
|
+
turnId,
|
|
815
|
+
itemId: asString(params.itemId),
|
|
816
|
+
delta: params.delta,
|
|
817
|
+
});
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
case "turn/completed": {
|
|
822
|
+
const { status, errorMessage } = extractTurnStatus(params);
|
|
823
|
+
|
|
824
|
+
if (status === "completed") {
|
|
825
|
+
const finalResult =
|
|
826
|
+
agentOutput.trim() || "Completed without textual output.";
|
|
827
|
+
await settle(async () => {
|
|
828
|
+
await markTaskCompleted(taskId, task.title, finalResult);
|
|
829
|
+
await insertLog(taskId, "completed", {
|
|
830
|
+
threadId,
|
|
831
|
+
turnId,
|
|
832
|
+
result: finalResult.slice(0, 1000),
|
|
833
|
+
profileId,
|
|
834
|
+
});
|
|
835
|
+
await finalizeTaskUsage(usageState, "completed");
|
|
836
|
+
});
|
|
837
|
+
resolveCompletion?.();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (status === "interrupted" || abortController.signal.aborted) {
|
|
842
|
+
await settle(async () => {
|
|
843
|
+
await markTaskCancelled(taskId);
|
|
844
|
+
await insertLog(taskId, "cancelled", { threadId, turnId });
|
|
845
|
+
await finalizeTaskUsage(usageState, "cancelled");
|
|
846
|
+
});
|
|
847
|
+
resolveCompletion?.();
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const message = errorMessage || "Codex turn failed";
|
|
852
|
+
await settle(async () => {
|
|
853
|
+
await markTaskFailed(taskId, task.title, message);
|
|
854
|
+
await insertLog(taskId, "failed", { threadId, turnId, error: message });
|
|
855
|
+
await finalizeTaskUsage(usageState, "failed");
|
|
856
|
+
});
|
|
857
|
+
rejectCompletion?.(new Error(message));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
default:
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
})().catch(reject);
|
|
865
|
+
};
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
await initializeOpenAIClient(client, apiKey);
|
|
869
|
+
await updateOpenAIAuthStatus(source);
|
|
870
|
+
|
|
871
|
+
if (threadId) {
|
|
872
|
+
await client.request("thread/resume", {
|
|
873
|
+
threadId,
|
|
874
|
+
cwd,
|
|
875
|
+
approvalPolicy: "on-request",
|
|
876
|
+
sandbox: "workspace-write",
|
|
877
|
+
developerInstructions: instructions || null,
|
|
878
|
+
});
|
|
879
|
+
} else {
|
|
880
|
+
const threadResponse = (await client.request("thread/start", {
|
|
881
|
+
cwd,
|
|
882
|
+
approvalPolicy: "on-request",
|
|
883
|
+
sandbox: "workspace-write",
|
|
884
|
+
serviceName: "stagent",
|
|
885
|
+
developerInstructions: instructions || null,
|
|
886
|
+
experimentalRawEvents: false,
|
|
887
|
+
ephemeral: false,
|
|
888
|
+
})) as { thread: { id: string } };
|
|
889
|
+
|
|
890
|
+
threadId = threadResponse.thread.id;
|
|
891
|
+
await db
|
|
892
|
+
.update(tasks)
|
|
893
|
+
.set({ sessionId: threadId, updatedAt: new Date() })
|
|
894
|
+
.where(eq(tasks.id, taskId));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
await insertLog(taskId, "thread_started", {
|
|
898
|
+
threadId,
|
|
899
|
+
profileId,
|
|
900
|
+
runtime: "openai-codex-app-server",
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
await client.request("turn/start", {
|
|
904
|
+
threadId,
|
|
905
|
+
input: buildTurnInput(prompt),
|
|
906
|
+
cwd,
|
|
907
|
+
approvalPolicy: "on-request",
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
setExecution(taskId, {
|
|
911
|
+
abortController,
|
|
912
|
+
sessionId: threadId,
|
|
913
|
+
taskId,
|
|
914
|
+
startedAt: new Date(),
|
|
915
|
+
interrupt: async () => {
|
|
916
|
+
if (client && threadId && turnId) {
|
|
917
|
+
await client.request("turn/interrupt", { threadId, turnId });
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
cleanup: async () => {
|
|
921
|
+
if (client) {
|
|
922
|
+
await client.close();
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
metadata: {
|
|
926
|
+
runtimeId: "openai-codex-app-server",
|
|
927
|
+
threadId,
|
|
928
|
+
turnId,
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
await completion;
|
|
933
|
+
} catch (error) {
|
|
934
|
+
if (abortController.signal.aborted || settled) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
939
|
+
await settle(async () => {
|
|
940
|
+
await markTaskFailed(taskId, task.title, message);
|
|
941
|
+
await insertLog(taskId, "failed", { threadId, turnId, error: message });
|
|
942
|
+
await finalizeTaskUsage(usageState, "failed");
|
|
943
|
+
});
|
|
944
|
+
throw error;
|
|
945
|
+
} finally {
|
|
946
|
+
if (client) {
|
|
947
|
+
await client.close();
|
|
948
|
+
}
|
|
949
|
+
removeExecution(taskId);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function resumeOpenAICodexTask(taskId: string) {
|
|
954
|
+
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
|
|
955
|
+
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
956
|
+
if (!task.sessionId) {
|
|
957
|
+
throw new Error("No session to resume — use Retry instead");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
await db
|
|
961
|
+
.update(tasks)
|
|
962
|
+
.set({
|
|
963
|
+
resumeCount: sql`${tasks.resumeCount} + 1`,
|
|
964
|
+
updatedAt: new Date(),
|
|
965
|
+
})
|
|
966
|
+
.where(eq(tasks.id, taskId));
|
|
967
|
+
|
|
968
|
+
await executeOpenAICodexTask(taskId, { resume: true });
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function cancelOpenAICodexTask(taskId: string) {
|
|
972
|
+
const execution = getExecution(taskId);
|
|
973
|
+
execution?.abortController.abort();
|
|
974
|
+
|
|
975
|
+
if (execution?.interrupt) {
|
|
976
|
+
await execution.interrupt();
|
|
977
|
+
}
|
|
978
|
+
if (execution?.cleanup) {
|
|
979
|
+
await execution.cleanup();
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
removeExecution(taskId);
|
|
983
|
+
await markTaskCancelled(taskId);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function runOpenAITaskAssist(
|
|
987
|
+
input: TaskAssistInput
|
|
988
|
+
): Promise<TaskAssistResponse> {
|
|
989
|
+
const prompt = [
|
|
990
|
+
input.title ? `Task title: ${input.title}` : "",
|
|
991
|
+
input.description ? `Description: ${input.description}` : "",
|
|
992
|
+
]
|
|
993
|
+
.filter(Boolean)
|
|
994
|
+
.join("\n");
|
|
995
|
+
|
|
996
|
+
const startedAt = new Date();
|
|
997
|
+
let usage: UsageSnapshot = {};
|
|
998
|
+
|
|
999
|
+
try {
|
|
1000
|
+
const result = await runAssistTurn({
|
|
1001
|
+
prompt,
|
|
1002
|
+
developerInstructions: TASK_ASSIST_SYSTEM_PROMPT,
|
|
1003
|
+
cwd: process.cwd(),
|
|
1004
|
+
});
|
|
1005
|
+
usage = result.usage;
|
|
1006
|
+
const parsed = extractJsonObject(result.text);
|
|
1007
|
+
|
|
1008
|
+
await recordUsageLedgerEntry({
|
|
1009
|
+
activityType: "task_assist",
|
|
1010
|
+
runtimeId: "openai-codex-app-server",
|
|
1011
|
+
providerId: "openai",
|
|
1012
|
+
modelId: usage.modelId ?? null,
|
|
1013
|
+
inputTokens: usage.inputTokens ?? null,
|
|
1014
|
+
outputTokens: usage.outputTokens ?? null,
|
|
1015
|
+
totalTokens: usage.totalTokens ?? null,
|
|
1016
|
+
status: "completed",
|
|
1017
|
+
startedAt,
|
|
1018
|
+
finishedAt: new Date(),
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
return parsed;
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
await recordUsageLedgerEntry({
|
|
1024
|
+
activityType: "task_assist",
|
|
1025
|
+
runtimeId: "openai-codex-app-server",
|
|
1026
|
+
providerId: "openai",
|
|
1027
|
+
modelId: usage.modelId ?? null,
|
|
1028
|
+
inputTokens: usage.inputTokens ?? null,
|
|
1029
|
+
outputTokens: usage.outputTokens ?? null,
|
|
1030
|
+
totalTokens: usage.totalTokens ?? null,
|
|
1031
|
+
status: "failed",
|
|
1032
|
+
startedAt,
|
|
1033
|
+
finishedAt: new Date(),
|
|
1034
|
+
});
|
|
1035
|
+
throw error;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async function testOpenAIConnection(): Promise<RuntimeConnectionResult> {
|
|
1040
|
+
const { apiKey, source } = await getOpenAIApiKey();
|
|
1041
|
+
if (!apiKey) {
|
|
1042
|
+
return {
|
|
1043
|
+
connected: false,
|
|
1044
|
+
apiKeySource: "unknown",
|
|
1045
|
+
error: "OpenAI API key is not configured",
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
let client: CodexAppServerClient | null = null;
|
|
1050
|
+
try {
|
|
1051
|
+
client = await CodexAppServerClient.connect({
|
|
1052
|
+
cwd: process.cwd(),
|
|
1053
|
+
env: { OPENAI_API_KEY: apiKey },
|
|
1054
|
+
});
|
|
1055
|
+
await initializeOpenAIClient(client, apiKey);
|
|
1056
|
+
await client.request("account/read", { refreshToken: false });
|
|
1057
|
+
await updateOpenAIAuthStatus(source);
|
|
1058
|
+
return { connected: true, apiKeySource: source };
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
return {
|
|
1061
|
+
connected: false,
|
|
1062
|
+
apiKeySource: source,
|
|
1063
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1064
|
+
};
|
|
1065
|
+
} finally {
|
|
1066
|
+
if (client) {
|
|
1067
|
+
await client.close();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export const openAICodexRuntimeAdapter: AgentRuntimeAdapter = {
|
|
1073
|
+
metadata: getRuntimeCatalogEntry("openai-codex-app-server"),
|
|
1074
|
+
executeTask(taskId: string) {
|
|
1075
|
+
return executeOpenAICodexTask(taskId);
|
|
1076
|
+
},
|
|
1077
|
+
resumeTask(taskId: string) {
|
|
1078
|
+
return resumeOpenAICodexTask(taskId);
|
|
1079
|
+
},
|
|
1080
|
+
cancelTask(taskId: string) {
|
|
1081
|
+
return cancelOpenAICodexTask(taskId);
|
|
1082
|
+
},
|
|
1083
|
+
runTaskAssist(input: TaskAssistInput) {
|
|
1084
|
+
return runOpenAITaskAssist(input);
|
|
1085
|
+
},
|
|
1086
|
+
testConnection() {
|
|
1087
|
+
return testOpenAIConnection();
|
|
1088
|
+
},
|
|
1089
|
+
};
|