@nexrall/code-core 1.4.24 → 1.4.26
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/dist/agent/agentRegistry.d.ts +45 -0
- package/dist/agent/agentRegistry.d.ts.map +1 -0
- package/dist/agent/agentRegistry.js +138 -0
- package/dist/agent/agentTypes.d.ts +31 -0
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +182 -33
- package/dist/agent/loop.d.ts +14 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +176 -18
- package/dist/agent/planMode.d.ts +20 -0
- package/dist/agent/planMode.d.ts.map +1 -0
- package/dist/agent/planMode.js +252 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +14 -0
- package/dist/plugins/index.d.ts +20 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +44 -3
- package/dist/plugins/installer.d.ts +40 -2
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +85 -14
- package/dist/plugins/sources.d.ts +49 -0
- package/dist/plugins/sources.d.ts.map +1 -0
- package/dist/plugins/sources.js +197 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Message } from '../types';
|
|
2
|
+
export interface StoredAgent {
|
|
3
|
+
id: string;
|
|
4
|
+
/**
|
|
5
|
+
* The agent type this ran as, or null for an unnamed (general-purpose) run.
|
|
6
|
+
*
|
|
7
|
+
* Stored so resumption can be re-authorised against the CURRENT permission
|
|
8
|
+
* rules. Without it, an id would be a permanent bypass of any deny rule added
|
|
9
|
+
* after the agent first ran.
|
|
10
|
+
*/
|
|
11
|
+
agentName: string | null;
|
|
12
|
+
/** Full conversation: the sub-agent's own user/assistant/tool messages. */
|
|
13
|
+
messages: Message[];
|
|
14
|
+
/** The description shown in the UI, for `/agents --resumable` style listings. */
|
|
15
|
+
description: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
updatedAt: number;
|
|
18
|
+
/**
|
|
19
|
+
* Monotonic touch counter, used for ordering instead of `updatedAt`.
|
|
20
|
+
*
|
|
21
|
+
* Date.now() has millisecond resolution and these operations take
|
|
22
|
+
* microseconds, so two agents touched in the same tick compare EQUAL and a
|
|
23
|
+
* stable sort silently falls back to insertion order — i.e. "most recent"
|
|
24
|
+
* would have been wrong exactly when several things happen at once, which is
|
|
25
|
+
* the normal case for parallel sub-tasks. The timestamps are kept because
|
|
26
|
+
* they are meaningful to humans; the ordering does not depend on them.
|
|
27
|
+
*/
|
|
28
|
+
seq: number;
|
|
29
|
+
/** Rough JSON size, cached so the byte cap doesn't re-serialise on every write. */
|
|
30
|
+
bytes: number;
|
|
31
|
+
}
|
|
32
|
+
/** Store a finished sub-agent's transcript. Returns its resumable id. */
|
|
33
|
+
export declare function rememberAgent(agentName: string | null, description: string, messages: Message[]): string;
|
|
34
|
+
/** Append a resumed run's new messages to an existing agent. */
|
|
35
|
+
export declare function updateAgent(id: string, messages: Message[]): void;
|
|
36
|
+
export declare function getAgent(id: string): StoredAgent | undefined;
|
|
37
|
+
/** Newest first — the order a "which agents can I resume?" list wants. */
|
|
38
|
+
export declare function listAgents(): StoredAgent[];
|
|
39
|
+
/** Test seam. Not exported through index.ts's public surface by intent. */
|
|
40
|
+
export declare function _resetAgentRegistry(): void;
|
|
41
|
+
export declare const _limits: {
|
|
42
|
+
MAX_AGENTS: number;
|
|
43
|
+
MAX_TOTAL_BYTES: number;
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=agentRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentRegistry.d.ts","sourceRoot":"","sources":["../../src/agent/agentRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AA8CxC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;OASG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;CACf;AA4CD,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,OAAO,EAAE,GAClB,MAAM,CAeR;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI,CAYjE;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAE5D;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,IAAI,WAAW,EAAE,CAE1C;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,eAAO,MAAM,OAAO;;;CAAkC,CAAC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._limits = void 0;
|
|
4
|
+
exports.rememberAgent = rememberAgent;
|
|
5
|
+
exports.updateAgent = updateAgent;
|
|
6
|
+
exports.getAgent = getAgent;
|
|
7
|
+
exports.listAgents = listAgents;
|
|
8
|
+
exports._resetAgentRegistry = _resetAgentRegistry;
|
|
9
|
+
// ─── Resumable sub-agents ────────────────────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// A sub-agent normally starts with a blank slate: fresh context, no memory of a
|
|
12
|
+
// previous run. That is the whole point for one-shot research — but it makes
|
|
13
|
+
// follow-up work absurdly expensive. "Now also check the auth path" re-reads
|
|
14
|
+
// every file the last run already read, because the only way to add to a
|
|
15
|
+
// finished sub-task is to describe the whole job again from scratch.
|
|
16
|
+
//
|
|
17
|
+
// This registry keeps a completed sub-agent's transcript in memory so a later
|
|
18
|
+
// `task` call can continue it instead of restarting it. Claude Code does the
|
|
19
|
+
// same thing; the design decisions worth writing down are the ones about what
|
|
20
|
+
// NOT to do:
|
|
21
|
+
//
|
|
22
|
+
// • IN-MEMORY, process-lifetime only. Deliberately not written to disk. A
|
|
23
|
+
// transcript is unredacted tool output — file contents, command output,
|
|
24
|
+
// whatever a repo happens to contain. Persisting it would create a new
|
|
25
|
+
// durable copy of material the user never asked us to store, in a new place
|
|
26
|
+
// they would have to know to clean up. Losing resumability when the process
|
|
27
|
+
// exits is a much smaller cost than that.
|
|
28
|
+
//
|
|
29
|
+
// • BOUNDED, by entries and by bytes. A transcript is the largest object this
|
|
30
|
+
// process handles, and an agent loop that spawns sub-agents in a cycle would
|
|
31
|
+
// otherwise grow the heap until the CLI dies — a leak that shows up only in
|
|
32
|
+
// the longest sessions, i.e. the ones where losing work hurts most.
|
|
33
|
+
//
|
|
34
|
+
// • Resumption re-checks permission at the CALL SITE, not here. An id is a
|
|
35
|
+
// capability: if a rule denies `task(explorer)` after an explorer agent has
|
|
36
|
+
// already run, resuming by id must not hand that agent back. This module
|
|
37
|
+
// therefore stores the agent's NAME alongside its history, so the caller can
|
|
38
|
+
// re-evaluate the current rules against it. See runSubTask.
|
|
39
|
+
/** Max resumable agents kept alive. Oldest is evicted first. */
|
|
40
|
+
const MAX_AGENTS = 20;
|
|
41
|
+
/**
|
|
42
|
+
* Max total transcript bytes across all stored agents (~8 MB of JSON).
|
|
43
|
+
*
|
|
44
|
+
* Sized to comfortably hold a handful of long research runs while staying far
|
|
45
|
+
* below the point where it competes with the model context for memory. Eviction
|
|
46
|
+
* is by age, not by size, so one enormous transcript cannot pin out many small
|
|
47
|
+
* useful ones.
|
|
48
|
+
*/
|
|
49
|
+
const MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
50
|
+
/**
|
|
51
|
+
* Insertion-ordered store. A Map is enough: Maps iterate in insertion order, so
|
|
52
|
+
* "oldest" is simply the first key, and re-inserting on update moves an entry to
|
|
53
|
+
* the back — giving LRU-by-touch for free without a second data structure.
|
|
54
|
+
*/
|
|
55
|
+
const _agents = new Map();
|
|
56
|
+
let _counter = 0;
|
|
57
|
+
let _seq = 0;
|
|
58
|
+
function totalBytes() {
|
|
59
|
+
let n = 0;
|
|
60
|
+
for (const a of _agents.values())
|
|
61
|
+
n += a.bytes;
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
function evictUntilWithinLimits() {
|
|
65
|
+
while (_agents.size > MAX_AGENTS) {
|
|
66
|
+
const oldest = _agents.keys().next().value;
|
|
67
|
+
if (oldest === undefined)
|
|
68
|
+
break;
|
|
69
|
+
_agents.delete(oldest);
|
|
70
|
+
}
|
|
71
|
+
// Byte cap is checked second: an entry that is individually enormous should
|
|
72
|
+
// still be storable (it is the most expensive thing to recompute), but it
|
|
73
|
+
// must not drag the total past the cap alongside its neighbours.
|
|
74
|
+
while (totalBytes() > MAX_TOTAL_BYTES && _agents.size > 1) {
|
|
75
|
+
const oldest = _agents.keys().next().value;
|
|
76
|
+
if (oldest === undefined)
|
|
77
|
+
break;
|
|
78
|
+
_agents.delete(oldest);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function sizeOf(messages) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.stringify(messages).length;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// A transcript that cannot be serialised (cycles shouldn't happen, but a
|
|
87
|
+
// crash here would take down a completed sub-task's result) is treated as
|
|
88
|
+
// large so it gets evicted early rather than silently counted as free.
|
|
89
|
+
return MAX_TOTAL_BYTES;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Store a finished sub-agent's transcript. Returns its resumable id. */
|
|
93
|
+
function rememberAgent(agentName, description, messages) {
|
|
94
|
+
const id = `agent_${++_counter}`;
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
_agents.set(id, {
|
|
97
|
+
id,
|
|
98
|
+
agentName,
|
|
99
|
+
description,
|
|
100
|
+
messages,
|
|
101
|
+
createdAt: now,
|
|
102
|
+
updatedAt: now,
|
|
103
|
+
seq: ++_seq,
|
|
104
|
+
bytes: sizeOf(messages),
|
|
105
|
+
});
|
|
106
|
+
evictUntilWithinLimits();
|
|
107
|
+
return id;
|
|
108
|
+
}
|
|
109
|
+
/** Append a resumed run's new messages to an existing agent. */
|
|
110
|
+
function updateAgent(id, messages) {
|
|
111
|
+
const existing = _agents.get(id);
|
|
112
|
+
if (!existing)
|
|
113
|
+
return;
|
|
114
|
+
existing.messages = messages;
|
|
115
|
+
existing.updatedAt = Date.now();
|
|
116
|
+
existing.seq = ++_seq;
|
|
117
|
+
existing.bytes = sizeOf(messages);
|
|
118
|
+
// Re-insert so this becomes the most recently used entry, protecting an
|
|
119
|
+
// actively-continued agent from being evicted by a burst of new one-shots.
|
|
120
|
+
_agents.delete(id);
|
|
121
|
+
_agents.set(id, existing);
|
|
122
|
+
evictUntilWithinLimits();
|
|
123
|
+
}
|
|
124
|
+
function getAgent(id) {
|
|
125
|
+
return _agents.get(id);
|
|
126
|
+
}
|
|
127
|
+
/** Newest first — the order a "which agents can I resume?" list wants. */
|
|
128
|
+
function listAgents() {
|
|
129
|
+
return [..._agents.values()].sort((a, b) => b.seq - a.seq);
|
|
130
|
+
}
|
|
131
|
+
/** Test seam. Not exported through index.ts's public surface by intent. */
|
|
132
|
+
function _resetAgentRegistry() {
|
|
133
|
+
_agents.clear();
|
|
134
|
+
_counter = 0;
|
|
135
|
+
_seq = 0;
|
|
136
|
+
}
|
|
137
|
+
exports._limits = { MAX_AGENTS, MAX_TOTAL_BYTES };
|
|
138
|
+
//# sourceMappingURL=agentRegistry.js.map
|
|
@@ -20,6 +20,20 @@ export interface AgentType {
|
|
|
20
20
|
*/
|
|
21
21
|
testFilesOnly?: boolean;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* A problem found while loading an agent definition.
|
|
25
|
+
*
|
|
26
|
+
* These used to be silently swallowed. Every one of them changed what an agent
|
|
27
|
+
* could DO — a mistyped tool name removed a capability, a missing frontmatter
|
|
28
|
+
* block removed the allowlist entirely — and the user was told nothing.
|
|
29
|
+
*/
|
|
30
|
+
export interface AgentWarning {
|
|
31
|
+
/** Absolute path of the definition file the problem was found in. */
|
|
32
|
+
file: string;
|
|
33
|
+
/** Agent name, when one could be determined. */
|
|
34
|
+
agent: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* Discover all agent types. Precedence: project > global > plugin > builtin.
|
|
25
39
|
*
|
|
@@ -30,6 +44,23 @@ export interface AgentType {
|
|
|
30
44
|
* readdir order is not guaranteed to be stable across machines or platforms.
|
|
31
45
|
*/
|
|
32
46
|
export declare function loadAgentTypes(workDir: string): AgentType[];
|
|
47
|
+
/**
|
|
48
|
+
* As `loadAgentTypes`, but also reports what was wrong with the definitions.
|
|
49
|
+
*
|
|
50
|
+
* Split in two so the common caller stays a one-liner while the CLI's `/agents`
|
|
51
|
+
* and the agent loop can surface problems. Warnings never hide an agent: a
|
|
52
|
+
* flawed definition still loads (fail-closed on PERMISSIONS, not on existence),
|
|
53
|
+
* because making a user's agent vanish over a typo is its own kind of silent
|
|
54
|
+
* failure.
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadAgentTypesWithWarnings(workDir: string): {
|
|
57
|
+
types: AgentType[];
|
|
58
|
+
warnings: AgentWarning[];
|
|
59
|
+
};
|
|
60
|
+
/** The tool names a hand-written allowlist may use (exported for validation + tests). */
|
|
61
|
+
export declare function knownToolNames(): string[];
|
|
62
|
+
/** The built-in agents, for clients that want to show them alongside user-defined ones. */
|
|
63
|
+
export declare function builtinAgents(): AgentType[];
|
|
33
64
|
/**
|
|
34
65
|
* One line per agent for the system prompt's <available_subagents> block.
|
|
35
66
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AA6QD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AA0JD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAqB5G;AAED,yFAAyF;AACzF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,IAAI,SAAS,EAAE,CAE3C;AACD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,CAY1D;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAIjG"}
|
package/dist/agent/agentTypes.js
CHANGED
|
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.loadAgentTypes = loadAgentTypes;
|
|
37
|
+
exports.loadAgentTypesWithWarnings = loadAgentTypesWithWarnings;
|
|
38
|
+
exports.knownToolNames = knownToolNames;
|
|
39
|
+
exports.builtinAgents = builtinAgents;
|
|
37
40
|
exports.summariseAgents = summariseAgents;
|
|
38
41
|
exports.findAgentType = findAgentType;
|
|
39
42
|
const fs = __importStar(require("fs"));
|
|
@@ -57,17 +60,38 @@ const index_1 = require("../plugins/index");
|
|
|
57
60
|
const READ_ONLY_TOOLS = [
|
|
58
61
|
// Universal
|
|
59
62
|
'read_file', 'search_files', 'glob', 'list_directory', 'bash', 'bash_output',
|
|
63
|
+
// kill_shell belongs next to bash_output: an agent that can start a background
|
|
64
|
+
// process and poll it but never stop it leaks that process past its own
|
|
65
|
+
// lifetime. Stopping a shell you started is not a write to the repo.
|
|
66
|
+
'kill_shell',
|
|
60
67
|
'notebook_read', 'todo_write', 'todo_read',
|
|
68
|
+
// Skills are reusable prompt playbooks, and loop.ts advertises the skills
|
|
69
|
+
// catalogue to sub-agents at EVERY depth — so withholding the tool that loads
|
|
70
|
+
// one meant showing every sub-agent a menu it could not order from.
|
|
71
|
+
'use_skill',
|
|
61
72
|
// VS Code language server (ignored on the CLI)
|
|
62
73
|
'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
|
|
63
74
|
'get_hover', 'get_diagnostics',
|
|
64
75
|
];
|
|
65
|
-
/**
|
|
66
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Read-only + the network, for agents that must consult external sources.
|
|
78
|
+
*
|
|
79
|
+
* `web_search` is deliberately ABSENT despite the name of this list. It is a
|
|
80
|
+
* SERVER-SIDE tool: Anthropic executes it and returns the result inside the same
|
|
81
|
+
* assistant message, so loop.ts filters those blocks out before the permission
|
|
82
|
+
* gate ever runs. Listing it here would be theatre — it grants nothing (agents
|
|
83
|
+
* without it can still search) and denies nothing. Naming the absence is the
|
|
84
|
+
* only way to stop it being "helpfully" re-added.
|
|
85
|
+
*/
|
|
86
|
+
const RESEARCH_TOOLS = [...READ_ONLY_TOOLS, 'fetch_url'];
|
|
67
87
|
/** Read-only + the write tools, for agents that produce code. */
|
|
68
88
|
const WRITE_TOOLS = [
|
|
69
89
|
...READ_ONLY_TOOLS,
|
|
70
90
|
'write_file', 'edit_file', 'multi_edit', 'create_directory', 'move_file', 'copy_file',
|
|
91
|
+
// notebook_edit is a write tool like any other, and allowsTestOnlyWrite already
|
|
92
|
+
// knows its shape (`source` is cell CONTENT, not a path). Omitting it just meant
|
|
93
|
+
// test-writer silently could not touch notebooks.
|
|
94
|
+
'notebook_edit',
|
|
71
95
|
];
|
|
72
96
|
const BUILTIN_AGENTS = [
|
|
73
97
|
{
|
|
@@ -247,17 +271,43 @@ const BUILTIN_AGENTS = [
|
|
|
247
271
|
].join('\n'),
|
|
248
272
|
},
|
|
249
273
|
];
|
|
274
|
+
/**
|
|
275
|
+
* Every tool name a client may offer, for validating a hand-written allowlist.
|
|
276
|
+
*
|
|
277
|
+
* Deliberately a SEPARATE list rather than an import from tools/executor.ts:
|
|
278
|
+
* that module's TOOL_MAP is the CLI's local dispatch table and legitimately
|
|
279
|
+
* lacks `get_diagnostics` (intercepted by VS Code before the executor) and
|
|
280
|
+
* `web_search` (run server-side by Anthropic). Validating against it would
|
|
281
|
+
* reject two perfectly valid names. A canary test asserts every name used by the
|
|
282
|
+
* builtin agents appears here, so the two cannot drift apart unnoticed.
|
|
283
|
+
*/
|
|
284
|
+
const KNOWN_TOOL_NAMES = new Set([
|
|
285
|
+
'read_file', 'write_file', 'edit_file', 'multi_edit', 'list_directory', 'create_directory',
|
|
286
|
+
'move_file', 'copy_file', 'delete_file', 'search_files', 'glob',
|
|
287
|
+
'bash', 'bash_output', 'kill_shell',
|
|
288
|
+
'notebook_read', 'notebook_edit',
|
|
289
|
+
'todo_write', 'todo_read', 'memory_write', 'memory_read', 'use_skill',
|
|
290
|
+
'fetch_url', 'web_search', 'generate_image', 'stock_photo', 'open_in_browser',
|
|
291
|
+
'task',
|
|
292
|
+
'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
|
|
293
|
+
'get_hover', 'get_diagnostics',
|
|
294
|
+
]);
|
|
295
|
+
/** Frontmatter keys this parser understands, for typo detection. */
|
|
296
|
+
const KNOWN_META_KEYS = new Set([
|
|
297
|
+
'name', 'description', 'tools', 'model', 'test_files_only', 'testfilesonly',
|
|
298
|
+
]);
|
|
299
|
+
const VALID_MODELS = ['turbo', 'pro', 'ultra'];
|
|
250
300
|
function parseFrontmatter(raw) {
|
|
251
301
|
const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
|
|
252
302
|
if (!m)
|
|
253
|
-
return { meta: {}, body: raw.trim() };
|
|
303
|
+
return { meta: {}, body: raw.trim(), ok: false };
|
|
254
304
|
const meta = {};
|
|
255
305
|
for (const line of m[1].split(/\r?\n/)) {
|
|
256
306
|
const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
257
307
|
if (kv)
|
|
258
308
|
meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
259
309
|
}
|
|
260
|
-
return { meta, body: (m[2] ?? '').trim() };
|
|
310
|
+
return { meta, body: (m[2] ?? '').trim(), ok: true };
|
|
261
311
|
}
|
|
262
312
|
function parseModel(v) {
|
|
263
313
|
const s = (v ?? '').toLowerCase();
|
|
@@ -277,41 +327,118 @@ function parseToolList(v) {
|
|
|
277
327
|
.filter(Boolean);
|
|
278
328
|
return tools.length ? tools : undefined;
|
|
279
329
|
}
|
|
280
|
-
function loadDir(dir, source, into) {
|
|
330
|
+
function loadDir(dir, source, into, warnings) {
|
|
281
331
|
let entries;
|
|
282
332
|
try {
|
|
283
|
-
entries = fs.readdirSync(dir
|
|
333
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
284
334
|
}
|
|
285
335
|
catch {
|
|
336
|
+
// Overwhelmingly "no .nexrall/agents here", which is the normal case and not
|
|
337
|
+
// worth a word. A genuine permission error is rare enough that the cost of
|
|
338
|
+
// staying quiet is lower than warning on every repo that has no agents.
|
|
286
339
|
return;
|
|
287
340
|
}
|
|
288
|
-
for (const
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
const full = path.join(dir, entry.name);
|
|
343
|
+
// Recurse into subfolders so definitions can be organised (agents/review/…),
|
|
344
|
+
// matching Claude Code. Identity still comes only from the name field / filename.
|
|
345
|
+
if (entry.isDirectory()) {
|
|
346
|
+
loadDir(full, source, into, warnings);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!entry.name.endsWith('.md'))
|
|
350
|
+
continue;
|
|
351
|
+
let raw;
|
|
289
352
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
353
|
+
raw = fs.readFileSync(full, 'utf-8');
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
warnings.push({ file: full, agent: path.basename(entry.name, '.md'), message: `could not be read (${err.message}) — this agent was skipped` });
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const { meta, body, ok } = parseFrontmatter(raw);
|
|
360
|
+
const name = (meta.name || path.basename(entry.name, '.md')).trim();
|
|
361
|
+
if (!name)
|
|
362
|
+
continue;
|
|
363
|
+
// Earlier tiers win (project > global > plugin).
|
|
364
|
+
if (source !== 'project' && into.has(name))
|
|
365
|
+
continue;
|
|
366
|
+
const tools = parseToolList(meta.tools);
|
|
367
|
+
let effectiveTools = tools;
|
|
368
|
+
// ── FAIL CLOSED on a definition we could not parse ────────────────────────
|
|
369
|
+
//
|
|
370
|
+
// A file with no `---` frontmatter block used to yield tools === undefined,
|
|
371
|
+
// which means "no allowlist" — i.e. FULL access, including write_file, bash
|
|
372
|
+
// and delete_file. So the worse the file, the more power it got: the exact
|
|
373
|
+
// inversion you do not want in a permission system. A malformed definition
|
|
374
|
+
// is now read-only, which is both the safe reading and, for anyone hand-
|
|
375
|
+
// writing an agent, almost always the intended one.
|
|
376
|
+
if (!ok) {
|
|
377
|
+
effectiveTools = READ_ONLY_TOOLS;
|
|
378
|
+
warnings.push({
|
|
379
|
+
file: full,
|
|
380
|
+
agent: name,
|
|
381
|
+
message: 'has no valid YAML frontmatter (a `---` block must be the first thing in the file), so no ' +
|
|
382
|
+
'tool allowlist could be read. Treating it as READ-ONLY. Add frontmatter with a `tools:` line ' +
|
|
383
|
+
'to grant more.',
|
|
310
384
|
});
|
|
311
385
|
}
|
|
312
|
-
|
|
313
|
-
|
|
386
|
+
else {
|
|
387
|
+
if (!meta.description) {
|
|
388
|
+
warnings.push({
|
|
389
|
+
file: full,
|
|
390
|
+
agent: name,
|
|
391
|
+
message: 'has no `description:` — that text is the ONLY thing the model uses to decide when to delegate to this agent, so it will rarely be picked.',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (meta.model !== undefined && parseModel(meta.model) === undefined) {
|
|
395
|
+
warnings.push({
|
|
396
|
+
file: full,
|
|
397
|
+
agent: name,
|
|
398
|
+
message: `has model: "${meta.model}", which is not valid — use one of ${VALID_MODELS.join(', ')}, or omit the line to inherit the current session's model.`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
// The highest-value warning of the lot. An allowlist only ever GRANTS, so a
|
|
402
|
+
// misspelled name is not an error anywhere — the tool is simply never
|
|
403
|
+
// permitted, and the model is told "Permission denied", which points it at
|
|
404
|
+
// the user rather than at the typo.
|
|
405
|
+
const unknown = (tools ?? []).filter((t) => !KNOWN_TOOL_NAMES.has(t) && !t.includes('__'));
|
|
406
|
+
if (unknown.length) {
|
|
407
|
+
warnings.push({
|
|
408
|
+
file: full,
|
|
409
|
+
agent: name,
|
|
410
|
+
message: `lists unknown tool name(s): ${unknown.join(', ')}. An allowlist only grants, so these silently do nothing and the agent cannot use them. Tool names are lower_snake_case (read_file, search_files, glob, bash).`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k));
|
|
414
|
+
if (strayKeys.length) {
|
|
415
|
+
warnings.push({
|
|
416
|
+
file: full,
|
|
417
|
+
agent: name,
|
|
418
|
+
message: `has unrecognised frontmatter key(s): ${strayKeys.join(', ')} — these are ignored.`,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (!body) {
|
|
422
|
+
warnings.push({
|
|
423
|
+
file: full,
|
|
424
|
+
agent: name,
|
|
425
|
+
message: 'has an empty body — the text below the frontmatter IS the agent\'s system prompt, so it currently has no instructions.',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
314
428
|
}
|
|
429
|
+
into.set(name, {
|
|
430
|
+
name,
|
|
431
|
+
description: meta.description || `Custom ${name} agent`,
|
|
432
|
+
tools: effectiveTools,
|
|
433
|
+
model: parseModel(meta.model),
|
|
434
|
+
prompt: body,
|
|
435
|
+
source,
|
|
436
|
+
// Exposed to user/plugin definitions too — `test_files_only: true` (or
|
|
437
|
+
// `testFilesOnly`) lets anyone build a test-writing agent that genuinely
|
|
438
|
+
// cannot touch production source, rather than only the builtin getting
|
|
439
|
+
// that guarantee.
|
|
440
|
+
...(parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}),
|
|
441
|
+
});
|
|
315
442
|
}
|
|
316
443
|
}
|
|
317
444
|
/**
|
|
@@ -324,11 +451,24 @@ function loadDir(dir, source, into) {
|
|
|
324
451
|
* readdir order is not guaranteed to be stable across machines or platforms.
|
|
325
452
|
*/
|
|
326
453
|
function loadAgentTypes(workDir) {
|
|
454
|
+
return loadAgentTypesWithWarnings(workDir).types;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* As `loadAgentTypes`, but also reports what was wrong with the definitions.
|
|
458
|
+
*
|
|
459
|
+
* Split in two so the common caller stays a one-liner while the CLI's `/agents`
|
|
460
|
+
* and the agent loop can surface problems. Warnings never hide an agent: a
|
|
461
|
+
* flawed definition still loads (fail-closed on PERMISSIONS, not on existence),
|
|
462
|
+
* because making a user's agent vanish over a typo is its own kind of silent
|
|
463
|
+
* failure.
|
|
464
|
+
*/
|
|
465
|
+
function loadAgentTypesWithWarnings(workDir) {
|
|
327
466
|
const out = new Map();
|
|
328
|
-
|
|
329
|
-
loadDir(path.join(
|
|
467
|
+
const warnings = [];
|
|
468
|
+
loadDir(path.join(workDir, '.nexrall', 'agents'), 'project', out, warnings);
|
|
469
|
+
loadDir(path.join(os.homedir(), '.nexrall', 'agents'), 'global', out, warnings);
|
|
330
470
|
for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'agents'))
|
|
331
|
-
loadDir(dir, 'plugin', out);
|
|
471
|
+
loadDir(dir, 'plugin', out, warnings);
|
|
332
472
|
for (const agent of BUILTIN_AGENTS) {
|
|
333
473
|
if (!out.has(agent.name))
|
|
334
474
|
out.set(agent.name, agent);
|
|
@@ -336,7 +476,7 @@ function loadAgentTypes(workDir) {
|
|
|
336
476
|
// Builtins first in their declared order (the common, cache-friendly case), then
|
|
337
477
|
// everything user-supplied alphabetically.
|
|
338
478
|
const builtinOrder = new Map(BUILTIN_AGENTS.map((a, i) => [a.name, i]));
|
|
339
|
-
|
|
479
|
+
const types = [...out.values()].sort((a, b) => {
|
|
340
480
|
const ai = builtinOrder.get(a.name);
|
|
341
481
|
const bi = builtinOrder.get(b.name);
|
|
342
482
|
if (ai !== undefined && bi !== undefined)
|
|
@@ -347,6 +487,15 @@ function loadAgentTypes(workDir) {
|
|
|
347
487
|
return 1;
|
|
348
488
|
return a.name.localeCompare(b.name);
|
|
349
489
|
});
|
|
490
|
+
return { types, warnings };
|
|
491
|
+
}
|
|
492
|
+
/** The tool names a hand-written allowlist may use (exported for validation + tests). */
|
|
493
|
+
function knownToolNames() {
|
|
494
|
+
return [...KNOWN_TOOL_NAMES].sort();
|
|
495
|
+
}
|
|
496
|
+
/** The built-in agents, for clients that want to show them alongside user-defined ones. */
|
|
497
|
+
function builtinAgents() {
|
|
498
|
+
return BUILTIN_AGENTS;
|
|
350
499
|
}
|
|
351
500
|
/**
|
|
352
501
|
* One line per agent for the system prompt's <available_subagents> block.
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -6,6 +6,20 @@ export declare function resolveMaxIterations(optionValue: number | undefined, se
|
|
|
6
6
|
* dozen lines.
|
|
7
7
|
*/
|
|
8
8
|
export declare function createLimiter(max: number): <T>(fn: () => Promise<T>) => Promise<T>;
|
|
9
|
+
/**
|
|
10
|
+
* Thrown by a sub-agent's permission gate when the AGENT DEFINITION forbids a
|
|
11
|
+
* tool — as opposed to the user declining it.
|
|
12
|
+
*
|
|
13
|
+
* The distinction matters to the model, which is why this is an exception rather
|
|
14
|
+
* than a `false`: both used to collapse into "Permission denied by user", so an
|
|
15
|
+
* agent blocked by its own allowlist (very often a mistyped tool name) was told
|
|
16
|
+
* the human had refused. The rational response to that is to ask again, which
|
|
17
|
+
* can never succeed. Carrying a reason lets the tool_result say what is actually
|
|
18
|
+
* true and what to do instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ToolNotAllowedError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
9
23
|
/**
|
|
10
24
|
* Reduce a sub-agent's message history to the text its parent should receive.
|
|
11
25
|
*
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AA2KlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AAkED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAgBlF;AAuJD;;;;;;;;;;GAUG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AA4BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,UAAU,UAAO,GAAG,MAAM,CAYjF;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAc,GAAG,MAAM,CAKtE;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAoBpE;AAkTD,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAE1E;AA8BD,kHAAkH;AAClH,wBAAgB,oBAAoB,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAEzE;AAuBD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,OAAO,CA8BrG;AAED,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAoDN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,SAAI,GAAG,MAAM,CAgCpF;AAsKD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,GACA,OAAO,CAAC,OAAO,CAAC,CAqElB;AAID,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CAgxBpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAqCtE"}
|