Skip to content

Commit 347c1b8

Browse files
committed
feat: Implement autonomous sub-agent orchestration and Browser profile (#9661)
1 parent fdf8873 commit 347c1b8

6 files changed

Lines changed: 116 additions & 21 deletions

File tree

ai/Agent.mjs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ class Agent extends Base {
3535
* @member {Object|null} providerConfig=null
3636
*/
3737
providerConfig: null,
38+
/**
39+
* The maximum number of interaction turns a sub-agent can execute
40+
* before being deliberately recycled to flush its context window.
41+
* @member {Number} maxSubAgentLifespan=50
42+
*/
43+
maxSubAgentLifespan: 50,
3844
/**
3945
* A list of server names (keys in ClientConfig) to connect to.
4046
* @member {String[]} servers=[]
@@ -45,16 +51,29 @@ class Agent extends Base {
4551
* @member {Object} subAgents
4652
*/
4753
subAgents: {
54+
browser : async () => (await import('./agent/profile/Browser.mjs')).default,
4855
librarian: async () => (await import('./agent/profile/Librarian.mjs')).default
4956
}
5057
}
5158

59+
/**
60+
* Map of currently running sub-agent instances.
61+
* @member {Object} activeSubAgents={}
62+
*/
63+
activeSubAgents = {}
64+
5265
/**
5366
* Map of connected Client instances, keyed by server name.
5467
* @member {Object} clients={}
5568
*/
5669
clients = {}
5770

71+
/**
72+
* Track turn numbers to prevent hallucination cascades.
73+
* @member {Object} subAgentTurns={}
74+
*/
75+
subAgentTurns = {}
76+
5877
/**
5978
* Async initialization sequence.
6079
* Creates and connects all configured clients, then initializes the Cognitive Runtime.
@@ -157,33 +176,54 @@ class Agent extends Base {
157176
* Spawns a sub-agent ephemerally to execute a task and return the synthesis.
158177
* @param {String} profileName The alias inside subAgents config.
159178
* @param {String} request The query/task to delegate.
179+
* @param {Boolean} [forceFresh=false] Whether to force a topic switch / new instance.
160180
* @returns {Promise<String>} The generated result content.
161181
*/
162-
async delegate(profileName, request) {
182+
async delegate(profileName, request, forceFresh = false) {
163183
const getProfileClass = this.subAgents[profileName];
164184

165185
if (!getProfileClass) {
166186
throw new Error(`Sub-Agent profile '${profileName}' not found.`);
167187
}
168188

169-
const ProfileClass = await getProfileClass();
189+
// Context Flush (Max Tasks Gate) or explicit Reset
190+
if (this.activeSubAgents[profileName]) {
191+
if (forceFresh || this.subAgentTurns[profileName] >= this.maxSubAgentLifespan) {
192+
console.log(`[Agent] Recycling sub-agent '${profileName}' (Max Turns Reached / Forced Switch)`);
193+
await this.activeSubAgents[profileName].disconnect();
194+
delete this.activeSubAgents[profileName];
195+
this.subAgentTurns[profileName] = 0;
196+
}
197+
}
170198

171-
console.log(`[Agent] Delegating to Sub-Agent: ${profileName} (${ProfileClass.config.className})`);
199+
let subAgent = this.activeSubAgents[profileName];
172200

173-
const subAgent = Neo.create(ProfileClass);
174-
await subAgent.initAsync();
201+
if (!subAgent) {
202+
const ProfileClass = await getProfileClass();
203+
console.log(`[Agent] Booting fresh Sub-Agent: ${profileName} (${ProfileClass.config.className})`);
204+
205+
subAgent = Neo.create(ProfileClass);
206+
await subAgent.initAsync();
207+
208+
this.activeSubAgents[profileName] = subAgent;
209+
this.subAgentTurns[profileName] = 0;
210+
} else {
211+
console.log(`[Agent] Re-using active Sub-Agent: ${profileName} (Turn ${this.subAgentTurns[profileName] + 1})`);
212+
}
175213

176214
try {
215+
this.subAgentTurns[profileName]++;
216+
177217
const result = await subAgent.loop.processEvent({
178218
type: 'delegate',
179219
data: request,
180220
systemPrompt: subAgent.systemPrompt
181221
});
182222

183223
return result;
184-
} finally {
185-
console.log(`[Agent] Sub-Agent ${profileName} completed. Terminating connection.`);
186-
await subAgent.disconnect();
224+
} catch (err) {
225+
console.error(`[Agent] Delegate execution failed for ${profileName}:`, err);
226+
throw err;
187227
}
188228
}
189229
}

ai/agent/Loop.mjs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Assembler from '../context/Assembler.mjs';
22
import Base from '../../src/core/Base.mjs';
33
import ClassSystemUtil from '../../src/util/ClassSystem.mjs';
44
import Provider from '../provider/Base.mjs';
5-
import SDK from '../services.mjs';
5+
import * as SDK from '../services.mjs';
66

77
/**
88
* The cognitive event loop for the Agent.
@@ -202,8 +202,29 @@ class Loop extends Base {
202202
console.error(`[Loop] Failed to fetch tools from client: ${clientName}`, e);
203203
}
204204
}
205+
205206
console.log(`[Loop] Discovered ${this.tools.length} total tools from clients.`);
206207
}
208+
209+
// 2. Inject Native Tools
210+
this.tools.push({
211+
name: "delegate_task",
212+
description: "Delegates a complex task or query to a specialized sub-agent expert. Use this when you lack the context or tools to fulfill the user's request. Wait for the sub-agent's response to formulate your final answer.",
213+
inputSchema: {
214+
type: "object",
215+
properties: {
216+
agent: {
217+
type: "string",
218+
description: "The name of the specialized sub-agent profile (e.g. 'librarian', 'browser')."
219+
},
220+
query: {
221+
type: "string",
222+
description: "The specific task, objective, or question to delegate to the sub-agent."
223+
}
224+
},
225+
required: ["agent", "query"]
226+
}
227+
});
207228
}
208229

209230
/**

ai/agent/profile/Browser.mjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Agent from '../../Agent.mjs';
2+
3+
/**
4+
* A specialized sub-agent for autonomous telemetry and visual DOM manipulation.
5+
*
6+
* @class Neo.ai.agent.profile.Browser
7+
* @extends Neo.ai.Agent
8+
*/
9+
class Browser extends Agent {
10+
static config = {
11+
/**
12+
* @member {String} className='Neo.ai.agent.profile.Browser'
13+
* @protected
14+
*/
15+
className: 'Neo.ai.agent.profile.Browser',
16+
/**
17+
* Configurable model provider.
18+
* @member {String|Neo.ai.provider.Base} modelProvider='gemini'
19+
*/
20+
modelProvider: 'gemini',
21+
/**
22+
* Connects to the Neural-Link and Chrome DevTools MCP servers.
23+
* @member {String[]} servers=['chrome-devtools','neural-link']
24+
*/
25+
servers: ['chrome-devtools', 'neural-link'],
26+
/**
27+
* Specialized system prompt tailored for visual inspection and control.
28+
* @member {String} systemPrompt
29+
*/
30+
systemPrompt: 'You are the Browser, a specialized sub-agent expert in visual telemetry and component tree interrogation. You have direct access to the active Neo.mjs App Worker session. Your mandate is to inspect visual component trees, fetch computed styles, simulate events, and report actual DOM/VDOM state back to the orchestrator utilizing your registered tools.'
31+
}
32+
}
33+
34+
export default Neo.setupClass(Browser);

ai/agent/profile/Librarian.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ class Librarian extends Agent {
1414
* @protected
1515
*/
1616
className: 'Neo.ai.agent.profile.Librarian',
17-
/**
18-
* The Librarian exclusively connects to the knowledge-base server.
19-
* @member {String[]} servers=['knowledge-base']
20-
*/
21-
servers: ['knowledge-base'],
2217
/**
2318
* Configurable model provider. Defaults to 'gemini' for reasoning speed
2419
* but can be instantiated with 'ollama' (e.g. gemma-4-31b-it) to support
2520
* swarms and offline sub-agent spawning.
2621
* @member {String|Neo.ai.provider.Base} modelProvider='gemini'
2722
*/
2823
modelProvider: 'gemini',
24+
/**
25+
* The Librarian exclusively connects to the knowledge-base server.
26+
* @member {String[]} servers=['knowledge-base']
27+
*/
28+
servers: ['knowledge-base'],
2929
/**
3030
* Specialized system prompt tailored for GraphRAG synthesis.
3131
* The loop will inject this when processing research tasks.

ai/mcp/client/config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ const defaultConfig = {
1313
* The value is an object with 'command' and 'args' properties.
1414
*/
1515
mcpServers: {
16+
"chrome-devtools": {
17+
command: "npx",
18+
args : ["-y", "chrome-devtools-mcp@latest"]
19+
},
1620
"github-workflow": {
1721
command : "npm",
1822
args : ["run", "ai:mcp-server-github-workflow"],

test/playwright/unit/ai/agent/Librarian.spec.mjs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,18 @@ test.describe('Librarian Sub-Agent Orchestration', () => {
7272
};
7373
};
7474

75-
// We use a highly explicit prompt to force Gemini to output a manual JSON tool call
76-
// so our fallback parser inside Loop.mjs intercepts it and triggers `delegate_task`.
75+
// Since delegate_task is now injected natively into the Loop's tools array,
76+
// we can simply instruct the model to use the tool, and it will trigger it natively.
7777
const event = {
7878
type: 'user:input',
7979
priority: 'high',
80-
data: 'You must research the architectural purpose of Neo.component.Base. You do not have the context. You MUST delegate this by outputting strictly the following JSON object and nothing else: { "tool": "delegate_task", "agent": "librarian", "query": "What is the architectural purpose of Neo.component.Base?" }'
80+
data: 'You must research the architectural purpose of Neo.component.Base. You do not have the context. Delegate this to the "librarian" sub-agent using the delegate_task tool. Once you get the result, formulate your final answer.'
8181
};
8282

8383
// Bypass the scheduler and force synchronous processing for the test environment
8484
const finalAnswer = await primaryAgent.loop.processEvent(event);
8585

8686
expect(delegateCalled).toBeTruthy();
8787
expect(delegatedAgentAlias).toBe('librarian');
88-
expect(finalAnswer).toBeDefined();
89-
90-
// Output for debugging
91-
console.log('[Playwright E2E] Primary Agent resolved final answer:', finalAnswer);
9288
});
9389
});

0 commit comments

Comments
 (0)