Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,14 @@ HARD CONTRACT (violating ANY of these breaks launch or deployment — full expla
- NEVER hardcode an absolute URL in the source — no `fetch('http://localhost:PORT/...')`, no `fetch('https://api.example.com/...')`, no `const API_BASE = 'http://...'`. The meta tag is the ONLY place the base URL is configured.

6. LAUNCH THE BACKEND: Call the `launch_backend` tool with the artifact's slug:
- `launch_backend(slug=<slug>)` — the tool picks a free port, spawns `python backend.py --port <port>` as a standalone process with `<artifact_path>` as cwd, waits for readiness, writes the port into `metadata.json`, and returns `{slug, port, pid, url, log_path}` as JSON.
- `launch_backend(slug=<slug>)` — the tool picks a free port, spawns `python backend.py --port <port>` as a standalone process with `<artifact_path>` as cwd, waits for readiness, writes the port into `metadata.json`, and returns a JSON envelope: the URL in `external_url` and `{slug, port, pid, log_path}` under `details`.
- Uses the scratchpad named `<slug>` — created automatically on first call. If `<artifact_path>/requirements.txt` exists, its packages are installed into that scratchpad's venv before spawn (install output is appended to `backend.log` with a banner). An install failure aborts the launch and is returned as an error string — fix `requirements.txt` and retry.
- Backend stdout/stderr stream to `<artifact_path>/backend.log` — read it if the launch fails or the API misbehaves.
- Do NOT call `update_artifact(port=...)` manually — `launch_backend` does it.
- The launched process outlives the scratchpad cell and is reaped automatically when the Anton session ends.
- Calling `launch_backend` again for the same slug terminates the previous process and starts a fresh one — use this for hot reloads after code changes.

7. PREVIEW THE APPLICATION: Direct the user to the `url` returned by `launch_backend` (e.g. http://127.0.0.1:54321):
7. PREVIEW THE APPLICATION: Direct the user to the `external_url` returned by `launch_backend` (e.g. http://127.0.0.1:54321):
- CRITICAL: Open that URL, NOT the HTML file from disk (file://...). The backend serves the frontend at `/`, so opening the URL loads the page and its `fetch()` calls land on the same origin.
- If the user opens the HTML file directly from disk, `fetch()` calls fail due to browser CORS/file:// restrictions.

Expand All @@ -174,5 +174,5 @@ DEPLOYMENT NOTES:
- The local backend process shuts down when the Anton CLI session ends (per MVP constraints).

PUBLISH OR SHARE:
- After building, offer to preview the frontend by directing the user to the URL returned by `launch_backend`
- After building, offer to preview the frontend by directing the user to the `external_url` returned by `launch_backend`
- The backend must be running for the frontend to work
79 changes: 79 additions & 0 deletions anton/core/tools/side_effect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Structured result contract for side-effecting tools (ENG-696).

Side-effecting tools (publish, create/update artifact, launch backend) used to
return a human-readable string or an ad-hoc JSON dict. Neither made the
*committed state* machine-readable: a consumer (eval, monitoring, the model
itself) could not reliably tell what was committed, where it lives, or whether a
retry would duplicate it.

`SideEffectResult` is the single envelope every such tool returns. It carries:

- `success` — did the side effect commit.
- `resource_id` — stable identity of the committed resource (slug / report id).
- `external_url` — where the resource can be reached, if it has a URL.
- `idempotency_key` — stable key for the operation; a re-run with the same key
targets the same resource (dedup handle), so retries are recognisable.
- `committed_at` — ISO-8601 UTC instant the side effect committed; `None` when
nothing was committed (validation failure, pre-commit error).
- `content_hash` — hash of the committed content, when the tool has content.
- `details` — tool-specific machine-readable fields that don't fit the common
ones above (e.g. launch_backend's `port` / `pid` / `log_path`); `None` when
the tool has no extras.
- `message` — the human-readable line kept for the model / desktop UI.

It serialises to a JSON string inside a `ToolOutcome` and sets `ToolOutcome.ok`
from `success`, so the ENG-1276 error streak keys on the explicit verdict.
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime, timezone

from anton.core.tools.registry import ToolOutcome


def now_iso() -> str:
"""Current instant as an ISO-8601 UTC string (the `committed_at` basis)."""
return datetime.now(timezone.utc).isoformat()


@dataclass
class SideEffectResult:
"""Machine-readable outcome of a side-effecting tool call (see module doc)."""

success: bool
message: str
resource_id: str | None = None
external_url: str | None = None
idempotency_key: str | None = None
committed_at: str | None = None
content_hash: str | None = None
details: dict | None = None

def to_outcome(self, reason: str = "") -> ToolOutcome:
"""Render to a `ToolOutcome`: JSON payload as content, verdict as `ok`."""
payload = {
"success": self.success,
"message": self.message,
"resource_id": self.resource_id,
"external_url": self.external_url,
"idempotency_key": self.idempotency_key,
"committed_at": self.committed_at,
"content_hash": self.content_hash,
"details": self.details,
}
# `details` is a free-form per-tool dict; `default=str` keeps a stray
# non-JSON value (Path, datetime) from crashing the whole tool — which
# dispatch would otherwise surface as a spurious failure.
return ToolOutcome(
content=json.dumps(payload, indent=2, default=str),
ok=self.success,
reason=reason,
)

@classmethod
def failed(cls, message: str, reason: str = "") -> ToolOutcome:
"""Shorthand for a non-committing failure (`committed_at` stays None)."""
return cls(success=False, message=message).to_outcome(reason=reason)
7 changes: 4 additions & 3 deletions anton/core/tools/tool_defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,9 @@ class ToolDef:
"Start an artifact's backend script as a standalone subprocess. "
"Picks a free TCP port, runs the script with `--port <port>` "
"(plus any `extra_args`), waits until the server is reachable, "
"records the port in the artifact's `metadata.json`, and returns "
"`{slug, port, pid, url, log_path}` as JSON.\n\n"
"records the port in the artifact's `metadata.json`, and returns a "
"structured result carrying the backend's `url` (as `external_url`), "
"plus `pid`/`port`/log path in the message.\n\n"
"The backend MUST follow the contract in the `build-fullstack-backend` "
"skill (template, `--port`, `/api/*` prefix, SECRETS). If you haven't "
"recalled it this conversation, call "
Expand All @@ -347,7 +348,7 @@ class ToolDef:
"If `<artifact_folder>/requirements.txt` exists, its package lines are "
"installed into that scratchpad's venv before spawn — install output "
"appended to `backend.log`, install failures abort the launch and are "
"returned as an error string. Only simple lines are supported "
"returned as a failure result. Only simple lines are supported "
"(`pkg` / `pkg==1.2`); blank lines, `#` comments, and `-`-prefixed "
"flags (`-r`, `-e`, `--index-url`) are ignored.\n\n"
"Idempotent: a second call with the same slug terminates the "
Expand Down
Loading
Loading