icopilot 2.2.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/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const NEGATIVE_PATTERNS = [
|
|
2
|
+
/\bdo not\b/i,
|
|
3
|
+
/\bdon't\b/i,
|
|
4
|
+
/\bnot\b/i,
|
|
5
|
+
/\bnever\b/i,
|
|
6
|
+
/\bavoid\b/i,
|
|
7
|
+
/\bskip\b/i,
|
|
8
|
+
/\bdisable\b/i,
|
|
9
|
+
/\bremove\b/i,
|
|
10
|
+
/\breject\b/i,
|
|
11
|
+
];
|
|
12
|
+
const CONFLICTING_PHRASES = [
|
|
13
|
+
[/\benable\b/i, /\bdisable\b/i],
|
|
14
|
+
[/\badd\b/i, /\bremove\b/i],
|
|
15
|
+
[/\buse\b/i, /\bavoid\b/i],
|
|
16
|
+
[/\balways\b/i, /\bnever\b/i],
|
|
17
|
+
[/\binclude\b/i, /\bexclude\b/i],
|
|
18
|
+
[/\bsync\b/i, /\basync\b/i],
|
|
19
|
+
];
|
|
20
|
+
export function aggregateResults(results) {
|
|
21
|
+
const sources = unique(results.map((result) => result.name.trim()).filter(Boolean));
|
|
22
|
+
if (results.length === 0) {
|
|
23
|
+
return {
|
|
24
|
+
summary: ['## Summary', '- No agent results available.'].join('\n'),
|
|
25
|
+
details: [],
|
|
26
|
+
conflicts: [],
|
|
27
|
+
sources,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const details = results.map((result) => ({
|
|
31
|
+
result,
|
|
32
|
+
points: extractPoints(result.output),
|
|
33
|
+
}));
|
|
34
|
+
const pointIndex = new Map();
|
|
35
|
+
for (const detail of details) {
|
|
36
|
+
for (const point of detail.points) {
|
|
37
|
+
const existing = pointIndex.get(point.normalized);
|
|
38
|
+
if (existing) {
|
|
39
|
+
existing.mentions += 1;
|
|
40
|
+
existing.confidence = Math.max(existing.confidence, detail.result.confidence ?? 0);
|
|
41
|
+
if (!existing.sources.includes(detail.result.name)) {
|
|
42
|
+
existing.sources.push(detail.result.name);
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
pointIndex.set(point.normalized, {
|
|
47
|
+
point,
|
|
48
|
+
mentions: 1,
|
|
49
|
+
sources: [detail.result.name],
|
|
50
|
+
confidence: detail.result.confidence ?? 0,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const uniquePoints = [...pointIndex.values()].sort((left, right) => right.mentions - left.mentions ||
|
|
55
|
+
right.confidence - left.confidence ||
|
|
56
|
+
right.point.original.length - left.point.original.length);
|
|
57
|
+
const summaryLines = uniquePoints.slice(0, 5).map(({ point, sources: pointSources }) => {
|
|
58
|
+
const suffix = pointSources.length > 1 ? ` _(sources: ${pointSources.join(', ')})_` : '';
|
|
59
|
+
return `- ${point.original}${suffix}`;
|
|
60
|
+
});
|
|
61
|
+
const conflictMessages = detectConflicts(details);
|
|
62
|
+
return {
|
|
63
|
+
summary: [
|
|
64
|
+
'## Summary',
|
|
65
|
+
...(summaryLines.length > 0 ? summaryLines : ['- No shared recommendations found.']),
|
|
66
|
+
].join('\n'),
|
|
67
|
+
details: details.map((detail) => formatDetailSection(detail)),
|
|
68
|
+
conflicts: conflictMessages,
|
|
69
|
+
sources,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function formatDetailSection(detail) {
|
|
73
|
+
const metadata = [];
|
|
74
|
+
if (typeof detail.result.confidence === 'number') {
|
|
75
|
+
metadata.push(`confidence: ${formatConfidence(detail.result.confidence)}`);
|
|
76
|
+
}
|
|
77
|
+
if (typeof detail.result.tokens === 'number') {
|
|
78
|
+
metadata.push(`tokens: ${detail.result.tokens}`);
|
|
79
|
+
}
|
|
80
|
+
const header = `### ${detail.result.name}`;
|
|
81
|
+
const meta = metadata.length > 0 ? `${metadata.join(' • ')}` : undefined;
|
|
82
|
+
const lines = detail.points.length > 0
|
|
83
|
+
? detail.points.map((point) => `- ${point.original}`)
|
|
84
|
+
: ['- No actionable points extracted.'];
|
|
85
|
+
return [header, meta, ...lines].filter((line) => Boolean(line)).join('\n');
|
|
86
|
+
}
|
|
87
|
+
function detectConflicts(details) {
|
|
88
|
+
const conflicts = new Set();
|
|
89
|
+
for (let index = 0; index < details.length; index += 1) {
|
|
90
|
+
for (let compareIndex = index + 1; compareIndex < details.length; compareIndex += 1) {
|
|
91
|
+
const left = details[index];
|
|
92
|
+
const right = details[compareIndex];
|
|
93
|
+
for (const leftPoint of left.points) {
|
|
94
|
+
for (const rightPoint of right.points) {
|
|
95
|
+
if (leftPoint.normalized === rightPoint.normalized)
|
|
96
|
+
continue;
|
|
97
|
+
if (leftPoint.subject !== rightPoint.subject)
|
|
98
|
+
continue;
|
|
99
|
+
if (!isConflict(leftPoint.original, rightPoint.original))
|
|
100
|
+
continue;
|
|
101
|
+
conflicts.add(`- Conflict between **${left.result.name}** and **${right.result.name}** on _${leftPoint.subject}_: "${leftPoint.original}" vs "${rightPoint.original}"`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return [...conflicts];
|
|
107
|
+
}
|
|
108
|
+
function isConflict(left, right) {
|
|
109
|
+
const leftNegative = hasNegativeIntent(left);
|
|
110
|
+
const rightNegative = hasNegativeIntent(right);
|
|
111
|
+
if (leftNegative !== rightNegative) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return CONFLICTING_PHRASES.some(([first, second]) => (first.test(left) && second.test(right)) || (second.test(left) && first.test(right)));
|
|
115
|
+
}
|
|
116
|
+
function hasNegativeIntent(value) {
|
|
117
|
+
return NEGATIVE_PATTERNS.some((pattern) => pattern.test(value));
|
|
118
|
+
}
|
|
119
|
+
function extractPoints(output) {
|
|
120
|
+
const segments = output
|
|
121
|
+
.split(/\r?\n+/)
|
|
122
|
+
.flatMap((line) => splitLineIntoPoints(line))
|
|
123
|
+
.map((entry) => entry.trim())
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
const points = [];
|
|
127
|
+
for (const segment of segments) {
|
|
128
|
+
const original = cleanupSegment(segment);
|
|
129
|
+
if (!original)
|
|
130
|
+
continue;
|
|
131
|
+
const normalized = normalizePoint(original);
|
|
132
|
+
if (!normalized || seen.has(normalized))
|
|
133
|
+
continue;
|
|
134
|
+
seen.add(normalized);
|
|
135
|
+
points.push({
|
|
136
|
+
original,
|
|
137
|
+
normalized,
|
|
138
|
+
subject: deriveSubject(normalized),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return points;
|
|
142
|
+
}
|
|
143
|
+
function splitLineIntoPoints(line) {
|
|
144
|
+
const trimmed = line.trim();
|
|
145
|
+
if (!trimmed)
|
|
146
|
+
return [];
|
|
147
|
+
if (/^\s*[-*•]\s+/.test(line) || /^\s*\d+[.)]\s+/.test(line)) {
|
|
148
|
+
return [trimmed.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, '')];
|
|
149
|
+
}
|
|
150
|
+
return trimmed
|
|
151
|
+
.split(/(?<=[.!?])\s+(?=[A-Z0-9])/)
|
|
152
|
+
.map((entry) => entry.trim())
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
function cleanupSegment(value) {
|
|
156
|
+
return value
|
|
157
|
+
.replace(/^#+\s*/, '')
|
|
158
|
+
.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, '')
|
|
159
|
+
.replace(/\s+/g, ' ')
|
|
160
|
+
.trim();
|
|
161
|
+
}
|
|
162
|
+
function normalizePoint(value) {
|
|
163
|
+
return value
|
|
164
|
+
.toLowerCase()
|
|
165
|
+
.replace(/[`*_~]/g, '')
|
|
166
|
+
.replace(/[^\w\s]/g, ' ')
|
|
167
|
+
.replace(/\s+/g, ' ')
|
|
168
|
+
.trim();
|
|
169
|
+
}
|
|
170
|
+
function deriveSubject(normalized) {
|
|
171
|
+
const withoutLeadVerb = normalized.replace(/^(?:use|avoid|enable|disable|add|remove|include|exclude|always|never|do not|don t|not|skip)\s+/, '');
|
|
172
|
+
return withoutLeadVerb
|
|
173
|
+
.split(' ')
|
|
174
|
+
.filter((token) => token.length > 2)
|
|
175
|
+
.slice(0, 6)
|
|
176
|
+
.join(' ')
|
|
177
|
+
.trim();
|
|
178
|
+
}
|
|
179
|
+
function formatConfidence(confidence) {
|
|
180
|
+
if (confidence >= 0 && confidence <= 1) {
|
|
181
|
+
return `${Math.round(confidence * 100)}%`;
|
|
182
|
+
}
|
|
183
|
+
return `${confidence}`;
|
|
184
|
+
}
|
|
185
|
+
function unique(values) {
|
|
186
|
+
return [...new Set(values)];
|
|
187
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseDocument } from 'yaml';
|
|
4
|
+
const AGENTS_DIR = path.join('.icopilot', 'agents');
|
|
5
|
+
let cachedAgents = [];
|
|
6
|
+
export function loadCustomAgents(projectRoot) {
|
|
7
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
8
|
+
const agentsDir = path.join(resolvedRoot, AGENTS_DIR);
|
|
9
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
10
|
+
const agents = fs
|
|
11
|
+
.readdirSync(agentsDir, { withFileTypes: true })
|
|
12
|
+
.filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name))
|
|
13
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
14
|
+
.map((entry) => loadAgentFile(path.join(agentsDir, entry.name)));
|
|
15
|
+
cachedAgents = agents;
|
|
16
|
+
return agents.map(cloneAgent);
|
|
17
|
+
}
|
|
18
|
+
export function getCustomAgent(name) {
|
|
19
|
+
const match = cachedAgents.find((agent) => agent.name === name) ??
|
|
20
|
+
cachedAgents.find((agent) => agent.name.toLowerCase() === name.toLowerCase());
|
|
21
|
+
return match ? cloneAgent(match) : undefined;
|
|
22
|
+
}
|
|
23
|
+
export function listCustomAgents() {
|
|
24
|
+
return cachedAgents.map(cloneAgent);
|
|
25
|
+
}
|
|
26
|
+
function loadAgentFile(filePath) {
|
|
27
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
28
|
+
const document = parseDocument(raw);
|
|
29
|
+
if (document.errors.length > 0) {
|
|
30
|
+
const [firstError] = document.errors;
|
|
31
|
+
throw new Error(`Invalid YAML in ${path.relative(process.cwd(), filePath)}: ${firstError?.message ?? 'parse error'}`);
|
|
32
|
+
}
|
|
33
|
+
return validateAgentDef(document.toJSON(), filePath);
|
|
34
|
+
}
|
|
35
|
+
function validateAgentDef(value, filePath) {
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
37
|
+
throw new Error(`${label(filePath)} must contain a YAML object`);
|
|
38
|
+
}
|
|
39
|
+
const record = value;
|
|
40
|
+
const agent = {
|
|
41
|
+
name: requiredString(record.name, 'name', filePath),
|
|
42
|
+
description: requiredString(record.description, 'description', filePath),
|
|
43
|
+
systemPrompt: requiredString(record.systemPrompt, 'systemPrompt', filePath),
|
|
44
|
+
};
|
|
45
|
+
const model = optionalString(record.model, 'model', filePath);
|
|
46
|
+
if (model)
|
|
47
|
+
agent.model = model;
|
|
48
|
+
if (record.temperature !== undefined) {
|
|
49
|
+
agent.temperature = numberInRange(record.temperature, 'temperature', filePath, 0, 2);
|
|
50
|
+
}
|
|
51
|
+
if (record.tools !== undefined) {
|
|
52
|
+
if (!Array.isArray(record.tools) ||
|
|
53
|
+
record.tools.some((tool) => typeof tool !== 'string' || tool.trim().length === 0)) {
|
|
54
|
+
throw new Error(`${label(filePath)} field "tools" must be an array of non-empty strings`);
|
|
55
|
+
}
|
|
56
|
+
agent.tools = [...new Set(record.tools.map((tool) => tool.trim()))];
|
|
57
|
+
}
|
|
58
|
+
if (record.maxTokens !== undefined) {
|
|
59
|
+
agent.maxTokens = positiveInteger(record.maxTokens, 'maxTokens', filePath);
|
|
60
|
+
}
|
|
61
|
+
return agent;
|
|
62
|
+
}
|
|
63
|
+
function requiredString(value, field, filePath) {
|
|
64
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
65
|
+
throw new Error(`${label(filePath)} field "${field}" must be a non-empty string`);
|
|
66
|
+
}
|
|
67
|
+
return value.trim();
|
|
68
|
+
}
|
|
69
|
+
function optionalString(value, field, filePath) {
|
|
70
|
+
if (value === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
73
|
+
throw new Error(`${label(filePath)} field "${field}" must be a non-empty string`);
|
|
74
|
+
}
|
|
75
|
+
return value.trim();
|
|
76
|
+
}
|
|
77
|
+
function positiveInteger(value, field, filePath) {
|
|
78
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
79
|
+
throw new Error(`${label(filePath)} field "${field}" must be a positive integer`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function numberInRange(value, field, filePath, min, max) {
|
|
84
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
|
|
85
|
+
throw new Error(`${label(filePath)} field "${field}" must be a number between ${min} and ${max}`);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function cloneAgent(agent) {
|
|
90
|
+
return {
|
|
91
|
+
...agent,
|
|
92
|
+
tools: agent.tools ? [...agent.tools] : undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function label(filePath) {
|
|
96
|
+
return path.relative(process.cwd(), filePath) || filePath;
|
|
97
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { Session } from '../session/session.js';
|
|
2
|
+
import { runTurn } from '../modes/turn.js';
|
|
3
|
+
import { countTokensSync } from '../util/tokens.js';
|
|
4
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
5
|
+
export class GoalDrivenAgent {
|
|
6
|
+
session;
|
|
7
|
+
signal;
|
|
8
|
+
maxRetries;
|
|
9
|
+
runTurnImpl;
|
|
10
|
+
progress = {
|
|
11
|
+
phase: 'idle',
|
|
12
|
+
currentAttempt: 0,
|
|
13
|
+
maxAttempts: DEFAULT_MAX_RETRIES,
|
|
14
|
+
completedSteps: 0,
|
|
15
|
+
totalSteps: 0,
|
|
16
|
+
};
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.session = options.session ?? new Session();
|
|
19
|
+
this.signal = options.signal ?? new AbortController().signal;
|
|
20
|
+
this.maxRetries = clampRetries(options.maxRetries ?? DEFAULT_MAX_RETRIES);
|
|
21
|
+
this.runTurnImpl = options.runTurn ?? runTurn;
|
|
22
|
+
this.progress.maxAttempts = this.maxRetries;
|
|
23
|
+
}
|
|
24
|
+
plan(goal) {
|
|
25
|
+
const normalizedGoal = normalizeGoal(goal);
|
|
26
|
+
const files = collectFiles(normalizedGoal);
|
|
27
|
+
const steps = [
|
|
28
|
+
{
|
|
29
|
+
id: 'analyze-goal',
|
|
30
|
+
description: `Analyze the current implementation, constraints, and affected surfaces for: ${normalizedGoal.description}`,
|
|
31
|
+
type: 'analyze',
|
|
32
|
+
files,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'create-artifacts',
|
|
36
|
+
description: 'Create any new files, modules, or scaffolding required to deliver the goal end-to-end.',
|
|
37
|
+
type: 'create',
|
|
38
|
+
files,
|
|
39
|
+
dependencies: ['analyze-goal'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'modify-integrations',
|
|
43
|
+
description: 'Modify the existing integration points, command wiring, and supporting code needed by the goal.',
|
|
44
|
+
type: 'modify',
|
|
45
|
+
files,
|
|
46
|
+
dependencies: ['create-artifacts'],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'test-goal',
|
|
50
|
+
description: buildTestStepDescription(normalizedGoal),
|
|
51
|
+
type: 'test',
|
|
52
|
+
files,
|
|
53
|
+
dependencies: ['modify-integrations'],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'verify-goal',
|
|
57
|
+
description: buildVerifyStepDescription(normalizedGoal),
|
|
58
|
+
type: 'verify',
|
|
59
|
+
files,
|
|
60
|
+
dependencies: ['test-goal'],
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
const estimatedTokens = safeTokenEstimate([
|
|
64
|
+
normalizedGoal.description,
|
|
65
|
+
normalizedGoal.constraints?.join('\n') ?? '',
|
|
66
|
+
normalizedGoal.acceptanceCriteria?.join('\n') ?? '',
|
|
67
|
+
normalizedGoal.scope?.join('\n') ?? '',
|
|
68
|
+
...steps.map((step) => `${step.id}:${step.description}`),
|
|
69
|
+
].join('\n'));
|
|
70
|
+
this.progress = {
|
|
71
|
+
phase: 'planned',
|
|
72
|
+
goal: normalizedGoal,
|
|
73
|
+
currentAttempt: 0,
|
|
74
|
+
maxAttempts: this.maxRetries,
|
|
75
|
+
completedSteps: 0,
|
|
76
|
+
totalSteps: steps.length,
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
goal: normalizedGoal,
|
|
80
|
+
steps,
|
|
81
|
+
estimatedTokens,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async execute(plan) {
|
|
85
|
+
this.progress = {
|
|
86
|
+
...this.progress,
|
|
87
|
+
phase: 'executing',
|
|
88
|
+
goal: plan.goal,
|
|
89
|
+
currentAttempt: 0,
|
|
90
|
+
completedSteps: 0,
|
|
91
|
+
totalSteps: plan.steps.length,
|
|
92
|
+
currentStepId: undefined,
|
|
93
|
+
lastError: undefined,
|
|
94
|
+
result: undefined,
|
|
95
|
+
verification: undefined,
|
|
96
|
+
};
|
|
97
|
+
const previousMode = this.session.state.mode;
|
|
98
|
+
const previousPrompt = this.session.state.systemPrompt;
|
|
99
|
+
setSessionMode(this.session, 'ask');
|
|
100
|
+
setSystemPrompt(this.session, buildGoalSystemPrompt(plan.goal));
|
|
101
|
+
let lastResult = null;
|
|
102
|
+
let retryFeedback = [];
|
|
103
|
+
try {
|
|
104
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
105
|
+
const stepResults = createStepResults(plan.steps, attempt);
|
|
106
|
+
this.progress.currentAttempt = attempt;
|
|
107
|
+
this.progress.phase = 'executing';
|
|
108
|
+
this.progress.completedSteps = 0;
|
|
109
|
+
this.progress.currentStepId = undefined;
|
|
110
|
+
this.progress.lastError = undefined;
|
|
111
|
+
let aborted = false;
|
|
112
|
+
for (let index = 0; index < plan.steps.length; index++) {
|
|
113
|
+
const step = plan.steps[index];
|
|
114
|
+
const stepResult = stepResults[index];
|
|
115
|
+
this.progress.currentStepId = step.id;
|
|
116
|
+
stepResult.status = 'running';
|
|
117
|
+
try {
|
|
118
|
+
throwIfAborted(this.signal);
|
|
119
|
+
await this.runTurnImpl({
|
|
120
|
+
session: this.session,
|
|
121
|
+
userInput: buildGoalStepPrompt(plan, step, attempt, this.maxRetries, retryFeedback),
|
|
122
|
+
signal: this.signal,
|
|
123
|
+
});
|
|
124
|
+
stepResult.status = 'completed';
|
|
125
|
+
stepResult.output = findLastAssistantMessage(this.session.state.messages);
|
|
126
|
+
this.progress.completedSteps += 1;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (isAbortError(error) || this.signal.aborted) {
|
|
130
|
+
aborted = true;
|
|
131
|
+
stepResult.status = 'skipped';
|
|
132
|
+
stepResult.error = 'Goal execution was aborted.';
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
stepResult.status = 'failed';
|
|
136
|
+
stepResult.error = error instanceof Error ? error.message : String(error);
|
|
137
|
+
this.progress.lastError = stepResult.error;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
this.progress.phase = aborted ? 'aborted' : 'verifying';
|
|
142
|
+
const summary = summarizeResult(plan.goal, stepResults, attempt, aborted);
|
|
143
|
+
const result = {
|
|
144
|
+
goal: plan.goal,
|
|
145
|
+
plan,
|
|
146
|
+
success: false,
|
|
147
|
+
attempts: attempt,
|
|
148
|
+
summary,
|
|
149
|
+
aborted,
|
|
150
|
+
stepResults,
|
|
151
|
+
verification: {
|
|
152
|
+
ok: false,
|
|
153
|
+
score: 0,
|
|
154
|
+
issues: [],
|
|
155
|
+
attempts: attempt,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
const verification = this.verify(plan.goal, result);
|
|
159
|
+
result.verification = verification;
|
|
160
|
+
result.success = verification.ok;
|
|
161
|
+
this.progress.verification = verification;
|
|
162
|
+
this.progress.result = result;
|
|
163
|
+
lastResult = result;
|
|
164
|
+
if (aborted) {
|
|
165
|
+
this.progress.phase = 'aborted';
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
if (verification.ok) {
|
|
169
|
+
this.progress.phase = 'completed';
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
retryFeedback = verification.issues;
|
|
173
|
+
this.progress.phase = attempt >= this.maxRetries ? 'failed' : 'executing';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
setSystemPrompt(this.session, previousPrompt);
|
|
178
|
+
setSessionMode(this.session, previousMode);
|
|
179
|
+
}
|
|
180
|
+
if (lastResult) {
|
|
181
|
+
this.progress.result = lastResult;
|
|
182
|
+
return lastResult;
|
|
183
|
+
}
|
|
184
|
+
const fallbackPlan = plan.steps.length > 0 ? plan : this.plan(plan.goal);
|
|
185
|
+
const fallbackResult = {
|
|
186
|
+
goal: plan.goal,
|
|
187
|
+
plan: fallbackPlan,
|
|
188
|
+
success: false,
|
|
189
|
+
attempts: this.progress.currentAttempt,
|
|
190
|
+
summary: 'Goal execution did not run.',
|
|
191
|
+
aborted: false,
|
|
192
|
+
stepResults: createStepResults(fallbackPlan.steps, this.progress.currentAttempt || 1),
|
|
193
|
+
verification: {
|
|
194
|
+
ok: false,
|
|
195
|
+
score: 0,
|
|
196
|
+
issues: ['Goal execution did not run.'],
|
|
197
|
+
attempts: this.progress.currentAttempt || 1,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
this.progress.phase = 'failed';
|
|
201
|
+
this.progress.result = fallbackResult;
|
|
202
|
+
this.progress.verification = fallbackResult.verification;
|
|
203
|
+
return fallbackResult;
|
|
204
|
+
}
|
|
205
|
+
verify(goal, result) {
|
|
206
|
+
const issues = [];
|
|
207
|
+
if (result.aborted) {
|
|
208
|
+
issues.push('Goal execution was aborted before completion.');
|
|
209
|
+
}
|
|
210
|
+
const failedSteps = result.stepResults.filter((step) => step.status === 'failed');
|
|
211
|
+
for (const step of failedSteps) {
|
|
212
|
+
issues.push(`Step "${step.description}" failed${step.error ? `: ${step.error}` : '.'}`);
|
|
213
|
+
}
|
|
214
|
+
const completedSteps = result.stepResults.filter((step) => step.status === 'completed');
|
|
215
|
+
if (completedSteps.length === 0) {
|
|
216
|
+
issues.push('No goal step completed successfully.');
|
|
217
|
+
}
|
|
218
|
+
if ((goal.acceptanceCriteria?.length ?? 0) > 0) {
|
|
219
|
+
const hasValidationStep = result.plan.steps.some((step) => step.type === 'test' || step.type === 'verify');
|
|
220
|
+
if (!hasValidationStep) {
|
|
221
|
+
issues.push('The plan does not include a validation step for the acceptance criteria.');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const hasVerifyCompletion = result.stepResults.some((step) => step.type === 'verify' && step.status === 'completed');
|
|
225
|
+
if (!hasVerifyCompletion) {
|
|
226
|
+
issues.push('The verification step did not complete successfully.');
|
|
227
|
+
}
|
|
228
|
+
const scoreBase = result.plan.steps.length > 0 ? completedSteps.length / result.plan.steps.length : 0;
|
|
229
|
+
const score = Math.max(0, Math.min(1, Math.round((scoreBase - issues.length * 0.08) * 100) / 100));
|
|
230
|
+
return {
|
|
231
|
+
ok: issues.length === 0,
|
|
232
|
+
score,
|
|
233
|
+
issues,
|
|
234
|
+
attempts: result.attempts,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
getProgress() {
|
|
238
|
+
return {
|
|
239
|
+
...this.progress,
|
|
240
|
+
verification: this.progress.verification
|
|
241
|
+
? {
|
|
242
|
+
...this.progress.verification,
|
|
243
|
+
issues: [...this.progress.verification.issues],
|
|
244
|
+
}
|
|
245
|
+
: undefined,
|
|
246
|
+
result: this.progress.result
|
|
247
|
+
? {
|
|
248
|
+
...this.progress.result,
|
|
249
|
+
stepResults: this.progress.result.stepResults.map((step) => ({ ...step })),
|
|
250
|
+
verification: {
|
|
251
|
+
...this.progress.result.verification,
|
|
252
|
+
issues: [...this.progress.result.verification.issues],
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
: undefined,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function normalizeGoal(goal) {
|
|
260
|
+
return {
|
|
261
|
+
description: goal.description.trim(),
|
|
262
|
+
constraints: normalizeStringList(goal.constraints),
|
|
263
|
+
acceptanceCriteria: normalizeStringList(goal.acceptanceCriteria),
|
|
264
|
+
scope: normalizeStringList(goal.scope),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function normalizeStringList(values) {
|
|
268
|
+
if (!Array.isArray(values))
|
|
269
|
+
return undefined;
|
|
270
|
+
const normalized = values.map((value) => value.trim()).filter(Boolean);
|
|
271
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
272
|
+
}
|
|
273
|
+
function collectFiles(goal) {
|
|
274
|
+
const files = new Set();
|
|
275
|
+
for (const entry of goal.scope ?? []) {
|
|
276
|
+
const normalized = entry.trim();
|
|
277
|
+
if (/\.[a-z0-9]+$/i.test(normalized) || normalized.includes('/') || normalized.includes('\\')) {
|
|
278
|
+
files.add(normalized);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return files.size > 0 ? [...files] : undefined;
|
|
282
|
+
}
|
|
283
|
+
function buildTestStepDescription(goal) {
|
|
284
|
+
const criteria = goal.acceptanceCriteria && goal.acceptanceCriteria.length > 0
|
|
285
|
+
? `Validate these acceptance criteria: ${goal.acceptanceCriteria.join('; ')}.`
|
|
286
|
+
: 'Run or update the most relevant automated and manual checks for the goal.';
|
|
287
|
+
return `${criteria} Fix any gaps before proceeding.`;
|
|
288
|
+
}
|
|
289
|
+
function buildVerifyStepDescription(goal) {
|
|
290
|
+
const constraints = goal.constraints && goal.constraints.length > 0
|
|
291
|
+
? ` Re-check constraints: ${goal.constraints.join('; ')}.`
|
|
292
|
+
: '';
|
|
293
|
+
return `Verify the end-to-end outcome, summarize the changes made, and confirm the goal is complete.${constraints}`;
|
|
294
|
+
}
|
|
295
|
+
function safeTokenEstimate(text) {
|
|
296
|
+
try {
|
|
297
|
+
return countTokensSync(text);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function createStepResults(steps, attempt) {
|
|
304
|
+
return steps.map((step) => ({
|
|
305
|
+
stepId: step.id,
|
|
306
|
+
description: step.description,
|
|
307
|
+
type: step.type,
|
|
308
|
+
status: 'pending',
|
|
309
|
+
attempts: attempt,
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
function buildGoalSystemPrompt(goal) {
|
|
313
|
+
const lines = [
|
|
314
|
+
'You are a goal-driven implementation agent.',
|
|
315
|
+
'Plan, implement, test, and verify the requested feature end-to-end.',
|
|
316
|
+
'Use the available tools to inspect code, make changes, run tests, and confirm the result.',
|
|
317
|
+
'Keep changes aligned to the stated scope and constraints.',
|
|
318
|
+
'',
|
|
319
|
+
`Goal: ${goal.description}`,
|
|
320
|
+
];
|
|
321
|
+
if (goal.scope?.length) {
|
|
322
|
+
lines.push(`Scope: ${goal.scope.join('; ')}`);
|
|
323
|
+
}
|
|
324
|
+
if (goal.constraints?.length) {
|
|
325
|
+
lines.push(`Constraints: ${goal.constraints.join('; ')}`);
|
|
326
|
+
}
|
|
327
|
+
if (goal.acceptanceCriteria?.length) {
|
|
328
|
+
lines.push(`Acceptance criteria: ${goal.acceptanceCriteria.join('; ')}`);
|
|
329
|
+
}
|
|
330
|
+
return lines.join('\n');
|
|
331
|
+
}
|
|
332
|
+
function buildGoalStepPrompt(plan, step, attempt, maxAttempts, retryFeedback) {
|
|
333
|
+
const lines = [
|
|
334
|
+
`Goal attempt ${attempt} of ${maxAttempts}.`,
|
|
335
|
+
`Goal: ${plan.goal.description}`,
|
|
336
|
+
`Current step (${step.type}): ${step.description}`,
|
|
337
|
+
];
|
|
338
|
+
if (step.files?.length) {
|
|
339
|
+
lines.push(`Relevant files: ${step.files.join(', ')}`);
|
|
340
|
+
}
|
|
341
|
+
if (step.dependencies?.length) {
|
|
342
|
+
lines.push(`Step dependencies already satisfied: ${step.dependencies.join(', ')}`);
|
|
343
|
+
}
|
|
344
|
+
if (retryFeedback.length > 0) {
|
|
345
|
+
lines.push(`Retry guidance: ${retryFeedback.join(' | ')}`);
|
|
346
|
+
}
|
|
347
|
+
if (plan.goal.acceptanceCriteria?.length) {
|
|
348
|
+
lines.push(`Acceptance criteria: ${plan.goal.acceptanceCriteria.join('; ')}`);
|
|
349
|
+
}
|
|
350
|
+
if (plan.goal.constraints?.length) {
|
|
351
|
+
lines.push(`Constraints: ${plan.goal.constraints.join('; ')}`);
|
|
352
|
+
}
|
|
353
|
+
lines.push('Do the work for this step now and report the concrete outcome.');
|
|
354
|
+
return lines.join('\n');
|
|
355
|
+
}
|
|
356
|
+
function summarizeResult(goal, stepResults, attempt, aborted) {
|
|
357
|
+
const completed = stepResults.filter((step) => step.status === 'completed').length;
|
|
358
|
+
const failed = stepResults.filter((step) => step.status === 'failed').length;
|
|
359
|
+
const skipped = stepResults.filter((step) => step.status === 'skipped').length;
|
|
360
|
+
const status = aborted ? 'aborted' : failed > 0 ? 'failed' : 'completed';
|
|
361
|
+
return `${goal.description} — ${status} on attempt ${attempt} (${completed} completed, ${failed} failed, ${skipped} skipped).`;
|
|
362
|
+
}
|
|
363
|
+
function findLastAssistantMessage(messages) {
|
|
364
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
365
|
+
const message = messages[index];
|
|
366
|
+
if (message?.role !== 'assistant') {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (typeof message.content === 'string') {
|
|
370
|
+
return message.content;
|
|
371
|
+
}
|
|
372
|
+
if (Array.isArray(message.content)) {
|
|
373
|
+
return message.content
|
|
374
|
+
.map((part) => {
|
|
375
|
+
if (typeof part === 'string')
|
|
376
|
+
return part;
|
|
377
|
+
return 'text' in part && typeof part.text === 'string' ? part.text : '';
|
|
378
|
+
})
|
|
379
|
+
.join('\n');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return '';
|
|
383
|
+
}
|
|
384
|
+
function setSessionMode(session, mode) {
|
|
385
|
+
if (typeof session.setMode === 'function') {
|
|
386
|
+
session.setMode(mode);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
session.state.mode = mode;
|
|
390
|
+
}
|
|
391
|
+
function setSystemPrompt(session, prompt) {
|
|
392
|
+
if (typeof session.setSystemPrompt === 'function') {
|
|
393
|
+
session.setSystemPrompt(prompt);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
session.state.systemPrompt = prompt;
|
|
397
|
+
}
|
|
398
|
+
function clampRetries(value) {
|
|
399
|
+
if (!Number.isFinite(value))
|
|
400
|
+
return DEFAULT_MAX_RETRIES;
|
|
401
|
+
return Math.max(1, Math.min(Math.trunc(value), DEFAULT_MAX_RETRIES));
|
|
402
|
+
}
|
|
403
|
+
function throwIfAborted(signal) {
|
|
404
|
+
if (signal.aborted) {
|
|
405
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function isAbortError(error) {
|
|
409
|
+
return Boolean((error instanceof DOMException && error.name === 'AbortError') ||
|
|
410
|
+
(error instanceof Error && error.name === 'AbortError'));
|
|
411
|
+
}
|