@robota-sdk/agent-command 3.0.0-beta.79 → 3.0.0-beta.81

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 (200) hide show
  1. package/README.md +11 -2
  2. package/dist/node/index.cjs +76 -38
  3. package/dist/node/index.d.cts +946 -0
  4. package/dist/node/index.d.cts.map +1 -0
  5. package/dist/node/index.d.ts +564 -51
  6. package/dist/node/index.d.ts.map +1 -1
  7. package/dist/node/index.js +76 -38
  8. package/dist/node/index.js.map +1 -1
  9. package/package.json +34 -16
  10. package/src/advisor/advisor-command-module.ts +47 -0
  11. package/src/advisor/advisor-command.test.ts +102 -0
  12. package/src/advisor/advisor-command.ts +55 -0
  13. package/src/advisor/advisor-cost-functional.test.ts +92 -0
  14. package/src/advisor/index.ts +6 -0
  15. package/src/agent/__tests__/agent-command.test.ts +39 -8
  16. package/src/agent/agent-command-module.ts +14 -7
  17. package/src/agent/agent-command-parser.ts +1 -1
  18. package/src/agent/agent-command.ts +3 -3
  19. package/src/background/__tests__/background-command-module.test.ts +59 -52
  20. package/src/background/background-command-module.ts +2 -1
  21. package/src/background/background-command.ts +3 -3
  22. package/src/command-module-utils.ts +33 -0
  23. package/src/compact/__tests__/compact-command-module.test.ts +31 -23
  24. package/src/compact/compact-command-module.ts +10 -1
  25. package/src/compact/compact-command.ts +6 -3
  26. package/src/context/__tests__/context-command-module.test.ts +214 -63
  27. package/src/context/auto-compact-format.ts +25 -0
  28. package/src/context/context-breakdown.ts +295 -0
  29. package/src/context/context-command-module.ts +45 -14
  30. package/src/context/context-command.ts +34 -257
  31. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
  32. package/src/default/__tests__/default-command-modules.test.ts +59 -7
  33. package/src/default/__tests__/model-exposure.test.ts +241 -0
  34. package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
  35. package/src/default/default-command-modules.ts +130 -18
  36. package/src/default/index.ts +4 -1
  37. package/src/devices/__tests__/devices-command-module.test.ts +212 -0
  38. package/src/devices/devices-command-module.ts +224 -0
  39. package/src/devices/devices-command-port.ts +82 -0
  40. package/src/devices/index.ts +11 -0
  41. package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
  42. package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
  43. package/src/doctor/__tests__/doctor-fixture.ts +145 -0
  44. package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
  45. package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
  46. package/src/doctor/doctor-command-module.ts +132 -0
  47. package/src/doctor/doctor-extensions-probe.ts +299 -0
  48. package/src/doctor/doctor-node-deps.ts +89 -0
  49. package/src/doctor/doctor-redaction.ts +99 -0
  50. package/src/doctor/doctor-render.ts +70 -0
  51. package/src/doctor/doctor-repair.ts +136 -0
  52. package/src/doctor/doctor-runner.ts +122 -0
  53. package/src/doctor/doctor-settings-probe.ts +239 -0
  54. package/src/doctor/doctor-storage-probe.ts +187 -0
  55. package/src/doctor/doctor-types.ts +109 -0
  56. package/src/doctor/index.ts +32 -0
  57. package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
  58. package/src/editor/editor-command-module.ts +6 -5
  59. package/src/editor/editor-command.ts +10 -5
  60. package/src/effort/effort-command-module.ts +51 -0
  61. package/src/effort/effort-command.test.ts +140 -0
  62. package/src/effort/effort-command.ts +122 -0
  63. package/src/effort/index.ts +6 -0
  64. package/src/exit/__tests__/exit-command-module.test.ts +16 -9
  65. package/src/exit/exit-command-module.ts +2 -1
  66. package/src/exit/exit-command.ts +6 -6
  67. package/src/fork/__tests__/fork-command.test.ts +184 -0
  68. package/src/fork/fork-command-module.ts +61 -0
  69. package/src/fork/fork-command.ts +162 -0
  70. package/src/fork/index.ts +8 -0
  71. package/src/git/__tests__/fake-git-port.ts +28 -0
  72. package/src/git/__tests__/git-command-module.test.ts +103 -0
  73. package/src/git/__tests__/git-commit.test.ts +185 -0
  74. package/src/git/__tests__/git-diff.test.ts +125 -0
  75. package/src/git/__tests__/git-process.test.ts +221 -0
  76. package/src/git/__tests__/git-status.test.ts +117 -0
  77. package/src/git/git-command-module.ts +139 -0
  78. package/src/git/git-commit.ts +251 -0
  79. package/src/git/git-diff.ts +129 -0
  80. package/src/git/git-process.ts +169 -0
  81. package/src/git/git-status.ts +134 -0
  82. package/src/git/index.ts +17 -0
  83. package/src/goal/__tests__/goal-command.test.ts +9 -4
  84. package/src/goal/goal-command-module.ts +2 -1
  85. package/src/goal/goal-command.ts +6 -9
  86. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  87. package/src/handoff/handoff-command-module.ts +48 -0
  88. package/src/handoff/handoff-command.ts +187 -0
  89. package/src/handoff/index.ts +6 -0
  90. package/src/help/__tests__/help-command-module.test.ts +63 -40
  91. package/src/help/__tests__/help-command.test.ts +46 -37
  92. package/src/help/help-command-module.ts +2 -1
  93. package/src/help/help-command.ts +3 -3
  94. package/src/index.ts +124 -2
  95. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  96. package/src/keybindings/index.ts +6 -0
  97. package/src/keybindings/keybindings-command-module.ts +74 -0
  98. package/src/language/__tests__/language-command-module.test.ts +33 -28
  99. package/src/language/language-command-module.ts +2 -1
  100. package/src/language/language-command.ts +8 -6
  101. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
  102. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
  103. package/src/mcp-activation/index.ts +12 -0
  104. package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
  105. package/src/mcp-activation/mcp-activation-command.ts +448 -0
  106. package/src/mcp-activation/mcp-model-notice.ts +85 -0
  107. package/src/mcp-activation/mcp-model-status.ts +99 -0
  108. package/src/memory/__tests__/memory-command-module.test.ts +167 -74
  109. package/src/memory/memory-command-module.ts +18 -1
  110. package/src/memory/memory-command.ts +38 -38
  111. package/src/mode/__tests__/mode-command-module.test.ts +51 -38
  112. package/src/mode/mode-command-module.ts +2 -1
  113. package/src/mode/mode-command.ts +15 -7
  114. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  115. package/src/output-style/index.ts +6 -0
  116. package/src/output-style/output-style-command-module.ts +143 -0
  117. package/src/peers/__tests__/peers-command.test.ts +241 -0
  118. package/src/peers/index.ts +9 -0
  119. package/src/peers/peers-command-module.ts +47 -0
  120. package/src/peers/peers-command.ts +142 -0
  121. package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
  122. package/src/permissions/permissions-command-module.ts +2 -1
  123. package/src/permissions/permissions-command.ts +46 -4
  124. package/src/plan/__tests__/plan-command.test.ts +129 -0
  125. package/src/plan/index.ts +9 -0
  126. package/src/plan/plan-command-module.ts +49 -0
  127. package/src/plan/plan-command.ts +92 -0
  128. package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
  129. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  130. package/src/plugin/plugin-command-module.ts +3 -1
  131. package/src/plugin/plugin-command.ts +17 -16
  132. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  133. package/src/preset/__tests__/preset-command-module.test.ts +102 -56
  134. package/src/preset/preset-command-module.ts +26 -8
  135. package/src/preset/preset-command.ts +85 -27
  136. package/src/provider/__tests__/org-policy.test.ts +4 -5
  137. package/src/provider/__tests__/provider-command-module.test.ts +11 -14
  138. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  139. package/src/provider/__tests__/scripted-interaction.ts +7 -5
  140. package/src/provider/provider-command-execution.ts +4 -4
  141. package/src/provider/provider-command-module.ts +2 -1
  142. package/src/provider/provider-command-profile-lifecycle.ts +4 -4
  143. package/src/provider/provider-command-profile-operations.ts +3 -3
  144. package/src/provider/provider-command-profile.ts +1 -1
  145. package/src/provider/provider-command-setup.ts +4 -4
  146. package/src/provider/provider-startup.ts +62 -35
  147. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  148. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  149. package/src/remote-control/index.ts +8 -0
  150. package/src/remote-control/remote-control-command-module.ts +61 -0
  151. package/src/remote-control/remote-control-command.ts +138 -0
  152. package/src/reset/__tests__/reset-command-module.test.ts +5 -5
  153. package/src/reset/reset-command-module.ts +2 -1
  154. package/src/reset/reset-command.ts +3 -4
  155. package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
  156. package/src/rewind/rewind-command-module.ts +2 -1
  157. package/src/rewind/rewind-command.ts +73 -6
  158. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  159. package/src/sandbox/index.ts +2 -0
  160. package/src/sandbox/sandbox-command-module.ts +56 -0
  161. package/src/sandbox/sandbox-command.ts +94 -0
  162. package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
  163. package/src/schedule/__tests__/loop-command.test.ts +360 -0
  164. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  165. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  166. package/src/schedule/__tests__/schedule-command.test.ts +143 -3
  167. package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
  168. package/src/schedule/index.ts +3 -0
  169. package/src/schedule/loop-command.ts +333 -0
  170. package/src/schedule/loop-jitter.ts +41 -0
  171. package/src/schedule/schedule-command-module.ts +92 -14
  172. package/src/schedule/schedule-command.ts +98 -9
  173. package/src/schedule/schedule-spec-parser.ts +5 -1
  174. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  175. package/src/session/__tests__/session-command-module.test.ts +210 -45
  176. package/src/session/session-command-module.ts +68 -37
  177. package/src/session/session-command.ts +147 -80
  178. package/src/settings/settings-command-module.ts +3 -2
  179. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  180. package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
  181. package/src/shell/resolve-shell.ts +2 -2
  182. package/src/shell/shell-command-module.ts +6 -5
  183. package/src/shell/shell-command.ts +9 -5
  184. package/src/skills/__tests__/skills-command-module.test.ts +69 -28
  185. package/src/skills/index.ts +1 -0
  186. package/src/skills/skills-command-module.ts +22 -5
  187. package/src/skills/skills-command.ts +14 -13
  188. package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
  189. package/src/statusline/statusline-command-module.ts +2 -1
  190. package/src/statusline/statusline-command.ts +4 -4
  191. package/src/theme/__tests__/theme-command.test.ts +204 -0
  192. package/src/theme/index.ts +12 -0
  193. package/src/theme/theme-command-module.ts +211 -0
  194. package/src/user-local/__tests__/user-local-command.test.ts +145 -110
  195. package/src/user-local/user-local-command-constants.ts +1 -1
  196. package/src/user-local/user-local-command-module.ts +6 -5
  197. package/src/user-local/user-local-command.ts +12 -6
  198. package/src/user-local/user-local-memory-command.ts +17 -7
  199. package/src/plugins/default-plugin-command-adapter.ts +0 -164
  200. package/src/plugins/default-plugin-command-source-loader.ts +0 -31
package/package.json CHANGED
@@ -1,21 +1,32 @@
1
1
  {
2
2
  "name": "@robota-sdk/agent-command",
3
- "version": "3.0.0-beta.79",
3
+ "version": "3.0.0-beta.81",
4
4
  "description": "Consolidated command module implementations for Robota SDK CLI",
5
5
  "type": "module",
6
6
  "main": "dist/node/index.js",
7
7
  "types": "dist/node/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/node/index.d.ts",
11
10
  "source": "./src/index.ts",
12
11
  "node": {
13
- "import": "./dist/node/index.js",
14
- "require": "./dist/node/index.cjs"
12
+ "import": {
13
+ "types": "./dist/node/index.d.ts",
14
+ "default": "./dist/node/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/node/index.d.cts",
18
+ "default": "./dist/node/index.cjs"
19
+ }
15
20
  },
16
21
  "default": {
17
- "import": "./dist/node/index.js",
18
- "require": "./dist/node/index.cjs"
22
+ "import": {
23
+ "types": "./dist/node/index.d.ts",
24
+ "default": "./dist/node/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/node/index.d.cts",
28
+ "default": "./dist/node/index.cjs"
29
+ }
19
30
  }
20
31
  }
21
32
  },
@@ -24,29 +35,36 @@
24
35
  "src"
25
36
  ],
26
37
  "dependencies": {
27
- "@robota-sdk/agent-core": "3.0.0-beta.79",
28
- "@robota-sdk/agent-framework": "3.0.0-beta.79",
29
- "@robota-sdk/agent-interface-transport": "3.0.0-beta.79",
30
- "@robota-sdk/agent-preset": "3.0.0-beta.79"
38
+ "@robota-sdk/agent-core": "3.0.0-beta.81",
39
+ "@robota-sdk/agent-framework": "3.0.0-beta.81",
40
+ "@robota-sdk/agent-interface-execution": "3.0.0-beta.81",
41
+ "@robota-sdk/agent-interface-command": "3.0.0-beta.81",
42
+ "@robota-sdk/agent-preset": "3.0.0-beta.81",
43
+ "@robota-sdk/agent-interface-session": "3.0.0-beta.81"
31
44
  },
32
45
  "devDependencies": {
33
46
  "@types/node": "^22.19.21",
34
47
  "rimraf": "^5.0.10",
35
- "tsdown": "^0.22.2",
36
- "typescript": "^5.9.3",
48
+ "tsdown": "^0.22.14",
49
+ "typescript": "^6.0.3",
37
50
  "vitest": "^3.2.6"
38
51
  },
39
52
  "license": "AGPL-3.0-only OR LicenseRef-Commercial",
40
53
  "publishConfig": {
41
54
  "access": "public"
42
55
  },
56
+ "volta": {
57
+ "extends": "../../package.json"
58
+ },
43
59
  "scripts": {
44
- "build": "tsdown",
45
- "build:js": "tsdown --no-dts",
46
- "build:types": "tsdown --dts",
47
- "typecheck": "tsc --noEmit",
60
+ "build": "rimraf dist && tsdown",
61
+ "build:js": "pnpm run build",
62
+ "build:types": "pnpm run build",
63
+ "typecheck": "tsgo --noEmit && tsgo -p tsconfig.examples.json --noEmit",
48
64
  "test": "vitest run",
49
65
  "test:coverage": "vitest run --coverage",
66
+ "scenario:verify": "pnpm scenario:verify:semantic-command-roles",
67
+ "scenario:verify:semantic-command-roles": "pnpm exec tsx --conditions=source examples/verify-semantic-command-roles.ts",
50
68
  "clean": "rimraf dist"
51
69
  }
52
70
  }
@@ -0,0 +1,47 @@
1
+ import { executeAdvisorCommand } from './advisor-command.js';
2
+
3
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
4
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
5
+
6
+ export function createAdvisorCommandEntry(): ICommand {
7
+ return {
8
+ name: 'advisor',
9
+ displayName: 'Advisor',
10
+ description: 'Show, set, or turn off the model the main model can consult for advice',
11
+ source: 'advisor',
12
+ argumentHint: '<profile> | <profile>:<model> | off',
13
+ // User-only: choosing a second model changes cost and what sees the conversation; the user decides.
14
+ modelInvocable: false,
15
+ };
16
+ }
17
+
18
+ function createAdvisorSystemCommand(): ISystemCommand {
19
+ const entry = createAdvisorCommandEntry();
20
+ return {
21
+ name: entry.name,
22
+ displayName: entry.displayName,
23
+ description: entry.description,
24
+ requiresPermission: false,
25
+ userInvocable: true,
26
+ modelInvocable: false,
27
+ argumentHint: entry.argumentHint,
28
+ lifecycle: 'inline',
29
+ execute: executeAdvisorCommand,
30
+ };
31
+ }
32
+
33
+ export class AdvisorCommandSource implements ICommandSource {
34
+ readonly name = 'advisor';
35
+
36
+ getCommands(): ICommand[] {
37
+ return [createAdvisorCommandEntry()];
38
+ }
39
+ }
40
+
41
+ export function createAdvisorCommandModule(): ICommandModule {
42
+ return {
43
+ name: 'agent-command-advisor',
44
+ commandSources: [new AdvisorCommandSource()],
45
+ systemCommands: [createAdvisorSystemCommand()],
46
+ };
47
+ }
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { AdvisorController, createAdvisorTool } from '@robota-sdk/agent-framework';
4
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
5
+
6
+ import { executeAdvisorCommand } from './advisor-command.js';
7
+
8
+ import type { IAIProvider } from '@robota-sdk/agent-core';
9
+ import type { ICommandSettingsDocument } from '@robota-sdk/agent-framework';
10
+
11
+ function advisorController(options: {
12
+ registered: boolean;
13
+ allowed?: string[];
14
+ }): AdvisorController {
15
+ return new AdvisorController({
16
+ ...(options.registered ? { spec: { profile: 'strong' } } : {}),
17
+ resolveTarget: (spec) => {
18
+ if (spec.profile === 'missing') throw new Error('Provider profile "missing" not found.');
19
+ return {
20
+ provider: {} as IAIProvider,
21
+ model: `${spec.profile}-model`,
22
+ destination: 'vendor-a@default',
23
+ };
24
+ },
25
+ consent: { has: () => true, grant: () => undefined },
26
+ ...(options.allowed ? { allowedProfiles: options.allowed } : {}),
27
+ });
28
+ }
29
+
30
+ function host(advisor: AdvisorController | undefined, initial: ICommandSettingsDocument = {}) {
31
+ let document: ICommandSettingsDocument = { ...initial };
32
+ const write = vi.fn((next: ICommandSettingsDocument) => {
33
+ document = next;
34
+ });
35
+ const context = createTestCommandHost({
36
+ overrides: {
37
+ getCommandHostAdapters: () => ({
38
+ ...(advisor ? { advisor } : {}),
39
+ settings: { read: () => document, write },
40
+ }),
41
+ },
42
+ });
43
+ return { context, write, read: () => document };
44
+ }
45
+
46
+ describe('/advisor', () => {
47
+ it('saves the default and retargets the live advisor without changing the tool', async () => {
48
+ const advisor = advisorController({ registered: true });
49
+ const schema = JSON.stringify(createAdvisorTool(advisor).schema);
50
+ const { context, read } = host(advisor, { theme: 'dark' });
51
+
52
+ const result = await executeAdvisorCommand(context, 'other:big');
53
+
54
+ expect(result.success).toBe(true);
55
+ expect(read()).toEqual({ theme: 'dark', advisorModel: 'other:big' });
56
+ expect(advisor.status()).toMatchObject({ target: 'other:big', enabled: true });
57
+ expect(JSON.stringify(createAdvisorTool(advisor).schema)).toBe(schema);
58
+ });
59
+
60
+ it('turns the advisor off and removes the saved default', async () => {
61
+ const advisor = advisorController({ registered: true });
62
+ const { context, read } = host(advisor, { advisorModel: 'strong', theme: 'dark' });
63
+
64
+ const result = await executeAdvisorCommand(context, 'off');
65
+
66
+ expect(result.success).toBe(true);
67
+ expect(read()).toEqual({ theme: 'dark' });
68
+ expect(advisor.status().enabled).toBe(false);
69
+ });
70
+
71
+ it('says a new advisor takes effect next session when this one started without the tool', async () => {
72
+ const { context } = host(advisorController({ registered: false }));
73
+ const result = await executeAdvisorCommand(context, 'strong');
74
+ expect(result.message).toContain('next session');
75
+ });
76
+
77
+ it('refuses a profile outside the organization allowlist and saves nothing', async () => {
78
+ const { context, write } = host(advisorController({ registered: true, allowed: ['strong'] }));
79
+ const result = await executeAdvisorCommand(context, 'elsewhere');
80
+ expect(result.success).toBe(false);
81
+ expect(result.message).toContain('organization policy');
82
+ expect(write).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('refuses a profile that does not exist and saves nothing', async () => {
86
+ const { context, write } = host(advisorController({ registered: true }));
87
+ const result = await executeAdvisorCommand(context, 'missing');
88
+ expect(result.success).toBe(false);
89
+ expect(write).not.toHaveBeenCalled();
90
+ });
91
+
92
+ it('reports the current advisor', async () => {
93
+ const { context } = host(advisorController({ registered: true }));
94
+ const result = await executeAdvisorCommand(context, '');
95
+ expect(result.message).toContain('Advisor: strong');
96
+ });
97
+
98
+ it('fails explicitly on a host without an advisor', async () => {
99
+ const { context } = host(undefined);
100
+ expect((await executeAdvisorCommand(context, '')).success).toBe(false);
101
+ });
102
+ });
@@ -0,0 +1,55 @@
1
+ import type {
2
+ IAdvisorStatus,
3
+ ICommandHostAdapterAccess,
4
+ ICommandHostWorkspace,
5
+ } from '@robota-sdk/agent-framework';
6
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
7
+
8
+ /** The user-settings key `/advisor` saves the default advisor under. */
9
+ export const ADVISOR_SETTING_KEY = 'advisorModel';
10
+
11
+ function describeStatus(status: IAdvisorStatus): string {
12
+ if (status.killSwitch) return 'Advisor: disabled by the environment kill switch.';
13
+ if (status.target === undefined || !status.enabled) {
14
+ return 'Advisor: off. Set one with /advisor <profile> or /advisor <profile>:<model>.';
15
+ }
16
+ const availability = status.registered
17
+ ? `${status.sessionCalls}/${status.maxCallsPerSession} calls used this session`
18
+ : 'available from the next session';
19
+ return `Advisor: ${status.target} (${availability}).`;
20
+ }
21
+
22
+ /**
23
+ * `/advisor` — show the advisor, set it (`<profile>` or `<profile>:<model>`), or turn it off. A
24
+ * change the user makes is also saved as the default for later sessions.
25
+ */
26
+ export async function executeAdvisorCommand(
27
+ context: ICommandHostAdapterAccess & ICommandHostWorkspace,
28
+ args: string,
29
+ ): Promise<ICommandResult> {
30
+ const adapters = context.getCommandHostAdapters?.() ?? {};
31
+ const advisor = adapters.advisor;
32
+ if (advisor === undefined) {
33
+ return { success: false, message: 'The advisor is not available in this environment.' };
34
+ }
35
+ const value = args.trim();
36
+ if (value.length === 0) {
37
+ const status = advisor.status();
38
+ return { success: true, message: describeStatus(status), data: { advisor: { ...status } } };
39
+ }
40
+ const result = advisor.set(value);
41
+ if (!result.success || result.saved === undefined) {
42
+ return { success: result.success, message: result.message };
43
+ }
44
+ const settings = adapters.settings;
45
+ if (settings !== undefined && context.getCommandInvocationSource() === 'user') {
46
+ const current = settings.read();
47
+ if (result.saved === 'off') {
48
+ const { [ADVISOR_SETTING_KEY]: _removed, ...rest } = current;
49
+ settings.write(rest);
50
+ } else {
51
+ settings.write({ ...current, [ADVISOR_SETTING_KEY]: result.saved });
52
+ }
53
+ }
54
+ return { success: true, message: result.message, data: { advisor: { ...advisor.status() } } };
55
+ }
@@ -0,0 +1,92 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { calculateModelCost } from '@robota-sdk/agent-core';
4
+ import { AdvisorController, createAdvisorTool } from '@robota-sdk/agent-framework';
5
+ import { scriptedSession, type ScriptedSessionHarness } from '@robota-sdk/agent-framework/testing';
6
+
7
+ import { createSessionCommandModule } from '../session/session-command-module.js';
8
+
9
+ import type { IAIProvider } from '@robota-sdk/agent-core';
10
+
11
+ const ADVISOR_MODEL = 'claude-opus-4-6';
12
+ const MAIN_MODEL = 'claude-haiku-4-5';
13
+
14
+ function advisorProvider(): IAIProvider {
15
+ return {
16
+ name: 'advisor-vendor',
17
+ version: 'test',
18
+ chat: vi.fn(async () => ({
19
+ id: 'advice',
20
+ role: 'assistant' as const,
21
+ content: 'Run the tests before declaring done.',
22
+ state: 'complete' as const,
23
+ timestamp: new Date(),
24
+ metadata: { inputTokens: 3_000, outputTokens: 400 },
25
+ })),
26
+ } as unknown as IAIProvider;
27
+ }
28
+
29
+ describe('advisor usage in a real session', () => {
30
+ let harness: ScriptedSessionHarness | undefined;
31
+
32
+ afterEach(async () => {
33
+ await harness?.dispose();
34
+ harness = undefined;
35
+ });
36
+
37
+ it('/cost totals the turn and the advisor call, pricing the advisor on its model, and both persist', async () => {
38
+ const controller = new AdvisorController({
39
+ spec: { profile: 'strong' },
40
+ resolveTarget: () => ({
41
+ provider: advisorProvider(),
42
+ model: ADVISOR_MODEL,
43
+ destination: 'advisor-vendor@api.test',
44
+ }),
45
+ consent: { has: () => true, grant: () => undefined },
46
+ });
47
+ harness = scriptedSession({
48
+ turns: [
49
+ {
50
+ toolCalls: [{ name: 'Advisor', args: { question: 'done?' } }],
51
+ usage: { inputTokens: 1_000, outputTokens: 100 },
52
+ },
53
+ { text: 'All done.', usage: { inputTokens: 1_200, outputTokens: 50 } },
54
+ ],
55
+ model: MAIN_MODEL,
56
+ persistence: true,
57
+ additionalTools: [createAdvisorTool(controller)],
58
+ commandModules: [createSessionCommandModule()],
59
+ });
60
+
61
+ await harness.submit('finish the task');
62
+ const result = await harness.command('cost');
63
+ const data = result?.data as Record<string, number | string[] | undefined>;
64
+
65
+ const advisorCost = calculateModelCost(ADVISOR_MODEL, 3_000, 400)!;
66
+ const persisted = harness.sessionRecord()?.history ?? [];
67
+ const summaries = persisted
68
+ .filter((entry) => entry.type === 'usage-summary')
69
+ .map(
70
+ (entry) =>
71
+ entry.data as { promptTokens: number; completionTokens: number; source?: unknown },
72
+ );
73
+ const turn = summaries.find((summary) => summary.source === undefined);
74
+ expect(turn).toBeDefined();
75
+ expect(summaries).toHaveLength(2);
76
+ expect(data.inputTokens).toBe(turn!.promptTokens + 3_000);
77
+ expect(data.outputTokens).toBe(turn!.completionTokens + 400);
78
+ expect(data.estimatedCostUsd as number).toBeGreaterThan(advisorCost);
79
+ expect(result?.message).toContain(`Advisor (${ADVISOR_MODEL})`);
80
+ expect(result?.message).toContain('mixed');
81
+
82
+ const advisorObservation = persisted.find(
83
+ (entry) =>
84
+ entry.type === 'usage-observation' &&
85
+ (entry.data as { modelId?: string }).modelId === ADVISOR_MODEL,
86
+ );
87
+ expect(advisorObservation?.data).toMatchObject({
88
+ providerId: 'advisor-vendor',
89
+ usage: { promptTokens: 3_000, completionTokens: 400, costUsd: advisorCost },
90
+ });
91
+ });
92
+ });
@@ -0,0 +1,6 @@
1
+ export {
2
+ AdvisorCommandSource,
3
+ createAdvisorCommandEntry,
4
+ createAdvisorCommandModule,
5
+ } from './advisor-command-module.js';
6
+ export { ADVISOR_SETTING_KEY, executeAdvisorCommand } from './advisor-command.js';
@@ -4,10 +4,12 @@ import {
4
4
  SystemCommandExecutor,
5
5
  createSystemCommands,
6
6
  } from '@robota-sdk/agent-framework';
7
- import type { IAgentJobHostContext, ICommandHostContext } from '@robota-sdk/agent-framework';
7
+ import type { IAgentJobHostContext } from '@robota-sdk/agent-framework';
8
8
  import { createAgentCommandModule } from '../agent-command-module.js';
9
+ import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
10
+ import { createTestAgentJobHost } from '@robota-sdk/agent-framework/testing';
9
11
 
10
- function createMockSession(overrides?: Record<string, unknown>): ICommandHostContext {
12
+ function createMockSession(overrides?: Record<string, unknown>) {
11
13
  const session = {
12
14
  listAgentDefinitions: vi.fn().mockReturnValue([
13
15
  { name: 'general-purpose', description: 'General-purpose task execution agent.' },
@@ -67,9 +69,15 @@ function createMockSession(overrides?: Record<string, unknown>): ICommandHostCon
67
69
  closeAgentJob: vi.fn(),
68
70
  ...overrides,
69
71
  };
70
- (session as Record<string, unknown>)['getAgentJobCapability'] = () =>
71
- session as unknown as IAgentJobHostContext;
72
- return session as unknown as ICommandHostContext;
72
+ // ARCH-029: the literal this replaced injected `getAgentJobCapability` at RUNTIME through a
73
+ // `Record<string, unknown>` cast, then asserted the result satisfied two contracts. Statically the
74
+ // object did not have that member at all. It is now declared, and checked.
75
+ return createTestCommandHost({
76
+ overrides: {
77
+ ...session,
78
+ getAgentJobCapability: () => createTestAgentJobHost(session),
79
+ },
80
+ });
73
81
  }
74
82
 
75
83
  describe('agent command module', () => {
@@ -81,6 +89,7 @@ describe('agent command module', () => {
81
89
  .map((command) => command.name);
82
90
 
83
91
  const module = createAgentCommandModule();
92
+ expect(module.systemCommands?.[0]?.semanticRole).toBe('subagentSpawn');
84
93
  const executor = new SystemCommandExecutor([
85
94
  ...createSystemCommands(),
86
95
  ...(module.systemCommands ?? []),
@@ -100,6 +109,28 @@ describe('agent command module', () => {
100
109
  expect(module.systemCommands).toHaveLength(1);
101
110
  });
102
111
 
112
+ it('keeps the palette agent actions on the executable command', () => {
113
+ const module = createAgentCommandModule();
114
+ const palette = module.commandSources?.[0]?.getCommands()[0];
115
+ const executable = module.systemCommands?.[0];
116
+
117
+ expect(palette?.subcommands?.map(({ name }) => name)).toEqual([
118
+ 'list',
119
+ 'run',
120
+ 'parallel',
121
+ 'wait',
122
+ 'read',
123
+ 'send',
124
+ 'stop',
125
+ 'close',
126
+ 'open',
127
+ ]);
128
+ expect(executable?.subcommands).toEqual(palette?.subcommands);
129
+ expect(palette?.subcommands?.find(({ name }) => name === 'open')?.description).toBe(
130
+ 'Read an agent job log page (alias for read)',
131
+ );
132
+ });
133
+
103
134
  it('projects agent from its injected command source', () => {
104
135
  const module = createAgentCommandModule();
105
136
  const registry = new CommandRegistry();
@@ -115,7 +146,7 @@ describe('agent command module', () => {
115
146
  modelInvocable: true,
116
147
  safety: 'background-agent',
117
148
  });
118
- expect(agent?.description).toContain('Subagent jobs');
149
+ expect(agent?.description).toContain('background subagent jobs');
119
150
  expect(agent?.description).toContain('parallel');
120
151
  expect(agent?.description).toContain('consolidated');
121
152
  expect(agent?.description).toContain('When the user explicitly asks');
@@ -484,7 +515,7 @@ describe('agent command module', () => {
484
515
  const result = await executor.execute('agent', session, '');
485
516
 
486
517
  expect(result?.success).toBe(true);
487
- expect(result?.effects).toEqual([{ type: 'agent-switcher-requested' }]);
518
+ expect(result?.uiIntents).toEqual([{ type: 'show-agent-switcher' }]);
488
519
  });
489
520
 
490
521
  it('shows text list when agent list subcommand is used explicitly', async () => {
@@ -499,6 +530,6 @@ describe('agent command module', () => {
499
530
 
500
531
  expect(result?.success).toBe(true);
501
532
  expect(result?.message).toContain('Available agents:');
502
- expect(result?.effects).toBeUndefined();
533
+ expect(result?.uiIntents).toBeUndefined();
503
534
  });
504
535
  });
@@ -2,14 +2,14 @@ import { executeAgentCommand } from './agent-command.js';
2
2
 
3
3
  import type {
4
4
  IAgentJobHostContext,
5
- ICommandHostContext,
5
+ ICommandHostAgentJobs,
6
6
  ICommandModule,
7
7
  ISystemCommand,
8
8
  } from '@robota-sdk/agent-framework';
9
- import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-transport';
9
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
10
10
 
11
- function getAgentHostContext(context: ICommandHostContext): IAgentJobHostContext {
12
- const cap = context.getAgentJobCapability?.();
11
+ function getAgentHostContext(context: ICommandHostAgentJobs): IAgentJobHostContext {
12
+ const cap = context.getAgentJobCapability();
13
13
  if (!cap) throw new Error('Agent job capability is not available in this context.');
14
14
  return cap;
15
15
  }
@@ -24,7 +24,7 @@ function createAgentSubcommands(): ICommand[] {
24
24
  { name: 'send', description: 'Send follow-up input to an agent job', source: 'agent' },
25
25
  { name: 'stop', description: 'Cancel a running agent job', source: 'agent' },
26
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' },
27
+ { name: 'open', description: 'Read an agent job log page (alias for read)', source: 'agent' },
28
28
  ];
29
29
  }
30
30
 
@@ -32,13 +32,17 @@ export function createAgentCommandEntry(): ICommand {
32
32
  return {
33
33
  name: 'agent',
34
34
  displayName: 'Agent Jobs',
35
- description: [
36
- 'Subagent jobs command.',
35
+ description: 'Start and manage background subagent jobs',
36
+ // Model-invocable: delegating work to subagents is the model's own decision, and each job runs
37
+ // under the session's permissions, so starting one widens nothing.
38
+ modelDescription: [
39
+ 'Start and manage background subagent jobs.',
37
40
  'Natural-language arguments start one background agent job.',
38
41
  '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
42
  'If the target item is unspecified, include target selection inside the agent prompt instead of delaying execution.',
40
43
  'The parallel form starts multiple background agent jobs as a wait_all group and returns a consolidated group summary unless --detach is present.',
41
44
  'list, wait, read, send, stop, close, and open manage existing agent jobs.',
45
+ 'Returns the started job id (or group summary), the job list, or the requested log page.',
42
46
  ].join(' '),
43
47
  source: 'agent',
44
48
  modelInvocable: true,
@@ -53,14 +57,17 @@ export function createAgentSystemCommand(): ISystemCommand {
53
57
  const entry = createAgentCommandEntry();
54
58
  return {
55
59
  name: entry.name,
60
+ semanticRole: 'subagentSpawn',
56
61
  ...(entry.displayName !== undefined ? { displayName: entry.displayName } : {}),
57
62
  description: entry.description,
58
63
  requiresPermission: false,
59
64
  execute: (context, args) => executeAgentCommand(getAgentHostContext(context), args),
60
65
  ...(entry.modelInvocable !== undefined ? { modelInvocable: entry.modelInvocable } : {}),
66
+ ...(entry.modelDescription !== undefined ? { modelDescription: entry.modelDescription } : {}),
61
67
  ...(entry.userInvocable !== undefined ? { userInvocable: entry.userInvocable } : {}),
62
68
  ...(entry.argumentHint !== undefined ? { argumentHint: entry.argumentHint } : {}),
63
69
  ...(entry.safety !== undefined ? { safety: entry.safety } : {}),
70
+ ...(entry.subcommands !== undefined ? { subcommands: entry.subcommands } : {}),
64
71
  };
65
72
  }
66
73
 
@@ -1,4 +1,4 @@
1
- import type { TBackgroundTaskIsolation } from '@robota-sdk/agent-interface-transport';
1
+ import type { TBackgroundTaskIsolation } from '@robota-sdk/agent-interface-execution';
2
2
 
3
3
  const DEFAULT_AGENT_TYPE = 'general-purpose';
4
4
 
@@ -4,8 +4,8 @@ import { parseParallelRequests, parseRunRequest, tokenizeArgs } from './agent-co
4
4
 
5
5
  import type { IAgentRunRequest } from './agent-command-parser.js';
6
6
  import type { IAgentJobHostContext } from '@robota-sdk/agent-framework';
7
- import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
8
- import type { ISubagentJobState } from '@robota-sdk/agent-interface-transport';
7
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
8
+ import type { ISubagentJobState } from '@robota-sdk/agent-interface-execution';
9
9
 
10
10
  function formatError<TError>(error: TError): string {
11
11
  return error instanceof Error ? error.message : String(error);
@@ -41,7 +41,7 @@ async function spawnAgentJob(
41
41
  }
42
42
 
43
43
  function executeOpenSwitcher(): ICommandResult {
44
- return { message: '', effects: [{ type: 'agent-switcher-requested' }], success: true };
44
+ return { message: '', uiIntents: [{ type: 'show-agent-switcher' }], success: true };
45
45
  }
46
46
 
47
47
  async function executeList(session: IAgentJobHostContext): Promise<ICommandResult> {