fruxon 0.7.2__tar.gz → 0.8.0__tar.gz

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 (72) hide show
  1. {fruxon-0.7.2 → fruxon-0.8.0}/HISTORY.md +23 -0
  2. {fruxon-0.7.2 → fruxon-0.8.0}/PKG-INFO +2 -2
  3. {fruxon-0.7.2 → fruxon-0.8.0}/README.md +1 -1
  4. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/_version.py +2 -2
  5. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents.py +1 -177
  6. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents_draft.py +186 -0
  7. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/run.py +28 -10
  8. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +2 -2
  9. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-build-agent/SKILL.md +2 -2
  10. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-debug-revision/SKILL.md +4 -4
  11. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_cli.py +16 -13
  12. {fruxon-0.7.2 → fruxon-0.8.0}/.gitignore +0 -0
  13. {fruxon-0.7.2 → fruxon-0.8.0}/LICENSE +0 -0
  14. {fruxon-0.7.2 → fruxon-0.8.0}/pyproject.toml +0 -0
  15. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/__init__.py +0 -0
  16. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/__main__.py +0 -0
  17. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/_ssl.py +0 -0
  18. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/__init__.py +0 -0
  19. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/_schema.py +0 -0
  20. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/_shared.py +0 -0
  21. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents_budget.py +0 -0
  22. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents_revisions.py +0 -0
  23. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents_tests.py +0 -0
  24. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/auth.py +0 -0
  25. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/chat.py +0 -0
  26. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/completion.py +0 -0
  27. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/config.py +0 -0
  28. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/describe.py +0 -0
  29. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/doctor.py +0 -0
  30. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/examples.py +0 -0
  31. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/guides.py +0 -0
  32. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/integrations.py +0 -0
  33. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/keys.py +0 -0
  34. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/llm_providers.py +0 -0
  35. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/skills.py +0 -0
  36. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/tools.py +0 -0
  37. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/trace.py +0 -0
  38. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli_auth.py +0 -0
  39. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/credentials.py +0 -0
  40. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/doctor.py +0 -0
  41. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/exceptions.py +0 -0
  42. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/fruxon.py +0 -0
  43. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/models.py +0 -0
  44. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/output.py +0 -0
  45. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/params.py +0 -0
  46. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/__init__.py +0 -0
  47. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
  48. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-meet/SKILL.md +0 -0
  49. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
  50. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/ui.py +0 -0
  51. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/update_check.py +0 -0
  52. {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/validation.py +0 -0
  53. {fruxon-0.7.2 → fruxon-0.8.0}/tests/__init__.py +0 -0
  54. {fruxon-0.7.2 → fruxon-0.8.0}/tests/conftest.py +0 -0
  55. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_actor.py +0 -0
  56. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_budgets.py +0 -0
  57. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_client.py +0 -0
  58. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_credentials.py +0 -0
  59. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_doctor.py +0 -0
  60. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_draft_evaluate_cli.py +0 -0
  61. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_drafts.py +0 -0
  62. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_fruxon.py +0 -0
  63. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_guides.py +0 -0
  64. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_output.py +0 -0
  65. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_params.py +0 -0
  66. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_schema.py +0 -0
  67. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_skills.py +0 -0
  68. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_ssl.py +0 -0
  69. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_test_chats.py +0 -0
  70. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_ui.py +0 -0
  71. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_update_check.py +0 -0
  72. {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_validation.py +0 -0
@@ -1,5 +1,28 @@
1
1
  # History
2
2
 
3
+ ## Unreleased — naming + response-metadata polish
4
+
5
+ - **Renamed `fruxon agents test` → `fruxon agents draft run`.** The
6
+ old verb was awkward: structurally a draft operation but living
7
+ outside the `draft` group; semantically *running* the draft but
8
+ named *test*; and one letter away from the `agents tests` history
9
+ group (different concept). The rename collapses the authoring loop
10
+ under a single subgroup — pull / push / status / run / evaluate /
11
+ discard — and removes the singular/plural ambiguity. `fruxon run`
12
+ stays as the production verb against the deployed revision; the
13
+ invariant is now clean: anything draft-y is under `draft`.
14
+ - **Surface response metadata in the human footer.** The done event
15
+ carries `agentRevision` (which revision actually ran) — useful
16
+ context that the footer dropped on the floor. Now reads
17
+ `<duration> · $<cost> · rev <N> · record <id>`. Both `run` and
18
+ `draft run` benefit.
19
+ - **Production-bucket label in the streaming banner.** `fruxon run`'s
20
+ subtitle now reads `production · streaming` / `production · sync`
21
+ / `production · revision N` (when pinned). `agents draft run`
22
+ mirrors with `draft · <agent> · base rev N`. The bucket is visible
23
+ at a glance, mirroring the CLI's typed-exit-codes posture: the
24
+ expensive operation should be the visible one.
25
+
3
26
  ## Unreleased — agent-first overhaul
4
27
 
5
28
  The CLI is now a first-class surface for AI-agent drivers (Claude
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fruxon
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Summary: The Fruxon SDK is a lightweight Python client for integrating with the Fruxon platform.
5
5
  Project-URL: bugs, https://github.com/fruxon-ai/fruxon-sdk/issues
6
6
  Project-URL: changelog, https://github.com/fruxon-ai/fruxon-sdk/blob/main/HISTORY.md
@@ -96,7 +96,7 @@ Agent authoring + management:
96
96
  | `fruxon agents schema <id>` | Typed parameter metadata — names, types, required, options. The shape `fruxon run` will accept. |
97
97
  | `fruxon agents validate <id> -p k=v` | Pre-flight a payload against the schema. Catches missing-required / wrong-type / invalid-option client-side, surfacing every finding in one pass. |
98
98
  | `fruxon agents create --file <body.json>` | Provision a new agent shell. Pair with `--schema` to print the JSON schema for the body first. |
99
- | `fruxon agents test <id> --file <def.json>` | Run a draft flow definition against an existing agent without publishing it. A CI gate for agent changes. |
99
+ | `fruxon agents draft run <id> --file <def.json>` | Run a draft flow definition against an existing agent without publishing it. A CI gate for agent changes. |
100
100
  | `fruxon agents revisions create/get/deploy` | Mint and deploy immutable revisions. `create --deploy` does both in one step. |
101
101
  | `fruxon agents draft pull/push/status/undo/redo/reset/discard` | Local working-copy authoring loop — same draft an open studio tab edits. |
102
102
  | `fruxon agents draft evaluate <id> --dataset <uuid>` | Score the draft against a golden dataset (expensive — every sample is a full agent run). |
@@ -64,7 +64,7 @@ Agent authoring + management:
64
64
  | `fruxon agents schema <id>` | Typed parameter metadata — names, types, required, options. The shape `fruxon run` will accept. |
65
65
  | `fruxon agents validate <id> -p k=v` | Pre-flight a payload against the schema. Catches missing-required / wrong-type / invalid-option client-side, surfacing every finding in one pass. |
66
66
  | `fruxon agents create --file <body.json>` | Provision a new agent shell. Pair with `--schema` to print the JSON schema for the body first. |
67
- | `fruxon agents test <id> --file <def.json>` | Run a draft flow definition against an existing agent without publishing it. A CI gate for agent changes. |
67
+ | `fruxon agents draft run <id> --file <def.json>` | Run a draft flow definition against an existing agent without publishing it. A CI gate for agent changes. |
68
68
  | `fruxon agents revisions create/get/deploy` | Mint and deploy immutable revisions. `create --deploy` does both in one step. |
69
69
  | `fruxon agents draft pull/push/status/undo/redo/reset/discard` | Local working-copy authoring loop — same draft an open studio tab edits. |
70
70
  | `fruxon agents draft evaluate <id> --dataset <uuid>` | Score the draft against a golden dataset (expensive — every sample is a full agent run). |
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.7.2'
22
- __version_tuple__ = version_tuple = (0, 7, 2)
21
+ __version__ = version = '0.8.0'
22
+ __version_tuple__ = version_tuple = (0, 8, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -19,10 +19,9 @@ import typer
19
19
 
20
20
  from fruxon.cli import app
21
21
  from fruxon.cli._shared import build_client, load_json_body
22
- from fruxon.exceptions import FruxonError, NotFoundError, StaleDraftError
22
+ from fruxon.exceptions import FruxonError, NotFoundError
23
23
  from fruxon.fruxon import Agent, FruxonClient
24
24
  from fruxon.ui import (
25
- EXIT_CONFLICT,
26
25
  EXIT_VALIDATION,
27
26
  dashboard_url,
28
27
  fail,
@@ -523,181 +522,6 @@ def _strip_markup(text: str) -> str:
523
522
  return _RICH_MARKUP_RE.sub("", text)
524
523
 
525
524
 
526
- @agents_app.command("test")
527
- def agents_test(
528
- agent: Annotated[str, typer.Argument(help="Agent identifier the draft belongs to.")],
529
- file: Annotated[
530
- str | None,
531
- typer.Option(
532
- "--file",
533
- "-f",
534
- help=(
535
- "Draft head JSON to push before running (same shape as `draft pull`/`push`). "
536
- "Pass `-` to read from stdin. Omit to test the server-side draft as-is."
537
- ),
538
- ),
539
- ] = None,
540
- param: Annotated[
541
- list[str] | None,
542
- typer.Option(
543
- "--param",
544
- "-p",
545
- help=(
546
- "Runtime parameter — `key=value` (string) or `key:=42` (typed JSON). "
547
- "Repeatable. Overlays the file's parameters."
548
- ),
549
- ),
550
- ] = None,
551
- base_revision: Annotated[
552
- int | None,
553
- typer.Option(
554
- "--base-revision",
555
- help=(
556
- "Revision the draft is based on — masked secrets resolve from its stored "
557
- "configs. Overrides the file; defaults to the agent's current revision."
558
- ),
559
- ),
560
- ] = None,
561
- session_id: Annotated[
562
- str | None,
563
- typer.Option("--session", "-s", help="Session ID for multi-turn continuity."),
564
- ] = None,
565
- org: Annotated[str | None, typer.Option("--org", help="Organization identifier override.")] = None,
566
- base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
567
- stream: Annotated[
568
- bool,
569
- typer.Option(
570
- "--stream/--no-stream",
571
- help=(
572
- "Stream the run as it happens (default). --no-stream waits for the full "
573
- "result; implied by --output json/table."
574
- ),
575
- ),
576
- ] = True,
577
- output: Annotated[
578
- str | None,
579
- typer.Option(
580
- "--output",
581
- "-o",
582
- help="Output format: text (default), json, table. json/table imply --no-stream.",
583
- ),
584
- ] = None,
585
- verbose: Annotated[
586
- bool,
587
- typer.Option("--verbose", "-v", help="Expand tool calls in the streaming view (full args + results)."),
588
- ] = False,
589
- ):
590
- """Run the server-side draft for an agent without publishing it.
591
-
592
- This is the "validate before you ship" loop. The server-side draft
593
- must already exist (``draft pull`` seeds it, ``draft push`` updates
594
- it). With ``--file`` the local draft head is pushed first, so the
595
- edit-then-test cycle is one command. Without ``--file`` the existing
596
- draft is tested as-is.
597
-
598
- The result is identical in shape to ``fruxon run`` — same streaming
599
- view, same ``--output`` formats, same duration/cost/record footer —
600
- because ``draft:test`` returns the same envelope as ``:execute``.
601
- The only difference is *what* runs: the agent's draft revision, not
602
- the deployed one.
603
-
604
- Examples:
605
- fruxon agents draft pull my-agent
606
- # edit my-agent.draft.json
607
- fruxon agents test my-agent --file my-agent.draft.json -p query="hello"
608
- fruxon agents test my-agent --no-stream -o json
609
-
610
- Procedural guide:
611
- fruxon guides show fruxon-build-agent # how to author the flow
612
- fruxon guides show fruxon-debug-revision # if validation fails
613
- """
614
- from fruxon.cli.run import _hint_for_run_error, _render_sync_result, _run_stream
615
- from fruxon.output import FORMATS
616
- from fruxon.params import ParamError
617
- from fruxon.params import parse as parse_params
618
-
619
- output = resolve_output_format(output, human_default="text", agent_default="json")
620
- if output not in FORMATS:
621
- fail(
622
- f"Unknown output format: [bold]{output}[/bold]",
623
- hint=f"Valid formats: {', '.join(FORMATS)}.",
624
- code=EXIT_VALIDATION,
625
- )
626
- # json/table need the full result envelope — silently drop streaming
627
- # rather than making the user also type --no-stream (same rule as run).
628
- if output != "text":
629
- stream = False
630
-
631
- try:
632
- flag_params = parse_params(flag_values=param, params_file=None, stdin_input=False)
633
- except ParamError as exc:
634
- fail(
635
- str(exc),
636
- hint="Run [bold]fruxon agents test --help[/bold] for accepted parameter forms.",
637
- code=EXIT_VALIDATION,
638
- )
639
-
640
- client = build_client(org, base_url)
641
-
642
- # Resolve the draft's base revision and current version. The sidecar
643
- # — written by ``draft pull`` / ``draft push`` — is the source of
644
- # truth so a CI run doesn't have to thread these through flags.
645
- meta = _read_sidecar(agent) if base_revision is None or file else None
646
- base_rev: int | str
647
- if base_revision is not None:
648
- base_rev = base_revision
649
- else:
650
- base_rev = meta["baseRevision"] # type: ignore[index]
651
- version: int | None = meta.get("version") if meta else None
652
-
653
- # With --file: push the local draft head first so the test runs
654
- # against exactly what's on disk. Idempotent — if the file matches
655
- # the last push, the server just bumps the version.
656
- if file:
657
- head = load_json_body(file, what="a draft head object")
658
- try:
659
- result_put = client.put_draft(agent, base_rev, head, if_match=version)
660
- except StaleDraftError:
661
- fail(
662
- "The draft changed on the server since you pulled.",
663
- hint=(
664
- f"Your local file is untouched. Run [bold]fruxon agents draft pull {agent}[/bold] "
665
- "to get the latest, reconcile your edits, then test again."
666
- ),
667
- code=EXIT_CONFLICT,
668
- )
669
- except FruxonError as e:
670
- fail_from_api_error(e, hint=_hint_for_run_error(e, agent=agent, client=client))
671
- version = result_put.get("version")
672
- _write_sidecar(agent, base_revision=int(base_rev), version=version, file=str(file))
673
-
674
- request: dict[str, object] = {}
675
- if flag_params:
676
- request["parameters"] = flag_params
677
- if session_id is not None:
678
- request["sessionId"] = session_id
679
-
680
- if stderr.is_terminal:
681
- print_banner(subtitle=f"test · {agent}")
682
-
683
- if stream:
684
- _run_stream(
685
- lambda: client.stream_test(agent, base_rev, request, if_match=version),
686
- agent=agent,
687
- client=client,
688
- verbose=verbose,
689
- )
690
- return
691
-
692
- with stderr.status(f"[bold]Testing draft against [cyan]{agent}[/cyan]…[/bold]"):
693
- try:
694
- result = client.test(agent, base_rev, request, if_match=version)
695
- except FruxonError as e:
696
- fail_from_api_error(e, hint=_hint_for_run_error(e, agent=agent, client=client))
697
-
698
- _render_sync_result(result, output, agent=agent)
699
-
700
-
701
525
  def _fetch_parameters_safely(client: FruxonClient, fetched: Agent) -> list[str]:
702
526
  """Fetch the deployed revision's parameter names, swallowing errors.
703
527
 
@@ -36,6 +36,7 @@ from fruxon.ui import (
36
36
  fail,
37
37
  fail_from_api_error,
38
38
  is_agent_mode,
39
+ print_banner,
39
40
  resolve_output_format,
40
41
  say_info,
41
42
  say_ok,
@@ -396,6 +397,191 @@ def draft_reset(
396
397
  _draft_step(agent, org, base_url, "reset", output)
397
398
 
398
399
 
400
+ @draft_app.command("run")
401
+ def draft_run(
402
+ agent: Annotated[str, typer.Argument(help="Agent identifier the draft belongs to.")],
403
+ file: Annotated[
404
+ str | None,
405
+ typer.Option(
406
+ "--file",
407
+ "-f",
408
+ help=(
409
+ "Draft head JSON to push before running (same shape as `draft pull`/`push`). "
410
+ "Pass `-` to read from stdin. Omit to run the server-side draft as-is."
411
+ ),
412
+ ),
413
+ ] = None,
414
+ param: Annotated[
415
+ list[str] | None,
416
+ typer.Option(
417
+ "--param",
418
+ "-p",
419
+ help=(
420
+ "Runtime parameter — `key=value` (string) or `key:=42` (typed JSON). "
421
+ "Repeatable. Overlays the file's parameters."
422
+ ),
423
+ ),
424
+ ] = None,
425
+ base_revision: Annotated[
426
+ int | None,
427
+ typer.Option(
428
+ "--base-revision",
429
+ help=(
430
+ "Revision the draft is based on — masked secrets resolve from its stored "
431
+ "configs. Overrides the file; defaults to the agent's current revision."
432
+ ),
433
+ ),
434
+ ] = None,
435
+ session_id: Annotated[
436
+ str | None,
437
+ typer.Option("--session", "-s", help="Session ID for multi-turn continuity."),
438
+ ] = None,
439
+ org: Annotated[str | None, typer.Option("--org", help="Organization identifier override.")] = None,
440
+ base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
441
+ stream: Annotated[
442
+ bool,
443
+ typer.Option(
444
+ "--stream/--no-stream",
445
+ help=(
446
+ "Stream the run as it happens (default). --no-stream waits for the full "
447
+ "result; implied by --output json/table."
448
+ ),
449
+ ),
450
+ ] = True,
451
+ output: Annotated[
452
+ str | None,
453
+ typer.Option(
454
+ "--output",
455
+ "-o",
456
+ help="Output format: text (default), json, table. json/table imply --no-stream.",
457
+ ),
458
+ ] = None,
459
+ verbose: Annotated[
460
+ bool,
461
+ typer.Option("--verbose", "-v", help="Expand tool calls in the streaming view (full args + results)."),
462
+ ] = False,
463
+ ):
464
+ """Run the agent's draft revision — same shape as ``fruxon run``, on the draft.
465
+
466
+ This is the "validate before you ship" loop. The server-side draft
467
+ must already exist (``draft pull`` seeds it, ``draft push`` updates
468
+ it). With ``--file`` the local draft head is pushed first, so the
469
+ edit-then-run cycle is one command. Without ``--file`` the existing
470
+ draft is run as-is.
471
+
472
+ The result is identical in shape to ``fruxon run`` — same streaming
473
+ view, same ``--output`` formats, same duration/cost/record footer —
474
+ because ``draft:test`` returns the same envelope as ``:execute``.
475
+ The only difference is *what* runs: the agent's draft revision (Origin=
476
+ TEST, owner-scoped, doesn't pollute prod metrics or budgets), not the
477
+ deployed one.
478
+
479
+ The split mirrors the rest of ``draft``: every operation against the
480
+ working copy lives under this group. ``fruxon run`` (no subgroup) is
481
+ reserved for production traffic against the deployed revision.
482
+
483
+ Examples:
484
+ fruxon agents draft pull my-agent
485
+ # edit my-agent.draft.json
486
+ fruxon agents draft run my-agent --file my-agent.draft.json -p query="hello"
487
+ fruxon agents draft run my-agent --no-stream -o json
488
+
489
+ Procedural guide:
490
+ fruxon guides show fruxon-build-agent # how to author the flow
491
+ fruxon guides show fruxon-debug-revision # if validation fails
492
+ """
493
+ from fruxon.cli._shared import load_json_body
494
+ from fruxon.cli.run import _hint_for_run_error, _render_sync_result, _run_stream
495
+ from fruxon.output import FORMATS
496
+ from fruxon.params import ParamError
497
+ from fruxon.params import parse as parse_params
498
+
499
+ output = resolve_output_format(output, human_default="text", agent_default="json")
500
+ if output not in FORMATS:
501
+ fail(
502
+ f"Unknown output format: [bold]{output}[/bold]",
503
+ hint=f"Valid formats: {', '.join(FORMATS)}.",
504
+ code=EXIT_VALIDATION,
505
+ )
506
+ # json/table need the full result envelope — silently drop streaming
507
+ # rather than making the user also type --no-stream (same rule as run).
508
+ if output != "text":
509
+ stream = False
510
+
511
+ try:
512
+ flag_params = parse_params(flag_values=param, params_file=None, stdin_input=False)
513
+ except ParamError as exc:
514
+ fail(
515
+ str(exc),
516
+ hint="Run [bold]fruxon agents draft run --help[/bold] for accepted parameter forms.",
517
+ code=EXIT_VALIDATION,
518
+ )
519
+
520
+ client = build_client(org, base_url)
521
+
522
+ # Resolve the draft's base revision and current version. The sidecar
523
+ # — written by ``draft pull`` / ``draft push`` — is the source of
524
+ # truth so a CI run doesn't have to thread these through flags.
525
+ meta = _read_sidecar(agent) if base_revision is None or file else None
526
+ base_rev: int | str
527
+ if base_revision is not None:
528
+ base_rev = base_revision
529
+ else:
530
+ base_rev = meta["baseRevision"] # type: ignore[index]
531
+ version: int | None = meta.get("version") if meta else None
532
+
533
+ # With --file: push the local draft head first so the run uses
534
+ # exactly what's on disk. Idempotent — if the file matches the
535
+ # last push, the server just bumps the version.
536
+ if file:
537
+ head = load_json_body(file, what="a draft head object")
538
+ try:
539
+ result_put = client.put_draft(agent, base_rev, head, if_match=version)
540
+ except StaleDraftError:
541
+ fail(
542
+ "The draft changed on the server since you pulled.",
543
+ hint=(
544
+ f"Your local file is untouched. Run [bold]fruxon agents draft pull {agent}[/bold] "
545
+ "to get the latest, reconcile your edits, then run again."
546
+ ),
547
+ code=EXIT_CONFLICT,
548
+ )
549
+ except FruxonError as e:
550
+ fail_from_api_error(e, hint=_hint_for_run_error(e, agent=agent, client=client))
551
+ version = result_put.get("version")
552
+ _write_sidecar(agent, base_revision=int(base_rev), version=version, file=str(file))
553
+
554
+ request: dict[str, object] = {}
555
+ if flag_params:
556
+ request["parameters"] = flag_params
557
+ if session_id is not None:
558
+ request["sessionId"] = session_id
559
+
560
+ if stderr.is_terminal:
561
+ # Surface ``draft · rev N`` in the header so the user (or a code
562
+ # reviewer reading scrollback) can tell at a glance which bucket
563
+ # this ran against. ``fruxon run`` mirrors the same pattern with
564
+ # ``production · rev N`` — keeps the two paths visibly distinct.
565
+ print_banner(subtitle=f"draft · {agent} · base rev {base_rev}")
566
+
567
+ if stream:
568
+ _run_stream(
569
+ lambda: client.stream_test(agent, base_rev, request, if_match=version),
570
+ agent=agent,
571
+ client=client,
572
+ verbose=verbose,
573
+ )
574
+ return
575
+
576
+ with stderr.status(f"[bold]Running draft for [cyan]{agent}[/cyan]…[/bold]"):
577
+ try:
578
+ result = client.test(agent, base_rev, request, if_match=version)
579
+ except FruxonError as e:
580
+ fail_from_api_error(e, hint=_hint_for_run_error(e, agent=agent, client=client))
581
+
582
+ _render_sync_result(result, output, agent=agent)
583
+
584
+
399
585
  @draft_app.command("evaluate")
400
586
  def draft_evaluate(
401
587
  agent: Annotated[str, typer.Argument(help="Agent identifier the draft belongs to.")],
@@ -230,8 +230,19 @@ def run(
230
230
  # stdout are all unaffected. Skip when stderr is being redirected too —
231
231
  # if both streams are captured the user is almost certainly scripting
232
232
  # and doesn't want ANSI chrome in their log file.
233
+ #
234
+ # ``production`` label is on the subtitle so the bucket is visible
235
+ # at a glance — the mirror of ``draft · …`` on ``agents draft run``.
236
+ # When a revision is pinned explicitly we surface its number; the
237
+ # deployed-revision number isn't known until the ``done`` event
238
+ # arrives, so for the default path we just say "production".
233
239
  if stderr.is_terminal:
234
- mode_subtitle = "streaming" if stream else (f"revision {revision}" if revision else "sync")
240
+ if revision:
241
+ mode_subtitle = f"production · revision {revision}"
242
+ elif stream:
243
+ mode_subtitle = "production · streaming"
244
+ else:
245
+ mode_subtitle = "production · sync"
235
246
  print_banner(subtitle=mode_subtitle)
236
247
 
237
248
  # Parse all the param input forms — flag values, JSON file, stdin —
@@ -1257,25 +1268,32 @@ def _truncate(text: str, max_len: int) -> str:
1257
1268
 
1258
1269
 
1259
1270
  def _render_done(data: dict[str, object], *, agent: str | None = None) -> None:
1260
- """Render the final-event footer with duration, cost, and record ID.
1261
-
1262
- The execution record ID is the user's handle to follow up via
1263
- ``fruxon trace`` printing it here is what makes that command
1264
- discoverable in practice. When the agent name is known we follow up
1265
- with a copy-pasteable ``fruxon trace`` line so the user doesn't have
1266
- to assemble it themselves. Falls back gracefully when any of the
1267
- three are absent so partial server payloads still produce a useful
1268
- line.
1271
+ """Render the final-event footer with duration, cost, revision, and record ID.
1272
+
1273
+ Pulls the headline fields from the done event's ``trace`` envelope.
1274
+ Each field is included only when the server actually returned it —
1275
+ a partial payload (older backend, abbreviated stream) still produces
1276
+ a useful line. The order is read-left-to-right "what cost what":
1277
+ duration cost which revision ran record id → trace pointer.
1278
+
1279
+ ``agentRevision`` matters because the user needs to know *what*
1280
+ just executed — same agent id can be a draft run, a pinned old
1281
+ revision, or the currently-deployed one. Without surfacing it,
1282
+ "did my new revision actually run?" becomes a separate ``trace``
1283
+ call.
1269
1284
  """
1270
1285
  trace = data.get("trace") if isinstance(data.get("trace"), dict) else {}
1271
1286
  duration = trace.get("duration") if isinstance(trace, dict) else None
1272
1287
  total_cost = trace.get("totalCost") if isinstance(trace, dict) else None
1288
+ agent_revision = trace.get("agentRevision") if isinstance(trace, dict) else None
1273
1289
  record_id = data.get("executionRecordId")
1274
1290
  parts: list[str] = []
1275
1291
  if isinstance(duration, (int, float)) and duration:
1276
1292
  parts.append(format_duration(duration))
1277
1293
  if isinstance(total_cost, (int, float)) and total_cost:
1278
1294
  parts.append(f"${float(total_cost):.4f}")
1295
+ if isinstance(agent_revision, (int, float)) and agent_revision:
1296
+ parts.append(f"rev [bold]{int(agent_revision)}[/bold]")
1279
1297
  if isinstance(record_id, str) and record_id:
1280
1298
  parts.append(f"record [cyan]{record_id}[/cyan]")
1281
1299
  if parts:
@@ -164,7 +164,7 @@ of the trace fields — same `type: "done"`, distinguished by the
164
164
  "status":"waiting_for_human","human_approval_request_id":"har-7"}
165
165
  ```
166
166
 
167
- **Step traces.** `fruxon agents test` additionally emits
167
+ **Step traces.** `fruxon agents draft run` additionally emits
168
168
  `{"type":"step_trace","id":"…","name":"…","step_type":"LlmStep",
169
169
  "status":"succeeded","duration_ms":1234}` when each flow step
170
170
  finishes — useful for CI gates that need per-step cost attribution.
@@ -221,7 +221,7 @@ the bypass flag.
221
221
  | `fruxon login` (key already stored) | Pass `--api-key …` to replace, or `fruxon logout` first |
222
222
  | `fruxon run --edit` | Pass the value via `-p key=value` or `--params file.json` |
223
223
  | `keys delete` (no --yes) | Pass `--yes` |
224
- | `agents test chats delete` (no --yes) | Pass `--yes` |
224
+ | `agents tests delete` (no --yes) | Pass `--yes` |
225
225
  | `agents budget delete` (no --yes) | Pass `--yes` |
226
226
  | `agents draft evaluate` (no --yes) | Pass `--yes` — it costs real money |
227
227
 
@@ -42,7 +42,7 @@ revision from it.
42
42
  5. **Sync the draft**: `fruxon agents draft push <agent>` saves your
43
43
  edits to the server-side draft. It is visible in an open studio
44
44
  within ~1s, and reversible with `fruxon agents draft undo`.
45
- 6. **Test**: `fruxon agents test <agent> --file <agent>.draft.json -p k=v`
45
+ 6. **Test**: `fruxon agents draft run <agent> --file <agent>.draft.json -p k=v`
46
46
  runs the body like a normal run. The execution IS persisted, but
47
47
  stamped with `Origin=TEST` so it never mixes with production
48
48
  metrics (see "Origins" below). In agent mode this emits NDJSON
@@ -79,7 +79,7 @@ Every execution is tagged with an **origin** at the moment it runs:
79
79
 
80
80
  - **`PRODUCTION`** — real end-user traffic from a connector, the
81
81
  gateway, scheduled triggers, etc. The `:execute` path.
82
- - **`TEST`** — your own development runs from `fruxon agents test`
82
+ - **`TEST`** — your own development runs from `fruxon agents draft run`
83
83
  / `:streamTest` / the studio "Run test" button. Owner-scoped
84
84
  (you only ever see your own test rows; admins see the org-wide
85
85
  total).
@@ -13,7 +13,7 @@ You are now operating as a Fruxon revision debugging specialist.
13
13
  1. Read the exact error from the failing CLI call (don't guess).
14
14
  2. Map the error to one of the categories below.
15
15
  3. Apply the targeted fix.
16
- 4. Re-run with `fruxon agents test <agent> --file fixed.json` first;
16
+ 4. Re-run with `fruxon agents draft run <agent> --file fixed.json` first;
17
17
  only re-create the revision when the test passes.
18
18
 
19
19
  ## Reading errors (agent mode vs human mode)
@@ -90,7 +90,7 @@ integration tool on `step.tools` references an
90
90
  `integrationConfigId` that doesn't appear in
91
91
  `revision.integrationConfigs`. Add the config or remove the tool.
92
92
 
93
- ## Runtime errors during `fruxon agents test`
93
+ ## Runtime errors during `fruxon agents draft run`
94
94
  **Tool unknown / no LLM tool registered for X** — usually means a
95
95
  built-in tool ended up on `step.tools` instead of
96
96
  `step.provider.builtInTools`. Move it.
@@ -114,7 +114,7 @@ shows whether the local and server versions have diverged.
114
114
  missing required / unknown / wrong-type / invalid-option errors
115
115
  client-side, surfacing every finding in one pass (no round-trip
116
116
  per error). Exits 12 on failure with a structured `errors` list.
117
- - **Test before you create**: `fruxon agents test` catches most
117
+ - **Test before you create**: `fruxon agents draft run` catches most
118
118
  errors without persisting a bad revision.
119
119
  - **Diff against the schema**: re-run `fruxon agents revisions create
120
120
  --schema` and compare your body field-by-field for missing /
@@ -124,7 +124,7 @@ shows whether the local and server versions have diverged.
124
124
  - **Read the trace**: after a failed run,
125
125
  `fruxon trace <agent> <record-id>` shows step-by-step status —
126
126
  which step failed and why.
127
- - **Replay your test history**: every `fruxon agents test` run
127
+ - **Replay your test history**: every `fruxon agents draft run` run
128
128
  persists server-side tagged `Origin=TEST` (owner-scoped). Useful
129
129
  when iterating on a hard-to-reproduce issue:
130
130
  ```
@@ -1555,7 +1555,7 @@ class TestAgents:
1555
1555
  self._seed_sidecar(monkeypatch, tmp_path)
1556
1556
  draft = tmp_path / "agent.json"
1557
1557
  draft.write_text('{"flow": {}}')
1558
- result = runner.invoke(app, ["agents", "test", "agent-1", "--file", str(draft)])
1558
+ result = runner.invoke(app, ["agents", "draft", "run", "agent-1", "--file", str(draft)])
1559
1559
  assert result.exit_code == EXIT_AUTH_REQUIRED
1560
1560
  assert "No API key" in result.stderr or "fruxon login" in result.stderr
1561
1561
 
@@ -1565,7 +1565,7 @@ class TestAgents:
1565
1565
  draft = tmp_path / "agent.json"
1566
1566
  draft.write_text('{"flow": {"steps": []}}')
1567
1567
  captured = self._patch_client(monkeypatch, test_result=_result("draft says hi"))
1568
- result = runner.invoke(app, ["agents", "test", "agent-1", "--file", str(draft), "--no-stream"])
1568
+ result = runner.invoke(app, ["agents", "draft", "run", "agent-1", "--file", str(draft), "--no-stream"])
1569
1569
  assert result.exit_code == 0, result.stderr
1570
1570
  # File pushed via put_draft using sidecar's base revision + version.
1571
1571
  put_agent, put_rev, put_head, put_if_match = captured["put_draft_args"]
@@ -1589,7 +1589,7 @@ class TestAgents:
1589
1589
  captured = self._patch_client(monkeypatch, test_result=_result())
1590
1590
  result = runner.invoke(
1591
1591
  app,
1592
- ["agents", "test", "agent-1", "--file", str(draft), "--no-stream", "-p", "q=hi", "-p", "lang=en"],
1592
+ ["agents", "draft", "run", "agent-1", "--file", str(draft), "--no-stream", "-p", "q=hi", "-p", "lang=en"],
1593
1593
  )
1594
1594
  assert result.exit_code == 0, result.stderr
1595
1595
  _, request = captured["test_args"]
@@ -1605,7 +1605,8 @@ class TestAgents:
1605
1605
  app,
1606
1606
  [
1607
1607
  "agents",
1608
- "test",
1608
+ "draft",
1609
+ "run",
1609
1610
  "agent-1",
1610
1611
  "--file",
1611
1612
  str(draft),
@@ -1627,7 +1628,7 @@ class TestAgents:
1627
1628
  credentials.save(credentials.StoredCredentials(api_key="fxn_x", org="acme"))
1628
1629
  self._seed_sidecar(monkeypatch, tmp_path)
1629
1630
  captured = self._patch_client(monkeypatch, test_result=_result())
1630
- result = runner.invoke(app, ["agents", "test", "agent-1", "--no-stream"])
1631
+ result = runner.invoke(app, ["agents", "draft", "run", "agent-1", "--no-stream"])
1631
1632
  assert result.exit_code == 0, result.stderr
1632
1633
  assert "put_draft_args" not in captured
1633
1634
  assert captured["test_base_revision"] == 2
@@ -1647,7 +1648,7 @@ class TestAgents:
1647
1648
  StreamEvent(event="done", data={"trace": {}, "executionRecordId": "rec-stream-9"}),
1648
1649
  ]
1649
1650
  self._patch_client(monkeypatch, test_stream=events)
1650
- result = runner.invoke(app, ["agents", "test", "agent-1", "--file", str(draft)])
1651
+ result = runner.invoke(app, ["agents", "draft", "run", "agent-1", "--file", str(draft)])
1651
1652
  assert result.exit_code == 0, result.stderr
1652
1653
  assert "draft output" in result.stdout
1653
1654
  assert "rec-stream-9" in result.stderr
@@ -1658,7 +1659,7 @@ class TestAgents:
1658
1659
  draft = tmp_path / "agent.json"
1659
1660
  draft.write_text('{"flow": {}}')
1660
1661
  captured = self._patch_client(monkeypatch, test_result=_result("json body"))
1661
- result = runner.invoke(app, ["agents", "test", "agent-1", "--file", str(draft), "--output", "json"])
1662
+ result = runner.invoke(app, ["agents", "draft", "run", "agent-1", "--file", str(draft), "--output", "json"])
1662
1663
  assert result.exit_code == 0, result.stderr
1663
1664
  assert "test_args" in captured
1664
1665
  import json as json_mod
@@ -1672,7 +1673,7 @@ class TestAgents:
1672
1673
  captured = self._patch_client(monkeypatch, test_result=_result())
1673
1674
  result = runner.invoke(
1674
1675
  app,
1675
- ["agents", "test", "agent-1", "--file", "-", "--no-stream"],
1676
+ ["agents", "draft", "run", "agent-1", "--file", "-", "--no-stream"],
1676
1677
  input='{"flow": {"piped": true}}',
1677
1678
  )
1678
1679
  assert result.exit_code == 0, result.stderr
@@ -1685,7 +1686,7 @@ class TestAgents:
1685
1686
  draft = tmp_path / "agent.json"
1686
1687
  draft.write_text("{not valid json")
1687
1688
  self._patch_client(monkeypatch, test_result=_result())
1688
- result = runner.invoke(app, ["agents", "test", "agent-1", "--file", str(draft), "--no-stream"])
1689
+ result = runner.invoke(app, ["agents", "draft", "run", "agent-1", "--file", str(draft), "--no-stream"])
1689
1690
  assert result.exit_code == EXIT_VALIDATION
1690
1691
  assert "not valid JSON" in result.stderr
1691
1692
 
@@ -1695,7 +1696,7 @@ class TestAgents:
1695
1696
  draft = tmp_path / "agent.json"
1696
1697
  draft.write_text('["a", "list"]')
1697
1698
  self._patch_client(monkeypatch, test_result=_result())
1698
- result = runner.invoke(app, ["agents", "test", "agent-1", "--file", str(draft), "--no-stream"])
1699
+ result = runner.invoke(app, ["agents", "draft", "run", "agent-1", "--file", str(draft), "--no-stream"])
1699
1700
  assert result.exit_code == EXIT_VALIDATION
1700
1701
  assert "must contain a JSON object" in result.stderr
1701
1702
 
@@ -1703,7 +1704,9 @@ class TestAgents:
1703
1704
  credentials.save(credentials.StoredCredentials(api_key="fxn_x", org="acme"))
1704
1705
  self._seed_sidecar(monkeypatch, tmp_path)
1705
1706
  self._patch_client(monkeypatch, test_result=_result())
1706
- result = runner.invoke(app, ["agents", "test", "agent-1", "--file", "/nonexistent/agent.json", "--no-stream"])
1707
+ result = runner.invoke(
1708
+ app, ["agents", "draft", "run", "agent-1", "--file", "/nonexistent/agent.json", "--no-stream"]
1709
+ )
1707
1710
  assert result.exit_code == EXIT_VALIDATION
1708
1711
  assert "Couldn't read file" in result.stderr
1709
1712
 
@@ -1712,7 +1715,7 @@ class TestAgents:
1712
1715
  credentials.save(credentials.StoredCredentials(api_key="fxn_x", org="acme"))
1713
1716
  monkeypatch.chdir(tmp_path)
1714
1717
  self._patch_client(monkeypatch, test_result=_result())
1715
- result = runner.invoke(app, ["agents", "test", "agent-1", "--no-stream"])
1718
+ result = runner.invoke(app, ["agents", "draft", "run", "agent-1", "--no-stream"])
1716
1719
  assert result.exit_code == EXIT_VALIDATION
1717
1720
  assert "draft pull" in result.stderr
1718
1721
 
@@ -1727,7 +1730,7 @@ class TestAgents:
1727
1730
  monkeypatch,
1728
1731
  test_raises=ForbiddenError(status=403, title="Forbidden", detail="not an editor"),
1729
1732
  )
1730
- result = runner.invoke(app, ["agents", "test", "agent-1", "--file", str(draft), "--no-stream"])
1733
+ result = runner.invoke(app, ["agents", "draft", "run", "agent-1", "--file", str(draft), "--no-stream"])
1731
1734
  # 403 maps to auth_required — same class as 401, since recovery
1732
1735
  # is the same (rotate creds / get the right scope).
1733
1736
  assert result.exit_code == EXIT_AUTH_REQUIRED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes