stagent 0.5.0 → 0.6.1
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/README.md +8 -8
- package/dist/cli.js +146 -2
- package/docs/.coverage-gaps.json +21 -0
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +36 -14
- package/docs/features/chat.md +33 -56
- package/docs/features/cost-usage.md +14 -10
- package/docs/features/dashboard-kanban.md +30 -13
- package/docs/features/delivery-channels.md +198 -0
- package/docs/features/design-system.md +10 -10
- package/docs/features/documents.md +8 -8
- package/docs/features/home-workspace.md +20 -15
- package/docs/features/inbox-notifications.md +22 -10
- package/docs/features/keyboard-navigation.md +11 -11
- package/docs/features/monitoring.md +1 -1
- package/docs/features/playbook.md +30 -32
- package/docs/features/profiles.md +33 -11
- package/docs/features/projects.md +2 -2
- package/docs/features/provider-runtimes.md +58 -14
- package/docs/features/schedules.md +70 -40
- package/docs/features/settings.md +74 -46
- package/docs/features/shared-components.md +7 -15
- package/docs/features/tool-permissions.md +9 -9
- package/docs/features/workflows.md +32 -21
- package/docs/getting-started.md +33 -9
- package/docs/index.md +25 -16
- package/docs/journeys/developer.md +124 -207
- package/docs/journeys/personal-use.md +70 -79
- package/docs/journeys/power-user.md +107 -151
- package/docs/journeys/work-use.md +81 -113
- package/docs/manifest.json +77 -45
- package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
- package/docs/use-cases/agency-operator.md +84 -0
- package/docs/use-cases/solo-founder.md +75 -0
- package/docs/why-stagent.md +59 -0
- package/package.json +10 -3
- package/src/app/api/channels/[id]/route.ts +104 -0
- package/src/app/api/channels/[id]/test/route.ts +52 -0
- package/src/app/api/channels/inbound/slack/route.ts +116 -0
- package/src/app/api/channels/inbound/telegram/poll/route.ts +140 -0
- package/src/app/api/channels/inbound/telegram/route.ts +87 -0
- package/src/app/api/channels/route.ts +72 -0
- package/src/app/api/chat/conversations/route.ts +15 -0
- package/src/app/api/chat/entities/search/route.ts +46 -31
- package/src/app/api/data/clear/route.ts +4 -0
- package/src/app/api/data/seed/route.ts +4 -0
- package/src/app/api/documents/route.ts +36 -6
- package/src/app/api/environment/profiles/suggest/route.ts +19 -3
- package/src/app/api/environment/scan/route.ts +8 -1
- package/src/app/api/handoffs/[id]/route.ts +76 -0
- package/src/app/api/handoffs/route.ts +89 -0
- package/src/app/api/memory/route.ts +181 -0
- package/src/app/api/profiles/[id]/route.ts +16 -1
- package/src/app/api/profiles/[id]/test/route.ts +4 -0
- package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
- package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
- package/src/app/api/profiles/assist/route.ts +35 -0
- package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
- package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
- package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
- package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
- package/src/app/api/profiles/import-repo/route.ts +29 -0
- package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
- package/src/app/api/profiles/route.ts +73 -22
- package/src/app/api/runtimes/ollama/route.ts +86 -0
- package/src/app/api/runtimes/suggest/route.ts +29 -0
- package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
- package/src/app/api/schedules/[id]/route.ts +41 -3
- package/src/app/api/schedules/parse/route.ts +66 -0
- package/src/app/api/schedules/route.ts +71 -12
- package/src/app/api/settings/author-default/route.ts +7 -0
- package/src/app/api/settings/learning/route.ts +41 -0
- package/src/app/api/settings/ollama/route.ts +34 -0
- package/src/app/api/settings/providers/route.ts +57 -0
- package/src/app/api/settings/routing/route.ts +24 -0
- package/src/app/api/settings/web-search/route.ts +28 -0
- package/src/app/api/tasks/[id]/execute/route.ts +13 -1
- package/src/app/api/tasks/[id]/respond/route.ts +23 -1
- package/src/app/documents/page.tsx +3 -0
- package/src/app/environment/page.tsx +8 -1
- package/src/app/settings/page.tsx +10 -4
- package/src/app/workflows/[id]/edit/page.tsx +2 -0
- package/src/app/workflows/new/page.tsx +2 -0
- package/src/components/chat/chat-command-popover.tsx +22 -19
- package/src/components/chat/chat-input.tsx +5 -0
- package/src/components/chat/chat-model-selector.tsx +42 -1
- package/src/components/chat/chat-shell.tsx +2 -0
- package/src/components/dashboard/welcome-landing.tsx +9 -9
- package/src/components/environment/artifact-card.tsx +27 -1
- package/src/components/environment/environment-dashboard.tsx +50 -2
- package/src/components/environment/environment-summary-card.tsx +5 -2
- package/src/components/environment/suggested-profiles.tsx +117 -52
- package/src/components/handoffs/handoff-approval-card.tsx +159 -0
- package/src/components/memory/memory-browser.tsx +315 -0
- package/src/components/profiles/learned-context-panel.tsx +4 -4
- package/src/components/profiles/profile-assist-panel.tsx +512 -0
- package/src/components/profiles/profile-browser.tsx +109 -8
- package/src/components/profiles/profile-card.tsx +29 -1
- package/src/components/profiles/profile-detail-view.tsx +200 -28
- package/src/components/profiles/profile-form-view.tsx +220 -82
- package/src/components/profiles/repo-import-wizard.tsx +648 -0
- package/src/components/profiles/smoke-test-editor.tsx +106 -0
- package/src/components/schedules/schedule-create-sheet.tsx +9 -1
- package/src/components/schedules/schedule-form.tsx +348 -9
- package/src/components/schedules/schedule-list.tsx +15 -2
- package/src/components/settings/auth-method-selector.tsx +7 -1
- package/src/components/settings/budget-guardrails-section.tsx +111 -48
- package/src/components/settings/channels-section.tsx +526 -0
- package/src/components/settings/chat-settings-section.tsx +27 -1
- package/src/components/settings/data-management-section.tsx +8 -6
- package/src/components/settings/learning-context-section.tsx +124 -0
- package/src/components/settings/ollama-section.tsx +270 -0
- package/src/components/settings/providers-runtimes-section.tsx +499 -0
- package/src/components/settings/web-search-section.tsx +101 -0
- package/src/components/shared/tag-input.tsx +156 -0
- package/src/components/tasks/kanban-board.tsx +32 -0
- package/src/components/tasks/kanban-column.tsx +4 -2
- package/src/components/tasks/task-card.tsx +1 -0
- package/src/components/tasks/task-chip-bar.tsx +6 -1
- package/src/components/tasks/task-create-panel.tsx +55 -5
- package/src/components/workflows/workflow-form-view.tsx +38 -3
- package/src/hooks/use-chat-autocomplete.ts +24 -26
- package/src/hooks/use-project-skills.ts +66 -0
- package/src/hooks/use-tag-suggestions.ts +31 -0
- package/src/instrumentation.ts +4 -1
- package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
- package/src/lib/agents/agentic-loop.ts +235 -0
- package/src/lib/agents/browser-mcp.ts +59 -4
- package/src/lib/agents/claude-agent.ts +27 -200
- package/src/lib/agents/handoff/bus.ts +164 -0
- package/src/lib/agents/handoff/governance.ts +47 -0
- package/src/lib/agents/handoff/types.ts +16 -0
- package/src/lib/agents/learned-context.ts +27 -7
- package/src/lib/agents/memory/decay.ts +61 -0
- package/src/lib/agents/memory/extractor.ts +181 -0
- package/src/lib/agents/memory/retrieval.ts +96 -0
- package/src/lib/agents/memory/types.ts +6 -0
- package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/project-profiles.ts +193 -0
- package/src/lib/agents/profiles/registry.ts +130 -6
- package/src/lib/agents/profiles/types.ts +28 -0
- package/src/lib/agents/router.ts +174 -2
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
- package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
- package/src/lib/agents/runtime/catalog.ts +57 -2
- package/src/lib/agents/runtime/claude.ts +205 -1
- package/src/lib/agents/runtime/index.ts +22 -0
- package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
- package/src/lib/agents/runtime/openai-direct.ts +514 -0
- package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
- package/src/lib/agents/runtime/types.ts +2 -0
- package/src/lib/agents/tool-permissions.ts +203 -0
- package/src/lib/channels/gateway.ts +321 -0
- package/src/lib/channels/poller.ts +268 -0
- package/src/lib/channels/registry.ts +90 -0
- package/src/lib/channels/slack-adapter.ts +188 -0
- package/src/lib/channels/telegram-adapter.ts +218 -0
- package/src/lib/channels/types.ts +75 -0
- package/src/lib/channels/webhook-adapter.ts +74 -0
- package/src/lib/chat/context-builder.ts +22 -2
- package/src/lib/chat/engine.ts +95 -13
- package/src/lib/chat/ollama-engine.ts +198 -0
- package/src/lib/chat/stagent-tools.ts +106 -20
- package/src/lib/chat/tool-catalog.ts +24 -0
- package/src/lib/chat/tool-registry.ts +90 -0
- package/src/lib/chat/tools/chat-history-tools.ts +4 -4
- package/src/lib/chat/tools/document-tools.ts +7 -7
- package/src/lib/chat/tools/handoff-tools.ts +70 -0
- package/src/lib/chat/tools/notification-tools.ts +4 -4
- package/src/lib/chat/tools/profile-tools.ts +3 -3
- package/src/lib/chat/tools/project-tools.ts +3 -3
- package/src/lib/chat/tools/schedule-tools.ts +29 -13
- package/src/lib/chat/tools/settings-tools.ts +2 -2
- package/src/lib/chat/tools/task-tools.ts +66 -11
- package/src/lib/chat/tools/usage-tools.ts +2 -2
- package/src/lib/chat/tools/workflow-tools.ts +8 -8
- package/src/lib/chat/types.ts +11 -5
- package/src/lib/constants/known-tools.ts +19 -0
- package/src/lib/constants/prose-styles.ts +1 -1
- package/src/lib/constants/settings.ts +7 -0
- package/src/lib/data/channel-bindings.ts +85 -0
- package/src/lib/data/clear.ts +22 -0
- package/src/lib/data/profile-test-results.ts +48 -0
- package/src/lib/data/seed-data/conversations.ts +196 -0
- package/src/lib/data/seed-data/learned-context.ts +99 -0
- package/src/lib/data/seed-data/notifications.ts +54 -1
- package/src/lib/data/seed-data/profile-test-results.ts +96 -0
- package/src/lib/data/seed-data/repo-imports.ts +51 -0
- package/src/lib/data/seed-data/views.ts +60 -0
- package/src/lib/data/seed.ts +51 -0
- package/src/lib/db/bootstrap.ts +162 -0
- package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
- package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
- package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
- package/src/lib/db/schema.ts +190 -1
- package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
- package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
- package/src/lib/environment/auto-scan.ts +48 -0
- package/src/lib/environment/data.ts +25 -0
- package/src/lib/environment/profile-generator.ts +40 -10
- package/src/lib/environment/profile-linker.ts +143 -0
- package/src/lib/environment/profile-rules.ts +96 -0
- package/src/lib/import/dedup.ts +149 -0
- package/src/lib/import/format-adapter.ts +631 -0
- package/src/lib/import/github-api.ts +219 -0
- package/src/lib/import/repo-scanner.ts +251 -0
- package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
- package/src/lib/schedules/active-hours.ts +120 -0
- package/src/lib/schedules/heartbeat-parser.ts +224 -0
- package/src/lib/schedules/heartbeat-prompt.ts +153 -0
- package/src/lib/schedules/nlp-parser.ts +357 -0
- package/src/lib/schedules/scheduler.ts +218 -3
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
- package/src/lib/settings/helpers.ts +6 -0
- package/src/lib/settings/routing.ts +24 -0
- package/src/lib/settings/runtime-setup.ts +28 -1
- package/src/lib/usage/ledger.ts +2 -1
- package/src/lib/validators/__tests__/settings.test.ts +9 -0
- package/src/lib/validators/profile.ts +39 -0
- package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
- package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
- package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
- package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
- package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Direct API runtime adapter.
|
|
3
|
+
*
|
|
4
|
+
* Calls the OpenAI Responses API directly via `openai` SDK
|
|
5
|
+
* (no Codex binary needed). Supports streaming, hybrid tool use
|
|
6
|
+
* (server-side + client-side), and session resume via
|
|
7
|
+
* `previous_response_id`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { db } from "@/lib/db";
|
|
11
|
+
import { tasks, agentLogs, notifications } from "@/lib/db/schema";
|
|
12
|
+
import { eq } from "drizzle-orm";
|
|
13
|
+
import { setExecution, removeExecution, getExecution } from "../execution-manager";
|
|
14
|
+
import { DEFAULT_MAX_TURNS, DEFAULT_MAX_BUDGET_USD } from "@/lib/constants/task-status";
|
|
15
|
+
import {
|
|
16
|
+
buildTaskQueryContext,
|
|
17
|
+
createTaskUsageState,
|
|
18
|
+
} from "../claude-agent";
|
|
19
|
+
import { createToolServer } from "@/lib/chat/stagent-tools";
|
|
20
|
+
import type { OpenAIFunctionDef } from "@/lib/chat/tool-registry";
|
|
21
|
+
import { handleToolPermission, clearPermissionCache } from "../tool-permissions";
|
|
22
|
+
import {
|
|
23
|
+
runAgenticLoop,
|
|
24
|
+
type LoopMessage,
|
|
25
|
+
type ModelTurnResult,
|
|
26
|
+
type TurnUsage,
|
|
27
|
+
type AgentStreamEvent,
|
|
28
|
+
} from "../agentic-loop";
|
|
29
|
+
import { getRuntimeCatalogEntry } from "./catalog";
|
|
30
|
+
import type {
|
|
31
|
+
AgentRuntimeAdapter,
|
|
32
|
+
RuntimeConnectionResult,
|
|
33
|
+
TaskAssistInput,
|
|
34
|
+
} from "./types";
|
|
35
|
+
import type { TaskAssistResponse } from "./task-assist-types";
|
|
36
|
+
import type { ProfileTestReport } from "../profiles/test-types";
|
|
37
|
+
import { getProfile, listProfiles } from "../profiles/registry";
|
|
38
|
+
import {
|
|
39
|
+
recordUsageLedgerEntry,
|
|
40
|
+
resolveUsageActivityType,
|
|
41
|
+
} from "@/lib/usage/ledger";
|
|
42
|
+
|
|
43
|
+
// ── SDK lazy import ──────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
type OpenAISDK = typeof import("openai");
|
|
46
|
+
let _sdk: OpenAISDK | null = null;
|
|
47
|
+
|
|
48
|
+
async function getOpenAISDK(): Promise<OpenAISDK> {
|
|
49
|
+
if (!_sdk) {
|
|
50
|
+
_sdk = await import("openai");
|
|
51
|
+
}
|
|
52
|
+
return _sdk;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── API key resolution ───────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
async function getOpenAIApiKeyValue(): Promise<string> {
|
|
58
|
+
const { getOpenAIApiKey } = await import("@/lib/settings/openai-auth");
|
|
59
|
+
const { apiKey } = await getOpenAIApiKey();
|
|
60
|
+
if (apiKey) return apiKey;
|
|
61
|
+
throw new Error("No OpenAI API key configured. Set one in Settings > Authentication.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Model call via Responses API ─────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/** Options for OpenAI model call. */
|
|
67
|
+
interface OpenAICallOptions {
|
|
68
|
+
modelId?: string;
|
|
69
|
+
previousResponseId?: string | null;
|
|
70
|
+
/** Server-side tools to enable (e.g., { web_search_preview: true, code_interpreter: true }). */
|
|
71
|
+
serverTools?: Record<string, boolean>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function callOpenAIModel(
|
|
75
|
+
client: InstanceType<OpenAISDK["default"]>,
|
|
76
|
+
instructions: string,
|
|
77
|
+
input: LoopMessage[],
|
|
78
|
+
tools: OpenAIFunctionDef[],
|
|
79
|
+
_signal: AbortSignal,
|
|
80
|
+
emitEvent: (event: AgentStreamEvent) => void,
|
|
81
|
+
options: OpenAICallOptions = {},
|
|
82
|
+
): Promise<ModelTurnResult & { responseId?: string }> {
|
|
83
|
+
const modelId = options.modelId ?? "gpt-4.1";
|
|
84
|
+
|
|
85
|
+
// Build tool array: Stagent function tools + enabled server-side tools
|
|
86
|
+
const serverToolConfig = options.serverTools ?? { web_search_preview: true };
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
const allTools: any[] = [...tools];
|
|
89
|
+
for (const [toolType, enabled] of Object.entries(serverToolConfig)) {
|
|
90
|
+
if (enabled) allTools.push({ type: toolType });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const response = await client.responses.create({
|
|
94
|
+
model: modelId,
|
|
95
|
+
instructions,
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
+
input: input as any,
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
tools: allTools as any,
|
|
100
|
+
...(options.previousResponseId ? { previous_response_id: options.previousResponseId } : {}),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
emitEvent({ type: "status", phase: "running" });
|
|
104
|
+
|
|
105
|
+
let text = "";
|
|
106
|
+
const toolCalls: ModelTurnResult["toolCalls"] = [];
|
|
107
|
+
const usage: TurnUsage = { modelId };
|
|
108
|
+
let responseId: string | undefined;
|
|
109
|
+
|
|
110
|
+
// Process response output
|
|
111
|
+
responseId = response.id;
|
|
112
|
+
|
|
113
|
+
if (response.usage) {
|
|
114
|
+
usage.inputTokens = response.usage.input_tokens;
|
|
115
|
+
usage.outputTokens = response.usage.output_tokens;
|
|
116
|
+
usage.totalTokens = response.usage.input_tokens + response.usage.output_tokens;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const item of response.output) {
|
|
120
|
+
if (item.type === "message") {
|
|
121
|
+
for (const part of item.content) {
|
|
122
|
+
if (part.type === "output_text") {
|
|
123
|
+
text += part.text;
|
|
124
|
+
emitEvent({ type: "delta", content: part.text });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else if (item.type === "function_call") {
|
|
128
|
+
// Client-side Stagent tool call — needs HITL
|
|
129
|
+
let parsedArgs: Record<string, unknown> = {};
|
|
130
|
+
try {
|
|
131
|
+
parsedArgs = JSON.parse(item.arguments);
|
|
132
|
+
} catch {
|
|
133
|
+
parsedArgs = {};
|
|
134
|
+
}
|
|
135
|
+
toolCalls.push({
|
|
136
|
+
id: item.call_id,
|
|
137
|
+
name: item.name,
|
|
138
|
+
arguments: parsedArgs,
|
|
139
|
+
});
|
|
140
|
+
emitEvent({ type: "status", phase: "tool_use", message: item.name });
|
|
141
|
+
}
|
|
142
|
+
// Server-side tool results (web_search_call etc.) are in output
|
|
143
|
+
// but already handled by the API — just log them
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const isComplete = response.status === "completed" && toolCalls.length === 0;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
text,
|
|
150
|
+
toolCalls,
|
|
151
|
+
isComplete,
|
|
152
|
+
needsContinuation: false, // Responses API handles continuations internally
|
|
153
|
+
usage,
|
|
154
|
+
responseId,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Session persistence ──────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
async function saveResponseId(taskId: string, profileId: string, responseId: string) {
|
|
161
|
+
await db.insert(agentLogs).values({
|
|
162
|
+
id: crypto.randomUUID(),
|
|
163
|
+
taskId,
|
|
164
|
+
agentType: profileId,
|
|
165
|
+
event: "openai_response_id",
|
|
166
|
+
payload: JSON.stringify({ responseId }),
|
|
167
|
+
timestamp: new Date(),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function loadResponseId(taskId: string): Promise<string | null> {
|
|
172
|
+
const [log] = await db
|
|
173
|
+
.select()
|
|
174
|
+
.from(agentLogs)
|
|
175
|
+
.where(eq(agentLogs.taskId, taskId))
|
|
176
|
+
.orderBy(agentLogs.timestamp)
|
|
177
|
+
.limit(1);
|
|
178
|
+
|
|
179
|
+
if (!log?.payload) return null;
|
|
180
|
+
try {
|
|
181
|
+
const data = JSON.parse(log.payload);
|
|
182
|
+
return data.responseId ?? null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Core task execution ──────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
async function executeOpenAIDirectTask(taskId: string, isResume = false): Promise<void> {
|
|
191
|
+
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
|
|
192
|
+
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
193
|
+
|
|
194
|
+
const agentProfileId = task.agentProfile ?? "general";
|
|
195
|
+
const usageState = createTaskUsageState(task, isResume);
|
|
196
|
+
const abortController = new AbortController();
|
|
197
|
+
|
|
198
|
+
setExecution(taskId, {
|
|
199
|
+
abortController,
|
|
200
|
+
sessionId: null,
|
|
201
|
+
taskId,
|
|
202
|
+
startedAt: new Date(),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await db
|
|
207
|
+
.update(tasks)
|
|
208
|
+
.set({ status: "running", updatedAt: new Date() })
|
|
209
|
+
.where(eq(tasks.id, taskId));
|
|
210
|
+
|
|
211
|
+
const ctx = await buildTaskQueryContext(task, agentProfileId);
|
|
212
|
+
const apiKey = await getOpenAIApiKeyValue();
|
|
213
|
+
const sdk = await getOpenAISDK();
|
|
214
|
+
const client = new sdk.default({ apiKey });
|
|
215
|
+
|
|
216
|
+
const toolServer = createToolServer(task.projectId);
|
|
217
|
+
const { tools, executeHandler } = toolServer.forProvider("openai");
|
|
218
|
+
|
|
219
|
+
// Resolve model
|
|
220
|
+
const { getSetting } = await import("@/lib/settings/helpers");
|
|
221
|
+
const modelId = (await getSetting("openai_direct_model")) ?? "gpt-4.1";
|
|
222
|
+
const maxTurns = ctx.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
223
|
+
|
|
224
|
+
// For resume: load previous response ID
|
|
225
|
+
let previousResponseId: string | null = null;
|
|
226
|
+
if (isResume) {
|
|
227
|
+
previousResponseId = await loadResponseId(taskId);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const initialMessages: LoopMessage[] = [
|
|
231
|
+
{ role: "user", content: ctx.userPrompt },
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
// Log start
|
|
235
|
+
await db.insert(agentLogs).values({
|
|
236
|
+
id: crypto.randomUUID(),
|
|
237
|
+
taskId,
|
|
238
|
+
agentType: agentProfileId,
|
|
239
|
+
event: isResume ? "resumed" : "started",
|
|
240
|
+
payload: JSON.stringify({
|
|
241
|
+
runtime: "openai-direct",
|
|
242
|
+
model: modelId,
|
|
243
|
+
maxTurns,
|
|
244
|
+
}),
|
|
245
|
+
timestamp: new Date(),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Track last response ID for resume
|
|
249
|
+
let lastResponseId: string | null = previousResponseId;
|
|
250
|
+
|
|
251
|
+
const result = await runAgenticLoop(initialMessages, {
|
|
252
|
+
async callModel(messages, signal) {
|
|
253
|
+
// Resolve capability overrides from profile
|
|
254
|
+
const profile = getProfile(agentProfileId);
|
|
255
|
+
const capOverrides = profile?.capabilityOverrides?.["openai-direct"];
|
|
256
|
+
|
|
257
|
+
const turnResult = await callOpenAIModel(
|
|
258
|
+
client,
|
|
259
|
+
ctx.systemInstructions,
|
|
260
|
+
messages,
|
|
261
|
+
tools as OpenAIFunctionDef[],
|
|
262
|
+
signal,
|
|
263
|
+
(evt) => {
|
|
264
|
+
void db.insert(agentLogs).values({
|
|
265
|
+
id: crypto.randomUUID(),
|
|
266
|
+
taskId,
|
|
267
|
+
agentType: agentProfileId,
|
|
268
|
+
event: "stream",
|
|
269
|
+
payload: JSON.stringify(evt),
|
|
270
|
+
timestamp: new Date(),
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
modelId: capOverrides?.modelId ?? modelId,
|
|
275
|
+
previousResponseId: lastResponseId,
|
|
276
|
+
serverTools: capOverrides?.serverTools,
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Store response ID for resume
|
|
281
|
+
if (turnResult.responseId) {
|
|
282
|
+
lastResponseId = turnResult.responseId;
|
|
283
|
+
await saveResponseId(taskId, agentProfileId, turnResult.responseId);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return turnResult;
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
formatToolResult(toolCallId, _toolName, result) {
|
|
290
|
+
return {
|
|
291
|
+
type: "function_call_output",
|
|
292
|
+
call_id: toolCallId,
|
|
293
|
+
output: result.content.map((c) => c.text).join("\n"),
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
formatContinuation() {
|
|
298
|
+
return { role: "user", content: "Please continue." };
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
async executeTool(name, args) {
|
|
302
|
+
return executeHandler(name, args);
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
async checkPermission(toolName, args) {
|
|
306
|
+
return handleToolPermission(taskId, toolName, args, ctx.canUseToolPolicy);
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
emitEvent(_event) {
|
|
310
|
+
// Events logged in callModel
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
maxTurns,
|
|
314
|
+
maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
|
|
315
|
+
signal: abortController.signal,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const finalStatus = result.stopReason === "complete" ? "completed" : "failed";
|
|
319
|
+
const resultText = result.stopReason === "complete"
|
|
320
|
+
? result.finalText
|
|
321
|
+
: `Task stopped: ${result.stopReason}`;
|
|
322
|
+
|
|
323
|
+
await db
|
|
324
|
+
.update(tasks)
|
|
325
|
+
.set({ status: finalStatus, result: resultText, updatedAt: new Date() })
|
|
326
|
+
.where(eq(tasks.id, taskId));
|
|
327
|
+
|
|
328
|
+
await db.insert(notifications).values({
|
|
329
|
+
id: crypto.randomUUID(),
|
|
330
|
+
taskId,
|
|
331
|
+
type: finalStatus === "completed" ? "task_completed" : "task_failed",
|
|
332
|
+
title: `Task ${finalStatus}: ${task.title}`,
|
|
333
|
+
body: resultText.slice(0, 500),
|
|
334
|
+
createdAt: new Date(),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await db.insert(agentLogs).values({
|
|
338
|
+
id: crypto.randomUUID(),
|
|
339
|
+
taskId,
|
|
340
|
+
agentType: agentProfileId,
|
|
341
|
+
event: finalStatus,
|
|
342
|
+
payload: JSON.stringify({
|
|
343
|
+
result: resultText.slice(0, 1000),
|
|
344
|
+
turns: result.turnCount,
|
|
345
|
+
usage: result.totalUsage,
|
|
346
|
+
}),
|
|
347
|
+
timestamp: new Date(),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await recordUsageLedgerEntry({
|
|
351
|
+
taskId,
|
|
352
|
+
workflowId: task.workflowId ?? null,
|
|
353
|
+
scheduleId: task.scheduleId ?? null,
|
|
354
|
+
projectId: task.projectId ?? null,
|
|
355
|
+
activityType: resolveUsageActivityType({
|
|
356
|
+
workflowId: task.workflowId,
|
|
357
|
+
scheduleId: task.scheduleId,
|
|
358
|
+
isResume,
|
|
359
|
+
}),
|
|
360
|
+
runtimeId: "openai-direct",
|
|
361
|
+
providerId: "openai",
|
|
362
|
+
modelId: result.totalUsage.modelId ?? modelId,
|
|
363
|
+
inputTokens: result.totalUsage.inputTokens ?? null,
|
|
364
|
+
outputTokens: result.totalUsage.outputTokens ?? null,
|
|
365
|
+
totalTokens: result.totalUsage.totalTokens ?? null,
|
|
366
|
+
status: finalStatus,
|
|
367
|
+
startedAt: usageState.startedAt,
|
|
368
|
+
finishedAt: new Date(),
|
|
369
|
+
});
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (!abortController.signal.aborted) {
|
|
372
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
373
|
+
await db
|
|
374
|
+
.update(tasks)
|
|
375
|
+
.set({ status: "failed", result: errorMsg, updatedAt: new Date() })
|
|
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: ${task.title}`,
|
|
383
|
+
body: errorMsg.slice(0, 500),
|
|
384
|
+
createdAt: new Date(),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
} finally {
|
|
388
|
+
clearPermissionCache(taskId);
|
|
389
|
+
removeExecution(taskId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── Task Assist ──────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
async function runOpenAITaskAssist(input: TaskAssistInput): Promise<TaskAssistResponse> {
|
|
396
|
+
const apiKey = await getOpenAIApiKeyValue();
|
|
397
|
+
const sdk = await getOpenAISDK();
|
|
398
|
+
const client = new sdk.default({ apiKey });
|
|
399
|
+
|
|
400
|
+
const profileIds = listProfiles().map((p) => p.id);
|
|
401
|
+
const profileList = profileIds.length > 0
|
|
402
|
+
? `Available agent profiles: ${profileIds.join(", ")}`
|
|
403
|
+
: "No explicit profiles available.";
|
|
404
|
+
|
|
405
|
+
const instructions = `You are an AI task definition assistant. Analyze the given task and return ONLY a JSON object (no markdown) with:
|
|
406
|
+
- "improvedDescription", "breakdown", "recommendedPattern", "complexity", "needsCheckpoint", "reasoning"
|
|
407
|
+
|
|
408
|
+
${profileList}`;
|
|
409
|
+
|
|
410
|
+
const userContent = [
|
|
411
|
+
input.title ? `Task title: ${input.title}` : "",
|
|
412
|
+
input.description ? `Description: ${input.description}` : "",
|
|
413
|
+
].filter(Boolean).join("\n");
|
|
414
|
+
|
|
415
|
+
const response = await client.responses.create({
|
|
416
|
+
model: "gpt-4.1",
|
|
417
|
+
instructions,
|
|
418
|
+
input: userContent || "Analyze this task",
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
422
|
+
const text = (response.output as any[])
|
|
423
|
+
.filter((item) => item.type === "message")
|
|
424
|
+
.flatMap((item) =>
|
|
425
|
+
(item.content ?? [])
|
|
426
|
+
.filter((p: { type: string }) => p.type === "output_text")
|
|
427
|
+
.map((p: { text?: string }) => p.text ?? ""),
|
|
428
|
+
)
|
|
429
|
+
.join("");
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
return JSON.parse(text);
|
|
433
|
+
} catch {
|
|
434
|
+
return {
|
|
435
|
+
improvedDescription: text,
|
|
436
|
+
breakdown: [],
|
|
437
|
+
recommendedPattern: "single",
|
|
438
|
+
complexity: "simple",
|
|
439
|
+
needsCheckpoint: false,
|
|
440
|
+
reasoning: "Failed to parse structured response",
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Connection Test ──────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
async function testOpenAIConnection(): Promise<RuntimeConnectionResult> {
|
|
448
|
+
try {
|
|
449
|
+
const apiKey = await getOpenAIApiKeyValue();
|
|
450
|
+
const sdk = await getOpenAISDK();
|
|
451
|
+
const client = new sdk.default({ apiKey });
|
|
452
|
+
|
|
453
|
+
// Simple validation
|
|
454
|
+
await client.responses.create({
|
|
455
|
+
model: "gpt-4.1-mini",
|
|
456
|
+
input: "ping",
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return { connected: true, apiKeySource: "db" };
|
|
460
|
+
} catch (err) {
|
|
461
|
+
return {
|
|
462
|
+
connected: false,
|
|
463
|
+
error: err instanceof Error ? err.message : "Connection failed",
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Adapter export ───────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
export const openAIDirectRuntimeAdapter: AgentRuntimeAdapter = {
|
|
471
|
+
metadata: getRuntimeCatalogEntry("openai-direct" as never), // Registered in catalog.ts
|
|
472
|
+
|
|
473
|
+
async executeTask(taskId: string) {
|
|
474
|
+
return executeOpenAIDirectTask(taskId, false);
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
async resumeTask(taskId: string) {
|
|
478
|
+
return executeOpenAIDirectTask(taskId, true);
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
async cancelTask(taskId: string) {
|
|
482
|
+
const execution = getExecution(taskId);
|
|
483
|
+
if (execution?.abortController) {
|
|
484
|
+
execution.abortController.abort();
|
|
485
|
+
}
|
|
486
|
+
await db
|
|
487
|
+
.update(tasks)
|
|
488
|
+
.set({ status: "cancelled", updatedAt: new Date() })
|
|
489
|
+
.where(eq(tasks.id, taskId));
|
|
490
|
+
removeExecution(taskId);
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
async runTaskAssist(input: TaskAssistInput) {
|
|
494
|
+
return runOpenAITaskAssist(input);
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
async runProfileTests(profileId: string): Promise<ProfileTestReport> {
|
|
498
|
+
const profile = getProfile(profileId);
|
|
499
|
+
return {
|
|
500
|
+
profileId,
|
|
501
|
+
profileName: profile?.name ?? profileId,
|
|
502
|
+
runtimeId: "openai-direct",
|
|
503
|
+
results: [],
|
|
504
|
+
totalPassed: 0,
|
|
505
|
+
totalFailed: 0,
|
|
506
|
+
unsupported: true,
|
|
507
|
+
unsupportedReason: "Profile smoke tests not yet implemented for OpenAI Direct runtime",
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
async testConnection() {
|
|
512
|
+
return testOpenAIConnection();
|
|
513
|
+
},
|
|
514
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface ProfileAssistRequest {
|
|
2
|
+
/** Natural language description of desired agent */
|
|
3
|
+
goal: string;
|
|
4
|
+
/** Optional domain hint */
|
|
5
|
+
domain?: "work" | "personal";
|
|
6
|
+
/** Operation mode */
|
|
7
|
+
mode: "generate" | "refine-skillmd" | "suggest-tests";
|
|
8
|
+
/** Existing SKILL.md for refine/suggest-tests modes */
|
|
9
|
+
existingSkillMd?: string;
|
|
10
|
+
/** Existing tags for context */
|
|
11
|
+
existingTags?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ProfileAssistResponse {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
domain: "work" | "personal";
|
|
18
|
+
tags: string[];
|
|
19
|
+
skillMd: string;
|
|
20
|
+
allowedTools: string[];
|
|
21
|
+
canUseToolPolicy: {
|
|
22
|
+
autoApprove: string[];
|
|
23
|
+
autoDeny: string[];
|
|
24
|
+
};
|
|
25
|
+
maxTurns: number;
|
|
26
|
+
outputFormat: string;
|
|
27
|
+
supportedRuntimes: string[];
|
|
28
|
+
tests: Array<{ task: string; expectedKeywords: string[] }>;
|
|
29
|
+
reasoning: string;
|
|
30
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ApiKeySource } from "@/lib/constants/settings";
|
|
|
2
2
|
import type { ProfileTestReport } from "@/lib/agents/profiles/test-types";
|
|
3
3
|
import type { RuntimeCapabilities, RuntimeCatalogEntry } from "./catalog";
|
|
4
4
|
import type { TaskAssistResponse } from "./task-assist-types";
|
|
5
|
+
import type { ProfileAssistRequest, ProfileAssistResponse } from "./profile-assist-types";
|
|
5
6
|
|
|
6
7
|
export interface RuntimeConnectionResult {
|
|
7
8
|
connected: boolean;
|
|
@@ -20,6 +21,7 @@ export interface AgentRuntimeAdapter {
|
|
|
20
21
|
resumeTask(taskId: string): Promise<void>;
|
|
21
22
|
cancelTask(taskId: string): Promise<void>;
|
|
22
23
|
runTaskAssist?(input: TaskAssistInput): Promise<TaskAssistResponse>;
|
|
24
|
+
runProfileAssist?(input: ProfileAssistRequest): Promise<ProfileAssistResponse>;
|
|
23
25
|
runProfileTests?(profileId: string): Promise<ProfileTestReport>;
|
|
24
26
|
testConnection?(): Promise<RuntimeConnectionResult>;
|
|
25
27
|
}
|