Drive Ten Arms from your own code
The one metered task, search-plan, is available over HTTP. It takes 1 lost everyday item, the last moment it was surely in hand, what happened since and up to 9 spots it could be, and returns a reading per spot: the person's own words that bear on it, a score, how easily a careful look would show the item there, why it could be there and how to search it - plus the steps of the story that no spot covers and a retrace line.
The division of labour first: the model reads the words; the page does every number. It turns the scores into chances, adds a share for somewhere not listed, applies the person's search marks with Bayes' rule and picks the verdict. Search marks are never sent, so marking spots costs nothing.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"ten-arms"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, item, last_seen, since, spots, spot_count, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: a reading is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the verdicts
Every run sends task: search-plan (the one task is search-plan); a missing or different one is still answered as search-plan. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always search-plan. |
item | yes | The lost thing in the person's words, up to 60 characters. |
last_seen | yes | The last moment they surely had it, up to 200 characters. |
since | yes | What they did after that, up to 280 characters, or not given. |
spots | yes | Numbered lines, N. name with an optional - note: ...; 2 to 9 spots. |
spot_count | yes | How many spots are listed. |
facts | yes | The page's lines (spot count, the not-listed share, judge before any searching). |
retry_note | no | Only on the page's one reformat retry. |
The page's arithmetic: "somewhere not listed" = 10 + 10 per uncovered entry (at most 50). The rest is shared over the spots in parts of score + 1 (largest remainder). Look-ease: easy 0.9, fair 0.6, hard 0.3. A spot marked searched keeps chance × (1 − look-ease × f), f = 1 for searched well and 0.5 for a quick look, and everything is renormalised. The order is chance now × look-ease, then the easier spot, then the lower number. Verdict, first match wins: widen-the-search when not listed holds 40% or more; go-straight-there when the best find chance is at least twice the runner-up's; otherwise sweep-top-three.
The reply
{"task": "search-plan", "summary": "...", "spots": [{"n": 1, "score": 0, "spot_it": "fair", "evidence": "...", "why": "...", "how_to_look": "..."}], "elsewhere": {"uncovered": ["..."], "why": "...", "retrace": "..."}, "closing": "..."}
| Field | What it holds |
|---|---|
spots[] | One per listed spot, in order: n, score 0 to 3 (3 the story puts the item there, 2 the note says it usually lives there, 1 the story names the spot, 0 none), spot_it (easy, fair, hard), evidence (2 to 10 words copied from the input, or empty), why (at most 20 words), how_to_look (at most 25). |
elsewhere | uncovered (up to 4 steps of the story no spot covers, copied), why (at most 25 words), retrace (the person's steps in order, at most 30). |
summary | Where the trail goes cold, at most 30 words. |
closing | 1 calm sentence, at most 20 words. |
The page then checks the reply (SearchKit.check) - spot order, quotes that are really the person's words, word limits, invented numbers or times, ranking words, safety - and lists any disagreement under "The page's check".
Worked examples - every verdict
These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free. The verdicts come from the sample's search marks (shown in each heading), which are not part of the input.
House keys after the shopping - no spot searched yet - verdict: go straight there
Input:
{
"task": "search-plan",
"item": "house keys on a red fob",
"last_seen": "I unlocked the front door with them when I got back from the shop",
"since": "I put the shopping away in the kitchen, hung my coat on the hook and sat on the sofa with a cup of tea",
"spots": "1. front door - note: I sometimes leave them in the lock\n2. key bowl by the door - note: where they are meant to live\n3. kitchen counter - note: where the shopping bags went\n4. coat pocket - note: the coat I wore to the shop\n5. sofa",
"spot_count": "5",
"facts": "spots listed: 5 (numbered 1 to 5); reply with exactly 5 spot entries in that order\nthe page adds its own share for somewhere not listed, from the uncovered steps\nchances are judged before any searching: ignore any searches the story says were already done - the page applies the person's search marks itself"
}
Reply:
{
"task": "search-plan",
"summary": "Between unlocking the front door and sitting on the sofa, the story never says what became of the keys.",
"spots": [
{
"n": 1,
"score": 3,
"spot_it": "fair",
"evidence": "unlocked the front door with them",
"why": "You used them to unlock this door, so they could be right here.",
"how_to_look": "Check the lock and handle closely, and look along the bottom edge of the door."
},
{
"n": 2,
"score": 2,
"spot_it": "easy",
"evidence": "where they are meant to live",
"why": "The keys are meant to live here, so they might simply be in this bowl.",
"how_to_look": "Glance into the bowl and move anything inside it aside to see the bottom."
},
{
"n": 3,
"score": 1,
"spot_it": "easy",
"evidence": "put the shopping away in the kitchen",
"why": "You put the shopping away in the kitchen, so the keys could have been set down here too.",
"how_to_look": "Scan the whole counter surface, checking under anything sitting on it."
},
{
"n": 4,
"score": 1,
"spot_it": "fair",
"evidence": "hung my coat on the hook",
"why": "You hung this coat on the hook, so the keys could still be in its pocket.",
"how_to_look": "Reach all the way into the pocket and feel along the bottom seam."
},
{
"n": 5,
"score": 1,
"spot_it": "hard",
"evidence": "sat on the sofa",
"why": "You sat on the sofa, so the keys could have been set down there too.",
"how_to_look": "Check between and under the cushions and run a hand down the sides."
}
],
"elsewhere": {
"uncovered": [],
"why": "The steps after unlocking the door are all covered by the spots already listed, with nothing left outside them.",
"retrace": "unlocked the front door then put the shopping away in the kitchen then hung your coat on the hook then sat on the sofa with a cup of tea"
},
"closing": "Take a calm look through each spot without rushing, and trust what turns up."
}
Reading glasses after a night in bed - bedside table and bathroom shelf searched well - verdict: sweep the top three
Input:
{
"task": "search-plan",
"item": "reading glasses in a brown case",
"last_seen": "I had them on while reading in bed last night",
"since": "this morning I made breakfast, had a shower and got dressed in the bedroom",
"spots": "1. bed - note: the duvet is in a heap\n2. bedside table - note: where I usually put them\n3. bathroom shelf\n4. kitchen table - note: where I ate breakfast\n5. work bag - note: I packed it yesterday",
"spot_count": "5",
"facts": "spots listed: 5 (numbered 1 to 5); reply with exactly 5 spot entries in that order\nthe page adds its own share for somewhere not listed, from the uncovered steps\nchances are judged before any searching: ignore any searches the story says were already done - the page applies the person's search marks itself"
}
Reply:
{
"task": "search-plan",
"summary": "Between reading in bed last night and starting your morning, your story does not say what happened to the glasses.",
"spots": [
{
"n": 1,
"score": 3,
"spot_it": "hard",
"evidence": "in bed last night",
"why": "You had them on in bed last night, so they could still be there.",
"how_to_look": "Shake out the duvet and covers, then check along the mattress and underneath it."
},
{
"n": 2,
"score": 2,
"spot_it": "easy",
"evidence": "where I usually put them",
"why": "This is where you usually put them, so they may be there.",
"how_to_look": "Look across the top of the bedside table, checking around anything placed on it."
},
{
"n": 3,
"score": 0,
"spot_it": "easy",
"evidence": "",
"why": "Nothing you wrote points to this spot.",
"how_to_look": "Scan along the bathroom shelf, checking each item sitting on it."
},
{
"n": 4,
"score": 1,
"spot_it": "easy",
"evidence": "where I ate breakfast",
"why": "You ate breakfast at the kitchen table, so they could be nearby.",
"how_to_look": "Check all over the kitchen table and look underneath anything placed on it."
},
{
"n": 5,
"score": 0,
"spot_it": "fair",
"evidence": "I packed it yesterday",
"why": "You packed the work bag yesterday, so they could be inside.",
"how_to_look": "Open the work bag and check inside, including any pockets or compartments."
}
],
"elsewhere": {
"uncovered": [
"had a shower",
"got dressed in the bedroom"
],
"why": "The shower and getting dressed are not covered by the spots above, so something could still be in those steps.",
"retrace": "reading in bed last night then made breakfast then had a shower then got dressed in the bedroom"
},
"closing": "Take your time with the search, and know there is no rush to it."
}
Umbrella after a café and a bus - every spot but the bus searched well - verdict: widen the search
Input:
{
"task": "search-plan",
"item": "black folding umbrella",
"last_seen": "I had it at the café when it started raining",
"since": "I walked to the bus stop, took the bus home and popped into the corner shop on my street",
"spots": "1. the café\n2. the bus\n3. the corner shop\n4. umbrella stand in the hall\n5. my backpack - note: it fits in the side pocket",
"spot_count": "5",
"facts": "spots listed: 5 (numbered 1 to 5); reply with exactly 5 spot entries in that order\nthe page adds its own share for somewhere not listed, from the uncovered steps\nchances are judged before any searching: ignore any searches the story says were already done - the page applies the person's search marks itself"
}
Reply:
{
"task": "search-plan",
"summary": "After the café, nothing you describe says what happened to the umbrella until you reached home.",
"spots": [
{
"n": 1,
"score": 3,
"spot_it": "fair",
"evidence": "I had it at the café",
"why": "You had the umbrella at the café, so it could still be there.",
"how_to_look": "Ask the café staff if they have seen or put aside the umbrella."
},
{
"n": 2,
"score": 1,
"spot_it": "fair",
"evidence": "took the bus home",
"why": "You took the bus home, so the umbrella could have been left on the bus.",
"how_to_look": "Ask the bus staff whether the umbrella was handed in or left behind."
},
{
"n": 3,
"score": 1,
"spot_it": "fair",
"evidence": "popped into the corner shop",
"why": "You popped into the corner shop, so the umbrella could have been set down there.",
"how_to_look": "Ask the corner shop staff if the umbrella was left or handed in."
},
{
"n": 4,
"score": 0,
"spot_it": "easy",
"evidence": "",
"why": "Nothing in your words points to the umbrella stand in the hall.",
"how_to_look": "Look at the umbrella stand in the hall; a quick glance will show whether the umbrella is there."
},
{
"n": 5,
"score": 0,
"spot_it": "fair",
"evidence": "it fits in the side pocket",
"why": "The backpack's side pocket fits the umbrella, so it could be tucked inside.",
"how_to_look": "Open the backpack and check inside, including the side pocket, by feel and sight."
}
],
"elsewhere": {
"uncovered": [
"the bus stop",
"on my street"
],
"why": "The bus stop and your street are not among the listed spots, so the umbrella could also be along either stretch.",
"retrace": "You had it at the café when it started raining, then walked to the bus stop, then took the bus home, then popped into the corner shop on your street."
},
"closing": "Take your time checking each spot calmly, without rushing or assuming where the umbrella must be."
}
Build the input yourself
Load the engine in node (global.window = global; require("./searchkit.js")), then:
const K = SearchKit;
const form = K.form({ item: "house keys on a red fob", last_seen: "I unlocked the front door with them when I got back from the shop",
since: "I hung my coat on the hook", spots: [{ name: "front door" }, { name: "coat pocket", mark: "quick" }] });
const built = K.buildInput(form);
const wire = K.toWire(built.input); // POST this to /estimate and /run (search marks are not sent)
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.plan(form, reply) // chances before and now, find chances, order, verdict
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, "my keys") // the plan as Markdown
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://ten-arms.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://ten-arms.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// ten-arms.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://ten-arms.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://ten-arms.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// TenArms.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class TenArms {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://ten-arms.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://ten-arms.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://ten-arms.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://ten-arms.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"ten-arms"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "ten-arms"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "ten-arms" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "ten-arms"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "ten-arms"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "ten-arms" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "ten-arms"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "ten-arms" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "search-plan", "item": "house keys on a red fob", "last_seen": "I unlocked the front door with them when I got back from the shop", "since": "...", "spots": "1. front door - note: ...\n2. key bowl by the door ...", "spot_count": "5", "facts": "spots listed: 5 ..."}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "search-plan", "item": "house keys on a red fob", "last_seen": "I unlocked the front door with them when I got back from the shop", "since": "...", "spots": "1. front door - note: ...\n2. key bowl by the door ...", "spot_count": "5", "facts": "spots listed: 5 ..."}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "search-plan", item, last_seen, since, spots, spot_count, facts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses ten-arms:<task>:<inputHash>:a<attempt> (the one task is search-plan), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="ten-arms:search-plan:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "ten-arms:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), len(reply.get("spots", [])))
import { createHash } from "node:crypto";
const key = `ten-arms:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, (reply.spots || []).length);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "ten-arms:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "ten-arms:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "ten-arms:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], (reply["spots"] || []).length].inspect
$key = "ten-arms:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", count($reply["spots"] ?? []), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"ten-arms:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: ten-arms:search-plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "search-plan", "item": "house keys on a red fob", "last_seen": "I unlocked the front door with them when I got back from the shop", "since": "...", "spots": "1. front door - note: ...\n2. key bowl by the door ...", "spot_count": "5", "facts": "spots listed: 5 ..."}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "ten-arms:search-plan:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `ten-arms:search-plan:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "ten-arms:search-plan:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "ten-arms:search-plan:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "ten-arms:search-plan:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: ten-arms:search-plan:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"ten-arms:search-plan:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);