runbios-mcp 0.3.0 → 0.4.0-dev.333
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/CHANGELOG.md +56 -0
- package/README.md +15 -5
- package/dist/assistant/tool-runtime.d.ts.map +1 -1
- package/dist/assistant/tool-runtime.js +6 -1
- package/dist/assistant/tool-runtime.js.map +1 -1
- package/dist/config.d.ts +48 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +83 -6
- package/dist/config.js.map +1 -1
- package/dist/http/assistant.js +1 -1
- package/dist/http/assistant.js.map +1 -1
- package/dist/http/metadata.js +1 -1
- package/dist/http/metadata.js.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +188 -53
- package/dist/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -22,7 +22,7 @@ const RAW_FAILURE_TEXT_FIELDS = [
|
|
|
22
22
|
"last_error",
|
|
23
23
|
"stage_detail",
|
|
24
24
|
];
|
|
25
|
-
import { LOOP_TOOL_PREFIX, resolveLaunchGates, withoutGatedLoopIntrospection } from "./config.js";
|
|
25
|
+
import { LOOP_TOOL_PREFIX, loopToolOffered, resolveLaunchGates, withoutGatedLoopIntrospection, } from "./config.js";
|
|
26
26
|
import { VERSION } from "./version.js";
|
|
27
27
|
export { VERSION };
|
|
28
28
|
/**
|
|
@@ -687,6 +687,49 @@ export function summarizeTrainingProgress(data) {
|
|
|
687
687
|
};
|
|
688
688
|
return { ...payload, progress_summary: summary };
|
|
689
689
|
}
|
|
690
|
+
/** What every loop tool registered while the loop is SWITCHED OFF says
|
|
691
|
+
* (config.ts LaunchGates.loopSwitchedOff): only the five routes that stop
|
|
692
|
+
* what a pipeline still runs answer there. */
|
|
693
|
+
export function loopSwitchedOffNote(name) {
|
|
694
|
+
const lead = "THE CONSCIOUS LOOP IS SWITCHED OFF on this platform: nothing new is recorded or trained, and this tool is offered so nothing a pipeline started keeps charging.";
|
|
695
|
+
if (name === "loop_update_pipeline") {
|
|
696
|
+
return `${lead} Only switching a pipeline off is accepted now: send enabled false (expected_revision and workspace_id may come with it); any other change answers 403 LOOP_COMING_SOON.`;
|
|
697
|
+
}
|
|
698
|
+
return lead;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* A loop tool's whole description while the loop is SWITCHED OFF. An agent
|
|
702
|
+
* that reads a tool name will try it, so no description may name a loop tool
|
|
703
|
+
* that is not registered then: the open-loop descriptions of the list, the
|
|
704
|
+
* read and the edit name loop_list_pipeline_attempts, loop_create_pipeline,
|
|
705
|
+
* loop_list_models and more, so those three say only what still works and
|
|
706
|
+
* name only the five. The delete and the cancel name nothing else of the loop
|
|
707
|
+
* (the delete names loop_update_pipeline, which stays), so they keep their own
|
|
708
|
+
* words with the note.
|
|
709
|
+
*/
|
|
710
|
+
export function loopSwitchedOffDescription(name, description) {
|
|
711
|
+
const note = loopSwitchedOffNote(name);
|
|
712
|
+
switch (name) {
|
|
713
|
+
case "loop_list_pipelines":
|
|
714
|
+
return `Every Conscious Loop pipeline in the workspace, with what each still runs: its live version (a deployment that keeps answering and billing until it is stopped or the pipeline is deleted) and any attempt in flight (active_run). ${note} To stop paying: loop_update_pipeline with enabled false stops new attempts, loop_cancel_training_run stops an attempt in flight (active_run.run_id), and loop_delete_pipeline stops its live version and deletes it for good.`;
|
|
715
|
+
case "loop_get_pipeline":
|
|
716
|
+
return `One Conscious Loop pipeline by its id (rule_id, from loop_list_pipelines): its revision, whether it is switched on, its live version and any attempt in flight (active_run, whose run_id loop_cancel_training_run takes). ${note}`;
|
|
717
|
+
case "loop_update_pipeline":
|
|
718
|
+
return `Switch a Conscious Loop pipeline off, so it starts no new attempt. An attempt already in flight goes on unless loop_cancel_training_run stops it, and its live version keeps answering (and billing) until it is stopped or loop_delete_pipeline deletes the pipeline. Pass expected_revision (the revision loop_get_pipeline shows) to be refused REVISION_MISMATCH rather than overwrite someone else's edit. ${note}`;
|
|
719
|
+
default:
|
|
720
|
+
return `${description} ${note}`;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/** loop_update_pipeline's parameters while the loop is SWITCHED OFF: only
|
|
724
|
+
* switching a pipeline off is accepted there, so nothing else is offered. */
|
|
725
|
+
export function loopSwitchedOffUpdateShape() {
|
|
726
|
+
return {
|
|
727
|
+
pipeline_id: z.string().describe("The pipeline's id (rule_id), from loop_list_pipelines"),
|
|
728
|
+
workspace_id: z.string().optional().describe("The workspace the pipeline belongs to. Omit to use the key's own workspace."),
|
|
729
|
+
enabled: z.literal(false).describe("false: switch the pipeline off. Nothing else is accepted while the Conscious Loop is switched off."),
|
|
730
|
+
expected_revision: z.number().int().min(0).optional().describe("The revision you read"),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
690
733
|
/* ─── Server setup ─── */
|
|
691
734
|
export function createBiosMcpServer(client, hooks, deploymentCaps,
|
|
692
735
|
// Pre-launch surface gates (src/config.ts). While a gate is on, its creation
|
|
@@ -823,12 +866,24 @@ launchGates = resolveLaunchGates(), allowedTools) {
|
|
|
823
866
|
};
|
|
824
867
|
// The platform guide's Conscious Loop section. Where the loop is not offered
|
|
825
868
|
// (launchGates.loop) no loop tool is registered, so the guide says nothing about
|
|
826
|
-
// the loop at all: an agent that reads a tool name will try it.
|
|
827
|
-
//
|
|
828
|
-
//
|
|
869
|
+
// the loop at all: an agent that reads a tool name will try it. Where the API
|
|
870
|
+
// says it is switched off (launchGates.loopSwitchedOff) the five tools that
|
|
871
|
+
// stop what a pipeline still runs are registered, and the guide names those
|
|
872
|
+
// five and nothing else. The alias tools are inference tools that stay
|
|
873
|
+
// registered either way, so they keep a line of their own there.
|
|
874
|
+
const aliasGuideSection = `## Inference Aliases
|
|
875
|
+
- set_inference_alias / list_inference_aliases / delete_inference_alias - Re-pointable public handles: a name customers call that can be moved to different weights without them changing anything.`;
|
|
829
876
|
const loopGuideSection = launchGates.loop
|
|
830
|
-
?
|
|
831
|
-
|
|
877
|
+
? launchGates.loopSwitchedOff === true
|
|
878
|
+
? `## Conscious Loop: switched off
|
|
879
|
+
The Conscious Loop is switched off on this platform: nothing new is recorded or trained. So that nothing a member started keeps charging them, five tools still work:
|
|
880
|
+
- loop_list_pipelines / loop_get_pipeline - Find the pipelines and see what each still runs (its live version's deployment, an attempt in flight).
|
|
881
|
+
- loop_update_pipeline - Switch a pipeline off: send enabled false (with its expected_revision); no other change is accepted while the loop is switched off.
|
|
882
|
+
- loop_delete_pipeline - Delete a pipeline for good: its live version's deployment is stopped unless another name answers from it. Confirm with the user first.
|
|
883
|
+
- loop_cancel_training_run - Cancel an attempt still running.
|
|
884
|
+
|
|
885
|
+
${aliasGuideSection}`
|
|
886
|
+
: aliasGuideSection
|
|
832
887
|
: `## Conscious Loop MCP Tools
|
|
833
888
|
The loop turns what a deployed model actually did into what it learns next.
|
|
834
889
|
NOTHING IS RECORDED until a source is switched on — capture is a consent
|
|
@@ -839,20 +894,21 @@ pipeline is deleted. Every other source is switched on with loop_set_capture.
|
|
|
839
894
|
|
|
840
895
|
1. loop_set_capture / loop_list_capture_settings - Turn recording on or off for a source, choose how long conversations are kept, and the tags stamped on everything it records (a tag is how live traffic reaches a pipeline). A pipeline's own tag is already on; any other source records nothing until this is done.
|
|
841
896
|
2. loop_capture_trace / loop_import_rows - Record one exchange as it happens (what the model was asked, what it answered, any tool calls), whoever served the model — Run BiOS, another provider, or your own servers. Send it to a pipeline's own_tag as the source, add labels to feed other pipelines too, and give feedback on the trace_id it returns. loop_import_rows brings in data that already exists instead: an export, a spreadsheet of past answers, preference pairs. Imported rows arrive as conversations NEEDING REVIEW, never as finished training data.
|
|
842
|
-
3. loop_list_traces / loop_get_trace / loop_delete_trace - Read what has been recorded; pipeline=<id> lists one pipeline's samples. Deleting a conversation a pipeline's live version learned from rebuilds that pipeline's model from the base without it (the answer's rebuilds names each). Credentials and personal details are stripped before storage, never after.
|
|
897
|
+
3. loop_list_traces / loop_get_trace / loop_delete_trace - Read what has been recorded; pipeline=<id> lists one pipeline's samples, each with its state there (held back, learned, trained, waiting or left out). Where the platform scores samples (a pipeline's auto_scoring is not null), samples nobody reviewed are scored by the judge while a pipeline is short of its next attempt's samples, and those scoring 0.8 or more are trained on; judge=picked|below_bar|unscored splits them, and a person's feedback always decides. Deleting a conversation a pipeline's live version learned from rebuilds that pipeline's model from the base without it (the answer's rebuilds names each). Credentials and personal details are stripped before storage, never after.
|
|
843
898
|
4. loop_add_signal / loop_list_signals - Feedback: accepted (good), rejected (bad), or edited with the corrected answer. Append-only. A correction is the most valuable feedback there is.
|
|
844
899
|
5. loop_label_trace / loop_remove_label / loop_list_labels / loop_get_trace_pipelines - Tags. A pipeline trains on every conversation carrying any of its tags, and a conversation can carry several and feed several pipelines; loop_get_trace_pipelines says which pipelines one conversation feeds.
|
|
845
|
-
6.
|
|
846
|
-
7.
|
|
847
|
-
8.
|
|
848
|
-
9.
|
|
849
|
-
10.
|
|
850
|
-
11.
|
|
851
|
-
12.
|
|
852
|
-
13.
|
|
853
|
-
14.
|
|
854
|
-
15.
|
|
855
|
-
16.
|
|
900
|
+
6. loop_list_datasets / loop_get_dataset / loop_preview_dataset / loop_create_dataset / loop_update_dataset / loop_delete_dataset - Datasets: saved sets of conditions (tags, any or all of them; feedback; where samples came from; which model answered; when they were recorded) that select samples live, so samples that match later join. A pipeline trains on its own tags OR the datasets it adds (loop_update_pipeline datasets), narrowed by its conditions. loop_preview_dataset counts what conditions select before anything is saved. Changing a dataset changes what every pipeline using it trains on from its next attempt.
|
|
901
|
+
7. loop_stats / loop_get_metrics_history - How much is recorded and reviewed (the workspace's totals: samples recorded lately, every sample by its feedback, and how many pipelines are training, waiting on the user, serving or paused), and how the attempts and the feedback moved over time.
|
|
902
|
+
8. loop_list_models - The base models a pipeline can train, and the platform's fixed sample minimums (floors). The only list a pipeline's model may come from.
|
|
903
|
+
9. loop_create_pipeline - One use case: a name, a model (optionally a challenger model), its tags, when to train (optionally a schedule; the sample minimums are fixed by the platform) and how a new version is promoted. It switches on the source named by its own tag, so an app can send samples at once. SPEND CONSENT: saving it authorises attempts on the workspace's wallet whenever its data has grown enough; the platform chooses the GPUs, and what is charged is the GPU time its attempts use and, where the platform scores samples, the judge's scoring of unreviewed samples, shown on the attempt it was for. Say so and wait for a yes.
|
|
904
|
+
10. loop_list_pipelines / loop_get_pipeline / loop_list_pipeline_attempts / loop_update_pipeline / loop_delete_pipeline - Each pipeline's versions (winners only, v1, v2 ... numbers only go up), its attempts and how each ended (the latest on the pipeline, every one a page at a time with loop_list_pipeline_attempts, each with an explanation in plain words), the live version and whether its deployment answers (live_deployment), status and next_reason, and its settings. Deleting a pipeline stops its live version's deployment (unless another pipeline or model name answers from it too) and deletes its history.
|
|
905
|
+
11. loop_run_pipeline / loop_import_into_pipeline - Train now (skips the schedule, never the minimum), and bring a file in as this pipeline's samples.
|
|
906
|
+
12. loop_create_benchmark / loop_list_benchmarks / loop_get_benchmark / loop_list_benchmark_items / loop_get_benchmark_history / loop_retire_benchmark / loop_get_benchmark_run / loop_list_benchmark_run_items - Fixed sets of conversations every attempt is replayed on, so "is it getting better" has a yardstick that does not move, and each replay's report: its scores, how many conversations each model passed (score 0.70 or more), and every conversation behind them.
|
|
907
|
+
13. loop_list_pipeline_benchmarks / loop_attach_pipeline_benchmark / loop_move_pipeline_benchmark / loop_remove_pipeline_benchmark / loop_compare_versions / loop_compare_attempts - A pipeline's benchmarks in priority order (1 is the primary), its versions side by side (v1 against v5), and any two attempts side by side, put live or not, conversation by conversation on the held-back samples both were scored on.
|
|
908
|
+
14. loop_list_training_runs / loop_get_training_run - Every attempt of every pipeline (result filters by what became of it; each row's attempt is as its pipeline's own list says it): its timeline, why it went live or did not (promotion, and verdict_checks: every bar its comparison needed, what it got and whether it was met), what each part cost, and available_actions — what this reader may actually do next, rather than what the state looks like it allows.
|
|
909
|
+
15. loop_get_evaluation / loop_list_evaluation_items - The comparison behind a verdict: the win rate, the pass rates (the share of conversations each model passed: score 0.70 or more) and the per-dimension means on the held-back samples, and the paired conversations themselves, with the judge's reasons. Read the warnings out, not just the verdict.
|
|
910
|
+
16. loop_promote_training_run / loop_reject_training_run / loop_rollback_training_run / loop_cancel_training_run - The decision. Promotion makes an attempt the live version and changes what real customers get; it is undoable for 30 days.
|
|
911
|
+
17. set_inference_alias / list_inference_aliases / delete_inference_alias - The re-pointable public handles a promotion writes: a name customers call that can be moved to different weights without them changing anything.
|
|
856
912
|
|
|
857
913
|
END TO END: capture what a model was asked and answered (or import it), say whether each answer was good, bad or corrected, and tag it; create ONE pipeline per use case. When enough new samples carry its tags to meet the platform's fixed minimum (loop_list_models floors; the pipeline's minimums say how many are still needed) and its schedule is due, if it has one, the platform trains an attempt, compares it with the live version on held-back samples it never trains on and on the pipeline's benchmarks, and promotes it by the promotion policy: when it is better, or when a member approves it. A rebuild after a member deleted a conversation the live version learned goes live unless it is clearly worse, under every policy but manual, where it waits for a member like any attempt; so a version need not have beaten the one before it. Everything before the pipeline costs nothing; saving a pipeline is where money starts, which is why it is spend consent. Two fields carry the state a user actually asks about and BOTH must be read back in plain words rather than summarised as 'fine': status says whether the platform is working or the user has to act, and next_reason says what a quiet pipeline is waiting for, because a pipeline waiting for samples looks identical to a broken one.`;
|
|
858
914
|
const server = {
|
|
@@ -860,13 +916,20 @@ END TO END: capture what a model was asked and answered (or import it), say whet
|
|
|
860
916
|
if (allowedTools && !allowedTools.has(name))
|
|
861
917
|
return;
|
|
862
918
|
// PRE-LAUNCH GATE (launchGates.loop — src/config.ts): where the Conscious
|
|
863
|
-
// Loop is
|
|
864
|
-
// included.
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
|
|
919
|
+
// Loop is not offered the WHOLE family stays unregistered, reads
|
|
920
|
+
// included. Where the API says it is SWITCHED OFF
|
|
921
|
+
// (launchGates.loopSwitchedOff), loop-service still answers the five
|
|
922
|
+
// routes a member needs to stop paying (list and read pipelines, switch
|
|
923
|
+
// one off, delete one, cancel an attempt), so their five tools stay, each
|
|
924
|
+
// saying what still works; every other loop route would only answer 403
|
|
925
|
+
// LOOP_COMING_SOON.
|
|
926
|
+
if (!loopToolOffered(name, launchGates))
|
|
869
927
|
return;
|
|
928
|
+
if (launchGates.loop && name.startsWith(LOOP_TOOL_PREFIX)) {
|
|
929
|
+
description = loopSwitchedOffDescription(name, description);
|
|
930
|
+
if (name === "loop_update_pipeline")
|
|
931
|
+
paramsSchema = loopSwitchedOffUpdateShape();
|
|
932
|
+
}
|
|
870
933
|
mcp.tool(name, description, paramsSchema, wrapToolHandler(name, cb, hooks?.onToolCall));
|
|
871
934
|
registeredTools.add(name);
|
|
872
935
|
},
|
|
@@ -3024,7 +3087,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3024
3087
|
});
|
|
3025
3088
|
return json(data);
|
|
3026
3089
|
});
|
|
3027
|
-
server.tool("loop_import_rows", "Bring data the user ALREADY HAS into the loop — an export from another provider, a spreadsheet of past answers, a set of preference pairs. This does NOT create a training set: it creates conversations, in the same place captured ones live, subject to the same review, tags and pipelines. A row becomes trainable when something says it is good, never because it arrived in a file. Each row is read for what it is: a prompt/chosen/rejected triple becomes a correction (chosen is learned from); an answer plus a yes-or-no becomes a thumbs verdict; a question and an answer waits for review; a question with no answer waits for an answer; a paragraph of prose is refused, because it is not a conversation. A verdict that arrives with the file is kept, but recorded as having come from the user's earlier process rather than from a reviewer here. WHAT COMES BACK, and what to tell the user: imported is what this call created and trace_ids names the conversations from the file that are now in the loop; already_present is rows an earlier import of the same file already had, so they were not stored twice; by_shape counts the shapes this call CREATED, not what was merely recognised and not rows that were already here, so a file that was silently the wrong shape shows up there rather than merely importing fewer rows than expected, and reviewed and needs_review likewise count only what this call wrote; refused
|
|
3090
|
+
server.tool("loop_import_rows", "Bring data the user ALREADY HAS into the loop — an export from another provider, a spreadsheet of past answers, a set of preference pairs. This does NOT create a training set: it creates conversations, in the same place captured ones live, subject to the same review, tags and pipelines. A row becomes trainable when something says it is good, never because it arrived in a file. Each row is read for what it is: a prompt/chosen/rejected triple becomes a correction (chosen is learned from); an answer plus a yes-or-no becomes a thumbs verdict; a question and an answer waits for review; a question with no answer waits for an answer; a paragraph of prose is refused, because it is not a conversation. A verdict that arrives with the file is kept, but recorded as having come from the user's earlier process rather than from a reviewer here. WHAT COMES BACK, and what to tell the user: imported is what this call created and trace_ids names the conversations from the file that are now in the loop; already_present is rows an earlier import of the same file already had, so they were not stored twice; by_shape counts the shapes this call CREATED, not what was merely recognised and not rows that were already here, so a file that was silently the wrong shape shows up there rather than merely importing fewer rows than expected, and reviewed and needs_review likewise count only what this call wrote; refused is rows whose shape could not be read, which the user fixes by changing the file, and refused_reasons says why, one {code, count, sentence} per reason: read the sentences back, never the codes (refused_why carries the codes alone, from an older service too); not_saved with not_saved_rows is rows that were read and understood and then could not be stored, which the file cannot fix; verdicts_not_saved with verdicts_not_saved_rows is rows whose conversation stored and whose VERDICT did not, so those conversations are in the loop waiting for review rather than reviewed. A call with any not_saved or verdicts_not_saved rows fails with code IMPORT_INCOMPLETE and carries all of the same counts. HOW TO RETRY SAFELY: every response carries import_id, the token that call was filed under. Send the SAME rows again with that import_id and anything already imported comes back under already_present instead of being stored a second time, while a verdict that failed to write is attempted again. Do NOT retry without repeating import_id: a call that repeats no token is its own import and stores everything again, deliberately, because two calls carrying the same rows are as likely to be two pages of one export as one call sent twice. SENDING MORE THAN 5000 ROWS: split the file and either give every row its own row_ids entry, after which chunking and ordering stop mattering, or send one import_id with the row_offset each page starts at. Without row_ids, do not send back only the rows not_saved_rows names: a shorter list moves every row after the gap and imports it again.", {
|
|
3028
3091
|
source: z.string().describe("Where this came from, e.g. 'zendesk-2026' or 'gpt4-history'. Required, and becomes the source every imported conversation is filed under — do not use a generic word like 'import', because that produces a corpus nobody can slice later."),
|
|
3029
3092
|
rows: z.array(z.record(z.string(), z.any())).describe("The rows themselves, already parsed. At most 5000 per call: the call is synchronous. Pass each row in whatever shape it already has rather than reshaping it — the shape is how the server decides whether it carries a verdict."),
|
|
3030
3093
|
model: z.string().optional().describe("Which model produced these answers, when the rows do not say per-row"),
|
|
@@ -3040,24 +3103,28 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3040
3103
|
});
|
|
3041
3104
|
return json(data);
|
|
3042
3105
|
});
|
|
3043
|
-
server.tool("loop_list_traces", "List recorded conversations (samples) for this workspace, newest first. To read a pipeline's data by feedback, pass pipeline with signal ('good', 'bad' or 'corrected': the
|
|
3106
|
+
server.tool("loop_list_traces", "List recorded conversations (samples) for this workspace, newest first. To read a pipeline's data by feedback, pass pipeline with signal ('good', 'bad' or 'corrected': the feedback from a PERSON that decides the sample) or unsignalled true (what no person has reviewed yet). Good, bad and corrected are only ever a person's words: the judge's score of an unreviewed sample is not feedback, so a sample it scored stays unsignalled, and judge splits that unreviewed queue into picked (scored 0.8 or more, trained on), below_bar (not trained on) and unscored; the three add up to unsignalled, and a row the judge scored carries judge {score, picked}. A bad sample is never trained on. With pipeline, each row carries state, what happens to it in that pipeline: held_back (held back to test its attempts, never trained on), learned (by version_no), trained (by attempt_no, which did not go live), waiting (for the next attempt) or left_out, with reason, the sentence to read back. dataset lists what one saved dataset selects (not with pipeline); attempt (a run id) with split train or held_back lists the samples that attempt trained on or held back. from/to narrow by when a conversation was recorded. A conversation kept in the workspace's own storage carries payload_stored_externally; when its text could not be read just now it also carries payload_unavailable, with messages null and completion empty. That conversation is NOT empty: say its text could not be read right now, do not review it, and read it again later. q searches the text; samples kept in the workspace's own storage cannot be searched, and a search's answer says how many were left out as unsearched: say so rather than reporting the search as complete.", {
|
|
3044
3107
|
q: z.string().min(2).max(200).optional().describe("Text search, 2 to 200 characters: any case, matched as written in every message, the answer and its tool calls. Combines with every other filter."),
|
|
3045
3108
|
source: z.string().optional().describe("Only conversations from this source"),
|
|
3046
3109
|
model: z.string().optional().describe("Only what THIS model answered, e.g. 'moonshotai/kimi-k3'. The filter distillation is made of — every other filter selects by what was asked or what somebody said about the answer, never by who produced it."),
|
|
3047
3110
|
origin: z.enum(["captured", "imported"]).optional().describe("captured = the workspace's own model behaviour; imported = a file somebody brought in. Pooling them makes 'train only on what we actually served' unanswerable."),
|
|
3048
3111
|
conversation_id: z.string().optional(),
|
|
3049
|
-
pipeline: z.string().optional().describe("Only the conversations this pipeline
|
|
3112
|
+
pipeline: z.string().optional().describe("Only the conversations this pipeline trains on (its id, from loop_list_pipelines): its tags or its datasets, narrowed by its conditions, reviewed or not. Each row then carries state."),
|
|
3113
|
+
dataset: z.string().optional().describe("Only the conversations one saved dataset selects (its id, from loop_list_datasets): the same samples its samples count. Not with pipeline."),
|
|
3114
|
+
attempt: z.string().optional().describe("Only the samples of one attempt's set (a run id, from loop_list_pipeline_attempts); pass split too."),
|
|
3115
|
+
split: z.enum(["train", "held_back"]).optional().describe("With attempt: train (what it trained on) or held_back (what it was tested on)."),
|
|
3050
3116
|
label: z.string().optional().describe("Only conversations carrying this bare tag. A tag with children matches them too."),
|
|
3051
3117
|
attributes: z.record(z.string(), z.string()).optional().describe("Only conversations matching every one of these dimensions, e.g. {\"category\":\"billing\",\"language\":\"es\"}"),
|
|
3052
3118
|
unlabelled: z.boolean().optional().describe("Only the conversations nobody has described yet — the pile worth looking at before deciding what the dimensions should be"),
|
|
3053
|
-
signal: z.enum(["good", "bad", "corrected"]).optional().describe("Only conversations whose deciding feedback is this: good (accepted), bad (rejected) or corrected (an edited or gold answer)"),
|
|
3054
|
-
unsignalled: z.boolean().optional().describe("Only conversations
|
|
3055
|
-
signalled_only: z.boolean().optional().describe("Only conversations
|
|
3119
|
+
signal: z.enum(["good", "bad", "corrected"]).optional().describe("Only conversations whose deciding feedback from a person is this: good (accepted), bad (rejected) or corrected (an edited or gold answer). The judge's score is never one of these"),
|
|
3120
|
+
unsignalled: z.boolean().optional().describe("Only conversations no person has given feedback on yet: the review queue, whether or not the judge scored them"),
|
|
3121
|
+
signalled_only: z.boolean().optional().describe("Only conversations a person gave feedback on (good, bad or corrected)"),
|
|
3122
|
+
judge: z.enum(["picked", "below_bar", "unscored"]).optional().describe("The unreviewed samples, split by the judge: picked (scored 0.8 or more, trained on unless a person says otherwise), below_bar (scored lower, not trained on) or unscored (not scored yet). Each implies no feedback from a person"),
|
|
3056
3123
|
from: z.string().datetime({ offset: true }).optional().describe("Only conversations recorded at or after this RFC 3339 time"),
|
|
3057
3124
|
to: z.string().datetime({ offset: true }).optional().describe("Only conversations recorded at or before this RFC 3339 time"),
|
|
3058
3125
|
limit: z.number().int().min(1).max(200).optional().describe("Default 50, maximum 200"),
|
|
3059
3126
|
offset: z.number().int().optional(),
|
|
3060
|
-
}, async ({ q, source, model, origin, conversation_id, pipeline, label, attributes, unlabelled, signal, unsignalled, signalled_only, from, to, limit, offset }) => {
|
|
3127
|
+
}, async ({ q, source, model, origin, conversation_id, pipeline, dataset, attempt, split, label, attributes, unlabelled, signal, unsignalled, signalled_only, judge, from, to, limit, offset }) => {
|
|
3061
3128
|
const attrs = Object.entries(attributes ?? {})
|
|
3062
3129
|
.filter(([k, v]) => k && v)
|
|
3063
3130
|
.map(([k, v]) => `${k}:${v}`);
|
|
@@ -3069,12 +3136,16 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3069
3136
|
origin,
|
|
3070
3137
|
conversation_id,
|
|
3071
3138
|
pipeline,
|
|
3139
|
+
dataset,
|
|
3140
|
+
attempt,
|
|
3141
|
+
split,
|
|
3072
3142
|
label,
|
|
3073
3143
|
attr: attrs.length ? attrs : undefined,
|
|
3074
3144
|
unlabelled: unlabelled ? "true" : undefined,
|
|
3075
3145
|
signal,
|
|
3076
3146
|
unsignalled: unsignalled ? "true" : undefined,
|
|
3077
3147
|
signalled: signalled_only ? "true" : undefined,
|
|
3148
|
+
judge,
|
|
3078
3149
|
from,
|
|
3079
3150
|
to,
|
|
3080
3151
|
limit: limit?.toString(),
|
|
@@ -3083,7 +3154,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3083
3154
|
});
|
|
3084
3155
|
return json(data);
|
|
3085
3156
|
});
|
|
3086
|
-
server.tool("loop_get_trace", "Read one recorded conversation in full, with every verdict and tag anyone has recorded on it. Anything that looked like a credential or a personal detail was removed before it was stored, and redaction_report says what. payload_unavailable true (messages null, completion empty) means its text is kept in the workspace's own storage and could not be read just now, not that it is empty: say so, do not review it, and read it again later.", { trace_id: z.string().describe("The trace id returned by loop_capture_trace or loop_list_traces") }, async ({ trace_id }) => {
|
|
3157
|
+
server.tool("loop_get_trace", "Read one recorded conversation in full, with every verdict and tag anyone has recorded on it. When the judge scored it (it scores only samples nobody reviewed), judge says its score and whether it was picked (0.8 or more: trained on), and signals holds the judge's score (source judge, verdict scored) with picked, per-aspect metadata.scores and its reason (reason_unavailable true: the reason could not be read from storage just now). It is not feedback: say 'the judge scored it', never that it was marked good, and a person's feedback decides over it. Anything that looked like a credential or a personal detail was removed before it was stored, and redaction_report says what. payload_unavailable true (messages null, completion empty) means its text is kept in the workspace's own storage and could not be read just now, not that it is empty: say so, do not review it, and read it again later.", { trace_id: z.string().describe("The trace id returned by loop_capture_trace or loop_list_traces") }, async ({ trace_id }) => {
|
|
3087
3158
|
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}`);
|
|
3088
3159
|
return json(data);
|
|
3089
3160
|
});
|
|
@@ -3129,12 +3200,12 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3129
3200
|
return json(data);
|
|
3130
3201
|
});
|
|
3131
3202
|
server.tool("loop_list_labels", "Every label in this workspace with how many conversations carry it. Use it to see where the corpus actually is before choosing a slice to train on — and to discover the master groups, since a parent may have no conversations labelled with it directly. Two fields answer the group question and they answer different ones: 'parent' is the group ALL of a label's conversations are in, and is null the moment they disagree; 'parents' is every group ANY of them are in, each with how many of this label's conversations are in it. LIST THE MASTER GROUPS FROM 'parents' — a label with 12 conversations, 5 of them under support, reports parent null and parents [{support, 5}], so reading only 'parent' finds no group for it. Never state that a label is in a group on the strength of 'parents' alone; say how many of it are.", {}, async () => json(await client.api("/api/loop/labels")));
|
|
3132
|
-
server.tool("loop_get_trace_pipelines", "Which pipelines one conversation feeds: every live pipeline whose tags select it, and whether each is switched on. Being listed means the pipeline would select it, not that it will be trained on — the build still drops duplicates and answers it cannot use, and holds some back to judge by. An empty list is a real answer: the conversation counts toward 'reviewed' and trains nothing, so add a pipeline's tag to it (loop_label_trace) if it should.", { trace_id: z.string() }, async ({ trace_id }) => json(await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/pipelines`)));
|
|
3133
|
-
server.tool("loop_stats", "How much has been recorded in this workspace, how much of it has feedback, and how many samples a pipeline could train on right now (ready.sft: the ones marked good or corrected). ready.sft is an upper bound: duplicates, the held-back set and conversations too long to train on come out of it when an attempt trains. capture lists the calls sent with X-Loop-Pipeline in the last capture.days days that were not recorded under a tag, one row per tag (a call naming two tags is counted under each, so never add rows up as calls): reason no_pipeline (no pipeline has that tag: misspelt, never made, or deleted), too_large or incomplete (the answer could not be kept); empty when every call was recorded.", {}, async () => {
|
|
3203
|
+
server.tool("loop_get_trace_pipelines", "Which pipelines one conversation feeds: every live pipeline whose tags or datasets select it (and whose conditions keep it), and whether each is switched on. Being listed means the pipeline would select it, not that it will be trained on — the build still drops duplicates and answers it cannot use, and holds some back to judge by. Each pipeline's state says what happens to it there: held_back (held back to test its attempts, never trained on), learned (version_no learned it), trained (attempt_no trained on it and did not go live), waiting (for the next attempt) or left_out (not trained on as it is), and reason is the sentence to read back (for left_out, why). An empty list is a real answer: the conversation counts toward 'reviewed' and trains nothing, so add a pipeline's tag to it (loop_label_trace) if it should.", { trace_id: z.string() }, async ({ trace_id }) => json(await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/pipelines`)));
|
|
3204
|
+
server.tool("loop_stats", "How much has been recorded in this workspace, how much of it has feedback, and how many samples a pipeline could train on right now (ready.sft: the ones marked good or corrected). ready.sft is an upper bound: duplicates, the held-back set and conversations too long to train on come out of it when an attempt trains. capture lists the calls sent with X-Loop-Pipeline in the last capture.days days that were not recorded under a tag, one row per tag (a call naming two tags is counted under each, so never add rows up as calls): reason no_pipeline (no pipeline has that tag: misspelt, never made, or deleted), too_large or incomplete (the answer could not be kept); empty when every call was recorded. The workspace's totals: samples_recent is how many samples were recorded in the last 48 hours; feedback counts every sample by the feedback a person gave it (good, bad, corrected, unreviewed -- they add up to traces); pipelines is how many pipelines there are and how many are training, needs_you (waiting on the user), serving or paused.", {}, async () => {
|
|
3134
3205
|
const data = await client.api("/api/loop/stats");
|
|
3135
3206
|
return json(data);
|
|
3136
3207
|
});
|
|
3137
|
-
server.tool("loop_get_metrics_history", "How the loop moved over time: every attempt as a point, oldest first (its attempt_no, the version_no it became — null when it did not become one —, its win rate, cost and the recipe it trained on) and each day's feedback counted by verdict. truncated says older attempts exist beyond the limit. Pass rule_id (a pipeline's id) for one pipeline's attempts; the feedback counts are always the workspace's.", {
|
|
3208
|
+
server.tool("loop_get_metrics_history", "How the loop moved over time: every attempt as a point, oldest first (its attempt_no, the version_no it became — null when it did not become one —, its win rate, cost and the recipe it trained on, and the bars its comparison was judged against: mean_change_needed is the average score change it had to clear, promote_margin the most that bar asks, and the bar is lower near the top: a quarter of the room left) and each day's feedback counted by verdict. truncated says older attempts exist beyond the limit. Pass rule_id (a pipeline's id) for one pipeline's attempts; the feedback counts are always the workspace's. samples is the samples recorded each day over days, by their feedback today (good, bad, corrected, unreviewed): with rule_id, that pipeline's.", {
|
|
3138
3209
|
rule_id: z.string().optional().describe("One pipeline's id, from loop_list_pipelines. Omit for the whole workspace."),
|
|
3139
3210
|
limit: z.number().int().min(1).max(500).optional().describe("How many attempts at most: 200 when omitted, at most 500"),
|
|
3140
3211
|
days: z.number().int().min(1).max(365).optional().describe("Days of feedback counts: 90 when omitted, at most 365"),
|
|
@@ -3151,7 +3222,8 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3151
3222
|
/* LoRA, the held-back floor, the comparison -- and SAVING A */
|
|
3152
3223
|
/* PIPELINE IS THE AUTHORIZATION TO SPEND: whenever its data has grown */
|
|
3153
3224
|
/* enough it trains an attempt on the workspace's wallet with nobody in the */
|
|
3154
|
-
/* room.
|
|
3225
|
+
/* room. What is charged is the GPU time the attempts use and, where the */
|
|
3226
|
+
/* platform scores samples, the judge's scoring of unreviewed samples. */
|
|
3155
3227
|
/* */
|
|
3156
3228
|
/* The wire shapes are the frozen contract in docs/loop-training-contracts.md.*/
|
|
3157
3229
|
/* On an edit an ABSENT key leaves that setting alone and a PRESENT null */
|
|
@@ -3204,10 +3276,25 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3204
3276
|
lora_alpha: z.number().int().min(1).max(512).nullable().optional().describe("1 to 512. null: automatic (training_defaults.lora_alpha)."),
|
|
3205
3277
|
replay_percent: z.number().int().min(0).max(50).nullable().optional().describe("0 to 50: the share of already-learned samples an attempt replays when it builds on the live version, so the new version does not forget them. null: automatic (50)."),
|
|
3206
3278
|
}).strict().nullable().optional().describe("The pipeline's training settings (its training on loop_get_pipeline, each null while automatic; training_defaults says what automatic means for its model). Send only the keys that change: an absent key keeps the pipeline's, null puts that one back to automatic, a value sets it; training null puts every one back to automatic. Leave training out to keep them all. A change applies from the next attempt, and earlier attempts keep the settings they trained with (their recipe; custom true when one applied). Out of range is 422 TRAINING_SETTING_OUT_OF_RANGE naming the field and its range. Automatic is recommended: say so, and change a setting only when the user asks for it."),
|
|
3279
|
+
datasets: z.array(z.string()).max(32).optional().describe("Saved datasets (ids from loop_list_datasets) whose samples it also trains on, beside its tags; at most 32. On an edit the list replaces the previous one and [] takes every one off. A change applies from the next attempt: nothing is trained again and versions already made keep what they learned."),
|
|
3280
|
+
// STRICT, and three-state: absent keeps the conditions, null clears them,
|
|
3281
|
+
// an object replaces them whole. Tags are refused inside it by the service
|
|
3282
|
+
// (a pipeline's tags are its tags field), so they are not offered.
|
|
3283
|
+
conditions: z.object({
|
|
3284
|
+
feedback: z.array(z.enum(["good", "corrected", "bad", "unreviewed"])).optional().describe("Keep only samples whose feedback from a person is one of these; must include good or corrected (what a pipeline learns from). A sample the judge picked has no feedback from a person, so it is unreviewed here: a list without unreviewed trains only on samples a person reviewed."),
|
|
3285
|
+
origin: z.enum(["captured", "imported"]).nullable().optional().describe("captured: sent by the user's app. imported: from imported files."),
|
|
3286
|
+
model: z.string().min(1).max(200).nullable().optional().describe("Only samples this model answered, exactly."),
|
|
3287
|
+
from: z.string().datetime({ offset: true }).nullable().optional().describe("Only samples recorded at or after this RFC 3339 time."),
|
|
3288
|
+
to: z.string().datetime({ offset: true }).nullable().optional().describe("Only samples recorded at or before this RFC 3339 time."),
|
|
3289
|
+
}).strict().nullable().optional().describe("What narrows everything the pipeline trains on (its tags and its datasets alike). Absent keeps them, null clears them, an object replaces them. A change applies from the next attempt; to make the live version forget samples, delete them (loop_delete_trace)."),
|
|
3290
|
+
// STRICT: the switch is the only key the service takes under it.
|
|
3291
|
+
auto_scoring: z.object({
|
|
3292
|
+
enabled: z.boolean().describe("true (the default): where the platform scores samples (the pipeline's auto_scoring is not null), the judge scores samples nobody reviewed while the pipeline is short of the samples its next attempt needs, and the ones scoring 0.8 or more are trained on. false trains only on samples a person reviewed."),
|
|
3293
|
+
}).strict().optional().describe("The judge's scoring of unreviewed samples, on by default. It is not a training change and nothing is asked again; what it cost shows on the attempt it was for (sample_scoring_cents, inside cost_cents)."),
|
|
3207
3294
|
};
|
|
3208
3295
|
server.tool("loop_list_models", "The base models a pipeline can train — the ONLY models loop_create_pipeline accepts — recommended first, each with its name, author, family and size, and floors, the platform's fixed sample minimums (read-only; no pipeline sets them): first_version_samples (what a new pipeline's first attempt needs at the default held-back share, the held-back samples included), new_samples (each later attempt waits for new_samples new samples, 1,000, counted since the last trained attempt, however many samples that attempt trained on; where a pipeline builds on its live version -- its chain is set -- that attempt trains a new adapter on top of the live version, on what it has not learned yet plus a replay of what it already learned (about half of what it trains on, picked at random, so it is not forgotten), and a winning version is merged in and served as one model), and held_back (at least min_rows samples are held back, or the share a pipeline chooses from default_percent to max_percent if that is more; each attempt is judged on at most scored_rows of them, so a share that holds back more only keeps samples out of training). Offer the user a choice from this list; never type a model id from memory. unavailable lists the other models the platform knows, each with reason_code (not_trainable_sft_lora, not_servable_as_adapter, too_large_for_available_gpus, not_verified, registry_missing) and reason, one plain sentence: when the user asks for a model that is not in models, read its reason back rather than guessing why. 503 MODELS_UNAVAILABLE: the list could not be read just then; try again in a minute.", {}, async () => json(await client.api("/api/loop/models")));
|
|
3209
|
-
server.tool("loop_create_pipeline", "Create a pipeline: one use case, trained again whenever enough new samples carrying its tags exist (1,000 new ones for each attempt after the first; where the pipeline builds on its live version, each attempt trains a new adapter on top of it, on what it has not learned plus a random replay of what it learned, about half of what it trains on, and a winner is merged in). SPEND CONSENT: saving it authorises the platform to train attempts on the workspace's wallet with nobody present — whenever train_when is met it trains on GPUs the platform chooses, compares the attempt with the live version, and promotes it by the promotion policy.
|
|
3210
|
-
server.tool("loop_update_pipeline", "Edit a pipeline: its name, use case, model, challenger, tags, when to train, held-back share, promotion policy, version limit, or switch it off and on. Send ONLY what changes: an absent key leaves it alone, and null clears max_versions (no limit), challenger_model (no challenger), train_when.schedule (train as soon as the minimum is met), training (every training setting back to automatic) or one key inside training (that setting back to automatic). A training change applies from the next attempt; 422 TRAINING_SETTING_OUT_OF_RANGE names the field and its range. Saving is the authorization, as on a create: a change that makes attempts cost more (a bigger model, switching it back on) is SPEND CONSENT, so say so first. A larger held-back share is not SPEND CONSENT (nothing is asked again and the limits do not move), and what it changes depends on how many are held back: while fewer than floors.held_back.scored_rows (1,500) are, each attempt can score more of them, paid from the wallet; once that many are, a larger share scores nothing more and costs nothing more, and only keeps more new samples out of training, for good; and a share that still comes to the floor of floors.held_back.min_rows (100) changes nothing. Say whichever applies, never that a raise always costs more. Pass expected_revision (the pipeline's revision as you read it) to be refused REVISION_MISMATCH rather than overwrite someone else's edit; REVISION_MISMATCH also comes back, sent or not, when the pipeline changed while the edit was being saved (a version went live, which can change its models): nothing was saved, so read it again and redo the edit. A model change while an attempt is running is refused RUN_ACTIVE. 409 NOT_A_SIMPLE_PIPELINE: this pipeline was created before pipelines had tags, a use case or a challenger, so those three cannot be edited; everything else can. Every other refusal is loop_create_pipeline's, with the same meaning.", {
|
|
3296
|
+
server.tool("loop_create_pipeline", "Create a pipeline: one use case, trained again whenever enough new samples carrying its tags exist (1,000 new ones for each attempt after the first; where the pipeline builds on its live version, each attempt trains a new adapter on top of it, on what it has not learned plus a random replay of what it learned, about half of what it trains on, and a winner is merged in). SPEND CONSENT: saving it authorises the platform to train attempts on the workspace's wallet with nobody present — whenever train_when is met it trains on GPUs the platform chooses, compares the attempt with the live version, and promotes it by the promotion policy. What is charged is the GPU time its attempts actually use and, where the platform scores samples, the judge's scoring of unreviewed samples, shown on the attempt it was for. Say both in plain words and WAIT FOR AN EXPLICIT YES before calling. Take model.id from loop_list_models, never from memory. Leave training out (automatic settings, recommended) unless the user asks for a setting. datasets (from loop_list_datasets) adds their samples to what its tags select and conditions narrows both; leave both out unless the user asks. The judge's scoring of unreviewed samples is on unless auto_scoring.enabled is false. Where the platform scores samples (the pipeline's auto_scoring is not null), while the pipeline is short of the samples its next attempt needs, the judge scores samples nobody reviewed and those scoring 0.8 or more are trained on. Nothing is saved when it is refused. 409 PIPELINE_NAME_TAKEN or OWN_TAG_TAKEN: choose another name. 422 NO_GPU_FITS, MODEL_NOT_TRAINABLE or PROMOTION_*: the message names the fix. 422 NO_JUDGE_MODEL: no listed model can score the attempts; tell the user rather than retrying. 422 MODEL_REVISION_UNAVAILABLE: the model could not be pinned because the training service did not answer; try again shortly. 422 METHOD_NOT_ENABLED or CEILING_TOO_LOW: the platform cannot train this model now and nothing in the request changes that; tell the user. 409 TRAINING_NOT_ENABLED: the platform's training service would not take the pipeline while the platform's services were being updated; nothing was saved and nothing in the request is wrong, so try again in a few minutes. 503 MODELS_UNAVAILABLE, GPU_OPTIONS_UNAVAILABLE or AGENT_COULD_NOT_START: a service the save needs did not answer just then; try again in a minute.", pipelineInputShape, async (input) => json(await client.api("/api/loop/pipelines", { method: "POST", body: input })));
|
|
3297
|
+
server.tool("loop_update_pipeline", "Edit a pipeline: its name, use case, model, challenger, tags, datasets, conditions, the judge's scoring of unreviewed samples (auto_scoring), when to train, held-back share, promotion policy, version limit, or switch it off and on. Send ONLY what changes: datasets [] takes every dataset off, conditions null clears them, and a change to either applies from the next attempt (nothing is trained again; versions already made keep what they learned); an absent key leaves it alone, and null clears max_versions (no limit), challenger_model (no challenger), train_when.schedule (train as soon as the minimum is met), training (every training setting back to automatic) or one key inside training (that setting back to automatic). A training change applies from the next attempt; 422 TRAINING_SETTING_OUT_OF_RANGE names the field and its range. Saving is the authorization, as on a create: a change that makes attempts cost more (a bigger model, switching it back on, turning auto_scoring back on) is SPEND CONSENT, so say so first. A larger held-back share is not SPEND CONSENT (nothing is asked again and the limits do not move), and what it changes depends on how many are held back: while fewer than floors.held_back.scored_rows (1,500) are, each attempt can score more of them, paid from the wallet; once that many are, a larger share scores nothing more and costs nothing more, and only keeps more new samples out of training, for good; and a share that still comes to the floor of floors.held_back.min_rows (100) changes nothing. Say whichever applies, never that a raise always costs more. Pass expected_revision (the pipeline's revision as you read it) to be refused REVISION_MISMATCH rather than overwrite someone else's edit; REVISION_MISMATCH also comes back, sent or not, when the pipeline changed while the edit was being saved (a version went live, which can change its models): nothing was saved, so read it again and redo the edit. A model change while an attempt is running is refused RUN_ACTIVE. 409 NOT_A_SIMPLE_PIPELINE: this pipeline was created before pipelines had tags, a use case or a challenger, so those three cannot be edited; everything else can. Every other refusal is loop_create_pipeline's, with the same meaning.", {
|
|
3211
3298
|
pipeline_id: z.string().describe("The pipeline's id (rule_id), from loop_list_pipelines"),
|
|
3212
3299
|
...pipelineInputShape,
|
|
3213
3300
|
expected_revision: z.number().int().min(0).optional().describe("The revision you read"),
|
|
@@ -3216,7 +3303,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3216
3303
|
body,
|
|
3217
3304
|
})));
|
|
3218
3305
|
server.tool("loop_delete_pipeline", "Delete a pipeline for good. An attempt still running is cancelled; the deployment its live version answers from is stopped and deleted, so the live version stops answering and is no longer billed, unless another pipeline or model name of yours answers from it too. If one does, that deployment keeps running and billed until you stop it on Deployments, and its trained checkpoint is kept (and billed as storage) until that deployment is deleted. Its history is deleted in the background after the call returns: the checkpoints its attempts trained, the sets they trained on, and every conversation of their comparison and benchmark reports. Its samples and tags are kept. Recording from its own tag's source stops: an app still sending there gets captured:false. THIS CANNOT BE UNDONE and the name is spent: the response's retained_name can never be used by another pipeline in this workspace. To stop it for now, loop_update_pipeline with enabled false. Confirm with the user first, and say that the live version stops answering and the history is deleted.", { pipeline_id: z.string() }, async ({ pipeline_id }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}`, { method: "DELETE" })));
|
|
3219
|
-
server.tool("loop_run_pipeline", "Train now: start an attempt without waiting for the schedule. SPEND CONSENT: it trains on real GPUs charged to the wallet, so say so and get a yes. It never skips the minimum: 422 NOT_ENOUGH_SAMPLES carries have and need — read both back. 409 RUN_ACTIVE: an attempt is already running. An attempt waiting for GPUs to compare (active_run.state waiting_gpus) is the one exception: Train now books its two comparison machines again at once and answers 202; nothing is trained again. With a rebuild on its way (chain.rebuild: a member deleted a conversation the live version learned from), Train now starts the rebuild: a retrain from the base on every remaining sample, with no wait for new ones, at the version limit too (each rebuild that goes live is a version, so the pipeline can end past its limit). 409 VERSION_LIMIT_REACHED: raise or remove max_versions. 409 RULE_PAUSED: it is switched off or paused (paused_reason says why). 409 PIPELINE_NEEDS_SAVE: its settings changed without a pipeline save; save it with loop_update_pipeline, then train again. 409 MONTHLY_LIMIT_REACHED: the platform's runaway guard for this month; read resumes_at back. 409 SCORING_CAPPED: the comparison's key is at its monthly cap, so what it trained could not be scored; read resumes_at back (the pipeline's scoring_capped_until says the same before you try). 409 LIVE_VERSION_STOPPED: the deployment the live version answers from is stopped, paused or deleted, so an attempt could not be compared against it; the user resumes it in Deployments or rolls the version back. 503 AGENT_COULD_NOT_START or 409 AGENT_OFF: the key every attempt is scored with could not be renewed just then, so nothing was started; train now again in a minute (saving the pipeline does not help). Do not retry any of the others unchanged.", { pipeline_id: z.string() }, async ({ pipeline_id }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}/run`, {
|
|
3306
|
+
server.tool("loop_run_pipeline", "Train now: start an attempt without waiting for the schedule. SPEND CONSENT: it trains on real GPUs charged to the wallet, so say so and get a yes. It never skips the minimum: 422 NOT_ENOUGH_SAMPLES carries have and need — read both back. 409 RUN_ACTIVE: an attempt is already running. An attempt waiting for GPUs to compare (active_run.state waiting_gpus) is the one exception: Train now books its two comparison machines again at once and answers 202; nothing is trained again. With a rebuild on its way (chain.rebuild: a member deleted a conversation the live version learned from), Train now starts the rebuild: a retrain from the base on every remaining sample, with no wait for new ones, at the version limit too (each rebuild that goes live is a version, so the pipeline can end past its limit). 409 VERSION_LIMIT_REACHED: raise or remove max_versions. 409 RULE_PAUSED: it is switched off or paused (paused_reason says why). 409 PIPELINE_NEEDS_SAVE: its settings changed without a pipeline save; save it with loop_update_pipeline, then train again. 409 MONTHLY_LIMIT_REACHED: the platform's runaway guard for this month; read resumes_at back, unless message says no month fits (then read message back: a smaller model, or a save). 409 SCORING_CAPPED: the comparison's key is at its monthly cap, so what it trained could not be scored; read resumes_at back (the pipeline's scoring_capped_until says the same before you try). 409 LIVE_VERSION_STOPPED: the deployment the live version answers from is stopped, paused or deleted, so an attempt could not be compared against it; the user resumes it in Deployments or rolls the version back. 503 AGENT_COULD_NOT_START or 409 AGENT_OFF: the key every attempt is scored with could not be renewed just then, so nothing was started; train now again in a minute (saving the pipeline does not help). Do not retry any of the others unchanged.", { pipeline_id: z.string() }, async ({ pipeline_id }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}/run`, {
|
|
3220
3307
|
method: "POST",
|
|
3221
3308
|
body: {},
|
|
3222
3309
|
})));
|
|
@@ -3235,16 +3322,64 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3235
3322
|
method: "POST",
|
|
3236
3323
|
body,
|
|
3237
3324
|
})));
|
|
3238
|
-
|
|
3325
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
3326
|
+
/* TOOLS: Conscious Loop — datasets */
|
|
3327
|
+
/* */
|
|
3328
|
+
/* A DATASET is a saved, named set of conditions -- tags (any or all), */
|
|
3329
|
+
/* feedback, where samples came from, which model answered, when they were */
|
|
3330
|
+
/* recorded -- and a live selection, not a copy. A pipeline trains on its */
|
|
3331
|
+
/* own tags OR any dataset it adds, narrowed by its conditions. These are */
|
|
3332
|
+
/* the loop's datasets, not the beta Datasets product (list_datasets), */
|
|
3333
|
+
/* which holds files. */
|
|
3334
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
3335
|
+
const datasetFeedbackSchema = z.array(z.enum(["good", "corrected", "bad", "unreviewed"])).describe("Keep only samples whose feedback from a person is one of these. unreviewed is no feedback from a person, whether or not the judge scored it. Empty keeps every sample.");
|
|
3336
|
+
/** A dataset's conditions, every one optional; on an edit null clears origin, model, from and to, and [] clears tags and feedback. */
|
|
3337
|
+
const datasetConditionsShape = {
|
|
3338
|
+
tags: z.array(z.string()).max(32).optional().describe("Tags it selects by, at most 32; a tag selects its child tags too."),
|
|
3339
|
+
match: z.enum(["any", "all"]).optional().describe("any (the default): a sample carrying at least one of tags. all: every one of them."),
|
|
3340
|
+
feedback: datasetFeedbackSchema.optional(),
|
|
3341
|
+
origin: z.enum(["captured", "imported"]).nullable().optional().describe("captured: sent by the user's app. imported: from imported files."),
|
|
3342
|
+
model: z.string().min(1).max(200).nullable().optional().describe("Only samples this model answered, exactly."),
|
|
3343
|
+
from: z.string().datetime({ offset: true }).nullable().optional().describe("Only samples recorded at or after this RFC 3339 time."),
|
|
3344
|
+
to: z.string().datetime({ offset: true }).nullable().optional().describe("Only samples recorded at or before this RFC 3339 time."),
|
|
3345
|
+
};
|
|
3346
|
+
server.tool("loop_list_datasets", "The workspace's saved datasets, most recently changed first: each with its conditions, how many samples it selects now (samples, by feedback in feedback_counts), how many of them are in a benchmark and so never trained on (in_benchmarks), and the pipelines that train on it (used_by). A page: total says how many there are; pass offset for the next page.", {
|
|
3347
|
+
limit: z.number().int().min(1).max(200).optional().describe("How many datasets, 50 when absent, at most 200"),
|
|
3348
|
+
offset: z.number().int().min(0).optional(),
|
|
3349
|
+
}, async ({ limit, offset }) => json(await client.api("/api/loop/datasets", {
|
|
3350
|
+
params: { limit: limit?.toString(), offset: offset ? offset.toString() : undefined },
|
|
3351
|
+
})));
|
|
3352
|
+
server.tool("loop_get_dataset", "One saved dataset: its conditions, its revision, how many samples it selects now by feedback (samples is the same number loop_list_traces with dataset totals), in_benchmarks, and the pipelines that train on it (used_by).", { dataset_id: z.string().describe("The dataset's id, from loop_list_datasets") }, async ({ dataset_id }) => json(await client.api(`/api/loop/datasets/${encodeURIComponent(dataset_id)}`)));
|
|
3353
|
+
server.tool("loop_preview_dataset", "Count what conditions would select, before saving anything: samples by feedback, in_benchmarks, and the 20 models that answered most of them. datasets adds saved datasets' samples, as a pipeline adds them, so a pipeline's tags, datasets and conditions can be counted in one call. The counts are before duplicates and over-long conversations are set aside: say so, and use the pipeline's own minimums for exact training numbers. It writes nothing.", {
|
|
3354
|
+
...datasetConditionsShape,
|
|
3355
|
+
datasets: z.array(z.string()).optional().describe("Saved dataset ids whose samples are added to what the conditions select"),
|
|
3356
|
+
}, async (body) => json(await client.api("/api/loop/datasets/preview", { method: "POST", body })));
|
|
3357
|
+
server.tool("loop_create_dataset", "Save a dataset: a name and any of its conditions, all of which hold together; a dataset with no condition is every sample in the workspace. It is a live selection, not a copy: samples that match later join it. Nothing trains on it until a pipeline adds it (loop_update_pipeline datasets). 409 DATASET_NAME_TAKEN: the name is taken in this workspace, whatever its case; choose another.", {
|
|
3358
|
+
name: z.string().min(1).max(80).describe("What the dataset is called, 1 to 80 characters, unique in the workspace"),
|
|
3359
|
+
...datasetConditionsShape,
|
|
3360
|
+
}, async (body) => json(await client.api("/api/loop/datasets", { method: "POST", body })));
|
|
3361
|
+
server.tool("loop_update_dataset", "Change a saved dataset: send ONLY what changes (null clears origin, model, from or to; [] clears tags or feedback). Every pipeline in its used_by trains on the new selection from its next attempt: samples that join count as new samples and can bring a pipeline to its next attempt, which is paid from the wallet, so widening a dataset a pipeline uses is SPEND CONSENT for that pipeline: say which pipelines it changes first and wait for a yes. Nothing is trained again and versions already made keep what they learned. Pass expected_revision to be refused REVISION_MISMATCH rather than overwrite someone else's edit.", {
|
|
3362
|
+
dataset_id: z.string().describe("The dataset's id, from loop_list_datasets"),
|
|
3363
|
+
name: z.string().min(1).max(80).optional(),
|
|
3364
|
+
...datasetConditionsShape,
|
|
3365
|
+
expected_revision: z.number().int().min(1).optional().describe("The revision you read"),
|
|
3366
|
+
}, async ({ dataset_id, ...body }) => json(await client.api(`/api/loop/datasets/${encodeURIComponent(dataset_id)}`, {
|
|
3367
|
+
method: "PUT",
|
|
3368
|
+
body,
|
|
3369
|
+
})));
|
|
3370
|
+
server.tool("loop_delete_dataset", "Delete a saved dataset for good. Its samples stay. Every pipeline that trained on it (removed_from) stops taking its samples from its next attempt; versions already made keep what they learned. Confirm with the user first, naming the pipelines in its used_by.", { dataset_id: z.string().describe("The dataset's id, from loop_list_datasets") }, async ({ dataset_id }) => json(await client.api(`/api/loop/datasets/${encodeURIComponent(dataset_id)}`, { method: "DELETE" })));
|
|
3371
|
+
server.tool("loop_list_training_runs", "Every attempt of every pipeline, newest first: what state it is in, what it has spent, and what the comparison said. result filters by what became of an attempt, in loop_list_pipeline_attempts' words, and each row carries attempt: the attempt as its pipeline's own list says it (outcome, cost, pass rates and explanation, the plain words to read back; null for a run that is not a numbered attempt). A run sitting in 'awaiting_review' is waiting for a PERSON — it holds a candidate that is being paid for by the minute, so surface those first when the user asks what needs attention. A run in 'waiting_gpus' trained and is waiting for GPUs to run both models side by side: each try can bill the machine that got a GPU for up to 20 minutes while it waits for the other, nothing bills between tries, it keeps what it trained, tries again after 30 minutes, then 1, 2 and 4 hours, and ends after four tries or 24 hours (its last_reason says when; a last_reason opening 'waiting for room in this workspace to compare' means the workspace is at its limit of running deployments, and stopping a deployment lets it run), and Train now tries again at once. billed_candidate_up_to true means billed_candidate_cents is 'up to': a comparison machine was charged in it, counted from its first boot while the platform bills from its first answer (and at the hourly cap when no price was reported), so say 'up to', never 'spent'. It is false again once billing has settled every such machine: the run's timeline then has one line per machine saying what billing charged for the time it served and what it had been counted at (and why, when billing's figure is the higher one), so read that line back when a cost changed. The new model's machine of a run that went live (a version) or that a person took over is never settled, so that run stays 'up to'. comparison_returned_cents above 0 means a comparison machine never started (never reached running) and was not charged -- the platform bills it nothing, and the figures beside it are already net of it -- so say 'Comparison machine: not charged (it never started)' -- or, while a comparison figure above 0 remains beside it, that the one that never started was not charged and the one that ran was -- never that it was charged.", {
|
|
3239
3372
|
rule_id: z.string().optional().describe("Only this pipeline's attempts (its id, from loop_list_pipelines)"),
|
|
3240
3373
|
state: z.string().optional().describe("Only runs in this state, e.g. 'awaiting_review'"),
|
|
3374
|
+
result: z.enum(["became_version", "not_better", "inconclusive", "not_tested", "failed", "stopped", "running", "waiting_for_review"]).optional()
|
|
3375
|
+
.describe("Only the attempts that ended this way, as loop_list_pipeline_attempts filters them; running includes one waiting for a decision"),
|
|
3241
3376
|
limit: z.number().int().min(1).max(200).optional(),
|
|
3242
3377
|
offset: z.number().int().min(0).optional(),
|
|
3243
|
-
}, async ({ rule_id, state, limit, offset }) => json(await client.api("/api/loop/training-runs", {
|
|
3244
|
-
params: { rule_id, state, limit: limit?.toString(), offset: offset?.toString() },
|
|
3378
|
+
}, async ({ rule_id, state, result, limit, offset }) => json(await client.api("/api/loop/training-runs", {
|
|
3379
|
+
params: { rule_id, state, result, limit: limit?.toString(), offset: offset?.toString() },
|
|
3245
3380
|
})));
|
|
3246
|
-
server.tool("loop_get_training_run", "One run in full: the base model it trained (base_model_id, model_family) and the recipe, how the platform trained it (training_summary, the service's own sentence -- the steps it took, the passes planned over its samples and the checkpoint it kept; read it back as written, and trained_steps is the steps the trainer counted, never an estimate; both null until its job ended, and on an attempt from before), the rows it trained on and held back (lineage says what it was built on: trained_from versions -- a new adapter on top of built_on's versions merged into the base -- or base, with from_base_reason; and train_rows = new_samples + carried_samples, from attempts that did not go live, + replayed_samples, drawn from what those versions learned (the counts are null on a run from before the chain, or of a pipeline that does not build on its live version: say only train_rows then); error_code CHAIN_MOVED means it was built on a version that was rolled back while it trained, so it was set aside with no comparison paid and its samples go to the next attempt, NO_MERGED_EXPORT that its training finished without the merged model it is served as, TRAINING_SERVICE_BEHIND that the platform's training service was being updated when it asked for its training, so it stopped before any training started with nothing charged and the next attempt asks again (STEP_STALLED:TRAINING_SERVICE_BEHIND: the same after six hours of waiting for that update; the pipeline tries again on its own), and ENV_GATED:SWITCHED_OFF that the Conscious Loop was switched off on the platform before its training started, so it stopped there with no GPU time charged), its GPU time and what it was billed (billed_candidate_up_to true means billed_candidate_cents is 'up to': a comparison machine was charged in it, counted from its first boot while the platform bills from its first answer, and at the hourly cap when no price was reported -- say 'up to', never 'spent'), the timeline of every state it passed through and why, links to the job, the candidate and the comparison, benchmark_runs (every benchmark it was replayed on; each run_id is the replay's id for loop_get_benchmark_run and loop_list_benchmark_run_items), promotion (outcome as the pipeline's attempts list says it; decision, where rejected means a member chose not to publish it even when the verdict says better, auto_rejected that the pipeline's
|
|
3247
|
-
server.tool("loop_list_pipelines", "Every pipeline in the workspace: the series of versions it has produced (versions are attempts that were put live; attempts[] lists every attempt, winner or not, each with its outcome, trigger and decision -- rejected means a member chose not to publish it, where outcome says stopped, and expired that nobody decided before the review deadline; auto_rejected that the pipeline's
|
|
3381
|
+
server.tool("loop_get_training_run", "One run in full: the base model it trained (base_model_id, model_family) and the recipe, how the platform trained it (training_summary, the service's own sentence -- the steps it took, the passes planned over its samples and the checkpoint it kept; read it back as written, and trained_steps is the steps the trainer counted, never an estimate; both null until its job ended, and on an attempt from before), the rows it trained on and held back (lineage says what it was built on: trained_from versions -- a new adapter on top of built_on's versions merged into the base -- or base, with from_base_reason; and train_rows = new_samples + carried_samples, from attempts that did not go live, + replayed_samples, drawn from what those versions learned (the counts are null on a run from before the chain, or of a pipeline that does not build on its live version: say only train_rows then); error_code CHAIN_MOVED means it was built on a version that was rolled back while it trained, so it was set aside with no comparison paid and its samples go to the next attempt, NO_MERGED_EXPORT that its training finished without the merged model it is served as, TRAINING_SERVICE_BEHIND that the platform's training service was being updated when it asked for its training, so it stopped before any training started with nothing charged and the next attempt asks again (STEP_STALLED:TRAINING_SERVICE_BEHIND: the same after six hours of waiting for that update; the pipeline tries again on its own), and ENV_GATED:SWITCHED_OFF that the Conscious Loop was switched off on the platform before its training started, so it stopped there with no GPU time charged), its GPU time and what it was billed (billed_candidate_up_to true means billed_candidate_cents is 'up to': a comparison machine was charged in it, counted from its first boot while the platform bills from its first answer, and at the hourly cap when no price was reported -- say 'up to', never 'spent'; it is false again once billing has settled every such machine, and the timeline then says, one line per machine, what billing charged for the time it served and what it had been counted at -- read that line back when a cost changed; the new model's machine of a run that went live (a version) or that a person took over is never settled, so that run stays 'up to'; comparison_returned_cents above 0 means a comparison machine never started (never reached running) and was not charged -- the platform bills it nothing, and the figures beside it are already net of it -- so say 'Comparison machine: not charged (it never started)' -- or, while a comparison figure above 0 remains beside it, that the one that never started was not charged and the one that ran was -- never that it was charged), the timeline of every state it passed through and why, links to the job, the candidate and the comparison, benchmark_runs (every benchmark it was replayed on; each run_id is the replay's id for loop_get_benchmark_run and loop_list_benchmark_run_items), promotion (outcome as the pipeline's attempts list says it; decision, where rejected means a member chose not to publish it even when the verdict says better, auto_rejected that the pipeline's going-live choice turned it down on its own, and expired that nobody decided before the review deadline -- an expired attempt whose verdict is better was better, never say the pipeline refused it; and the going-live choice's reason and every measurement it read, each better, not_better, worse or not_measured with its reason -- null and empty when nothing was recorded), verdict_checks (every bar the held-back comparison had to clear -- conversations compared, failures, a rubric set, mean change, win rate, and the chance a model no better would win this often -- each with needed, got and met; null until the comparison is finished, when no thresholds were recorded, and for a report decided under earlier bars that these would not explain: when the user asks why an attempt did not go live, read the bars that are not met back in these words), verdict_checks_withheld (decided_under_earlier_rule when a finished report's bars are withheld because an earlier version of the platform's bars decided it, null otherwise: then say the comparison was decided under an earlier version of the platform's bars, so its bars are not listed -- its verdict stands as recorded; never say they were not recorded, and never work them out from today's bars), cost (training_cents, comparison_cents, scoring_cents, benchmark_cents, sample_scoring_cents -- what the judge's scoring of unreviewed samples cost before the attempt -- and total_cents, the attempt's cost_cents on the pipeline; up_to true means comparison_cents and the total are 'up to', counted from the comparison machines' first boot, and false once billing has settled every one of them (the timeline says what each was charged); comparison_returned_cents is what was taken off comparison_cents for a machine that never started: say it was not charged, and that comparison_cents above 0 is the one that ran), explanation (what became of it in plain words, as on the pipeline's attempts: read it back rather than the codes), run.trained_on (what it trained on as it was then: the pipeline's tags, its conditions and each dataset at its revision; null when its set is gone), traces_filter_attempt (true: loop_list_traces with attempt and split lists its samples), and available_actions — which says what THIS reader may actually do next. Use available_actions rather than guessing from the state: a run can look promotable and not be, because the consent moved or the candidate is already gone.", { run_id: z.string() }, async ({ run_id }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}`)));
|
|
3382
|
+
server.tool("loop_list_pipelines", "Every pipeline in the workspace: the series of versions it has produced (versions are attempts that were put live; attempts[] lists every attempt, winner or not, each with its outcome, trigger and decision -- rejected means a member chose not to publish it, where outcome says stopped, and expired that nobody decided before the review deadline; auto_rejected that the pipeline's going-live choice turned it down on its own; an expired attempt whose verdict is better was better: never say the pipeline refused it -- and train_rows and holdout_rows, the samples it trained on and held back; each attempt is judged on at most scored_rows (1,500) of the held-back samples, the same ones every attempt), which one serves the app today (live_version, null when what serves is not one of its versions) and whether its deployment still answers (live_deployment.status: serving answers; starting is on its way up; stopped, paused_for_funds or deleted answers nothing and is not billed; paused_by_platform was paused because billing could not be reached and answers nothing; failed answers nothing; live_deployment null means no check is recorded yet or what serves is not a deployment -- never say a live version with any status but serving is answering the app), how each version did against the champion of its day and on the standing benchmark, versions_made against max_versions, the run in flight and which version it will become, and status. Read-only. When the user asks 'is this getting better' or 'what is it doing', answer from this, and read status back honestly: needs_review and needs_funds are waiting on THEM (a decision, a top-up), paused names why in paused_reason (consent_invalid means its settings changed since it was last saved: saving it again starts it), complete means the version limit is reached, and running is the platform working -- including an attempt whose active_run.state is waiting_gpus: it trained, but no GPUs were free to run the new model and the one it is compared with side by side, so both machines were stopped; each try can bill the machine that got a GPU for up to 20 minutes while it waits for the other, nothing bills between tries, it keeps what it trained, tries again after 30 minutes, then 1, 2 and 4 hours, and ends after four tries or 24 hours (its last_reason says when; a last_reason opening 'waiting for room in this workspace to compare' means the workspace is at its limit of running deployments, and stopping a deployment lets it run), and Train now tries again at once. cost_up_to true on an attempt, active_run or a version means its cost_cents is 'up to': a comparison machine was charged in it, counted from its first boot while the platform bills it from its first answer (and at the hourly cap when no price was reported), so say 'up to', never 'spent'. It is false again once billing has settled every such machine (the attempt's timeline says what billing charged each one for the time it served). A version's own new-model machine is the one that went live and is never settled, so a version whose machine was charged stays 'up to'. On an attempt or active_run, comparison_returned_cents above 0 means a comparison machine never started (never reached running) and was not charged -- the platform bills it nothing, and the figures beside it are already net of it -- so say 'Comparison machine: not charged (it never started)' -- or, while a comparison figure above 0 remains beside it, that the one that never started was not charged and the one that ran was -- never that it was charged. next_reason is the pipeline's own sentence about what it is waiting for. scoring_capped_until, when set, is when the workspace's scoring key comes off its monthly cap: until then nothing trains and Train now answers SCORING_CAPPED, so say so instead of offering Train now. A monthly safety limit, set by the platform from the model's prices, can hold a pipeline until the 1st of next month (UTC): next_reason then says so, and Train now answers 409 MONTHLY_LIMIT_REACHED with resumes_at. It has no field and no figure to read back. A pipeline trains only on new samples: it never retrains the conversations an attempt already had with a recipe changed. The list is a page (100 unless limit says, at most 100), newest first: total is how many pipelines match and truncated is true when more exist after this page. When truncated, say it is the newest of total, and pass offset (this page's offset plus its length) for the next page. q finds pipelines by name, tag or model; status keeps those training, waiting on the user (needs_you), serving or paused. Each pipeline's attempts[] here is its latest 5 and its versions[] the latest few and the live one (attempts_total and versions_total say how many there are); loop_get_pipeline has every version, and loop_list_pipeline_attempts reads every attempt. Each attempt's explanation is its outcome in plain words (headline, detail, next): read those back rather than the codes. Each pipeline carries samples_total and samples_recent (recorded in the last 48 hours), and minimums with have and short, as loop_get_pipeline does.", {
|
|
3248
3383
|
limit: z.number().int().min(1).max(100).optional().describe("How many pipelines, 100 when absent, at most 100"),
|
|
3249
3384
|
offset: z.number().int().min(0).optional().describe("How many of the newest pipelines to skip"),
|
|
3250
3385
|
q: z.string().max(200).optional().describe("A case-insensitive substring of the name, the tag or the model, at most 200 characters"),
|
|
@@ -3257,11 +3392,11 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3257
3392
|
status,
|
|
3258
3393
|
},
|
|
3259
3394
|
})));
|
|
3260
|
-
server.tool("loop_list_pipeline_attempts", "A pipeline's attempts, newest first, a page at a time (50 unless limit says, at most 200): every training run it made, winner or not, each with the fields attempts[] has on loop_get_pipeline -- its outcome, decision, lineage, the samples it trained on and held back, its held-back pass rates (the share each side passed, score 0.70 or more), what it cost (cost_up_to true means 'up to', never 'spent') -- and explanation, its outcome in plain words: headline, detail and next (what happens now; null when there is nothing to do). Read the explanation back rather than the codes. total is how many match the filters; pass offset for the next page. result keeps the attempts that became_version, were not_better, inconclusive, failed, stopped, or are running (waiting_for_review included) or waiting_for_review; model keeps the pipeline's own model's attempts (main) or its second model's (second). Read-only.", {
|
|
3395
|
+
server.tool("loop_list_pipeline_attempts", "A pipeline's attempts, newest first, a page at a time (50 unless limit says, at most 200): every training run it made, winner or not, each with the fields attempts[] has on loop_get_pipeline -- its outcome, decision, lineage, the samples it trained on and held back, its held-back pass rates (the share each side passed, score 0.70 or more), what it cost (cost_up_to true means 'up to', never 'spent', and it is false again once billing has settled every comparison machine charged in it, whose timeline says what each was charged; comparison_returned_cents above 0 means a comparison machine never started and was not charged: say 'Comparison machine: not charged (it never started)', or, while the cost still counts a comparison machine that ran, that the one that never started was not charged) -- and explanation, its outcome in plain words: headline, detail and next (what happens now; null when there is nothing to do). Read the explanation back rather than the codes. total is how many match the filters; pass offset for the next page. result keeps the attempts that became_version, were not_better, inconclusive (tested, could not decide), not_tested (ended before it was tested), failed, stopped, or are running (waiting_for_review included) or waiting_for_review; model keeps the pipeline's own model's attempts (main) or its second model's (second). Read-only.", {
|
|
3261
3396
|
pipeline_id: z.string().describe("The pipeline's id (rule_id), from loop_list_pipelines"),
|
|
3262
3397
|
limit: z.number().int().min(1).max(200).optional().describe("How many attempts, 50 when absent, at most 200"),
|
|
3263
3398
|
offset: z.number().int().min(0).optional().describe("How many of the newest attempts to skip"),
|
|
3264
|
-
result: z.enum(["became_version", "not_better", "inconclusive", "failed", "stopped", "running", "waiting_for_review"]).optional().describe("Only the attempts that ended this way; running includes one waiting for a decision"),
|
|
3399
|
+
result: z.enum(["became_version", "not_better", "inconclusive", "not_tested", "failed", "stopped", "running", "waiting_for_review"]).optional().describe("Only the attempts that ended this way; running includes one waiting for a decision"),
|
|
3265
3400
|
model: z.enum(["main", "second"]).optional().describe("main: the pipeline's own model's attempts. second: its second model's."),
|
|
3266
3401
|
}, async ({ pipeline_id, limit, offset, result, model }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}/attempts`, {
|
|
3267
3402
|
params: {
|
|
@@ -3271,8 +3406,8 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3271
3406
|
model,
|
|
3272
3407
|
},
|
|
3273
3408
|
})));
|
|
3274
|
-
server.tool("loop_get_pipeline", "One pipeline in full, by its id: every version with its verdict, the decision a person made, its win rate, its pass rates on the held-back samples (candidate_pass_rate and incumbent_pass_rate: the share each side passed, score 0.70 or more) and benchmark score, what it cost and why it was made (trigger, in the same words as attempts[]), plus the run in flight. When the monthly safety limit holds it, next_reason says so; the limit has no field. Read-only: promote, reject and roll back stay on the run tools, and its settings on loop_update_pipeline. minimums says how many samples it has against what the next attempt needs. scoring_capped_until, when set, is when the workspace's scoring key comes off its monthly cap: until then nothing trains and Train now answers SCORING_CAPPED with that time as resumes_at. Only compare benchmark scores whose benchmark_comparable is true; the others were measured on a different yardstick. versions[] are WINNERS ONLY -- attempts that were put live, numbered v1, v2 ... in the order they went live, each with its attempt, base_model_id and model_family -- and live_version is the one the app's serving name reaches now, with live_deployment.status saying whether its deployment answers (serving answers; starting is on its way up; stopped, paused_for_funds or deleted answers nothing and is not billed; paused_by_platform was paused because billing could not be reached and answers nothing; failed answers nothing; null when no check is recorded yet or what serves is not a deployment); attempts[] is the pipeline's latest 20 training runs, winner or not (attempts_total says how many it made, and loop_list_pipeline_attempts pages through every one), each with explanation (its outcome in plain words: headline, detail, next -- read it back rather than the codes) and its outcome (became_version, not_better, inconclusive, stopped, failed, running, waiting_for_review), trigger (data, schedule, manual, challenger for the second model -- trained from its own base on all samples, never on this pipeline's versions -- or rebuild, a retrain from the base after a member deleted a conversation the live version learned from), decision (what was decided about it once compared, and by whom: promoted, auto_promoted, rejected, auto_rejected, expired or rolled_back; null while nothing has been -- rejected means a member chose not to publish it, where outcome says stopped, so say a member declined it rather than that it stopped; promoted that a member put it live and auto_promoted that the pipeline's rule did; auto_rejected that the pipeline's rule turned it down on its own; expired that nobody decided before the review deadline, so it was retired undecided -- an expired attempt whose verdict is better reads stopped and was better: never say the rule refused it), and train_rows and holdout_rows (the samples its set trained on and held back; null until its set was built). lineage on each version, attempt and the run in flight says what it was built on: trained_from versions (a new adapter trained on top of the live version: the pinned base with built_on's versions merged in, oldest first) or base (with from_base_reason: first_version, challenger, rebuild -- a deleted conversation when lineage.rebuild is true, or, when it is false, a live version already built on 64 versions, the most one attempt can build on -- model_cannot_build_on or chain_link_unreadable), and what it trained on -- train_rows = new_samples + carried_samples (samples of the attempts in carried_from_attempts, which did not go live, trained again) + replayed_samples (about half: as many as new_samples + carried_samples, or all of learned_before when that is fewer, drawn at random from what the versions it builds on already learned, so they are not forgotten); learned_before is what those versions had learned and was not trained again, except the replayed ones; served_as is adapter or merged (one model); learned_deleted_conversation true means it, or a version it was built on (merged into it), learned a conversation that was deleted, so it can no longer be rolled back to. The counts are null on an attempt from before the chain, or of a pipeline that does not build on its live version: say only train_rows then. Say it as 'built on v1 + v2, trained on 4,000 samples: 1,000 new, 1,000 from attempt 3 and 2,000 of what v1 + v2 learned, replayed' -- name the replayed part whenever replayed_samples is above 0, so the parts add up to train_rows. chain, when set, is the live version as the versions merged into it (versions, live one last), learned_samples and not_learned_samples (trained by attempts that did not go live, so trained again next), and rebuild when a deleted conversation the live version learned has a retrain from the base on its way; chain null means the pipeline does not build on its live version and every attempt trains from the base on all samples. Nothing in flight is a version yet. holdout is the pipeline's held-back share: percent (5 to 20), min_rows (at least that many samples are held back whatever the share), rows (how many it holds back today) and held_before (how many of rows an earlier run of the pipeline held back: they stay held back whatever became of that run and whatever the share says, so a lower share releases none of them; before the first version the first attempt needs held_before plus 1,000 to train on when that is more than the share asks for). Each attempt is judged on at most scored_rows (1,500) of the held-back samples, the same ones every attempt, so rows above that only keeps samples out of training. storage is what the pipeline's history takes up in storage (bytes, objects, and where: platform, your_storage or both): every conversation, answer and scoring reason of its reports, and the training rows its attempts are built from, is kept there and never in the database; it is not billed, counts toward the workspace's storage quota and is deleted with the pipeline. Where the pipeline builds on its live version, the merged models its versions answer from (the live one's, and each one a rollback can still put back; an attempt that did not go live keeps its merged model, billed, until it is cleaned up within a day) are billed as model storage, like any trained model, and each version's own adapter is billed as checkpoint storage, like any training checkpoint; its reports, benchmark results and training rows are not billed. An active_run whose last_reason says it is waiting for storage is holding answers already paid for until storage can keep them; they are kept then without asking the models again. cost_up_to true on attempts[], active_run or versions[] means that cost_cents is 'up to', the most it can have cost: a comparison machine was charged in it, counted from its first boot while the platform bills from its first answer (and at the hourly cap when no price was reported), so say 'up to', never 'spent'. An active_run.state of waiting_gpus means the attempt trained and is waiting for GPUs to run the new model and the one it is compared with side by side: each try can bill the machine that got a GPU for up to 20 minutes while it waits for the other, nothing bills between tries, it keeps what it trained, tries again after 30 minutes, then 1, 2 and 4 hours, and ends after four tries or 24 hours (its last_reason says when; a last_reason opening 'waiting for room in this workspace to compare' means the workspace is at its limit of running deployments, and stopping a deployment lets it run), and Train now tries again at once. training is the pipeline's training settings (epochs, learning_rate, lora_rank, lora_alpha, replay_percent), each null while automatic, and training_defaults what automatic means for its model (learning_rate, lora_rank, lora_alpha, replay_percent, and min_epochs to max_epochs); say 'automatic' for a null, never a guessed number. capture lists the calls sent with X-Loop-Pipeline in the last capture.days days that were not recorded, one row per tag (a call naming two tags is counted under each, so never add rows up as calls): under this pipeline's tags too_large or incomplete (the answer could not be kept), and under any tag no pipeline had no_pipeline (misspelt, or a deleted pipeline's); empty when every call was recorded.", { rule_id: z.string().describe("The pipeline's id, from loop_list_pipelines") }, async ({ rule_id }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(rule_id)}`)));
|
|
3275
|
-
server.tool("loop_list_pipeline_benchmarks", "The benchmarks a pipeline is measured on, in priority order (1 is the primary), then the ones it stopped using, and its promotion policy. Every attempt is replayed on each attached benchmark; the policy decides which of those measurements make it a version. Each says how it scores an answer (check: judge,
|
|
3409
|
+
server.tool("loop_get_pipeline", "One pipeline in full, by its id: what it trains on (tags, the datasets it adds, and the conditions that narrow both), samples_total (samples recorded so far, the same samples loop_list_traces with pipeline lists) and samples_recent (in the last 48 hours), auto_scoring (the judge's scoring of its unreviewed samples, null where the platform does not score them: read its message back as written; picked, below_bar and unscored add up to its unreviewed samples), minimums.picked_by_judge (how many of the samples it has the judge picked), every version with its verdict, the decision a person made, its win rate, its pass rates on the held-back samples (candidate_pass_rate and incumbent_pass_rate: the share each side passed, score 0.70 or more) and benchmark score, what it cost and why it was made (trigger, in the same words as attempts[]), plus the run in flight. When the monthly safety limit holds it, next_reason says so; the limit has no field. Read-only: promote, reject and roll back stay on the run tools, and its settings on loop_update_pipeline. minimums says how many samples it has against what the next attempt needs. scoring_capped_until, when set, is when the workspace's scoring key comes off its monthly cap: until then nothing trains and Train now answers SCORING_CAPPED with that time as resumes_at. Only compare benchmark scores whose benchmark_comparable is true; the others were measured on a different yardstick. versions[] are WINNERS ONLY -- attempts that were put live, numbered v1, v2 ... in the order they went live, each with its attempt, base_model_id and model_family -- and live_version is the one the app's serving name reaches now, with live_deployment.status saying whether its deployment answers (serving answers; starting is on its way up; stopped, paused_for_funds or deleted answers nothing and is not billed; paused_by_platform was paused because billing could not be reached and answers nothing; failed answers nothing; null when no check is recorded yet or what serves is not a deployment; live_deployment.deployment_id is the deployment to open); attempts[] is the pipeline's latest 20 training runs, winner or not (attempts_total says how many it made, and loop_list_pipeline_attempts pages through every one), each with explanation (its outcome in plain words: headline, detail, next -- read it back rather than the codes) and its outcome (became_version, not_better, inconclusive -- its comparison ran and could not decide, or compared nothing; its explanation says which and why --, not_tested -- it ended before it was tested --, stopped, failed, running, waiting_for_review), trigger (data, schedule, manual, challenger for the second model -- trained from its own base on all samples, never on this pipeline's versions -- or rebuild, a retrain from the base after a member deleted a conversation the live version learned from), decision (what was decided about it once compared, and by whom: promoted, auto_promoted, rejected, auto_rejected, expired or rolled_back; null while nothing has been -- rejected means a member chose not to publish it, where outcome says stopped, so say a member declined it rather than that it stopped; promoted that a member put it live and auto_promoted that the pipeline's going-live choice did; auto_rejected that the pipeline's going-live choice turned it down on its own; expired that nobody decided before the review deadline, so it was retired undecided -- an expired attempt whose verdict is better reads stopped and was better: never say the pipeline refused it), and train_rows and holdout_rows (the samples its set trained on and held back; null until its set was built). lineage on each version, attempt and the run in flight says what it was built on: trained_from versions (a new adapter trained on top of the live version: the pinned base with built_on's versions merged in, oldest first) or base (with from_base_reason: first_version, challenger, rebuild -- a deleted conversation when lineage.rebuild is true, or, when it is false, a live version already built on 64 versions, the most one attempt can build on -- model_cannot_build_on or chain_link_unreadable), and what it trained on -- train_rows = new_samples + carried_samples (samples of the attempts in carried_from_attempts, which did not go live, trained again) + replayed_samples (about half: as many as new_samples + carried_samples, or all of learned_before when that is fewer, drawn at random from what the versions it builds on already learned, so they are not forgotten); learned_before is what those versions had learned and was not trained again, except the replayed ones; served_as is adapter or merged (one model); learned_deleted_conversation true means it, or a version it was built on (merged into it), learned a conversation that was deleted, so it can no longer be rolled back to. The counts are null on an attempt from before the chain, or of a pipeline that does not build on its live version: say only train_rows then. Say it as 'built on v1 + v2, trained on 4,000 samples: 1,000 new, 1,000 from attempt 3 and 2,000 of what v1 + v2 learned, replayed' -- name the replayed part whenever replayed_samples is above 0, so the parts add up to train_rows. chain, when set, is the live version as the versions merged into it (versions, live one last), learned_samples and not_learned_samples (trained by attempts that did not go live, so trained again next), and rebuild when a deleted conversation the live version learned has a retrain from the base on its way; chain null means the pipeline does not build on its live version and every attempt trains from the base on all samples. Nothing in flight is a version yet. holdout is the pipeline's held-back share: percent (5 to 20), min_rows (at least that many samples are held back whatever the share), rows (how many it holds back today) and held_before (how many of rows an earlier run of the pipeline held back: they stay held back whatever became of that run and whatever the share says, so a lower share releases none of them; before the first version the first attempt needs held_before plus 1,000 to train on when that is more than the share asks for). Each attempt is judged on at most scored_rows (1,500) of the held-back samples, the same ones every attempt, so rows above that only keeps samples out of training. storage is what the pipeline's history takes up in storage (bytes, objects, where: platform, your_storage or both, and billed): every conversation, answer and scoring reason of its reports, and the training rows its attempts are built from, is kept there and never in the database; it counts toward the workspace's storage quota and is deleted with the pipeline. Only the part in the platform's storage is ever billed, as Conscious Loop storage at the storage rate with no daily minimum (each day's cost is added up and charged once it reaches a whole cent), and only when billed is true; the part in the workspace's own storage never is -- with billed false, say it is not billed. Deleting the pipeline deletes its history and stops that charge. Where the pipeline builds on its live version, the merged models its versions answer from (the live one's, and each one a rollback can still put back; an attempt that did not go live keeps its merged model, billed, until it is cleaned up within a day) are billed as model storage, like any trained model, and each version's own adapter is billed as checkpoint storage, like any training checkpoint; its reports, benchmark results and training rows are billed only as its storage says (billed). An active_run whose last_reason says it is waiting for storage is holding answers already paid for until storage can keep them; they are kept then without asking the models again. cost_up_to true on attempts[], active_run or versions[] means that cost_cents is 'up to': a comparison machine was charged in it, counted from its first boot while the platform bills from its first answer (and at the hourly cap when no price was reported), so say 'up to', never 'spent'. It is false again once billing has settled every such machine (the attempt's timeline says what billing charged each one for the time it served). A version's own new-model machine is the one that went live and is never settled, so a version whose machine was charged stays 'up to'. On attempts[] and active_run, comparison_returned_cents above 0 means a comparison machine never started (never reached running) and was not charged -- the platform bills it nothing, and the figures beside it are already net of it -- so say 'Comparison machine: not charged (it never started)' -- or, while a comparison figure above 0 remains beside it, that the one that never started was not charged and the one that ran was -- never that it was charged. An active_run.state of waiting_gpus means the attempt trained and is waiting for GPUs to run the new model and the one it is compared with side by side: each try can bill the machine that got a GPU for up to 20 minutes while it waits for the other, nothing bills between tries, it keeps what it trained, tries again after 30 minutes, then 1, 2 and 4 hours, and ends after four tries or 24 hours (its last_reason says when; a last_reason opening 'waiting for room in this workspace to compare' means the workspace is at its limit of running deployments, and stopping a deployment lets it run), and Train now tries again at once. training is the pipeline's training settings (epochs, learning_rate, lora_rank, lora_alpha, replay_percent), each null while automatic, and training_defaults what automatic means for its model (learning_rate, lora_rank, lora_alpha, replay_percent, and min_epochs to max_epochs); say 'automatic' for a null, never a guessed number. capture lists the calls sent with X-Loop-Pipeline in the last capture.days days that were not recorded, one row per tag (a call naming two tags is counted under each, so never add rows up as calls): under this pipeline's tags too_large or incomplete (the answer could not be kept), and under any tag no pipeline had no_pipeline (misspelt, or a deleted pipeline's); empty when every call was recorded. On attempts[], versions[] and active_run, sample_scoring_cents is what the judge's scoring of unreviewed samples cost before that attempt (already inside cost_cents, never on top), sample_scored and sample_picked what it scored and picked, and train_rows_by_judge how many of train_rows the judge picked rather than a person; each attempt's trained_on is what it trained on as it was then (its tags, conditions and each dataset at its revision).", { rule_id: z.string().describe("The pipeline's id, from loop_list_pipelines") }, async ({ rule_id }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(rule_id)}`)));
|
|
3410
|
+
server.tool("loop_list_pipeline_benchmarks", "The benchmarks a pipeline is measured on, in priority order (1 is the primary), then the ones it stopped using, and its promotion policy. Every attempt is replayed on each attached benchmark; the policy decides which of those measurements make it a version. Each says how it scores an answer (check: judge, the judge reading its rubric, or exact, the answer must match its right answer) and what its pinned set takes up in storage (storage: bytes, objects, where, billed), so no benchmark needs reading one by one to say either. A benchmark's pinned set is never billed: it is kept until the workspace is deleted, whatever is done with the benchmark, so billed is always false on it -- say it is not billed.", { pipeline_id: z.string() }, async ({ pipeline_id }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}/benchmarks`)));
|
|
3276
3411
|
server.tool("loop_attach_pipeline_benchmark", "Measure a pipeline on one more benchmark (from loop_list_benchmarks or loop_create_benchmark), last unless priority is given; priority 1 makes it the primary. Applies from the next attempt; older versions are not re-measured and show not_measured for it. 409 BENCHMARK_ALREADY_ATTACHED or BENCHMARK_RETIRED.", {
|
|
3277
3412
|
pipeline_id: z.string(),
|
|
3278
3413
|
benchmark_id: z.string(),
|
|
@@ -3302,7 +3437,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3302
3437
|
}, async ({ pipeline_id, a, b }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}/versions/compare`, {
|
|
3303
3438
|
params: { a: a?.toString(), b: b?.toString() },
|
|
3304
3439
|
})));
|
|
3305
|
-
server.tool("loop_compare_attempts", "Any two attempts of a pipeline side by side, put live or not -- the one to use for 'why was attempt 8 not better than 7' or a challenger against the attempt whose samples it trained on. a and b are RUN ids (attempts[].run_id from loop_get_pipeline), two different attempts of this pipeline. Each side has its outcome, decision, its lineage (what it was built on and trained on: 'attempt 3 built on v1, trained on 2,000' beside 'attempt 4 built on v1 + v2, trained on 1,000' is often the difference that matters) and held-back numbers against what it was compared with on its day, with the share each side passed (score 0.70 or more); never subtract two attempts' win rates, their opponents differ. benchmarks lists every benchmark both replayed, with delta (b minus a) only where comparable, why otherwise. shared_sample counts the held-back conversations both were scored on and how many b scored higher, lower or the same on (by the tie band: 0.02 or more above counts as higher; less than 0.02 apart is about the same): these are two absolute scores, not a reading of one answer against the other. shared_sample.a and .b are each attempt's mean and pass rate over those same conversations -- the like-for-like figures, which may be subtracted; each side's holdout numbers are over its own held-back set, which grows between attempts, so do not subtract those. When shared_sample.comparable is false the two were scored differently (another
|
|
3440
|
+
server.tool("loop_compare_attempts", "Any two attempts of a pipeline side by side, put live or not -- the one to use for 'why was attempt 8 not better than 7' or a challenger against the attempt whose samples it trained on. a and b are RUN ids (attempts[].run_id from loop_get_pipeline), two different attempts of this pipeline. Each side has its outcome, decision, its lineage (what it was built on and trained on: 'attempt 3 built on v1, trained on 2,000' beside 'attempt 4 built on v1 + v2, trained on 1,000' is often the difference that matters) and held-back numbers against what it was compared with on its day, with the share each side passed (score 0.70 or more); never subtract two attempts' win rates, their opponents differ. benchmarks lists every benchmark both replayed, with delta (b minus a) only where comparable, why otherwise. shared_sample counts the held-back conversations both were scored on and how many b scored higher, lower or the same on (by the tie band: 0.02 or more above counts as higher; less than 0.02 apart is about the same): these are two absolute scores, not a reading of one answer against the other. shared_sample.a and .b are each attempt's mean and pass rate over those same conversations -- the like-for-like figures, which may be subtracted; each side's holdout numbers are over its own held-back set, which grows between attempts, so do not subtract those. When shared_sample.comparable is false the two were scored differently (another judge, holdout.judge_model, or another rubric) or how one was scored was not recorded (an older attempt that froze no rubric is never taken to match): say why in its words, and never read a direction off them. A conversation whose right answer changed between the two attempts has direction null and says why (answer_changed counts them); so does one where it is not known whether both were scored against the same right answer, because one was compared before that was recorded (answer_unknown counts them; never read those as unchanged or as changed). items is one page of them, numbers and ids only: read an answer with loop_list_evaluation_items on that side's holdout.evaluation_id. filter narrows items and total to one direction, and is refused when the two are not comparable.", {
|
|
3306
3441
|
pipeline_id: z.string(),
|
|
3307
3442
|
a: z.string().describe("The first attempt's run id"),
|
|
3308
3443
|
b: z.string().describe("The second attempt's run id; directions say b against a"),
|
|
@@ -3357,25 +3492,25 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3357
3492
|
}, async ({ evaluation_id, winner, candidate_passed, incumbent_passed, orders, has_reference, status, sort, q, limit, offset }) => json(await client.api(`/api/loop/evaluations/${encodeURIComponent(evaluation_id)}/items`, {
|
|
3358
3493
|
params: { winner, candidate_passed: candidate_passed?.toString(), incumbent_passed: incumbent_passed?.toString(), orders, has_reference: has_reference?.toString(), status, sort, q, limit: limit?.toString(), offset: offset?.toString() },
|
|
3359
3494
|
})));
|
|
3360
|
-
server.tool("loop_create_benchmark", "Pin a fixed set of 20 to 200 conversations (source.kind 'traces' with trace_ids) as a benchmark: the same questions, each with the right answer a person gave it, scored the same way on every attempt of every pipeline it is attached to, so 'is it getting better' has a yardstick that does not move. check says how, and is frozen: 'judge' (the default:
|
|
3495
|
+
server.tool("loop_create_benchmark", "Pin a fixed set of 20 to 200 conversations (source.kind 'traces' with trace_ids) as a benchmark: the same questions, each with the right answer a person gave it, scored the same way on every attempt of every pipeline it is attached to, so 'is it getting better' has a yardstick that does not move. check says how, and is frozen: 'judge' (the default: the judge reads both answers against the right answer) or 'exact' (the answer or the tool call must match the right answer, and no model is asked -- for tool calls, numbers and labels; a written answer rarely matches word for word; a value hidden when the conversation was captured, such as an email or a long number kept as [redacted:email], cannot be checked, so an answer that matches everywhere else still scores 0 and says so). An exact benchmark needs a right answer on every conversation: 422 BENCHMARK_ROW_HAS_NO_ANSWER names the ones without and nothing is saved -- mark their answers good or correct them first, leave them out, or use 'judge'. It is refused rather than trimmed when a conversation has nothing to ask a model, or is too large to keep in storage: 422 BENCHMARK_ROW_UNUSABLE names it, and nothing is saved -- leave it out. 409 CONFLICT when the conversations could not be stored just then: nothing was saved; try again in a minute. Attach it with loop_attach_pipeline_benchmark.", {
|
|
3361
3496
|
name: z.string(),
|
|
3362
3497
|
source: z.object({
|
|
3363
3498
|
kind: z.enum(["traces"]).describe("Always 'traces': a benchmark pins the conversations named in trace_ids"),
|
|
3364
3499
|
trace_ids: z.array(z.string()).min(1).describe("The conversations to pin, 20 to 200 (from loop_list_traces, or the trace_ids loop_import_rows returns for a file)"),
|
|
3365
3500
|
}),
|
|
3366
|
-
check: z.enum(["judge", "exact"]).optional().describe("How answers are scored: judge (the default:
|
|
3501
|
+
check: z.enum(["judge", "exact"]).optional().describe("How answers are scored: judge (the default: the judge reads both answers against the right answer) or exact (the answer or the tool call must match the right answer; no model is asked)"),
|
|
3367
3502
|
}, async ({ name, source, check }) => json(await client.api("/api/loop/benchmarks", {
|
|
3368
3503
|
method: "POST",
|
|
3369
3504
|
body: { name, source, check },
|
|
3370
3505
|
})));
|
|
3371
|
-
server.tool("loop_list_benchmarks", "The workspace's benchmarks, newest first. status 'active' hides the retired ones. Each carries check (how it is scored: judge,
|
|
3506
|
+
server.tool("loop_list_benchmarks", "The workspace's benchmarks, newest first. status 'active' hides the retired ones. Each carries check (how it is scored: judge, scored by the judge, or exact, the exact check with no model) and storage -- what its pinned set takes up in storage (bytes, objects, where: platform, your_storage or both, and billed); it counts toward the workspace's storage quota and is deleted with the workspace. A benchmark's pinned set is never billed: it is kept until the workspace is deleted, whatever is done with the benchmark, so billed is always false on it -- say it is not billed.", {
|
|
3372
3507
|
status: z.enum(["active", "retired"]).optional(),
|
|
3373
3508
|
limit: z.number().int().min(1).max(200).optional(),
|
|
3374
3509
|
offset: z.number().int().min(0).optional(),
|
|
3375
3510
|
}, async ({ status, limit, offset }) => json(await client.api("/api/loop/benchmarks", {
|
|
3376
3511
|
params: { status, limit: limit?.toString(), offset: offset?.toString() },
|
|
3377
3512
|
})));
|
|
3378
|
-
server.tool("loop_get_benchmark", "One benchmark: how it is scored (check: judge,
|
|
3513
|
+
server.tool("loop_get_benchmark", "One benchmark: how it is scored (check: judge, scored by the judge, or exact, the exact check with no model -- then judge_model is empty), its frozen judge and scoring rule, the fingerprint of its set, and storage -- what its pinned set takes up in storage (bytes, objects, where: platform, your_storage or both, and billed). The set is kept in storage only; it counts toward the workspace's storage quota and is deleted with the workspace. A benchmark's pinned set is never billed: it is kept until the workspace is deleted, whatever is done with the benchmark, so billed is always false on it -- say it is not billed.", { benchmark_id: z.string() }, async ({ benchmark_id }) => json(await client.api(`/api/loop/benchmarks/${encodeURIComponent(benchmark_id)}`)));
|
|
3379
3514
|
server.tool("loop_list_benchmark_items", "The conversations a benchmark pinned, each with the right answer pinned with it (reference: answer, kind and tool_calls -- an answer marked good that only calls a tool has an empty answer and its calls; null when nobody gave one). A conversation deleted since keeps its pinned copy; its source link reads null.", {
|
|
3380
3515
|
benchmark_id: z.string(),
|
|
3381
3516
|
limit: z.number().int().min(1).max(200).optional().describe("50 when omitted, at most 200"),
|