@robota-sdk/agent-command 3.0.0-beta.83 → 3.0.0-beta.84
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/CHANGELOG.md +27 -0
- package/README.md +71 -62
- package/dist/node/index.cjs +75 -72
- package/dist/node/index.d.cts +37 -45
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +37 -45
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +78 -75
- package/dist/node/index.js.map +1 -1
- package/package.json +9 -8
- package/src/agent/__tests__/agent-command.test.ts +62 -0
- package/src/agent/agent-command-parser.ts +2 -2
- package/src/agent/agent-command.ts +20 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +2 -2
- package/src/default/__tests__/default-command-modules.test.ts +5 -4
- package/src/default/__tests__/model-exposure.test.ts +2 -0
- package/src/default/default-command-modules.ts +8 -0
- package/src/effort/effort-command.test.ts +65 -2
- package/src/effort/effort-command.ts +19 -5
- package/src/git/__tests__/fake-git-port.ts +9 -2
- package/src/git/git-command-module.ts +3 -4
- package/src/git/git-commit.ts +6 -3
- package/src/git/index.ts +4 -1
- package/src/goal/goal-command.ts +3 -1
- package/src/index.ts +6 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +131 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +6 -0
- package/src/mcp-activation/mcp-activation-command.ts +54 -1
- package/src/memory/__tests__/memory-command-module.test.ts +40 -2
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/memory-command-module.ts +4 -2
- package/src/memory/memory-command.ts +42 -2
- package/src/mode/__tests__/mode-command-module.test.ts +40 -0
- package/src/mode/mode-command.ts +1 -1
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +240 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +223 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +81 -1
- package/src/plugin/plugin-command.ts +47 -4
- package/src/provider/__tests__/org-policy.test.ts +4 -0
- package/src/provider/__tests__/provider-command-module.test.ts +276 -14
- package/src/provider/__tests__/scripted-interaction.ts +12 -2
- package/src/provider/provider-command-execution.ts +54 -8
- package/src/provider/provider-command-profile-lifecycle.ts +48 -2
- package/src/provider/provider-command-profile-operations.ts +14 -0
- package/src/provider/provider-command-profile.ts +7 -4
- package/src/provider/provider-command-setup.ts +14 -1
- package/src/rewind/__tests__/rewind-command-module.test.ts +2 -1
- package/src/schedule/__tests__/loop-command-functional.test.ts +13 -0
- package/src/schedule/__tests__/loop-command.test.ts +238 -4
- package/src/schedule/loop-command.ts +95 -3
- package/src/schedule/schedule-command-module.ts +8 -4
- package/src/settings/settings-command-module.ts +2 -2
- package/src/git/__tests__/git-diff.test.ts +0 -125
- package/src/git/__tests__/git-process.test.ts +0 -221
- package/src/git/__tests__/git-status.test.ts +0 -117
- package/src/git/git-diff.ts +0 -129
- package/src/git/git-process.ts +0 -169
- package/src/git/git-status.ts +0 -134
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-command",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0-beta.84",
|
|
4
|
+
"description": "Slash-command modules for Robota SDK sessions (/help, /permissions, /mcp, /rewind, …)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
7
7
|
"types": "dist/node/index.d.ts",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"CHANGELOG.md"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@robota-sdk/agent-core": "3.0.0-beta.
|
|
44
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
45
|
-
"@robota-sdk/agent-interface-command": "3.0.0-beta.
|
|
46
|
-
"@robota-sdk/agent-interface-execution": "3.0.0-beta.
|
|
47
|
-
"@robota-sdk/agent-interface-session": "3.0.0-beta.
|
|
48
|
-
"@robota-sdk/agent-preset": "3.0.0-beta.
|
|
43
|
+
"@robota-sdk/agent-core": "3.0.0-beta.84",
|
|
44
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.84",
|
|
45
|
+
"@robota-sdk/agent-interface-command": "3.0.0-beta.84",
|
|
46
|
+
"@robota-sdk/agent-interface-execution": "3.0.0-beta.84",
|
|
47
|
+
"@robota-sdk/agent-interface-session": "3.0.0-beta.84",
|
|
48
|
+
"@robota-sdk/agent-preset": "3.0.0-beta.84"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.19.21",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"url": "https://github.com/woojubb/robota.git",
|
|
61
61
|
"directory": "packages/agent-command"
|
|
62
62
|
},
|
|
63
|
+
"homepage": "https://docs.robota.io/en/packages/agent-command/",
|
|
63
64
|
"publishConfig": {
|
|
64
65
|
"access": "public"
|
|
65
66
|
},
|
|
@@ -15,6 +15,7 @@ function createMockSession(overrides?: Record<string, unknown>) {
|
|
|
15
15
|
{ name: 'general-purpose', description: 'General-purpose task execution agent.' },
|
|
16
16
|
{ name: 'Plan', description: 'Read-only planning agent.' },
|
|
17
17
|
]),
|
|
18
|
+
setDefaultAgentType: vi.fn(),
|
|
18
19
|
listAgentJobs: vi.fn().mockReturnValue([]),
|
|
19
20
|
readBackgroundTaskLog: vi.fn().mockResolvedValue({ taskId: 'agent_1', lines: [] }),
|
|
20
21
|
spawnAgentJob: vi.fn().mockResolvedValue({
|
|
@@ -518,6 +519,67 @@ describe('agent command module', () => {
|
|
|
518
519
|
expect(result?.uiIntents).toEqual([{ type: 'show-agent-switcher' }]);
|
|
519
520
|
});
|
|
520
521
|
|
|
522
|
+
it('#3282 §4: /agent <name> with nothing after it selects a default agent instead of failing', async () => {
|
|
523
|
+
const module = createAgentCommandModule();
|
|
524
|
+
const executor = new SystemCommandExecutor([
|
|
525
|
+
...createSystemCommands(),
|
|
526
|
+
...(module.systemCommands ?? []),
|
|
527
|
+
]);
|
|
528
|
+
const session = createMockSession();
|
|
529
|
+
|
|
530
|
+
const result = await executor.execute('agent', session, 'Plan');
|
|
531
|
+
|
|
532
|
+
expect(result?.success).toBe(true);
|
|
533
|
+
expect(result?.message).toBe('Default agent: Plan');
|
|
534
|
+
expect(result?.data).toEqual({ agentType: 'Plan' });
|
|
535
|
+
expect(
|
|
536
|
+
(session as unknown as { setDefaultAgentType: ReturnType<typeof vi.fn> }).setDefaultAgentType,
|
|
537
|
+
).toHaveBeenCalledWith('Plan');
|
|
538
|
+
expect(
|
|
539
|
+
(session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> }).spawnAgentJob,
|
|
540
|
+
).not.toHaveBeenCalled();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('#3282 §4: a single unrecognized word is still a prompt for the default agent, not a selection', async () => {
|
|
544
|
+
const module = createAgentCommandModule();
|
|
545
|
+
const executor = new SystemCommandExecutor([
|
|
546
|
+
...createSystemCommands(),
|
|
547
|
+
...(module.systemCommands ?? []),
|
|
548
|
+
]);
|
|
549
|
+
const session = createMockSession();
|
|
550
|
+
|
|
551
|
+
const result = await executor.execute('agent', session, 'NotARealAgent');
|
|
552
|
+
|
|
553
|
+
expect(result?.success).toBe(true);
|
|
554
|
+
expect(
|
|
555
|
+
(session as unknown as { spawnAgentJob: ReturnType<typeof vi.fn> }).spawnAgentJob,
|
|
556
|
+
).toHaveBeenCalledWith({
|
|
557
|
+
agentType: 'general-purpose',
|
|
558
|
+
label: 'general-purpose',
|
|
559
|
+
mode: 'background',
|
|
560
|
+
prompt: 'NotARealAgent',
|
|
561
|
+
});
|
|
562
|
+
expect(
|
|
563
|
+
(session as unknown as { setDefaultAgentType: ReturnType<typeof vi.fn> }).setDefaultAgentType,
|
|
564
|
+
).not.toHaveBeenCalled();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('#3282 §4: a known agent name followed by a prompt still runs a job, not a selection', async () => {
|
|
568
|
+
const module = createAgentCommandModule();
|
|
569
|
+
const executor = new SystemCommandExecutor([
|
|
570
|
+
...createSystemCommands(),
|
|
571
|
+
...(module.systemCommands ?? []),
|
|
572
|
+
]);
|
|
573
|
+
const session = createMockSession();
|
|
574
|
+
|
|
575
|
+
const result = await executor.execute('agent', session, 'Plan "draft architecture"');
|
|
576
|
+
|
|
577
|
+
expect(result?.success).toBe(true);
|
|
578
|
+
expect(
|
|
579
|
+
(session as unknown as { setDefaultAgentType: ReturnType<typeof vi.fn> }).setDefaultAgentType,
|
|
580
|
+
).not.toHaveBeenCalled();
|
|
581
|
+
});
|
|
582
|
+
|
|
521
583
|
it('shows text list when agent list subcommand is used explicitly', async () => {
|
|
522
584
|
const module = createAgentCommandModule();
|
|
523
585
|
const executor = new SystemCommandExecutor([
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DEFAULT_AGENT_TYPE } from '@robota-sdk/agent-framework';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import type { TBackgroundTaskIsolation } from '@robota-sdk/agent-interface-execution';
|
|
4
4
|
|
|
5
5
|
export type TAgentMode = 'background';
|
|
6
6
|
|
|
@@ -44,6 +44,21 @@ function executeOpenSwitcher(): ICommandResult {
|
|
|
44
44
|
return { message: '', uiIntents: [{ type: 'show-agent-switcher' }], success: true };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* #3282 §4: `/agent <name>` with nothing after the name sets the session's default agent type —
|
|
49
|
+
* the one `/agent <prompt>` falls back to, and the one the agent switcher shows checked. Distinct
|
|
50
|
+
* from `executeRun`, which a name FOLLOWED by a prompt still reaches (a prompt makes it a run, not
|
|
51
|
+
* a selection).
|
|
52
|
+
*/
|
|
53
|
+
function executeSelectDefaultAgent(session: IAgentJobHostContext, agentType: string): ICommandResult {
|
|
54
|
+
session.setDefaultAgentType(agentType);
|
|
55
|
+
return {
|
|
56
|
+
message: `Default agent: ${agentType}`,
|
|
57
|
+
success: true,
|
|
58
|
+
data: { agentType },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
async function executeList(session: IAgentJobHostContext): Promise<ICommandResult> {
|
|
48
63
|
const agents = session.listAgentDefinitions();
|
|
49
64
|
const jobs = session.listAgentJobs();
|
|
@@ -223,6 +238,11 @@ export async function executeAgentCommand(
|
|
|
223
238
|
if (action === 'send') return executeSend(session, tokens);
|
|
224
239
|
if (action === 'stop' || action === 'cancel') return executeStop(session, tokens);
|
|
225
240
|
if (action === 'close') return executeClose(session, tokens);
|
|
241
|
+
// `/agent <name>` alone (a known agent, nothing after it) selects a default rather than failing
|
|
242
|
+
// the "run" parse for want of a prompt — the same one-token shape the switcher sends.
|
|
243
|
+
if (tokens.length === 0 && getAvailableAgentNames(session).has(action)) {
|
|
244
|
+
return executeSelectDefaultAgent(session, action);
|
|
245
|
+
}
|
|
226
246
|
return executeRun(session, [action, ...tokens]);
|
|
227
247
|
} catch (error) {
|
|
228
248
|
return { message: formatError(error), success: false };
|
|
@@ -44,7 +44,7 @@ Run it with no arguments.",
|
|
|
44
44
|
},
|
|
45
45
|
"loop": {
|
|
46
46
|
"argumentHint": "[prompt] | <N><s|m|h|d> [prompt] | <prompt> every <N> <unit> | list | stop <id>",
|
|
47
|
-
"description": "Repeat a prompt on a fixed cadence (\`<N><s|m|h|d>
|
|
47
|
+
"description": "Repeat a prompt on a fixed cadence (\`<N><s|m|h|d> <prompt>\`) or self-paced (\`<prompt>\`: each iteration chooses its next delay or stops). Use it when the user asks for recurring work in this session, and always give your own prompt: any form that would fall back to the host's default maintenance prompt — bare \`/loop\` or an interval alone (\`<N><s|m|h|d>\`) — needs a person to confirm it and is always refused for you, whether or not one is attached. \`list\` shows active loops; \`stop <id>\` ends one, even mid-turn. Returns the loop id and its next fire time, the loop list, or a refusal naming the command to suggest.",
|
|
48
48
|
"kind": "builtin-command",
|
|
49
49
|
"modelInvocable": true,
|
|
50
50
|
"name": "loop",
|
|
@@ -64,7 +64,7 @@ Subcommands you may run (bare \`mcp\` is also allowed):
|
|
|
64
64
|
},
|
|
65
65
|
"memory": {
|
|
66
66
|
"argumentHint": "[list | show [topic] | add <user|feedback|project|reference> <topic> <text> | pending | used]",
|
|
67
|
-
"description": "Read and write this project’s durable memory. Use \`list\` or \`show [topic]\` to look up stored conventions, preferences or references before asking the user again; \`add <type> <topic> <text>\` to save a durable preference, project convention, feedback item or reference worth reusing across sessions (never secrets, credentials or transient facts); \`pending\` to see candidates awaiting the user’s review; \`used\` to report which memory items informed this turn. Bare lists topics. Returns the requested topics or entries, or a confirmation of what was saved. Approving or rejecting a pending candidate is the user’s review: suggest \`/memory approve <id>\` or \`/memory reject <id>\`.
|
|
67
|
+
"description": "Read and write this project’s durable memory. Use \`list\` or \`show [topic]\` to look up stored conventions, preferences or references before asking the user again; \`add <type> <topic> <text>\` to save a durable preference, project convention, feedback item or reference worth reusing across sessions (never secrets, credentials or transient facts); \`pending\` to see candidates awaiting the user’s review; \`used\` to report which memory items informed this turn. Bare lists topics. In plan mode \`add\` is refused: save after plan mode ends. Returns the requested topics or entries, or a confirmation of what was saved. Approving or rejecting a pending candidate is the user’s review: suggest \`/memory approve <id>\` or \`/memory reject <id>\`.
|
|
68
68
|
Subcommands you may run (bare \`memory\` is also allowed):
|
|
69
69
|
- list: List project memory topics
|
|
70
70
|
- show: Show project memory index or a topic
|
|
@@ -50,10 +50,10 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
|
|
|
50
50
|
const names = moduleNames(baseOptions);
|
|
51
51
|
// No-regression: the default set length is the documented 32 modules (SELFHOST-002 added
|
|
52
52
|
// `/plan`; PEER-004 added `/peers`; CLI-1994 added `/fork`; MCP-2520 added `/mcp`; CLI-1988 added
|
|
53
|
-
// `/output-style`; FLOW-008 added `/effort`; `/advisor` joined beside it
|
|
54
|
-
// assertion that matters — a length on its own can be restored by any
|
|
55
|
-
// exists only to catch an addition that also removed something.
|
|
56
|
-
expect(names).toHaveLength(
|
|
53
|
+
// `/output-style`; FLOW-008 added `/effort`; `/advisor` joined beside it; #3282 §2 added `/model`).
|
|
54
|
+
// The list below is the assertion that matters — a length on its own can be restored by any
|
|
55
|
+
// substitution, and the count exists only to catch an addition that also removed something.
|
|
56
|
+
expect(names).toHaveLength(39);
|
|
57
57
|
expect(names).toEqual([
|
|
58
58
|
'agent-command-skills',
|
|
59
59
|
'agent-command-help',
|
|
@@ -94,6 +94,7 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
|
|
|
94
94
|
'agent-command-handoff',
|
|
95
95
|
'agent-command-remote-control',
|
|
96
96
|
'agent-command-provider',
|
|
97
|
+
'agent-command-model',
|
|
97
98
|
]);
|
|
98
99
|
});
|
|
99
100
|
|
|
@@ -109,6 +109,8 @@ const USER_ONLY_COMMANDS = [
|
|
|
109
109
|
'events',
|
|
110
110
|
// Account and credential actions.
|
|
111
111
|
'provider',
|
|
112
|
+
// Cost and capability decisions: the person chooses, never the model on its own (#3282 §2).
|
|
113
|
+
'model',
|
|
112
114
|
// Permission widening or permission-mode changes.
|
|
113
115
|
'mode',
|
|
114
116
|
'permissions',
|
|
@@ -20,6 +20,7 @@ import { createLanguageCommandModule } from '../language/index.js';
|
|
|
20
20
|
import { createMCPActivationCommandModule } from '../mcp-activation/index.js';
|
|
21
21
|
import { createMemoryCommandModule } from '../memory/index.js';
|
|
22
22
|
import { createModeCommandModule } from '../mode/index.js';
|
|
23
|
+
import { createModelCommandModule } from '../model/index.js';
|
|
23
24
|
import { createSandboxCommandModule } from '../sandbox/index.js';
|
|
24
25
|
import { createOutputStyleCommandModule } from '../output-style/index.js';
|
|
25
26
|
import { createEventsCommandModule } from '../events/index.js';
|
|
@@ -204,6 +205,13 @@ export function createDefaultCommandModules({
|
|
|
204
205
|
settings: providerSettingsAdapter,
|
|
205
206
|
...(orgPolicy === undefined ? {} : { orgPolicy }),
|
|
206
207
|
}),
|
|
208
|
+
// #3282 §2: shares the provider command's settings adapter and definitions — one settings write
|
|
209
|
+
// owner, never a second copy that could drift from what `/provider switch` reads and writes.
|
|
210
|
+
createModelCommandModule({
|
|
211
|
+
providerDefinitions,
|
|
212
|
+
settings: providerSettingsAdapter,
|
|
213
|
+
...(orgPolicy === undefined ? {} : { orgPolicy }),
|
|
214
|
+
}),
|
|
207
215
|
];
|
|
208
216
|
const builtModuleNames = modules.map((module) => module.name);
|
|
209
217
|
return {
|
|
@@ -5,6 +5,37 @@ import { executeEffortCommand } from './effort-command.js';
|
|
|
5
5
|
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
6
6
|
|
|
7
7
|
describe('/effort', () => {
|
|
8
|
+
it('#3282 §2: reports plainly when the model has no matching effort control at all', async () => {
|
|
9
|
+
const host = createTestCommandHost({
|
|
10
|
+
overrides: {
|
|
11
|
+
getCommandHostAdapters: () => ({
|
|
12
|
+
settings: { read: () => ({}), write: vi.fn() },
|
|
13
|
+
effort: {
|
|
14
|
+
getResolution: () => ({
|
|
15
|
+
requested: 'low',
|
|
16
|
+
effective: 'low',
|
|
17
|
+
source: 'command',
|
|
18
|
+
disposition: 'not-applied',
|
|
19
|
+
modelDefault: 'high',
|
|
20
|
+
}),
|
|
21
|
+
apply: async () => ({
|
|
22
|
+
requested: 'low',
|
|
23
|
+
effective: 'low',
|
|
24
|
+
source: 'command',
|
|
25
|
+
disposition: 'not-applied',
|
|
26
|
+
modelDefault: 'high',
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const result = await executeEffortCommand(host, 'low');
|
|
34
|
+
|
|
35
|
+
expect(result.success).toBe(true);
|
|
36
|
+
expect(result.message).toBe("This model doesn't support effort levels, so it will use its default.");
|
|
37
|
+
});
|
|
38
|
+
|
|
8
39
|
it('applies a requested level and returns the complete resolution', async () => {
|
|
9
40
|
const applyModelOptions = vi.fn();
|
|
10
41
|
const writeSettings = vi.fn();
|
|
@@ -36,6 +67,7 @@ describe('/effort', () => {
|
|
|
36
67
|
const result = await executeEffortCommand(host, 'low');
|
|
37
68
|
|
|
38
69
|
expect(result.success).toBe(true);
|
|
70
|
+
expect(result.message).toBe('Effort: Low');
|
|
39
71
|
expect(result.data).toMatchObject({
|
|
40
72
|
effort: { requested: 'low', effective: 'low', source: 'command', disposition: 'applied' },
|
|
41
73
|
});
|
|
@@ -50,7 +82,7 @@ describe('/effort', () => {
|
|
|
50
82
|
const result = await executeEffortCommand(host, '');
|
|
51
83
|
|
|
52
84
|
expect(result.success).toBe(true);
|
|
53
|
-
expect(result.message).
|
|
85
|
+
expect(result.message).toBe('Effort: High');
|
|
54
86
|
expect(applyModelOptions).not.toHaveBeenCalled();
|
|
55
87
|
});
|
|
56
88
|
|
|
@@ -133,8 +165,39 @@ describe('/effort', () => {
|
|
|
133
165
|
const result = await executeEffortCommand(host, '');
|
|
134
166
|
|
|
135
167
|
expect(result.success).toBe(true);
|
|
136
|
-
expect(result.message).
|
|
168
|
+
expect(result.message).toBe('Effort: Medium');
|
|
137
169
|
expect(applyModelOptions).not.toHaveBeenCalled();
|
|
138
170
|
expect(writeSettings).not.toHaveBeenCalled();
|
|
139
171
|
});
|
|
172
|
+
|
|
173
|
+
it('#3282 §2: uses the full plain word for xhigh, not the internal id', async () => {
|
|
174
|
+
const host = createTestCommandHost({
|
|
175
|
+
overrides: {
|
|
176
|
+
getCommandHostAdapters: () => ({
|
|
177
|
+
settings: { read: () => ({}), write: vi.fn() },
|
|
178
|
+
effort: {
|
|
179
|
+
getResolution: () => ({
|
|
180
|
+
requested: 'auto',
|
|
181
|
+
effective: 'high',
|
|
182
|
+
source: 'model-default',
|
|
183
|
+
disposition: 'model-default',
|
|
184
|
+
modelDefault: 'high',
|
|
185
|
+
}),
|
|
186
|
+
apply: async (selection) => ({
|
|
187
|
+
requested: selection,
|
|
188
|
+
effective: selection === 'auto' ? 'high' : selection,
|
|
189
|
+
source: 'command',
|
|
190
|
+
disposition: 'applied',
|
|
191
|
+
modelDefault: 'high',
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const result = await executeEffortCommand(host, 'xhigh');
|
|
199
|
+
|
|
200
|
+
expect(result.success).toBe(true);
|
|
201
|
+
expect(result.message).toBe('Effort: Extra high');
|
|
202
|
+
});
|
|
140
203
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MODEL_EFFORT_VALUES, selectAction } from '@robota-sdk/agent-core';
|
|
2
|
-
import { parseModelEffort, resolveModelEffort } from '@robota-sdk/agent-framework';
|
|
2
|
+
import { EFFORT_LEVEL_LABELS, parseModelEffort, resolveModelEffort } from '@robota-sdk/agent-framework';
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
ICommandHostAdapterAccess,
|
|
@@ -14,13 +14,27 @@ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
|
14
14
|
|
|
15
15
|
const EFFORT_SELECTIONS: readonly TEffortSelection[] = ['auto', ...MODEL_EFFORT_VALUES];
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* #3282 §2 — re-exported for callers already importing it from here; the canonical definition moved
|
|
19
|
+
* to `agent-framework` (`effort/effort-resolution.ts`) so headless `/goal` output (in that same
|
|
20
|
+
* package) can use the same labels without a circular dependency on `agent-command`.
|
|
21
|
+
*/
|
|
22
|
+
export { EFFORT_LEVEL_LABELS };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Plain outcome text (#3282 §2), replacing the earlier `Model effort: requested=…, effective=…,
|
|
26
|
+
* source=…, disposition=…` internal-field dump. `not-applied` means the model has no matching native
|
|
27
|
+
* control at all — the earlier fields carried no information a person could act on for that case, so
|
|
28
|
+
* it gets its own plain sentence instead of a label.
|
|
29
|
+
*/
|
|
17
30
|
function formatEffortMessage(resolution: {
|
|
18
31
|
requested: TEffortSelection;
|
|
19
|
-
effective: string;
|
|
20
|
-
source: string;
|
|
21
32
|
disposition: string;
|
|
22
33
|
}): string {
|
|
23
|
-
|
|
34
|
+
if (resolution.disposition === 'not-applied') {
|
|
35
|
+
return "This model doesn't support effort levels, so it will use its default.";
|
|
36
|
+
}
|
|
37
|
+
return `Effort: ${EFFORT_LEVEL_LABELS[resolution.requested]}`;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
async function askForEffort(
|
|
@@ -32,7 +46,7 @@ async function askForEffort(
|
|
|
32
46
|
selectAction(
|
|
33
47
|
'effort',
|
|
34
48
|
'Select model effort',
|
|
35
|
-
EFFORT_SELECTIONS.map((value) => ({ value, label: value })),
|
|
49
|
+
EFFORT_SELECTIONS.map((value) => ({ value, label: EFFORT_LEVEL_LABELS[value] })),
|
|
36
50
|
),
|
|
37
51
|
);
|
|
38
52
|
const selected = response.type === 'answer' ? response.values[0] : undefined;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A scripted `IGitProcessPort`: answers by argv, records every call, throws on an unscripted one.
|
|
3
|
+
*
|
|
4
|
+
* The port and outcome types moved to `@robota-sdk/agent-framework` with the git process module
|
|
5
|
+
* (#3282 §4c); this double stays here (duplicated, not imported cross-package) because it is a tiny,
|
|
6
|
+
* self-contained test fixture, the same call `agent-framework`'s own `git/__tests__/fake-git-port.ts`
|
|
7
|
+
* makes for its side.
|
|
8
|
+
*/
|
|
9
|
+
import type { IGitProcessPort, TGitProcessOutcome } from '@robota-sdk/agent-framework';
|
|
3
10
|
|
|
4
11
|
export interface IFakeGitPort extends IGitProcessPort {
|
|
5
12
|
readonly calls: readonly (readonly string[])[];
|
|
@@ -5,15 +5,14 @@
|
|
|
5
5
|
* on the user path (`SystemCommandExecutor.executeCommand` calls `execute` directly).
|
|
6
6
|
*/
|
|
7
7
|
import { executeGitCommit } from './git-commit.js';
|
|
8
|
-
import { executeGitDiff } from './git-diff.js';
|
|
9
|
-
import { createGitProcess } from './git-process.js';
|
|
10
|
-
import { executeGitStatus } from './git-status.js';
|
|
11
8
|
|
|
12
|
-
import
|
|
9
|
+
import { createGitProcess, executeGitDiff, executeGitStatus } from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
13
11
|
import type {
|
|
14
12
|
ICommandHostUserInteraction,
|
|
15
13
|
ICommandHostWorkspace,
|
|
16
14
|
ICommandModule,
|
|
15
|
+
IGitProcessPort,
|
|
17
16
|
ISystemCommand,
|
|
18
17
|
} from '@robota-sdk/agent-framework';
|
|
19
18
|
import type { ICommand, ICommandResult, ICommandSource } from '@robota-sdk/agent-interface-command';
|
package/src/git/git-commit.ts
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { confirmAction, isConfirmed, textAction } from '@robota-sdk/agent-core';
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
gitFailureMessage,
|
|
14
|
+
parseStatusPorcelainV2,
|
|
15
|
+
GIT_STATUS_ARGS,
|
|
16
|
+
} from '@robota-sdk/agent-framework';
|
|
14
17
|
|
|
15
|
-
import type { IGitProcessPort } from '
|
|
18
|
+
import type { IGitProcessPort } from '@robota-sdk/agent-framework';
|
|
16
19
|
import type { IUserInteraction } from '@robota-sdk/agent-core';
|
|
17
20
|
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
18
21
|
|
package/src/git/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// The git process port and the status/diff readers moved to `@robota-sdk/agent-framework` (#3282
|
|
2
|
+
// §4c) so the Project panel's session-level reads and this command run through the exact same
|
|
3
|
+
// functions; re-exported here so an existing `from '@robota-sdk/agent-command'` import keeps working.
|
|
1
4
|
export {
|
|
2
5
|
createGitProcess,
|
|
3
6
|
gitEnvironment,
|
|
@@ -6,7 +9,7 @@ export {
|
|
|
6
9
|
type IGitProcessRunOptions,
|
|
7
10
|
type TGitProcessFailureReason,
|
|
8
11
|
type TGitProcessOutcome,
|
|
9
|
-
} from '
|
|
12
|
+
} from '@robota-sdk/agent-framework';
|
|
10
13
|
export {
|
|
11
14
|
createGitCommandEntry,
|
|
12
15
|
createGitCommandModule,
|
package/src/goal/goal-command.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* command host context; all loop logic lives in agent-framework.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { isGoalCancelVerb } from '@robota-sdk/agent-framework';
|
|
8
|
+
|
|
7
9
|
import type { ICommandHostGoal } from '@robota-sdk/agent-framework';
|
|
8
10
|
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
9
11
|
import type { IGoalState } from '@robota-sdk/agent-interface-session';
|
|
@@ -45,7 +47,7 @@ export async function executeGoalCommand(
|
|
|
45
47
|
: { message: 'No goal is set.', success: true };
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
if (
|
|
50
|
+
if (isGoalCancelVerb(trimmed)) {
|
|
49
51
|
const stopped = context.cancelGoal();
|
|
50
52
|
return stopped
|
|
51
53
|
? { message: `Goal cancelled: ${stopped.objective}`, success: true }
|
package/src/index.ts
CHANGED
|
@@ -123,6 +123,12 @@ export {
|
|
|
123
123
|
} from './mcp-activation/mcp-model-notice.js';
|
|
124
124
|
export type { TMCPUserAction, TMCPUserActionSurface } from './mcp-activation/mcp-model-notice.js';
|
|
125
125
|
export * from './mode/index.js';
|
|
126
|
+
export {
|
|
127
|
+
createModelCommandEntry,
|
|
128
|
+
createModelCommandModule,
|
|
129
|
+
ModelCommandSource,
|
|
130
|
+
} from './model/model-command-module.js';
|
|
131
|
+
export { executeModelCommand } from './model/model-command.js';
|
|
126
132
|
export {
|
|
127
133
|
createSandboxCommandModule,
|
|
128
134
|
executeSandboxCommand,
|
|
@@ -655,6 +655,137 @@ describe('/mcp login', () => {
|
|
|
655
655
|
});
|
|
656
656
|
});
|
|
657
657
|
|
|
658
|
+
describe('/mcp reload (#3282 §4 part b-2)', () => {
|
|
659
|
+
function harness(
|
|
660
|
+
reload: ICommandMCPActivationAdapter['reload'],
|
|
661
|
+
taken: (names: string[]) => string[] = (names) => names,
|
|
662
|
+
) {
|
|
663
|
+
const added: IToolWithEventService[][] = [];
|
|
664
|
+
const reloadToolsAddedCalls: { token: string; added: readonly string[] }[] = [];
|
|
665
|
+
const port: ICommandMCPActivationAdapter = {
|
|
666
|
+
list: () => [],
|
|
667
|
+
approve: async () => summary(),
|
|
668
|
+
reject: async () => summary(),
|
|
669
|
+
revoke: async () => summary(),
|
|
670
|
+
reload,
|
|
671
|
+
reloadToolsAdded: (token, names) => reloadToolsAddedCalls.push({ token, added: names }),
|
|
672
|
+
};
|
|
673
|
+
const host = createTestCommandHost({
|
|
674
|
+
overrides: { getCommandHostAdapters: () => ({ mcpActivation: port }) },
|
|
675
|
+
});
|
|
676
|
+
const session = host.getSession();
|
|
677
|
+
host.getSession = () => ({
|
|
678
|
+
...session,
|
|
679
|
+
addTools: async (tools) => {
|
|
680
|
+
added.push([...tools]);
|
|
681
|
+
return taken(tools.map((tool) => tool.schema.name));
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
return {
|
|
685
|
+
added,
|
|
686
|
+
reloadToolsAddedCalls,
|
|
687
|
+
run: (args: string) => executeMCPActivationCommand(host, args),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const forecast = new FunctionTool(
|
|
692
|
+
{ name: 'weather__forecast', description: 'Forecast', parameters: { type: 'object', properties: {} } },
|
|
693
|
+
async () => 'sunny',
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
it('retries not-connected servers, adds the new tools to the session, and names what changed', async () => {
|
|
697
|
+
const h = harness(async () => ({
|
|
698
|
+
tools: [forecast],
|
|
699
|
+
connectedServerIds: ['weather'],
|
|
700
|
+
failedServerIds: ['flaky'],
|
|
701
|
+
reloadToken: 'token-1',
|
|
702
|
+
}));
|
|
703
|
+
|
|
704
|
+
const result = await h.run('reload');
|
|
705
|
+
|
|
706
|
+
expect(result.success).toBe(true);
|
|
707
|
+
expect(result.message).toContain('Connected: weather.');
|
|
708
|
+
expect(result.message).toContain('Still not connected: flaky');
|
|
709
|
+
expect(h.added).toEqual([[forecast]]);
|
|
710
|
+
expect(result.data).toEqual({
|
|
711
|
+
connectedServerIds: ['weather'],
|
|
712
|
+
failedServerIds: ['flaky'],
|
|
713
|
+
tools: ['weather__forecast'],
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('says plainly when nothing newly connected', async () => {
|
|
718
|
+
const h = harness(async () => ({ tools: [], connectedServerIds: [], failedServerIds: [] }));
|
|
719
|
+
const result = await h.run('reload');
|
|
720
|
+
expect(result.success).toBe(true);
|
|
721
|
+
expect(result.message).toContain('No server newly connected.');
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('is refused when the host offers no reload', async () => {
|
|
725
|
+
const h = harness(undefined);
|
|
726
|
+
const result = await h.run('reload');
|
|
727
|
+
expect(result.success).toBe(false);
|
|
728
|
+
expect(result.message).toBe('Reloading MCP servers is not available in this environment.');
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('tells the port which reloaded tools the session actually took, keyed to the reload it came from', async () => {
|
|
732
|
+
const h = harness(
|
|
733
|
+
async () => ({
|
|
734
|
+
tools: [forecast],
|
|
735
|
+
connectedServerIds: ['weather'],
|
|
736
|
+
failedServerIds: [],
|
|
737
|
+
reloadToken: 'token-1',
|
|
738
|
+
}),
|
|
739
|
+
(names) => names,
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
const result = await h.run('reload');
|
|
743
|
+
|
|
744
|
+
expect(result.success).toBe(true);
|
|
745
|
+
expect(h.reloadToolsAddedCalls).toEqual([{ token: 'token-1', added: ['weather__forecast'] }]);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('tells the port when a reloaded tool collided and was dropped', async () => {
|
|
749
|
+
const h = harness(
|
|
750
|
+
async () => ({
|
|
751
|
+
tools: [forecast],
|
|
752
|
+
connectedServerIds: ['weather'],
|
|
753
|
+
failedServerIds: [],
|
|
754
|
+
reloadToken: 'token-1',
|
|
755
|
+
}),
|
|
756
|
+
() => [],
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
const result = await h.run('reload');
|
|
760
|
+
|
|
761
|
+
expect(result.success).toBe(true);
|
|
762
|
+
expect(h.reloadToolsAddedCalls).toEqual([{ token: 'token-1', added: [] }]);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('never calls it when the reload has no token to acknowledge (nothing staged)', async () => {
|
|
766
|
+
// A defensive case: a host that returned tools without a token (should not happen in practice,
|
|
767
|
+
// since the composition only omits one when there is nothing to stage) must not be acknowledged
|
|
768
|
+
// — there would be nothing for the token to correlate to.
|
|
769
|
+
const h = harness(async () => ({
|
|
770
|
+
tools: [forecast],
|
|
771
|
+
connectedServerIds: ['weather'],
|
|
772
|
+
failedServerIds: [],
|
|
773
|
+
}));
|
|
774
|
+
|
|
775
|
+
await h.run('reload');
|
|
776
|
+
|
|
777
|
+
expect(h.reloadToolsAddedCalls).toEqual([]);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('never calls it when nothing newly connected', async () => {
|
|
781
|
+
const h = harness(async () => ({ tools: [], connectedServerIds: [], failedServerIds: [] }));
|
|
782
|
+
|
|
783
|
+
await h.run('reload');
|
|
784
|
+
|
|
785
|
+
expect(h.reloadToolsAddedCalls).toEqual([]);
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
658
789
|
describe('/mcp command entry', () => {
|
|
659
790
|
it('opens only `status` to the model, and describes login for the user', () => {
|
|
660
791
|
const entry = createMCPActivationCommandEntry();
|
|
@@ -35,6 +35,12 @@ export function createMCPActivationCommandEntry(): ICommand {
|
|
|
35
35
|
source: 'mcp-activation',
|
|
36
36
|
modelInvocable: true,
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
name: 'reload',
|
|
40
|
+
description: 'Retry connecting every MCP server that is not currently connected',
|
|
41
|
+
source: 'mcp-activation',
|
|
42
|
+
modelInvocable: false,
|
|
43
|
+
},
|
|
38
44
|
...(['approve', 'reject', 'revoke', 'login', 'logout'] as const).map((verb) => ({
|
|
39
45
|
name: verb,
|
|
40
46
|
description: USER_ONLY_VERB_DESCRIPTION[verb],
|