@artemiskit/core 0.6.0 → 0.6.1

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 (37) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +12 -0
  3. package/dist/adapters/types.d.ts +7 -0
  4. package/dist/adapters/types.d.ts.map +1 -1
  5. package/dist/agent-workflow/catalog.d.ts +2 -1
  6. package/dist/agent-workflow/catalog.d.ts.map +1 -1
  7. package/dist/agent-workflow/environment.d.ts +47 -0
  8. package/dist/agent-workflow/environment.d.ts.map +1 -0
  9. package/dist/agent-workflow/index.d.ts +4 -1
  10. package/dist/agent-workflow/index.d.ts.map +1 -1
  11. package/dist/agent-workflow/parser.d.ts +1 -1
  12. package/dist/agent-workflow/parser.d.ts.map +1 -1
  13. package/dist/agent-workflow/sandbox-fixtures/qualify.d.ts +2 -0
  14. package/dist/agent-workflow/sandbox-fixtures/qualify.d.ts.map +1 -0
  15. package/dist/agent-workflow/sandbox.d.ts +12 -0
  16. package/dist/agent-workflow/sandbox.d.ts.map +1 -0
  17. package/dist/agent-workflow/schema.d.ts +117 -7
  18. package/dist/agent-workflow/schema.d.ts.map +1 -1
  19. package/dist/agent-workflow/session.d.ts +113 -0
  20. package/dist/agent-workflow/session.d.ts.map +1 -0
  21. package/dist/agent-workflow/target.d.ts +20 -7
  22. package/dist/agent-workflow/target.d.ts.map +1 -1
  23. package/dist/index.js +1366 -21
  24. package/package.json +1 -1
  25. package/src/adapters/types.ts +7 -0
  26. package/src/agent-workflow/catalog.ts +4 -3
  27. package/src/agent-workflow/environment.ts +207 -0
  28. package/src/agent-workflow/index.ts +5 -1
  29. package/src/agent-workflow/parser.ts +1 -1
  30. package/src/agent-workflow/sandbox-fixtures/qualify.ts +305 -0
  31. package/src/agent-workflow/sandbox.test.ts +117 -0
  32. package/src/agent-workflow/sandbox.ts +438 -0
  33. package/src/agent-workflow/schema.ts +18 -2
  34. package/src/agent-workflow/session.test.ts +629 -0
  35. package/src/agent-workflow/session.ts +1119 -0
  36. package/src/agent-workflow/target.test.ts +5 -1
  37. package/src/agent-workflow/target.ts +82 -17
@@ -71,6 +71,7 @@ describe('ModelClient agent target', () => {
71
71
  );
72
72
  const result = await target.turn(request);
73
73
  expect(received).toEqual({
74
+ maxRetries: 0,
74
75
  prompt: request.messages,
75
76
  tools: request.tools,
76
77
  model: request.model,
@@ -167,7 +168,10 @@ describe('ModelClient agent target', () => {
167
168
  const target = createModelClientTarget(
168
169
  client({ generate: async () => ({ ...response, ...override }) })
169
170
  );
170
- expect(await target.turn(request)).toEqual({ status: 'invalid', code: 'invalid_response' });
171
+ expect(await target.turn(request)).toMatchObject({
172
+ status: 'invalid',
173
+ code: 'invalid_response',
174
+ });
171
175
  });
172
176
  test('rejects tool calls beyond the declared per-turn budget', async () => {
173
177
  expect(
@@ -1,6 +1,8 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import Ajv from 'ajv';
2
3
  import { z } from 'zod';
3
4
  import type { GenerateOptions, ModelClient, TokenUsage, ToolCall } from '../adapters/types';
5
+ import { getWorkflowTool } from './catalog';
4
6
 
5
7
  const identifier = z.string().min(1).max(256);
6
8
  const toolName = z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$/);
@@ -21,7 +23,7 @@ const messageSchema = z
21
23
  })
22
24
  .strict();
23
25
  const timeoutSchema = z.number().int().min(1).max(2_147_483_647);
24
- const requestSchema = z
26
+ export const agentTurnRequestSchema = z
25
27
  .object({
26
28
  messages: z.array(messageSchema).min(1).max(1000),
27
29
  tools: z
@@ -59,6 +61,7 @@ const requestSchema = z
59
61
  })
60
62
  .strict();
61
63
  const resultSchema = z.object({
64
+ usageAvailable: z.boolean().optional(),
62
65
  id: identifier,
63
66
  model: identifier,
64
67
  text,
@@ -75,8 +78,15 @@ const resultSchema = z.object({
75
78
  functionCall: z.unknown().optional(),
76
79
  });
77
80
 
78
- export type AgentTurnRequest = z.infer<typeof requestSchema>;
81
+ export type AgentTurnRequest = z.infer<typeof agentTurnRequestSchema>;
79
82
  export type AgentTargetFailure = {
83
+ rejectedCall?: {
84
+ requestedCallIdHash: string;
85
+ tool: string;
86
+ reason: 'undeclared_tool' | 'invalid_arguments' | 'duplicate_id';
87
+ };
88
+ tokens?: TokenUsage;
89
+ usageAvailable?: boolean;
80
90
  status: 'unsupported' | 'invalid' | 'error';
81
91
  code:
82
92
  | 'invalid_request'
@@ -89,8 +99,7 @@ export type AgentTargetFailure = {
89
99
  export type AgentTargetCapabilities = {
90
100
  status: 'available';
91
101
  toolUse: boolean;
92
- /** ModelClient has no AbortSignal contract. Timeouts bound waiting, not transport work. */
93
- transportCancellation: false;
102
+ transportCancellation: boolean;
94
103
  };
95
104
  export type AgentTurnResult =
96
105
  | AgentTargetFailure
@@ -101,6 +110,7 @@ export type AgentTurnResult =
101
110
  message: { role: 'assistant'; content: string; tool_calls?: ToolCall[] };
102
111
  /** Adapter-reported counts only; zero can mean unavailable in existing adapters. */
103
112
  tokens: TokenUsage;
113
+ usageAvailable?: boolean;
104
114
  latencyMs: number;
105
115
  finishReason?: 'stop' | 'length' | 'tool_calls' | 'content_filter';
106
116
  };
@@ -113,6 +123,8 @@ export interface AgentTarget {
113
123
  signal?: AbortSignal
114
124
  ): Promise<AgentTargetCapabilities | AgentTargetFailure>;
115
125
  turn(request: AgentTurnRequest, signal?: AbortSignal): Promise<AgentTurnResult>;
126
+ /** Wait for underlying callbacks hidden behind a bounded turn facade. */
127
+ drain?(options: { timeoutMs: number }): Promise<{ pendingOperations: number }>;
116
128
  }
117
129
 
118
130
  const failure = (
@@ -145,7 +157,7 @@ function bounded<T>(
145
157
  });
146
158
  }
147
159
 
148
- function validTranscript(messages: AgentTurnRequest['messages']): boolean {
160
+ export function validWorkflowTranscript(messages: AgentTurnRequest['messages']): boolean {
149
161
  const seen = new Set<string>();
150
162
  const pending = new Set<string>();
151
163
  for (const message of messages) {
@@ -172,8 +184,8 @@ function validTranscript(messages: AgentTurnRequest['messages']): boolean {
172
184
  }
173
185
 
174
186
  /**
175
- * Bridge existing adapters without provider-specific dispatch. Callers must configure adapter
176
- * transport timeouts/retries separately: cancellation here cannot stop an in-flight provider call.
187
+ * Bridge existing adapters without provider-specific dispatch. Workflow calls disable supported
188
+ * transport retries and propagate abort only when the adapter declares transport cancellation.
177
189
  * Returned text/tool arguments are working conversation data, not sanitized retained evidence.
178
190
  */
179
191
  export function createModelClientTarget(client: ModelClient): AgentTarget {
@@ -185,26 +197,44 @@ export function createModelClientTarget(client: ModelClient): AgentTarget {
185
197
  ) {
186
198
  throw new TypeError('Invalid ModelClient');
187
199
  }
200
+ const pending = new Set<Promise<unknown>>();
201
+ const track = <T>(promise: Promise<T>): Promise<T> => {
202
+ pending.add(promise);
203
+ void promise.then(
204
+ () => pending.delete(promise),
205
+ () => pending.delete(promise)
206
+ );
207
+ return promise;
208
+ };
188
209
  const readCapabilities = async (): Promise<AgentTargetCapabilities | AgentTargetFailure> => {
189
- const value = await client.capabilities();
210
+ const value = await track(client.capabilities());
190
211
  if (!value || typeof value.toolUse !== 'boolean') return failure('invalid', 'invalid_response');
191
- return { status: 'available', toolUse: value.toolUse, transportCancellation: false };
212
+ return {
213
+ status: 'available',
214
+ toolUse: value.toolUse,
215
+ transportCancellation: value.transportCancellation === true,
216
+ };
192
217
  };
193
218
  return {
194
219
  provider: client.provider,
220
+ async drain({ timeoutMs }) {
221
+ if (!timeoutSchema.safeParse(timeoutMs).success) return { pendingOperations: pending.size };
222
+ await bounded(() => Promise.allSettled([...pending]), timeoutMs);
223
+ return { pendingOperations: pending.size };
224
+ },
195
225
  async capabilities(options, signal) {
196
226
  if (!timeoutSchema.safeParse(options?.timeoutMs).success)
197
227
  return failure('invalid', 'invalid_request');
198
228
  return bounded(readCapabilities, options.timeoutMs, signal);
199
229
  },
200
230
  async turn(request, signal) {
201
- let parsed: ReturnType<typeof requestSchema.safeParse>;
231
+ let parsed: ReturnType<typeof agentTurnRequestSchema.safeParse>;
202
232
  try {
203
- parsed = requestSchema.safeParse(request);
233
+ parsed = agentTurnRequestSchema.safeParse(request);
204
234
  } catch {
205
235
  return failure('invalid', 'invalid_request');
206
236
  }
207
- if (!parsed.success || !validTranscript(parsed.data.messages))
237
+ if (!parsed.success || !validWorkflowTranscript(parsed.data.messages))
208
238
  return failure('invalid', 'invalid_request');
209
239
  const input = parsed.data;
210
240
  const validators = new Map<string, ReturnType<Ajv['compile']>>();
@@ -223,6 +253,11 @@ export function createModelClientTarget(client: ModelClient): AgentTarget {
223
253
  return failure('invalid', 'invalid_request');
224
254
  }
225
255
  const started = Date.now();
256
+ const controller = new AbortController();
257
+ const abort = () => controller.abort();
258
+ signal?.addEventListener('abort', abort, { once: true });
259
+ if (signal?.aborted) controller.abort();
260
+ const timer = setTimeout(abort, input.budgets.timeoutMs);
226
261
  return bounded(
227
262
  async (): Promise<AgentTurnResult> => {
228
263
  const capabilities = await readCapabilities();
@@ -232,12 +267,14 @@ export function createModelClientTarget(client: ModelClient): AgentTarget {
232
267
  if (signal?.aborted) return failure('error', 'aborted');
233
268
  if (Date.now() - started >= input.budgets.timeoutMs) return failure('error', 'timeout');
234
269
  const options: GenerateOptions = {
270
+ maxRetries: 0,
235
271
  prompt: input.messages,
236
272
  tools: input.tools,
237
273
  model: input.model,
238
274
  ...input.generation,
275
+ ...(capabilities.transportCancellation ? { signal: controller.signal } : {}),
239
276
  };
240
- const generated = resultSchema.safeParse(await client.generate(options));
277
+ const generated = resultSchema.safeParse(await track(client.generate(options)));
241
278
  if (!generated.success) return failure('invalid', 'invalid_response');
242
279
  const result = generated.data;
243
280
  const calls = result.toolCalls ?? [];
@@ -252,10 +289,26 @@ export function createModelClientTarget(client: ModelClient): AgentTarget {
252
289
  const ids = new Set(
253
290
  input.messages.flatMap((message) => message.tool_calls?.map((call) => call.id) ?? [])
254
291
  );
292
+ const rejected = (
293
+ call: ToolCall,
294
+ reason: NonNullable<AgentTargetFailure['rejectedCall']>['reason']
295
+ ): AgentTargetFailure => ({
296
+ ...failure('invalid', 'invalid_response'),
297
+ tokens: result.tokens,
298
+ ...(result.usageAvailable !== undefined
299
+ ? { usageAvailable: result.usageAvailable }
300
+ : {}),
301
+ rejectedCall: {
302
+ requestedCallIdHash: createHash('sha256').update(call.id).digest('hex'),
303
+ tool: getWorkflowTool(call.function.name)?.id ?? 'unknown',
304
+ reason,
305
+ },
306
+ });
255
307
  for (const call of calls) {
256
- if (ids.has(call.id)) return failure('invalid', 'invalid_response');
308
+ if (ids.has(call.id)) return rejected(call, 'duplicate_id');
257
309
  ids.add(call.id);
258
310
  const validate = validators.get(call.function.name);
311
+ if (!validate) return rejected(call, 'undeclared_tool');
259
312
  try {
260
313
  const args: unknown = JSON.parse(call.function.arguments);
261
314
  if (
@@ -265,9 +318,9 @@ export function createModelClientTarget(client: ModelClient): AgentTarget {
265
318
  !validate ||
266
319
  validate(args) !== true
267
320
  )
268
- return failure('invalid', 'invalid_response');
321
+ return rejected(call, 'invalid_arguments');
269
322
  } catch {
270
- return failure('invalid', 'invalid_response');
323
+ return rejected(call, 'invalid_arguments');
271
324
  }
272
325
  }
273
326
  return {
@@ -280,13 +333,25 @@ export function createModelClientTarget(client: ModelClient): AgentTarget {
280
333
  ...(calls.length ? { tool_calls: calls } : {}),
281
334
  },
282
335
  tokens: result.tokens,
336
+ ...(result.usageAvailable !== undefined
337
+ ? { usageAvailable: result.usageAvailable }
338
+ : {}),
283
339
  latencyMs: result.latencyMs,
284
340
  finishReason: result.finishReason,
285
341
  };
286
342
  },
287
343
  input.budgets.timeoutMs,
288
344
  signal
289
- );
345
+ )
346
+ .then((result) =>
347
+ controller.signal.aborted
348
+ ? failure('error', signal?.aborted ? 'aborted' : 'timeout')
349
+ : result
350
+ )
351
+ .finally(() => {
352
+ clearTimeout(timer);
353
+ signal?.removeEventListener('abort', abort);
354
+ });
290
355
  },
291
356
  };
292
357
  }