@robota-sdk/agent-command 3.0.0-beta.74 → 3.0.0-beta.76
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/dist/node/index.cjs +34 -33
- package/dist/node/index.d.ts +25 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +34 -33
- package/dist/node/index.js.map +1 -1
- package/package.json +5 -4
- package/src/context/context-command.ts +4 -4
- package/src/default/__tests__/default-command-modules.test.ts +97 -0
- package/src/default/default-command-modules.ts +40 -1
- package/src/index.ts +1 -0
- package/src/preset/__tests__/preset-command-module.test.ts +138 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/preset-command-module.ts +79 -0
- package/src/preset/preset-command.ts +61 -0
- package/src/session/model-pricing.ts +7 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-command",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.76",
|
|
4
4
|
"description": "Consolidated command module implementations for Robota SDK CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
"src"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@robota-sdk/agent-core": "3.0.0-beta.
|
|
28
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
29
|
-
"@robota-sdk/agent-interface-transport": "3.0.0-beta.
|
|
27
|
+
"@robota-sdk/agent-core": "3.0.0-beta.76",
|
|
28
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.76",
|
|
29
|
+
"@robota-sdk/agent-interface-transport": "3.0.0-beta.76",
|
|
30
|
+
"@robota-sdk/agent-preset": "3.0.0-beta.76"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^22.19.21",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CONTEXT_ESTIMATE_CHARS_PER_TOKEN } from '@robota-sdk/agent-core';
|
|
1
2
|
import {
|
|
2
3
|
addCommandContextReference,
|
|
3
4
|
clearCommandContextReferences,
|
|
@@ -282,12 +283,11 @@ function formatContextReferenceSummary(references: readonly IContextReferenceIte
|
|
|
282
283
|
return `References: ${active} active, ${observed} observed`;
|
|
283
284
|
}
|
|
284
285
|
|
|
285
|
-
// 1 token ≈ 4 chars —
|
|
286
|
-
const CHARS_PER_TOKEN = 4;
|
|
286
|
+
// 1 token ≈ 4 chars — sourced from the agent-core estimation SSOT.
|
|
287
287
|
const TOOL_ARG_MAX_LEN = 60;
|
|
288
288
|
|
|
289
289
|
function estimateTokens(charLength: number): number {
|
|
290
|
-
return Math.ceil(charLength /
|
|
290
|
+
return Math.ceil(charLength / CONTEXT_ESTIMATE_CHARS_PER_TOKEN);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
function formatContextReferenceLine(reference: IContextReferenceItem): string {
|
|
@@ -349,7 +349,7 @@ function computeMessageTokensByRole(rawMessages: TUniversalMessage[]): IMessageT
|
|
|
349
349
|
let toolCallCount = 0;
|
|
350
350
|
|
|
351
351
|
for (const msg of rawMessages) {
|
|
352
|
-
const t = Math.ceil(JSON.stringify(msg).length /
|
|
352
|
+
const t = Math.ceil(JSON.stringify(msg).length / CONTEXT_ESTIMATE_CHARS_PER_TOKEN);
|
|
353
353
|
if (msg.role === 'system') {
|
|
354
354
|
systemTokens += t;
|
|
355
355
|
} else if (msg.role === 'user') {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
4
|
+
import type {
|
|
5
|
+
IProviderCommandSettingsAdapter,
|
|
6
|
+
TProviderSettingsDocument,
|
|
7
|
+
} from '@robota-sdk/agent-framework';
|
|
8
|
+
|
|
9
|
+
import { createDefaultCommandModules } from '../default-command-modules.js';
|
|
10
|
+
|
|
11
|
+
const providerDefinitions: readonly IProviderDefinition[] = [
|
|
12
|
+
{
|
|
13
|
+
type: 'anthropic',
|
|
14
|
+
defaults: { model: 'claude-sonnet-4-6', apiKey: '$ENV:ANTHROPIC_API_KEY' },
|
|
15
|
+
setupSteps: [{ key: 'apiKey', title: 'anthropic API key', masked: true }],
|
|
16
|
+
requiresApiKey: true,
|
|
17
|
+
createProvider: () => {
|
|
18
|
+
throw new Error('not used');
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const providerSettingsAdapter: IProviderCommandSettingsAdapter = {
|
|
24
|
+
readMergedSettings: () => ({}) as TProviderSettingsDocument,
|
|
25
|
+
readTargetSettings: () => ({}) as TProviderSettingsDocument,
|
|
26
|
+
writeTargetSettings: () => undefined,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const baseOptions = { cwd: '/tmp', providerDefinitions, providerSettingsAdapter } as const;
|
|
30
|
+
|
|
31
|
+
function moduleNames(opts: Parameters<typeof createDefaultCommandModules>[0]): string[] {
|
|
32
|
+
return createDefaultCommandModules(opts).map((module) => module.name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('createDefaultCommandModules — PRESET-004 module-selection delta', () => {
|
|
36
|
+
// Module `name`s are the stable ICommandModule ids (the `agent-command-*` form).
|
|
37
|
+
const HELP = 'agent-command-help';
|
|
38
|
+
const AGENT = 'agent-command-agent';
|
|
39
|
+
const BACKGROUND = 'agent-command-background';
|
|
40
|
+
|
|
41
|
+
it('TC-04: neither enabled nor disabled given → full default set unchanged (no-regression)', () => {
|
|
42
|
+
const names = moduleNames(baseOptions);
|
|
43
|
+
// No-regression: the default set length is the documented 21 modules.
|
|
44
|
+
expect(names).toHaveLength(21);
|
|
45
|
+
expect(names).toEqual([
|
|
46
|
+
'agent-command-skills',
|
|
47
|
+
'agent-command-help',
|
|
48
|
+
'agent-command-agent',
|
|
49
|
+
'agent-command-permissions',
|
|
50
|
+
'agent-command-mode',
|
|
51
|
+
'agent-command-preset',
|
|
52
|
+
'agent-command-language',
|
|
53
|
+
'agent-command-background',
|
|
54
|
+
'agent-command-memory',
|
|
55
|
+
'agent-command-user-local',
|
|
56
|
+
'agent-command-compact',
|
|
57
|
+
'agent-command-context',
|
|
58
|
+
'agent-command-exit',
|
|
59
|
+
'agent-command-session',
|
|
60
|
+
'agent-command-reset',
|
|
61
|
+
'agent-command-rewind',
|
|
62
|
+
'agent-command-schedule',
|
|
63
|
+
'agent-command-statusline',
|
|
64
|
+
'agent-command-plugin',
|
|
65
|
+
'agent-command-settings',
|
|
66
|
+
'agent-command-provider',
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('TC-01: enabledCommandModules whitelist keeps exactly the listed module names', () => {
|
|
71
|
+
const names = moduleNames({ ...baseOptions, enabledCommandModules: [HELP, AGENT] });
|
|
72
|
+
expect(new Set(names)).toEqual(new Set([HELP, AGENT]));
|
|
73
|
+
expect(names).toHaveLength(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('TC-02: disabledCommandModules blacklist removes the named module', () => {
|
|
77
|
+
const full = moduleNames(baseOptions);
|
|
78
|
+
const names = moduleNames({ ...baseOptions, disabledCommandModules: [BACKGROUND] });
|
|
79
|
+
expect(names).not.toContain(BACKGROUND);
|
|
80
|
+
expect(names).toHaveLength(full.length - 1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('TC-03: a name in both enabled and disabled is excluded (deny > allow)', () => {
|
|
84
|
+
const names = moduleNames({
|
|
85
|
+
...baseOptions,
|
|
86
|
+
enabledCommandModules: [HELP, AGENT],
|
|
87
|
+
disabledCommandModules: [AGENT],
|
|
88
|
+
});
|
|
89
|
+
expect(new Set(names)).toEqual(new Set([HELP]));
|
|
90
|
+
expect(names).not.toContain(AGENT);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('whitelist with an unknown name simply yields no module for it', () => {
|
|
94
|
+
const names = moduleNames({ ...baseOptions, enabledCommandModules: [HELP, 'does-not-exist'] });
|
|
95
|
+
expect(names).toEqual([HELP]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -9,6 +9,7 @@ import { createMemoryCommandModule } from '../memory/index.js';
|
|
|
9
9
|
import { createModeCommandModule } from '../mode/index.js';
|
|
10
10
|
import { createPermissionsCommandModule } from '../permissions/index.js';
|
|
11
11
|
import { createPluginCommandModule } from '../plugin/index.js';
|
|
12
|
+
import { createPresetCommandModule } from '../preset/index.js';
|
|
12
13
|
import { createProviderCommandModule } from '../provider/index.js';
|
|
13
14
|
import { createResetCommandModule } from '../reset/index.js';
|
|
14
15
|
import { createRewindCommandModule } from '../rewind/index.js';
|
|
@@ -26,19 +27,56 @@ export interface IDefaultCommandModulesOptions {
|
|
|
26
27
|
cwd: string;
|
|
27
28
|
providerDefinitions: readonly IProviderDefinition[];
|
|
28
29
|
providerSettingsAdapter: IProviderCommandSettingsAdapter;
|
|
30
|
+
/**
|
|
31
|
+
* Whitelist of module `name`s to keep. When provided, only modules whose `name`
|
|
32
|
+
* appears here survive. Omitted → all modules kept (no-regression).
|
|
33
|
+
*/
|
|
34
|
+
enabledCommandModules?: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* Blacklist of module `name`s to remove. Applied after the whitelist, so a name
|
|
37
|
+
* present in both is removed (deny > allow). Omitted → no modules removed.
|
|
38
|
+
*/
|
|
39
|
+
disabledCommandModules?: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Apply the preset module-selection delta to the default module set.
|
|
44
|
+
*
|
|
45
|
+
* Rules: if `enabled` is provided, keep only modules whose `name` is in it; then
|
|
46
|
+
* remove any module whose `name` is in `disabled` (deny > allow). Neither given →
|
|
47
|
+
* the full default set is returned unchanged (no-regression).
|
|
48
|
+
*/
|
|
49
|
+
function applyModuleSelection(
|
|
50
|
+
modules: readonly ICommandModule[],
|
|
51
|
+
enabled: readonly string[] | undefined,
|
|
52
|
+
disabled: readonly string[] | undefined,
|
|
53
|
+
): readonly ICommandModule[] {
|
|
54
|
+
let selected = modules;
|
|
55
|
+
if (enabled !== undefined) {
|
|
56
|
+
const allow = new Set(enabled);
|
|
57
|
+
selected = selected.filter((module) => allow.has(module.name));
|
|
58
|
+
}
|
|
59
|
+
if (disabled !== undefined) {
|
|
60
|
+
const deny = new Set(disabled);
|
|
61
|
+
selected = selected.filter((module) => !deny.has(module.name));
|
|
62
|
+
}
|
|
63
|
+
return selected;
|
|
29
64
|
}
|
|
30
65
|
|
|
31
66
|
export function createDefaultCommandModules({
|
|
32
67
|
cwd,
|
|
33
68
|
providerDefinitions,
|
|
34
69
|
providerSettingsAdapter,
|
|
70
|
+
enabledCommandModules,
|
|
71
|
+
disabledCommandModules,
|
|
35
72
|
}: IDefaultCommandModulesOptions): readonly ICommandModule[] {
|
|
36
|
-
|
|
73
|
+
const modules: readonly ICommandModule[] = [
|
|
37
74
|
createSkillsCommandModule({ cwd }),
|
|
38
75
|
createHelpCommandModule(),
|
|
39
76
|
createAgentCommandModule(),
|
|
40
77
|
createPermissionsCommandModule(),
|
|
41
78
|
createModeCommandModule(),
|
|
79
|
+
createPresetCommandModule(),
|
|
42
80
|
createLanguageCommandModule(),
|
|
43
81
|
createBackgroundCommandModule(),
|
|
44
82
|
createMemoryCommandModule(),
|
|
@@ -58,4 +96,5 @@ export function createDefaultCommandModules({
|
|
|
58
96
|
settings: providerSettingsAdapter,
|
|
59
97
|
}),
|
|
60
98
|
];
|
|
99
|
+
return applyModuleSelection(modules, enabledCommandModules, disabledCommandModules);
|
|
61
100
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './memory/index.js';
|
|
|
10
10
|
export * from './mode/index.js';
|
|
11
11
|
export * from './permissions/index.js';
|
|
12
12
|
export * from './plugin/index.js';
|
|
13
|
+
export * from './preset/index.js';
|
|
13
14
|
export * from './provider/index.js';
|
|
14
15
|
export * from './reset/index.js';
|
|
15
16
|
export * from './rewind/index.js';
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { listPresets } from '@robota-sdk/agent-preset';
|
|
3
|
+
import type { ICommandHostContext, ICommandSessionRuntime } from '@robota-sdk/agent-framework';
|
|
4
|
+
import { createPresetCommandModule, executePresetCommand } from '../index.js';
|
|
5
|
+
|
|
6
|
+
function createSessionRuntime(overrides?: Partial<ICommandSessionRuntime>): ICommandSessionRuntime {
|
|
7
|
+
return {
|
|
8
|
+
clearHistory: () => undefined,
|
|
9
|
+
compact: async () => undefined,
|
|
10
|
+
getContextState: () => ({
|
|
11
|
+
maxTokens: 100,
|
|
12
|
+
usedTokens: 10,
|
|
13
|
+
usedPercentage: 10,
|
|
14
|
+
remainingPercentage: 90,
|
|
15
|
+
}),
|
|
16
|
+
getPermissionMode: () => 'default',
|
|
17
|
+
setPermissionMode: () => undefined,
|
|
18
|
+
getSessionId: () => 'session_1',
|
|
19
|
+
getMessageCount: () => 0,
|
|
20
|
+
getSessionAllowedTools: () => [],
|
|
21
|
+
getAutoCompactThreshold: () => false,
|
|
22
|
+
getFullHistory: () => [],
|
|
23
|
+
getHistory: () => [],
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createCommandHostContext(
|
|
29
|
+
runtime: ICommandSessionRuntime,
|
|
30
|
+
overrides?: Partial<ICommandHostContext>,
|
|
31
|
+
): ICommandHostContext {
|
|
32
|
+
return {
|
|
33
|
+
getSession: () => runtime,
|
|
34
|
+
getContextState: () => ({
|
|
35
|
+
maxTokens: 100,
|
|
36
|
+
usedTokens: 10,
|
|
37
|
+
usedPercentage: 10,
|
|
38
|
+
remainingPercentage: 90,
|
|
39
|
+
}),
|
|
40
|
+
getAutoCompactThreshold: () => 0.8,
|
|
41
|
+
compactContext: async () => undefined,
|
|
42
|
+
getCwd: () => '/workspace',
|
|
43
|
+
listEditCheckpoints: () => [],
|
|
44
|
+
restoreEditCheckpoint: async () => ({
|
|
45
|
+
target: {
|
|
46
|
+
id: 'checkpoint_1',
|
|
47
|
+
sessionId: 'session_1',
|
|
48
|
+
sequence: 1,
|
|
49
|
+
prompt: 'edit',
|
|
50
|
+
createdAt: '2026-05-03T00:00:00.000Z',
|
|
51
|
+
fileCount: 0,
|
|
52
|
+
},
|
|
53
|
+
restoredCheckpointCount: 1,
|
|
54
|
+
restoredFileCount: 0,
|
|
55
|
+
removedCheckpointCount: 0,
|
|
56
|
+
}),
|
|
57
|
+
rollbackEditCheckpoint: async () => ({
|
|
58
|
+
target: {
|
|
59
|
+
id: 'checkpoint_1',
|
|
60
|
+
sessionId: 'session_1',
|
|
61
|
+
sequence: 1,
|
|
62
|
+
prompt: 'edit',
|
|
63
|
+
createdAt: '2026-05-03T00:00:00.000Z',
|
|
64
|
+
fileCount: 0,
|
|
65
|
+
},
|
|
66
|
+
restoredCheckpointCount: 1,
|
|
67
|
+
restoredFileCount: 0,
|
|
68
|
+
removedCheckpointCount: 0,
|
|
69
|
+
}),
|
|
70
|
+
getUsedMemoryReferences: () => [],
|
|
71
|
+
recordMemoryEvent: () => undefined,
|
|
72
|
+
listBackgroundTasks: () => [],
|
|
73
|
+
readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
|
|
74
|
+
cancelBackgroundTask: async () => undefined,
|
|
75
|
+
closeBackgroundTask: async () => undefined,
|
|
76
|
+
...overrides,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('preset command module', () => {
|
|
81
|
+
it('exposes a single preset system command (structure)', async () => {
|
|
82
|
+
const module = createPresetCommandModule();
|
|
83
|
+
expect(module.systemCommands?.map((command) => command.name)).toContain('preset');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('TC-01: lists every preset and marks the active one', async () => {
|
|
87
|
+
const runtime = createSessionRuntime({ getActivePresetId: () => 'autonomous-builder' });
|
|
88
|
+
const context = createCommandHostContext(runtime);
|
|
89
|
+
|
|
90
|
+
const result = await executePresetCommand(context, '');
|
|
91
|
+
|
|
92
|
+
expect(result.success).toBe(true);
|
|
93
|
+
for (const preset of listPresets()) {
|
|
94
|
+
expect(result.message).toContain(preset.id);
|
|
95
|
+
}
|
|
96
|
+
// The active preset is marked with the `* ` prefix.
|
|
97
|
+
expect(result.message).toContain('* autonomous-builder');
|
|
98
|
+
expect(result.message).not.toContain('* default');
|
|
99
|
+
expect(result.data?.active).toBe('autonomous-builder');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('TC-02: switches to a valid preset and drives the live re-apply seams', async () => {
|
|
103
|
+
const setActivePresetId = vi.fn();
|
|
104
|
+
const setPermissionMode = vi.fn();
|
|
105
|
+
const applyModelOptions = vi.fn();
|
|
106
|
+
const runtime = createSessionRuntime({
|
|
107
|
+
setActivePresetId,
|
|
108
|
+
setPermissionMode,
|
|
109
|
+
applyModelOptions,
|
|
110
|
+
});
|
|
111
|
+
const context = createCommandHostContext(runtime);
|
|
112
|
+
|
|
113
|
+
const result = await executePresetCommand(context, 'careful-reviewer');
|
|
114
|
+
|
|
115
|
+
expect(result.success).toBe(true);
|
|
116
|
+
expect(result.message).toBe('Switched to preset: careful-reviewer');
|
|
117
|
+
expect(result.data?.preset).toBe('careful-reviewer');
|
|
118
|
+
expect(setActivePresetId).toHaveBeenCalledWith('careful-reviewer');
|
|
119
|
+
// careful-reviewer resolves autonomy ask-first → default permission posture (PRESET-012)
|
|
120
|
+
// and effort high → applyModelOptions (PRESET-013).
|
|
121
|
+
expect(setPermissionMode).toHaveBeenCalledWith('default');
|
|
122
|
+
expect(applyModelOptions).toHaveBeenCalledWith(expect.objectContaining({ effort: 'high' }));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('TC-04: rejects an unknown preset id without switching', async () => {
|
|
126
|
+
const setActivePresetId = vi.fn();
|
|
127
|
+
const runtime = createSessionRuntime({ setActivePresetId });
|
|
128
|
+
const context = createCommandHostContext(runtime);
|
|
129
|
+
|
|
130
|
+
const result = await executePresetCommand(context, '__nope__');
|
|
131
|
+
|
|
132
|
+
expect(result.success).toBe(false);
|
|
133
|
+
for (const preset of listPresets()) {
|
|
134
|
+
expect(result.message).toContain(preset.id);
|
|
135
|
+
}
|
|
136
|
+
expect(setActivePresetId).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { listPresets } from '@robota-sdk/agent-preset';
|
|
2
|
+
|
|
3
|
+
import { executePresetCommand } from './preset-command.js';
|
|
4
|
+
|
|
5
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
6
|
+
import type {
|
|
7
|
+
ICommand,
|
|
8
|
+
ICommandInteractionHint,
|
|
9
|
+
ICommandSource,
|
|
10
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
11
|
+
|
|
12
|
+
const PRESET_COMMAND_DESCRIPTION = 'List presets or switch the active preset';
|
|
13
|
+
const PRESET_ARGUMENT_HINT = 'list | <preset-id>';
|
|
14
|
+
|
|
15
|
+
/** Build one subcommand per registered preset id (mirrors the permission-mode subcommands). */
|
|
16
|
+
function buildPresetSubcommands(source = 'preset'): ICommand[] {
|
|
17
|
+
return listPresets().map((preset) => ({
|
|
18
|
+
name: preset.id,
|
|
19
|
+
description: preset.description,
|
|
20
|
+
source,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createPresetCommandEntry(): ICommand {
|
|
25
|
+
return {
|
|
26
|
+
name: 'preset',
|
|
27
|
+
displayName: 'Agent Preset',
|
|
28
|
+
description: PRESET_COMMAND_DESCRIPTION,
|
|
29
|
+
source: 'preset',
|
|
30
|
+
argumentHint: PRESET_ARGUMENT_HINT,
|
|
31
|
+
subcommands: buildPresetSubcommands('preset'),
|
|
32
|
+
modelInvocable: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createPresetSystemCommand(): ISystemCommand {
|
|
37
|
+
const entry = createPresetCommandEntry();
|
|
38
|
+
return {
|
|
39
|
+
name: entry.name,
|
|
40
|
+
displayName: entry.displayName,
|
|
41
|
+
description: entry.description,
|
|
42
|
+
requiresPermission: false,
|
|
43
|
+
userInvocable: true,
|
|
44
|
+
modelInvocable: false,
|
|
45
|
+
argumentHint: entry.argumentHint,
|
|
46
|
+
subcommands: entry.subcommands,
|
|
47
|
+
lifecycle: 'inline',
|
|
48
|
+
execute: executePresetCommand,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class PresetCommandSource implements ICommandSource {
|
|
53
|
+
readonly name = 'preset';
|
|
54
|
+
|
|
55
|
+
getCommands(): ICommand[] {
|
|
56
|
+
return [createPresetCommandEntry()];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PRESET_INTERACTION_HINTS: Record<string, ICommandInteractionHint> = {
|
|
61
|
+
preset: {
|
|
62
|
+
type: 'pick',
|
|
63
|
+
getItems: () =>
|
|
64
|
+
buildPresetSubcommands().map((sub) => ({
|
|
65
|
+
label: sub.name,
|
|
66
|
+
value: sub.name,
|
|
67
|
+
description: sub.description,
|
|
68
|
+
})),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function createPresetCommandModule(): ICommandModule {
|
|
73
|
+
return {
|
|
74
|
+
name: 'agent-command-preset',
|
|
75
|
+
commandSources: [new PresetCommandSource()],
|
|
76
|
+
systemCommands: [createPresetSystemCommand()],
|
|
77
|
+
interactionHints: PRESET_INTERACTION_HINTS,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { applyPresetToSession } from '@robota-sdk/agent-framework';
|
|
2
|
+
import { getPreset, listPresets, resolvePreset } from '@robota-sdk/agent-preset';
|
|
3
|
+
|
|
4
|
+
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
5
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-transport';
|
|
6
|
+
|
|
7
|
+
/** Default active preset id reported when the runtime has no recorded active preset. */
|
|
8
|
+
const DEFAULT_ACTIVE_PRESET_ID = 'default';
|
|
9
|
+
|
|
10
|
+
/** Read the active preset id from the session, defaulting when the optional seam is absent. */
|
|
11
|
+
function readActivePresetId(context: ICommandHostContext): string {
|
|
12
|
+
return context.getSession().getActivePresetId?.() ?? DEFAULT_ACTIVE_PRESET_ID;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Build the `/preset` listing: one line per preset, marking the active one with a `*` prefix. */
|
|
16
|
+
function formatPresetList(active: string): string {
|
|
17
|
+
const lines = listPresets().map((preset) => {
|
|
18
|
+
const marker = preset.id === active ? '* ' : ' ';
|
|
19
|
+
return `${marker}${preset.id} — ${preset.title}: ${preset.description}`;
|
|
20
|
+
});
|
|
21
|
+
return ['Available presets:', ...lines].join('\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Build the rejection message for an unknown preset id, listing the valid ids. */
|
|
25
|
+
function formatUnknownPresetMessage(id: string): string {
|
|
26
|
+
const ids = listPresets()
|
|
27
|
+
.map((preset) => preset.id)
|
|
28
|
+
.join(', ');
|
|
29
|
+
return `Unknown preset: ${id}. Available: ${ids}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function executePresetCommand(
|
|
33
|
+
context: ICommandHostContext,
|
|
34
|
+
args: string,
|
|
35
|
+
): Promise<ICommandResult> {
|
|
36
|
+
const id = args.trim().split(/\s+/)[0];
|
|
37
|
+
|
|
38
|
+
if (id === undefined || id.length === 0 || id === 'list') {
|
|
39
|
+
const active = readActivePresetId(context);
|
|
40
|
+
return {
|
|
41
|
+
message: formatPresetList(active),
|
|
42
|
+
success: true,
|
|
43
|
+
data: { presets: listPresets(), active },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (getPreset(id) === undefined) {
|
|
48
|
+
return {
|
|
49
|
+
message: formatUnknownPresetMessage(id),
|
|
50
|
+
success: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const resolved = resolvePreset(id);
|
|
55
|
+
await applyPresetToSession(context, id, resolved);
|
|
56
|
+
return {
|
|
57
|
+
message: `Switched to preset: ${id}`,
|
|
58
|
+
success: true,
|
|
59
|
+
data: { preset: id },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,69 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
interface IModelPrice {
|
|
3
|
-
inputPerMillion: number;
|
|
4
|
-
outputPerMillion: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const MODEL_PRICES: Record<string, IModelPrice> = {
|
|
8
|
-
// Anthropic Claude 4
|
|
9
|
-
'claude-opus-4-7': { inputPerMillion: 15, outputPerMillion: 75 },
|
|
10
|
-
'claude-opus-4-5': { inputPerMillion: 15, outputPerMillion: 75 },
|
|
11
|
-
'claude-sonnet-4-6': { inputPerMillion: 3, outputPerMillion: 15 },
|
|
12
|
-
'claude-sonnet-4-5': { inputPerMillion: 3, outputPerMillion: 15 },
|
|
13
|
-
'claude-haiku-4-5': { inputPerMillion: 0.8, outputPerMillion: 4 },
|
|
14
|
-
// Anthropic Claude 3
|
|
15
|
-
'claude-3-5-sonnet-20241022': { inputPerMillion: 3, outputPerMillion: 15 },
|
|
16
|
-
'claude-3-5-haiku-20241022': { inputPerMillion: 0.8, outputPerMillion: 4 },
|
|
17
|
-
'claude-3-opus-20240229': { inputPerMillion: 15, outputPerMillion: 75 },
|
|
18
|
-
// OpenAI
|
|
19
|
-
'gpt-4o': { inputPerMillion: 2.5, outputPerMillion: 10 },
|
|
20
|
-
'gpt-4o-mini': { inputPerMillion: 0.15, outputPerMillion: 0.6 },
|
|
21
|
-
o1: { inputPerMillion: 15, outputPerMillion: 60 },
|
|
22
|
-
'o1-mini': { inputPerMillion: 3, outputPerMillion: 12 },
|
|
23
|
-
o3: { inputPerMillion: 10, outputPerMillion: 40 },
|
|
24
|
-
'o3-mini': { inputPerMillion: 1.1, outputPerMillion: 4.4 },
|
|
25
|
-
// DeepSeek
|
|
26
|
-
'deepseek-chat': { inputPerMillion: 0.14, outputPerMillion: 0.28 },
|
|
27
|
-
'deepseek-reasoner': { inputPerMillion: 0.55, outputPerMillion: 2.19 },
|
|
28
|
-
// Google Gemini
|
|
29
|
-
'gemini-2.0-flash': { inputPerMillion: 0.1, outputPerMillion: 0.4 },
|
|
30
|
-
'gemini-2.0-flash-thinking': { inputPerMillion: 0.35, outputPerMillion: 3.5 },
|
|
31
|
-
'gemini-1.5-pro': { inputPerMillion: 1.25, outputPerMillion: 5 },
|
|
32
|
-
'gemini-1.5-flash': { inputPerMillion: 0.075, outputPerMillion: 0.3 },
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const PATTERN_PRICES: Array<{ pattern: RegExp; price: IModelPrice }> = [
|
|
36
|
-
{ pattern: /claude-opus/i, price: { inputPerMillion: 15, outputPerMillion: 75 } },
|
|
37
|
-
{ pattern: /claude-sonnet/i, price: { inputPerMillion: 3, outputPerMillion: 15 } },
|
|
38
|
-
{ pattern: /claude-haiku/i, price: { inputPerMillion: 0.8, outputPerMillion: 4 } },
|
|
39
|
-
{ pattern: /gpt-4o-mini/i, price: { inputPerMillion: 0.15, outputPerMillion: 0.6 } },
|
|
40
|
-
{ pattern: /gpt-4/i, price: { inputPerMillion: 2.5, outputPerMillion: 10 } },
|
|
41
|
-
{ pattern: /deepseek/i, price: { inputPerMillion: 0.14, outputPerMillion: 0.28 } },
|
|
42
|
-
{ pattern: /gemini-2/i, price: { inputPerMillion: 0.1, outputPerMillion: 0.4 } },
|
|
43
|
-
{ pattern: /gemini-1/i, price: { inputPerMillion: 1.25, outputPerMillion: 5 } },
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
function lookupPrice(modelId: string): IModelPrice | undefined {
|
|
47
|
-
const exact = MODEL_PRICES[modelId];
|
|
48
|
-
if (exact) return exact;
|
|
49
|
-
for (const { pattern, price } of PATTERN_PRICES) {
|
|
50
|
-
if (pattern.test(modelId)) return price;
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
1
|
+
import { calculateModelCost } from '@robota-sdk/agent-core';
|
|
54
2
|
|
|
55
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Returns USD cost, or undefined if the model is not in the pricing table.
|
|
5
|
+
* Delegates to the agent-core pricing SSOT (`calculateModelCost`); this package owns only the
|
|
6
|
+
* command-facing display helpers below.
|
|
7
|
+
*/
|
|
56
8
|
export function calculateCost(
|
|
57
9
|
modelId: string,
|
|
58
10
|
inputTokens: number,
|
|
59
11
|
outputTokens: number,
|
|
60
12
|
): number | undefined {
|
|
61
|
-
|
|
62
|
-
if (!price) return undefined;
|
|
63
|
-
return (
|
|
64
|
-
(inputTokens / 1_000_000) * price.inputPerMillion +
|
|
65
|
-
(outputTokens / 1_000_000) * price.outputPerMillion
|
|
66
|
-
);
|
|
13
|
+
return calculateModelCost(modelId, inputTokens, outputTokens);
|
|
67
14
|
}
|
|
68
15
|
|
|
69
16
|
/** Format a USD amount for display (e.g. "$0.0043"). */
|