@agents-at-scale/ark 0.1.42 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/arkServices.js +12 -18
- package/dist/commands/completion/index.js +38 -3
- package/dist/commands/evaluation/index.spec.js +1 -6
- package/dist/commands/generate/generators/project.js +3 -3
- package/dist/commands/generate/generators/team.js +4 -1
- package/dist/commands/generate/index.js +2 -2
- package/dist/commands/install/index.js +27 -0
- package/dist/commands/marketplace/index.d.ts +4 -0
- package/dist/commands/marketplace/index.js +50 -0
- package/dist/commands/models/create.js +1 -1
- package/dist/commands/models/create.spec.js +6 -2
- package/dist/commands/models/providers/azure.spec.js +3 -1
- package/dist/commands/queries/delete.d.ts +7 -0
- package/dist/commands/queries/delete.js +24 -0
- package/dist/commands/queries/delete.spec.d.ts +1 -0
- package/dist/commands/queries/delete.spec.js +74 -0
- package/dist/commands/queries/index.js +42 -4
- package/dist/commands/queries/list.d.ts +6 -0
- package/dist/commands/queries/list.js +66 -0
- package/dist/commands/queries/list.spec.d.ts +1 -0
- package/dist/commands/queries/list.spec.js +170 -0
- package/dist/commands/queries/validation.d.ts +2 -0
- package/dist/commands/queries/validation.js +10 -0
- package/dist/commands/queries/validation.spec.d.ts +1 -0
- package/dist/commands/queries/validation.spec.js +27 -0
- package/dist/commands/query/index.js +2 -0
- package/dist/commands/query/index.spec.js +24 -0
- package/dist/commands/uninstall/index.js +27 -0
- package/dist/components/ChatUI.js +14 -4
- package/dist/index.js +2 -0
- package/dist/lib/arkApiClient.js +2 -0
- package/dist/lib/chatClient.d.ts +4 -0
- package/dist/lib/chatClient.js +23 -7
- package/dist/lib/chatClient.spec.d.ts +1 -0
- package/dist/lib/chatClient.spec.js +108 -0
- package/dist/lib/constants.d.ts +3 -0
- package/dist/lib/constants.js +8 -0
- package/dist/lib/executeQuery.d.ts +1 -4
- package/dist/lib/executeQuery.js +103 -104
- package/dist/lib/executeQuery.spec.js +218 -99
- package/dist/lib/kubectl.d.ts +7 -0
- package/dist/lib/kubectl.js +27 -0
- package/dist/lib/kubectl.spec.js +89 -1
- package/dist/lib/types.d.ts +22 -7
- package/dist/marketplaceServices.d.ts +15 -0
- package/dist/marketplaceServices.js +51 -0
- package/package.json +1 -1
- package/templates/models/azure.yaml +1 -1
- package/templates/project/Makefile +1 -1
- package/templates/project/README.md +1 -1
- package/templates/project/scripts/setup.sh +2 -2
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { UNSUPPORTED_OUTPUT_FORMAT_MESSAGE } from './validation.js';
|
|
3
|
+
import output from '../../lib/output.js';
|
|
4
|
+
const mockExeca = jest.fn();
|
|
5
|
+
jest.unstable_mockModule('execa', () => ({
|
|
6
|
+
execa: mockExeca,
|
|
7
|
+
}));
|
|
8
|
+
const { createQueriesCommand } = await import('./index.js');
|
|
9
|
+
describe('queries list command', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
console.log = jest.fn();
|
|
13
|
+
jest.spyOn(output, 'warning').mockImplementation(() => { });
|
|
14
|
+
jest.spyOn(output, 'error').mockImplementation(() => { });
|
|
15
|
+
jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
16
|
+
});
|
|
17
|
+
it('should list all queries in text format by default', async () => {
|
|
18
|
+
const mockQueries = [
|
|
19
|
+
{
|
|
20
|
+
metadata: {
|
|
21
|
+
name: 'query-1',
|
|
22
|
+
creationTimestamp: '2024-01-01T00:00:00Z',
|
|
23
|
+
},
|
|
24
|
+
status: {
|
|
25
|
+
phase: 'done',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
metadata: {
|
|
30
|
+
name: 'query-2',
|
|
31
|
+
creationTimestamp: '2024-01-02T00:00:00Z',
|
|
32
|
+
},
|
|
33
|
+
status: {
|
|
34
|
+
phase: 'running',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
mockExeca.mockResolvedValue({
|
|
39
|
+
stdout: JSON.stringify({ items: mockQueries }),
|
|
40
|
+
});
|
|
41
|
+
const command = createQueriesCommand({});
|
|
42
|
+
await command.parseAsync(['node', 'test', 'list']);
|
|
43
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/NAME.*STATUS/));
|
|
44
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/query-1/));
|
|
45
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/query-2/));
|
|
46
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', '-o', 'json'], { stdio: 'pipe' });
|
|
47
|
+
});
|
|
48
|
+
it('should list all queries in JSON format', async () => {
|
|
49
|
+
const mockQueries = [
|
|
50
|
+
{
|
|
51
|
+
metadata: {
|
|
52
|
+
name: 'query-1',
|
|
53
|
+
creationTimestamp: '2024-01-01T00:00:00Z',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
metadata: {
|
|
58
|
+
name: 'query-2',
|
|
59
|
+
creationTimestamp: '2024-01-02T00:00:00Z',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
mockExeca.mockResolvedValue({
|
|
64
|
+
stdout: JSON.stringify({ items: mockQueries }),
|
|
65
|
+
});
|
|
66
|
+
const command = createQueriesCommand({});
|
|
67
|
+
await command.parseAsync(['node', 'test', '--output', 'json']);
|
|
68
|
+
expect(console.log).toHaveBeenCalledWith(JSON.stringify(mockQueries, null, 2));
|
|
69
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', '-o', 'json'], { stdio: 'pipe' });
|
|
70
|
+
});
|
|
71
|
+
it('should support sorting by creation timestamp', async () => {
|
|
72
|
+
const mockQueries = [
|
|
73
|
+
{
|
|
74
|
+
metadata: {
|
|
75
|
+
name: 'query-1',
|
|
76
|
+
creationTimestamp: '2024-01-01T00:00:00Z',
|
|
77
|
+
},
|
|
78
|
+
status: {
|
|
79
|
+
phase: 'done',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
metadata: {
|
|
84
|
+
name: 'query-2',
|
|
85
|
+
creationTimestamp: '2024-01-02T00:00:00Z',
|
|
86
|
+
},
|
|
87
|
+
status: {
|
|
88
|
+
phase: 'running',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
mockExeca.mockResolvedValue({
|
|
93
|
+
stdout: JSON.stringify({ items: mockQueries }),
|
|
94
|
+
});
|
|
95
|
+
const command = createQueriesCommand({});
|
|
96
|
+
await command.parseAsync([
|
|
97
|
+
'node',
|
|
98
|
+
'test',
|
|
99
|
+
'--sort-by',
|
|
100
|
+
'.metadata.creationTimestamp',
|
|
101
|
+
]);
|
|
102
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/NAME.*STATUS/));
|
|
103
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/query-1/));
|
|
104
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/query-2/));
|
|
105
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', '--sort-by=.metadata.creationTimestamp', '-o', 'json'], { stdio: 'pipe' });
|
|
106
|
+
});
|
|
107
|
+
it('should display warning when no queries exist', async () => {
|
|
108
|
+
mockExeca.mockResolvedValue({
|
|
109
|
+
stdout: JSON.stringify({ items: [] }),
|
|
110
|
+
});
|
|
111
|
+
const command = createQueriesCommand({});
|
|
112
|
+
await command.parseAsync(['node', 'test', 'list']);
|
|
113
|
+
expect(output.warning).toHaveBeenCalledWith('no queries available');
|
|
114
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'queries', '-o', 'json'], { stdio: 'pipe' });
|
|
115
|
+
});
|
|
116
|
+
it('should handle errors when listing queries', async () => {
|
|
117
|
+
mockExeca.mockRejectedValue(new Error('kubectl connection failed'));
|
|
118
|
+
const command = createQueriesCommand({});
|
|
119
|
+
await command.parseAsync(['node', 'test', 'list']);
|
|
120
|
+
expect(output.error).toHaveBeenCalled();
|
|
121
|
+
expect(process.exit).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
it('should handle invalid output format gracefully', async () => {
|
|
124
|
+
const mockQueries = [
|
|
125
|
+
{
|
|
126
|
+
metadata: {
|
|
127
|
+
name: 'query-1',
|
|
128
|
+
creationTimestamp: '2024-01-01T00:00:00Z',
|
|
129
|
+
},
|
|
130
|
+
status: {
|
|
131
|
+
phase: 'done',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
mockExeca.mockResolvedValue({
|
|
136
|
+
stdout: JSON.stringify({ items: mockQueries }),
|
|
137
|
+
});
|
|
138
|
+
const command = createQueriesCommand({});
|
|
139
|
+
await command.parseAsync(['node', 'test', '--output', 'xml']);
|
|
140
|
+
expect(output.error).toHaveBeenCalledWith(expect.anything(), expect.stringMatching(UNSUPPORTED_OUTPUT_FORMAT_MESSAGE));
|
|
141
|
+
expect(mockExeca).not.toHaveBeenCalled();
|
|
142
|
+
expect(console.log).not.toHaveBeenCalledWith(expect.stringMatching(/query-1/));
|
|
143
|
+
expect(process.exit).toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
it('should list many queries without truncation', async () => {
|
|
146
|
+
// Create 100 mock queries
|
|
147
|
+
const mockQueries = Array.from({ length: 100 }, (_, i) => ({
|
|
148
|
+
metadata: {
|
|
149
|
+
name: `query-${i + 1}`,
|
|
150
|
+
creationTimestamp: new Date(2024, 0, i + 1).toISOString(),
|
|
151
|
+
},
|
|
152
|
+
status: {
|
|
153
|
+
phase: i % 3 === 0 ? 'done' : i % 2 === 0 ? 'running' : 'initializing',
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
mockExeca.mockResolvedValue({
|
|
157
|
+
stdout: JSON.stringify({ items: mockQueries }),
|
|
158
|
+
});
|
|
159
|
+
const command = createQueriesCommand({});
|
|
160
|
+
await command.parseAsync(['node', 'test', 'list']);
|
|
161
|
+
// Check for header and separator
|
|
162
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/NAME.*STATUS/));
|
|
163
|
+
// Verify all queries are logged
|
|
164
|
+
for (let i = 1; i <= 100; i++) {
|
|
165
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`query-${i}`)));
|
|
166
|
+
}
|
|
167
|
+
// Verify console.log was called: header + 100 queries
|
|
168
|
+
expect(console.log).toHaveBeenCalledTimes(101);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { InvalidArgumentError } from 'commander';
|
|
2
|
+
const SUPPORTED_OUTPUT_FORMATS = ['json', 'text'];
|
|
3
|
+
export const UNSUPPORTED_OUTPUT_FORMAT_MESSAGE = `unsupported "output" format`;
|
|
4
|
+
const VALID_OUTPUT_FORMATS_MESSAGE = `valid formats are: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}`;
|
|
5
|
+
export function assertSupportedOutputFormat(format) {
|
|
6
|
+
if (format && !SUPPORTED_OUTPUT_FORMATS.includes(format)) {
|
|
7
|
+
const message = `${UNSUPPORTED_OUTPUT_FORMAT_MESSAGE}: "${format}". ${VALID_OUTPUT_FORMATS_MESSAGE}`;
|
|
8
|
+
throw new InvalidArgumentError(message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { InvalidArgumentError } from 'commander';
|
|
3
|
+
import { assertSupportedOutputFormat, UNSUPPORTED_OUTPUT_FORMAT_MESSAGE, } from './validation.js';
|
|
4
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
5
|
+
describe('queries validation', () => {
|
|
6
|
+
describe('assertSupportedOutputFormat', () => {
|
|
7
|
+
it('should not throw for supported formats', () => {
|
|
8
|
+
expect(() => assertSupportedOutputFormat('json')).not.toThrow();
|
|
9
|
+
expect(() => assertSupportedOutputFormat('text')).not.toThrow();
|
|
10
|
+
});
|
|
11
|
+
it('should not throw when format is undefined', () => {
|
|
12
|
+
expect(() => assertSupportedOutputFormat(undefined)).not.toThrow();
|
|
13
|
+
});
|
|
14
|
+
it('should throw InvalidArgumentError for unsupported format', () => {
|
|
15
|
+
expect(() => assertSupportedOutputFormat('xml')).toThrow(InvalidArgumentError);
|
|
16
|
+
});
|
|
17
|
+
it('should include format and supported formats in error message', () => {
|
|
18
|
+
expect(() => assertSupportedOutputFormat('xml')).toThrow(UNSUPPORTED_OUTPUT_FORMAT_MESSAGE);
|
|
19
|
+
});
|
|
20
|
+
it('should work with various invalid formats', () => {
|
|
21
|
+
const invalidFormats = ['yaml', 'csv', 'html', 'pdf'];
|
|
22
|
+
for (const format of invalidFormats) {
|
|
23
|
+
expect(() => assertSupportedOutputFormat(format)).toThrow(InvalidArgumentError);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -9,6 +9,7 @@ export function createQueryCommand(_) {
|
|
|
9
9
|
.argument('<target>', 'Query target (e.g., model/default, agent/my-agent)')
|
|
10
10
|
.argument('<message>', 'Message to send')
|
|
11
11
|
.option('-o, --output <format>', 'Output format: yaml, json, or name (prints only resource name)')
|
|
12
|
+
.option('--session-id <sessionId>', 'Session ID to associate with the query for conversation continuity')
|
|
12
13
|
.action(async (target, message, options) => {
|
|
13
14
|
const parsed = parseTarget(target);
|
|
14
15
|
if (!parsed) {
|
|
@@ -20,6 +21,7 @@ export function createQueryCommand(_) {
|
|
|
20
21
|
targetName: parsed.name,
|
|
21
22
|
message,
|
|
22
23
|
outputFormat: options.output,
|
|
24
|
+
sessionId: options.sessionId,
|
|
23
25
|
});
|
|
24
26
|
});
|
|
25
27
|
return queryCommand;
|
|
@@ -68,6 +68,30 @@ describe('createQueryCommand', () => {
|
|
|
68
68
|
outputFormat: 'json',
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
|
+
it('should pass session-id option to executeQuery', async () => {
|
|
72
|
+
mockParseTarget.mockReturnValue({
|
|
73
|
+
type: 'agent',
|
|
74
|
+
name: 'test-agent',
|
|
75
|
+
});
|
|
76
|
+
mockExecuteQuery.mockResolvedValue(undefined);
|
|
77
|
+
const command = createQueryCommand({});
|
|
78
|
+
await command.parseAsync([
|
|
79
|
+
'node',
|
|
80
|
+
'test',
|
|
81
|
+
'agent/test-agent',
|
|
82
|
+
'Hello world',
|
|
83
|
+
'--session-id',
|
|
84
|
+
'my-session-123',
|
|
85
|
+
]);
|
|
86
|
+
expect(mockParseTarget).toHaveBeenCalledWith('agent/test-agent');
|
|
87
|
+
expect(mockExecuteQuery).toHaveBeenCalledWith({
|
|
88
|
+
targetType: 'agent',
|
|
89
|
+
targetName: 'test-agent',
|
|
90
|
+
message: 'Hello world',
|
|
91
|
+
outputFormat: undefined,
|
|
92
|
+
sessionId: 'my-session-123',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
71
95
|
it('should error on invalid target format', async () => {
|
|
72
96
|
mockParseTarget.mockReturnValue(null);
|
|
73
97
|
const command = createQueryCommand({});
|
|
@@ -5,6 +5,7 @@ import inquirer from 'inquirer';
|
|
|
5
5
|
import { showNoClusterError } from '../../lib/startup.js';
|
|
6
6
|
import output from '../../lib/output.js';
|
|
7
7
|
import { getInstallableServices } from '../../arkServices.js';
|
|
8
|
+
import { isMarketplaceService, extractMarketplaceServiceName, getMarketplaceService, getAllMarketplaceServices, } from '../../marketplaceServices.js';
|
|
8
9
|
async function uninstallService(service, verbose = false) {
|
|
9
10
|
const helmArgs = ['uninstall', service.helmReleaseName, '--ignore-not-found'];
|
|
10
11
|
// Only add namespace flag if service has explicit namespace
|
|
@@ -25,6 +26,32 @@ async function uninstallArk(config, serviceName, options = {}) {
|
|
|
25
26
|
console.log(); // Add blank line after cluster info
|
|
26
27
|
// If a specific service is requested, uninstall only that service
|
|
27
28
|
if (serviceName) {
|
|
29
|
+
// Check if it's a marketplace service
|
|
30
|
+
if (isMarketplaceService(serviceName)) {
|
|
31
|
+
const marketplaceServiceName = extractMarketplaceServiceName(serviceName);
|
|
32
|
+
const service = getMarketplaceService(marketplaceServiceName);
|
|
33
|
+
if (!service) {
|
|
34
|
+
output.error(`marketplace service '${marketplaceServiceName}' not found`);
|
|
35
|
+
output.info('available marketplace services:');
|
|
36
|
+
const marketplaceServices = getAllMarketplaceServices();
|
|
37
|
+
for (const serviceName of Object.keys(marketplaceServices)) {
|
|
38
|
+
output.info(` marketplace/services/${serviceName}`);
|
|
39
|
+
}
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
output.info(`uninstalling marketplace service ${service.name}...`);
|
|
43
|
+
try {
|
|
44
|
+
await uninstallService(service, options.verbose);
|
|
45
|
+
output.success(`${service.name} uninstalled successfully`);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
output.error(`failed to uninstall ${service.name}`);
|
|
49
|
+
console.error(error);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Core ARK service
|
|
28
55
|
const services = getInstallableServices();
|
|
29
56
|
const service = Object.values(services).find((s) => s.name === serviceName);
|
|
30
57
|
if (!service) {
|
|
@@ -18,12 +18,12 @@ const generateMessageId = () => {
|
|
|
18
18
|
// Configure marked with terminal renderer for markdown output
|
|
19
19
|
const configureMarkdown = () => {
|
|
20
20
|
marked.setOptions({
|
|
21
|
-
// @ts-
|
|
21
|
+
// @ts-ignore - TerminalRenderer types are incomplete
|
|
22
22
|
renderer: new TerminalRenderer({
|
|
23
23
|
showSectionPrefix: false,
|
|
24
24
|
width: 80,
|
|
25
25
|
reflowText: true,
|
|
26
|
-
// @ts-
|
|
26
|
+
// @ts-ignore - preserveNewlines exists but not in types
|
|
27
27
|
preserveNewlines: true,
|
|
28
28
|
}),
|
|
29
29
|
});
|
|
@@ -61,6 +61,8 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
|
|
|
61
61
|
streamingEnabled: config?.chat?.streaming ?? true,
|
|
62
62
|
currentTarget: undefined,
|
|
63
63
|
});
|
|
64
|
+
// Track A2A context ID for conversation continuity using ref
|
|
65
|
+
const a2aContextIdRef = React.useRef(undefined);
|
|
64
66
|
React.useEffect(() => {
|
|
65
67
|
if (showAgentSelector && agents.length === 0) {
|
|
66
68
|
setSelectorLoading(true);
|
|
@@ -330,11 +332,13 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
|
|
|
330
332
|
if (value.startsWith('/reset')) {
|
|
331
333
|
// Clear all messages
|
|
332
334
|
setMessages([]);
|
|
335
|
+
// Clear A2A context ID
|
|
336
|
+
a2aContextIdRef.current = undefined;
|
|
333
337
|
// Add system message to show the reset
|
|
334
338
|
const systemMessage = {
|
|
335
339
|
id: generateMessageId(),
|
|
336
340
|
type: 'system',
|
|
337
|
-
content: 'Message history cleared',
|
|
341
|
+
content: 'Message history and A2A context cleared',
|
|
338
342
|
timestamp: new Date(),
|
|
339
343
|
command: '/reset',
|
|
340
344
|
};
|
|
@@ -462,7 +466,13 @@ const ChatUI = ({ initialTargetId, arkApiClient, arkApiProxy, config, }) => {
|
|
|
462
466
|
setMessages((prev) => [...prev, agentMessage]);
|
|
463
467
|
}
|
|
464
468
|
// Send message and get response with abort signal
|
|
465
|
-
const fullResponse = await chatClientRef.current.sendMessage(target.id, apiMessages, chatConfig, (chunk, toolCalls, arkMetadata) => {
|
|
469
|
+
const fullResponse = await chatClientRef.current.sendMessage(target.id, apiMessages, { ...chatConfig, a2aContextId: a2aContextIdRef.current }, (chunk, toolCalls, arkMetadata) => {
|
|
470
|
+
// Extract A2A context ID from first response
|
|
471
|
+
// Chat TUI always queries a single target, so contextId is in responses[0]
|
|
472
|
+
if (arkMetadata?.completedQuery?.status?.responses?.[0]?.a2a?.contextId) {
|
|
473
|
+
a2aContextIdRef.current =
|
|
474
|
+
arkMetadata.completedQuery.status.responses[0].a2a.contextId;
|
|
475
|
+
}
|
|
466
476
|
// Update message progressively as chunks arrive
|
|
467
477
|
setMessages((prev) => {
|
|
468
478
|
const newMessages = [...prev];
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createDocsCommand } from './commands/docs/index.js';
|
|
|
16
16
|
import { createEvaluationCommand } from './commands/evaluation/index.js';
|
|
17
17
|
import { createGenerateCommand } from './commands/generate/index.js';
|
|
18
18
|
import { createInstallCommand } from './commands/install/index.js';
|
|
19
|
+
import { createMarketplaceCommand } from './commands/marketplace/index.js';
|
|
19
20
|
import { createMemoryCommand } from './commands/memory/index.js';
|
|
20
21
|
import { createModelsCommand } from './commands/models/index.js';
|
|
21
22
|
import { createQueryCommand } from './commands/query/index.js';
|
|
@@ -49,6 +50,7 @@ async function main() {
|
|
|
49
50
|
program.addCommand(createEvaluationCommand(config));
|
|
50
51
|
program.addCommand(createGenerateCommand(config));
|
|
51
52
|
program.addCommand(createInstallCommand(config));
|
|
53
|
+
program.addCommand(createMarketplaceCommand(config));
|
|
52
54
|
program.addCommand(createMemoryCommand(config));
|
|
53
55
|
program.addCommand(createModelsCommand(config));
|
|
54
56
|
program.addCommand(createQueryCommand(config));
|
package/dist/lib/arkApiClient.js
CHANGED
|
@@ -146,6 +146,8 @@ export class ArkApiClient {
|
|
|
146
146
|
}));
|
|
147
147
|
}
|
|
148
148
|
async *createChatCompletionStream(params) {
|
|
149
|
+
// Errors from OpenAI SDK will automatically propagate with proper error messages
|
|
150
|
+
// and kill the CLI, so no try/catch needed here
|
|
149
151
|
const stream = await this.openai.chat.completions.create({
|
|
150
152
|
...params,
|
|
151
153
|
stream: true,
|
package/dist/lib/chatClient.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { ArkApiClient, QueryTarget } from './arkApiClient.js';
|
|
2
|
+
import type { Query } from './types.js';
|
|
2
3
|
export { QueryTarget };
|
|
3
4
|
export interface ChatConfig {
|
|
4
5
|
streamingEnabled: boolean;
|
|
5
6
|
currentTarget?: QueryTarget;
|
|
7
|
+
a2aContextId?: string;
|
|
8
|
+
sessionId?: string;
|
|
6
9
|
}
|
|
7
10
|
export interface ToolCall {
|
|
8
11
|
id: string;
|
|
@@ -18,6 +21,7 @@ export interface ArkMetadata {
|
|
|
18
21
|
model?: string;
|
|
19
22
|
query?: string;
|
|
20
23
|
target?: string;
|
|
24
|
+
completedQuery?: Query;
|
|
21
25
|
}
|
|
22
26
|
export declare class ChatClient {
|
|
23
27
|
private arkApiClient;
|
package/dist/lib/chatClient.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { QUERY_ANNOTATIONS } from './constants.js';
|
|
1
2
|
export class ChatClient {
|
|
2
3
|
constructor(arkApiClient) {
|
|
3
4
|
this.arkApiClient = arkApiClient;
|
|
@@ -15,6 +16,21 @@ export class ChatClient {
|
|
|
15
16
|
messages: messages,
|
|
16
17
|
signal: signal,
|
|
17
18
|
};
|
|
19
|
+
// Build metadata object - only add if we have something to include
|
|
20
|
+
if (config.sessionId || config.a2aContextId) {
|
|
21
|
+
params.metadata = {};
|
|
22
|
+
// Add sessionId directly to metadata (goes to spec, not annotations)
|
|
23
|
+
if (config.sessionId) {
|
|
24
|
+
params.metadata.sessionId = config.sessionId;
|
|
25
|
+
}
|
|
26
|
+
// Add A2A context ID to queryAnnotations (goes to annotations)
|
|
27
|
+
if (config.a2aContextId) {
|
|
28
|
+
const queryAnnotations = {
|
|
29
|
+
[QUERY_ANNOTATIONS.A2A_CONTEXT_ID]: config.a2aContextId,
|
|
30
|
+
};
|
|
31
|
+
params.metadata.queryAnnotations = JSON.stringify(queryAnnotations);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
18
34
|
if (shouldStream) {
|
|
19
35
|
let fullResponse = '';
|
|
20
36
|
const toolCallsById = new Map();
|
|
@@ -23,16 +39,15 @@ export class ChatClient {
|
|
|
23
39
|
if (signal?.aborted) {
|
|
24
40
|
break;
|
|
25
41
|
}
|
|
26
|
-
const delta = chunk.choices[0]?.delta;
|
|
42
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
27
43
|
// Extract ARK metadata if present
|
|
28
44
|
const arkMetadata = chunk.ark;
|
|
29
|
-
// Handle regular content
|
|
30
45
|
const content = delta?.content || '';
|
|
31
46
|
if (content) {
|
|
32
47
|
fullResponse += content;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
}
|
|
49
|
+
if (onChunk) {
|
|
50
|
+
onChunk(content, undefined, arkMetadata);
|
|
36
51
|
}
|
|
37
52
|
// Handle tool calls
|
|
38
53
|
if (delta?.tool_calls) {
|
|
@@ -68,6 +83,7 @@ export class ChatClient {
|
|
|
68
83
|
const response = await this.arkApiClient.createChatCompletion(params);
|
|
69
84
|
const message = response.choices[0]?.message;
|
|
70
85
|
const content = message?.content || '';
|
|
86
|
+
const arkMetadata = response.ark;
|
|
71
87
|
// Handle tool calls in non-streaming mode
|
|
72
88
|
if (message?.tool_calls && message.tool_calls.length > 0) {
|
|
73
89
|
const toolCalls = message.tool_calls.map((tc) => ({
|
|
@@ -80,12 +96,12 @@ export class ChatClient {
|
|
|
80
96
|
}));
|
|
81
97
|
// Send tool calls first
|
|
82
98
|
if (onChunk) {
|
|
83
|
-
onChunk('', toolCalls);
|
|
99
|
+
onChunk('', toolCalls, arkMetadata);
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
// Send content after tool calls
|
|
87
103
|
if (content && onChunk) {
|
|
88
|
-
onChunk(content);
|
|
104
|
+
onChunk(content, undefined, arkMetadata);
|
|
89
105
|
}
|
|
90
106
|
return content;
|
|
91
107
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { QUERY_ANNOTATIONS } from './constants.js';
|
|
3
|
+
const mockCreateChatCompletion = jest.fn();
|
|
4
|
+
const mockArkApiClient = {
|
|
5
|
+
createChatCompletion: mockCreateChatCompletion,
|
|
6
|
+
createChatCompletionStream: jest.fn(),
|
|
7
|
+
getQueryTargets: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
const { ChatClient } = await import('./chatClient.js');
|
|
10
|
+
describe('ChatClient', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
describe('sendMessage', () => {
|
|
15
|
+
it('should include sessionId directly in metadata when provided', async () => {
|
|
16
|
+
const client = new ChatClient(mockArkApiClient);
|
|
17
|
+
mockCreateChatCompletion.mockResolvedValue({
|
|
18
|
+
id: 'test-id',
|
|
19
|
+
object: 'chat.completion',
|
|
20
|
+
created: 1234567890,
|
|
21
|
+
model: 'test-model',
|
|
22
|
+
choices: [
|
|
23
|
+
{
|
|
24
|
+
index: 0,
|
|
25
|
+
message: { role: 'assistant', content: 'Hello' },
|
|
26
|
+
finish_reason: 'stop',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
usage: {
|
|
30
|
+
prompt_tokens: 10,
|
|
31
|
+
completion_tokens: 5,
|
|
32
|
+
total_tokens: 15,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
await client.sendMessage('agent/test-agent', [{ role: 'user', content: 'Hello' }], { streamingEnabled: false, sessionId: 'test-session-123' });
|
|
36
|
+
expect(mockCreateChatCompletion).toHaveBeenCalledWith(expect.objectContaining({
|
|
37
|
+
model: 'agent/test-agent',
|
|
38
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
39
|
+
metadata: {
|
|
40
|
+
sessionId: 'test-session-123',
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
});
|
|
44
|
+
it('should include both sessionId in metadata and a2aContextId in queryAnnotations when both provided', async () => {
|
|
45
|
+
const client = new ChatClient(mockArkApiClient);
|
|
46
|
+
mockCreateChatCompletion.mockResolvedValue({
|
|
47
|
+
id: 'test-id',
|
|
48
|
+
object: 'chat.completion',
|
|
49
|
+
created: 1234567890,
|
|
50
|
+
model: 'test-model',
|
|
51
|
+
choices: [
|
|
52
|
+
{
|
|
53
|
+
index: 0,
|
|
54
|
+
message: { role: 'assistant', content: 'Hello' },
|
|
55
|
+
finish_reason: 'stop',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
usage: {
|
|
59
|
+
prompt_tokens: 10,
|
|
60
|
+
completion_tokens: 5,
|
|
61
|
+
total_tokens: 15,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
await client.sendMessage('agent/test-agent', [{ role: 'user', content: 'Hello' }], {
|
|
65
|
+
streamingEnabled: false,
|
|
66
|
+
sessionId: 'test-session-123',
|
|
67
|
+
a2aContextId: 'a2a-context-456',
|
|
68
|
+
});
|
|
69
|
+
expect(mockCreateChatCompletion).toHaveBeenCalled();
|
|
70
|
+
const callArgs = mockCreateChatCompletion.mock.calls[0][0];
|
|
71
|
+
expect(callArgs.model).toBe('agent/test-agent');
|
|
72
|
+
expect(callArgs.messages).toEqual([{ role: 'user', content: 'Hello' }]);
|
|
73
|
+
expect(callArgs.metadata).toBeDefined();
|
|
74
|
+
expect(callArgs.metadata.sessionId).toBe('test-session-123');
|
|
75
|
+
expect(callArgs.metadata.queryAnnotations).toBeDefined();
|
|
76
|
+
const queryAnnotations = JSON.parse(callArgs.metadata.queryAnnotations);
|
|
77
|
+
expect(queryAnnotations[QUERY_ANNOTATIONS.A2A_CONTEXT_ID]).toBe('a2a-context-456');
|
|
78
|
+
});
|
|
79
|
+
it('should not include metadata when neither sessionId nor a2aContextId is provided', async () => {
|
|
80
|
+
const client = new ChatClient(mockArkApiClient);
|
|
81
|
+
mockCreateChatCompletion.mockResolvedValue({
|
|
82
|
+
id: 'test-id',
|
|
83
|
+
object: 'chat.completion',
|
|
84
|
+
created: 1234567890,
|
|
85
|
+
model: 'test-model',
|
|
86
|
+
choices: [
|
|
87
|
+
{
|
|
88
|
+
index: 0,
|
|
89
|
+
message: { role: 'assistant', content: 'Hello' },
|
|
90
|
+
finish_reason: 'stop',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
usage: {
|
|
94
|
+
prompt_tokens: 10,
|
|
95
|
+
completion_tokens: 5,
|
|
96
|
+
total_tokens: 15,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
await client.sendMessage('agent/test-agent', [{ role: 'user', content: 'Hello' }], { streamingEnabled: false });
|
|
100
|
+
expect(mockCreateChatCompletion).toHaveBeenCalledWith(expect.objectContaining({
|
|
101
|
+
model: 'agent/test-agent',
|
|
102
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
103
|
+
}));
|
|
104
|
+
const callArgs = mockCreateChatCompletion.mock.calls[0];
|
|
105
|
+
expect(callArgs[0].metadata).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// ARK annotation prefix - mirrors ark/internal/annotations/annotations.go
|
|
2
|
+
const ARK_PREFIX = 'ark.mckinsey.com/';
|
|
3
|
+
// Query annotation constants for metadata.queryAnnotations
|
|
4
|
+
// Note: sessionId is passed directly in metadata, not in queryAnnotations
|
|
5
|
+
export const QUERY_ANNOTATIONS = {
|
|
6
|
+
// A2A context ID annotation (goes to K8s annotations)
|
|
7
|
+
A2A_CONTEXT_ID: `${ARK_PREFIX}a2a-context-id`,
|
|
8
|
+
};
|
|
@@ -10,11 +10,8 @@ export interface QueryOptions {
|
|
|
10
10
|
watchTimeout?: string;
|
|
11
11
|
verbose?: boolean;
|
|
12
12
|
outputFormat?: string;
|
|
13
|
+
sessionId?: string;
|
|
13
14
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Execute a query against any ARK target (model, agent, team)
|
|
16
|
-
* This is the shared implementation used by all query commands
|
|
17
|
-
*/
|
|
18
15
|
export declare function executeQuery(options: QueryOptions): Promise<void>;
|
|
19
16
|
/**
|
|
20
17
|
* Parse a target string like "model/default" or "agent/weather"
|