@semalt-ai/code 1.8.5 → 1.19.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/.claude/settings.local.json +6 -1
- package/.github/workflows/ci.yml +69 -0
- package/CLAUDE.md +1584 -26
- package/README.md +147 -3
- package/examples/embed.js +74 -0
- package/index.js +251 -10
- package/lib/agent.js +711 -104
- package/lib/api.js +213 -49
- package/lib/args.js +74 -2
- package/lib/audit.js +23 -1
- package/lib/background.js +584 -0
- package/lib/checkpoints.js +757 -0
- package/lib/commands/auth.js +94 -0
- package/lib/commands/chat-session.js +306 -0
- package/lib/commands/chat-slash.js +399 -0
- package/lib/commands/chat-turn.js +446 -0
- package/lib/commands/chat.js +403 -0
- package/lib/commands/custom.js +157 -0
- package/lib/commands/history-utils.js +66 -0
- package/lib/commands/index.js +268 -0
- package/lib/commands/mcp.js +113 -0
- package/lib/commands/oneshot.js +193 -0
- package/lib/commands/registry.js +269 -0
- package/lib/commands/tasks.js +89 -0
- package/lib/compact.js +87 -0
- package/lib/config.js +333 -11
- package/lib/constants.js +372 -3
- package/lib/deny.js +199 -0
- package/lib/doctor.js +160 -0
- package/lib/headless.js +167 -0
- package/lib/hooks.js +286 -0
- package/lib/images.js +264 -0
- package/lib/internals.js +49 -0
- package/lib/mcp/boundary.js +131 -0
- package/lib/mcp/client.js +270 -0
- package/lib/mcp/oauth.js +134 -0
- package/lib/memory.js +209 -0
- package/lib/metrics.js +37 -2
- package/lib/payload.js +54 -0
- package/lib/permission-rules.js +401 -0
- package/lib/permissions.js +100 -10
- package/lib/pricing.js +67 -0
- package/lib/proc.js +62 -0
- package/lib/prompts.js +84 -5
- package/lib/sandbox.js +568 -0
- package/lib/sdk.js +328 -0
- package/lib/secrets.js +211 -0
- package/lib/skills.js +223 -0
- package/lib/subagents.js +516 -0
- package/lib/tool_registry.js +2558 -0
- package/lib/tool_specs.js +222 -2
- package/lib/tools.js +272 -1020
- package/lib/ui/format.js +22 -1
- package/lib/ui/input-field.js +16 -7
- package/lib/ui/status-bar.js +79 -11
- package/lib/ui/theme.js +1 -0
- package/lib/ui/web-activity.js +218 -0
- package/lib/verify.js +229 -0
- package/lib/web-extract.js +213 -0
- package/lib/web-summarize.js +68 -0
- package/package.json +19 -4
- package/scripts/lint.js +57 -0
- package/test/agent-loop.test.js +389 -0
- package/test/background.test.js +414 -0
- package/test/chat.test.js +114 -0
- package/test/checkpoints-agent.test.js +181 -0
- package/test/checkpoints.test.js +650 -0
- package/test/command-registry.test.js +160 -0
- package/test/compact.test.js +116 -0
- package/test/completion-lazy.test.js +52 -0
- package/test/config-merge.test.js +324 -0
- package/test/config-quarantine.test.js +128 -0
- package/test/config-write-guard-allow-anywhere.test.js +56 -0
- package/test/config-write-guard-skip.test.js +46 -0
- package/test/config-write-guard.test.js +153 -0
- package/test/context-split.test.js +215 -0
- package/test/cost-doctor.test.js +142 -0
- package/test/custom-commands-chat.test.js +106 -0
- package/test/custom-commands.test.js +230 -0
- package/test/deny-windows.test.js +120 -0
- package/test/deny.test.js +83 -0
- package/test/download-allow-anywhere.test.js +66 -0
- package/test/download-confine.test.js +153 -0
- package/test/executors.test.js +362 -0
- package/test/extract-tool-calls.test.js +315 -0
- package/test/fetch-url-validation.test.js +219 -0
- package/test/fixtures/tool-calls.js +57 -0
- package/test/fixtures/web-page.js +91 -0
- package/test/git-tools.test.js +384 -0
- package/test/grep-glob-serialize.test.js +242 -0
- package/test/grep-glob.test.js +268 -0
- package/test/harness/README.md +57 -0
- package/test/harness/chat-harness.js +142 -0
- package/test/harness/memwarn-headless-child.js +65 -0
- package/test/harness/mock-llm.js +120 -0
- package/test/harness/mock-mcp-server.js +142 -0
- package/test/harness/sse-server.js +69 -0
- package/test/headless.test.js +203 -0
- package/test/history-utils.test.js +88 -0
- package/test/hooks-agent.test.js +238 -0
- package/test/hooks-verify-sandbox.test.js +232 -0
- package/test/hooks.test.js +216 -0
- package/test/http-get-user-agent.test.js +142 -0
- package/test/images-api.test.js +208 -0
- package/test/images.test.js +238 -0
- package/test/max-iterations.test.js +216 -0
- package/test/mcp-boundary.test.js +57 -0
- package/test/mcp-client.test.js +267 -0
- package/test/mcp-oauth.test.js +86 -0
- package/test/memory-truncation-warning.test.js +222 -0
- package/test/memory.test.js +198 -0
- package/test/native-dispatch.test.js +356 -0
- package/test/output-chokepoint.test.js +188 -0
- package/test/path-guards.test.js +134 -0
- package/test/payload.test.js +99 -0
- package/test/permission-rules-agent.test.js +210 -0
- package/test/permission-rules.test.js +297 -0
- package/test/permissions.test.js +163 -0
- package/test/plan-mode.test.js +167 -0
- package/test/read-paginate.test.js +275 -0
- package/test/readonly-tools.test.js +177 -0
- package/test/result-cap.test.js +233 -0
- package/test/sandbox-agent.test.js +147 -0
- package/test/sandbox-integration.test.js +216 -0
- package/test/sandbox.test.js +408 -0
- package/test/sdk.test.js +234 -0
- package/test/shell-output-cap.test.js +181 -0
- package/test/skills-chat.test.js +110 -0
- package/test/skills.test.js +295 -0
- package/test/smoke.test.js +68 -0
- package/test/status-bar-pause.test.js +164 -0
- package/test/stream-parser.test.js +147 -0
- package/test/subagents-agent.test.js +178 -0
- package/test/subagents.test.js +222 -0
- package/test/tool-registry.test.js +85 -0
- package/test/trim-budget.test.js +101 -0
- package/test/verify-agent.test.js +317 -0
- package/test/verify.test.js +141 -0
- package/test/web-activity-ordering.test.js +194 -0
- package/test/web-activity.test.js +207 -0
- package/test/web-data-extraction-guidance.test.js +71 -0
- package/test/web-extract.test.js +185 -0
- package/test/web-fetch-agent.test.js +291 -0
- package/test/web-fetch-mode.test.js +193 -0
- package/test/web-search.test.js +380 -0
- package/lib/commands.js +0 -1438
package/lib/subagents.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Subagents — isolated child agent loops (Task 3.6)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
//
|
|
7
|
+
// A subagent is a SECOND agent loop run with its OWN isolated message history.
|
|
8
|
+
// It exists to keep the parent's context clean: noisy work (research, reading
|
|
9
|
+
// large files, review) happens in the child, and ONLY the child's final result
|
|
10
|
+
// returns to the parent. The parent never absorbs the child's intermediate
|
|
11
|
+
// turns.
|
|
12
|
+
//
|
|
13
|
+
// This builds directly on the runAgentLoop factory (lib/agent.js): a child
|
|
14
|
+
// runner is just another createAgentRunner instance, wired with WRAPPED
|
|
15
|
+
// executors that enforce the child's allowed-tool set, and sharing the parent's
|
|
16
|
+
// permission manager (no privilege escalation).
|
|
17
|
+
//
|
|
18
|
+
// Security model (load-bearing, per Phase 0):
|
|
19
|
+
// * No privilege escalation — the child uses the SAME permissionManager as
|
|
20
|
+
// the parent, so it can never auto-approve anything the parent wouldn't.
|
|
21
|
+
// * Tool constraint — a child's tools are restricted to its allowed set (from
|
|
22
|
+
// a `.semalt/agents/<name>.md` definition or an inline `tools` list). The
|
|
23
|
+
// wrapped agentExecShell/agentExecFile HARD-REFUSE anything outside the set,
|
|
24
|
+
// which is the enforcement that holds for BOTH the XML and native paths.
|
|
25
|
+
// * No recursion — a child can never invoke `spawn_agent` itself.
|
|
26
|
+
// * Untrusted result — a subagent's returned text may include external data it
|
|
27
|
+
// read, so the parent fences it in the UNTRUSTED_EXTERNAL_CONTENT delimiter
|
|
28
|
+
// (lib/agent.js), exactly like http_get / MCP / hook output.
|
|
29
|
+
//
|
|
30
|
+
// Custom agent definitions live at `.semalt/agents/<name>.md` (project) and
|
|
31
|
+
// `~/.semalt-ai/agents/<name>.md` (global) with optional frontmatter
|
|
32
|
+
// (name / model / tools / description) and a Markdown body = the child's system
|
|
33
|
+
// prompt. Discovery mirrors skills (Task 3.5) and custom commands (Task 3.1).
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const os = require('os');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
|
|
39
|
+
const { TOOL_REGISTRY } = require('./tool_registry');
|
|
40
|
+
|
|
41
|
+
const DEFAULT_MAX_CONCURRENCY = 3;
|
|
42
|
+
const DEFAULT_MAX_ITERATIONS = 12;
|
|
43
|
+
const SPAWN_AGENT_TOOL = 'spawn_agent';
|
|
44
|
+
|
|
45
|
+
// ── Agent-definition discovery ─────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
// Split a frontmatter list value: `read_file, grep shell` or `[a, b]` → [a,b].
|
|
48
|
+
function _splitList(val) {
|
|
49
|
+
if (Array.isArray(val)) return val.map((v) => String(v).trim()).filter(Boolean);
|
|
50
|
+
if (typeof val !== 'string') return [];
|
|
51
|
+
const s = val.trim().replace(/^\[|\]$/g, '');
|
|
52
|
+
return s.split(/[\s,]+/).map((x) => x.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Turn a name into a slash/command-safe slug (lowercase, hyphenated). Mirrors
|
|
56
|
+
// the skills slugifier so `Code Reviewer` → `code-reviewer`.
|
|
57
|
+
function _slugify(s) {
|
|
58
|
+
return String(s || '')
|
|
59
|
+
.trim()
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[\s_]+/g, '-')
|
|
62
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
63
|
+
.replace(/-+/g, '-')
|
|
64
|
+
.replace(/^-|-$/g, '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Parse optional `---`-delimited frontmatter at the top of an agent definition.
|
|
68
|
+
// Recognized keys: name, model, tools (a.k.a. allowed-tools), description. The
|
|
69
|
+
// body is the child's system prompt. Pure. With no frontmatter the whole text
|
|
70
|
+
// is the body.
|
|
71
|
+
function parseAgentFrontmatter(text) {
|
|
72
|
+
const meta = { name: '', model: '', description: '', tools: [] };
|
|
73
|
+
if (typeof text !== 'string') return { meta, body: '' };
|
|
74
|
+
const src = text.replace(/^/, '').replace(/\r\n/g, '\n');
|
|
75
|
+
const m = /^---\n([\s\S]*?)\n---[ \t]*\n?/.exec(src);
|
|
76
|
+
if (!m) return { meta, body: src };
|
|
77
|
+
const body = src.slice(m[0].length);
|
|
78
|
+
for (const rawLine of m[1].split('\n')) {
|
|
79
|
+
const line = rawLine.trim();
|
|
80
|
+
if (!line || line.startsWith('#')) continue;
|
|
81
|
+
const idx = line.indexOf(':');
|
|
82
|
+
if (idx < 0) continue;
|
|
83
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
84
|
+
const val = line.slice(idx + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
85
|
+
if (key === 'name') meta.name = val;
|
|
86
|
+
else if (key === 'model') meta.model = val;
|
|
87
|
+
else if (key === 'description') meta.description = val;
|
|
88
|
+
else if (key === 'tools' || key === 'allowed-tools' || key === 'allowed_tools') meta.tools = _splitList(val);
|
|
89
|
+
}
|
|
90
|
+
return { meta, body };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Walk up from startDir for the nearest `.semalt/agents` directory, bounded by
|
|
94
|
+
// the repo root (the directory holding `.git` is the last one checked). Mirrors
|
|
95
|
+
// findProjectSkillsDir (Task 3.5). Returns the directory path or null.
|
|
96
|
+
function findProjectAgentsDir(startDir) {
|
|
97
|
+
let dir = path.resolve(startDir);
|
|
98
|
+
while (true) {
|
|
99
|
+
const candidate = path.join(dir, '.semalt', 'agents');
|
|
100
|
+
try { if (fs.statSync(candidate).isDirectory()) return candidate; } catch {}
|
|
101
|
+
let atRepoRoot = false;
|
|
102
|
+
try { atRepoRoot = fs.existsSync(path.join(dir, '.git')); } catch {}
|
|
103
|
+
if (atRepoRoot) break;
|
|
104
|
+
const parent = path.dirname(dir);
|
|
105
|
+
if (parent === dir) break;
|
|
106
|
+
dir = parent;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Read every `<name>.md` agent definition under `dir` into a def list, sorted by
|
|
112
|
+
// filename for deterministic order. Unreadable files are skipped.
|
|
113
|
+
function loadAgentDefsFromDir(dir, source) {
|
|
114
|
+
const out = [];
|
|
115
|
+
let entries;
|
|
116
|
+
try { entries = fs.readdirSync(dir); } catch { return out; }
|
|
117
|
+
for (const entry of entries.slice().sort()) {
|
|
118
|
+
if (!/\.md$/i.test(entry)) continue;
|
|
119
|
+
const file = path.join(dir, entry);
|
|
120
|
+
let raw;
|
|
121
|
+
try {
|
|
122
|
+
if (!fs.statSync(file).isFile()) continue;
|
|
123
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
124
|
+
} catch { continue; }
|
|
125
|
+
const { meta, body } = parseAgentFrontmatter(raw);
|
|
126
|
+
const base = entry.replace(/\.md$/i, '');
|
|
127
|
+
const slug = _slugify(meta.name || base);
|
|
128
|
+
if (!slug) continue;
|
|
129
|
+
out.push({
|
|
130
|
+
name: meta.name || base,
|
|
131
|
+
slug,
|
|
132
|
+
model: meta.model || '',
|
|
133
|
+
tools: meta.tools || [],
|
|
134
|
+
description: meta.description || '',
|
|
135
|
+
systemPrompt: (body || '').trim(),
|
|
136
|
+
source,
|
|
137
|
+
file,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Discover agent definitions for a (home, cwd). Global defs load first, then the
|
|
144
|
+
// nearest project defs; on a slug collision project wins. home/cwd injectable.
|
|
145
|
+
function discoverAgentDefs(opts = {}) {
|
|
146
|
+
const home = opts.home || os.homedir();
|
|
147
|
+
const cwd = opts.cwd || process.cwd();
|
|
148
|
+
const global = loadAgentDefsFromDir(path.join(home, '.semalt-ai', 'agents'), 'global');
|
|
149
|
+
const projectDir = findProjectAgentsDir(cwd);
|
|
150
|
+
const project = projectDir ? loadAgentDefsFromDir(projectDir, 'project') : [];
|
|
151
|
+
const bySlug = new Map();
|
|
152
|
+
for (const d of project) if (!bySlug.has(d.slug)) bySlug.set(d.slug, d);
|
|
153
|
+
for (const d of global) if (!bySlug.has(d.slug)) bySlug.set(d.slug, d);
|
|
154
|
+
return Array.from(bySlug.values());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Allowed-tool resolution ────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
// identifier (tag / specName / canonical action) → canonical action, built once
|
|
160
|
+
// from the static registry so a def listing `read_file` or `grep` resolves to
|
|
161
|
+
// the action tuple[0] the loop dispatches on.
|
|
162
|
+
let _idMap = null;
|
|
163
|
+
function _identifierMap() {
|
|
164
|
+
if (_idMap) return _idMap;
|
|
165
|
+
_idMap = new Map();
|
|
166
|
+
for (const e of TOOL_REGISTRY) {
|
|
167
|
+
const ids = new Set([e.tool, ...(e.specNames || []), ...(e.tags || [])]);
|
|
168
|
+
for (const id of ids) _idMap.set(String(id).toLowerCase(), e.tool);
|
|
169
|
+
}
|
|
170
|
+
return _idMap;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Resolve a list of tool identifiers to a Set of canonical actions the child may
|
|
174
|
+
// run. `null`/empty → null, meaning "inherit all of the parent's tools" (still
|
|
175
|
+
// bounded by the shared permission manager). `*`/`all` also means inherit-all.
|
|
176
|
+
// `spawn_agent` is always dropped (no recursion). Unknown names are kept verbatim
|
|
177
|
+
// so dynamic/MCP tool names still pass through.
|
|
178
|
+
function resolveAllowedActions(list) {
|
|
179
|
+
if (!list || !list.length) return null;
|
|
180
|
+
const map = _identifierMap();
|
|
181
|
+
const set = new Set();
|
|
182
|
+
for (const name of list) {
|
|
183
|
+
const key = String(name || '').toLowerCase().trim();
|
|
184
|
+
if (!key) continue;
|
|
185
|
+
if (key === '*' || key === 'all') return null;
|
|
186
|
+
if (key === SPAWN_AGENT_TOOL) continue;
|
|
187
|
+
set.add(map.get(key) || key);
|
|
188
|
+
}
|
|
189
|
+
return set;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Result formatting ──────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function _lastAssistantText(messages) {
|
|
195
|
+
if (!Array.isArray(messages)) return '';
|
|
196
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
197
|
+
const m = messages[i];
|
|
198
|
+
if (m && m.role === 'assistant' && typeof m.content === 'string' && m.content.trim()) {
|
|
199
|
+
return m.content.trim();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _formatResults(results, parallel) {
|
|
206
|
+
if (!results.length) return '(no subagents were run)';
|
|
207
|
+
if (results.length === 1 && !parallel) {
|
|
208
|
+
const r = results[0];
|
|
209
|
+
return r.error ? `Subagent failed: ${r.error}` : (r.output || '(subagent produced no output)');
|
|
210
|
+
}
|
|
211
|
+
return results.map((r, i) => {
|
|
212
|
+
const head = `--- Subagent ${i + 1}${r.label ? ` (${r.label})` : ''} ---`;
|
|
213
|
+
const bodyText = r.error ? `failed: ${r.error}` : (r.output || '(no output)');
|
|
214
|
+
return `${head}\n${bodyText}`;
|
|
215
|
+
}).join('\n\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Manager ────────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
// Build the subagent manager. `deps` carries the same collaborators the agent
|
|
221
|
+
// runner needs (chatStream / extractToolCalls / agentExecShell / agentExecFile /
|
|
222
|
+
// describePermission / permissionManager / ui / getConfig) so it can construct a
|
|
223
|
+
// constrained CHILD runner per spawn. `agentDefs` are the discovered custom
|
|
224
|
+
// definitions. `createRunner` and `runChild` are injectable seams for tests.
|
|
225
|
+
function createSubagentManager(deps = {}) {
|
|
226
|
+
const {
|
|
227
|
+
chatStream,
|
|
228
|
+
extractToolCalls,
|
|
229
|
+
agentExecShell,
|
|
230
|
+
agentExecFile,
|
|
231
|
+
describePermission,
|
|
232
|
+
permissionManager,
|
|
233
|
+
ui,
|
|
234
|
+
getConfig,
|
|
235
|
+
agentDefs = [],
|
|
236
|
+
maxConcurrency = DEFAULT_MAX_CONCURRENCY,
|
|
237
|
+
maxIterations = DEFAULT_MAX_ITERATIONS,
|
|
238
|
+
createRunner = null,
|
|
239
|
+
runChild = null,
|
|
240
|
+
} = deps;
|
|
241
|
+
|
|
242
|
+
const _concurrency = Math.max(1, parseInt(maxConcurrency, 10) || DEFAULT_MAX_CONCURRENCY);
|
|
243
|
+
|
|
244
|
+
const _defsBySlug = new Map();
|
|
245
|
+
for (const d of agentDefs) if (d && d.slug && !_defsBySlug.has(d.slug)) _defsBySlug.set(d.slug, d);
|
|
246
|
+
|
|
247
|
+
function _resolveRunner() {
|
|
248
|
+
if (typeof createRunner === 'function') return createRunner;
|
|
249
|
+
// Lazy require to avoid a load-time cycle (agent.js does not require this).
|
|
250
|
+
return require('./agent').createAgentRunner;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Normalize a raw spawn spec (string prompt or an object) into a concrete
|
|
254
|
+
// child-run plan: prompt, model, system prompt, allowed-action set, label.
|
|
255
|
+
function resolveSpec(raw) {
|
|
256
|
+
let r = raw;
|
|
257
|
+
if (typeof raw === 'string') r = { prompt: raw };
|
|
258
|
+
r = (r && typeof r === 'object') ? r : {};
|
|
259
|
+
|
|
260
|
+
const prompt = String(r.prompt != null ? r.prompt : (r.task != null ? r.task : '')).trim();
|
|
261
|
+
const agentName = r.agent || r.agent_type || r.name || '';
|
|
262
|
+
const def = agentName ? (_defsBySlug.get(_slugify(agentName)) || null) : null;
|
|
263
|
+
|
|
264
|
+
const cfg = (getConfig ? getConfig() : {}) || {};
|
|
265
|
+
const model = r.model || (def && def.model) || cfg.default_model || 'default';
|
|
266
|
+
|
|
267
|
+
const sysOverride = r.system_prompt || r.systemPrompt || (def && def.systemPrompt) || null;
|
|
268
|
+
|
|
269
|
+
let toolList = null;
|
|
270
|
+
if (Array.isArray(r.tools)) toolList = r.tools;
|
|
271
|
+
else if (typeof r.tools === 'string') toolList = _splitList(r.tools);
|
|
272
|
+
else if (def && def.tools && def.tools.length) toolList = def.tools;
|
|
273
|
+
const allowedActions = resolveAllowedActions(toolList);
|
|
274
|
+
|
|
275
|
+
const label = r.label || (def && def.name) || agentName || 'subagent';
|
|
276
|
+
const iters = Number.isInteger(r.max_iterations) && r.max_iterations > 0 ? r.max_iterations : maxIterations;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
prompt,
|
|
280
|
+
agentName: def ? def.name : (agentName || ''),
|
|
281
|
+
model,
|
|
282
|
+
systemPrompt: sysOverride ? String(sysOverride) : null,
|
|
283
|
+
allowedActions,
|
|
284
|
+
label,
|
|
285
|
+
maxIterations: iters,
|
|
286
|
+
def,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Build a CHILD agent runner whose executors enforce `allowedActions` and can
|
|
291
|
+
// never spawn further subagents, sharing the parent's permission manager.
|
|
292
|
+
function _buildChildRunner(allowedActions) {
|
|
293
|
+
// Tool constraint is enforced at the EXECUTOR (not by filtering parse
|
|
294
|
+
// output): a hard refusal here holds for BOTH the XML and native paths, and
|
|
295
|
+
// gives the child feedback so it can adapt to an allowed tool instead of
|
|
296
|
+
// silently ending.
|
|
297
|
+
const isAllowed = (action) => {
|
|
298
|
+
if (action === SPAWN_AGENT_TOOL) return false; // no recursion, ever
|
|
299
|
+
if (!allowedActions) return true; // inherit-all (still permission-bounded)
|
|
300
|
+
return allowedActions.has(action);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const childExecShell = (command, options) => {
|
|
304
|
+
if (!isAllowed('shell')) {
|
|
305
|
+
return Promise.resolve({ exit_code: -1, stdout: '', stderr: 'shell is not permitted for this subagent', blocked: true });
|
|
306
|
+
}
|
|
307
|
+
return agentExecShell(command, options);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const childExecFile = (action, ...rest) => {
|
|
311
|
+
if (!isAllowed(action)) {
|
|
312
|
+
return Promise.resolve({ error: `tool "${action}" is not permitted for this subagent` });
|
|
313
|
+
}
|
|
314
|
+
return agentExecFile(action, ...rest);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Lifecycle hooks are about the USER turn, not internal sub-loops — give the
|
|
318
|
+
// child a no-op runner so Stop/UserPromptSubmit don't double-fire.
|
|
319
|
+
const noopHooks = { run: async () => ({ feedback: [], blocked: false, blockReason: '' }) };
|
|
320
|
+
|
|
321
|
+
const create = _resolveRunner();
|
|
322
|
+
return create({
|
|
323
|
+
chatStream,
|
|
324
|
+
extractToolCalls,
|
|
325
|
+
agentExecShell: childExecShell,
|
|
326
|
+
agentExecFile: childExecFile,
|
|
327
|
+
describePermission,
|
|
328
|
+
permissionManager,
|
|
329
|
+
ui,
|
|
330
|
+
getConfig,
|
|
331
|
+
hooks: noopHooks,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Run one subagent through a fresh, isolated message history and return only
|
|
336
|
+
// its final assistant text.
|
|
337
|
+
async function _defaultRunChild(spec, opts = {}) {
|
|
338
|
+
const runner = _buildChildRunner(spec.allowedActions);
|
|
339
|
+
const childMessages = [{ role: 'user', content: spec.prompt || '(no task provided)' }];
|
|
340
|
+
const signal = opts.signal || null;
|
|
341
|
+
const result = await runner.runAgentLoop(childMessages, spec.model, spec.maxIterations, null, {
|
|
342
|
+
// A bare onError keeps any warning/abort path from calling array methods
|
|
343
|
+
// (messages.sysWarn) that only exist on the chat's message wrapper.
|
|
344
|
+
callbacks: { onError: () => {} },
|
|
345
|
+
systemPrompt: spec.systemPrompt, // null → child uses the default system prompt
|
|
346
|
+
getAbortFlag: signal ? () => signal.aborted : null,
|
|
347
|
+
// Self-verification (Task 4.2) is a TOP-LEVEL gate on the user's task. A
|
|
348
|
+
// child declaring its sub-task done must not trigger the configured verify
|
|
349
|
+
// command — that belongs to the parent's final "done", not each subagent.
|
|
350
|
+
noVerify: true,
|
|
351
|
+
});
|
|
352
|
+
return _lastAssistantText(result && result.messages ? result.messages : childMessages);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function runOne(raw, opts = {}) {
|
|
356
|
+
const spec = resolveSpec(raw);
|
|
357
|
+
const exec = typeof runChild === 'function' ? runChild : _defaultRunChild;
|
|
358
|
+
let output = '';
|
|
359
|
+
let error = null;
|
|
360
|
+
try {
|
|
361
|
+
output = await exec(spec, opts);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
error = err && err.message ? err.message : String(err);
|
|
364
|
+
}
|
|
365
|
+
return { label: spec.label, agent: spec.agentName || '', model: spec.model, output: output || '', error };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Run many subagents with bounded concurrency (a fixed-size worker pool).
|
|
369
|
+
async function runMany(rawSpecs, opts = {}) {
|
|
370
|
+
const specs = Array.isArray(rawSpecs) ? rawSpecs : [rawSpecs];
|
|
371
|
+
if (!specs.length) return [];
|
|
372
|
+
const limit = Math.max(1, Math.min(_concurrency, specs.length));
|
|
373
|
+
const results = new Array(specs.length);
|
|
374
|
+
let next = 0;
|
|
375
|
+
async function worker() {
|
|
376
|
+
while (true) {
|
|
377
|
+
const i = next++;
|
|
378
|
+
if (i >= specs.length) return;
|
|
379
|
+
results[i] = await runOne(specs[i], opts);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const workers = [];
|
|
383
|
+
for (let i = 0; i < limit; i++) workers.push(worker());
|
|
384
|
+
await Promise.all(workers);
|
|
385
|
+
return results;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Tool entry point. Accepts a single spec or a `{ tasks: [...] }` / array form
|
|
389
|
+
// for bounded-parallel execution. Returns the subagent envelope the agent loop
|
|
390
|
+
// fences as untrusted.
|
|
391
|
+
async function spawn(params, opts = {}) {
|
|
392
|
+
let specs;
|
|
393
|
+
let parallel = false;
|
|
394
|
+
if (params && Array.isArray(params.tasks) && params.tasks.length) {
|
|
395
|
+
specs = params.tasks;
|
|
396
|
+
parallel = true;
|
|
397
|
+
} else if (Array.isArray(params)) {
|
|
398
|
+
specs = params;
|
|
399
|
+
parallel = true;
|
|
400
|
+
} else {
|
|
401
|
+
specs = [params];
|
|
402
|
+
}
|
|
403
|
+
const results = await runMany(specs, opts);
|
|
404
|
+
return { subagent: true, content: _formatResults(results, parallel), count: results.length, results };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
spawn,
|
|
409
|
+
runOne,
|
|
410
|
+
runMany,
|
|
411
|
+
resolveSpec,
|
|
412
|
+
listAgents: () => agentDefs.slice(),
|
|
413
|
+
permissionManager, // exposed so callers/tests can confirm no new manager was built
|
|
414
|
+
maxConcurrency: _concurrency,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── spawn_agent dynamic tool entry ─────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
function _parseSpawnXml(text, name) {
|
|
421
|
+
const out = [];
|
|
422
|
+
const re = new RegExp(`<${name}\\b([^>]*?)(?:\\/>|>([\\s\\S]*?)<\\/${name}>)`, 'g');
|
|
423
|
+
for (const m of text.matchAll(re)) {
|
|
424
|
+
const attrStr = m[1] || '';
|
|
425
|
+
const body = m[2] != null ? m[2] : '';
|
|
426
|
+
const attr = (k) => {
|
|
427
|
+
const mm = attrStr.match(new RegExp(`${k}="([^"]*)"`)) || attrStr.match(new RegExp(`${k}='([^']*)'`));
|
|
428
|
+
return mm ? mm[1] : null;
|
|
429
|
+
};
|
|
430
|
+
const trimmed = body.trim();
|
|
431
|
+
let params = {};
|
|
432
|
+
if (trimmed.startsWith('{')) {
|
|
433
|
+
try { params = JSON.parse(trimmed); } catch { params = { prompt: trimmed }; }
|
|
434
|
+
} else if (trimmed) {
|
|
435
|
+
params = { prompt: trimmed };
|
|
436
|
+
}
|
|
437
|
+
const agent = attr('agent') || attr('name');
|
|
438
|
+
const model = attr('model');
|
|
439
|
+
if (agent && !params.agent) params.agent = agent;
|
|
440
|
+
if (model && !params.model) params.model = model;
|
|
441
|
+
out.push([name, params]);
|
|
442
|
+
}
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Build the dynamic tool-registry entry for `spawn_agent`. Same shape as a
|
|
447
|
+
// static / MCP entry so it dispatches through the SAME agent loop. Registered at
|
|
448
|
+
// startup via lib/tool_registry.registerDynamicTool — kept out of the static
|
|
449
|
+
// parity check (lib/constants.js) like all dynamic tools.
|
|
450
|
+
function buildSpawnAgentEntry(manager) {
|
|
451
|
+
const name = SPAWN_AGENT_TOOL;
|
|
452
|
+
return {
|
|
453
|
+
tool: name,
|
|
454
|
+
subagent: true,
|
|
455
|
+
spec: {
|
|
456
|
+
description:
|
|
457
|
+
'Launch a subagent with its OWN isolated context to handle a focused, ' +
|
|
458
|
+
'noisy task (research, reading large files, review) and return ONLY its ' +
|
|
459
|
+
'final result, keeping your context clean. Optionally pass `agent` to use ' +
|
|
460
|
+
'a named definition from .semalt/agents, `model` to override the model, or ' +
|
|
461
|
+
'`tasks` (an array of task objects) to run several independent subagents ' +
|
|
462
|
+
'in parallel with bounded concurrency.',
|
|
463
|
+
parameters: {
|
|
464
|
+
type: 'object',
|
|
465
|
+
properties: {
|
|
466
|
+
prompt: { type: 'string', description: 'The task/instructions for the subagent.' },
|
|
467
|
+
agent: { type: 'string', description: 'Optional named agent definition (.semalt/agents/<name>.md).' },
|
|
468
|
+
model: { type: 'string', description: 'Optional model override for the subagent.' },
|
|
469
|
+
tasks: {
|
|
470
|
+
type: 'array',
|
|
471
|
+
description: 'Optional independent subagent tasks to run in parallel. Each item is an object with the same fields as a single call.',
|
|
472
|
+
items: {
|
|
473
|
+
type: 'object',
|
|
474
|
+
properties: {
|
|
475
|
+
prompt: { type: 'string' },
|
|
476
|
+
agent: { type: 'string' },
|
|
477
|
+
model: { type: 'string' },
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
fromParams: (p) => [name, (p && typeof p === 'object') ? p : {}],
|
|
485
|
+
parseXml: (text) => _parseSpawnXml(text, name),
|
|
486
|
+
// Spawning a subagent runs a whole tool-use loop, so it REQUIRES approval by
|
|
487
|
+
// default — its tag is in no --allow-* tier, so a tier flag can never
|
|
488
|
+
// auto-launch one. The child's own tool calls are then gated by the SAME
|
|
489
|
+
// permission manager (no privilege escalation).
|
|
490
|
+
permission: (_ctx, args) => {
|
|
491
|
+
const p = (args && args[0]) || {};
|
|
492
|
+
const label = p.agent
|
|
493
|
+
? `agent "${p.agent}"`
|
|
494
|
+
: (Array.isArray(p.tasks) ? `${p.tasks.length} parallel subagent(s)` : 'subagent');
|
|
495
|
+
return { actionType: 'agent', description: `Spawn ${label}`, tag: name };
|
|
496
|
+
},
|
|
497
|
+
execute: async (_ctx, args, options) => {
|
|
498
|
+
const params = (args && args[0]) || {};
|
|
499
|
+
const signal = (options && options.signal) || null;
|
|
500
|
+
return manager.spawn(params, { signal });
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
module.exports = {
|
|
506
|
+
DEFAULT_MAX_CONCURRENCY,
|
|
507
|
+
DEFAULT_MAX_ITERATIONS,
|
|
508
|
+
SPAWN_AGENT_TOOL,
|
|
509
|
+
parseAgentFrontmatter,
|
|
510
|
+
findProjectAgentsDir,
|
|
511
|
+
loadAgentDefsFromDir,
|
|
512
|
+
discoverAgentDefs,
|
|
513
|
+
resolveAllowedActions,
|
|
514
|
+
createSubagentManager,
|
|
515
|
+
buildSpawnAgentEntry,
|
|
516
|
+
};
|