@robota-sdk/agent-command 3.0.0-beta.64

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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/dist/node/index.cjs +30 -0
  3. package/dist/node/index.d.ts +293 -0
  4. package/dist/node/index.d.ts.map +1 -0
  5. package/dist/node/index.js +31 -0
  6. package/dist/node/index.js.map +1 -0
  7. package/package.json +48 -0
  8. package/src/agent/__tests__/agent-command.test.ts +504 -0
  9. package/src/agent/agent-command-module.ts +82 -0
  10. package/src/agent/agent-command-parser.ts +180 -0
  11. package/src/agent/agent-command.ts +235 -0
  12. package/src/agent/index.ts +7 -0
  13. package/src/background/__tests__/background-command-module.test.ts +255 -0
  14. package/src/background/background-command-module.ts +53 -0
  15. package/src/background/background-command.ts +63 -0
  16. package/src/background/index.ts +6 -0
  17. package/src/compact/__tests__/compact-command-module.test.ts +162 -0
  18. package/src/compact/compact-command-module.ts +51 -0
  19. package/src/compact/compact-command.ts +21 -0
  20. package/src/compact/index.ts +6 -0
  21. package/src/context/__tests__/context-command-module.test.ts +294 -0
  22. package/src/context/context-command-module.ts +54 -0
  23. package/src/context/context-command.ts +298 -0
  24. package/src/context/index.ts +6 -0
  25. package/src/exit/__tests__/exit-command-module.test.ts +35 -0
  26. package/src/exit/exit-command-module.ts +48 -0
  27. package/src/exit/exit-command.ts +10 -0
  28. package/src/exit/index.ts +6 -0
  29. package/src/help/__tests__/help-command-module.test.ts +106 -0
  30. package/src/help/help-command-module.ts +48 -0
  31. package/src/help/help-command.ts +9 -0
  32. package/src/help/index.ts +6 -0
  33. package/src/index.ts +20 -0
  34. package/src/language/__tests__/language-command-module.test.ts +105 -0
  35. package/src/language/index.ts +6 -0
  36. package/src/language/language-command-module.ts +56 -0
  37. package/src/language/language-command.ts +22 -0
  38. package/src/memory/__tests__/memory-command-module.test.ts +272 -0
  39. package/src/memory/index.ts +6 -0
  40. package/src/memory/memory-command-module.ts +57 -0
  41. package/src/memory/memory-command.ts +234 -0
  42. package/src/mode/__tests__/mode-command-module.test.ts +143 -0
  43. package/src/mode/index.ts +6 -0
  44. package/src/mode/mode-command-module.ts +56 -0
  45. package/src/mode/mode-command.ts +34 -0
  46. package/src/model/__tests__/model-command-module.test.ts +273 -0
  47. package/src/model/index.ts +6 -0
  48. package/src/model/model-command-module.ts +68 -0
  49. package/src/model/model-command.ts +40 -0
  50. package/src/permissions/__tests__/permissions-command-module.test.ts +164 -0
  51. package/src/permissions/index.ts +6 -0
  52. package/src/permissions/permissions-command-module.ts +56 -0
  53. package/src/permissions/permissions-command.ts +45 -0
  54. package/src/plugin/__tests__/plugin-command-module.test.ts +214 -0
  55. package/src/plugin/index.ts +7 -0
  56. package/src/plugin/plugin-command-module.ts +81 -0
  57. package/src/plugin/plugin-command.ts +230 -0
  58. package/src/provider/__tests__/provider-command-module.test.ts +488 -0
  59. package/src/provider/__tests__/provider-setup-flow.test.ts +43 -0
  60. package/src/provider/index.ts +30 -0
  61. package/src/provider/provider-command-execution.ts +150 -0
  62. package/src/provider/provider-command-module.ts +65 -0
  63. package/src/provider/provider-command-profile-lifecycle.ts +211 -0
  64. package/src/provider/provider-command-profile-operations.ts +198 -0
  65. package/src/provider/provider-command-profile.ts +109 -0
  66. package/src/provider/provider-command-setup.ts +104 -0
  67. package/src/provider/provider-setup-flow.ts +309 -0
  68. package/src/reset/__tests__/reset-command-module.test.ts +63 -0
  69. package/src/reset/index.ts +2 -0
  70. package/src/reset/reset-command-module.ts +49 -0
  71. package/src/reset/reset-command.ts +10 -0
  72. package/src/rewind/__tests__/rewind-command-module.test.ts +215 -0
  73. package/src/rewind/index.ts +2 -0
  74. package/src/rewind/rewind-command-module.ts +57 -0
  75. package/src/rewind/rewind-command.ts +184 -0
  76. package/src/session/__tests__/session-command-module.test.ts +339 -0
  77. package/src/session/index.ts +17 -0
  78. package/src/session/session-command-module.ts +168 -0
  79. package/src/session/session-command.ts +74 -0
  80. package/src/settings/index.ts +7 -0
  81. package/src/settings/settings-command-module.ts +50 -0
  82. package/src/skills/__tests__/skills-command-module.test.ts +157 -0
  83. package/src/skills/index.ts +6 -0
  84. package/src/skills/skills-command-module.ts +62 -0
  85. package/src/skills/skills-command.ts +110 -0
  86. package/src/statusline/__tests__/statusline-command-module.test.ts +95 -0
  87. package/src/statusline/index.ts +6 -0
  88. package/src/statusline/statusline-command-module.ts +56 -0
  89. package/src/statusline/statusline-command.ts +79 -0
  90. package/src/user-local/__tests__/user-local-command.test.ts +145 -0
  91. package/src/user-local/index.ts +13 -0
  92. package/src/user-local/user-local-command-constants.ts +5 -0
  93. package/src/user-local/user-local-command-module.ts +67 -0
  94. package/src/user-local/user-local-command.ts +205 -0
  95. package/src/user-local/user-local-memory-command.ts +147 -0
@@ -0,0 +1,504 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ CommandRegistry,
4
+ SystemCommandExecutor,
5
+ createSystemCommands,
6
+ } from '@robota-sdk/agent-framework';
7
+ import type { IAgentJobHostContext, ICommandHostContext } from '@robota-sdk/agent-framework';
8
+ import { createAgentCommandModule } from '../agent-command-module.js';
9
+
10
+ function createMockSession(overrides?: Record<string, unknown>): ICommandHostContext {
11
+ const session = {
12
+ listAgentDefinitions: vi.fn().mockReturnValue([
13
+ { name: 'general-purpose', description: 'General-purpose task execution agent.' },
14
+ { name: 'Plan', description: 'Read-only planning agent.' },
15
+ ]),
16
+ listAgentJobs: vi.fn().mockReturnValue([]),
17
+ readBackgroundTaskLog: vi.fn().mockResolvedValue({ taskId: 'agent_1', lines: [] }),
18
+ spawnAgentJob: vi.fn().mockResolvedValue({
19
+ id: 'agent_1',
20
+ type: 'Plan',
21
+ label: 'Plan',
22
+ parentSessionId: 'test-session-id',
23
+ status: 'running',
24
+ mode: 'background',
25
+ depth: 1,
26
+ cwd: '/workspace',
27
+ promptPreview: 'draft architecture',
28
+ updatedAt: '2026-05-01T00:00:00.000Z',
29
+ }),
30
+ waitAgentJob: vi.fn(),
31
+ createBackgroundJobGroup: vi.fn().mockReturnValue({
32
+ id: 'group_1',
33
+ parentSessionId: 'test-session-id',
34
+ waitPolicy: 'wait_all',
35
+ taskIds: ['agent_1', 'agent_2'],
36
+ status: 'running',
37
+ createdAt: '2026-05-01T00:00:00.000Z',
38
+ updatedAt: '2026-05-01T00:00:00.000Z',
39
+ results: [],
40
+ }),
41
+ waitBackgroundJobGroup: vi.fn().mockResolvedValue({
42
+ id: 'group_1',
43
+ parentSessionId: 'test-session-id',
44
+ waitPolicy: 'wait_all',
45
+ taskIds: ['agent_1', 'agent_2'],
46
+ status: 'completed',
47
+ createdAt: '2026-05-01T00:00:00.000Z',
48
+ updatedAt: '2026-05-01T00:00:02.000Z',
49
+ completedAt: '2026-05-01T00:00:02.000Z',
50
+ results: [
51
+ {
52
+ taskId: 'agent_1',
53
+ label: 'developer',
54
+ status: 'completed',
55
+ summary: 'developer summary',
56
+ },
57
+ {
58
+ taskId: 'agent_2',
59
+ label: 'designer',
60
+ status: 'completed',
61
+ summary: 'designer summary',
62
+ },
63
+ ],
64
+ }),
65
+ sendAgentJob: vi.fn(),
66
+ cancelAgentJob: vi.fn(),
67
+ closeAgentJob: vi.fn(),
68
+ ...overrides,
69
+ };
70
+ (session as Record<string, unknown>)['getAgentJobCapability'] = () =>
71
+ session as unknown as IAgentJobHostContext;
72
+ return session as unknown as ICommandHostContext;
73
+ }
74
+
75
+ describe('agent command module', () => {
76
+ it('contributes agent without changing SDK core commands', () => {
77
+ const coreExecutor = new SystemCommandExecutor(createSystemCommands());
78
+ expect(coreExecutor.hasCommand('agent')).toBe(false);
79
+ const coreModelCommands = coreExecutor
80
+ .listModelInvocableCommands()
81
+ .map((command) => command.name);
82
+
83
+ const module = createAgentCommandModule();
84
+ const executor = new SystemCommandExecutor([
85
+ ...createSystemCommands(),
86
+ ...(module.systemCommands ?? []),
87
+ ]);
88
+ const modelCommands = executor.listModelInvocableCommands().map((command) => command.name);
89
+
90
+ expect(executor.hasCommand('agent')).toBe(true);
91
+ expect(modelCommands).toEqual([...coreModelCommands, 'agent']);
92
+ expect(coreModelCommands).not.toContain('agent');
93
+ });
94
+
95
+ it('requests agent runtime wiring through the command module contract', () => {
96
+ const module = createAgentCommandModule();
97
+
98
+ expect(module.sessionRequirements).toEqual(['agent-runtime']);
99
+ expect(module.commandSources).toHaveLength(1);
100
+ expect(module.systemCommands).toHaveLength(1);
101
+ });
102
+
103
+ it('projects agent from its injected command source', () => {
104
+ const module = createAgentCommandModule();
105
+ const registry = new CommandRegistry();
106
+ registry.addModule(module);
107
+
108
+ const agent = registry
109
+ .getCapabilityDescriptors()
110
+ .find((descriptor) => descriptor.name === 'agent');
111
+
112
+ expect(agent).toMatchObject({
113
+ kind: 'builtin-command',
114
+ userInvocable: true,
115
+ modelInvocable: true,
116
+ safety: 'background-agent',
117
+ });
118
+ expect(agent?.description).toContain('Subagent jobs');
119
+ expect(agent?.description).toContain('parallel');
120
+ expect(agent?.description).toContain('consolidated');
121
+ expect(agent?.description).toContain('When the user explicitly asks');
122
+ expect(agent?.description).toContain('start the requested agent command immediately');
123
+ expect(agent?.description).toContain('do not ask a follow-up question');
124
+ expect(agent?.description).toContain('target selection inside the agent prompt');
125
+ expect(agent?.description).not.toContain('<agent>');
126
+ expect(agent?.description).not.toContain('XML/HTML');
127
+ expect(agent?.argumentHint).toContain('PROMPT');
128
+ expect(agent?.argumentHint).not.toContain('<');
129
+ });
130
+
131
+ it('lists agent jobs with preserved worktree handoff metadata', async () => {
132
+ const module = createAgentCommandModule();
133
+ const executor = new SystemCommandExecutor([
134
+ ...createSystemCommands(),
135
+ ...(module.systemCommands ?? []),
136
+ ]);
137
+ const session = createMockSession({
138
+ listAgentJobs: vi.fn().mockReturnValue([
139
+ {
140
+ id: 'agent_1',
141
+ type: 'general-purpose',
142
+ label: 'general-purpose',
143
+ parentSessionId: 'test-session-id',
144
+ status: 'completed',
145
+ mode: 'background',
146
+ depth: 1,
147
+ cwd: '/workspace',
148
+ promptPreview: 'change files',
149
+ updatedAt: '2026-05-01T00:00:00.000Z',
150
+ worktreePath: '/workspace/.robota/worktrees/agent_1',
151
+ branchName: 'robota/agent_1',
152
+ },
153
+ ]),
154
+ });
155
+
156
+ const result = await executor.execute('agent', session, 'list');
157
+
158
+ expect(result?.success).toBe(true);
159
+ expect(result?.message).toContain('worktree=/workspace/.robota/worktrees/agent_1');
160
+ expect(result?.message).toContain('branch=robota/agent_1');
161
+ });
162
+
163
+ it('spawns a background agent from direct natural-language /agent input', async () => {
164
+ const module = createAgentCommandModule();
165
+ const executor = new SystemCommandExecutor([
166
+ ...createSystemCommands(),
167
+ ...(module.systemCommands ?? []),
168
+ ]);
169
+ const session = createMockSession();
170
+
171
+ const result = await executor.execute('agent', session, 'analyze the selected task');
172
+
173
+ expect(result?.success).toBe(true);
174
+ expect(result?.data?.agentId).toBe('agent_1');
175
+ expect(
176
+ (session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> }).spawnAgentJob,
177
+ ).toHaveBeenCalledWith({
178
+ agentType: 'general-purpose',
179
+ label: 'general-purpose',
180
+ mode: 'background',
181
+ prompt: 'analyze the selected task',
182
+ });
183
+ expect(
184
+ (session as unknown as { waitAgentJob: ReturnType<typeof vi.fn> }).waitAgentJob,
185
+ ).not.toHaveBeenCalled();
186
+ });
187
+
188
+ it('uses plain placeholder names in usage errors', async () => {
189
+ const module = createAgentCommandModule();
190
+ const executor = new SystemCommandExecutor([
191
+ ...createSystemCommands(),
192
+ ...(module.systemCommands ?? []),
193
+ ]);
194
+ const session = createMockSession();
195
+
196
+ const runResult = await executor.execute('agent', session, 'run');
197
+ const parallelResult = await executor.execute('agent', session, 'parallel');
198
+
199
+ expect(runResult?.message).toContain('AGENT_NAME');
200
+ expect(parallelResult?.message).toContain('LABEL');
201
+ expect(runResult?.message).not.toContain('<');
202
+ expect(parallelResult?.message).not.toContain('<');
203
+ });
204
+
205
+ it('spawns a named background agent from direct /agent input', async () => {
206
+ const module = createAgentCommandModule();
207
+ const executor = new SystemCommandExecutor([
208
+ ...createSystemCommands(),
209
+ ...(module.systemCommands ?? []),
210
+ ]);
211
+ const session = createMockSession();
212
+
213
+ const result = await executor.execute('agent', session, 'Plan "draft architecture"');
214
+
215
+ expect(result?.success).toBe(true);
216
+ expect(result?.data?.agentId).toBe('agent_1');
217
+ expect(
218
+ (session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> }).spawnAgentJob,
219
+ ).toHaveBeenCalledWith({
220
+ agentType: 'Plan',
221
+ label: 'Plan',
222
+ mode: 'background',
223
+ prompt: 'draft architecture',
224
+ });
225
+ expect(
226
+ (session as unknown as { waitAgentJob: ReturnType<typeof vi.fn> }).waitAgentJob,
227
+ ).not.toHaveBeenCalled();
228
+ });
229
+
230
+ it('keeps /agent run as a compatibility alias that defaults to background general-purpose', async () => {
231
+ const module = createAgentCommandModule();
232
+ const executor = new SystemCommandExecutor([
233
+ ...createSystemCommands(),
234
+ ...(module.systemCommands ?? []),
235
+ ]);
236
+ const session = createMockSession();
237
+
238
+ const result = await executor.execute('agent', session, 'run "analyze this task"');
239
+
240
+ expect(result?.success).toBe(true);
241
+ expect(result?.data?.agentId).toBe('agent_1');
242
+ expect(
243
+ (session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> }).spawnAgentJob,
244
+ ).toHaveBeenCalledWith({
245
+ agentType: 'general-purpose',
246
+ label: 'general-purpose',
247
+ mode: 'background',
248
+ prompt: 'analyze this task',
249
+ });
250
+ });
251
+
252
+ it('does not treat the first natural-language token as an agent type for /agent run', async () => {
253
+ const module = createAgentCommandModule();
254
+ const executor = new SystemCommandExecutor([
255
+ ...createSystemCommands(),
256
+ ...(module.systemCommands ?? []),
257
+ ]);
258
+ const session = createMockSession();
259
+
260
+ const result = await executor.execute('agent', session, 'run analyze this task');
261
+
262
+ expect(result?.success).toBe(true);
263
+ expect(
264
+ (session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> }).spawnAgentJob,
265
+ ).toHaveBeenCalledWith({
266
+ agentType: 'general-purpose',
267
+ label: 'general-purpose',
268
+ mode: 'background',
269
+ prompt: 'analyze this task',
270
+ });
271
+ });
272
+
273
+ it('returns a command failure for explicit unknown agent types instead of throwing', async () => {
274
+ const module = createAgentCommandModule();
275
+ const executor = new SystemCommandExecutor([
276
+ ...createSystemCommands(),
277
+ ...(module.systemCommands ?? []),
278
+ ]);
279
+ const session = createMockSession();
280
+
281
+ const result = await executor.execute(
282
+ 'agent',
283
+ session,
284
+ 'run --agent missing --background "draft architecture"',
285
+ );
286
+
287
+ expect(result?.success).toBe(false);
288
+ expect(result?.message).toContain('Unknown agent type: missing');
289
+ expect(
290
+ (session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> }).spawnAgentJob,
291
+ ).not.toHaveBeenCalled();
292
+ });
293
+
294
+ it('spawns every parallel background job before any wait path', async () => {
295
+ const module = createAgentCommandModule();
296
+ const executor = new SystemCommandExecutor([
297
+ ...createSystemCommands(),
298
+ ...(module.systemCommands ?? []),
299
+ ]);
300
+ const callOrder: string[] = [];
301
+ const session = createMockSession({
302
+ spawnAgentJob: vi.fn().mockImplementation((request: { label: string }) => {
303
+ callOrder.push(`spawn:${request.label}`);
304
+ return Promise.resolve({
305
+ id: `agent_${callOrder.length}`,
306
+ type: request.label,
307
+ label: request.label,
308
+ parentSessionId: 'test-session-id',
309
+ status: 'running',
310
+ mode: 'background',
311
+ depth: 1,
312
+ cwd: '/workspace',
313
+ promptPreview: request.label,
314
+ updatedAt: '2026-05-01T00:00:00.000Z',
315
+ });
316
+ }),
317
+ waitAgentJob: vi.fn().mockImplementation((jobId: string) => {
318
+ callOrder.push(`wait:${jobId}`);
319
+ return Promise.resolve({ jobId, output: 'done' });
320
+ }),
321
+ waitBackgroundJobGroup: vi.fn().mockImplementation((groupId: string) => {
322
+ callOrder.push(`wait-group:${groupId}`);
323
+ return Promise.resolve({
324
+ id: groupId,
325
+ parentSessionId: 'test-session-id',
326
+ waitPolicy: 'wait_all',
327
+ taskIds: ['agent_1', 'agent_2'],
328
+ status: 'completed',
329
+ createdAt: '2026-05-01T00:00:00.000Z',
330
+ updatedAt: '2026-05-01T00:00:02.000Z',
331
+ completedAt: '2026-05-01T00:00:02.000Z',
332
+ results: [],
333
+ });
334
+ }),
335
+ });
336
+
337
+ const result = await executor.execute(
338
+ 'agent',
339
+ session,
340
+ 'parallel developer=general-purpose:"implementation risks" designer=Plan:"architecture boundaries"',
341
+ );
342
+
343
+ expect(result?.success).toBe(true);
344
+ expect(result?.data?.agentIds).toEqual(['agent_1', 'agent_2']);
345
+ expect(result?.data?.groupId).toBe('group_1');
346
+ expect(
347
+ (session as unknown as { createBackgroundJobGroup: ReturnType<typeof vi.fn> })
348
+ .createBackgroundJobGroup,
349
+ ).toHaveBeenCalledWith({
350
+ waitPolicy: 'wait_all',
351
+ taskIds: ['agent_1', 'agent_2'],
352
+ label: 'agent parallel',
353
+ });
354
+ expect(callOrder).toEqual(['spawn:developer', 'spawn:designer', 'wait-group:group_1']);
355
+ });
356
+
357
+ it('supports simple parallel label prompt syntax with default agent type', async () => {
358
+ const module = createAgentCommandModule();
359
+ const executor = new SystemCommandExecutor([
360
+ ...createSystemCommands(),
361
+ ...(module.systemCommands ?? []),
362
+ ]);
363
+ const session = createMockSession();
364
+
365
+ const result = await executor.execute(
366
+ 'agent',
367
+ session,
368
+ 'parallel developer:"implementation risks" designer:"architecture boundaries"',
369
+ );
370
+
371
+ expect(result?.success).toBe(true);
372
+ const spawnAgentJob = (session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> })
373
+ .spawnAgentJob;
374
+ expect(spawnAgentJob).toHaveBeenNthCalledWith(1, {
375
+ agentType: 'general-purpose',
376
+ label: 'developer',
377
+ mode: 'background',
378
+ prompt: 'implementation risks',
379
+ });
380
+ expect(spawnAgentJob).toHaveBeenNthCalledWith(2, {
381
+ agentType: 'general-purpose',
382
+ label: 'designer',
383
+ mode: 'background',
384
+ prompt: 'architecture boundaries',
385
+ });
386
+ });
387
+
388
+ it('waits for a parallel group when --wait is requested', async () => {
389
+ const module = createAgentCommandModule();
390
+ const executor = new SystemCommandExecutor([
391
+ ...createSystemCommands(),
392
+ ...(module.systemCommands ?? []),
393
+ ]);
394
+ const session = createMockSession();
395
+
396
+ const result = await executor.execute(
397
+ 'agent',
398
+ session,
399
+ 'parallel --wait developer:"implementation risks" designer=Plan:"architecture boundaries"',
400
+ );
401
+
402
+ expect(result?.success).toBe(true);
403
+ expect(result?.message).toContain('Background job group group_1: completed');
404
+ expect(result?.message).toContain('[completed] developer agent_1: developer summary');
405
+ expect(
406
+ (session as unknown as { waitBackgroundJobGroup: ReturnType<typeof vi.fn> })
407
+ .waitBackgroundJobGroup,
408
+ ).toHaveBeenCalledWith('group_1');
409
+ });
410
+
411
+ it('waits for a parallel group by default so model-routed calls produce a consolidated result', async () => {
412
+ const module = createAgentCommandModule();
413
+ const executor = new SystemCommandExecutor([
414
+ ...createSystemCommands(),
415
+ ...(module.systemCommands ?? []),
416
+ ]);
417
+ const session = createMockSession();
418
+
419
+ const result = await executor.execute(
420
+ 'agent',
421
+ session,
422
+ 'parallel developer:"implementation risks" designer=Plan:"architecture boundaries"',
423
+ );
424
+
425
+ expect(result?.success).toBe(true);
426
+ expect(result?.message).toContain('Background job group group_1: completed');
427
+ expect(result?.message).toContain('[completed] developer agent_1: developer summary');
428
+ expect(
429
+ (session as unknown as { waitBackgroundJobGroup: ReturnType<typeof vi.fn> })
430
+ .waitBackgroundJobGroup,
431
+ ).toHaveBeenCalledWith('group_1');
432
+ });
433
+
434
+ it('detaches a parallel group only when --detach is explicit', async () => {
435
+ const module = createAgentCommandModule();
436
+ const executor = new SystemCommandExecutor([
437
+ ...createSystemCommands(),
438
+ ...(module.systemCommands ?? []),
439
+ ]);
440
+ const session = createMockSession();
441
+
442
+ const result = await executor.execute(
443
+ 'agent',
444
+ session,
445
+ 'parallel --detach developer:"implementation risks" designer=Plan:"architecture boundaries"',
446
+ );
447
+
448
+ expect(result?.success).toBe(true);
449
+ expect(result?.message).toContain('Started agent jobs:');
450
+ expect(result?.data?.groupId).toBe('group_1');
451
+ expect(
452
+ (session as unknown as { waitBackgroundJobGroup: ReturnType<typeof vi.fn> })
453
+ .waitBackgroundJobGroup,
454
+ ).not.toHaveBeenCalled();
455
+ });
456
+
457
+ it('waits for an existing agent group by id', async () => {
458
+ const module = createAgentCommandModule();
459
+ const executor = new SystemCommandExecutor([
460
+ ...createSystemCommands(),
461
+ ...(module.systemCommands ?? []),
462
+ ]);
463
+ const session = createMockSession();
464
+
465
+ const result = await executor.execute('agent', session, 'wait group_1');
466
+
467
+ expect(result?.success).toBe(true);
468
+ expect(result?.data?.groupId).toBe('group_1');
469
+ expect(result?.message).toContain('[completed] designer agent_2: designer summary');
470
+ expect(
471
+ (session as unknown as { waitBackgroundJobGroup: ReturnType<typeof vi.fn> })
472
+ .waitBackgroundJobGroup,
473
+ ).toHaveBeenCalledWith('group_1');
474
+ });
475
+
476
+ it('opens agent switcher when called with no args', async () => {
477
+ const module = createAgentCommandModule();
478
+ const executor = new SystemCommandExecutor([
479
+ ...createSystemCommands(),
480
+ ...(module.systemCommands ?? []),
481
+ ]);
482
+ const session = createMockSession();
483
+
484
+ const result = await executor.execute('agent', session, '');
485
+
486
+ expect(result?.success).toBe(true);
487
+ expect(result?.effects).toEqual([{ type: 'agent-switcher-requested' }]);
488
+ });
489
+
490
+ it('shows text list when agent list subcommand is used explicitly', async () => {
491
+ const module = createAgentCommandModule();
492
+ const executor = new SystemCommandExecutor([
493
+ ...createSystemCommands(),
494
+ ...(module.systemCommands ?? []),
495
+ ]);
496
+ const session = createMockSession();
497
+
498
+ const result = await executor.execute('agent', session, 'list');
499
+
500
+ expect(result?.success).toBe(true);
501
+ expect(result?.message).toContain('Available agents:');
502
+ expect(result?.effects).toBeUndefined();
503
+ });
504
+ });
@@ -0,0 +1,82 @@
1
+ import type {
2
+ IAgentJobHostContext,
3
+ ICommand,
4
+ ICommandHostContext,
5
+ ICommandModule,
6
+ ICommandSource,
7
+ ISystemCommand,
8
+ } from '@robota-sdk/agent-framework';
9
+ import { executeAgentCommand } from './agent-command.js';
10
+
11
+ function getAgentHostContext(context: ICommandHostContext): IAgentJobHostContext {
12
+ const cap = context.getAgentJobCapability?.();
13
+ if (!cap) throw new Error('Agent job capability is not available in this context.');
14
+ return cap;
15
+ }
16
+
17
+ function createAgentSubcommands(): ICommand[] {
18
+ return [
19
+ { name: 'list', description: 'List available agents and active jobs', source: 'agent' },
20
+ { name: 'run', description: 'Start one background agent job', source: 'agent' },
21
+ { name: 'parallel', description: 'Run multiple agents in parallel', source: 'agent' },
22
+ { name: 'wait', description: 'Wait for a background agent group summary', source: 'agent' },
23
+ { name: 'read', description: 'Read an agent job log page', source: 'agent' },
24
+ { name: 'send', description: 'Send follow-up input to an agent job', source: 'agent' },
25
+ { name: 'stop', description: 'Cancel a running agent job', source: 'agent' },
26
+ { name: 'close', description: 'Dismiss a terminal agent job', source: 'agent' },
27
+ { name: 'open', description: 'Focus an agent job detail view when supported', source: 'agent' },
28
+ ];
29
+ }
30
+
31
+ export function createAgentCommandEntry(): ICommand {
32
+ return {
33
+ name: 'agent',
34
+ displayName: 'Agent Jobs',
35
+ description: [
36
+ 'Subagent jobs command.',
37
+ 'Natural-language arguments start one background agent job.',
38
+ 'When the user explicitly asks to create, run, spawn, delegate to, or use agents/subagents, start the requested agent command immediately and do not ask a follow-up question unless execution is impossible or unsafe.',
39
+ 'If the target item is unspecified, include target selection inside the agent prompt instead of delaying execution.',
40
+ 'The parallel form starts multiple background agent jobs as a wait_all group and returns a consolidated group summary unless --detach is present.',
41
+ 'list, wait, read, send, stop, close, and open manage existing agent jobs.',
42
+ ].join(' '),
43
+ source: 'agent',
44
+ modelInvocable: true,
45
+ argumentHint:
46
+ 'PROMPT | AGENT_NAME PROMPT | list | parallel [--wait|--detach] LABEL:"PROMPT" [LABEL=AGENT_NAME:"PROMPT"] | wait GROUP_ID | read AGENT_ID [OFFSET] | send AGENT_ID PROMPT | stop AGENT_ID | close AGENT_ID',
47
+ safety: 'background-agent',
48
+ subcommands: createAgentSubcommands(),
49
+ };
50
+ }
51
+
52
+ export function createAgentSystemCommand(): ISystemCommand {
53
+ const entry = createAgentCommandEntry();
54
+ return {
55
+ name: entry.name,
56
+ ...(entry.displayName !== undefined ? { displayName: entry.displayName } : {}),
57
+ description: entry.description,
58
+ requiresPermission: false,
59
+ execute: (context, args) => executeAgentCommand(getAgentHostContext(context), args),
60
+ ...(entry.modelInvocable !== undefined ? { modelInvocable: entry.modelInvocable } : {}),
61
+ ...(entry.userInvocable !== undefined ? { userInvocable: entry.userInvocable } : {}),
62
+ ...(entry.argumentHint !== undefined ? { argumentHint: entry.argumentHint } : {}),
63
+ ...(entry.safety !== undefined ? { safety: entry.safety } : {}),
64
+ };
65
+ }
66
+
67
+ export class AgentCommandSource implements ICommandSource {
68
+ readonly name = 'agent';
69
+
70
+ getCommands(): ICommand[] {
71
+ return [createAgentCommandEntry()];
72
+ }
73
+ }
74
+
75
+ export function createAgentCommandModule(): ICommandModule {
76
+ return {
77
+ name: 'agent-command-agent',
78
+ commandSources: [new AgentCommandSource()],
79
+ systemCommands: [createAgentSystemCommand()],
80
+ sessionRequirements: ['agent-runtime'],
81
+ };
82
+ }