@vedmalex/ai-connect 0.9.0 → 0.10.0

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/bun/index.js CHANGED
@@ -2629,6 +2629,73 @@ function normalizeToolResult(result) {
2629
2629
  ...result.isError !== void 0 ? { isError: result.isError } : {}
2630
2630
  };
2631
2631
  }
2632
+ var MAX_CLIENT_TOOL_LOOP_DEPTH = 8;
2633
+ async function* executeToolCallsRound(params) {
2634
+ const { toolCalls, registry, route, request, runtime, messages, abort } = params;
2635
+ const assistantMessage = {
2636
+ role: "assistant",
2637
+ content: params.assistantText,
2638
+ toolCalls
2639
+ };
2640
+ const toolMessages = [];
2641
+ for (const toolCall of toolCalls) {
2642
+ const tool = registry.get(toolCall.name);
2643
+ if (!tool) {
2644
+ throw new ClientToolExecutionError(
2645
+ new AiConnectError(
2646
+ "validation_error",
2647
+ `No client tool handler is registered for "${toolCall.name}".`
2648
+ )
2649
+ );
2650
+ }
2651
+ if (abort.signal.aborted) {
2652
+ throw mapAbortError(abort.reason);
2653
+ }
2654
+ const startEvent = params.onToolStart?.(toolCall);
2655
+ if (startEvent) {
2656
+ yield startEvent;
2657
+ }
2658
+ let execution;
2659
+ try {
2660
+ execution = normalizeToolResult(
2661
+ await tool.execute(toolCall.arguments, {
2662
+ route,
2663
+ request,
2664
+ runtime,
2665
+ toolCall,
2666
+ messages,
2667
+ ...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
2668
+ })
2669
+ );
2670
+ } catch (error) {
2671
+ if (abort.signal.aborted) {
2672
+ throw mapAbortError(abort.reason);
2673
+ }
2674
+ throw new ClientToolExecutionError(
2675
+ new AiConnectError(
2676
+ "validation_error",
2677
+ `Client tool "${tool.function.name}" threw during execution: ${error instanceof Error ? error.message : String(error)}`,
2678
+ { cause: error }
2679
+ )
2680
+ );
2681
+ }
2682
+ toolMessages.push({
2683
+ role: "tool",
2684
+ content: execution.content,
2685
+ toolName: tool.function.name,
2686
+ toolCallId: toolCall.id,
2687
+ ...execution.data !== void 0 ? { data: execution.data } : {},
2688
+ ...execution.isError !== void 0 ? { isError: execution.isError } : {}
2689
+ });
2690
+ const finishEvent = params.onToolFinish?.(toolCall, {
2691
+ ...execution.isError !== void 0 ? { isError: execution.isError } : {}
2692
+ });
2693
+ if (finishEvent) {
2694
+ yield finishEvent;
2695
+ }
2696
+ }
2697
+ return { assistantMessage, toolMessages };
2698
+ }
2632
2699
  function sumOptionalNumbers(left, right) {
2633
2700
  if (left === void 0) {
2634
2701
  return right;
@@ -3232,7 +3299,7 @@ function createClient(configOrInput, options = {}) {
3232
3299
  let output = initialOutput;
3233
3300
  let messages = [...request.messages];
3234
3301
  let transportSummary2;
3235
- for (let round = 0; round < 8; round += 1) {
3302
+ for (let round = 0; ; round += 1) {
3236
3303
  const toolCalls = output.toolCalls ?? [];
3237
3304
  if (toolCalls.length === 0) {
3238
3305
  return {
@@ -3244,62 +3311,36 @@ function createClient(configOrInput, options = {}) {
3244
3311
  ...transportSummary2 ? { transportSummary: transportSummary2 } : {}
3245
3312
  };
3246
3313
  }
3247
- const assistantMessage = {
3248
- role: "assistant",
3249
- content: output.text ?? "",
3250
- toolCalls
3251
- };
3252
- const toolMessages = [];
3253
- for (const toolCall of toolCalls) {
3254
- const tool = registry.get(toolCall.name);
3255
- if (!tool) {
3256
- throw new ClientToolExecutionError(
3257
- new AiConnectError(
3258
- "validation_error",
3259
- `No client tool handler is registered for "${toolCall.name}".`
3260
- )
3261
- );
3262
- }
3263
- if (abort.signal.aborted) {
3264
- throw mapAbortError(abort.reason);
3265
- }
3266
- let execution;
3267
- try {
3268
- execution = normalizeToolResult(
3269
- await tool.execute(toolCall.arguments, {
3270
- route,
3271
- request,
3272
- runtime,
3273
- toolCall,
3274
- messages,
3275
- ...request.workingDirectory ? { workingDirectory: request.workingDirectory } : {}
3276
- })
3277
- );
3278
- } catch (error) {
3279
- if (abort.signal.aborted) {
3280
- throw mapAbortError(abort.reason);
3281
- }
3282
- throw new ClientToolExecutionError(
3283
- new AiConnectError(
3284
- "validation_error",
3285
- `Client tool "${tool.function.name}" threw during execution: ${error instanceof Error ? error.message : String(error)}`,
3286
- { cause: error }
3287
- )
3288
- );
3289
- }
3290
- toolMessages.push({
3291
- role: "tool",
3292
- content: execution.content,
3293
- toolName: tool.function.name,
3294
- toolCallId: toolCall.id,
3295
- ...execution.data !== void 0 ? { data: execution.data } : {},
3296
- ...execution.isError !== void 0 ? { isError: execution.isError } : {}
3297
- });
3298
- }
3299
- messages = [...messages, assistantMessage, ...toolMessages];
3314
+ const roundIterator = executeToolCallsRound({
3315
+ toolCalls,
3316
+ assistantText: output.text ?? "",
3317
+ registry,
3318
+ route,
3319
+ request,
3320
+ runtime,
3321
+ messages,
3322
+ abort
3323
+ });
3324
+ let roundStep = await roundIterator.next();
3325
+ while (!roundStep.done) {
3326
+ roundStep = await roundIterator.next();
3327
+ }
3328
+ messages = [
3329
+ ...messages,
3330
+ roundStep.value.assistantMessage,
3331
+ ...roundStep.value.toolMessages
3332
+ ];
3300
3333
  if (abort.signal.aborted) {
3301
3334
  throw mapAbortError(abort.reason);
3302
3335
  }
3336
+ if (round + 1 >= MAX_CLIENT_TOOL_LOOP_DEPTH) {
3337
+ throw new ClientToolExecutionError(
3338
+ new AiConnectError(
3339
+ "validation_error",
3340
+ `clientTools exceeded the maximum tool-call loop depth of ${MAX_CLIENT_TOOL_LOOP_DEPTH}.`
3341
+ )
3342
+ );
3343
+ }
3303
3344
  output = await runWithAbortRace(
3304
3345
  handler({
3305
3346
  route,
@@ -3322,12 +3363,6 @@ function createClient(configOrInput, options = {}) {
3322
3363
  warnings.push(...output.warnings ?? []);
3323
3364
  usage = mergeUsage(usage, output.usage);
3324
3365
  }
3325
- throw new ClientToolExecutionError(
3326
- new AiConnectError(
3327
- "validation_error",
3328
- "clientTools exceeded the maximum tool-call loop depth of 8."
3329
- )
3330
- );
3331
3366
  }
3332
3367
  async function emitWideEvent(event) {
3333
3368
  if (!logging?.logger) {
@@ -4387,12 +4422,6 @@ function createClient(configOrInput, options = {}) {
4387
4422
  },
4388
4423
  async *stream(request, opts) {
4389
4424
  const preparedRequest = prepareRequest(request);
4390
- if (preparedRequest.clientTools?.length) {
4391
- throw new AiConnectError(
4392
- "not_supported",
4393
- "clientTools are currently supported only by generate(), not stream()."
4394
- );
4395
- }
4396
4425
  const { abort, dispose: disposeAbort } = deriveAbort(
4397
4426
  opts,
4398
4427
  resolveOpTimeout(opts, "generateMs", options.timeouts),
@@ -4412,7 +4441,12 @@ function createClient(configOrInput, options = {}) {
4412
4441
  release = await limiter.acquire(abort.signal);
4413
4442
  const selectedModel = await resolveSelectedModel(preparedRequest);
4414
4443
  const normalizedRequest = withSelectedModel(preparedRequest, selectedModel);
4415
- const { requiresImage, requiresFile, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
4444
+ const {
4445
+ requiresImage,
4446
+ requiresFile,
4447
+ requiresClientTools,
4448
+ request: capabilityScopedRequest
4449
+ } = resolveRequiredCapabilities(normalizedRequest);
4416
4450
  const candidates = router.resolveCandidates(capabilityScopedRequest);
4417
4451
  const startedAt = nowMs(runtime);
4418
4452
  const requestId = resolveEventRequestId(normalizedRequest.logContext);
@@ -4420,10 +4454,12 @@ function createClient(configOrInput, options = {}) {
4420
4454
  const requestSummary = summarizeRequest(normalizedRequest, candidateSummaries);
4421
4455
  const attempts = [];
4422
4456
  const wideAttempts = [];
4457
+ let hasYieldedOutward = false;
4458
+ let sawStreamHandler = false;
4423
4459
  if (candidates.length === 0) {
4424
- const error = requiresImage || requiresFile ? new AiConnectError(
4460
+ const error = requiresImage || requiresFile || requiresClientTools ? new AiConnectError(
4425
4461
  "unsupported_capability",
4426
- `No eligible route supports the required ${requiresFile ? "document/file" : "image"} input for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`
4462
+ requiresImage || requiresFile ? `No eligible route supports the required ${requiresFile ? "document/file" : "image"} input for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".` : `No eligible route supports client-side tool execution for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`
4427
4463
  ) : new AiConnectError("not_supported", `No eligible routes are available for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`);
4428
4464
  await emitWideEvent({
4429
4465
  timestamp: isoTimestamp(startedAt),
@@ -4451,84 +4487,202 @@ function createClient(configOrInput, options = {}) {
4451
4487
  continue;
4452
4488
  }
4453
4489
  if (handler.stream) {
4490
+ sawStreamHandler = true;
4454
4491
  const routeSummary = summarizeRoute(route);
4455
4492
  const attemptStartedAt = nowMs(runtime);
4456
4493
  let lastResult;
4457
4494
  let accumulatedText = "";
4458
4495
  const routeRequest = applyRouteDefaults(route, normalizedRequest);
4459
- const iterable = handler.stream({
4460
- route,
4461
- request: routeRequest,
4462
- runtime,
4463
- attempt: 1,
4464
- abort
4465
- });
4466
- const iterator = iterable[Symbol.asyncIterator]();
4467
- let iteratorReturned = false;
4468
- const returnIterator = async () => {
4469
- if (iteratorReturned) {
4470
- return;
4471
- }
4472
- iteratorReturned = true;
4473
- try {
4474
- await iterator.return?.();
4475
- } catch {
4476
- }
4477
- };
4496
+ const roundTools = routeRequest.clientTools ?? [];
4497
+ const registry = new Map(
4498
+ roundTools.map((tool) => [tool.function.name, tool])
4499
+ );
4500
+ let loopMessages = routeRequest.messages;
4501
+ let aggregatedUsage;
4502
+ const aggregatedWarnings = [];
4503
+ let toolRoundsExecuted = 0;
4478
4504
  try {
4479
- while (true) {
4505
+ roundsLoop: for (let round = 0; ; round += 1) {
4506
+ if (round >= MAX_CLIENT_TOOL_LOOP_DEPTH) {
4507
+ throw new ClientToolExecutionError(
4508
+ new AiConnectError(
4509
+ "validation_error",
4510
+ `clientTools exceeded the maximum tool-call loop depth of ${MAX_CLIENT_TOOL_LOOP_DEPTH}.`
4511
+ )
4512
+ );
4513
+ }
4480
4514
  if (abort.signal.aborted) {
4481
- await returnIterator();
4482
4515
  throw mapAbortError(abort.reason);
4483
4516
  }
4484
- if (pauseSignal?.aborted) {
4517
+ const roundRequest = round === 0 ? routeRequest : { ...routeRequest, messages: loopMessages, attachments: [] };
4518
+ const iterable = handler.stream({
4519
+ route,
4520
+ request: roundRequest,
4521
+ runtime,
4522
+ attempt: 1,
4523
+ abort
4524
+ });
4525
+ const iterator = iterable[Symbol.asyncIterator]();
4526
+ let iteratorReturned = false;
4527
+ const returnIterator = async () => {
4528
+ if (iteratorReturned) {
4529
+ return;
4530
+ }
4531
+ iteratorReturned = true;
4532
+ try {
4533
+ await iterator.return?.();
4534
+ } catch {
4535
+ }
4536
+ };
4537
+ let roundResult;
4538
+ try {
4539
+ while (true) {
4540
+ if (abort.signal.aborted) {
4541
+ await returnIterator();
4542
+ throw mapAbortError(abort.reason);
4543
+ }
4544
+ if (pauseSignal?.aborted) {
4545
+ await returnIterator();
4546
+ const pausedResult = lastResult ?? {
4547
+ route,
4548
+ text: accumulatedText,
4549
+ attachments: [],
4550
+ warnings: [],
4551
+ attempts
4552
+ };
4553
+ wideAttempts.push({
4554
+ index: wideAttempts.length + 1,
4555
+ route: routeSummary,
4556
+ durationMs: nowMs(runtime) - attemptStartedAt,
4557
+ outcome: "success",
4558
+ retryCount: 0
4559
+ });
4560
+ await emitWideEvent({
4561
+ timestamp: isoTimestamp(startedAt),
4562
+ event: "ai_connect.operation",
4563
+ requestId,
4564
+ operationName: "stream",
4565
+ operation: normalizedRequest.operation,
4566
+ runtime: runtime.kind,
4567
+ outcome: "success",
4568
+ durationMs: nowMs(runtime) - startedAt,
4569
+ candidates: candidateSummaries,
4570
+ attempts: wideAttempts,
4571
+ selectedRoute: routeSummary,
4572
+ request: requestSummary,
4573
+ result: summarizeResult(pausedResult),
4574
+ ...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
4575
+ });
4576
+ yield { type: "paused", result: pausedResult };
4577
+ return;
4578
+ }
4579
+ const next = await iterator.next();
4580
+ if (next.done) {
4581
+ break;
4582
+ }
4583
+ const event = next.value;
4584
+ if (event.type === "delta") {
4585
+ accumulatedText += event.text;
4586
+ hasYieldedOutward = true;
4587
+ yield event;
4588
+ continue;
4589
+ }
4590
+ if (event.type === "paused") {
4591
+ lastResult = event.result;
4592
+ wideAttempts.push({
4593
+ index: wideAttempts.length + 1,
4594
+ route: routeSummary,
4595
+ durationMs: nowMs(runtime) - attemptStartedAt,
4596
+ outcome: "success",
4597
+ retryCount: 0
4598
+ });
4599
+ router.recordSuccess(route);
4600
+ await emitWideEvent({
4601
+ timestamp: isoTimestamp(startedAt),
4602
+ event: "ai_connect.operation",
4603
+ requestId,
4604
+ operationName: "stream",
4605
+ operation: normalizedRequest.operation,
4606
+ runtime: runtime.kind,
4607
+ outcome: "success",
4608
+ durationMs: nowMs(runtime) - startedAt,
4609
+ candidates: candidateSummaries,
4610
+ attempts: wideAttempts,
4611
+ selectedRoute: routeSummary,
4612
+ request: requestSummary,
4613
+ result: summarizeResult(event.result),
4614
+ ...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
4615
+ });
4616
+ yield event;
4617
+ return;
4618
+ }
4619
+ if (event.type === "result") {
4620
+ roundResult = event.result;
4621
+ break;
4622
+ }
4623
+ }
4624
+ } finally {
4485
4625
  await returnIterator();
4486
- const pausedResult = lastResult ?? {
4487
- route,
4488
- text: accumulatedText,
4489
- attachments: [],
4490
- warnings: [],
4491
- attempts
4492
- };
4493
- wideAttempts.push({
4494
- index: wideAttempts.length + 1,
4495
- route: routeSummary,
4496
- durationMs: nowMs(runtime) - attemptStartedAt,
4497
- outcome: "success",
4498
- retryCount: 0
4499
- });
4500
- await emitWideEvent({
4501
- timestamp: isoTimestamp(startedAt),
4502
- event: "ai_connect.operation",
4503
- requestId,
4504
- operationName: "stream",
4505
- operation: normalizedRequest.operation,
4506
- runtime: runtime.kind,
4507
- outcome: "success",
4508
- durationMs: nowMs(runtime) - startedAt,
4509
- candidates: candidateSummaries,
4510
- attempts: wideAttempts,
4511
- selectedRoute: routeSummary,
4512
- request: requestSummary,
4513
- result: summarizeResult(pausedResult),
4514
- ...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
4515
- });
4516
- yield { type: "paused", result: pausedResult };
4517
- return;
4518
4626
  }
4519
- const next = await iterator.next();
4520
- if (next.done) {
4521
- break;
4627
+ if (!roundResult) {
4628
+ break roundsLoop;
4629
+ }
4630
+ if (registry.size === 0 || !roundResult.toolCalls?.length) {
4631
+ if (toolRoundsExecuted === 0) {
4632
+ lastResult = roundResult;
4633
+ } else {
4634
+ const mergedUsage = mergeUsage(aggregatedUsage, roundResult.usage);
4635
+ lastResult = {
4636
+ ...roundResult,
4637
+ warnings: [
4638
+ ...aggregatedWarnings,
4639
+ ...roundResult.warnings ?? []
4640
+ ],
4641
+ ...mergedUsage ? { usage: mergedUsage } : {}
4642
+ };
4643
+ }
4644
+ yield { type: "result", result: lastResult };
4645
+ break roundsLoop;
4522
4646
  }
4523
- const event = next.value;
4524
- if (event.type === "delta") {
4525
- accumulatedText += event.text;
4526
- yield event;
4527
- continue;
4647
+ aggregatedUsage = mergeUsage(aggregatedUsage, roundResult.usage);
4648
+ aggregatedWarnings.push(...roundResult.warnings ?? []);
4649
+ const roundGen = executeToolCallsRound({
4650
+ toolCalls: roundResult.toolCalls,
4651
+ assistantText: roundResult.text ?? "",
4652
+ registry,
4653
+ route,
4654
+ request: routeRequest,
4655
+ runtime,
4656
+ messages: loopMessages,
4657
+ abort,
4658
+ // D3/UR-007: emit a `tool-call` BEFORE execute, `tool-result` AFTER.
4659
+ onToolStart: (toolCall) => ({
4660
+ type: "tool-call",
4661
+ toolCall: {
4662
+ id: toolCall.id,
4663
+ name: toolCall.name,
4664
+ arguments: toolCall.arguments
4665
+ }
4666
+ }),
4667
+ onToolFinish: (toolCall, execution) => ({
4668
+ type: "tool-result",
4669
+ toolCallId: toolCall.id,
4670
+ name: toolCall.name,
4671
+ ...execution.isError !== void 0 ? { isError: execution.isError } : {}
4672
+ })
4673
+ });
4674
+ let toolStep = await roundGen.next();
4675
+ while (!toolStep.done) {
4676
+ hasYieldedOutward = true;
4677
+ yield toolStep.value;
4678
+ toolStep = await roundGen.next();
4528
4679
  }
4529
- lastResult = event.result;
4530
- yield event;
4531
- break;
4680
+ loopMessages = [
4681
+ ...loopMessages,
4682
+ toolStep.value.assistantMessage,
4683
+ ...toolStep.value.toolMessages
4684
+ ];
4685
+ toolRoundsExecuted += 1;
4532
4686
  }
4533
4687
  wideAttempts.push({
4534
4688
  index: wideAttempts.length + 1,
@@ -4556,7 +4710,44 @@ function createClient(configOrInput, options = {}) {
4556
4710
  });
4557
4711
  return;
4558
4712
  } catch (error) {
4559
- await returnIterator();
4713
+ if (error instanceof ClientToolExecutionError && !abort.signal.aborted) {
4714
+ const toolError = error.aiError;
4715
+ attempts.push({
4716
+ routeId: route.id,
4717
+ handlerKey: route.handlerKey,
4718
+ errorCode: toolError.code,
4719
+ message: toolError.message
4720
+ });
4721
+ wideAttempts.push({
4722
+ index: wideAttempts.length + 1,
4723
+ route: routeSummary,
4724
+ durationMs: nowMs(runtime) - attemptStartedAt,
4725
+ outcome: "error",
4726
+ retryCount: 0,
4727
+ errorCode: toolError.code,
4728
+ message: toolError.message
4729
+ });
4730
+ await emitWideEvent({
4731
+ timestamp: isoTimestamp(startedAt),
4732
+ event: "ai_connect.operation",
4733
+ requestId,
4734
+ operationName: "stream",
4735
+ operation: normalizedRequest.operation,
4736
+ runtime: runtime.kind,
4737
+ outcome: "error",
4738
+ durationMs: nowMs(runtime) - startedAt,
4739
+ candidates: candidateSummaries,
4740
+ attempts: wideAttempts,
4741
+ request: requestSummary,
4742
+ error: {
4743
+ code: toolError.code,
4744
+ message: toolError.message,
4745
+ routeId: route.id
4746
+ },
4747
+ ...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
4748
+ });
4749
+ throw toolError;
4750
+ }
4560
4751
  const normalizedError = abort.signal.aborted ? mapAbortError(abort.reason) : toAiConnectError(error);
4561
4752
  attempts.push({
4562
4753
  routeId: route.id,
@@ -4595,6 +4786,28 @@ function createClient(configOrInput, options = {}) {
4595
4786
  });
4596
4787
  throw normalizedError;
4597
4788
  }
4789
+ if (hasYieldedOutward) {
4790
+ await emitWideEvent({
4791
+ timestamp: isoTimestamp(startedAt),
4792
+ event: "ai_connect.operation",
4793
+ requestId,
4794
+ operationName: "stream",
4795
+ operation: normalizedRequest.operation,
4796
+ runtime: runtime.kind,
4797
+ outcome: "error",
4798
+ durationMs: nowMs(runtime) - startedAt,
4799
+ candidates: candidateSummaries,
4800
+ attempts: wideAttempts,
4801
+ request: requestSummary,
4802
+ error: {
4803
+ code: normalizedError.code,
4804
+ message: normalizedError.message,
4805
+ routeId: route.id
4806
+ },
4807
+ ...normalizedRequest.logContext ? { context: normalizedRequest.logContext } : {}
4808
+ });
4809
+ throw normalizedError;
4810
+ }
4598
4811
  router.recordFailure(route, normalizedError.code);
4599
4812
  continue;
4600
4813
  }
@@ -4610,6 +4823,12 @@ function createClient(configOrInput, options = {}) {
4610
4823
  attempts,
4611
4824
  wideAttempts
4612
4825
  });
4826
+ if (requiresClientTools && !sawStreamHandler) {
4827
+ result.warnings = [
4828
+ ...result.warnings,
4829
+ "Streaming tool-loop is not supported by the available route(s); returned a non-streamed result via the generate tool-loop."
4830
+ ];
4831
+ }
4613
4832
  yield {
4614
4833
  type: "result",
4615
4834
  result