stagent 0.10.0 → 0.11.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 +44 -31
- package/dist/cli.js +24 -0
- package/docs/.coverage-gaps.json +154 -24
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +16 -2
- package/docs/features/settings.md +2 -2
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +3 -1
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +6 -2
- package/docs/getting-started.md +1 -1
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +25 -2
- package/docs/journeys/personal-use.md +12 -5
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -3
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/schedules/[id]/execute/route.ts +2 -2
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -12
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/documents/page.tsx +4 -1
- package/src/app/settings/page.tsx +2 -0
- package/src/components/book/content-blocks.tsx +1 -1
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +668 -47
- package/src/components/chat/chat-input.tsx +103 -8
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +73 -3
- package/src/components/chat/chat-shell.tsx +62 -3
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/playbook/playbook-detail-view.tsx +1 -1
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/command-palette.tsx +262 -2
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/tasks/task-bento-grid.tsx +12 -2
- package/src/components/tasks/task-card.tsx +3 -0
- package/src/components/tasks/task-chip-bar.tsx +30 -1
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +105 -46
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +97 -22
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex.ts +35 -0
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +30 -7
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +141 -1
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +109 -15
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +6 -19
- package/src/lib/chat/stream-telemetry.ts +9 -4
- package/src/lib/chat/system-prompt.ts +22 -0
- package/src/lib/chat/tool-catalog.ts +33 -3
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/helpers.ts +2 -0
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/task-tools.ts +6 -2
- package/src/lib/chat/tools/workflow-tools.ts +61 -20
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +2 -0
- package/src/lib/data/clear.ts +2 -6
- package/src/lib/db/bootstrap.ts +17 -0
- package/src/lib/db/schema.ts +26 -0
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +56 -2
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/instance/__tests__/detect.test.ts +1 -1
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
- package/src/lib/instance/fingerprint.ts +8 -10
- package/src/lib/instance/upgrade-poller.ts +53 -1
- package/src/lib/schedules/scheduler.ts +4 -4
- package/src/lib/utils/stagent-paths.ts +4 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +6 -0
- package/src/lib/workflows/engine.ts +5 -3
- package/src/test/setup.ts +10 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRuntimeCatalogEntry,
|
|
3
|
+
type AgentRuntimeId,
|
|
4
|
+
} from "@/lib/agents/runtime/catalog";
|
|
5
|
+
|
|
6
|
+
export interface RuntimeLaunchProgress {
|
|
7
|
+
hasTurnStarted?: boolean;
|
|
8
|
+
hasToolUse?: boolean;
|
|
9
|
+
hasResult?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class RetryableRuntimeLaunchError extends Error {
|
|
13
|
+
runtimeId: AgentRuntimeId;
|
|
14
|
+
cause: unknown;
|
|
15
|
+
|
|
16
|
+
constructor(input: {
|
|
17
|
+
runtimeId: AgentRuntimeId;
|
|
18
|
+
message: string;
|
|
19
|
+
cause: unknown;
|
|
20
|
+
}) {
|
|
21
|
+
super(input.message);
|
|
22
|
+
this.name = "RetryableRuntimeLaunchError";
|
|
23
|
+
this.runtimeId = input.runtimeId;
|
|
24
|
+
this.cause = input.cause;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isLikelyRuntimeUnavailableMessage(message: string): boolean {
|
|
29
|
+
const lower = message.toLowerCase();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
lower.includes("process exited with code") ||
|
|
33
|
+
lower.includes("command not found") ||
|
|
34
|
+
lower.includes("enoent") ||
|
|
35
|
+
lower.includes("not logged in") ||
|
|
36
|
+
lower.includes("authentication") ||
|
|
37
|
+
lower.includes("oauth") ||
|
|
38
|
+
lower.includes("token expired") ||
|
|
39
|
+
lower.includes("api key") ||
|
|
40
|
+
lower.includes("chatgpt sign-in is not configured") ||
|
|
41
|
+
lower.includes("failed to start") ||
|
|
42
|
+
lower.includes("runtime unavailable")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function classifyTaskFailureReason(error: unknown): string {
|
|
47
|
+
if (!(error instanceof Error)) return "sdk_error";
|
|
48
|
+
if (error.name === "AbortError" || error.message.includes("aborted")) {
|
|
49
|
+
return "aborted";
|
|
50
|
+
}
|
|
51
|
+
const lower = error.message.toLowerCase();
|
|
52
|
+
if (
|
|
53
|
+
lower.includes("turn") &&
|
|
54
|
+
(lower.includes("limit") || lower.includes("exhausted") || lower.includes("max"))
|
|
55
|
+
) {
|
|
56
|
+
return "turn_limit_exceeded";
|
|
57
|
+
}
|
|
58
|
+
if (lower.includes("timeout") || lower.includes("timed out")) return "timeout";
|
|
59
|
+
if (lower.includes("budget")) return "budget_exceeded";
|
|
60
|
+
if (
|
|
61
|
+
lower.includes("authentication") ||
|
|
62
|
+
lower.includes("oauth") ||
|
|
63
|
+
lower.includes("not logged in") ||
|
|
64
|
+
lower.includes("token expired")
|
|
65
|
+
) {
|
|
66
|
+
return "auth_failed";
|
|
67
|
+
}
|
|
68
|
+
if (lower.includes("rate limit") || lower.includes("429")) {
|
|
69
|
+
return "rate_limited";
|
|
70
|
+
}
|
|
71
|
+
return "sdk_error";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function toRetryableRuntimeLaunchError(input: {
|
|
75
|
+
runtimeId: AgentRuntimeId;
|
|
76
|
+
error: unknown;
|
|
77
|
+
progress: RuntimeLaunchProgress;
|
|
78
|
+
}): RetryableRuntimeLaunchError | null {
|
|
79
|
+
if (!(input.error instanceof Error)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
input.progress.hasTurnStarted ||
|
|
85
|
+
input.progress.hasToolUse ||
|
|
86
|
+
input.progress.hasResult
|
|
87
|
+
) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!isLikelyRuntimeUnavailableMessage(input.error.message)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const label = getRuntimeCatalogEntry(input.runtimeId).label;
|
|
96
|
+
return new RetryableRuntimeLaunchError({
|
|
97
|
+
runtimeId: input.runtimeId,
|
|
98
|
+
message: `${label} failed to launch before task execution started: ${input.error.message}`,
|
|
99
|
+
cause: input.error,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -23,6 +23,12 @@ import {
|
|
|
23
23
|
readCodexAuthStateFromClient,
|
|
24
24
|
resolveOpenAICodexAuthContext,
|
|
25
25
|
} from "./openai-codex-auth";
|
|
26
|
+
import {
|
|
27
|
+
classifyTaskFailureReason,
|
|
28
|
+
RetryableRuntimeLaunchError,
|
|
29
|
+
toRetryableRuntimeLaunchError,
|
|
30
|
+
type RuntimeLaunchProgress,
|
|
31
|
+
} from "./launch-failure";
|
|
26
32
|
import type {
|
|
27
33
|
AgentRuntimeAdapter,
|
|
28
34
|
RuntimeConnectionResult,
|
|
@@ -228,6 +234,14 @@ async function finalizeTaskUsage(
|
|
|
228
234
|
startedAt: state.startedAt,
|
|
229
235
|
finishedAt: new Date(),
|
|
230
236
|
});
|
|
237
|
+
|
|
238
|
+
await db
|
|
239
|
+
.update(tasks)
|
|
240
|
+
.set({
|
|
241
|
+
effectiveModelId: state.modelId ?? null,
|
|
242
|
+
updatedAt: new Date(),
|
|
243
|
+
})
|
|
244
|
+
.where(eq(tasks.id, state.taskId));
|
|
231
245
|
}
|
|
232
246
|
|
|
233
247
|
function buildTurnInput(prompt: string) {
|
|
@@ -373,6 +387,7 @@ async function markTaskFailed(taskId: string, title: string, message: string) {
|
|
|
373
387
|
.set({
|
|
374
388
|
status: "failed",
|
|
375
389
|
result: message,
|
|
390
|
+
failureReason: classifyTaskFailureReason(new Error(message)),
|
|
376
391
|
updatedAt: new Date(),
|
|
377
392
|
})
|
|
378
393
|
.where(eq(tasks.id, taskId));
|
|
@@ -690,6 +705,7 @@ async function executeOpenAICodexTask(
|
|
|
690
705
|
let turnId: string | null = null;
|
|
691
706
|
let agentOutput = "";
|
|
692
707
|
let settled = false;
|
|
708
|
+
const launchProgress: RuntimeLaunchProgress = {};
|
|
693
709
|
let resolveCompletion: (() => void) | null = null;
|
|
694
710
|
let rejectCompletion: ((error: Error) => void) | null = null;
|
|
695
711
|
const usageState = createTaskUsageState(task, Boolean(task.sessionId));
|
|
@@ -704,6 +720,18 @@ async function executeOpenAICodexTask(
|
|
|
704
720
|
client = await auth.connect(cwd);
|
|
705
721
|
|
|
706
722
|
client.onProcessError = (error) => {
|
|
723
|
+
const retryableLaunchError =
|
|
724
|
+
!options.resume
|
|
725
|
+
? toRetryableRuntimeLaunchError({
|
|
726
|
+
runtimeId: "openai-codex-app-server",
|
|
727
|
+
error,
|
|
728
|
+
progress: launchProgress,
|
|
729
|
+
})
|
|
730
|
+
: null;
|
|
731
|
+
if (retryableLaunchError) {
|
|
732
|
+
rejectCompletion?.(retryableLaunchError);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
707
735
|
if (settled) return;
|
|
708
736
|
void settle(async () => {
|
|
709
737
|
await markTaskFailed(taskId, task.title, error.message);
|
|
@@ -744,6 +772,7 @@ async function executeOpenAICodexTask(
|
|
|
744
772
|
|
|
745
773
|
case "turn/started": {
|
|
746
774
|
turnId = extractTurnId(params);
|
|
775
|
+
launchProgress.hasTurnStarted = true;
|
|
747
776
|
setExecution(taskId, {
|
|
748
777
|
abortController,
|
|
749
778
|
sessionId: threadId,
|
|
@@ -782,6 +811,7 @@ async function executeOpenAICodexTask(
|
|
|
782
811
|
}
|
|
783
812
|
|
|
784
813
|
case "item/commandExecution/outputDelta": {
|
|
814
|
+
launchProgress.hasToolUse = true;
|
|
785
815
|
await insertLog(taskId, "command_output_delta", {
|
|
786
816
|
threadId,
|
|
787
817
|
turnId,
|
|
@@ -803,6 +833,7 @@ async function executeOpenAICodexTask(
|
|
|
803
833
|
|
|
804
834
|
case "turn/completed": {
|
|
805
835
|
const { status, errorMessage } = extractTurnStatus(params);
|
|
836
|
+
launchProgress.hasResult = true;
|
|
806
837
|
|
|
807
838
|
if (status === "completed") {
|
|
808
839
|
const finalResult =
|
|
@@ -917,6 +948,10 @@ async function executeOpenAICodexTask(
|
|
|
917
948
|
return;
|
|
918
949
|
}
|
|
919
950
|
|
|
951
|
+
if (error instanceof RetryableRuntimeLaunchError) {
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
|
|
920
955
|
const message = error instanceof Error ? error.message : String(error);
|
|
921
956
|
await settle(async () => {
|
|
922
957
|
await markTaskFailed(taskId, task.title, message);
|
|
@@ -396,6 +396,14 @@ async function executeOpenAIDirectTask(taskId: string, isResume = false): Promis
|
|
|
396
396
|
startedAt: usageState.startedAt,
|
|
397
397
|
finishedAt: new Date(),
|
|
398
398
|
});
|
|
399
|
+
|
|
400
|
+
await db
|
|
401
|
+
.update(tasks)
|
|
402
|
+
.set({
|
|
403
|
+
effectiveModelId: result.totalUsage.modelId ?? modelId,
|
|
404
|
+
updatedAt: new Date(),
|
|
405
|
+
})
|
|
406
|
+
.where(eq(tasks.id, taskId));
|
|
399
407
|
} catch (err) {
|
|
400
408
|
if (!abortController.signal.aborted) {
|
|
401
409
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { db } from "@/lib/db";
|
|
2
|
+
import { agentLogs, notifications, tasks } from "@/lib/db/schema";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import { executeTaskWithRuntime, resumeTaskWithRuntime } from "@/lib/agents/runtime";
|
|
5
|
+
import {
|
|
6
|
+
resolveResumeExecutionTarget,
|
|
7
|
+
resolveTaskExecutionTarget,
|
|
8
|
+
type ResolvedExecutionTarget,
|
|
9
|
+
} from "@/lib/agents/runtime/execution-target";
|
|
10
|
+
import {
|
|
11
|
+
classifyTaskFailureReason,
|
|
12
|
+
RetryableRuntimeLaunchError,
|
|
13
|
+
} from "@/lib/agents/runtime/launch-failure";
|
|
14
|
+
import { getRuntimeCatalogEntry } from "@/lib/agents/runtime/catalog";
|
|
15
|
+
|
|
16
|
+
async function persistExecutionTarget(
|
|
17
|
+
taskId: string,
|
|
18
|
+
target: ResolvedExecutionTarget
|
|
19
|
+
) {
|
|
20
|
+
await db
|
|
21
|
+
.update(tasks)
|
|
22
|
+
.set({
|
|
23
|
+
effectiveRuntimeId: target.effectiveRuntimeId,
|
|
24
|
+
effectiveModelId: target.effectiveModelId,
|
|
25
|
+
runtimeFallbackReason: target.fallbackReason,
|
|
26
|
+
updatedAt: new Date(),
|
|
27
|
+
})
|
|
28
|
+
.where(eq(tasks.id, taskId));
|
|
29
|
+
|
|
30
|
+
if (target.fallbackApplied && target.fallbackReason) {
|
|
31
|
+
await db.insert(agentLogs).values({
|
|
32
|
+
id: crypto.randomUUID(),
|
|
33
|
+
taskId,
|
|
34
|
+
agentType: "runtime-router",
|
|
35
|
+
event: "runtime_fallback",
|
|
36
|
+
payload: JSON.stringify({
|
|
37
|
+
requestedRuntimeId: target.requestedRuntimeId,
|
|
38
|
+
effectiveRuntimeId: target.effectiveRuntimeId,
|
|
39
|
+
reason: target.fallbackReason,
|
|
40
|
+
}),
|
|
41
|
+
timestamp: new Date(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function logRuntimeLaunchFailure(
|
|
47
|
+
taskId: string,
|
|
48
|
+
error: RetryableRuntimeLaunchError
|
|
49
|
+
) {
|
|
50
|
+
await db.insert(agentLogs).values({
|
|
51
|
+
id: crypto.randomUUID(),
|
|
52
|
+
taskId,
|
|
53
|
+
agentType: "runtime-router",
|
|
54
|
+
event: "runtime_launch_failed",
|
|
55
|
+
payload: JSON.stringify({
|
|
56
|
+
runtimeId: error.runtimeId,
|
|
57
|
+
error: error.message,
|
|
58
|
+
}),
|
|
59
|
+
timestamp: new Date(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function markTaskLaunchFailed(
|
|
64
|
+
taskId: string,
|
|
65
|
+
taskTitle: string,
|
|
66
|
+
error: unknown
|
|
67
|
+
) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
await db
|
|
70
|
+
.update(tasks)
|
|
71
|
+
.set({
|
|
72
|
+
status: "failed",
|
|
73
|
+
result: message,
|
|
74
|
+
failureReason: classifyTaskFailureReason(
|
|
75
|
+
error instanceof Error ? error : new Error(message)
|
|
76
|
+
),
|
|
77
|
+
sessionId: null,
|
|
78
|
+
updatedAt: new Date(),
|
|
79
|
+
})
|
|
80
|
+
.where(eq(tasks.id, taskId));
|
|
81
|
+
|
|
82
|
+
await db.insert(notifications).values({
|
|
83
|
+
id: crypto.randomUUID(),
|
|
84
|
+
taskId,
|
|
85
|
+
type: "task_failed",
|
|
86
|
+
title: `Task failed: ${taskTitle}`,
|
|
87
|
+
body: message.slice(0, 500),
|
|
88
|
+
createdAt: new Date(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildLaunchFallbackTarget(input: {
|
|
93
|
+
originalTarget: ResolvedExecutionTarget;
|
|
94
|
+
retryTarget: ResolvedExecutionTarget;
|
|
95
|
+
launchError: RetryableRuntimeLaunchError;
|
|
96
|
+
}): ResolvedExecutionTarget {
|
|
97
|
+
const effectiveLabel = getRuntimeCatalogEntry(
|
|
98
|
+
input.retryTarget.effectiveRuntimeId
|
|
99
|
+
).label;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
...input.retryTarget,
|
|
103
|
+
fallbackApplied: true,
|
|
104
|
+
fallbackReason: `${input.launchError.message}. Fell back to ${effectiveLabel}.`,
|
|
105
|
+
requestedRuntimeId:
|
|
106
|
+
input.retryTarget.requestedRuntimeId ?? input.originalTarget.requestedRuntimeId,
|
|
107
|
+
requestedModelId:
|
|
108
|
+
input.retryTarget.requestedModelId ?? input.originalTarget.requestedModelId,
|
|
109
|
+
effectiveModelId: input.retryTarget.effectiveModelId,
|
|
110
|
+
effectiveRuntimeId: input.retryTarget.effectiveRuntimeId,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function retryTaskWithFallback(
|
|
115
|
+
task: typeof tasks.$inferSelect,
|
|
116
|
+
originalTarget: ResolvedExecutionTarget,
|
|
117
|
+
launchError: RetryableRuntimeLaunchError
|
|
118
|
+
) {
|
|
119
|
+
await logRuntimeLaunchFailure(task.id, launchError);
|
|
120
|
+
|
|
121
|
+
let retryTarget: ResolvedExecutionTarget;
|
|
122
|
+
try {
|
|
123
|
+
retryTarget = await resolveTaskExecutionTarget({
|
|
124
|
+
title: task.title,
|
|
125
|
+
description: task.description,
|
|
126
|
+
requestedRuntimeId: originalTarget.requestedRuntimeId ?? task.assignedAgent,
|
|
127
|
+
profileId: task.agentProfile,
|
|
128
|
+
unavailableRuntimeIds: [launchError.runtimeId],
|
|
129
|
+
unavailableReasons: {
|
|
130
|
+
[launchError.runtimeId]: launchError.message,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
await markTaskLaunchFailed(task.id, task.title, error);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fallbackTarget = buildLaunchFallbackTarget({
|
|
139
|
+
originalTarget,
|
|
140
|
+
retryTarget,
|
|
141
|
+
launchError,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await db
|
|
145
|
+
.update(tasks)
|
|
146
|
+
.set({
|
|
147
|
+
status: "running",
|
|
148
|
+
result: null,
|
|
149
|
+
failureReason: null,
|
|
150
|
+
sessionId: null,
|
|
151
|
+
updatedAt: new Date(),
|
|
152
|
+
})
|
|
153
|
+
.where(eq(tasks.id, task.id));
|
|
154
|
+
|
|
155
|
+
await persistExecutionTarget(task.id, fallbackTarget);
|
|
156
|
+
try {
|
|
157
|
+
return await executeTaskWithRuntime(task.id, fallbackTarget.effectiveRuntimeId);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof RetryableRuntimeLaunchError) {
|
|
160
|
+
await markTaskLaunchFailed(task.id, task.title, error);
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function startTaskExecution(
|
|
167
|
+
taskId: string,
|
|
168
|
+
options?: { requestedRuntimeId?: string | null }
|
|
169
|
+
) {
|
|
170
|
+
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
|
|
171
|
+
if (!task) {
|
|
172
|
+
throw new Error(`Task ${taskId} not found`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const target = await resolveTaskExecutionTarget({
|
|
176
|
+
title: task.title,
|
|
177
|
+
description: task.description,
|
|
178
|
+
requestedRuntimeId: options?.requestedRuntimeId ?? task.assignedAgent,
|
|
179
|
+
profileId: task.agentProfile,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await db
|
|
183
|
+
.update(tasks)
|
|
184
|
+
.set({ status: "running", updatedAt: new Date() })
|
|
185
|
+
.where(eq(tasks.id, taskId));
|
|
186
|
+
await persistExecutionTarget(taskId, target);
|
|
187
|
+
try {
|
|
188
|
+
return await executeTaskWithRuntime(taskId, target.effectiveRuntimeId);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof RetryableRuntimeLaunchError) {
|
|
191
|
+
return retryTaskWithFallback(task, target, error);
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function resumeTaskExecution(
|
|
198
|
+
taskId: string,
|
|
199
|
+
options?: {
|
|
200
|
+
requestedRuntimeId?: string | null;
|
|
201
|
+
effectiveRuntimeId?: string | null;
|
|
202
|
+
}
|
|
203
|
+
) {
|
|
204
|
+
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
|
|
205
|
+
if (!task) {
|
|
206
|
+
throw new Error(`Task ${taskId} not found`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const target = await resolveResumeExecutionTarget({
|
|
210
|
+
requestedRuntimeId: options?.requestedRuntimeId ?? task.assignedAgent,
|
|
211
|
+
effectiveRuntimeId: options?.effectiveRuntimeId ?? task.effectiveRuntimeId,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await db
|
|
215
|
+
.update(tasks)
|
|
216
|
+
.set({ status: "running", updatedAt: new Date() })
|
|
217
|
+
.where(eq(tasks.id, taskId));
|
|
218
|
+
await persistExecutionTarget(taskId, target);
|
|
219
|
+
return resumeTaskWithRuntime(taskId, target.effectiveRuntimeId);
|
|
220
|
+
}
|
|
@@ -12,6 +12,7 @@ import { notifications } from "@/lib/db/schema";
|
|
|
12
12
|
import { eq } from "drizzle-orm";
|
|
13
13
|
import type { CanUseToolPolicy } from "./profiles/types";
|
|
14
14
|
import { isExaTool, isExaReadOnly } from "./browser-mcp";
|
|
15
|
+
import { CLAUDE_SDK_READ_ONLY_FS_TOOLS } from "./runtime/claude-sdk";
|
|
15
16
|
|
|
16
17
|
// ── Types ────────────────────────────────────────────────────────────
|
|
17
18
|
|
|
@@ -120,7 +121,10 @@ export async function handleToolPermission(
|
|
|
120
121
|
): Promise<ToolPermissionResponse> {
|
|
121
122
|
const isQuestion = toolName === "AskUserQuestion";
|
|
122
123
|
|
|
123
|
-
// Layer 1: Profile-level canUseToolPolicy — fastest check, no I/O
|
|
124
|
+
// Layer 1: Profile-level canUseToolPolicy — fastest check, no I/O.
|
|
125
|
+
// Runs BEFORE Layer 1.75's SDK filesystem auto-allow so `autoDeny: ["Read"]`
|
|
126
|
+
// still denies; `autoApprove` for Read/Grep/Glob is redundant (Layer 1.75
|
|
127
|
+
// would also allow) but harmless.
|
|
124
128
|
if (!isQuestion && canUseToolPolicy) {
|
|
125
129
|
if (canUseToolPolicy.autoApprove?.includes(toolName)) {
|
|
126
130
|
return buildAllowedToolPermissionResponse(input);
|
|
@@ -135,6 +139,17 @@ export async function handleToolPermission(
|
|
|
135
139
|
return buildAllowedToolPermissionResponse(input);
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
// Layer 1.75: SDK filesystem read-only tools and Skill invocations —
|
|
143
|
+
// auto-approve without I/O. Mirrors the chat-side Phase 1a policy
|
|
144
|
+
// (src/lib/chat/engine.ts canUseTool). Read/Grep/Glob are non-destructive;
|
|
145
|
+
// Skill load is equivalent to using `claude` CLI directly — any tool the
|
|
146
|
+
// loaded skill subsequently invokes (Bash, Edit, etc.) goes through this
|
|
147
|
+
// same canUseTool check. See features/chat-claude-sdk-skills.md Error
|
|
148
|
+
// & Rescue Registry row "settingSources loads hostile skill."
|
|
149
|
+
if (!isQuestion && (CLAUDE_SDK_READ_ONLY_FS_TOOLS.has(toolName) || toolName === "Skill")) {
|
|
150
|
+
return buildAllowedToolPermissionResponse(input);
|
|
151
|
+
}
|
|
152
|
+
|
|
138
153
|
// Layer 2: Saved user permissions — skip notification for pre-approved tools
|
|
139
154
|
if (!isQuestion) {
|
|
140
155
|
const { isToolAllowed } = await import("@/lib/settings/permissions");
|