Skip to content

Commit 792831b

Browse files
neo-opus-adatobiu
andauthored
feat(ai): FleetManager turnkey stopAgent + restartAgent (#13336) (#13337)
Expose the mutating lifecycle verbs on the control-plane facade, completing the start->stop->restart operator surface (the #13015 MVP criterion). Pure exposure of the already-merged FleetLifecycleService primitives. restartAgent routes through the provisioned start path (stop -> startAgent), NOT lifecycleService.restart -- which re-starts with no cwd and would re-spawn a provisioned agent in the FM dir, silently forking checkout-path-keyed auto-memory (the failure the provisioned start path prevents). removeAgent stays out: blocked on the auto-memory reconciliation policy, and removeAgentRepo forbids auto-wiring. Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
1 parent 95e3f33 commit 792831b

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

ai/services/fleet/FleetManager.mjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,33 @@ class FleetManager extends Base {
139139
managedRoot: this.getManagedRoot()
140140
});
141141
}
142+
143+
/**
144+
* @summary Turnkey stop: gracefully stop an agent's harness process (`SIGTERM`, then `SIGKILL` after
145+
* the timeout) via the lifecycle collaborator. A thin delegation — stopping a process needs no
146+
* `managedRoot` / provisioning, so it forwards straight to `FleetLifecycleService.stop`, mirroring how
147+
* `fleetRepoStatus` forwards to its aggregator.
148+
* @param {String} agentId Registry agent id.
149+
* @returns {Promise<Object>} `{success, id, state}` from the lifecycle service's `stop`.
150+
*/
151+
stopAgent(agentId) {
152+
return this.getLifecycleService().stop(agentId);
153+
}
154+
155+
/**
156+
* @summary Turnkey restart: stop the agent, then start it again through the provisioned path
157+
* ({@link startAgent}) — so the restarted harness re-ensures its repo and runs inside ITS checkout,
158+
* not the Fleet Manager's own directory. Deliberately NOT a delegation to the lifecycle service's own
159+
* `restart`, which re-starts with no `cwd`: that would re-spawn a provisioned agent in the wrong
160+
* directory, and the checkout-path-keyed auto-memory would silently fork (the exact failure the
161+
* provisioned start path prevents). Restarting a non-running agent is just a provisioned start.
162+
* @param {String} agentId Registry agent id.
163+
* @returns {Promise<Object>} the agent's lifecycle status (see {@link startAgent}).
164+
*/
165+
async restartAgent(agentId) {
166+
await this.stopAgent(agentId);
167+
return this.startAgent(agentId);
168+
}
142169
}
143170

144171
export default Neo.setupClass(FleetManager);

test/playwright/unit/ai/FleetManager.spec.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,35 @@ test.describe('Neo.ai.services.fleet.FleetManager', () => {
8989
expect(result).toEqual([{agentId: 'a'}]);
9090
});
9191

92+
test('stopAgent delegates to the lifecycle service stop with the agent id', async () => {
93+
const calls = [],
94+
lifecycle = {stop: id => { calls.push(id); return Promise.resolve({success: true, id, state: 'stopped'}); }};
95+
96+
FleetManager.lifecycleService = lifecycle;
97+
98+
const result = await FleetManager.stopAgent('agent-a');
99+
100+
expect(calls).toEqual(['agent-a']);
101+
expect(result).toMatchObject({success: true, id: 'agent-a', state: 'stopped'});
102+
});
103+
104+
test('restartAgent stops then re-starts via the PROVISIONED path (preserving the repo cwd)', async () => {
105+
const order = [],
106+
lifecycle = {stop: id => { order.push(`stop:${id}`); return Promise.resolve({success: true, id, state: 'stopped'}); }};
107+
108+
FleetManager.managedRoot = '/managed/root';
109+
FleetManager.lifecycleService = lifecycle;
110+
FleetManager.provisionAndStartFn = async args => { order.push(`provisionStart:${args.agentId}`); return {state: 'running'}; };
111+
112+
const status = await FleetManager.restartAgent('agent-a');
113+
114+
// stop runs BEFORE the provisioned start, and the start goes through the provision-then-start
115+
// composer (NOT lifecycleService.restart, which the stub deliberately omits) → the restarted
116+
// agent re-runs in its provisioned checkout, not the Fleet Manager's dir.
117+
expect(order).toEqual(['stop:agent-a', 'provisionStart:agent-a']);
118+
expect(status.state).toBe('running');
119+
});
120+
92121
test('seams default to the real composers (a no-injection construction wires them)', () => {
93122
expect(FleetManager.getProvisionAndStartFn()).toBe(startAgentProvisioned);
94123
expect(FleetManager.getRepoStatusFn()).toBe(inspectFleetRepos);

0 commit comments

Comments
 (0)