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.
- {fruxon-0.7.2 → fruxon-0.8.0}/HISTORY.md +23 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/PKG-INFO +2 -2
- {fruxon-0.7.2 → fruxon-0.8.0}/README.md +1 -1
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/_version.py +2 -2
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents.py +1 -177
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents_draft.py +186 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/run.py +28 -10
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +2 -2
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-build-agent/SKILL.md +2 -2
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-debug-revision/SKILL.md +4 -4
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_cli.py +16 -13
- {fruxon-0.7.2 → fruxon-0.8.0}/.gitignore +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/LICENSE +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/pyproject.toml +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/__init__.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/__main__.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/_ssl.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/__init__.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/_schema.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/_shared.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents_budget.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents_revisions.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/agents_tests.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/auth.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/chat.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/completion.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/config.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/describe.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/doctor.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/examples.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/guides.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/integrations.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/keys.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/llm_providers.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/skills.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/tools.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli/trace.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/cli_auth.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/credentials.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/doctor.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/exceptions.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/fruxon.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/models.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/output.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/params.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/__init__.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-meet/SKILL.md +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/ui.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/update_check.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/src/fruxon/validation.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/__init__.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/conftest.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_actor.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_budgets.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_client.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_credentials.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_doctor.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_draft_evaluate_cli.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_drafts.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_fruxon.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_guides.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_output.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_params.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_schema.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_skills.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_ssl.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_test_chats.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_ui.py +0 -0
- {fruxon-0.7.2 → fruxon-0.8.0}/tests/test_update_check.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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
|
-
"
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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(
|
|
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", "
|
|
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", "
|
|
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
|
|
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
|