stagent 0.10.0 → 0.11.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 +15 -2
- 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/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-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 +1 -1
- 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/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/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 +18 -0
- 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 +54 -0
- 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__/upgrade-poller.test.ts +50 -0
- package/src/lib/instance/fingerprint.ts +7 -9
- package/src/lib/instance/upgrade-poller.ts +53 -1
- package/src/lib/schedules/scheduler.ts +4 -4
- 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,456 @@
|
|
|
1
|
+
import { getProfile } from "@/lib/agents/profiles/registry";
|
|
2
|
+
import { profileSupportsRuntime } from "@/lib/agents/profiles/compatibility";
|
|
3
|
+
import { suggestRuntime } from "@/lib/agents/router";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_AGENT_RUNTIME,
|
|
6
|
+
getRuntimeCatalogEntry,
|
|
7
|
+
getRuntimeFeatures,
|
|
8
|
+
resolveAgentRuntime,
|
|
9
|
+
type AgentRuntimeId,
|
|
10
|
+
} from "./catalog";
|
|
11
|
+
import { testRuntimeConnection } from "./index";
|
|
12
|
+
import { getRoutingPreference } from "@/lib/settings/routing";
|
|
13
|
+
import { getRuntimeSetupStates, listConfiguredRuntimeIds } from "@/lib/settings/runtime-setup";
|
|
14
|
+
import { DEFAULT_CHAT_MODEL, getRuntimeForModel } from "@/lib/chat/types";
|
|
15
|
+
|
|
16
|
+
const FILESYSTEM_TOOL_NAMES = new Set([
|
|
17
|
+
"Read",
|
|
18
|
+
"Write",
|
|
19
|
+
"Edit",
|
|
20
|
+
"MultiEdit",
|
|
21
|
+
"Grep",
|
|
22
|
+
"Glob",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const CHAT_MODEL_FALLBACKS: Record<string, string[]> = {
|
|
26
|
+
haiku: ["gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.4"],
|
|
27
|
+
sonnet: ["gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini"],
|
|
28
|
+
opus: ["gpt-5.4", "gpt-5.3-codex", "gpt-5.4-mini"],
|
|
29
|
+
"gpt-5.4-mini": ["haiku", "sonnet", "opus"],
|
|
30
|
+
"gpt-5.3-codex": ["sonnet", "haiku", "opus"],
|
|
31
|
+
"gpt-5.4": ["opus", "sonnet", "haiku"],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export class RuntimeUnavailableError extends Error {
|
|
35
|
+
constructor(message: string) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "RuntimeUnavailableError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class RequestedModelUnavailableError extends Error {
|
|
42
|
+
constructor(message: string) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "RequestedModelUnavailableError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class NoCompatibleRuntimeError extends Error {
|
|
49
|
+
constructor(message: string) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "NoCompatibleRuntimeError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ResolvedExecutionTarget {
|
|
56
|
+
requestedRuntimeId: AgentRuntimeId | null;
|
|
57
|
+
effectiveRuntimeId: AgentRuntimeId;
|
|
58
|
+
requestedModelId: string | null;
|
|
59
|
+
effectiveModelId: string | null;
|
|
60
|
+
fallbackApplied: boolean;
|
|
61
|
+
fallbackReason: string | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type RuntimeRequirements = {
|
|
65
|
+
requiresBash: boolean;
|
|
66
|
+
requiresFilesystem: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type RuntimeAvailability = {
|
|
70
|
+
available: boolean;
|
|
71
|
+
reason: string | null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function unique<T>(values: T[]): T[] {
|
|
75
|
+
return Array.from(new Set(values));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getRuntimeLabel(runtimeId: AgentRuntimeId): string {
|
|
79
|
+
return getRuntimeCatalogEntry(runtimeId).label;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function detectRuntimeRequirements(profileId?: string | null): RuntimeRequirements {
|
|
83
|
+
const profile = profileId ? getProfile(profileId) : undefined;
|
|
84
|
+
const allowedTools = profile?.allowedTools ?? [];
|
|
85
|
+
|
|
86
|
+
const requiresBash = allowedTools.some(
|
|
87
|
+
(tool) => tool === "Bash" || tool.startsWith("Bash(")
|
|
88
|
+
);
|
|
89
|
+
const requiresFilesystem =
|
|
90
|
+
requiresBash ||
|
|
91
|
+
allowedTools.some((tool) => FILESYSTEM_TOOL_NAMES.has(tool));
|
|
92
|
+
|
|
93
|
+
return { requiresBash, requiresFilesystem };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function runtimeMeetsRequirements(
|
|
97
|
+
runtimeId: AgentRuntimeId,
|
|
98
|
+
requirements: RuntimeRequirements
|
|
99
|
+
): boolean {
|
|
100
|
+
const features = getRuntimeFeatures(runtimeId);
|
|
101
|
+
if (requirements.requiresBash && !features.hasBash) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (requirements.requiresFilesystem && !features.hasFilesystemTools) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function filterCompatibleRuntimes(
|
|
111
|
+
runtimeIds: AgentRuntimeId[],
|
|
112
|
+
profileId?: string | null
|
|
113
|
+
): AgentRuntimeId[] {
|
|
114
|
+
if (!profileId) {
|
|
115
|
+
return runtimeIds;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const profile = getProfile(profileId);
|
|
119
|
+
if (!profile) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return runtimeIds.filter((runtimeId) =>
|
|
124
|
+
profileSupportsRuntime(profile, runtimeId)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function checkRuntimeAvailability(
|
|
129
|
+
runtimeId: AgentRuntimeId
|
|
130
|
+
): Promise<RuntimeAvailability> {
|
|
131
|
+
const states = await getRuntimeSetupStates();
|
|
132
|
+
if (!states[runtimeId]?.configured) {
|
|
133
|
+
return {
|
|
134
|
+
available: false,
|
|
135
|
+
reason: `${getRuntimeLabel(runtimeId)} is not configured`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const connection = await testRuntimeConnection(runtimeId);
|
|
141
|
+
if (connection.connected) {
|
|
142
|
+
return { available: true, reason: null };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
available: false,
|
|
146
|
+
reason:
|
|
147
|
+
connection.error ??
|
|
148
|
+
`${getRuntimeLabel(runtimeId)} is unavailable`,
|
|
149
|
+
};
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
available: false,
|
|
153
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function getConfiguredCandidateRuntimes(
|
|
159
|
+
profileId?: string | null
|
|
160
|
+
): Promise<AgentRuntimeId[]> {
|
|
161
|
+
const states = await getRuntimeSetupStates();
|
|
162
|
+
return filterCompatibleRuntimes(
|
|
163
|
+
listConfiguredRuntimeIds(states) as AgentRuntimeId[],
|
|
164
|
+
profileId
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildTaskFallbackOrder(input: {
|
|
169
|
+
title: string;
|
|
170
|
+
description?: string | null;
|
|
171
|
+
profileId?: string | null;
|
|
172
|
+
requestedRuntimeId: AgentRuntimeId | null;
|
|
173
|
+
compatibleRuntimeIds: AgentRuntimeId[];
|
|
174
|
+
}): AgentRuntimeId[] {
|
|
175
|
+
const alternates = input.compatibleRuntimeIds.filter(
|
|
176
|
+
(runtimeId) => runtimeId !== input.requestedRuntimeId
|
|
177
|
+
);
|
|
178
|
+
if (alternates.length === 0) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const preferred = suggestRuntime(
|
|
183
|
+
input.title,
|
|
184
|
+
input.description,
|
|
185
|
+
input.profileId,
|
|
186
|
+
alternates,
|
|
187
|
+
"quality"
|
|
188
|
+
).runtimeId;
|
|
189
|
+
|
|
190
|
+
return unique([
|
|
191
|
+
preferred,
|
|
192
|
+
...alternates.filter(
|
|
193
|
+
(runtimeId) =>
|
|
194
|
+
input.requestedRuntimeId != null &&
|
|
195
|
+
getRuntimeCatalogEntry(runtimeId).providerId ===
|
|
196
|
+
getRuntimeCatalogEntry(input.requestedRuntimeId).providerId
|
|
197
|
+
),
|
|
198
|
+
...alternates,
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildRuntimeFallbackReason(input: {
|
|
203
|
+
requestedRuntimeId: AgentRuntimeId | null;
|
|
204
|
+
effectiveRuntimeId: AgentRuntimeId;
|
|
205
|
+
unavailableReason: string | null;
|
|
206
|
+
}): string | null {
|
|
207
|
+
if (!input.requestedRuntimeId) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const requestedLabel = getRuntimeLabel(input.requestedRuntimeId);
|
|
212
|
+
const effectiveLabel = getRuntimeLabel(input.effectiveRuntimeId);
|
|
213
|
+
const reason = input.unavailableReason ?? `${requestedLabel} is unavailable`;
|
|
214
|
+
return `${reason}. Fell back to ${effectiveLabel}.`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function resolveTaskExecutionTarget(input: {
|
|
218
|
+
title: string;
|
|
219
|
+
description?: string | null;
|
|
220
|
+
requestedRuntimeId?: string | null;
|
|
221
|
+
profileId?: string | null;
|
|
222
|
+
unavailableRuntimeIds?: string[];
|
|
223
|
+
unavailableReasons?: Record<string, string>;
|
|
224
|
+
}): Promise<ResolvedExecutionTarget> {
|
|
225
|
+
const requestedRuntimeId = input.requestedRuntimeId
|
|
226
|
+
? resolveAgentRuntime(input.requestedRuntimeId)
|
|
227
|
+
: null;
|
|
228
|
+
const requirements = detectRuntimeRequirements(input.profileId);
|
|
229
|
+
const unavailableRuntimeIds = new Set(
|
|
230
|
+
(input.unavailableRuntimeIds ?? []).map((runtimeId) =>
|
|
231
|
+
resolveAgentRuntime(runtimeId)
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
const configuredCandidates = await getConfiguredCandidateRuntimes(input.profileId);
|
|
235
|
+
const compatibleCandidates = configuredCandidates.filter((runtimeId) =>
|
|
236
|
+
runtimeMeetsRequirements(runtimeId, requirements)
|
|
237
|
+
);
|
|
238
|
+
const launchableCandidates = compatibleCandidates.filter(
|
|
239
|
+
(runtimeId) => !unavailableRuntimeIds.has(runtimeId)
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (compatibleCandidates.length === 0) {
|
|
243
|
+
throw new NoCompatibleRuntimeError(
|
|
244
|
+
"No compatible configured runtime is available for this task."
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (requestedRuntimeId) {
|
|
249
|
+
if (
|
|
250
|
+
compatibleCandidates.includes(requestedRuntimeId) &&
|
|
251
|
+
!unavailableRuntimeIds.has(requestedRuntimeId) &&
|
|
252
|
+
(await checkRuntimeAvailability(requestedRuntimeId)).available
|
|
253
|
+
) {
|
|
254
|
+
return {
|
|
255
|
+
requestedRuntimeId,
|
|
256
|
+
effectiveRuntimeId: requestedRuntimeId,
|
|
257
|
+
requestedModelId: null,
|
|
258
|
+
effectiveModelId: null,
|
|
259
|
+
fallbackApplied: false,
|
|
260
|
+
fallbackReason: null,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const availability = unavailableRuntimeIds.has(requestedRuntimeId)
|
|
265
|
+
? {
|
|
266
|
+
available: false,
|
|
267
|
+
reason:
|
|
268
|
+
input.unavailableReasons?.[requestedRuntimeId] ??
|
|
269
|
+
`${getRuntimeLabel(requestedRuntimeId)} is temporarily unavailable`,
|
|
270
|
+
}
|
|
271
|
+
: compatibleCandidates.includes(requestedRuntimeId)
|
|
272
|
+
? await checkRuntimeAvailability(requestedRuntimeId)
|
|
273
|
+
: {
|
|
274
|
+
available: false,
|
|
275
|
+
reason: `${getRuntimeLabel(requestedRuntimeId)} does not support this task/profile`,
|
|
276
|
+
};
|
|
277
|
+
const fallbackOrder = buildTaskFallbackOrder({
|
|
278
|
+
title: input.title,
|
|
279
|
+
description: input.description,
|
|
280
|
+
profileId: input.profileId,
|
|
281
|
+
requestedRuntimeId,
|
|
282
|
+
compatibleRuntimeIds: launchableCandidates,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
for (const candidate of fallbackOrder) {
|
|
286
|
+
const candidateAvailability = await checkRuntimeAvailability(candidate);
|
|
287
|
+
if (candidateAvailability.available) {
|
|
288
|
+
return {
|
|
289
|
+
requestedRuntimeId,
|
|
290
|
+
effectiveRuntimeId: candidate,
|
|
291
|
+
requestedModelId: null,
|
|
292
|
+
effectiveModelId: null,
|
|
293
|
+
fallbackApplied: true,
|
|
294
|
+
fallbackReason: buildRuntimeFallbackReason({
|
|
295
|
+
requestedRuntimeId,
|
|
296
|
+
effectiveRuntimeId: candidate,
|
|
297
|
+
unavailableReason: availability.reason,
|
|
298
|
+
}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
throw new NoCompatibleRuntimeError(
|
|
304
|
+
availability.reason ??
|
|
305
|
+
`No healthy alternate runtime is available for ${getRuntimeLabel(requestedRuntimeId)}.`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const routingPreference = await getRoutingPreference();
|
|
310
|
+
const suggested = suggestRuntime(
|
|
311
|
+
input.title,
|
|
312
|
+
input.description,
|
|
313
|
+
input.profileId,
|
|
314
|
+
launchableCandidates,
|
|
315
|
+
routingPreference
|
|
316
|
+
).runtimeId;
|
|
317
|
+
const autoOrder = unique([
|
|
318
|
+
suggested,
|
|
319
|
+
...launchableCandidates,
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
for (const candidate of autoOrder) {
|
|
323
|
+
const availability = await checkRuntimeAvailability(candidate);
|
|
324
|
+
if (availability.available) {
|
|
325
|
+
return {
|
|
326
|
+
requestedRuntimeId: null,
|
|
327
|
+
effectiveRuntimeId: candidate,
|
|
328
|
+
requestedModelId: null,
|
|
329
|
+
effectiveModelId: null,
|
|
330
|
+
fallbackApplied: false,
|
|
331
|
+
fallbackReason: null,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
throw new RuntimeUnavailableError(
|
|
337
|
+
"No healthy runtime is currently available to execute this task."
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function resolveResumeExecutionTarget(input: {
|
|
342
|
+
requestedRuntimeId?: string | null;
|
|
343
|
+
effectiveRuntimeId?: string | null;
|
|
344
|
+
}): Promise<ResolvedExecutionTarget> {
|
|
345
|
+
const requestedRuntimeId = input.requestedRuntimeId
|
|
346
|
+
? resolveAgentRuntime(input.requestedRuntimeId)
|
|
347
|
+
: null;
|
|
348
|
+
const resumeRuntimeId = input.effectiveRuntimeId
|
|
349
|
+
? resolveAgentRuntime(input.effectiveRuntimeId)
|
|
350
|
+
: requestedRuntimeId ?? DEFAULT_AGENT_RUNTIME;
|
|
351
|
+
const availability = await checkRuntimeAvailability(resumeRuntimeId);
|
|
352
|
+
|
|
353
|
+
if (!availability.available) {
|
|
354
|
+
throw new RuntimeUnavailableError(
|
|
355
|
+
availability.reason ??
|
|
356
|
+
`${getRuntimeLabel(resumeRuntimeId)} is unavailable for resume. Use Retry for a fresh execution.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
requestedRuntimeId,
|
|
362
|
+
effectiveRuntimeId: resumeRuntimeId,
|
|
363
|
+
requestedModelId: null,
|
|
364
|
+
effectiveModelId: null,
|
|
365
|
+
fallbackApplied: false,
|
|
366
|
+
fallbackReason: null,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildChatFallbackOrder(requestedModelId: string): string[] {
|
|
371
|
+
const fallbacks = CHAT_MODEL_FALLBACKS[requestedModelId] ?? [];
|
|
372
|
+
return unique([requestedModelId, ...fallbacks]);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildChatFallbackReason(input: {
|
|
376
|
+
requestedRuntimeId: AgentRuntimeId;
|
|
377
|
+
effectiveRuntimeId: AgentRuntimeId;
|
|
378
|
+
requestedModelId: string;
|
|
379
|
+
effectiveModelId: string;
|
|
380
|
+
unavailableReason: string | null;
|
|
381
|
+
}): string | null {
|
|
382
|
+
if (
|
|
383
|
+
input.requestedRuntimeId === input.effectiveRuntimeId &&
|
|
384
|
+
input.requestedModelId === input.effectiveModelId
|
|
385
|
+
) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const requestedLabel = `${input.requestedModelId} on ${getRuntimeLabel(input.requestedRuntimeId)}`;
|
|
390
|
+
const effectiveLabel = `${input.effectiveModelId} on ${getRuntimeLabel(input.effectiveRuntimeId)}`;
|
|
391
|
+
const reason = input.unavailableReason ?? `${requestedLabel} is unavailable`;
|
|
392
|
+
return `${reason}. Using ${effectiveLabel} for this turn.`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function resolveChatExecutionTarget(input: {
|
|
396
|
+
requestedRuntimeId?: string | null;
|
|
397
|
+
requestedModelId?: string | null;
|
|
398
|
+
}): Promise<ResolvedExecutionTarget> {
|
|
399
|
+
const requestedModelId =
|
|
400
|
+
input.requestedModelId ??
|
|
401
|
+
(input.requestedRuntimeId
|
|
402
|
+
? getRuntimeCatalogEntry(resolveAgentRuntime(input.requestedRuntimeId)).models.default
|
|
403
|
+
: DEFAULT_CHAT_MODEL);
|
|
404
|
+
const requestedRuntimeId = resolveAgentRuntime(
|
|
405
|
+
input.requestedRuntimeId ?? getRuntimeForModel(requestedModelId)
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const modelOrder = buildChatFallbackOrder(requestedModelId);
|
|
409
|
+
let requestedAvailability: RuntimeAvailability | null = null;
|
|
410
|
+
|
|
411
|
+
for (const candidateModelId of modelOrder) {
|
|
412
|
+
const candidateRuntimeId = resolveAgentRuntime(
|
|
413
|
+
getRuntimeForModel(candidateModelId)
|
|
414
|
+
);
|
|
415
|
+
if (
|
|
416
|
+
candidateRuntimeId !== "claude-code" &&
|
|
417
|
+
candidateRuntimeId !== "openai-codex-app-server" &&
|
|
418
|
+
candidateRuntimeId !== "ollama"
|
|
419
|
+
) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const availability = await checkRuntimeAvailability(candidateRuntimeId);
|
|
424
|
+
if (
|
|
425
|
+
candidateRuntimeId === requestedRuntimeId &&
|
|
426
|
+
requestedAvailability === null
|
|
427
|
+
) {
|
|
428
|
+
requestedAvailability = availability;
|
|
429
|
+
}
|
|
430
|
+
if (!availability.available) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
requestedRuntimeId,
|
|
436
|
+
effectiveRuntimeId: candidateRuntimeId,
|
|
437
|
+
requestedModelId,
|
|
438
|
+
effectiveModelId: candidateModelId,
|
|
439
|
+
fallbackApplied:
|
|
440
|
+
candidateRuntimeId !== requestedRuntimeId ||
|
|
441
|
+
candidateModelId !== requestedModelId,
|
|
442
|
+
fallbackReason: buildChatFallbackReason({
|
|
443
|
+
requestedRuntimeId,
|
|
444
|
+
effectiveRuntimeId: candidateRuntimeId,
|
|
445
|
+
requestedModelId,
|
|
446
|
+
effectiveModelId: candidateModelId,
|
|
447
|
+
unavailableReason: requestedAvailability?.reason ?? null,
|
|
448
|
+
}),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
throw new RequestedModelUnavailableError(
|
|
453
|
+
requestedAvailability?.reason ??
|
|
454
|
+
`No healthy runtime is available for ${requestedModelId}.`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
DEFAULT_AGENT_RUNTIME,
|
|
3
3
|
getRuntimeCapabilities,
|
|
4
4
|
getRuntimeCatalogEntry,
|
|
5
|
+
getRuntimeFeatures,
|
|
5
6
|
listRuntimeCatalog,
|
|
6
7
|
resolveAgentRuntime,
|
|
7
8
|
type AgentRuntimeId,
|
|
@@ -187,3 +188,6 @@ export async function testRuntimeConnection(
|
|
|
187
188
|
}
|
|
188
189
|
return adapter.testConnection();
|
|
189
190
|
}
|
|
191
|
+
|
|
192
|
+
export { getRuntimeFeatures };
|
|
193
|
+
export type { RuntimeFeatures } from "./catalog";
|
|
@@ -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);
|