@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.
Files changed (51) hide show
  1. package/dist/arkServices.js +12 -18
  2. package/dist/commands/completion/index.js +38 -3
  3. package/dist/commands/evaluation/index.spec.js +1 -6
  4. package/dist/commands/generate/generators/project.js +3 -3
  5. package/dist/commands/generate/generators/team.js +4 -1
  6. package/dist/commands/generate/index.js +2 -2
  7. package/dist/commands/install/index.js +27 -0
  8. package/dist/commands/marketplace/index.d.ts +4 -0
  9. package/dist/commands/marketplace/index.js +50 -0
  10. package/dist/commands/models/create.js +1 -1
  11. package/dist/commands/models/create.spec.js +6 -2
  12. package/dist/commands/models/providers/azure.spec.js +3 -1
  13. package/dist/commands/queries/delete.d.ts +7 -0
  14. package/dist/commands/queries/delete.js +24 -0
  15. package/dist/commands/queries/delete.spec.d.ts +1 -0
  16. package/dist/commands/queries/delete.spec.js +74 -0
  17. package/dist/commands/queries/index.js +42 -4
  18. package/dist/commands/queries/list.d.ts +6 -0
  19. package/dist/commands/queries/list.js +66 -0
  20. package/dist/commands/queries/list.spec.d.ts +1 -0
  21. package/dist/commands/queries/list.spec.js +170 -0
  22. package/dist/commands/queries/validation.d.ts +2 -0
  23. package/dist/commands/queries/validation.js +10 -0
  24. package/dist/commands/queries/validation.spec.d.ts +1 -0
  25. package/dist/commands/queries/validation.spec.js +27 -0
  26. package/dist/commands/query/index.js +2 -0
  27. package/dist/commands/query/index.spec.js +24 -0
  28. package/dist/commands/uninstall/index.js +27 -0
  29. package/dist/components/ChatUI.js +14 -4
  30. package/dist/index.js +2 -0
  31. package/dist/lib/arkApiClient.js +2 -0
  32. package/dist/lib/chatClient.d.ts +4 -0
  33. package/dist/lib/chatClient.js +23 -7
  34. package/dist/lib/chatClient.spec.d.ts +1 -0
  35. package/dist/lib/chatClient.spec.js +108 -0
  36. package/dist/lib/constants.d.ts +3 -0
  37. package/dist/lib/constants.js +8 -0
  38. package/dist/lib/executeQuery.d.ts +1 -4
  39. package/dist/lib/executeQuery.js +103 -104
  40. package/dist/lib/executeQuery.spec.js +218 -99
  41. package/dist/lib/kubectl.d.ts +7 -0
  42. package/dist/lib/kubectl.js +27 -0
  43. package/dist/lib/kubectl.spec.js +89 -1
  44. package/dist/lib/types.d.ts +22 -7
  45. package/dist/marketplaceServices.d.ts +15 -0
  46. package/dist/marketplaceServices.js +51 -0
  47. package/package.json +1 -1
  48. package/templates/models/azure.yaml +1 -1
  49. package/templates/project/Makefile +1 -1
  50. package/templates/project/README.md +1 -1
  51. 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,2 @@
1
+ export declare const UNSUPPORTED_OUTPUT_FORMAT_MESSAGE = "unsupported \"output\" format";
2
+ export declare function assertSupportedOutputFormat(format: string | undefined): void;
@@ -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-expect-error - TerminalRenderer types are incomplete
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-expect-error - preserveNewlines exists but not in types
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));
@@ -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,
@@ -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;
@@ -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
- if (onChunk) {
34
- onChunk(content, undefined, arkMetadata);
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,3 @@
1
+ export declare const QUERY_ANNOTATIONS: {
2
+ readonly A2A_CONTEXT_ID: "ark.mckinsey.com/a2a-context-id";
3
+ };
@@ -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"