antpath 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shared/operations.d.ts +33 -0
- package/dist/_shared/operations.js +43 -0
- package/dist/_shared/proxy-protocol.d.ts +10 -2
- package/dist/_shared/proxy-protocol.js +3 -2
- package/dist/_shared/runtime-types.d.ts +19 -0
- package/dist/_shared/submission.d.ts +21 -0
- package/dist/_shared/submission.js +93 -3
- package/dist/cli.mjs +116 -35
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +69 -0
- package/dist/client.js +67 -1
- package/dist/client.js.map +1 -1
- package/dist/proxy-endpoint.d.ts +26 -7
- package/dist/proxy-endpoint.js +19 -3
- package/dist/proxy-endpoint.js.map +1 -1
- package/dist/skill.d.ts +41 -0
- package/dist/skill.js +52 -0
- package/dist/skill.js.map +1 -1
- package/docs/credentials.md +34 -0
- package/docs/events.md +7 -0
- package/docs/mcp.md +28 -0
- package/docs/outputs.md +92 -12
- package/docs/quickstart.md +37 -0
- package/docs/skills.md +49 -0
- package/package.json +1 -1
package/docs/outputs.md
CHANGED
|
@@ -4,24 +4,104 @@ title: Outputs
|
|
|
4
4
|
|
|
5
5
|
# Outputs
|
|
6
6
|
|
|
7
|
-
Every run
|
|
7
|
+
Every run produces durable metadata (status, events, snapshots, cleanup state). File bytes are **opt-in** via the submission's `outputDirs` field. There is a single method, `RunRef.download()`, that returns the whole run — metadata plus opt-in file bytes — as a streaming zip.
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
8
10
|
|
|
9
11
|
```ts
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
+
const ref = await client.submitRun({
|
|
13
|
+
model: "claude-opus-4-7",
|
|
14
|
+
prompt: "Produce a report and stash working files",
|
|
15
|
+
outputDirs: ["/workspace/outputs", "/workspace/state"],
|
|
16
|
+
secrets: { anthropic: { apiKey } }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await ref.download({ to: "./run-archive.zip" });
|
|
12
20
|
```
|
|
13
21
|
|
|
14
22
|
```bash
|
|
15
|
-
antpath
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
antpath download <run-id> --out ./run-archive.zip --api-token …
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What `download()` returns
|
|
27
|
+
|
|
28
|
+
A zip with this layout:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
run.json # run record (status, runId, timestamps, snapshot)
|
|
32
|
+
events.jsonl # one event per line, ordered
|
|
33
|
+
outputs/<name> # one file per captured output object
|
|
34
|
+
manifest.json # { source, partial, outputs, missing }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`manifest.json` carries:
|
|
38
|
+
|
|
39
|
+
| Field | Meaning |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `source: "live" \| "s3"` | Where file bytes came from. `live` = streamed from Anthropic Files API (mid-session); `s3` = streamed from Supabase Storage (after capture completed). |
|
|
42
|
+
| `partial: boolean` | `true` if the run had not finished capture yet — more bytes may exist on a later call. |
|
|
43
|
+
| `outputs[]` | `{ id, path, byteSize, contentType? }` — each row corresponds to a file under `outputs/` in the zip. |
|
|
44
|
+
| `missing[]` | `{ id?, path?, reason, message? }` — anything the platform tried to include but could not. Reasons documented below. |
|
|
45
|
+
|
|
46
|
+
## Lifecycle behaviour
|
|
47
|
+
|
|
48
|
+
`download()` is lifecycle-aware in one method. You never branch on state — check `manifest.partial` if you care about staleness.
|
|
49
|
+
|
|
50
|
+
| Run state | Behaviour |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `pending` / `queued` / `provisioning` | HTTP 409 `run_not_started`. Wait for the run to start. |
|
|
53
|
+
| `provider_running`, mid-session | Live: stream-zip from Anthropic Files API + DB metadata/events. `source: "live"`, `partial: true`. |
|
|
54
|
+
| `cleaning_up` | Same as `provider_running` until S3 capture completes; then same as terminal. |
|
|
55
|
+
| `succeeded` / `failed` / `cancelled` / `terminated` | Terminal: stream-zip from S3 `output_objects` + DB. `source: "s3"`, `partial: false`. |
|
|
56
|
+
|
|
57
|
+
## `outputDirs` — opt-in file capture
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
client.submitRun({
|
|
61
|
+
/* ... */,
|
|
62
|
+
outputDirs: ["/workspace/outputs", "/workspace/state"]
|
|
63
|
+
});
|
|
18
64
|
```
|
|
19
65
|
|
|
20
|
-
|
|
66
|
+
Validation:
|
|
67
|
+
|
|
68
|
+
- absolute UNIX paths only (`/...`),
|
|
69
|
+
- no `..` segments, no NUL bytes,
|
|
70
|
+
- maximum 32 entries,
|
|
71
|
+
- maximum 512 bytes per entry.
|
|
72
|
+
|
|
73
|
+
Mechanism (no platform-magical paths — this is honest):
|
|
74
|
+
|
|
75
|
+
1. Worker submits the run, sends the user prompt, streams events.
|
|
76
|
+
2. At session-idle (the agent's primary task is done), the worker sends one synthetic `user.message` to the agent:
|
|
77
|
+
*"Run `/antpath/antpath outputs sync <dirs>` once."*
|
|
78
|
+
3. The agent runs that command via its bash tool. The in-container CLI walks the listed dirs, prints a JSON line per file, and the agent's tool output triggers Anthropic Managed Agents' file registration.
|
|
79
|
+
4. Worker walks the Files API, copies bytes into Supabase Storage, and tears down the session.
|
|
80
|
+
|
|
81
|
+
Cost: one extra agent turn (~hundreds of tokens, observable in `span.model_request_*` events). Document this against your token budget if you submit very high-volume runs.
|
|
82
|
+
|
|
83
|
+
Failure modes — anything that did not make it into the zip lands in `manifest.missing[]`:
|
|
84
|
+
|
|
85
|
+
| `reason` | What happened |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `agent_did_not_sync` | The agent refused or skipped the synthetic instruction. Run still succeeded, just no file bytes. |
|
|
88
|
+
| `agent_reported_error` | The `/antpath/antpath outputs sync` invocation returned non-zero (e.g. dir did not exist). |
|
|
89
|
+
| `session_terminated_pre_sync` | Session was terminated (cancel / timeout) before the sync turn ran. |
|
|
90
|
+
| `storage_cap_exceeded` | Workspace storage quota would have been breached. |
|
|
91
|
+
| `download_failed` | Files API entry could not be fetched. |
|
|
92
|
+
| `pending_session_terminal` | Mid-session download — file may show up on a later `download()` once the session reaches terminal. |
|
|
93
|
+
|
|
94
|
+
## Runs without `outputDirs`
|
|
95
|
+
|
|
96
|
+
Metadata still gets the full treatment. `download()` returns a zip with `run.json`, `events.jsonl`, and an empty `outputs/` directory (manifest `outputs: []`). No file bytes are captured because no path was opted in. This is the default and is intentional — see `references/architecture-decisions.md` for the design rationale.
|
|
97
|
+
|
|
98
|
+
## Mid-session download semantics
|
|
99
|
+
|
|
100
|
+
Mid-session calls are **best-effort and side-effect-free**: the BFF reads whatever the agent has already registered with the Files API and streams that. It does **not** trigger a synthetic sync — that would interfere with the running agent's plan. If you need the full output set, wait for the run to reach terminal status and call `download()` again. The manifest's `partial: true` flag tells you when to retry.
|
|
21
101
|
|
|
22
|
-
Safety
|
|
102
|
+
## Safety
|
|
23
103
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
104
|
+
- Filenames are sanitized for cross-platform safety; collisions are disambiguated with a short id suffix before the extension.
|
|
105
|
+
- Downloads stay within the requested local directory.
|
|
106
|
+
- The archive endpoint is workspace-scoped (`outputs:read` scope) and rate-limited (`ANTPATH_RATE_LIMIT_RUN_ARCHIVE_PER_MINUTE`, default 30/min/workspace).
|
|
107
|
+
- `manifest.json` never contains file bytes — only ids, paths, sizes, content types.
|
package/docs/quickstart.md
CHANGED
|
@@ -45,3 +45,40 @@ antpath run ./template.json \
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
Both surfaces hit the same dashboard BFF and operate on the same durable run records — pick whichever is most convenient.
|
|
48
|
+
|
|
49
|
+
## Safe retries with `idempotencyKey`
|
|
50
|
+
|
|
51
|
+
Every `submitRun` call carries an `idempotencyKey`. When omitted the SDK auto-generates a UUID per call. Supplying your own key makes retries deterministic:
|
|
52
|
+
|
|
53
|
+
| Submit shape | Server response |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| New `idempotencyKey` | HTTP 201 — new run created. |
|
|
56
|
+
| Same key + identical request body hash | HTTP 200 — returns the original run. The SDK call resolves with the existing `RunRef`. |
|
|
57
|
+
| Same key + **different** request body hash | HTTP 409 — body `{ error: { message, code: "idempotency_conflict", details: { existingRunId } } }`. The SDK throws an `HttpError` carrying that body. Use `details.existingRunId` to adopt the pre-existing run, or pick a fresh key. |
|
|
58
|
+
| Omitted `idempotencyKey` | A new UUID is generated on every call — repeat submissions create new runs. |
|
|
59
|
+
|
|
60
|
+
The request hash is computed server-side over the canonical submission JSON (model, prompt, system, environment, skill refs, MCP server descriptors, proxy endpoints, `outputDirs`, etc.) so reordering JSON keys, adding whitespace, or rotating the inline secret bundle does **not** change the hash. Bumping a variable or changing the prompt does.
|
|
61
|
+
|
|
62
|
+
Pattern for safe retries:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const idempotencyKey = crypto.randomUUID();
|
|
66
|
+
async function submitWithRetry() {
|
|
67
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
return await client.submitRun({
|
|
70
|
+
model: "claude-haiku-4-5",
|
|
71
|
+
prompt: "...",
|
|
72
|
+
idempotencyKey,
|
|
73
|
+
secrets: { anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! } }
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err instanceof Error && err.message.includes("network")) continue;
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw new Error("submitRun failed after retries");
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The same `idempotencyKey` reused with the same body will deterministically resolve to the same run id regardless of how many times the network drops between attempts.
|
package/docs/skills.md
CHANGED
|
@@ -16,3 +16,52 @@ Local directories are packaged as zip files and mounted under `/antpath/skills/`
|
|
|
16
16
|
Inline skills are uploaded as markdown files and mounted under `/antpath/skills/` unless overridden.
|
|
17
17
|
|
|
18
18
|
The platform also mounts the `antpath` CLI at `/antpath/antpath` and a per-run manifest at `/antpath/index.json` on **every** run. Skills can invoke the managed HTTP proxy via `/antpath/antpath proxy …` — see `credentials.md` for the policy/auth model.
|
|
19
|
+
|
|
20
|
+
## Workspace skills: upload, find, reuse
|
|
21
|
+
|
|
22
|
+
Workspace skills persist across runs. Build a `Skill` from files, upload once, reference by `skl_*` id thereafter:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { AntpathClient, Skill } from "antpath";
|
|
26
|
+
|
|
27
|
+
const client = new AntpathClient({ apiToken });
|
|
28
|
+
|
|
29
|
+
// First publish.
|
|
30
|
+
const draft = await Skill.fromPath("./skills/broll-provider-ingest", {
|
|
31
|
+
name: "broll-provider-ingest"
|
|
32
|
+
});
|
|
33
|
+
const persisted = await draft.upload(client);
|
|
34
|
+
console.log(persisted.record!.id); // skl_…
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `uploadIfChanged(client)`
|
|
38
|
+
|
|
39
|
+
Repeat publishes are common in CI. `uploadIfChanged` first checks whether a live skill with the same `(name, contentHash)` already exists, and only uploads when the bytes have actually changed:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const skill = await Skill.fromPath("./skills/broll-provider-ingest", {
|
|
43
|
+
name: "broll-provider-ingest"
|
|
44
|
+
}).then((draft) => draft.uploadIfChanged(client));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`contentHash` is `sha256:<hex>` of the canonical bundle zip (the SDK normalises file order, mtime, and permissions before hashing). Identical inputs always produce the same hash, so a no-op `uploadIfChanged` returns the existing record without re-uploading bytes.
|
|
48
|
+
|
|
49
|
+
After the call resolves, the returned `Skill` is workspace-backed regardless of whether bytes were uploaded — pass it straight to `submitRun({ skills: [...] })` or call `.record!.id` to persist the id in your manifest.
|
|
50
|
+
|
|
51
|
+
### `client.skills.findByHash(...)` / `findByName(...)`
|
|
52
|
+
|
|
53
|
+
You can also look up workspace skills directly:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const byHash = await client.skills.findByHash({
|
|
57
|
+
name: "broll-provider-ingest",
|
|
58
|
+
contentHash: "sha256:..."
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const byName = await client.skills.findByName("broll-provider-ingest");
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Both return `null` when no live, undeleted skill matches. `findByHash` is an indexed lookup on `(name, hash)` — use it when you have a precomputed hash. `findByName` is a list-and-filter convenience for the common "what's the current id of this skill?" case.
|
|
65
|
+
|
|
66
|
+
Soft-deleted skills are never returned by either method. Runs that pinned a soft-deleted skill at submission time keep working via their snapshot — see `references/architecture-decisions.md` for the full snapshot model.
|
|
67
|
+
|