@robota-sdk/agent-command 3.0.0-beta.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/node/index.cjs +30 -0
- package/dist/node/index.d.ts +293 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +31 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +48 -0
- package/src/agent/__tests__/agent-command.test.ts +504 -0
- package/src/agent/agent-command-module.ts +82 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +235 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +255 -0
- package/src/background/background-command-module.ts +53 -0
- package/src/background/background-command.ts +63 -0
- package/src/background/index.ts +6 -0
- package/src/compact/__tests__/compact-command-module.test.ts +162 -0
- package/src/compact/compact-command-module.ts +51 -0
- package/src/compact/compact-command.ts +21 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +294 -0
- package/src/context/context-command-module.ts +54 -0
- package/src/context/context-command.ts +298 -0
- package/src/context/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +35 -0
- package/src/exit/exit-command-module.ts +48 -0
- package/src/exit/exit-command.ts +10 -0
- package/src/exit/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +106 -0
- package/src/help/help-command-module.ts +48 -0
- package/src/help/help-command.ts +9 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +20 -0
- package/src/language/__tests__/language-command-module.test.ts +105 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +56 -0
- package/src/language/language-command.ts +22 -0
- package/src/memory/__tests__/memory-command-module.test.ts +272 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +57 -0
- package/src/memory/memory-command.ts +234 -0
- package/src/mode/__tests__/mode-command-module.test.ts +143 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +56 -0
- package/src/mode/mode-command.ts +34 -0
- package/src/model/__tests__/model-command-module.test.ts +273 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +68 -0
- package/src/model/model-command.ts +40 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +164 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +56 -0
- package/src/permissions/permissions-command.ts +45 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +214 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +81 -0
- package/src/plugin/plugin-command.ts +230 -0
- package/src/provider/__tests__/provider-command-module.test.ts +488 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +43 -0
- package/src/provider/index.ts +30 -0
- package/src/provider/provider-command-execution.ts +150 -0
- package/src/provider/provider-command-module.ts +65 -0
- package/src/provider/provider-command-profile-lifecycle.ts +211 -0
- package/src/provider/provider-command-profile-operations.ts +198 -0
- package/src/provider/provider-command-profile.ts +109 -0
- package/src/provider/provider-command-setup.ts +104 -0
- package/src/provider/provider-setup-flow.ts +309 -0
- package/src/reset/__tests__/reset-command-module.test.ts +63 -0
- package/src/reset/index.ts +2 -0
- package/src/reset/reset-command-module.ts +49 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +215 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +57 -0
- package/src/rewind/rewind-command.ts +184 -0
- package/src/session/__tests__/session-command-module.test.ts +339 -0
- package/src/session/index.ts +17 -0
- package/src/session/session-command-module.ts +168 -0
- package/src/session/session-command.ts +74 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +50 -0
- package/src/skills/__tests__/skills-command-module.test.ts +157 -0
- package/src/skills/index.ts +6 -0
- package/src/skills/skills-command-module.ts +62 -0
- package/src/skills/skills-command.ts +110 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +95 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +56 -0
- package/src/statusline/statusline-command.ts +79 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -0
- package/src/user-local/index.ts +13 -0
- package/src/user-local/user-local-command-constants.ts +5 -0
- package/src/user-local/user-local-command-module.ts +67 -0
- package/src/user-local/user-local-command.ts +205 -0
- package/src/user-local/user-local-memory-command.ts +147 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ICommand,
|
|
3
|
+
ICommandModule,
|
|
4
|
+
ICommandSource,
|
|
5
|
+
ISystemCommand,
|
|
6
|
+
} from '@robota-sdk/agent-framework';
|
|
7
|
+
import {
|
|
8
|
+
BACKGROUND_COMMAND_DESCRIPTION,
|
|
9
|
+
buildBackgroundCommandSubcommands,
|
|
10
|
+
} from '@robota-sdk/agent-framework';
|
|
11
|
+
import { executeBackgroundCommand } from './background-command.js';
|
|
12
|
+
|
|
13
|
+
export function createBackgroundCommandEntry(): ICommand {
|
|
14
|
+
return {
|
|
15
|
+
name: 'background',
|
|
16
|
+
displayName: 'Background Tasks',
|
|
17
|
+
description: BACKGROUND_COMMAND_DESCRIPTION,
|
|
18
|
+
source: 'background',
|
|
19
|
+
modelInvocable: false,
|
|
20
|
+
subcommands: buildBackgroundCommandSubcommands(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createBackgroundSystemCommand(): ISystemCommand {
|
|
25
|
+
const entry = createBackgroundCommandEntry();
|
|
26
|
+
return {
|
|
27
|
+
name: entry.name,
|
|
28
|
+
displayName: entry.displayName,
|
|
29
|
+
description: entry.description,
|
|
30
|
+
requiresPermission: false,
|
|
31
|
+
userInvocable: true,
|
|
32
|
+
modelInvocable: false,
|
|
33
|
+
lifecycle: 'inline',
|
|
34
|
+
subcommands: entry.subcommands,
|
|
35
|
+
execute: executeBackgroundCommand,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class BackgroundCommandSource implements ICommandSource {
|
|
40
|
+
readonly name = 'background';
|
|
41
|
+
|
|
42
|
+
getCommands(): ICommand[] {
|
|
43
|
+
return [createBackgroundCommandEntry()];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createBackgroundCommandModule(): ICommandModule {
|
|
48
|
+
return {
|
|
49
|
+
name: 'agent-command-background',
|
|
50
|
+
commandSources: [new BackgroundCommandSource()],
|
|
51
|
+
systemCommands: [createBackgroundSystemCommand()],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ICommandHostContext, ICommandResult } from '@robota-sdk/agent-framework';
|
|
2
|
+
import {
|
|
3
|
+
BACKGROUND_COMMAND_USAGE,
|
|
4
|
+
cancelCommandBackgroundTask,
|
|
5
|
+
closeCommandBackgroundTask,
|
|
6
|
+
formatCommandBackgroundTaskList,
|
|
7
|
+
listCommandBackgroundTasks,
|
|
8
|
+
parseCommandBackgroundLogCursor,
|
|
9
|
+
readCommandBackgroundTaskLog,
|
|
10
|
+
} from '@robota-sdk/agent-framework';
|
|
11
|
+
|
|
12
|
+
function parseCommandParts(args: string): string[] {
|
|
13
|
+
return args.trim().split(/\s+/).filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function executeBackgroundCommand(
|
|
17
|
+
context: ICommandHostContext,
|
|
18
|
+
args: string,
|
|
19
|
+
): Promise<ICommandResult> {
|
|
20
|
+
const [action = 'list', taskId, ...reasonParts] = parseCommandParts(args);
|
|
21
|
+
if (action === 'list') {
|
|
22
|
+
const tasks = listCommandBackgroundTasks(context);
|
|
23
|
+
return {
|
|
24
|
+
message: formatCommandBackgroundTaskList(tasks),
|
|
25
|
+
success: true,
|
|
26
|
+
data: { count: tasks.length },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!taskId) {
|
|
31
|
+
return {
|
|
32
|
+
message: BACKGROUND_COMMAND_USAGE,
|
|
33
|
+
success: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (action === 'read' || action === 'log' || action === 'open') {
|
|
38
|
+
const page = await readCommandBackgroundTaskLog(
|
|
39
|
+
context,
|
|
40
|
+
taskId,
|
|
41
|
+
parseCommandBackgroundLogCursor(reasonParts[0]),
|
|
42
|
+
);
|
|
43
|
+
const next = page.nextCursor ? `\nNext offset: ${page.nextCursor.offset}` : '';
|
|
44
|
+
return {
|
|
45
|
+
message:
|
|
46
|
+
page.lines.length > 0 ? `${page.lines.join('\n')}${next}` : `No log lines: ${taskId}`,
|
|
47
|
+
success: true,
|
|
48
|
+
data: { taskId, nextOffset: page.nextCursor?.offset },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (action === 'cancel' || action === 'stop') {
|
|
53
|
+
await cancelCommandBackgroundTask(context, taskId, reasonParts.join(' ') || undefined);
|
|
54
|
+
return { message: `Background task cancelled: ${taskId}`, success: true, data: { taskId } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (action === 'close' || action === 'dismiss') {
|
|
58
|
+
await closeCommandBackgroundTask(context, taskId);
|
|
59
|
+
return { message: `Background task closed: ${taskId}`, success: true, data: { taskId } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { message: `Unknown background action: ${action}`, success: false };
|
|
63
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { ICommandHostContext, ICommandSessionRuntime } from '@robota-sdk/agent-framework';
|
|
3
|
+
import { InteractiveSession, SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
4
|
+
import { createCompactCommandModule } from '../compact-command-module.js';
|
|
5
|
+
|
|
6
|
+
type TContextWindowState = ReturnType<ICommandHostContext['getContextState']>;
|
|
7
|
+
type TPermissionMode = ReturnType<ICommandSessionRuntime['getPermissionMode']>;
|
|
8
|
+
|
|
9
|
+
const BEFORE_CONTEXT: TContextWindowState = {
|
|
10
|
+
usedTokens: 80,
|
|
11
|
+
maxTokens: 100,
|
|
12
|
+
usedPercentage: 80,
|
|
13
|
+
remainingPercentage: 20,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const AFTER_CONTEXT: TContextWindowState = {
|
|
17
|
+
usedTokens: 35,
|
|
18
|
+
maxTokens: 100,
|
|
19
|
+
usedPercentage: 35,
|
|
20
|
+
remainingPercentage: 65,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function createRuntime(): ICommandSessionRuntime {
|
|
24
|
+
let mode: TPermissionMode = 'default';
|
|
25
|
+
return {
|
|
26
|
+
clearHistory: vi.fn(),
|
|
27
|
+
compact: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
getContextState: vi.fn().mockReturnValue(AFTER_CONTEXT),
|
|
29
|
+
getPermissionMode: () => mode,
|
|
30
|
+
setPermissionMode: (nextMode) => {
|
|
31
|
+
mode = nextMode;
|
|
32
|
+
},
|
|
33
|
+
getSessionId: () => 'session_1',
|
|
34
|
+
getMessageCount: () => 1,
|
|
35
|
+
getSessionAllowedTools: () => [],
|
|
36
|
+
getAutoCompactThreshold: () => 0.835,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createCommandHostContext(): ICommandHostContext & {
|
|
41
|
+
compactContext: ReturnType<typeof vi.fn>;
|
|
42
|
+
} {
|
|
43
|
+
const runtime = createRuntime();
|
|
44
|
+
const getContextState = vi
|
|
45
|
+
.fn()
|
|
46
|
+
.mockReturnValueOnce(BEFORE_CONTEXT)
|
|
47
|
+
.mockReturnValue(AFTER_CONTEXT);
|
|
48
|
+
return {
|
|
49
|
+
getSession: () => runtime,
|
|
50
|
+
getContextState,
|
|
51
|
+
getAutoCompactThreshold: () => 0.835,
|
|
52
|
+
compactContext: vi.fn().mockResolvedValue(undefined),
|
|
53
|
+
getCwd: () => '/workspace',
|
|
54
|
+
listCommands: () => [],
|
|
55
|
+
listEditCheckpoints: () => [],
|
|
56
|
+
restoreEditCheckpoint: vi.fn(),
|
|
57
|
+
rollbackEditCheckpoint: vi.fn(),
|
|
58
|
+
getUsedMemoryReferences: () => [],
|
|
59
|
+
recordMemoryEvent: vi.fn(),
|
|
60
|
+
listBackgroundTasks: () => [],
|
|
61
|
+
readBackgroundTaskLog: vi.fn().mockResolvedValue({ taskId: 'task_1', lines: [] }),
|
|
62
|
+
cancelBackgroundTask: vi.fn(),
|
|
63
|
+
closeBackgroundTask: vi.fn(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('createCompactCommandModule', () => {
|
|
68
|
+
it('provides compact metadata and a blocking executable command', () => {
|
|
69
|
+
const module = createCompactCommandModule();
|
|
70
|
+
const command = module.systemCommands?.[0];
|
|
71
|
+
const entry = module.commandSources?.[0]?.getCommands()[0];
|
|
72
|
+
|
|
73
|
+
expect(module.name).toBe('agent-command-compact');
|
|
74
|
+
expect(entry).toEqual(
|
|
75
|
+
expect.objectContaining({
|
|
76
|
+
name: 'compact',
|
|
77
|
+
description: 'Compress context window',
|
|
78
|
+
argumentHint: '[instructions]',
|
|
79
|
+
modelInvocable: true,
|
|
80
|
+
safety: 'write',
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
expect(command).toEqual(
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
name: 'compact',
|
|
86
|
+
lifecycle: 'blocking',
|
|
87
|
+
modelInvocable: true,
|
|
88
|
+
safety: 'write',
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('projects compact as a model-invocable descriptor through the command executor', () => {
|
|
94
|
+
const executor = new SystemCommandExecutor([
|
|
95
|
+
...(createCompactCommandModule().systemCommands ?? []),
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
expect(executor.listModelInvocableCommands()).toEqual([
|
|
99
|
+
{
|
|
100
|
+
name: 'compact',
|
|
101
|
+
kind: 'builtin-command',
|
|
102
|
+
description: 'Compress context window',
|
|
103
|
+
userInvocable: true,
|
|
104
|
+
modelInvocable: true,
|
|
105
|
+
argumentHint: '[instructions]',
|
|
106
|
+
safety: 'write',
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('compacts context through the command host facade', async () => {
|
|
112
|
+
const context = createCommandHostContext();
|
|
113
|
+
const executor = new SystemCommandExecutor([
|
|
114
|
+
...(createCompactCommandModule().systemCommands ?? []),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const result = await executor.execute('compact', context, ' focus on tests ');
|
|
118
|
+
|
|
119
|
+
expect(result?.success).toBe(true);
|
|
120
|
+
expect(result?.message).toBe('Context compacted: 80% -> 35%');
|
|
121
|
+
expect(result?.data).toEqual({ before: 80, after: 35 });
|
|
122
|
+
expect(context.compactContext).toHaveBeenCalledWith('focus on tests');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('runs through InteractiveSession foreground lifecycle when composed', async () => {
|
|
126
|
+
let resolveCompact: () => void;
|
|
127
|
+
const runtime = createRuntime();
|
|
128
|
+
const compact = vi.fn(
|
|
129
|
+
() =>
|
|
130
|
+
new Promise<void>((resolve) => {
|
|
131
|
+
resolveCompact = resolve;
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
const session = new InteractiveSession({
|
|
135
|
+
session: {
|
|
136
|
+
...runtime,
|
|
137
|
+
compact,
|
|
138
|
+
} as never,
|
|
139
|
+
commandModules: [createCompactCommandModule()],
|
|
140
|
+
});
|
|
141
|
+
const thinkingStates: boolean[] = [];
|
|
142
|
+
session.on('thinking', (isThinking) => thinkingStates.push(isThinking));
|
|
143
|
+
|
|
144
|
+
const pending = session.executeCommand('compact', 'focus on tests');
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
146
|
+
const blocked = await session.executeCommand('compact', '');
|
|
147
|
+
|
|
148
|
+
expect(session.isExecuting()).toBe(true);
|
|
149
|
+
expect(thinkingStates).toEqual([true]);
|
|
150
|
+
expect(blocked?.success).toBe(false);
|
|
151
|
+
expect(blocked?.message).toContain('already running');
|
|
152
|
+
expect(compact).toHaveBeenCalledWith('focus on tests');
|
|
153
|
+
|
|
154
|
+
resolveCompact!();
|
|
155
|
+
const result = await pending;
|
|
156
|
+
|
|
157
|
+
expect(result?.success).toBe(true);
|
|
158
|
+
expect(result?.message).toContain('Context compacted');
|
|
159
|
+
expect(session.isExecuting()).toBe(false);
|
|
160
|
+
expect(thinkingStates).toEqual([true, false]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ICommand,
|
|
3
|
+
ICommandModule,
|
|
4
|
+
ICommandSource,
|
|
5
|
+
ISystemCommand,
|
|
6
|
+
} from '@robota-sdk/agent-framework';
|
|
7
|
+
import { executeCompactCommand } from './compact-command.js';
|
|
8
|
+
|
|
9
|
+
export function createCompactCommandEntry(): ICommand {
|
|
10
|
+
return {
|
|
11
|
+
name: 'compact',
|
|
12
|
+
displayName: 'Compact Context',
|
|
13
|
+
description: 'Compress context window',
|
|
14
|
+
source: 'compact',
|
|
15
|
+
modelInvocable: true,
|
|
16
|
+
argumentHint: '[instructions]',
|
|
17
|
+
safety: 'write',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createCompactSystemCommand(): ISystemCommand {
|
|
22
|
+
const entry = createCompactCommandEntry();
|
|
23
|
+
return {
|
|
24
|
+
name: entry.name,
|
|
25
|
+
displayName: entry.displayName,
|
|
26
|
+
description: entry.description,
|
|
27
|
+
requiresPermission: false,
|
|
28
|
+
userInvocable: true,
|
|
29
|
+
modelInvocable: entry.modelInvocable,
|
|
30
|
+
argumentHint: entry.argumentHint,
|
|
31
|
+
safety: entry.safety,
|
|
32
|
+
lifecycle: 'blocking',
|
|
33
|
+
execute: executeCompactCommand,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class CompactCommandSource implements ICommandSource {
|
|
38
|
+
readonly name = 'compact';
|
|
39
|
+
|
|
40
|
+
getCommands(): ICommand[] {
|
|
41
|
+
return [createCompactCommandEntry()];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createCompactCommandModule(): ICommandModule {
|
|
46
|
+
return {
|
|
47
|
+
name: 'agent-command-compact',
|
|
48
|
+
commandSources: [new CompactCommandSource()],
|
|
49
|
+
systemCommands: [createCompactSystemCommand()],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ICommandHostContext, ICommandResult } from '@robota-sdk/agent-framework';
|
|
2
|
+
import { compactCommandContext } from '@robota-sdk/agent-framework';
|
|
3
|
+
|
|
4
|
+
function parseInstructions(args: string): string | undefined {
|
|
5
|
+
const instructions = args.trim();
|
|
6
|
+
return instructions.length > 0 ? instructions : undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function executeCompactCommand(
|
|
10
|
+
context: ICommandHostContext,
|
|
11
|
+
args: string,
|
|
12
|
+
): Promise<ICommandResult> {
|
|
13
|
+
const result = await compactCommandContext(context, parseInstructions(args));
|
|
14
|
+
const before = result.before.usedPercentage;
|
|
15
|
+
const after = result.after.usedPercentage;
|
|
16
|
+
return {
|
|
17
|
+
message: `Context compacted: ${Math.round(before)}% -> ${Math.round(after)}%`,
|
|
18
|
+
success: true,
|
|
19
|
+
data: { before, after },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
ICommandHostContext,
|
|
4
|
+
IContextReferenceAddResult,
|
|
5
|
+
IContextReferenceClearResult,
|
|
6
|
+
IContextReferenceItem,
|
|
7
|
+
IContextReferenceRemoveResult,
|
|
8
|
+
ICommandSessionRuntime,
|
|
9
|
+
TAutoCompactThresholdSource,
|
|
10
|
+
} from '@robota-sdk/agent-framework';
|
|
11
|
+
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
12
|
+
import { createContextCommandModule } from '../context-command-module.js';
|
|
13
|
+
|
|
14
|
+
type TContextWindowState = ReturnType<ICommandHostContext['getContextState']>;
|
|
15
|
+
type TPermissionMode = ReturnType<ICommandSessionRuntime['getPermissionMode']>;
|
|
16
|
+
|
|
17
|
+
const CONTEXT_STATE: TContextWindowState = {
|
|
18
|
+
usedTokens: 5000,
|
|
19
|
+
maxTokens: 200000,
|
|
20
|
+
usedPercentage: 2.5,
|
|
21
|
+
remainingPercentage: 97.5,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const MANUAL_REFERENCE: IContextReferenceItem = {
|
|
25
|
+
id: 'manual:AGENTS.md',
|
|
26
|
+
sourcePath: '/workspace/AGENTS.md',
|
|
27
|
+
relativePath: 'AGENTS.md',
|
|
28
|
+
originalReference: '@AGENTS.md',
|
|
29
|
+
loadType: 'manual',
|
|
30
|
+
status: 'active',
|
|
31
|
+
byteLength: 42,
|
|
32
|
+
loadedAt: '2026-05-05T00:00:00.000Z',
|
|
33
|
+
lastUsedAt: '2026-05-05T00:00:00.000Z',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const PROMPT_REFERENCE: IContextReferenceItem = {
|
|
37
|
+
id: 'prompt-reference:packages/agent-sdk/docs/SPEC.md',
|
|
38
|
+
sourcePath: '/workspace/packages/agent-sdk/docs/SPEC.md',
|
|
39
|
+
relativePath: 'packages/agent-sdk/docs/SPEC.md',
|
|
40
|
+
originalReference: '@packages/agent-sdk/docs/SPEC.md',
|
|
41
|
+
loadType: 'prompt-reference',
|
|
42
|
+
status: 'observed',
|
|
43
|
+
byteLength: 200,
|
|
44
|
+
loadedAt: '2026-05-05T00:00:01.000Z',
|
|
45
|
+
lastUsedAt: '2026-05-05T00:00:01.000Z',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function createRuntime(state: { threshold: number | false }): ICommandSessionRuntime {
|
|
49
|
+
let mode: TPermissionMode = 'default';
|
|
50
|
+
return {
|
|
51
|
+
clearHistory: vi.fn(),
|
|
52
|
+
compact: vi.fn().mockResolvedValue(undefined),
|
|
53
|
+
getContextState: () => CONTEXT_STATE,
|
|
54
|
+
getPermissionMode: () => mode,
|
|
55
|
+
setPermissionMode: (nextMode) => {
|
|
56
|
+
mode = nextMode;
|
|
57
|
+
},
|
|
58
|
+
getSessionId: () => 'session_1',
|
|
59
|
+
getMessageCount: () => 1,
|
|
60
|
+
getSessionAllowedTools: () => [],
|
|
61
|
+
getAutoCompactThreshold: () => state.threshold,
|
|
62
|
+
setAutoCompactThreshold: (threshold) => {
|
|
63
|
+
state.threshold = threshold;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createCommandHostContext(threshold: number | false = 0.835): ICommandHostContext & {
|
|
69
|
+
settings: Record<string, number | false>;
|
|
70
|
+
source: TAutoCompactThresholdSource;
|
|
71
|
+
references: IContextReferenceItem[];
|
|
72
|
+
} {
|
|
73
|
+
const state = {
|
|
74
|
+
threshold,
|
|
75
|
+
source: 'settings' as TAutoCompactThresholdSource,
|
|
76
|
+
settings: {} as Record<string, number | false>,
|
|
77
|
+
references: [] as IContextReferenceItem[],
|
|
78
|
+
};
|
|
79
|
+
const runtime = createRuntime(state);
|
|
80
|
+
return {
|
|
81
|
+
get settings() {
|
|
82
|
+
return state.settings;
|
|
83
|
+
},
|
|
84
|
+
get source() {
|
|
85
|
+
return state.source;
|
|
86
|
+
},
|
|
87
|
+
get references() {
|
|
88
|
+
return state.references;
|
|
89
|
+
},
|
|
90
|
+
getSession: () => runtime,
|
|
91
|
+
getContextState: () => CONTEXT_STATE,
|
|
92
|
+
getAutoCompactThreshold: () => state.threshold,
|
|
93
|
+
getAutoCompactThresholdSource: () => state.source,
|
|
94
|
+
setAutoCompactThreshold: (nextThreshold, source = 'session') => {
|
|
95
|
+
state.threshold = nextThreshold;
|
|
96
|
+
state.source = source;
|
|
97
|
+
},
|
|
98
|
+
getCommandHostAdapters: () => ({
|
|
99
|
+
settings: {
|
|
100
|
+
read: () => state.settings,
|
|
101
|
+
write: (settings) => {
|
|
102
|
+
const value = settings.autoCompactThreshold;
|
|
103
|
+
state.settings =
|
|
104
|
+
typeof value === 'number' || value === false ? { autoCompactThreshold: value } : {};
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
compactContext: vi.fn(),
|
|
109
|
+
listContextReferences: () => [...state.references],
|
|
110
|
+
addContextReference: async (path): Promise<IContextReferenceAddResult> => {
|
|
111
|
+
const reference = {
|
|
112
|
+
...MANUAL_REFERENCE,
|
|
113
|
+
relativePath: path,
|
|
114
|
+
sourcePath: `/workspace/${path}`,
|
|
115
|
+
originalReference: `@${path}`,
|
|
116
|
+
};
|
|
117
|
+
state.references = [...state.references, reference];
|
|
118
|
+
return { reference, evicted: [], diagnostics: [] };
|
|
119
|
+
},
|
|
120
|
+
removeContextReference: (path): IContextReferenceRemoveResult => {
|
|
121
|
+
const removed = state.references.find((reference) => reference.relativePath === path);
|
|
122
|
+
state.references = state.references.filter((reference) => reference.relativePath !== path);
|
|
123
|
+
return removed ? { removed } : {};
|
|
124
|
+
},
|
|
125
|
+
clearContextReferences: (): IContextReferenceClearResult => {
|
|
126
|
+
const removed = [...state.references];
|
|
127
|
+
state.references = [];
|
|
128
|
+
return { removed };
|
|
129
|
+
},
|
|
130
|
+
getCwd: () => '/workspace',
|
|
131
|
+
listCommands: () => [],
|
|
132
|
+
listEditCheckpoints: () => [],
|
|
133
|
+
restoreEditCheckpoint: vi.fn(),
|
|
134
|
+
rollbackEditCheckpoint: vi.fn(),
|
|
135
|
+
getUsedMemoryReferences: () => [],
|
|
136
|
+
recordMemoryEvent: vi.fn(),
|
|
137
|
+
listBackgroundTasks: () => [],
|
|
138
|
+
readBackgroundTaskLog: vi.fn().mockResolvedValue({ taskId: 'task_1', lines: [] }),
|
|
139
|
+
cancelBackgroundTask: vi.fn(),
|
|
140
|
+
closeBackgroundTask: vi.fn(),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createExecutor(): SystemCommandExecutor {
|
|
145
|
+
return new SystemCommandExecutor([...(createContextCommandModule().systemCommands ?? [])]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
describe('createContextCommandModule', () => {
|
|
149
|
+
it('provides context metadata and an executable command', () => {
|
|
150
|
+
const module = createContextCommandModule();
|
|
151
|
+
const command = module.systemCommands?.[0];
|
|
152
|
+
const entry = module.commandSources?.[0]?.getCommands()[0];
|
|
153
|
+
|
|
154
|
+
expect(module.name).toBe('agent-command-context');
|
|
155
|
+
expect(entry).toEqual(
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
name: 'context',
|
|
158
|
+
description: 'Context window info, reference inventory, and auto-compact controls',
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
expect(command).toEqual(
|
|
162
|
+
expect.objectContaining({
|
|
163
|
+
name: 'context',
|
|
164
|
+
modelInvocable: false,
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('formats context usage and enabled auto compact policy', async () => {
|
|
170
|
+
const result = await createExecutor().execute('context', createCommandHostContext(0.75), '');
|
|
171
|
+
|
|
172
|
+
expect(result?.success).toBe(true);
|
|
173
|
+
expect(result?.message).toContain('Context: 5,000 / 200,000 tokens (3%)');
|
|
174
|
+
expect(result?.message).toContain('Auto compact: 75% (settings)');
|
|
175
|
+
expect(result?.message).toContain('References: 0 active, 0 observed');
|
|
176
|
+
expect(result?.data).toEqual({
|
|
177
|
+
usedTokens: 5000,
|
|
178
|
+
maxTokens: 200000,
|
|
179
|
+
percentage: 2.5,
|
|
180
|
+
autoCompactThreshold: 0.75,
|
|
181
|
+
autoCompactThresholdSource: 'settings',
|
|
182
|
+
references: [],
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('lists active and observed context references', async () => {
|
|
187
|
+
const context = createCommandHostContext();
|
|
188
|
+
context.references.push(MANUAL_REFERENCE, PROMPT_REFERENCE);
|
|
189
|
+
|
|
190
|
+
const result = await createExecutor().execute('context', context, 'list');
|
|
191
|
+
|
|
192
|
+
expect(result?.success).toBe(true);
|
|
193
|
+
expect(result?.message).toContain('AGENTS.md [manual, active] 42 B');
|
|
194
|
+
expect(result?.message).toContain(
|
|
195
|
+
'packages/agent-sdk/docs/SPEC.md [prompt-reference, observed] 200 B',
|
|
196
|
+
);
|
|
197
|
+
expect(result?.data?.references).toEqual([MANUAL_REFERENCE, PROMPT_REFERENCE]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('adds manual context references through the SDK command API', async () => {
|
|
201
|
+
const context = createCommandHostContext();
|
|
202
|
+
|
|
203
|
+
const result = await createExecutor().execute('context', context, 'add AGENTS.md');
|
|
204
|
+
|
|
205
|
+
expect(result?.success).toBe(true);
|
|
206
|
+
expect(result?.message).toContain('Context reference added: AGENTS.md [manual, active] 42 B.');
|
|
207
|
+
expect(context.references[0]?.relativePath).toBe('AGENTS.md');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('removes context references through the SDK command API', async () => {
|
|
211
|
+
const context = createCommandHostContext();
|
|
212
|
+
context.references.push(MANUAL_REFERENCE);
|
|
213
|
+
|
|
214
|
+
const result = await createExecutor().execute('context', context, 'remove AGENTS.md');
|
|
215
|
+
|
|
216
|
+
expect(result?.success).toBe(true);
|
|
217
|
+
expect(result?.message).toBe('Context reference removed: AGENTS.md [manual, active] 42 B.');
|
|
218
|
+
expect(context.references).toEqual([]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('clears context references through the SDK command API', async () => {
|
|
222
|
+
const context = createCommandHostContext();
|
|
223
|
+
context.references.push(MANUAL_REFERENCE, PROMPT_REFERENCE);
|
|
224
|
+
|
|
225
|
+
const result = await createExecutor().execute('context', context, 'clear');
|
|
226
|
+
|
|
227
|
+
expect(result?.success).toBe(true);
|
|
228
|
+
expect(result?.message).toBe('Context references cleared: 2 removed.');
|
|
229
|
+
expect(context.references).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('formats disabled auto compact policy', async () => {
|
|
233
|
+
const result = await createExecutor().execute('context', createCommandHostContext(false), '');
|
|
234
|
+
|
|
235
|
+
expect(result?.success).toBe(true);
|
|
236
|
+
expect(result?.message).toContain('Auto compact: disabled');
|
|
237
|
+
expect(result?.data?.autoCompactThreshold).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('sets auto compact threshold and persists the setting', async () => {
|
|
241
|
+
const context = createCommandHostContext();
|
|
242
|
+
const result = await createExecutor().execute('context', context, 'auto 85%');
|
|
243
|
+
|
|
244
|
+
expect(result?.success).toBe(true);
|
|
245
|
+
expect(result?.message).toBe('Auto compact threshold set to 85% (settings).');
|
|
246
|
+
expect(context.settings.autoCompactThreshold).toBe(0.85);
|
|
247
|
+
expect(context.source).toBe('settings');
|
|
248
|
+
expect(context.getAutoCompactThreshold()).toBe(0.85);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('disables auto compact and persists false', async () => {
|
|
252
|
+
const context = createCommandHostContext();
|
|
253
|
+
const result = await createExecutor().execute('context', context, 'auto off');
|
|
254
|
+
|
|
255
|
+
expect(result?.success).toBe(true);
|
|
256
|
+
expect(result?.message).toBe('Auto compact disabled (settings).');
|
|
257
|
+
expect(context.settings.autoCompactThreshold).toBe(false);
|
|
258
|
+
expect(context.getAutoCompactThreshold()).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('enables auto compact at the documented default', async () => {
|
|
262
|
+
const context = createCommandHostContext(false);
|
|
263
|
+
const result = await createExecutor().execute('context', context, 'auto on');
|
|
264
|
+
|
|
265
|
+
expect(result?.success).toBe(true);
|
|
266
|
+
expect(result?.message).toBe('Auto compact enabled at 84% (settings).');
|
|
267
|
+
expect(context.settings.autoCompactThreshold).toBe(0.835);
|
|
268
|
+
expect(context.getAutoCompactThreshold()).toBe(0.835);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('resets auto compact to the documented default and removes persisted override', async () => {
|
|
272
|
+
const context = createCommandHostContext(0.5);
|
|
273
|
+
await createExecutor().execute('context', context, 'auto 70%');
|
|
274
|
+
|
|
275
|
+
const result = await createExecutor().execute('context', context, 'auto reset');
|
|
276
|
+
|
|
277
|
+
expect(result?.success).toBe(true);
|
|
278
|
+
expect(result?.message).toBe('Auto compact reset to default: 84% (settings).');
|
|
279
|
+
expect(context.settings).toEqual({});
|
|
280
|
+
expect(context.source).toBe('default');
|
|
281
|
+
expect(context.getAutoCompactThreshold()).toBe(0.835);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('rejects invalid auto compact thresholds', async () => {
|
|
285
|
+
const result = await createExecutor().execute(
|
|
286
|
+
'context',
|
|
287
|
+
createCommandHostContext(),
|
|
288
|
+
'auto 150%',
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(result?.success).toBe(false);
|
|
292
|
+
expect(result?.message).toContain('greater than 0% and at most 100%');
|
|
293
|
+
});
|
|
294
|
+
});
|