@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.
- package/README.md +11 -2
- package/dist/node/index.cjs +76 -38
- package/dist/node/index.d.cts +946 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +564 -51
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +76 -38
- package/dist/node/index.js.map +1 -1
- package/package.json +34 -16
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +39 -8
- package/src/agent/agent-command-module.ts +14 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +3 -3
- package/src/background/__tests__/background-command-module.test.ts +59 -52
- package/src/background/background-command-module.ts +2 -1
- package/src/background/background-command.ts +3 -3
- package/src/command-module-utils.ts +33 -0
- package/src/compact/__tests__/compact-command-module.test.ts +31 -23
- package/src/compact/compact-command-module.ts +10 -1
- package/src/compact/compact-command.ts +6 -3
- package/src/context/__tests__/context-command-module.test.ts +214 -63
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +45 -14
- package/src/context/context-command.ts +34 -257
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
- package/src/default/__tests__/default-command-modules.test.ts +59 -7
- package/src/default/__tests__/model-exposure.test.ts +241 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +130 -18
- package/src/default/index.ts +4 -1
- package/src/devices/__tests__/devices-command-module.test.ts +212 -0
- package/src/devices/devices-command-module.ts +224 -0
- package/src/devices/devices-command-port.ts +82 -0
- package/src/devices/index.ts +11 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +145 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +299 -0
- package/src/doctor/doctor-node-deps.ts +89 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
- package/src/editor/editor-command-module.ts +6 -5
- package/src/editor/editor-command.ts +10 -5
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +140 -0
- package/src/effort/effort-command.ts +122 -0
- package/src/effort/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +16 -9
- package/src/exit/exit-command-module.ts +2 -1
- package/src/exit/exit-command.ts +6 -6
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +28 -0
- package/src/git/__tests__/git-command-module.test.ts +103 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/__tests__/git-diff.test.ts +125 -0
- package/src/git/__tests__/git-process.test.ts +221 -0
- package/src/git/__tests__/git-status.test.ts +117 -0
- package/src/git/git-command-module.ts +139 -0
- package/src/git/git-commit.ts +251 -0
- package/src/git/git-diff.ts +129 -0
- package/src/git/git-process.ts +169 -0
- package/src/git/git-status.ts +134 -0
- package/src/git/index.ts +17 -0
- package/src/goal/__tests__/goal-command.test.ts +9 -4
- package/src/goal/goal-command-module.ts +2 -1
- package/src/goal/goal-command.ts +6 -9
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +48 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +63 -40
- package/src/help/__tests__/help-command.test.ts +46 -37
- package/src/help/help-command-module.ts +2 -1
- package/src/help/help-command.ts +3 -3
- package/src/index.ts +124 -2
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +74 -0
- package/src/language/__tests__/language-command-module.test.ts +33 -28
- package/src/language/language-command-module.ts +2 -1
- package/src/language/language-command.ts +8 -6
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
- package/src/mcp-activation/mcp-activation-command.ts +448 -0
- package/src/mcp-activation/mcp-model-notice.ts +85 -0
- package/src/mcp-activation/mcp-model-status.ts +99 -0
- package/src/memory/__tests__/memory-command-module.test.ts +167 -74
- package/src/memory/memory-command-module.ts +18 -1
- package/src/memory/memory-command.ts +38 -38
- package/src/mode/__tests__/mode-command-module.test.ts +51 -38
- package/src/mode/mode-command-module.ts +2 -1
- package/src/mode/mode-command.ts +15 -7
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command.test.ts +241 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +47 -0
- package/src/peers/peers-command.ts +142 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
- package/src/permissions/permissions-command-module.ts +2 -1
- package/src/permissions/permissions-command.ts +46 -4
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/plugin-command-module.ts +3 -1
- package/src/plugin/plugin-command.ts +17 -16
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +102 -56
- package/src/preset/preset-command-module.ts +26 -8
- package/src/preset/preset-command.ts +85 -27
- package/src/provider/__tests__/org-policy.test.ts +4 -5
- package/src/provider/__tests__/provider-command-module.test.ts +11 -14
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +7 -5
- package/src/provider/provider-command-execution.ts +4 -4
- package/src/provider/provider-command-module.ts +2 -1
- package/src/provider/provider-command-profile-lifecycle.ts +4 -4
- package/src/provider/provider-command-profile-operations.ts +3 -3
- package/src/provider/provider-command-profile.ts +1 -1
- package/src/provider/provider-command-setup.ts +4 -4
- package/src/provider/provider-startup.ts +62 -35
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +5 -5
- package/src/reset/reset-command-module.ts +2 -1
- package/src/reset/reset-command.ts +3 -4
- package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
- package/src/rewind/rewind-command-module.ts +2 -1
- package/src/rewind/rewind-command.ts +73 -6
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
- package/src/schedule/__tests__/loop-command.test.ts +360 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +143 -3
- package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/loop-command.ts +333 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +92 -14
- package/src/schedule/schedule-command.ts +98 -9
- package/src/schedule/schedule-spec-parser.ts +5 -1
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +210 -45
- package/src/session/session-command-module.ts +68 -37
- package/src/session/session-command.ts +147 -80
- package/src/settings/settings-command-module.ts +3 -2
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
- package/src/shell/resolve-shell.ts +2 -2
- package/src/shell/shell-command-module.ts +6 -5
- package/src/shell/shell-command.ts +9 -5
- package/src/skills/__tests__/skills-command-module.test.ts +69 -28
- package/src/skills/index.ts +1 -0
- package/src/skills/skills-command-module.ts +22 -5
- package/src/skills/skills-command.ts +14 -13
- package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
- package/src/statusline/statusline-command-module.ts +2 -1
- package/src/statusline/statusline-command.ts +4 -4
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +211 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -110
- package/src/user-local/user-local-command-constants.ts +1 -1
- package/src/user-local/user-local-command-module.ts +6 -5
- package/src/user-local/user-local-command.ts +12 -6
- package/src/user-local/user-local-memory-command.ts +17 -7
- package/src/plugins/default-plugin-command-adapter.ts +0 -164
- 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.
|
|
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":
|
|
14
|
-
|
|
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":
|
|
18
|
-
|
|
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.
|
|
28
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
29
|
-
"@robota-sdk/agent-interface-
|
|
30
|
-
"@robota-sdk/agent-
|
|
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.
|
|
36
|
-
"typescript": "^
|
|
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": "
|
|
46
|
-
"build:types": "
|
|
47
|
-
"typecheck": "
|
|
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
|
+
});
|
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
SystemCommandExecutor,
|
|
5
5
|
createSystemCommands,
|
|
6
6
|
} from '@robota-sdk/agent-framework';
|
|
7
|
-
import type { IAgentJobHostContext
|
|
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>)
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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('
|
|
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?.
|
|
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?.
|
|
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
|
-
|
|
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-
|
|
9
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
10
10
|
|
|
11
|
-
function getAgentHostContext(context:
|
|
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: '
|
|
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
|
-
|
|
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
|
|
|
@@ -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-
|
|
8
|
-
import type { ISubagentJobState } from '@robota-sdk/agent-interface-
|
|
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: '',
|
|
44
|
+
return { message: '', uiIntents: [{ type: 'show-agent-switcher' }], success: true };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
async function executeList(session: IAgentJobHostContext): Promise<ICommandResult> {
|