cjerzak commited on
Commit
11528b1
·
1 Parent(s): bc43d06

add files

Browse files
Files changed (3) hide show
  1. R/asa_api_helpers.R +501 -40
  2. R/plumber.R +17 -19
  3. Tests/api_contract_smoke.R +82 -2
R/asa_api_helpers.R CHANGED
@@ -318,6 +318,364 @@ asa_api_error_fields <- function(message, error_code = NULL, details = NULL) {
318
  payload
319
  }
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  asa_api_missing_auth_env_vars <- function() {
322
  required <- c("ASA_API_BEARER_TOKEN", "GUI_PASSWORD")
323
  required[!nzchar(vapply(required, asa_api_secret_env_value, character(1)))]
@@ -760,93 +1118,196 @@ asa_api_is_result_like <- function(value) {
760
  (is.list(value) && !is.null(value$status) && !is.null(value$message))
761
  }
762
 
763
- asa_api_run_single_via_asa <- function(payload) {
764
  asa_api_apply_env_defaults()
765
  prompt <- asa_api_require_prompt(payload)
766
  config <- asa_api_build_config(payload)
767
  run_args <- asa_api_build_run_args(payload)
 
 
 
 
 
 
 
 
 
768
 
769
  args <- c(list(prompt = prompt, config = config), run_args)
770
  args <- asa_api_filter_formals(asa::run_task, args)
771
 
772
- result <- do.call(asa::run_task, args)
 
 
 
 
 
 
 
 
 
 
 
773
  include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE)
774
  include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE)
775
-
776
- asa_api_normalize_result(
777
- result,
778
- include_raw_output = include_raw_output,
779
- include_trace_json = include_trace_json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
780
  )
781
  }
782
 
783
- asa_api_run_single_via_direct <- function(payload) {
784
  asa_api_apply_env_defaults()
785
  run_direct_task_fun <- asa_api_get_run_direct_task()
786
  prompt <- asa_api_require_prompt(payload)
787
  config <- asa_api_build_config(payload)
788
  direct_args <- asa_api_build_direct_args(payload, run_direct_task_fun = run_direct_task_fun)
 
 
 
 
 
 
 
 
 
789
 
790
  args <- c(list(prompt = prompt, config = config), direct_args)
791
  args <- asa_api_filter_formals(run_direct_task_fun, args)
792
 
793
- result <- do.call(run_direct_task_fun, args)
 
 
 
 
 
 
 
 
 
 
 
794
  include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE)
795
  include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE)
796
-
797
- asa_api_normalize_result(
798
- result,
799
- include_raw_output = include_raw_output,
800
- include_trace_json = include_trace_json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  )
802
  }
803
 
804
- asa_api_run_single <- function(payload, allow_direct_provider = FALSE) {
805
  use_direct_provider <- isTRUE(allow_direct_provider) &&
806
  asa_api_to_bool(payload$use_direct_provider, default = FALSE)
807
 
808
  if (use_direct_provider) {
809
- return(asa_api_run_single_via_direct(payload))
810
  }
811
 
812
- asa_api_run_single_via_asa(payload)
813
  }
814
 
815
- asa_api_run_batch <- function(payload) {
816
  asa_api_apply_env_defaults()
817
  prompts <- asa_api_require_prompts(payload)
818
  asa_api_validate_batch_supported_fields(payload)
819
  config <- asa_api_build_config(payload)
820
  batch_args <- asa_api_build_batch_args(payload)
 
 
 
 
 
 
 
 
 
 
821
 
822
  args <- c(list(prompts = prompts, config = config), batch_args)
823
  args <- asa_api_filter_formals(asa::run_task_batch, args)
824
 
825
- raw <- do.call(asa::run_task_batch, args)
 
 
 
 
 
 
 
 
 
 
 
826
  include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE)
827
  include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
828
 
829
- if (is.data.frame(raw) && "asa_result" %in% names(raw)) {
830
- items <- lapply(raw$asa_result, asa_api_normalize_result,
831
- include_raw_output = include_raw_output,
832
- include_trace_json = include_trace_json
833
- )
834
- } else if (is.list(raw) && length(raw) && all(vapply(raw, asa_api_is_result_like, logical(1)))) {
835
- items <- lapply(raw, asa_api_normalize_result,
836
- include_raw_output = include_raw_output,
837
- include_trace_json = include_trace_json
838
- )
839
- } else if (is.list(raw) && length(raw) == 0L) {
840
- items <- list()
841
- } else {
842
- items <- list(asa_api_to_plain(raw))
843
- }
844
-
845
- list(
846
- status = "success",
847
- results = items,
848
- count = length(items),
849
- circuit_breaker_aborted = isTRUE(attr(raw, "circuit_breaker_aborted"))
850
  )
851
  }
852
 
 
318
  payload
319
  }
320
 
321
+ asa_api_error_status <- function(message) {
322
+ if (grepl("direct-provider mode|does not provide `run_direct_task`", message, ignore.case = TRUE)) {
323
+ return(501L)
324
+ }
325
+ if (grepl("invalid json|required|must be|non-empty|unauthorized|password", message, ignore.case = TRUE)) {
326
+ return(400L)
327
+ }
328
+ 500L
329
+ }
330
+
331
+ asa_api_drop_nulls <- function(value) {
332
+ if (!is.list(value)) {
333
+ return(value)
334
+ }
335
+
336
+ value[!vapply(value, is.null, logical(1))]
337
+ }
338
+
339
+ asa_api_make_diagnostic_id <- function(prefix = "asaapi") {
340
+ sprintf(
341
+ "%s-%s-%06d",
342
+ asa_api_scalar_chr(prefix, default = "asaapi"),
343
+ format(Sys.time(), "%Y%m%dT%H%M%SZ", tz = "UTC"),
344
+ as.integer(stats::runif(1, min = 0, max = 999999))
345
+ )
346
+ }
347
+
348
+ asa_api_redact_text_excerpt <- function(text, max_chars = 120L) {
349
+ text <- asa_api_scalar_chr(text, default = "")
350
+ if (!nzchar(text)) {
351
+ return("")
352
+ }
353
+
354
+ text <- gsub("[[:cntrl:]]+", " ", text)
355
+ text <- gsub("[[:space:]]+", " ", text)
356
+ text <- trimws(text)
357
+ if (!nzchar(text)) {
358
+ return("")
359
+ }
360
+
361
+ max_chars <- asa_api_scalar_int(max_chars, default = 120L)
362
+ if (is.na(max_chars) || max_chars < 16L) {
363
+ max_chars <- 120L
364
+ }
365
+
366
+ if (nchar(text, type = "chars") > max_chars) {
367
+ paste0(substr(text, 1L, max_chars), "...")
368
+ } else {
369
+ text
370
+ }
371
+ }
372
+
373
+ asa_api_redact_call_text <- function(call_text, max_chars = 240L) {
374
+ call_text <- asa_api_scalar_chr(call_text, default = "")
375
+ if (!nzchar(call_text)) {
376
+ return("")
377
+ }
378
+
379
+ call_text <- gsub('"[^"]*"', '"<redacted>"', call_text)
380
+ call_text <- gsub("'[^']*'", "'<redacted>'", call_text)
381
+ asa_api_redact_text_excerpt(call_text, max_chars = max_chars)
382
+ }
383
+
384
+ asa_api_prompt_summary <- function(prompt) {
385
+ prompt <- asa_api_scalar_chr(prompt, default = "")
386
+
387
+ list(
388
+ chars = nchar(prompt, type = "chars"),
389
+ excerpt = asa_api_redact_text_excerpt(prompt)
390
+ )
391
+ }
392
+
393
+ asa_api_prompts_summary <- function(prompts) {
394
+ prompts <- prompts %||% character(0)
395
+ prompts <- as.character(prompts)
396
+ prompt_lengths <- nchar(prompts, type = "chars")
397
+ prompt_lengths[is.na(prompt_lengths)] <- 0L
398
+
399
+ list(
400
+ count = length(prompts),
401
+ total_chars = sum(prompt_lengths),
402
+ max_chars = if (length(prompt_lengths)) max(prompt_lengths) else 0L,
403
+ first_prompt = if (length(prompts)) asa_api_prompt_summary(prompts[[1]]) else NULL
404
+ )
405
+ }
406
+
407
+ asa_api_runtime_config_summary <- function(config) {
408
+ if (!is.list(config) || !length(config)) {
409
+ return(list())
410
+ }
411
+
412
+ asa_api_drop_nulls(list(
413
+ backend = asa_api_scalar_chr(config$backend, default = ""),
414
+ model = asa_api_scalar_chr(config$model, default = ""),
415
+ conda_env = asa_api_scalar_chr(config$conda_env, default = ""),
416
+ use_browser = if (!is.null(config$use_browser)) isTRUE(config$use_browser) else NULL,
417
+ timeout = asa_api_scalar_int(config$timeout, default = NA_integer_),
418
+ rate_limit = asa_api_scalar_num(config$rate_limit, default = NA_real_),
419
+ workers = asa_api_scalar_int(config$workers, default = NA_integer_),
420
+ memory_folding = if (!is.null(config$memory_folding)) isTRUE(config$memory_folding) else NULL,
421
+ recursion_limit = asa_api_scalar_int(config$recursion_limit, default = NA_integer_)
422
+ ))
423
+ }
424
+
425
+ asa_api_runtime_defaults_summary <- function() {
426
+ asa_api_drop_nulls(list(
427
+ backend = getOption("asa.default_backend", NULL),
428
+ model = getOption("asa.default_model", NULL),
429
+ conda_env = getOption("asa.default_conda_env", "asa_env"),
430
+ use_browser = asa_api_to_bool(Sys.getenv("ASA_USE_BROWSER_DEFAULT", unset = "false"), default = FALSE)
431
+ ))
432
+ }
433
+
434
+ asa_api_runtime_tor_summary <- function() {
435
+ tor <- asa_api_tor_health()
436
+
437
+ asa_api_drop_nulls(list(
438
+ tor_enabled = isTRUE(tor$tor_enabled),
439
+ tor_ready = isTRUE(tor$tor_ready),
440
+ tor_proxy_host = tor$tor_proxy_host %||% NULL,
441
+ tor_proxy_port = tor$tor_proxy_port %||% NULL,
442
+ tor_proxy_port_open = isTRUE(tor$tor_proxy_port_open),
443
+ tor_control_port = tor$tor_control_port %||% NULL,
444
+ tor_control_port_open = isTRUE(tor$tor_control_port_open),
445
+ tor_cookie_present = isTRUE(tor$tor_cookie_present),
446
+ tor_cookie_readable = isTRUE(tor$tor_cookie_readable)
447
+ ))
448
+ }
449
+
450
+ asa_api_runtime_error_summary <- function(error) {
451
+ if (is.null(error)) {
452
+ return(list())
453
+ }
454
+
455
+ err_call <- conditionCall(error)
456
+ call_text <- if (is.null(err_call)) "" else paste(deparse(err_call, width.cutoff = 120L), collapse = " ")
457
+
458
+ asa_api_drop_nulls(list(
459
+ message = conditionMessage(error),
460
+ class = as.character(class(error)),
461
+ call = if (nzchar(call_text)) asa_api_redact_call_text(call_text) else NULL
462
+ ))
463
+ }
464
+
465
+ asa_api_runtime_stage <- function(stage_ref, default = "invoke") {
466
+ if (is.environment(stage_ref)) {
467
+ return(asa_api_scalar_chr(stage_ref$value, default = default))
468
+ }
469
+
470
+ asa_api_scalar_chr(stage_ref, default = default)
471
+ }
472
+
473
+ asa_api_build_runtime_diagnostic <- function(mode,
474
+ route,
475
+ payload = NULL,
476
+ prompt = NULL,
477
+ prompts = NULL,
478
+ config = NULL,
479
+ forwarded_arg_names = character(0),
480
+ request_shape = list(),
481
+ stage = "invoke",
482
+ error = NULL,
483
+ diagnostic_id = asa_api_make_diagnostic_id()) {
484
+ request_shape <- asa_api_named_list(request_shape)
485
+ request_shape$include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE)
486
+ request_shape$include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE)
487
+ if (!is.null(payload$use_direct_provider)) {
488
+ request_shape$use_direct_provider <- asa_api_to_bool(payload$use_direct_provider, default = FALSE)
489
+ }
490
+ request_shape$forwarded_arg_names <- sort(unique(as.character(forwarded_arg_names)))
491
+
492
+ if (!is.null(prompt)) {
493
+ request_shape$prompt <- asa_api_prompt_summary(prompt)
494
+ }
495
+ if (!is.null(prompts)) {
496
+ request_shape$prompts <- asa_api_prompts_summary(prompts)
497
+ }
498
+
499
+ asa_api_drop_nulls(list(
500
+ diagnostic_id = asa_api_scalar_chr(diagnostic_id, default = asa_api_make_diagnostic_id()),
501
+ timestamp_utc = format(Sys.time(), "%Y-%m-%dT%H:%M:%SZ", tz = "UTC"),
502
+ mode = asa_api_scalar_chr(mode, default = "unknown"),
503
+ route = asa_api_scalar_chr(route, default = "unknown"),
504
+ stage = asa_api_scalar_chr(stage, default = "invoke"),
505
+ request = asa_api_drop_nulls(request_shape),
506
+ config = asa_api_runtime_config_summary(config),
507
+ runtime = list(
508
+ asa_version = tryCatch(as.character(utils::packageVersion("asa")), error = function(e) NA_character_),
509
+ defaults = asa_api_runtime_defaults_summary(),
510
+ direct_provider_available = tryCatch(isTRUE(asa_api_has_run_direct_task()), error = function(e) FALSE),
511
+ tor = asa_api_runtime_tor_summary()
512
+ ),
513
+ error = asa_api_runtime_error_summary(error)
514
+ ))
515
+ }
516
+
517
+ asa_api_emit_runtime_diagnostic <- function(diagnostic, log_con = stderr()) {
518
+ if (requireNamespace("reticulate", quietly = TRUE)) {
519
+ diagnostic <- tryCatch(reticulate::py_to_r(diagnostic), error = function(e) diagnostic)
520
+ }
521
+ diagnostic <- asa_api_named_list(diagnostic)
522
+ diagnostic_id <- asa_api_scalar_chr(diagnostic$diagnostic_id, default = "unknown")
523
+ mode <- asa_api_scalar_chr(diagnostic$mode, default = "unknown")
524
+ route <- asa_api_scalar_chr(diagnostic$route, default = "unknown")
525
+ stage <- asa_api_scalar_chr(diagnostic$stage, default = "invoke")
526
+ error_message <- asa_api_scalar_chr((diagnostic$error %||% list())$message, default = "Request failed.")
527
+
528
+ cat(
529
+ sprintf(
530
+ "[asa-api] runtime failure diagnostic_id=%s mode=%s route=%s stage=%s error=%s\n",
531
+ diagnostic_id,
532
+ mode,
533
+ route,
534
+ stage,
535
+ error_message
536
+ ),
537
+ file = log_con
538
+ )
539
+
540
+ diagnostic_json <- tryCatch(
541
+ jsonlite::toJSON(diagnostic, auto_unbox = TRUE, null = "null", force = TRUE, digits = NA),
542
+ error = function(e) {
543
+ jsonlite::toJSON(
544
+ list(
545
+ diagnostic_id = diagnostic_id,
546
+ serialization_error = conditionMessage(e)
547
+ ),
548
+ auto_unbox = TRUE,
549
+ null = "null",
550
+ force = TRUE
551
+ )
552
+ }
553
+ )
554
+ cat(
555
+ sprintf("[asa-api][diagnostic] %s\n", paste(diagnostic_json, collapse = "")),
556
+ file = log_con
557
+ )
558
+
559
+ invisible(diagnostic)
560
+ }
561
+
562
+ asa_api_runtime_error_code <- function(mode) {
563
+ mode <- asa_api_scalar_chr(mode, default = "")
564
+ if (identical(mode, "asa_agent_batch")) {
565
+ return("agent_batch_failure")
566
+ }
567
+ if (identical(mode, "provider_direct_single")) {
568
+ return("direct_provider_failure")
569
+ }
570
+ "agent_pipeline_failure"
571
+ }
572
+
573
+ asa_api_raise_runtime_failure <- function(error,
574
+ diagnostic,
575
+ mode,
576
+ status_code = 500L,
577
+ log_con = stderr()) {
578
+ diagnostic <- asa_api_emit_runtime_diagnostic(diagnostic, log_con = log_con)
579
+ diagnostic_id <- asa_api_scalar_chr(diagnostic$diagnostic_id, default = "unknown")
580
+ message <- sprintf("%s [diagnostic_id=%s]", conditionMessage(error), diagnostic_id)
581
+
582
+ condition <- simpleError(message)
583
+ condition$error_code <- asa_api_runtime_error_code(mode)
584
+ condition$details <- asa_api_drop_nulls(list(
585
+ diagnostic_id = diagnostic_id,
586
+ mode = diagnostic$mode %||% NULL,
587
+ route = diagnostic$route %||% NULL,
588
+ stage = diagnostic$stage %||% NULL,
589
+ request = diagnostic$request %||% NULL,
590
+ config = diagnostic$config %||% NULL,
591
+ runtime = diagnostic$runtime %||% NULL,
592
+ error = diagnostic$error %||% NULL
593
+ ))
594
+ condition$status_code <- asa_api_scalar_int(status_code, default = 500L)
595
+ class(condition) <- c("asa_api_runtime_error", class(condition))
596
+
597
+ stop(condition)
598
+ }
599
+
600
+ asa_api_invoke_with_runtime_diagnostics <- function(fn,
601
+ mode,
602
+ route,
603
+ payload = NULL,
604
+ prompt = NULL,
605
+ prompts = NULL,
606
+ config = NULL,
607
+ forwarded_arg_names = character(0),
608
+ request_shape = list(),
609
+ stage_ref = NULL,
610
+ status_code = 500L,
611
+ log_con = stderr()) {
612
+ tryCatch(
613
+ fn(),
614
+ error = function(e) {
615
+ diagnostic <- asa_api_build_runtime_diagnostic(
616
+ mode = mode,
617
+ route = route,
618
+ payload = payload,
619
+ prompt = prompt,
620
+ prompts = prompts,
621
+ config = config,
622
+ forwarded_arg_names = forwarded_arg_names,
623
+ request_shape = request_shape,
624
+ stage = asa_api_runtime_stage(stage_ref, default = "invoke"),
625
+ error = e
626
+ )
627
+ asa_api_raise_runtime_failure(
628
+ error = e,
629
+ diagnostic = diagnostic,
630
+ mode = mode,
631
+ status_code = status_code,
632
+ log_con = log_con
633
+ )
634
+ }
635
+ )
636
+ }
637
+
638
+ asa_api_error_failure <- function(error) {
639
+ message <- conditionMessage(error)
640
+ failure <- list(
641
+ status_code = asa_api_error_status(message),
642
+ message = message
643
+ )
644
+
645
+ code <- asa_api_scalar_chr(error$error_code %||% "", default = "")
646
+ if (nzchar(code)) {
647
+ failure$error_code <- code
648
+ }
649
+
650
+ details <- error$details %||% NULL
651
+ if (requireNamespace("reticulate", quietly = TRUE)) {
652
+ details <- tryCatch(reticulate::py_to_r(details), error = function(e) details)
653
+ }
654
+ if (!is.null(details) && !is.list(details)) {
655
+ details <- asa_api_to_plain(details)
656
+ }
657
+ if (is.list(details) && length(details)) {
658
+ failure$details <- details
659
+ }
660
+
661
+ explicit_status <- asa_api_scalar_int(error$status_code %||% NA_integer_, default = NA_integer_)
662
+ if (!is.na(explicit_status)) {
663
+ failure$status_code <- explicit_status
664
+ }
665
+
666
+ failure
667
+ }
668
+
669
+ asa_api_request_context_route <- function(request_context, default) {
670
+ context <- asa_api_named_list(request_context)
671
+ asa_api_scalar_chr(context$route, default = default)
672
+ }
673
+
674
+ asa_api_request_context_log_con <- function(request_context) {
675
+ context <- asa_api_named_list(request_context)
676
+ context$log_con %||% stderr()
677
+ }
678
+
679
  asa_api_missing_auth_env_vars <- function() {
680
  required <- c("ASA_API_BEARER_TOKEN", "GUI_PASSWORD")
681
  required[!nzchar(vapply(required, asa_api_secret_env_value, character(1)))]
 
1118
  (is.list(value) && !is.null(value$status) && !is.null(value$message))
1119
  }
1120
 
1121
+ asa_api_run_single_via_asa <- function(payload, request_context = NULL) {
1122
  asa_api_apply_env_defaults()
1123
  prompt <- asa_api_require_prompt(payload)
1124
  config <- asa_api_build_config(payload)
1125
  run_args <- asa_api_build_run_args(payload)
1126
+ route <- asa_api_request_context_route(request_context, "/v1/run")
1127
+ log_con <- asa_api_request_context_log_con(request_context)
1128
+ stage_ref <- new.env(parent = emptyenv())
1129
+ stage_ref$value <- "invoke"
1130
+ request_shape <- asa_api_drop_nulls(list(
1131
+ output_format = asa_api_scalar_chr(run_args$output_format, default = "text"),
1132
+ performance_profile = asa_api_scalar_chr(run_args$performance_profile, default = ""),
1133
+ use_plan_mode = if (!is.null(run_args$use_plan_mode)) isTRUE(run_args$use_plan_mode) else NULL
1134
+ ))
1135
 
1136
  args <- c(list(prompt = prompt, config = config), run_args)
1137
  args <- asa_api_filter_formals(asa::run_task, args)
1138
 
1139
+ result <- asa_api_invoke_with_runtime_diagnostics(
1140
+ fn = function() do.call(asa::run_task, args),
1141
+ mode = "asa_agent_single",
1142
+ route = route,
1143
+ payload = payload,
1144
+ prompt = prompt,
1145
+ config = config,
1146
+ forwarded_arg_names = names(run_args),
1147
+ request_shape = request_shape,
1148
+ stage_ref = stage_ref,
1149
+ log_con = log_con
1150
+ )
1151
  include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE)
1152
  include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE)
1153
+ stage_ref$value <- "normalize_result"
1154
+
1155
+ asa_api_invoke_with_runtime_diagnostics(
1156
+ fn = function() {
1157
+ asa_api_normalize_result(
1158
+ result,
1159
+ include_raw_output = include_raw_output,
1160
+ include_trace_json = include_trace_json
1161
+ )
1162
+ },
1163
+ mode = "asa_agent_single",
1164
+ route = route,
1165
+ payload = payload,
1166
+ prompt = prompt,
1167
+ config = config,
1168
+ forwarded_arg_names = names(run_args),
1169
+ request_shape = request_shape,
1170
+ stage_ref = stage_ref,
1171
+ log_con = log_con
1172
  )
1173
  }
1174
 
1175
+ asa_api_run_single_via_direct <- function(payload, request_context = NULL) {
1176
  asa_api_apply_env_defaults()
1177
  run_direct_task_fun <- asa_api_get_run_direct_task()
1178
  prompt <- asa_api_require_prompt(payload)
1179
  config <- asa_api_build_config(payload)
1180
  direct_args <- asa_api_build_direct_args(payload, run_direct_task_fun = run_direct_task_fun)
1181
+ route <- asa_api_request_context_route(request_context, "/gui/query")
1182
+ log_con <- asa_api_request_context_log_con(request_context)
1183
+ stage_ref <- new.env(parent = emptyenv())
1184
+ stage_ref$value <- "invoke"
1185
+ request_shape <- asa_api_drop_nulls(list(
1186
+ output_format = asa_api_scalar_chr(direct_args$output_format, default = "text"),
1187
+ performance_profile = asa_api_scalar_chr(direct_args$performance_profile, default = ""),
1188
+ use_plan_mode = if (!is.null(direct_args$use_plan_mode)) isTRUE(direct_args$use_plan_mode) else NULL
1189
+ ))
1190
 
1191
  args <- c(list(prompt = prompt, config = config), direct_args)
1192
  args <- asa_api_filter_formals(run_direct_task_fun, args)
1193
 
1194
+ result <- asa_api_invoke_with_runtime_diagnostics(
1195
+ fn = function() do.call(run_direct_task_fun, args),
1196
+ mode = "provider_direct_single",
1197
+ route = route,
1198
+ payload = payload,
1199
+ prompt = prompt,
1200
+ config = config,
1201
+ forwarded_arg_names = names(direct_args),
1202
+ request_shape = request_shape,
1203
+ stage_ref = stage_ref,
1204
+ log_con = log_con
1205
+ )
1206
  include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE)
1207
  include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE)
1208
+ stage_ref$value <- "normalize_result"
1209
+
1210
+ asa_api_invoke_with_runtime_diagnostics(
1211
+ fn = function() {
1212
+ asa_api_normalize_result(
1213
+ result,
1214
+ include_raw_output = include_raw_output,
1215
+ include_trace_json = include_trace_json
1216
+ )
1217
+ },
1218
+ mode = "provider_direct_single",
1219
+ route = route,
1220
+ payload = payload,
1221
+ prompt = prompt,
1222
+ config = config,
1223
+ forwarded_arg_names = names(direct_args),
1224
+ request_shape = request_shape,
1225
+ stage_ref = stage_ref,
1226
+ log_con = log_con
1227
  )
1228
  }
1229
 
1230
+ asa_api_run_single <- function(payload, allow_direct_provider = FALSE, request_context = NULL) {
1231
  use_direct_provider <- isTRUE(allow_direct_provider) &&
1232
  asa_api_to_bool(payload$use_direct_provider, default = FALSE)
1233
 
1234
  if (use_direct_provider) {
1235
+ return(asa_api_run_single_via_direct(payload, request_context = request_context))
1236
  }
1237
 
1238
+ asa_api_run_single_via_asa(payload, request_context = request_context)
1239
  }
1240
 
1241
+ asa_api_run_batch <- function(payload, request_context = NULL) {
1242
  asa_api_apply_env_defaults()
1243
  prompts <- asa_api_require_prompts(payload)
1244
  asa_api_validate_batch_supported_fields(payload)
1245
  config <- asa_api_build_config(payload)
1246
  batch_args <- asa_api_build_batch_args(payload)
1247
+ route <- asa_api_request_context_route(request_context, "/v1/batch")
1248
+ log_con <- asa_api_request_context_log_con(request_context)
1249
+ stage_ref <- new.env(parent = emptyenv())
1250
+ stage_ref$value <- "invoke"
1251
+ request_shape <- asa_api_drop_nulls(list(
1252
+ output_format = asa_api_scalar_chr(batch_args$output_format, default = "text"),
1253
+ parallel = if (!is.null(batch_args$parallel)) isTRUE(batch_args$parallel) else NULL,
1254
+ progress = if (!is.null(batch_args$progress)) isTRUE(batch_args$progress) else NULL,
1255
+ performance_profile = asa_api_scalar_chr(batch_args$performance_profile, default = "")
1256
+ ))
1257
 
1258
  args <- c(list(prompts = prompts, config = config), batch_args)
1259
  args <- asa_api_filter_formals(asa::run_task_batch, args)
1260
 
1261
+ raw <- asa_api_invoke_with_runtime_diagnostics(
1262
+ fn = function() do.call(asa::run_task_batch, args),
1263
+ mode = "asa_agent_batch",
1264
+ route = route,
1265
+ payload = payload,
1266
+ prompts = prompts,
1267
+ config = config,
1268
+ forwarded_arg_names = names(batch_args),
1269
+ request_shape = request_shape,
1270
+ stage_ref = stage_ref,
1271
+ log_con = log_con
1272
+ )
1273
  include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE)
1274
  include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE)
1275
+ stage_ref$value <- "normalize_batch_result"
1276
+
1277
+ asa_api_invoke_with_runtime_diagnostics(
1278
+ fn = function() {
1279
+ if (is.data.frame(raw) && "asa_result" %in% names(raw)) {
1280
+ items <- lapply(raw$asa_result, asa_api_normalize_result,
1281
+ include_raw_output = include_raw_output,
1282
+ include_trace_json = include_trace_json
1283
+ )
1284
+ } else if (is.list(raw) && length(raw) && all(vapply(raw, asa_api_is_result_like, logical(1)))) {
1285
+ items <- lapply(raw, asa_api_normalize_result,
1286
+ include_raw_output = include_raw_output,
1287
+ include_trace_json = include_trace_json
1288
+ )
1289
+ } else if (is.list(raw) && length(raw) == 0L) {
1290
+ items <- list()
1291
+ } else {
1292
+ items <- list(asa_api_to_plain(raw))
1293
+ }
1294
 
1295
+ list(
1296
+ status = "success",
1297
+ results = items,
1298
+ count = length(items),
1299
+ circuit_breaker_aborted = isTRUE(attr(raw, "circuit_breaker_aborted"))
1300
+ )
1301
+ },
1302
+ mode = "asa_agent_batch",
1303
+ route = route,
1304
+ payload = payload,
1305
+ prompts = prompts,
1306
+ config = config,
1307
+ forwarded_arg_names = names(batch_args),
1308
+ request_shape = request_shape,
1309
+ stage_ref = stage_ref,
1310
+ log_con = log_con
 
 
 
 
 
1311
  )
1312
  }
1313
 
R/plumber.R CHANGED
@@ -58,16 +58,6 @@ asa_api_failure_payload <- function(res, failure) {
58
  )
59
  }
60
 
61
- asa_api_error_status <- function(message) {
62
- if (grepl("direct-provider mode|does not provide `run_direct_task`", message, ignore.case = TRUE)) {
63
- return(501L)
64
- }
65
- if (grepl("invalid json|required|must be|non-empty|unauthorized|password", message, ignore.case = TRUE)) {
66
- return(400L)
67
- }
68
- 500L
69
- }
70
-
71
  #* @filter cors
72
  function(req, res) {
73
  allow_origin <- trimws(Sys.getenv("CORS_ALLOW_ORIGIN", unset = "*"))
@@ -142,10 +132,13 @@ function(req, res) {
142
  }
143
 
144
  tryCatch(
145
- asa_api_run_single(payload, allow_direct_provider = FALSE),
 
 
 
 
146
  error = function(e) {
147
- msg <- conditionMessage(e)
148
- asa_api_error_payload(res, asa_api_error_status(msg), msg)
149
  }
150
  )
151
  }
@@ -172,10 +165,12 @@ function(req, res) {
172
  }
173
 
174
  tryCatch(
175
- asa_api_run_batch(payload),
 
 
 
176
  error = function(e) {
177
- msg <- conditionMessage(e)
178
- asa_api_error_payload(res, asa_api_error_status(msg), msg)
179
  }
180
  )
181
  }
@@ -211,11 +206,14 @@ function(req, res) {
211
 
212
  tryCatch(
213
  asa_api_sanitize_gui_result(
214
- asa_api_run_single(payload, allow_direct_provider = TRUE)
 
 
 
 
215
  ),
216
  error = function(e) {
217
- msg <- conditionMessage(e)
218
- asa_api_error_payload(res, asa_api_error_status(msg), msg)
219
  }
220
  )
221
  }
 
58
  )
59
  }
60
 
 
 
 
 
 
 
 
 
 
 
61
  #* @filter cors
62
  function(req, res) {
63
  allow_origin <- trimws(Sys.getenv("CORS_ALLOW_ORIGIN", unset = "*"))
 
132
  }
133
 
134
  tryCatch(
135
+ asa_api_run_single(
136
+ payload,
137
+ allow_direct_provider = FALSE,
138
+ request_context = list(route = "/v1/run")
139
+ ),
140
  error = function(e) {
141
+ asa_api_failure_payload(res, asa_api_error_failure(e))
 
142
  }
143
  )
144
  }
 
165
  }
166
 
167
  tryCatch(
168
+ asa_api_run_batch(
169
+ payload,
170
+ request_context = list(route = "/v1/batch")
171
+ ),
172
  error = function(e) {
173
+ asa_api_failure_payload(res, asa_api_error_failure(e))
 
174
  }
175
  )
176
  }
 
206
 
207
  tryCatch(
208
  asa_api_sanitize_gui_result(
209
+ asa_api_run_single(
210
+ payload,
211
+ allow_direct_provider = TRUE,
212
+ request_context = list(route = "/gui/query")
213
+ )
214
  ),
215
  error = function(e) {
216
+ asa_api_failure_payload(res, asa_api_error_failure(e))
 
217
  }
218
  )
219
  }
Tests/api_contract_smoke.R CHANGED
@@ -322,6 +322,86 @@ assert_true(
322
  "Route error rendering should preserve structured auth diagnostics."
323
  )
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  health_payload <- asa_api_health_payload()
326
  assert_true(
327
  identical(isTRUE(health_payload$direct_provider_available), isTRUE(asa_api_has_run_direct_task())),
@@ -364,11 +444,11 @@ asa_api_refresh_auth_cache(force = TRUE)
364
  original_direct <- asa_api_run_single_via_direct
365
 
366
  dispatch_calls <- character(0)
367
- asa_api_run_single_via_asa <- function(payload) {
368
  dispatch_calls <<- c(dispatch_calls, "asa")
369
  list(status = "success", execution = list(mode = "asa_agent"))
370
  }
371
- asa_api_run_single_via_direct <- function(payload) {
372
  dispatch_calls <<- c(dispatch_calls, "direct")
373
  list(status = "success", execution = list(mode = "provider_direct"))
374
  }
 
322
  "Route error rendering should preserve structured auth diagnostics."
323
  )
324
 
325
+ assert_true(
326
+ identical(asa_api_runtime_error_code("asa_agent_batch"), "agent_batch_failure") &&
327
+ identical(asa_api_runtime_error_code("provider_direct_single"), "direct_provider_failure") &&
328
+ identical(asa_api_runtime_error_code("asa_agent_single"), "agent_pipeline_failure"),
329
+ "Runtime failures should map to stable mode-specific error codes."
330
+ )
331
+
332
+ diagnostic_prompt <- paste(rep("diagnostic prompt segment", 16), collapse = " ")
333
+ expected_excerpt <- asa_api_redact_text_excerpt(diagnostic_prompt)
334
+ diagnostic_log_lines <- character(0)
335
+ diagnostic_log_con <- textConnection("diagnostic_log_lines", "w", local = TRUE)
336
+ runtime_error <- NULL
337
+ tryCatch(
338
+ asa_api_invoke_with_runtime_diagnostics(
339
+ fn = function() {
340
+ stop("subscript out of bounds", call. = FALSE)
341
+ },
342
+ mode = "asa_agent_single",
343
+ route = "/v1/run",
344
+ payload = list(
345
+ include_raw_output = TRUE,
346
+ include_trace_json = FALSE
347
+ ),
348
+ prompt = diagnostic_prompt,
349
+ config = list(
350
+ backend = "gemini",
351
+ model = "gemini-2.5-pro",
352
+ conda_env = "asa_env",
353
+ use_browser = FALSE
354
+ ),
355
+ forwarded_arg_names = c("output_format", "performance_profile"),
356
+ request_shape = list(
357
+ output_format = "json",
358
+ performance_profile = "quality"
359
+ ),
360
+ stage_ref = "invoke",
361
+ log_con = diagnostic_log_con
362
+ ),
363
+ error = function(e) {
364
+ runtime_error <<- e
365
+ NULL
366
+ }
367
+ )
368
+ close(diagnostic_log_con)
369
+ assert_true(
370
+ inherits(runtime_error, "asa_api_runtime_error") &&
371
+ identical(runtime_error$error_code, "agent_pipeline_failure"),
372
+ "Runtime diagnostics should rethrow a structured asa_api_runtime_error."
373
+ )
374
+ runtime_failure <- asa_api_error_failure(runtime_error)
375
+ assert_true(
376
+ identical(runtime_failure$status_code, 500L) &&
377
+ identical(runtime_failure$error_code, "agent_pipeline_failure") &&
378
+ identical(runtime_failure$details$route, "/v1/run") &&
379
+ identical(runtime_failure$details$stage, "invoke") &&
380
+ identical(runtime_failure$details$request$output_format, "json") &&
381
+ identical(runtime_failure$details$request$include_raw_output, TRUE) &&
382
+ nzchar(runtime_failure$details$diagnostic_id),
383
+ "Structured runtime failures should preserve diagnostic metadata for API responses."
384
+ )
385
+ diagnostic_log_text <- paste(diagnostic_log_lines, collapse = "\n")
386
+ assert_true(
387
+ grepl("diagnostic_id=", diagnostic_log_text, fixed = TRUE) &&
388
+ grepl("subscript out of bounds", diagnostic_log_text, fixed = TRUE),
389
+ "Runtime diagnostics should log the failure message with a correlation id."
390
+ )
391
+ assert_true(
392
+ grepl(expected_excerpt, diagnostic_log_text, fixed = TRUE) &&
393
+ !grepl(diagnostic_prompt, diagnostic_log_text, fixed = TRUE),
394
+ "Runtime diagnostics should log only a redacted prompt excerpt, not the full prompt."
395
+ )
396
+ runtime_res_env <- new.env(parent = emptyenv())
397
+ runtime_payload <- asa_api_failure_payload(runtime_res_env, runtime_failure)
398
+ assert_true(
399
+ identical(runtime_res_env$status, 500L) &&
400
+ identical(runtime_payload$error_code, "agent_pipeline_failure") &&
401
+ identical(runtime_payload$details$diagnostic_id, runtime_failure$details$diagnostic_id),
402
+ "Route error rendering should preserve structured runtime diagnostics."
403
+ )
404
+
405
  health_payload <- asa_api_health_payload()
406
  assert_true(
407
  identical(isTRUE(health_payload$direct_provider_available), isTRUE(asa_api_has_run_direct_task())),
 
444
  original_direct <- asa_api_run_single_via_direct
445
 
446
  dispatch_calls <- character(0)
447
+ asa_api_run_single_via_asa <- function(payload, request_context = NULL) {
448
  dispatch_calls <<- c(dispatch_calls, "asa")
449
  list(status = "success", execution = list(mode = "asa_agent"))
450
  }
451
+ asa_api_run_single_via_direct <- function(payload, request_context = NULL) {
452
  dispatch_calls <<- c(dispatch_calls, "direct")
453
  list(status = "success", execution = list(mode = "provider_direct"))
454
  }