Use the scheduler from your own code
Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS, so you can script it from any language: re-sequence a board every time MRP drops a new release, wire it into an MES export, or re-run the schedule when a machine goes down. This page walks through each call with examples in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#.
Basics
Base URL: https://api.skillsafe.ai/v1/app-api. Every request sends
Authorization: Bearer <token> and JSON bodies with
Content-Type: application/json. Responses are wrapped in an envelope:
{"ok":true,"data":…} on success,
{"error":{"code","message"}} on failure.
There is no /apps/schedule-desk/ path segment. The app is bound to the token
itself, which is minted at POST /v1/app-api/guest with the slug — every
later call is just /me, /estimate, /run or
/run-stream.
| Code | Meaning | What to do |
|---|---|---|
unauthorized | Missing, stale or revoked token | Mint a new guest token, or sign in for a personal one |
not_found | The route does not exist | Check for a stray /apps/<slug>/ segment — that is the usual cause |
payment_required | Balance below the run minimum | Top up; /estimate tells you the reserve and the minimum in advance |
validation_error | Body is not the input object | Send the fields at the top level, not wrapped in {"input": …} |
rate_limited | Too many requests | Back off and retry; do not tight-loop |
/me, /estimate and /guest are free. /run
and /run-stream spend credits.
The input this app sends
These are the exact fields the app's own run path submits.
| Field | Type | Notes |
|---|---|---|
data | string, required | The released work orders and the scenario as pasted. Job lines shaped like WO-4411: 6h, due 24 (processing hours first, then the due time in hours from now), plus prose about lines and work centers, attended hours, the changeover matrix, frozen windows, disruptions and rush orders. The app cuts the middle at 40,000 characters and keeps both ends, announcing the cut in-band; do the same rather than truncating the tail, because the floor notes and the explicit ask live at the end. |
notes | string, optional | Up to 6,000 characters. Business context: frozen windows, quality holds, management priorities, what worries the scheduler. Also where the app folds in answers to a previous run's open questions. |
task | string, optional | One of Full schedule, Bottleneck check, Changeover sequencing, Rush order insertion or Disruption response. Defaults to Full schedule. The narrower tasks still report a serious risk from any area; the task sets where the depth goes. |
facts | string, optional | The output of the browser-side mechanical scan: the job lines it detected with processing and due hours, total load, and the naive single-machine EDD and SPT lateness reads. Treated by the prompt as a hint to cross-check, never as a verdict — where the model's own reading of the paste disagrees, its reading wins. The app computes it in the browser; you can compute it yourself or omit it. |
retry_note | string, optional | Only used on the app's one automatic reformat retry: a verbatim restatement of the output shape, sent when the first reply failed to parse. Omit it on a first attempt. |
Idempotency-Key header on every run. The app derives it from a hash of
the input plus the attempt number, so a dropped connection replays the same job instead of
billing a second one.
The output contract
The reply is plain text — no JSON, no code fence around the whole response. A reply that breaks any of these rules is rejected by the app's parser, which then retries once.
FEASIBILITY: Executable | At risk - constraints noted | Not schedulable BOTTLENECK: <the constraint line or work center, or exactly "Not identified"> CONFIDENCE: <integer 0-100, bare, no percent sign> SUMMARY: <2-4 sentences, may wrap, ends at the first blank line> ## Constraint assessment - … ## Job sequence - … ## Changeover plan - … ## Risks and watchouts - … ## Recommended actions - … ## Open questions - … (or the single bullet "- None.")
- bullet; a bullet may wrap onto indented continuation lines. A
section with nothing to report carries the single bullet - None.
1. Get a token
A guest token is one call and needs no account. For a personal token that bills your own credits, open the token page and sign in — it shows the token, copies it, and copies a ready-made shell export. You never need the browser developer console.
TOKEN="${SKILLSAFE_TOKEN:-YOUR_TOKEN}"
# Or mint a guest token, no account needed:
TOKEN=$(curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
-H "Content-Type: application/json" \
-d '{"slug":"schedule-desk"}' | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["token"])')
echo "${TOKEN:0:8}..."
import json, urllib.request
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # or leave blank and mint a guest token below
def call(path, body=None, token=None, method=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, method=method or ("POST" if data else "GET"))
req.add_header("Content-Type", "application/json")
if token:
req.add_header("Authorization", "Bearer " + token)
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
if TOKEN == "YOUR_TOKEN":
TOKEN = call("/guest", {"slug": "schedule-desk"})["token"]
print(TOKEN[:8] + "...")
const API = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // or mint a guest token below
async function call(path, body, method) {
const res = await fetch(API + path, {
method: method || (body ? "POST" : "GET"),
headers: {
"Content-Type": "application/json",
...(TOKEN && TOKEN !== "YOUR_TOKEN" ? { Authorization: "Bearer " + TOKEN } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.code + ": " + json.error?.message);
return json.data;
}
if (TOKEN === "YOUR_TOKEN") {
TOKEN = (await call("/guest", { slug: "schedule-desk" })).token;
}
console.log(TOKEN.slice(0, 8) + "...");
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const API = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN"
func call(path string, body any, out any) error {
var buf *bytes.Buffer = bytes.NewBuffer(nil)
method := "GET"
if body != nil {
method = "POST"
b, _ := json.Marshal(body)
buf = bytes.NewBuffer(b)
}
req, _ := http.NewRequest(method, API+path, buf)
req.Header.Set("Content-Type", "application/json")
if token != "YOUR_TOKEN" {
req.Header.Set("Authorization", "Bearer "+token)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct {
Data json.RawMessage `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return err
}
return json.Unmarshal(env.Data, out)
}
func main() {
var guest struct{ Token string `json:"token"` }
if token == "YOUR_TOKEN" {
_ = call("/guest", map[string]string{"slug": "schedule-desk"}, &guest)
token = guest.Token
}
fmt.Println(token[:8] + "...")
}
import java.net.URI;
import java.net.http.*;
public class ScheduleDesk {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String body) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(API + path))
.header("Content-Type", "application/json");
if (!token.equals("YOUR_TOKEN")) b.header("Authorization", "Bearer " + token);
b = body == null ? b.GET() : b.POST(HttpRequest.BodyPublishers.ofString(body));
return HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] args) throws Exception {
if (token.equals("YOUR_TOKEN")) {
String res = call("/guest", "{\"slug\":\"schedule-desk\"}");
token = res.replaceAll(".*\"token\"\\s*:\\s*\"([^\"]+)\".*", "$1");
}
System.out.println(token.substring(0, 8) + "...");
}
}
require "json"
require "net/http"
API = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = "YOUR_TOKEN"
def call(path, body = nil, token: nil, method: nil)
uri = URI(API.to_s + path)
req = if body || method == "POST"
Net::HTTP::Post.new(uri)
else
Net::HTTP::Get.new(uri)
end
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}" if token
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)["data"]
end
token = TOKEN == "YOUR_TOKEN" ? call("/guest", { slug: "schedule-desk" })["token"] : TOKEN
puts token[0, 8] + "..."
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$token = "YOUR_TOKEN";
function call(string $path, ?array $body = null, ?string $token = null, string $method = null): array {
$headers = ["Content-Type: application/json"];
if ($token) { $headers[] = "Authorization: Bearer $token"; }
$ch = curl_init(API . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
} elseif ($method) {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
}
$raw = curl_exec($ch);
curl_close($ch);
return json_decode($raw, true)["data"];
}
if ($token === "YOUR_TOKEN") {
$token = call("/guest", ["slug" => "schedule-desk"])["token"];
}
echo substr($token, 0, 8) . "...\n";
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string API = "https://api.skillsafe.ai/v1/app-api";
string token = "YOUR_TOKEN";
var http = new HttpClient();
async Task<JsonElement> Call(string path, object? body = null, string? method = null) {
var req = new HttpRequestMessage(
body != null ? HttpMethod.Post : new HttpMethod(method ?? "GET"), API + path);
if (body != null)
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
if (token != "YOUR_TOKEN")
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var res = await http.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
return doc.RootElement.GetProperty("data");
}
if (token == "YOUR_TOKEN") {
var guest = await Call("/guest", new { slug = "schedule-desk" });
token = guest.GetProperty("token").GetString()!;
}
Console.WriteLine(token[..8] + "...");
2. Check who you are and what you can spend
Free. Returns subject_type (user or guest), subject_id and credits.
curl -s https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer $TOKEN"
# {"ok":true,"data":{"subject_type":"guest","subject_id":"gst_...","credits":0}}
me = call("/me", token=TOKEN)
print(me["subject_type"], me["credits"])
const me = await call("/me");
console.log(me.subject_type, me.credits);
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = call("/me", nil, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("/me", null));
me = call("/me", token: token)
puts "#{me['subject_type']} #{me['credits']}"
$me = call("/me", null, $token);
echo $me["subject_type"] . " " . $me["credits"] . "\n";
var me = await Call("/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
3. Estimate before you spend
Free, creates no job and charges nothing. hold_credits is what gets
reserved, not the price — the actual charge is usually far lower, because the
hold prices the full output cap. If your balance sits between min_credits and
hold_credits the run still executes with a reduced cap and comes back with
truncated: true. Compare the balance from step 2 against
hold_credits before you submit; a 402 after the fact is avoidable.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"data":"Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...","notes":"The first 8h on Line 1 are frozen","task":"Full schedule","facts":""}'
# {"hold_credits":1578,"min_credits":149,"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000}
payload = {
"data": "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
"notes": "The first 8h on Line 1 are frozen",
"task": "Full schedule",
"facts": "",
}
est = call("/estimate", payload, token=TOKEN)
print(est["hold_credits"], est["min_credits"], est["model"])
const payload = {
data: "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
notes: "The first 8h on Line 1 are frozen",
task: "Full schedule",
facts: "",
};
const est = await call("/estimate", payload);
console.log(est.hold_credits, est.min_credits, est.model);
payload := map[string]string{
"data": "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
"notes": "The first 8h on Line 1 are frozen",
"task": "Full schedule",
"facts": "",
}
var est struct {
HoldCredits int `json:"hold_credits"`
MinCredits int `json:"min_credits"`
Model string `json:"model"`
}
_ = call("/estimate", payload, &est)
fmt.Println(est.HoldCredits, est.MinCredits, est.Model)
String payload = "{\"data\":\"Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...\","
+ "\"notes\":\"The first 8h on Line 1 are frozen\",\"task\":\"Full schedule\",\"facts\":\"\"}";
System.out.println(call("/estimate", payload));
payload = {
data: "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
notes: "The first 8h on Line 1 are frozen",
task: "Full schedule",
facts: ""
}
est = call("/estimate", payload, token: token)
puts "#{est['hold_credits']} #{est['min_credits']} #{est['model']}"
$payload = [
"data" => "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
"notes" => "The first 8h on Line 1 are frozen",
"task" => "Full schedule",
"facts" => "",
];
$est = call("/estimate", $payload, $token);
echo "{$est['hold_credits']} {$est['min_credits']} {$est['model']}\n";
var payload = new {
data = "Line 1 32h attended, Line 2 24h; WO-4411: 6h, due 30; WO-4412: 4h, due 16; A to B changeover 30 min ...",
notes = "The first 8h on Line 1 are frozen",
task = "Full schedule",
facts = ""
};
var est = await Call("/estimate", payload);
Console.WriteLine($"{est.GetProperty("hold_credits")} {est.GetProperty("model")}");
4. Run it, and poll for the result
This spends credits. The body is the input object directly —
not wrapped in {"input": …}. Send an Idempotency-Key: reuse
the same key and the same job comes back instead of a second charge. /run
returns a job_id; poll /jobs/{id} until status is
succeeded or failed, then read
output.output and parse it against the contract above.
KEY="schedule-desk-$(printf '%s' "$BOARD_TEXT" | shasum | cut -c1-8)-1" JOB=$(curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d @payload.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])') until curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \ -H "Authorization: Bearer $TOKEN" | grep -q '"status":"succeeded"'; do sleep 2; done curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" -H "Authorization: Bearer $TOKEN" \ | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["output"]["output"])'
import hashlib, time, urllib.request
key = "schedule-desk-" + hashlib.sha256(payload["data"].encode()).hexdigest()[:8] + "-1"
req = urllib.request.Request(API + "/run", data=json.dumps(payload).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call("/jobs/" + job_id, token=TOKEN)
if job["status"] in ("succeeded", "failed"):
break
time.sleep(2)
print(job["output"]["output"])
const enc = new TextEncoder().encode(payload.data);
const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", enc))]
.map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 8);
const key = `schedule-desk-${digest}-1`;
const res = await fetch(API + "/run", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + TOKEN,
"Idempotency-Key": key,
},
body: JSON.stringify(payload),
});
const { data } = await res.json();
let job;
do {
await new Promise((r) => setTimeout(r, 2000));
job = await call("/jobs/" + data.job_id);
} while (job.status !== "succeeded" && job.status !== "failed");
console.log(job.output.output);
b, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run", bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "schedule-desk-"+fingerprint+"-1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var env struct {
Data struct {
JobID string `json:"job_id"`
} `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&env)
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
for {
_ = call("/jobs/"+env.Data.JobID, nil, &job)
if job.Status == "succeeded" || job.Status == "failed" {
break
}
time.Sleep(2 * time.Second)
}
fmt.Println(job.Output.Output)
HttpRequest run = HttpRequest.newBuilder(URI.create(API + "/run"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", "schedule-desk-" + fingerprint + "-1")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
String created = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
String jobId = created.replaceAll(".*\"job_id\"\\s*:\\s*\"([^\"]+)\".*", "$1");
String job;
do {
Thread.sleep(2000);
job = call("/jobs/" + jobId, null);
} while (!job.contains("\"status\":\"succeeded\"") && !job.contains("\"status\":\"failed\""));
System.out.println(job);
require "digest"
key = "schedule-desk-#{Digest::SHA256.hexdigest(payload[:data])[0, 8]}-1"
uri = URI("#{API}/run")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}"
req["Idempotency-Key"] = key
req.body = JSON.dump(payload)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]
loop do
job = call("/jobs/#{job_id}", token: token)
if %w[succeeded failed].include?(job["status"])
puts job.dig("output", "output")
break
end
sleep 2
end
$key = "schedule-desk-" . substr(hash("sha256", $payload["data"]), 0, 8) . "-1";
$ch = curl_init(API . "/run");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer $token",
"Idempotency-Key: $key",
]);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);
do {
sleep(2);
$job = call("/jobs/$jobId", null, $token);
} while (!in_array($job["status"], ["succeeded", "failed"], true));
echo $job["output"]["output"] . "\n";
using System.Security.Cryptography;
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload.data)))[..8].ToLower();
var runReq = new HttpRequestMessage(HttpMethod.Post, API + "/run") {
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
runReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
runReq.Headers.Add("Idempotency-Key", $"schedule-desk-{hash}-1");
var created = JsonDocument.Parse(await (await http.SendAsync(runReq)).Content.ReadAsStringAsync());
var jobId = created.RootElement.GetProperty("data").GetProperty("job_id").GetString();
JsonElement job;
do {
await Task.Delay(2000);
job = await Call("/jobs/" + jobId);
} while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"));
Console.WriteLine(job.GetProperty("output").GetProperty("output").GetString());
5. Stream it instead
This spends credits. /run-stream returns
text/event-stream: delta events carry output as it generates and a
final done event carries charged_credits, truncated
and the authoritative full output. Prefer the done payload over
the concatenated deltas — the stream can drop the tail. The same
Idempotency-Key rule applies. In the browser, the vendored SDK wraps this:
ss.runStream(input, { idempotencyKey, onDelta }).
curl -N -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @payload.json
# event: delta
# data: {"text":"FEASIBILITY: At risk - constraints noted\n"}
# ...
# event: done
# data: {"job_id":"job_...","charged_credits":412,"truncated":false,"output":{"output":"FEASIBILITY: ..."}}
req = urllib.request.Request(API + "/run-stream", data=json.dumps(payload).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)
full = ""
with urllib.request.urlopen(req) as stream:
for raw in stream:
line = raw.decode().strip()
if line.startswith("data:"):
evt = json.loads(line[5:].strip())
if "text" in evt:
full += evt["text"]
elif "output" in evt:
full = evt["output"]["output"] # authoritative
print(full)
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + TOKEN,
"Idempotency-Key": key,
},
body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", full = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const evt = JSON.parse(line.slice(5).trim());
if (evt.text) full += evt.text;
else if (evt.output) full = evt.output.output; // authoritative
}
}
console.log(full);
b, _ = json.Marshal(payload)
req, _ = http.NewRequest("POST", API+"/run-stream", bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", key)
res, _ = http.DefaultClient.Do(req)
defer res.Body.Close()
full := ""
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
var evt struct {
Text string `json:"text"`
Output *struct {
Output string `json:"output"`
} `json:"output"`
}
_ = json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &evt)
if evt.Output != nil {
full = evt.Output.Output
} else {
full += evt.Text
}
}
fmt.Println(full)
HttpRequest stream = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
StringBuilder full = new StringBuilder();
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body()
.filter(l -> l.startsWith("data:"))
.forEach(l -> full.append(l.substring(5).trim()).append("\n"));
System.out.println(full); // parse each line as JSON; prefer the final done event
uri = URI("#{API}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}"
req["Idempotency-Key"] = key
req.body = JSON.dump(payload)
full = ""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
next unless line.start_with?("data:")
evt = JSON.parse(line[5..].strip) rescue next
full = evt.dig("output", "output") || (full + evt.fetch("text", ""))
end
end
end
end
puts full
$full = "";
$ch = curl_init(API . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer $token",
"Idempotency-Key: $key",
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$full) {
foreach (explode("\n", $chunk) as $line) {
if (strpos($line, "data:") !== 0) { continue; }
$evt = json_decode(trim(substr($line, 5)), true);
if (isset($evt["output"]["output"])) { $full = $evt["output"]["output"]; }
elseif (isset($evt["text"])) { $full .= $evt["text"]; }
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
echo $full . "\n";
var streamReq = new HttpRequestMessage(HttpMethod.Post, API + "/run-stream") {
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
streamReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
streamReq.Headers.Add("Idempotency-Key", key);
var resp = await http.SendAsync(streamReq, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await resp.Content.ReadAsStreamAsync());
var full = "";
while (await reader.ReadLineAsync() is string line) {
if (!line.StartsWith("data:")) continue;
var evt = JsonDocument.Parse(line[5..].Trim()).RootElement;
if (evt.TryGetProperty("output", out var o))
full = o.GetProperty("output").GetString()!;
else if (evt.TryGetProperty("text", out var t))
full += t.GetString();
}
Console.WriteLine(full);
6. Parse the reply
The four tag lines come first, then the six sections. A minimal, faithful parser: read
FEASIBILITY, BOTTLENECK and CONFIDENCE from the leading
lines, take SUMMARY up to the first blank line, then split on
## headings and read - bullets. Reject the reply rather than
guessing if a tag or a heading is missing — that is what the app does, and it is why
the retry exists.
# Reject anything that does not open with the contract: head -1 reply.txt | grep -Eq '^FEASIBILITY: (Executable|At risk - constraints noted|Not schedulable)$' || echo "contract violated" grep -c '^## ' reply.txt # must be 6
import re
SECTIONS = ["Constraint assessment", "Job sequence", "Changeover plan",
"Risks and watchouts", "Recommended actions", "Open questions"]
def parse(text):
feas = re.search(r"^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$", text, re.M)
drum = re.search(r"^BOTTLENECK:\s*(.+?)\s*$", text, re.M)
con = re.search(r"^CONFIDENCE:\s*(\d{1,3})\s*$", text, re.M)
summ = re.search(r"^SUMMARY:\s*(.*?)(?:\n\s*\n)", text, re.M | re.S)
if not (feas and drum and con and summ):
return None
out = {"feasibility": feas.group(1), "bottleneck": drum.group(1),
"confidence": int(con.group(1)), "summary": " ".join(summ.group(1).split()),
"sections": {}}
for name in SECTIONS:
body = re.search(r"^##\s+" + re.escape(name) + r"\s*$(.*?)(?=^##\s|\Z)",
text, re.M | re.S)
if body is None:
return None
bullets = [b.strip() for b in re.findall(r"^-\s+(.*)$", body.group(1), re.M)]
out["sections"][name] = [] if bullets == ["None."] else bullets
return out
const SECTIONS = ["Constraint assessment", "Job sequence", "Changeover plan",
"Risks and watchouts", "Recommended actions", "Open questions"];
function parse(text) {
const feas = /^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$/m.exec(text);
const drum = /^BOTTLENECK:\s*(.+?)\s*$/m.exec(text);
const con = /^CONFIDENCE:\s*(\d{1,3})\s*$/m.exec(text);
const summ = /^SUMMARY:\s*([\s\S]*?)\n\s*\n/m.exec(text);
if (!feas || !drum || !con || !summ) return null;
const sections = {};
for (const name of SECTIONS) {
const re = new RegExp("^##\\s+" + name + "\\s*$([\\s\\S]*?)(?=^##\\s|$)", "m");
const body = re.exec(text);
if (!body) return null;
const bullets = [...body[1].matchAll(/^-\s+(.*)$/gm)].map((m) => m[1].trim());
sections[name] = bullets.length === 1 && /^none\.?$/i.test(bullets[0]) ? [] : bullets;
}
return { feasibility: feas[1], bottleneck: drum[1], confidence: +con[1],
summary: summ[1].replace(/\s+/g, " ").trim(), sections };
}
// Split on the six headings, then read "- " bullets out of each block.
sections := regexp.MustCompile(`(?m)^##\s+(.+)$`).Split(reply, -1)
names := regexp.MustCompile(`(?m)^##\s+(.+)$`).FindAllStringSubmatch(reply, -1)
if len(names) != 6 {
return errors.New("contract violated: expected six sections")
}
for i, n := range names {
bullets := regexp.MustCompile(`(?m)^-\s+(.*)$`).FindAllStringSubmatch(sections[i+1], -1)
fmt.Println(n[1], len(bullets))
}
String[] names = {"Constraint assessment", "Job sequence", "Changeover plan",
"Risks and watchouts", "Recommended actions", "Open questions"};
for (String n : names) {
if (!reply.contains("## " + n)) throw new IllegalStateException("missing section: " + n);
}
java.util.regex.Matcher m =
java.util.regex.Pattern.compile("^FEASIBILITY:\\s*(Executable|At risk - constraints noted|Not schedulable)$",
java.util.regex.Pattern.MULTILINE).matcher(reply);
if (!m.find()) throw new IllegalStateException("no FEASIBILITY line");
System.out.println(m.group(1));
SECTIONS = ["Constraint assessment", "Job sequence", "Changeover plan",
"Risks and watchouts", "Recommended actions", "Open questions"].freeze
def parse(text)
feas = text[/^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$/, 1]
drum = text[/^BOTTLENECK:\s*(.+?)\s*$/, 1]
con = text[/^CONFIDENCE:\s*(\d{1,3})\s*$/, 1]
return nil unless feas && drum && con
sections = SECTIONS.to_h do |name|
body = text[/^##\s+#{Regexp.escape(name)}\s*$(.*?)(?=^##\s|\z)/m, 1]
return nil unless body
bullets = body.scan(/^-\s+(.*)$/).flatten.map(&:strip)
[name, bullets == ["None."] ? [] : bullets]
end
{ feasibility: feas, bottleneck: drum, confidence: con.to_i, sections: sections }
end
$sections = ["Constraint assessment", "Job sequence", "Changeover plan",
"Risks and watchouts", "Recommended actions", "Open questions"];
preg_match('/^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$/m', $reply, $feas);
preg_match('/^BOTTLENECK:\s*(.+?)\s*$/m', $reply, $drum);
preg_match('/^CONFIDENCE:\s*(\d{1,3})\s*$/m', $reply, $con);
if (!$feas || !$drum || !$con) { throw new RuntimeException("contract violated"); }
$out = [];
foreach ($sections as $name) {
$re = '/^##\s+' . preg_quote($name, '/') . '\s*$(.*?)(?=^##\s|\z)/ms';
if (!preg_match($re, $reply, $body)) { throw new RuntimeException("missing $name"); }
preg_match_all('/^-\s+(.*)$/m', $body[1], $bullets);
$out[$name] = $bullets[1];
}
using System.Text.RegularExpressions;
string[] names = { "Constraint assessment", "Job sequence", "Changeover plan",
"Risks and watchouts", "Recommended actions", "Open questions" };
var feas = Regex.Match(reply, @"^FEASIBILITY:\s*(Executable|At risk - constraints noted|Not schedulable)\s*$", RegexOptions.Multiline);
if (!feas.Success) throw new InvalidOperationException("contract violated");
foreach (var name in names) {
var body = Regex.Match(reply,
$@"^##\s+{Regex.Escape(name)}\s*$(.*?)(?=^##\s|\z)",
RegexOptions.Multiline | RegexOptions.Singleline);
if (!body.Success) throw new InvalidOperationException($"missing section: {name}");
var bullets = Regex.Matches(body.Groups[1].Value, @"^-\s+(.*)$", RegexOptions.Multiline);
Console.WriteLine($"{name}: {bullets.Count}");
}
Not schedulable is a
real answer — it means the paste carried no usable work orders or capacity signal, and
it is a result to act on rather than a failure to retry. When it is returned, Job sequence
and Changeover plan are both - None. and Open questions says what to paste.