stagent 0.4.0 → 0.6.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/README.md +67 -31
- package/dist/cli.js +151 -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 +53 -71
- 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 +77 -41
- package/docs/features/settings.md +134 -51
- 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 +79 -47
- package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
- package/docs/superpowers/specs/2026-03-27-chat-screenshot-display-design.md +303 -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 +12 -3
- package/src/app/api/channels/[id]/route.ts +103 -0
- package/src/app/api/channels/[id]/test/route.ts +52 -0
- package/src/app/api/channels/inbound/slack/route.ts +109 -0
- package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
- package/src/app/api/channels/inbound/telegram/route.ts +76 -0
- package/src/app/api/channels/route.ts +71 -0
- package/src/app/api/chat/conversations/[id]/messages/route.ts +3 -2
- package/src/app/api/chat/conversations/route.ts +15 -0
- package/src/app/api/chat/entities/search/route.ts +112 -0
- package/src/app/api/documents/[id]/file/route.ts +4 -1
- 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/projects/[id]/route.ts +119 -9
- package/src/app/api/projects/__tests__/delete-project.test.ts +170 -0
- 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/browser-tools/route.ts +68 -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/documents/page.tsx +3 -0
- package/src/app/environment/page.tsx +8 -1
- package/src/app/settings/page.tsx +12 -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 +280 -0
- package/src/components/chat/chat-input.tsx +90 -10
- package/src/components/chat/chat-message.tsx +9 -3
- package/src/components/chat/chat-model-selector.tsx +42 -1
- package/src/components/chat/chat-shell.tsx +31 -5
- package/src/components/chat/screenshot-gallery.tsx +96 -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/monitoring/log-entry.tsx +61 -27
- 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/projects/project-detail.tsx +15 -2
- package/src/components/schedules/schedule-create-sheet.tsx +32 -330
- package/src/components/schedules/schedule-detail-sheet.tsx +37 -21
- package/src/components/schedules/schedule-edit-sheet.tsx +159 -0
- package/src/components/schedules/schedule-form.tsx +749 -0
- package/src/components/schedules/schedule-list.tsx +31 -2
- package/src/components/settings/auth-method-selector.tsx +7 -1
- package/src/components/settings/browser-tools-section.tsx +247 -0
- 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/runtime-timeout-section.tsx +4 -4
- package/src/components/settings/web-search-section.tsx +101 -0
- package/src/components/shared/command-palette.tsx +1 -30
- package/src/components/shared/screenshot-lightbox.tsx +151 -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-caret-position.ts +104 -0
- package/src/hooks/use-chat-autocomplete.ts +288 -0
- 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__/browser-mcp.test.ts +175 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +6 -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 +174 -0
- package/src/lib/agents/claude-agent.ts +83 -198
- 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 +43 -0
- package/src/lib/channels/webhook-adapter.ts +74 -0
- package/src/lib/chat/command-data.ts +50 -0
- package/src/lib/chat/context-builder.ts +147 -3
- package/src/lib/chat/engine.ts +182 -19
- package/src/lib/chat/ollama-engine.ts +198 -0
- package/src/lib/chat/slash-commands.ts +191 -0
- package/src/lib/chat/stagent-tools.ts +106 -20
- package/src/lib/chat/tool-catalog.ts +209 -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 +43 -6
- 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 +22 -6
- package/src/lib/constants/known-tools.ts +19 -0
- package/src/lib/constants/prose-styles.ts +1 -1
- package/src/lib/constants/settings.ts +11 -0
- package/src/lib/data/channel-bindings.ts +85 -0
- package/src/lib/data/clear.ts +38 -4
- 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 +167 -0
- package/src/lib/db/migrations/0012_add_screenshot_columns.sql +5 -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 +192 -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/screenshots/__tests__/persist.test.ts +104 -0
- package/src/lib/screenshots/persist.ts +114 -0
- 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/utils/stagent-paths.ts +4 -0
- 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,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic agentic loop for direct API runtimes.
|
|
3
|
+
*
|
|
4
|
+
* The loop handles turn counting, budget tracking, abort signaling,
|
|
5
|
+
* and HITL tool permission checks. Provider-specific logic (API calls,
|
|
6
|
+
* event mapping, tool result formatting) is injected via callbacks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ToolResult } from "@/lib/chat/tool-registry";
|
|
10
|
+
import type { ToolPermissionResponse } from "./tool-permissions";
|
|
11
|
+
|
|
12
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** A single tool call extracted from the model response. */
|
|
15
|
+
export interface ToolCall {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
arguments: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Usage snapshot from a single model turn. */
|
|
22
|
+
export interface TurnUsage {
|
|
23
|
+
inputTokens?: number;
|
|
24
|
+
outputTokens?: number;
|
|
25
|
+
totalTokens?: number;
|
|
26
|
+
modelId?: string;
|
|
27
|
+
costUsd?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Events emitted during the loop for SSE streaming. */
|
|
31
|
+
export type AgentStreamEvent =
|
|
32
|
+
| { type: "status"; phase: "running" | "tool_use" | "thinking"; message?: string }
|
|
33
|
+
| { type: "delta"; content: string }
|
|
34
|
+
| { type: "done"; finalText: string }
|
|
35
|
+
| { type: "error"; message: string };
|
|
36
|
+
|
|
37
|
+
/** Result of a single model API call (accumulated from stream). */
|
|
38
|
+
export interface ModelTurnResult {
|
|
39
|
+
/** Concatenated text output from the model. */
|
|
40
|
+
text: string;
|
|
41
|
+
/** Tool calls requested by the model. */
|
|
42
|
+
toolCalls: ToolCall[];
|
|
43
|
+
/** Whether the model indicated it is done (end_turn / stop). */
|
|
44
|
+
isComplete: boolean;
|
|
45
|
+
/** Whether output was truncated by max_tokens. */
|
|
46
|
+
needsContinuation: boolean;
|
|
47
|
+
/** Usage for this turn. */
|
|
48
|
+
usage: TurnUsage;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Message in the conversation history (provider-agnostic shape). */
|
|
52
|
+
export type LoopMessage = Record<string, unknown>;
|
|
53
|
+
|
|
54
|
+
/** Configuration for the agentic loop — provider injects callbacks. */
|
|
55
|
+
export interface AgenticLoopConfig {
|
|
56
|
+
/**
|
|
57
|
+
* Call the model API with the current messages. Must stream events
|
|
58
|
+
* via `emitEvent` and return the accumulated turn result.
|
|
59
|
+
*/
|
|
60
|
+
callModel: (
|
|
61
|
+
messages: LoopMessage[],
|
|
62
|
+
signal: AbortSignal,
|
|
63
|
+
) => Promise<ModelTurnResult>;
|
|
64
|
+
|
|
65
|
+
/** Format a tool result for appending to the message history. */
|
|
66
|
+
formatToolResult: (
|
|
67
|
+
toolCallId: string,
|
|
68
|
+
toolName: string,
|
|
69
|
+
result: ToolResult,
|
|
70
|
+
) => LoopMessage;
|
|
71
|
+
|
|
72
|
+
/** Format a continuation message (e.g. after max_tokens truncation). */
|
|
73
|
+
formatContinuation: () => LoopMessage;
|
|
74
|
+
|
|
75
|
+
/** Execute a Stagent tool by name. */
|
|
76
|
+
executeTool: (
|
|
77
|
+
name: string,
|
|
78
|
+
args: Record<string, unknown>,
|
|
79
|
+
) => Promise<ToolResult>;
|
|
80
|
+
|
|
81
|
+
/** HITL permission check. Return allow/deny. */
|
|
82
|
+
checkPermission: (
|
|
83
|
+
toolName: string,
|
|
84
|
+
args: Record<string, unknown>,
|
|
85
|
+
) => Promise<ToolPermissionResponse>;
|
|
86
|
+
|
|
87
|
+
/** Emit SSE event for real-time UI streaming. */
|
|
88
|
+
emitEvent: (event: AgentStreamEvent) => void;
|
|
89
|
+
|
|
90
|
+
/** Maximum model turns before stopping. */
|
|
91
|
+
maxTurns: number;
|
|
92
|
+
|
|
93
|
+
/** Maximum budget in USD before stopping. */
|
|
94
|
+
maxBudgetUsd?: number;
|
|
95
|
+
|
|
96
|
+
/** Abort signal for cancellation. */
|
|
97
|
+
signal: AbortSignal;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Result of the agentic loop. */
|
|
101
|
+
export interface AgenticLoopResult {
|
|
102
|
+
finalText: string;
|
|
103
|
+
turnCount: number;
|
|
104
|
+
totalUsage: TurnUsage;
|
|
105
|
+
stopReason: "complete" | "max_turns" | "budget_exceeded" | "cancelled" | "error";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Loop implementation ──────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
function mergeTurnUsage(total: TurnUsage, turn: TurnUsage): TurnUsage {
|
|
111
|
+
return {
|
|
112
|
+
inputTokens: (total.inputTokens ?? 0) + (turn.inputTokens ?? 0),
|
|
113
|
+
outputTokens: (total.outputTokens ?? 0) + (turn.outputTokens ?? 0),
|
|
114
|
+
totalTokens: (total.totalTokens ?? 0) + (turn.totalTokens ?? 0),
|
|
115
|
+
modelId: turn.modelId ?? total.modelId,
|
|
116
|
+
costUsd: (total.costUsd ?? 0) + (turn.costUsd ?? 0),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run a provider-agnostic agentic loop.
|
|
122
|
+
*
|
|
123
|
+
* Repeatedly calls the model, handles tool execution with HITL checks,
|
|
124
|
+
* and enforces turn/budget limits until the model completes or a limit
|
|
125
|
+
* is reached.
|
|
126
|
+
*/
|
|
127
|
+
export async function runAgenticLoop(
|
|
128
|
+
initialMessages: LoopMessage[],
|
|
129
|
+
config: AgenticLoopConfig,
|
|
130
|
+
): Promise<AgenticLoopResult> {
|
|
131
|
+
const messages = [...initialMessages];
|
|
132
|
+
let turnCount = 0;
|
|
133
|
+
let totalUsage: TurnUsage = {};
|
|
134
|
+
let lastText = "";
|
|
135
|
+
|
|
136
|
+
while (turnCount < config.maxTurns) {
|
|
137
|
+
// Check cancellation
|
|
138
|
+
if (config.signal.aborted) {
|
|
139
|
+
return { finalText: lastText, turnCount, totalUsage, stopReason: "cancelled" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check budget
|
|
143
|
+
if (config.maxBudgetUsd && (totalUsage.costUsd ?? 0) >= config.maxBudgetUsd) {
|
|
144
|
+
config.emitEvent({ type: "error", message: "Budget limit exceeded" });
|
|
145
|
+
return { finalText: lastText, turnCount, totalUsage, stopReason: "budget_exceeded" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Call model
|
|
149
|
+
turnCount++;
|
|
150
|
+
let turnResult: ModelTurnResult;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
turnResult = await config.callModel(messages, config.signal);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (config.signal.aborted) {
|
|
156
|
+
return { finalText: lastText, turnCount, totalUsage, stopReason: "cancelled" };
|
|
157
|
+
}
|
|
158
|
+
const message = err instanceof Error ? err.message : "Model API call failed";
|
|
159
|
+
config.emitEvent({ type: "error", message });
|
|
160
|
+
return { finalText: lastText, turnCount, totalUsage, stopReason: "error" };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
totalUsage = mergeTurnUsage(totalUsage, turnResult.usage);
|
|
164
|
+
if (turnResult.text) lastText = turnResult.text;
|
|
165
|
+
|
|
166
|
+
// Handle completion
|
|
167
|
+
if (turnResult.isComplete && turnResult.toolCalls.length === 0) {
|
|
168
|
+
config.emitEvent({ type: "done", finalText: lastText });
|
|
169
|
+
return { finalText: lastText, turnCount, totalUsage, stopReason: "complete" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Handle tool calls
|
|
173
|
+
if (turnResult.toolCalls.length > 0) {
|
|
174
|
+
for (const toolCall of turnResult.toolCalls) {
|
|
175
|
+
if (config.signal.aborted) {
|
|
176
|
+
return { finalText: lastText, turnCount, totalUsage, stopReason: "cancelled" };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
config.emitEvent({
|
|
180
|
+
type: "status",
|
|
181
|
+
phase: "tool_use",
|
|
182
|
+
message: toolCall.name,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// HITL permission check
|
|
186
|
+
const permission = await config.checkPermission(
|
|
187
|
+
toolCall.name,
|
|
188
|
+
toolCall.arguments,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
let result: ToolResult;
|
|
192
|
+
if (permission.behavior === "deny") {
|
|
193
|
+
result = {
|
|
194
|
+
content: [{ type: "text", text: JSON.stringify({ error: permission.message ?? "Tool denied by user" }) }],
|
|
195
|
+
isError: true,
|
|
196
|
+
};
|
|
197
|
+
} else {
|
|
198
|
+
try {
|
|
199
|
+
result = await config.executeTool(
|
|
200
|
+
toolCall.name,
|
|
201
|
+
(permission.updatedInput as Record<string, unknown>) ?? toolCall.arguments,
|
|
202
|
+
);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
result = {
|
|
205
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : "Tool execution failed" }) }],
|
|
206
|
+
isError: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Append tool result to messages
|
|
212
|
+
messages.push(
|
|
213
|
+
config.formatToolResult(toolCall.id, toolCall.name, result),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Continue loop — model needs to process tool results
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Handle max_tokens continuation
|
|
222
|
+
if (turnResult.needsContinuation) {
|
|
223
|
+
messages.push(config.formatContinuation());
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Shouldn't reach here — safeguard
|
|
228
|
+
config.emitEvent({ type: "done", finalText: lastText });
|
|
229
|
+
return { finalText: lastText, turnCount, totalUsage, stopReason: "complete" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Max turns exceeded
|
|
233
|
+
config.emitEvent({ type: "error", message: `Max turns (${config.maxTurns}) reached` });
|
|
234
|
+
return { finalText: lastText, turnCount, totalUsage, stopReason: "max_turns" };
|
|
235
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { getSetting } from "@/lib/settings/helpers";
|
|
2
|
+
import { SETTINGS_KEYS } from "@/lib/constants/settings";
|
|
3
|
+
|
|
4
|
+
// ── MCP server config types (matches Claude Agent SDK shape) ─────────
|
|
5
|
+
|
|
6
|
+
interface McpStdioConfig {
|
|
7
|
+
type?: "stdio";
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface McpHttpConfig {
|
|
13
|
+
type: "http";
|
|
14
|
+
url: string;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type AnyMcpServerConfig = McpStdioConfig | McpHttpConfig;
|
|
19
|
+
|
|
20
|
+
// ── Read-only browser tools — auto-approved in chat & task permission callbacks
|
|
21
|
+
|
|
22
|
+
export const BROWSER_READ_ONLY_TOOLS = new Set([
|
|
23
|
+
// Chrome DevTools MCP — read-only
|
|
24
|
+
"mcp__chrome-devtools__take_screenshot",
|
|
25
|
+
"mcp__chrome-devtools__take_snapshot",
|
|
26
|
+
"mcp__chrome-devtools__take_memory_snapshot",
|
|
27
|
+
"mcp__chrome-devtools__list_pages",
|
|
28
|
+
"mcp__chrome-devtools__list_console_messages",
|
|
29
|
+
"mcp__chrome-devtools__list_network_requests",
|
|
30
|
+
"mcp__chrome-devtools__get_console_message",
|
|
31
|
+
"mcp__chrome-devtools__get_network_request",
|
|
32
|
+
"mcp__chrome-devtools__lighthouse_audit",
|
|
33
|
+
"mcp__chrome-devtools__performance_start_trace",
|
|
34
|
+
"mcp__chrome-devtools__performance_stop_trace",
|
|
35
|
+
"mcp__chrome-devtools__performance_analyze_insight",
|
|
36
|
+
// Playwright MCP — read-only
|
|
37
|
+
"mcp__playwright__browser_snapshot",
|
|
38
|
+
"mcp__playwright__browser_console_messages",
|
|
39
|
+
"mcp__playwright__browser_network_requests",
|
|
40
|
+
"mcp__playwright__browser_tabs",
|
|
41
|
+
"mcp__playwright__browser_take_screenshot",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// ── Helper: check if a tool name belongs to a browser MCP server ─────
|
|
45
|
+
|
|
46
|
+
export function isBrowserTool(toolName: string): boolean {
|
|
47
|
+
return (
|
|
48
|
+
toolName.startsWith("mcp__chrome-devtools__") ||
|
|
49
|
+
toolName.startsWith("mcp__playwright__")
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isBrowserReadOnly(toolName: string): boolean {
|
|
54
|
+
return BROWSER_READ_ONLY_TOOLS.has(toolName);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Config builder ───────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function parseExtraArgs(config: string | null): string[] {
|
|
60
|
+
if (!config) return [];
|
|
61
|
+
const trimmed = config.trim();
|
|
62
|
+
if (!trimmed) return [];
|
|
63
|
+
|
|
64
|
+
// Try JSON array first (e.g. '["--browser", "firefox"]')
|
|
65
|
+
if (trimmed.startsWith("[")) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(trimmed);
|
|
68
|
+
if (Array.isArray(parsed)) return parsed.filter((a): a is string => typeof a === "string");
|
|
69
|
+
} catch {
|
|
70
|
+
// Fall through to space-split
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Plain string: split on whitespace (e.g. "--headless --browser-url http://localhost:9222")
|
|
75
|
+
return trimmed.split(/\s+/).filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read browser MCP settings from DB and return MCP server configs
|
|
80
|
+
* for any enabled browser servers.
|
|
81
|
+
*
|
|
82
|
+
* Returns `{}` when neither server is enabled — zero overhead.
|
|
83
|
+
*/
|
|
84
|
+
export async function getBrowserMcpServers(): Promise<Record<string, McpStdioConfig>> {
|
|
85
|
+
const [chromeEnabled, playwrightEnabled, chromeConfig, playwrightConfig] =
|
|
86
|
+
await Promise.all([
|
|
87
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
|
|
88
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
|
|
89
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_CONFIG),
|
|
90
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_CONFIG),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const servers: Record<string, McpStdioConfig> = {};
|
|
94
|
+
|
|
95
|
+
if (chromeEnabled === "true") {
|
|
96
|
+
const extraArgs = parseExtraArgs(chromeConfig);
|
|
97
|
+
servers["chrome-devtools"] = {
|
|
98
|
+
command: "npx",
|
|
99
|
+
args: ["-y", "chrome-devtools-mcp@latest", ...extraArgs],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (playwrightEnabled === "true") {
|
|
104
|
+
const extraArgs = parseExtraArgs(playwrightConfig);
|
|
105
|
+
servers.playwright = {
|
|
106
|
+
command: "npx",
|
|
107
|
+
args: ["-y", "@playwright/mcp@latest", ...extraArgs],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return servers;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the allowedTools glob patterns for enabled browser MCP servers.
|
|
116
|
+
* Returns an empty array when no browser servers are enabled.
|
|
117
|
+
*/
|
|
118
|
+
export async function getBrowserAllowedToolPatterns(): Promise<string[]> {
|
|
119
|
+
const [chromeEnabled, playwrightEnabled] = await Promise.all([
|
|
120
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
|
|
121
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const patterns: string[] = [];
|
|
125
|
+
if (chromeEnabled === "true") patterns.push("mcp__chrome-devtools__*");
|
|
126
|
+
if (playwrightEnabled === "true") patterns.push("mcp__playwright__*");
|
|
127
|
+
return patterns;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Exa Search MCP — semantic web search ────────────────────────────
|
|
131
|
+
|
|
132
|
+
/** All Exa tools are read-only (search, similarity, content fetch) */
|
|
133
|
+
export const EXA_READ_ONLY_TOOLS = new Set([
|
|
134
|
+
"mcp__exa__web_search_exa",
|
|
135
|
+
"mcp__exa__find_similar",
|
|
136
|
+
"mcp__exa__get_contents",
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
export function isExaTool(toolName: string): boolean {
|
|
140
|
+
return toolName.startsWith("mcp__exa__");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function isExaReadOnly(toolName: string): boolean {
|
|
144
|
+
return EXA_READ_ONLY_TOOLS.has(toolName);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read external MCP server settings from DB and return configs
|
|
149
|
+
* for any enabled servers. Currently supports Exa Search.
|
|
150
|
+
*
|
|
151
|
+
* Returns `{}` when nothing is enabled — zero overhead.
|
|
152
|
+
*/
|
|
153
|
+
export async function getExternalMcpServers(): Promise<Record<string, AnyMcpServerConfig>> {
|
|
154
|
+
const exaEnabled = await getSetting(SETTINGS_KEYS.EXA_SEARCH_MCP_ENABLED);
|
|
155
|
+
|
|
156
|
+
const servers: Record<string, AnyMcpServerConfig> = {};
|
|
157
|
+
|
|
158
|
+
if (exaEnabled === "true") {
|
|
159
|
+
servers.exa = { type: "http", url: "https://mcp.exa.ai/mcp" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return servers;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build the allowedTools glob patterns for enabled external MCP servers.
|
|
167
|
+
*/
|
|
168
|
+
export async function getExternalAllowedToolPatterns(): Promise<string[]> {
|
|
169
|
+
const exaEnabled = await getSetting(SETTINGS_KEYS.EXA_SEARCH_MCP_ENABLED);
|
|
170
|
+
|
|
171
|
+
const patterns: string[] = [];
|
|
172
|
+
if (exaEnabled === "true") patterns.push("mcp__exa__*");
|
|
173
|
+
return patterns;
|
|
174
|
+
}
|