@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,180 @@
|
|
|
1
|
+
import type { TBackgroundTaskIsolation } from '@robota-sdk/agent-framework';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_AGENT_TYPE = 'general-purpose';
|
|
4
|
+
|
|
5
|
+
export type TAgentMode = 'background';
|
|
6
|
+
|
|
7
|
+
export interface IAgentRunRequest {
|
|
8
|
+
readonly agentType: string;
|
|
9
|
+
readonly label: string;
|
|
10
|
+
readonly mode: TAgentMode;
|
|
11
|
+
readonly prompt: string;
|
|
12
|
+
readonly model?: string;
|
|
13
|
+
readonly isolation?: TBackgroundTaskIsolation;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface IParsedAgentOptions {
|
|
17
|
+
readonly agentType?: string;
|
|
18
|
+
readonly model?: string;
|
|
19
|
+
readonly isolation?: TBackgroundTaskIsolation;
|
|
20
|
+
readonly positional: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function tokenizeArgs(args: string): string[] {
|
|
24
|
+
const tokens: string[] = [];
|
|
25
|
+
let current = '';
|
|
26
|
+
let quote: '"' | "'" | undefined;
|
|
27
|
+
let escaped = false;
|
|
28
|
+
|
|
29
|
+
for (const char of args) {
|
|
30
|
+
if (escaped) {
|
|
31
|
+
current += char;
|
|
32
|
+
escaped = false;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (quote && char === '\\') {
|
|
36
|
+
escaped = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (quote) {
|
|
40
|
+
if (char === quote) quote = undefined;
|
|
41
|
+
else current += char;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === '"' || char === "'") {
|
|
45
|
+
quote = char;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (/\s/.test(char)) {
|
|
49
|
+
if (current.length > 0) {
|
|
50
|
+
tokens.push(current);
|
|
51
|
+
current = '';
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
current += char;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (current.length > 0) tokens.push(current);
|
|
59
|
+
return tokens;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseOptions(tokens: readonly string[]): IParsedAgentOptions {
|
|
63
|
+
const positional: string[] = [];
|
|
64
|
+
let agentType: string | undefined;
|
|
65
|
+
let model: string | undefined;
|
|
66
|
+
let isolation: TBackgroundTaskIsolation | undefined;
|
|
67
|
+
|
|
68
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
69
|
+
const token = tokens[index];
|
|
70
|
+
if (token === '--background') {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (token === '--agent' || token === '--type' || token === '-a') {
|
|
74
|
+
agentType = tokens[index + 1];
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (token === '--model') {
|
|
79
|
+
model = tokens[index + 1];
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (token === '--isolation') {
|
|
84
|
+
const value = tokens[index + 1];
|
|
85
|
+
if (value === 'none' || value === 'worktree') isolation = value;
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (token !== undefined) positional.push(token);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
positional,
|
|
94
|
+
...(agentType ? { agentType } : {}),
|
|
95
|
+
...(model ? { model } : {}),
|
|
96
|
+
...(isolation ? { isolation } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createRequest(
|
|
101
|
+
options: IParsedAgentOptions,
|
|
102
|
+
agentType: string,
|
|
103
|
+
label: string,
|
|
104
|
+
prompt: string,
|
|
105
|
+
): IAgentRunRequest {
|
|
106
|
+
return {
|
|
107
|
+
agentType,
|
|
108
|
+
label,
|
|
109
|
+
mode: 'background',
|
|
110
|
+
prompt,
|
|
111
|
+
...(options.model ? { model: options.model } : {}),
|
|
112
|
+
...(options.isolation ? { isolation: options.isolation } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseRunRequest(
|
|
117
|
+
tokens: readonly string[],
|
|
118
|
+
availableAgentNames: ReadonlySet<string>,
|
|
119
|
+
): IAgentRunRequest | undefined {
|
|
120
|
+
const options = parseOptions(tokens);
|
|
121
|
+
const [first, ...rest] = options.positional;
|
|
122
|
+
let agentType = options.agentType ?? DEFAULT_AGENT_TYPE;
|
|
123
|
+
let promptParts = options.positional;
|
|
124
|
+
|
|
125
|
+
if (!options.agentType && first && availableAgentNames.has(first)) {
|
|
126
|
+
agentType = first;
|
|
127
|
+
promptParts = rest;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const prompt = promptParts.join(' ').trim();
|
|
131
|
+
if (!prompt) return undefined;
|
|
132
|
+
return createRequest(options, agentType, agentType, prompt);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseParallelRequests(
|
|
136
|
+
tokens: readonly string[],
|
|
137
|
+
availableAgentNames: ReadonlySet<string>,
|
|
138
|
+
): IAgentRunRequest[] {
|
|
139
|
+
const options = parseOptions(tokens);
|
|
140
|
+
return options.positional
|
|
141
|
+
.map((token) => parseAgentJobToken(token, options, availableAgentNames))
|
|
142
|
+
.filter((job): job is IAgentRunRequest => job !== undefined);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseAgentJobToken(
|
|
146
|
+
token: string,
|
|
147
|
+
options: IParsedAgentOptions,
|
|
148
|
+
availableAgentNames: ReadonlySet<string>,
|
|
149
|
+
): IAgentRunRequest | undefined {
|
|
150
|
+
const equalsIndex = token.indexOf('=');
|
|
151
|
+
if (equalsIndex > 0) {
|
|
152
|
+
const label = token.slice(0, equalsIndex);
|
|
153
|
+
const spec = token.slice(equalsIndex + 1);
|
|
154
|
+
return parseAgentPromptSpec(label, spec, options, availableAgentNames);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const colonIndex = token.indexOf(':');
|
|
158
|
+
if (colonIndex <= 0 || colonIndex === token.length - 1) return undefined;
|
|
159
|
+
const head = token.slice(0, colonIndex);
|
|
160
|
+
const prompt = token.slice(colonIndex + 1);
|
|
161
|
+
const agentType =
|
|
162
|
+
options.agentType ?? (availableAgentNames.has(head) ? head : DEFAULT_AGENT_TYPE);
|
|
163
|
+
return createRequest(options, agentType, head, prompt);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseAgentPromptSpec(
|
|
167
|
+
label: string,
|
|
168
|
+
spec: string,
|
|
169
|
+
options: IParsedAgentOptions,
|
|
170
|
+
availableAgentNames: ReadonlySet<string>,
|
|
171
|
+
): IAgentRunRequest | undefined {
|
|
172
|
+
const colonIndex = spec.indexOf(':');
|
|
173
|
+
if (colonIndex === -1) {
|
|
174
|
+
const agentType =
|
|
175
|
+
options.agentType ?? (availableAgentNames.has(label) ? label : DEFAULT_AGENT_TYPE);
|
|
176
|
+
return createRequest(options, agentType, label, spec);
|
|
177
|
+
}
|
|
178
|
+
if (colonIndex === 0 || colonIndex === spec.length - 1) return undefined;
|
|
179
|
+
return createRequest(options, spec.slice(0, colonIndex), label, spec.slice(colonIndex + 1));
|
|
180
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { summarizeBackgroundJobGroup } from '@robota-sdk/agent-framework';
|
|
2
|
+
import type {
|
|
3
|
+
IAgentJobHostContext,
|
|
4
|
+
ICommandResult,
|
|
5
|
+
ISubagentJobState,
|
|
6
|
+
} from '@robota-sdk/agent-framework';
|
|
7
|
+
import { parseParallelRequests, parseRunRequest, tokenizeArgs } from './agent-command-parser.js';
|
|
8
|
+
import type { IAgentRunRequest } from './agent-command-parser.js';
|
|
9
|
+
|
|
10
|
+
function formatError<TError>(error: TError): string {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getAvailableAgentNames(session: IAgentJobHostContext): ReadonlySet<string> {
|
|
15
|
+
return new Set(session.listAgentDefinitions().map((agent) => agent.name));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateAgentType(
|
|
19
|
+
session: IAgentJobHostContext,
|
|
20
|
+
agentType: string,
|
|
21
|
+
): ICommandResult | undefined {
|
|
22
|
+
const agents = session.listAgentDefinitions();
|
|
23
|
+
if (agents.some((agent) => agent.name === agentType)) return undefined;
|
|
24
|
+
return {
|
|
25
|
+
message: `Unknown agent type: ${agentType}\nAvailable agents: ${agents.map((agent) => agent.name).join(', ')}`,
|
|
26
|
+
success: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function spawnAgentJob(
|
|
31
|
+
session: IAgentJobHostContext,
|
|
32
|
+
request: IAgentRunRequest,
|
|
33
|
+
): Promise<ICommandResult | { state: ISubagentJobState }> {
|
|
34
|
+
const invalid = validateAgentType(session, request.agentType);
|
|
35
|
+
if (invalid) return invalid;
|
|
36
|
+
try {
|
|
37
|
+
return { state: await session.spawnAgentJob(request) };
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return { message: formatError(error), success: false };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function executeOpenSwitcher(): ICommandResult {
|
|
44
|
+
return { message: '', effects: [{ type: 'agent-switcher-requested' }], success: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function executeList(session: IAgentJobHostContext): Promise<ICommandResult> {
|
|
48
|
+
const agents = session.listAgentDefinitions();
|
|
49
|
+
const jobs = session.listAgentJobs();
|
|
50
|
+
const lines = [
|
|
51
|
+
'Available agents:',
|
|
52
|
+
...agents.map((agent) => ` ${agent.name} - ${agent.description}`),
|
|
53
|
+
'',
|
|
54
|
+
jobs.length === 0 ? 'No active agent jobs.' : 'Agent jobs:',
|
|
55
|
+
...jobs.map((job) => ` ${formatAgentJobLine(job)}`),
|
|
56
|
+
];
|
|
57
|
+
return {
|
|
58
|
+
message: lines.join('\n'),
|
|
59
|
+
success: true,
|
|
60
|
+
data: { agents: agents.length, jobs: jobs.length },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatAgentJobLine(job: ISubagentJobState): string {
|
|
65
|
+
const worktree = [
|
|
66
|
+
job.worktreePath ? `worktree=${job.worktreePath}` : undefined,
|
|
67
|
+
job.branchName ? `branch=${job.branchName}` : undefined,
|
|
68
|
+
].filter((segment): segment is string => segment !== undefined);
|
|
69
|
+
const metadata = worktree.length > 0 ? ` ${worktree.join(' ')}` : '';
|
|
70
|
+
return `${job.id} [${job.status}${metadata}] ${job.label} - ${job.promptPreview}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function executeRun(
|
|
74
|
+
session: IAgentJobHostContext,
|
|
75
|
+
tokens: readonly string[],
|
|
76
|
+
): Promise<ICommandResult> {
|
|
77
|
+
const request = parseRunRequest(tokens, getAvailableAgentNames(session));
|
|
78
|
+
if (!request) {
|
|
79
|
+
return {
|
|
80
|
+
message: 'Usage: agent run [AGENT_NAME] [--agent AGENT_NAME] PROMPT',
|
|
81
|
+
success: false,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const spawned = await spawnAgentJob(session, request);
|
|
86
|
+
if ('success' in spawned) return spawned;
|
|
87
|
+
const { state } = spawned;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
message: `Started agent job: ${state.id}`,
|
|
91
|
+
success: true,
|
|
92
|
+
data: { agentId: state.id, status: state.status },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function executeParallel(
|
|
97
|
+
session: IAgentJobHostContext,
|
|
98
|
+
tokens: readonly string[],
|
|
99
|
+
): Promise<ICommandResult> {
|
|
100
|
+
const wait = tokens.includes('--wait') || !tokens.includes('--detach');
|
|
101
|
+
const commandTokens = tokens.filter((token) => token !== '--wait' && token !== '--detach');
|
|
102
|
+
const jobs = parseParallelRequests(commandTokens, getAvailableAgentNames(session));
|
|
103
|
+
|
|
104
|
+
if (jobs.length === 0) {
|
|
105
|
+
return {
|
|
106
|
+
message: 'Usage: agent parallel [--wait|--detach] LABEL:"PROMPT" [LABEL=AGENT_NAME:"PROMPT"]',
|
|
107
|
+
success: false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const invalid = jobs
|
|
112
|
+
.map((job) => validateAgentType(session, job.agentType))
|
|
113
|
+
.find((result): result is ICommandResult => result !== undefined);
|
|
114
|
+
if (invalid) return invalid;
|
|
115
|
+
|
|
116
|
+
let states: ISubagentJobState[];
|
|
117
|
+
try {
|
|
118
|
+
states = await Promise.all(jobs.map((job) => session.spawnAgentJob(job)));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return { message: formatError(error), success: false };
|
|
121
|
+
}
|
|
122
|
+
const group = session.createBackgroundJobGroup({
|
|
123
|
+
waitPolicy: 'wait_all',
|
|
124
|
+
taskIds: states.map((state) => state.id),
|
|
125
|
+
label: 'agent parallel',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (wait) {
|
|
129
|
+
const completed = await session.waitBackgroundJobGroup(group.id);
|
|
130
|
+
const summary = summarizeBackgroundJobGroup(completed);
|
|
131
|
+
return {
|
|
132
|
+
message: formatGroupSummary(summary),
|
|
133
|
+
success: true,
|
|
134
|
+
data: { agentIds: states.map((state) => state.id), groupId: group.id, summary },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
message: ['Started agent jobs:', ...states.map((state) => `${state.label}: ${state.id}`)].join(
|
|
140
|
+
'\n',
|
|
141
|
+
),
|
|
142
|
+
success: true,
|
|
143
|
+
data: { agentIds: states.map((state) => state.id), groupId: group.id },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function executeWait(
|
|
148
|
+
session: IAgentJobHostContext,
|
|
149
|
+
tokens: readonly string[],
|
|
150
|
+
): Promise<ICommandResult> {
|
|
151
|
+
const [groupId] = tokens;
|
|
152
|
+
if (!groupId) return { message: 'Usage: agent wait GROUP_ID', success: false };
|
|
153
|
+
const completed = await session.waitBackgroundJobGroup(groupId);
|
|
154
|
+
const summary = summarizeBackgroundJobGroup(completed);
|
|
155
|
+
return {
|
|
156
|
+
message: formatGroupSummary(summary),
|
|
157
|
+
success: true,
|
|
158
|
+
data: { groupId, summary },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function executeRead(
|
|
163
|
+
session: IAgentJobHostContext,
|
|
164
|
+
tokens: readonly string[],
|
|
165
|
+
): Promise<ICommandResult> {
|
|
166
|
+
const [agentId, offset] = tokens;
|
|
167
|
+
if (!agentId) return { message: 'Usage: agent read AGENT_ID [OFFSET]', success: false };
|
|
168
|
+
const cursor = offset ? { offset: Number.parseInt(offset, 10) } : undefined;
|
|
169
|
+
const page = await session.readBackgroundTaskLog(agentId, cursor);
|
|
170
|
+
const next = page.nextCursor ? `\nNext offset: ${page.nextCursor.offset}` : '';
|
|
171
|
+
return {
|
|
172
|
+
message: page.lines.length > 0 ? `${page.lines.join('\n')}${next}` : `No log lines: ${agentId}`,
|
|
173
|
+
success: true,
|
|
174
|
+
data: { agentId, nextOffset: page.nextCursor?.offset },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function executeSend(
|
|
179
|
+
session: IAgentJobHostContext,
|
|
180
|
+
tokens: readonly string[],
|
|
181
|
+
): Promise<ICommandResult> {
|
|
182
|
+
const [agentId, ...promptParts] = tokens;
|
|
183
|
+
const prompt = promptParts.join(' ').trim();
|
|
184
|
+
if (!agentId || !prompt) {
|
|
185
|
+
return { message: 'Usage: agent send AGENT_ID PROMPT', success: false };
|
|
186
|
+
}
|
|
187
|
+
await session.sendAgentJob(agentId, prompt);
|
|
188
|
+
return { message: `Sent input to agent job: ${agentId}`, success: true, data: { agentId } };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function executeStop(
|
|
192
|
+
session: IAgentJobHostContext,
|
|
193
|
+
tokens: readonly string[],
|
|
194
|
+
): Promise<ICommandResult> {
|
|
195
|
+
const [agentId, ...reasonParts] = tokens;
|
|
196
|
+
if (!agentId) return { message: 'Usage: agent stop AGENT_ID [REASON]', success: false };
|
|
197
|
+
await session.cancelAgentJob(agentId, reasonParts.join(' ') || undefined);
|
|
198
|
+
return { message: `Agent job stopped: ${agentId}`, success: true, data: { agentId } };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function executeClose(
|
|
202
|
+
session: IAgentJobHostContext,
|
|
203
|
+
tokens: readonly string[],
|
|
204
|
+
): Promise<ICommandResult> {
|
|
205
|
+
const [agentId] = tokens;
|
|
206
|
+
if (!agentId) return { message: 'Usage: agent close AGENT_ID', success: false };
|
|
207
|
+
await session.closeAgentJob(agentId);
|
|
208
|
+
return { message: `Agent job closed: ${agentId}`, success: true, data: { agentId } };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function executeAgentCommand(
|
|
212
|
+
session: IAgentJobHostContext,
|
|
213
|
+
args: string,
|
|
214
|
+
): Promise<ICommandResult> {
|
|
215
|
+
try {
|
|
216
|
+
if (args.trim() === '') return executeOpenSwitcher();
|
|
217
|
+
const [action = 'list', ...tokens] = tokenizeArgs(args);
|
|
218
|
+
if (action === 'list' && tokens.length === 0) return executeList(session);
|
|
219
|
+
if (action === 'run') return executeRun(session, tokens);
|
|
220
|
+
if (action === 'parallel') return executeParallel(session, tokens);
|
|
221
|
+
if (action === 'wait') return executeWait(session, tokens);
|
|
222
|
+
if (action === 'read' || action === 'open') return executeRead(session, tokens);
|
|
223
|
+
if (action === 'send') return executeSend(session, tokens);
|
|
224
|
+
if (action === 'stop' || action === 'cancel') return executeStop(session, tokens);
|
|
225
|
+
if (action === 'close') return executeClose(session, tokens);
|
|
226
|
+
return executeRun(session, [action, ...tokens]);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return { message: formatError(error), success: false };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatGroupSummary(summary: ReturnType<typeof summarizeBackgroundJobGroup>): string {
|
|
233
|
+
const header = `Background job group ${summary.groupId}: ${summary.status} (${summary.completed}/${summary.total} completed, ${summary.failed} failed, ${summary.cancelled} cancelled, ${summary.pending} pending)`;
|
|
234
|
+
return [header, ...summary.lines].join('\n');
|
|
235
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
IBackgroundTaskState,
|
|
4
|
+
ICommandHostContext,
|
|
5
|
+
ICommandSessionRuntime,
|
|
6
|
+
} from '@robota-sdk/agent-framework';
|
|
7
|
+
import {
|
|
8
|
+
BackgroundCommandSource,
|
|
9
|
+
createBackgroundCommandEntry,
|
|
10
|
+
createBackgroundCommandModule,
|
|
11
|
+
executeBackgroundCommand,
|
|
12
|
+
} from '../index.js';
|
|
13
|
+
|
|
14
|
+
function createSessionRuntime(): ICommandSessionRuntime {
|
|
15
|
+
return {
|
|
16
|
+
clearHistory: () => undefined,
|
|
17
|
+
compact: async () => undefined,
|
|
18
|
+
getContextState: () => ({
|
|
19
|
+
maxTokens: 100,
|
|
20
|
+
usedTokens: 10,
|
|
21
|
+
usedPercentage: 10,
|
|
22
|
+
remainingPercentage: 90,
|
|
23
|
+
}),
|
|
24
|
+
getPermissionMode: () => 'default',
|
|
25
|
+
setPermissionMode: () => undefined,
|
|
26
|
+
getSessionId: () => 'session_1',
|
|
27
|
+
getMessageCount: () => 0,
|
|
28
|
+
getSessionAllowedTools: () => [],
|
|
29
|
+
getAutoCompactThreshold: () => false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createTask(overrides?: Partial<IBackgroundTaskState>): IBackgroundTaskState {
|
|
34
|
+
return {
|
|
35
|
+
id: 'agent_1',
|
|
36
|
+
kind: 'agent',
|
|
37
|
+
label: 'Explore',
|
|
38
|
+
status: 'running',
|
|
39
|
+
mode: 'background',
|
|
40
|
+
parentSessionId: 'session_parent',
|
|
41
|
+
depth: 1,
|
|
42
|
+
cwd: '/workspace',
|
|
43
|
+
updatedAt: '2026-04-30T00:00:00.000Z',
|
|
44
|
+
lastActivityAt: '2026-04-30T00:00:01.000Z',
|
|
45
|
+
unread: false,
|
|
46
|
+
promptPreview: 'Find files',
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createCommandHostContext(overrides?: Partial<ICommandHostContext>): ICommandHostContext {
|
|
52
|
+
return {
|
|
53
|
+
getSession: () => createSessionRuntime(),
|
|
54
|
+
getContextState: () => ({
|
|
55
|
+
maxTokens: 100,
|
|
56
|
+
usedTokens: 10,
|
|
57
|
+
usedPercentage: 10,
|
|
58
|
+
remainingPercentage: 90,
|
|
59
|
+
}),
|
|
60
|
+
getAutoCompactThreshold: () => 0.8,
|
|
61
|
+
compactContext: async () => undefined,
|
|
62
|
+
getCwd: () => '/workspace',
|
|
63
|
+
listEditCheckpoints: () => [],
|
|
64
|
+
restoreEditCheckpoint: async () => ({
|
|
65
|
+
target: {
|
|
66
|
+
id: 'checkpoint_1',
|
|
67
|
+
sessionId: 'session_1',
|
|
68
|
+
sequence: 1,
|
|
69
|
+
prompt: 'edit',
|
|
70
|
+
createdAt: '2026-05-03T00:00:00.000Z',
|
|
71
|
+
fileCount: 0,
|
|
72
|
+
},
|
|
73
|
+
restoredCheckpointCount: 1,
|
|
74
|
+
restoredFileCount: 0,
|
|
75
|
+
removedCheckpointCount: 0,
|
|
76
|
+
}),
|
|
77
|
+
rollbackEditCheckpoint: async () => ({
|
|
78
|
+
target: {
|
|
79
|
+
id: 'checkpoint_1',
|
|
80
|
+
sessionId: 'session_1',
|
|
81
|
+
sequence: 1,
|
|
82
|
+
prompt: 'edit',
|
|
83
|
+
createdAt: '2026-05-03T00:00:00.000Z',
|
|
84
|
+
fileCount: 0,
|
|
85
|
+
},
|
|
86
|
+
restoredCheckpointCount: 1,
|
|
87
|
+
restoredFileCount: 0,
|
|
88
|
+
removedCheckpointCount: 0,
|
|
89
|
+
}),
|
|
90
|
+
getUsedMemoryReferences: () => [],
|
|
91
|
+
recordMemoryEvent: () => undefined,
|
|
92
|
+
listBackgroundTasks: () => [],
|
|
93
|
+
readBackgroundTaskLog: async (taskId) => ({ taskId, lines: [] }),
|
|
94
|
+
cancelBackgroundTask: async () => undefined,
|
|
95
|
+
closeBackgroundTask: async () => undefined,
|
|
96
|
+
...overrides,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe('background command module', () => {
|
|
101
|
+
it('provides command metadata and executable registration from one module', () => {
|
|
102
|
+
const entry = createBackgroundCommandEntry();
|
|
103
|
+
const module = createBackgroundCommandModule();
|
|
104
|
+
|
|
105
|
+
expect(entry).toMatchObject({
|
|
106
|
+
name: 'background',
|
|
107
|
+
description: 'List and control background tasks',
|
|
108
|
+
source: 'background',
|
|
109
|
+
modelInvocable: false,
|
|
110
|
+
});
|
|
111
|
+
expect(entry.subcommands?.map((command) => command.name)).toEqual([
|
|
112
|
+
'list',
|
|
113
|
+
'read',
|
|
114
|
+
'cancel',
|
|
115
|
+
'close',
|
|
116
|
+
]);
|
|
117
|
+
expect(new BackgroundCommandSource().getCommands()).toEqual([entry]);
|
|
118
|
+
expect(module.systemCommands?.map((command) => command.name)).toEqual(['background']);
|
|
119
|
+
expect(module.commandSources?.flatMap((source) => source.getCommands())).toEqual([entry]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('lists background tasks', async () => {
|
|
123
|
+
const context = createCommandHostContext({
|
|
124
|
+
listBackgroundTasks: vi.fn().mockReturnValue([createTask()]),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = await executeBackgroundCommand(context, 'list');
|
|
128
|
+
|
|
129
|
+
expect(result.success).toBe(true);
|
|
130
|
+
expect(result.message).toContain(
|
|
131
|
+
'agent_1 [running lastActivityAt=2026-04-30T00:00:01.000Z] agent:Explore',
|
|
132
|
+
);
|
|
133
|
+
expect(result.message).toContain('Find files');
|
|
134
|
+
expect(result.data?.count).toBe(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('lists preserved worktree handoff metadata', async () => {
|
|
138
|
+
const context = createCommandHostContext({
|
|
139
|
+
listBackgroundTasks: vi.fn().mockReturnValue([
|
|
140
|
+
createTask({
|
|
141
|
+
status: 'completed',
|
|
142
|
+
worktreePath: '/workspace/.robota/worktrees/agent_1',
|
|
143
|
+
branchName: 'robota/agent_1',
|
|
144
|
+
worktreeStatus: ' M changed.ts\n?? new.ts',
|
|
145
|
+
worktreeNextAction: 'Review /workspace/.robota/worktrees/agent_1.',
|
|
146
|
+
}),
|
|
147
|
+
]),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await executeBackgroundCommand(context, 'list');
|
|
151
|
+
|
|
152
|
+
expect(result.success).toBe(true);
|
|
153
|
+
expect(result.message).toContain('worktree=/workspace/.robota/worktrees/agent_1');
|
|
154
|
+
expect(result.message).toContain('branch=robota/agent_1');
|
|
155
|
+
expect(result.message).toContain('worktreeStatus="M changed.ts ?? new.ts"');
|
|
156
|
+
expect(result.message).toContain('next="Review /workspace/.robota/worktrees/agent_1."');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('defaults to list when no action is provided', async () => {
|
|
160
|
+
const context = createCommandHostContext({
|
|
161
|
+
listBackgroundTasks: vi.fn().mockReturnValue([]),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = await executeBackgroundCommand(context, '');
|
|
165
|
+
|
|
166
|
+
expect(result).toEqual({
|
|
167
|
+
message: 'No background tasks.',
|
|
168
|
+
success: true,
|
|
169
|
+
data: { count: 0 },
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('reads a background task log page with cursor parsing', async () => {
|
|
174
|
+
const readBackgroundTaskLog = vi.fn().mockResolvedValue({
|
|
175
|
+
taskId: 'process_1',
|
|
176
|
+
nextCursor: { offset: 200 },
|
|
177
|
+
lines: ['[stdout] hello'],
|
|
178
|
+
});
|
|
179
|
+
const context = createCommandHostContext({ readBackgroundTaskLog });
|
|
180
|
+
|
|
181
|
+
const result = await executeBackgroundCommand(context, 'read process_1 0');
|
|
182
|
+
|
|
183
|
+
expect(readBackgroundTaskLog).toHaveBeenCalledWith('process_1', { offset: 0 });
|
|
184
|
+
expect(result.success).toBe(true);
|
|
185
|
+
expect(result.message).toContain('[stdout] hello');
|
|
186
|
+
expect(result.message).toContain('Next offset: 200');
|
|
187
|
+
expect(result.data).toEqual({ taskId: 'process_1', nextOffset: 200 });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('supports read aliases and omits invalid cursors', async () => {
|
|
191
|
+
const readBackgroundTaskLog = vi.fn().mockResolvedValue({
|
|
192
|
+
taskId: 'process_1',
|
|
193
|
+
lines: [],
|
|
194
|
+
});
|
|
195
|
+
const context = createCommandHostContext({ readBackgroundTaskLog });
|
|
196
|
+
|
|
197
|
+
const result = await executeBackgroundCommand(context, 'open process_1 nope');
|
|
198
|
+
|
|
199
|
+
expect(readBackgroundTaskLog).toHaveBeenCalledWith('process_1', undefined);
|
|
200
|
+
expect(result.message).toBe('No log lines: process_1');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('cancels a background task with optional reason', async () => {
|
|
204
|
+
const cancelBackgroundTask = vi.fn().mockResolvedValue(undefined);
|
|
205
|
+
const context = createCommandHostContext({ cancelBackgroundTask });
|
|
206
|
+
|
|
207
|
+
const result = await executeBackgroundCommand(context, 'cancel agent_1 no longer needed');
|
|
208
|
+
|
|
209
|
+
expect(cancelBackgroundTask).toHaveBeenCalledWith('agent_1', 'no longer needed');
|
|
210
|
+
expect(result).toEqual({
|
|
211
|
+
message: 'Background task cancelled: agent_1',
|
|
212
|
+
success: true,
|
|
213
|
+
data: { taskId: 'agent_1' },
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('supports cancel aliases without a reason', async () => {
|
|
218
|
+
const cancelBackgroundTask = vi.fn().mockResolvedValue(undefined);
|
|
219
|
+
const context = createCommandHostContext({ cancelBackgroundTask });
|
|
220
|
+
|
|
221
|
+
await executeBackgroundCommand(context, 'stop agent_1');
|
|
222
|
+
|
|
223
|
+
expect(cancelBackgroundTask).toHaveBeenCalledWith('agent_1', undefined);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('closes a background task', async () => {
|
|
227
|
+
const closeBackgroundTask = vi.fn().mockResolvedValue(undefined);
|
|
228
|
+
const context = createCommandHostContext({ closeBackgroundTask });
|
|
229
|
+
|
|
230
|
+
const result = await executeBackgroundCommand(context, 'dismiss agent_1');
|
|
231
|
+
|
|
232
|
+
expect(closeBackgroundTask).toHaveBeenCalledWith('agent_1');
|
|
233
|
+
expect(result).toEqual({
|
|
234
|
+
message: 'Background task closed: agent_1',
|
|
235
|
+
success: true,
|
|
236
|
+
data: { taskId: 'agent_1' },
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('returns usage when a task action omits task id', async () => {
|
|
241
|
+
const result = await executeBackgroundCommand(createCommandHostContext(), 'cancel');
|
|
242
|
+
|
|
243
|
+
expect(result.success).toBe(false);
|
|
244
|
+
expect(result.message).toContain('Usage: background list');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('rejects unknown actions', async () => {
|
|
248
|
+
const result = await executeBackgroundCommand(createCommandHostContext(), 'pause agent_1');
|
|
249
|
+
|
|
250
|
+
expect(result).toEqual({
|
|
251
|
+
message: 'Unknown background action: pause',
|
|
252
|
+
success: false,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|