runbios-mcp 0.4.0 → 0.5.0-dev.338
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 +40 -0
- package/README.md +63 -7
- package/dist/api-client.d.ts +43 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +150 -1
- package/dist/api-client.js.map +1 -1
- package/dist/assistant/tool-runtime.d.ts.map +1 -1
- package/dist/assistant/tool-runtime.js +7 -2
- package/dist/assistant/tool-runtime.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/http/assistant.js +2 -2
- 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 +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +289 -139
- 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
|
@@ -7,7 +7,7 @@ import { buildGPUOptionsParams, buildTrainingCreateBody, TRAINING_ADAPTERS, TRAI
|
|
|
7
7
|
import { buildDeploymentBody, buildDeploymentGPUOptionsParams, buildDeploymentPolicyBody, DEPLOYMENT_GPU_TIERS, DEPLOYMENT_SOURCE_TYPES, } from "./deployment-contract.js";
|
|
8
8
|
import { buildInferenceBody } from "./inference-contract.js";
|
|
9
9
|
import { redactBuildIdentity, redactBuildIdentityDeep, redactFreeTextFields, sanitizeArchitectureRegistry, stripBuildIdentity, } from "./redaction.js";
|
|
10
|
-
import { gpuRejectionCodeFor, gpuRejectionInstruction, gpuRejectionStatusFor, isPermanentGpuCode, } from "./api-client.js";
|
|
10
|
+
import { gpuRejectionCodeFor, gpuRejectionInstruction, gpuRejectionStatusFor, ApiHttpError, InferenceHttpError, isPermanentGpuCode, } from "./api-client.js";
|
|
11
11
|
/**
|
|
12
12
|
* Job/deployment fields whose upstream value is RAW text rather than a curated
|
|
13
13
|
* sentence. `stop_last_error` is the proven one: the training stop saga stores
|
|
@@ -711,11 +711,11 @@ export function loopSwitchedOffDescription(name, description) {
|
|
|
711
711
|
const note = loopSwitchedOffNote(name);
|
|
712
712
|
switch (name) {
|
|
713
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
|
|
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 training in progress (active_run). ${note} To stop paying: loop_update_pipeline with enabled false stops new trainings, loop_cancel_training_run stops a training in progress (active_run.run_id), and loop_delete_pipeline stops its live version and deletes it for good.`;
|
|
715
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
|
|
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 training in progress (active_run, whose run_id loop_cancel_training_run takes). ${note}`;
|
|
717
717
|
case "loop_update_pipeline":
|
|
718
|
-
return `Switch a Conscious Loop pipeline off, so it starts no new
|
|
718
|
+
return `Switch a Conscious Loop pipeline off, so it starts no new training. A training already in progress 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
719
|
default:
|
|
720
720
|
return `${description} ${note}`;
|
|
721
721
|
}
|
|
@@ -877,40 +877,56 @@ launchGates = resolveLaunchGates(), allowedTools) {
|
|
|
877
877
|
? launchGates.loopSwitchedOff === true
|
|
878
878
|
? `## Conscious Loop: switched off
|
|
879
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,
|
|
880
|
+
- loop_list_pipelines / loop_get_pipeline - Find the pipelines and see what each still runs (its live version's deployment, a training in progress).
|
|
881
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
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
|
|
883
|
+
- loop_cancel_training_run - Cancel a training still running.
|
|
884
884
|
|
|
885
885
|
${aliasGuideSection}`
|
|
886
886
|
: aliasGuideSection
|
|
887
887
|
: `## Conscious Loop MCP Tools
|
|
888
|
-
The
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
is
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
888
|
+
The Conscious Loop keeps improving a model from an app's conversations and
|
|
889
|
+
the user's feedback. Words to use with the user: a PIPELINE is one use case;
|
|
890
|
+
its pipeline ID (own_tag, e.g. support-replies) is made from its name when it
|
|
891
|
+
is created and never changes. A SAMPLE is one saved conversation; its sample
|
|
892
|
+
ID comes back in the reply's X-Loop-Sample-Id header and feedback names it. A
|
|
893
|
+
TRAINING is one numbered try at a better model (#1, #2 ...; the API's names
|
|
894
|
+
say 'attempt', and its id is run_id). A
|
|
895
|
+
VERSION is a training that went live (v1, v2 ... with no limit), and the LIVE
|
|
896
|
+
VERSION is the version the app gets answers from right now: it answers to the
|
|
897
|
+
model name loop-<pipeline ID>, and each new version takes that name over.
|
|
898
|
+
Every training is tested before it can go live: the AUTOMATIC TEST (questions
|
|
899
|
+
from the pipeline's own samples it never trained on) and any BENCHMARKS (the
|
|
900
|
+
user's fixed sets of questions). The JUDGE is an AI that compares each answer
|
|
901
|
+
with the right answer.
|
|
902
|
+
|
|
903
|
+
WHAT IS SAVED is a consent decision, not a default, and these tools cannot
|
|
904
|
+
bypass it. A call that carries the pipeline ID in the X-Loop-Pipeline header
|
|
905
|
+
(loop_chat does) is saved to that pipeline: the header is the consent for that
|
|
906
|
+
one call. Creating a pipeline switches on recording for the source named by its
|
|
907
|
+
pipeline ID, so loop_capture_trace to it is saved too, until the pipeline is
|
|
908
|
+
deleted. Every other source records nothing until loop_set_capture switches it
|
|
909
|
+
on.
|
|
910
|
+
|
|
911
|
+
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). The source named by a pipeline's ID is already on; any other source records nothing until this is done.
|
|
912
|
+
2. loop_chat / loop_capture_trace / loop_import_rows - Send conversations. loop_chat asks a model with the pipeline ID header and saves the conversation as a sample in one call (a real, billed model call): it returns the answer and its sample_id. Before the pipeline's first version pass model, the model the app uses now; after it, leave model out and the live version answers. loop_capture_trace records one exchange that already happened (what the model was asked, what it answered, any tool calls), whoever served the model — Run BiOS, another provider, or the user's own servers: send it with the pipeline ID (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: a file from another provider, a spreadsheet of past answers, preference pairs. Imported conversations arrive NEEDING REVIEW, never as finished training data.
|
|
913
|
+
3. loop_list_traces / loop_get_trace / loop_delete_trace - Read what has been saved; pipeline=<id> lists one pipeline's samples, each with its state there (kept aside for the automatic test, learned, trained on by a training that did not become a version, 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 training'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 original model without it (the answer's rebuilds names each). Credentials and personal details are stripped before storage, never after.
|
|
914
|
+
4. loop_add_signal / loop_list_signals - Feedback on a sample ID: good (learned as it is), bad (wrong: not learned), or corrected with the right answer (the right answer is learned instead). Feedback is added, never replaced: a second one sits beside the first, and the newest from a person decides. A correction is the most valuable feedback there is. Training that learns from rankings or rewards is not available yet, so bad with no right answer teaches nothing.
|
|
899
915
|
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.
|
|
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
|
|
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
|
|
902
|
-
8. loop_list_models - The
|
|
903
|
-
9. loop_create_pipeline - One use case: a name, a model (optionally a
|
|
904
|
-
10. loop_list_pipelines / loop_get_pipeline / loop_list_pipeline_attempts / loop_update_pipeline / loop_delete_pipeline - Each pipeline's versions (
|
|
916
|
+
6. loop_list_datasets / loop_get_dataset / loop_preview_dataset / loop_create_dataset / loop_update_dataset / loop_delete_dataset / loop_export_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 tags (its pipeline ID among them) 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 training. Each dataset says who saved it and which trainings learned from it; loop_export_dataset reads its samples back in the import's format, with their feedback.
|
|
917
|
+
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 trainings and the feedback moved over time.
|
|
918
|
+
8. loop_list_models - The models a pipeline can train, and the platform's fixed sample minimums (floors). The only list a pipeline's model may come from; unavailable says, in one plain sentence per model, why one is not offered.
|
|
919
|
+
9. loop_create_pipeline - One use case: a name, a model (optionally a second model, a different one), its tags, when to train (optionally a schedule; the sample minimums are fixed by the platform) and how a new version goes live. It switches on the source named by its pipeline ID, so an app can send samples at once. SPEND CONSENT: saving it authorises trainings 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 trainings use and, where the platform scores samples, the judge's scoring of unreviewed samples, shown on the training it was for. Say so and wait for a yes.
|
|
920
|
+
10. loop_list_pipelines / loop_get_pipeline / loop_list_pipeline_attempts / loop_update_pipeline / loop_delete_pipeline - Each pipeline's versions (only trainings that went live: v1, v2 ...: version version numbers count up; nothing untested goes live any more, and each version's tested says whether it was), its trainings 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
921
|
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 -
|
|
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
|
|
908
|
-
14. loop_list_training_runs / loop_get_training_run - Every
|
|
909
|
-
15. loop_get_evaluation / loop_list_evaluation_items - The
|
|
910
|
-
16. loop_promote_training_run / loop_reject_training_run / loop_rollback_training_run / loop_cancel_training_run - The decision.
|
|
911
|
-
17. set_inference_alias / list_inference_aliases / delete_inference_alias - The re-pointable public handles a
|
|
912
|
-
|
|
913
|
-
END TO END:
|
|
922
|
+
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 / loop_benchmark_questions - Benchmarks: fixed sets of the user's questions with right answers that every training is tested on, so "is it getting better" has a yardstick that does not move, and each benchmark run's report: its scores, how many questions each model got right (score 0.70 or more), and every question behind them. loop_benchmark_questions shows each question with how a pipeline's recent trainings did on it.
|
|
923
|
+
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 trainings side by side, live or not, question by question on the automatic-test questions both were scored on.
|
|
924
|
+
14. loop_list_training_runs / loop_get_training_run / loop_get_training_curve - Every training of every pipeline (result filters by what became of it; each item's 'attempt' is the training as its pipeline's own list says it): its timeline, why it went live or did not (its going-live record, and verdict_checks: every bar its test 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. loop_get_training_curve shows how one went while it trained, step by step, and the step it kept.
|
|
925
|
+
15. loop_get_evaluation / loop_list_evaluation_items - The automatic test behind a result: how many questions each model got right (score 0.70 or more), how often the new answer scored higher, the per-dimension means, and the questions themselves, with the judge's reasons. Read the warnings out, not just the result.
|
|
926
|
+
16. loop_promote_training_run / loop_reject_training_run / loop_rollback_training_run / loop_cancel_training_run / loop_retest_training_run - The decision. Putting a training live makes it the live version and changes what real customers get; going back to the version before is possible for 30 days. A training that was never tested (tested false) can never go live (NOT_TESTED); loop_retest_training_run tests it again.
|
|
927
|
+
17. set_inference_alias / list_inference_aliases / delete_inference_alias - The re-pointable public handles a version going live writes: a name customers call that can be moved to different weights without them changing anything.
|
|
928
|
+
|
|
929
|
+
END TO END: create ONE pipeline per use case; send the app's conversations with its pipeline ID (loop_chat, or the header in the app's own calls; or capture/import them), and say whether each answer was good, bad or corrected. 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 starts a training, tests it against the live version (before v1: the original model) on the automatic test and on the pipeline's benchmarks, and puts it live by the pipeline's going-live choice: 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 choice but manual, where it waits for a member like any training; so a version need not have done better than the one before it. Everything before the pipeline costs nothing but model calls; saving a pipeline is where training 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.`;
|
|
914
930
|
const server = {
|
|
915
931
|
tool(name, description, paramsSchema, cb) {
|
|
916
932
|
if (allowedTools && !allowedTools.has(name))
|
|
@@ -3029,13 +3045,16 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3029
3045
|
/* preset on purpose: what they return is raw prompts and completions, not */
|
|
3030
3046
|
/* catalog metadata, so a key holds them only by an explicit grant. */
|
|
3031
3047
|
/* */
|
|
3032
|
-
/*
|
|
3033
|
-
/* or
|
|
3034
|
-
/*
|
|
3035
|
-
/*
|
|
3036
|
-
/*
|
|
3037
|
-
/*
|
|
3038
|
-
|
|
3048
|
+
/* WHAT IS SAVED. A call carrying a pipeline ID in X-Loop-Pipeline (loop_chat,*/
|
|
3049
|
+
/* or the SDKs' loop option) is saved to that pipeline: the header is the */
|
|
3050
|
+
/* consent for that one call, whatever the recording switches say. Anything */
|
|
3051
|
+
/* else is recorded only from a source that is switched on: by */
|
|
3052
|
+
/* loop_set_capture, or -- for the source named by a pipeline's own tag -- */
|
|
3053
|
+
/* by creating the pipeline. An agent calling loop_capture_trace against a */
|
|
3054
|
+
/* source nobody switched on gets captured:false back, not an error and not */
|
|
3055
|
+
/* a stored conversation. */
|
|
3056
|
+
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
3057
|
+
server.tool("loop_set_capture", "Turn recording on or off for one source, and choose how long recordings are kept. THIS IS THE CONSENT DECISION for everything a source sends: until it is made, nothing that source sends is recorded. The one exception is a call that carries a pipeline ID in the X-Loop-Pipeline header (loop_chat, or the SDKs' loop option): the header is the consent for that one call, and it is saved to that pipeline whatever this switch says. The source is a deployment id for models served on Run BiOS, or any label you choose for traffic served elsewhere (another provider, your own servers, an agent framework). The source named by a pipeline's ID (own_tag) is switched on when the pipeline is created, and stamps that pipeline ID on what it records; deleting the pipeline switches it off. Recordings are deleted automatically once the retention window passes.", {
|
|
3039
3058
|
source: z.string().describe("The source to record: a Run BiOS deployment id, or a label you choose for traffic served elsewhere (e.g. 'my-support-agent')"),
|
|
3040
3059
|
enabled: z.boolean().describe("true starts recording new conversations from this source; false stops. Turning it off never deletes what is already stored."),
|
|
3041
3060
|
retention_days: z.number().int().optional().describe("How many days recordings are kept before automatic deletion (1-3650, default 30)"),
|
|
@@ -3049,7 +3068,74 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3049
3068
|
return json(data);
|
|
3050
3069
|
});
|
|
3051
3070
|
server.tool("loop_list_capture_settings", "Which sources are recorded: for each one whether capture is on, how long recordings are kept, the sample rate and the tags stamped on what it records. Pass source to read one; a source nobody configured reads back as disabled, because 'we are not recording this' is the honest answer.", { source: z.string().optional().describe("One source to read. Omit for every configured source.") }, async ({ source }) => json(await client.api(source ? `/api/loop/configs/${encodeURIComponent(source)}` : "/api/loop/configs")));
|
|
3052
|
-
|
|
3071
|
+
/**
|
|
3072
|
+
* A pipeline ID as the platform reads it: the gateways trim and lowercase the
|
|
3073
|
+
* X-Loop-Pipeline header and keep only this shape, and the pipeline's own model
|
|
3074
|
+
* is `loop-` and the ID exactly, matched case for case. So the ID is lowercased
|
|
3075
|
+
* here before it names the header and the model, and anything else is refused
|
|
3076
|
+
* before any call: sent, it would be dropped and nothing saved.
|
|
3077
|
+
*/
|
|
3078
|
+
const LOOP_PIPELINE_ID = /^[a-z0-9][a-z0-9._-]{0,62}$/;
|
|
3079
|
+
/** The text of a chat answer: a string content, or the text parts of a list of parts. */
|
|
3080
|
+
function loopChatAnswerText(message) {
|
|
3081
|
+
const content = message?.content;
|
|
3082
|
+
if (typeof content === "string")
|
|
3083
|
+
return content;
|
|
3084
|
+
if (Array.isArray(content)) {
|
|
3085
|
+
return content
|
|
3086
|
+
.map((part) => (part && typeof part === "object" && typeof part.text === "string"
|
|
3087
|
+
? part.text : ""))
|
|
3088
|
+
.join("");
|
|
3089
|
+
}
|
|
3090
|
+
return "";
|
|
3091
|
+
}
|
|
3092
|
+
server.tool("loop_chat", "Ask a model and save the conversation as a sample of a pipeline, in one call. It calls /v1/chat/completions with the pipeline ID in the X-Loop-Pipeline header, so the conversation is saved to that pipeline even where recording is off: the header is the consent for that one call. It is a real model call, billed like any other, so say so before calling it for the user. It returns answer (the model's text; empty when it only called a tool, and tool_calls then says what it called), sample_id (the ID feedback names; null when none came back, because the platform was not recording calls just then, so nothing was saved and there is nothing to give feedback on) and the model it asked. A sample ID does not prove the pipeline ID is right: if no pipeline has that ID (misspelt, or the pipeline was deleted), the conversation is not saved under it. If recording is switched on for the model that answered, the call is still recorded the way that model records every call, and feedback on it is then accepted; when nothing records it, feedback on it is refused, first as not recorded yet and then as not found. loop_stats counts such calls under capture (reason no_pipeline); check the pipeline ID with loop_list_pipelines. model defaults to the pipeline's own model, loop-<pipeline ID>, which answers with the live version (the version the app gets answers from right now). Before the pipeline's first version nothing answers to that name: pass model, the model the app uses now; without it the call fails with a plain sentence saying the pipeline has no live version right now, and no model answered (the same after its only live version was taken back). Then send feedback on sample_id with loop_add_signal: good, bad, or corrected with the right answer. Feedback sent the instant the answer arrives can reach the platform before the conversation is saved (409 SAMPLE_NOT_YET_RECORDED); loop_add_signal then waits and sends it again by itself.", {
|
|
3093
|
+
pipeline: z.string().min(1).max(63).describe("The pipeline ID: its own_tag, e.g. 'support-replies' (loop_list_pipelines shows it, and so does the pipeline's Connect page). It was made from the pipeline's name when it was created and never changes. One pipeline ID, no commas: lowercase letters, digits, dot, dash or underscore (capitals are taken as lowercase, as the platform does)."),
|
|
3094
|
+
messages: z.array(z.record(z.string(), z.unknown())).min(1).describe("The conversation to send, in chat-completions shape: [{\"role\": \"user\", \"content\": \"...\"}]"),
|
|
3095
|
+
model: z.string().min(1).optional().describe("The model to ask: a serverless model id or a deployment name. Leave it out to ask the pipeline's own model, loop-<pipeline ID>, which answers with the live version. Required before the pipeline's first version: pass the model the app uses now."),
|
|
3096
|
+
max_tokens: z.number().int().min(1).max(CONTEXT_TOOL_MAX).optional().describe(MAX_TOKENS_POLICY_TEXT),
|
|
3097
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
3098
|
+
request_id: z.string().min(1).max(255).optional(),
|
|
3099
|
+
}, async ({ pipeline, messages, model, max_tokens, temperature, request_id }, extra) => {
|
|
3100
|
+
const id = pipeline.trim().toLowerCase();
|
|
3101
|
+
if (!LOOP_PIPELINE_ID.test(id)) {
|
|
3102
|
+
throw new Error(`pipeline must be one pipeline ID, e.g. "support-replies" (got ${JSON.stringify(pipeline)})`);
|
|
3103
|
+
}
|
|
3104
|
+
const explicit = typeof model === "string" && model.trim() !== "";
|
|
3105
|
+
const asked = explicit ? model.trim() : `loop-${id}`;
|
|
3106
|
+
let result;
|
|
3107
|
+
try {
|
|
3108
|
+
result = await client.loopChatApi({ model: asked, messages, max_tokens, temperature }, id, request_id, extra.signal);
|
|
3109
|
+
}
|
|
3110
|
+
catch (error) {
|
|
3111
|
+
if (!explicit && error instanceof InferenceHttpError && error.status === 404) {
|
|
3112
|
+
throw new Error(`Pipeline "${id}" has no live version right now, so nothing answers to "${asked}". Until a version is live, `
|
|
3113
|
+
+ "pass model: the model the app uses now (the conversation still goes to the pipeline). "
|
|
3114
|
+
+ "If the pipeline has a live version, check the pipeline ID.");
|
|
3115
|
+
}
|
|
3116
|
+
throw error;
|
|
3117
|
+
}
|
|
3118
|
+
const { completion, sampleId } = result;
|
|
3119
|
+
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
3120
|
+
const first = choices[0];
|
|
3121
|
+
const message = first && typeof first.message === "object" && first.message !== null
|
|
3122
|
+
? first.message : undefined;
|
|
3123
|
+
const toolCalls = Array.isArray(message?.tool_calls) && message.tool_calls.length > 0 ? message.tool_calls : undefined;
|
|
3124
|
+
return json({
|
|
3125
|
+
answer: loopChatAnswerText(message),
|
|
3126
|
+
sample_id: sampleId,
|
|
3127
|
+
model: asked,
|
|
3128
|
+
pipeline: id,
|
|
3129
|
+
...(toolCalls ? { tool_calls: toolCalls } : {}),
|
|
3130
|
+
finish_reason: first?.finish_reason ?? null,
|
|
3131
|
+
usage: completion.usage ?? null,
|
|
3132
|
+
id: completion.id ?? null,
|
|
3133
|
+
next: sampleId
|
|
3134
|
+
? "Send feedback on sample_id with loop_add_signal: good, bad, or corrected with the right answer."
|
|
3135
|
+
: "No sample ID came back, so this conversation was not saved as a sample and cannot take feedback.",
|
|
3136
|
+
});
|
|
3137
|
+
});
|
|
3138
|
+
server.tool("loop_capture_trace", "Record one exchange — what the model was asked and what it answered — so it can be reviewed and later trained on. Works whoever served the model. To feed a pipeline, pass its pipeline ID (own_tag) as the source: creating the pipeline switched that source on, and it stamps the pipeline ID on everything it records. labels add tags of your own, so the conversation feeds those pipelines too. Pass tool_calls and tools when the turn invoked a function: without them a tool-using exchange trains the model to reply in prose exactly where it should have called something. The returned trace_id is the sample ID, Run BiOS's own — give feedback on it with loop_add_signal; if you pass request_id, sending the same one again returns the same sample instead of storing a duplicate. If the source is not switched on this returns captured:false and stores nothing. Already know how the answer did? Pass feedback and it is written with the exchange, so no loop_add_signal call is needed. To ask a model and save the conversation in one call, use loop_chat instead.", {
|
|
3053
3139
|
source: z.string().describe("The source being recorded: a pipeline's own_tag, or a source switched on with loop_set_capture"),
|
|
3054
3140
|
model: z.string().describe("The model that produced the answer, e.g. 'gpt-4o' or a Run BiOS model id"),
|
|
3055
3141
|
messages: z.array(z.object({
|
|
@@ -3069,9 +3155,9 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3069
3155
|
})).optional().describe("What the answering turn invoked, if anything"),
|
|
3070
3156
|
tools: z.any().optional().describe("The tool schema the model was offered. Training tool use without it teaches the model to invent function names."),
|
|
3071
3157
|
conversation_id: z.string().optional().describe("Ties the turns of one conversation together"),
|
|
3072
|
-
request_id: z.string().optional().describe("Your own idempotency handle —
|
|
3158
|
+
request_id: z.string().optional().describe("Your own idempotency handle — sending the same one again will not create a second sample"),
|
|
3073
3159
|
latency_ms: z.number().int().optional(),
|
|
3074
|
-
labels: z.array(z.string()).max(32).optional().describe("Tags for this conversation
|
|
3160
|
+
labels: z.array(z.string()).max(32).optional().describe("Tags for this conversation, beside the ones its source stamps. A pipeline trains on every sample carrying any of its tags. At most 32 in all."),
|
|
3075
3161
|
feedback: z.object({
|
|
3076
3162
|
verdict: z.enum(["good", "bad", "corrected"]),
|
|
3077
3163
|
correction: z.string().optional().describe("Required with corrected, and read only then: the answer that should have been given"),
|
|
@@ -3087,13 +3173,13 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3087
3173
|
});
|
|
3088
3174
|
return json(data);
|
|
3089
3175
|
});
|
|
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
|
|
3176
|
+
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 good or bad feedback; 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. Feedback 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 FEEDBACK 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 feedback that failed to write is written 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.", {
|
|
3091
3177
|
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."),
|
|
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
|
|
3178
|
+
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 feedback."),
|
|
3093
3179
|
model: z.string().optional().describe("Which model produced these answers, when the rows do not say per-row"),
|
|
3094
3180
|
labels: z.array(z.string()).optional().describe("Applied to every row. Importing a dump is usually the moment somebody knows what all of it is, and labels are what make a slice of it selectable later."),
|
|
3095
3181
|
attributes: z.record(z.string(), z.string()).optional().describe("Key/value facts applied to every row, e.g. {\"category\": \"billing\"}"),
|
|
3096
|
-
import_id: z.string().optional().describe("Repeat the import_id a previous call reported to make this call a RETRY of it: rows that already arrived come back under already_present rather than being stored again, and
|
|
3182
|
+
import_id: z.string().optional().describe("Repeat the import_id a previous call reported to make this call a RETRY of it: rows that already arrived come back under already_present rather than being stored again, and feedback that failed to write is written again. Leave it out for a fresh import or for the next page of one."),
|
|
3097
3183
|
row_offset: z.number().int().min(0).optional().describe("Where this page starts in the file, counting from 0. Send it when one import is split across several calls under one import_id, so two pages are not matched against each other row for row."),
|
|
3098
3184
|
row_ids: z.array(z.string()).optional().describe("Your own id per row, in the same order as rows: a ticket number, a conversation id, whatever the export already carries. The strongest identity and the one to use for a file big enough to page — chunking, ordering and subsets all stop mattering. One per row or none at all, and no two rows may share one."),
|
|
3099
3185
|
}, async ({ source, rows, model, labels, attributes, import_id, row_offset, row_ids }) => {
|
|
@@ -3103,16 +3189,16 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3103
3189
|
});
|
|
3104
3190
|
return json(data);
|
|
3105
3191
|
});
|
|
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
|
|
3192
|
+
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 sample the judge scored carries judge {score, picked}. A bad sample is never trained on. With pipeline, each sample carries state, what happens to it in that pipeline: held_back (kept aside for the automatic test, never trained on), learned (by version_no), trained (by training number attempt_no, which did not become a version), waiting (for the next training) or left_out, with reason, the sentence to read back. dataset lists what one saved dataset selects (not with pipeline); the 'attempt' filter (a training's run id) with split train or held_back lists the samples that training learned from or was tested on. 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.", {
|
|
3107
3193
|
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."),
|
|
3108
3194
|
source: z.string().optional().describe("Only conversations from this source"),
|
|
3109
3195
|
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."),
|
|
3110
3196
|
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."),
|
|
3111
3197
|
conversation_id: z.string().optional(),
|
|
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
|
|
3198
|
+
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 sample then carries state."),
|
|
3113
3199
|
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
|
|
3115
|
-
split: z.enum(["train", "held_back"]).optional().describe("With
|
|
3200
|
+
attempt: z.string().optional().describe("Only the samples of one training (its run id, from loop_list_pipeline_attempts); pass split too."),
|
|
3201
|
+
split: z.enum(["train", "held_back"]).optional().describe("With a training's run id: train (what it learned from) or held_back (its automatic test: what it was tested on)."),
|
|
3116
3202
|
label: z.string().optional().describe("Only conversations carrying this bare tag. A tag with children matches them too."),
|
|
3117
3203
|
attributes: z.record(z.string(), z.string()).optional().describe("Only conversations matching every one of these dimensions, e.g. {\"category\":\"billing\",\"language\":\"es\"}"),
|
|
3118
3204
|
unlabelled: z.boolean().optional().describe("Only the conversations nobody has described yet — the pile worth looking at before deciding what the dimensions should be"),
|
|
@@ -3154,31 +3240,66 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3154
3240
|
});
|
|
3155
3241
|
return json(data);
|
|
3156
3242
|
});
|
|
3157
|
-
server.tool("loop_get_trace", "Read one recorded conversation in full, with
|
|
3243
|
+
server.tool("loop_get_trace", "Read one recorded conversation in full, with all the feedback and every 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, with 'verdict' set to '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 sample ID: trace_id from loop_capture_trace or loop_list_traces, or sample_id from loop_chat") }, async ({ trace_id }) => {
|
|
3158
3244
|
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}`);
|
|
3159
3245
|
return json(data);
|
|
3160
3246
|
});
|
|
3161
|
-
server.tool("loop_delete_trace", "Delete one recorded conversation, with its feedback and tags, for good. It stops counting toward every pipeline that selected it. Where the live version of a pipeline learned from it, that pipeline rebuilds its model from the
|
|
3162
|
-
|
|
3247
|
+
server.tool("loop_delete_trace", "Delete one recorded conversation, with its feedback and tags, for good. It stops counting toward every pipeline that selected it. Where the live version of a pipeline learned from it, that pipeline rebuilds its model from the original model on every remaining sample, without it: the answer's rebuilds lists each such pipeline ({pipeline_id, pipeline_name}; empty when none) -- read them back, because a rebuild is a new training paid from the wallet. A switched-off pipeline is listed too: its rebuild starts when it is switched back on. Until the rebuild goes live the live version still holds what it learned from it; once it does, no version that learned it can be rolled back to, and their stored weights are deleted. Any other version already trained on it keeps what it learned, and a conversation that expires under the retention setting rebuilds nothing. Confirm with the user first: this cannot be undone.", { trace_id: z.string() }, async ({ trace_id }) => json(await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}`, { method: "DELETE" })));
|
|
3248
|
+
/**
|
|
3249
|
+
* Feedback that names a sample the gateway issued moments ago can arrive before
|
|
3250
|
+
* the sample is saved, and is refused 409 SAMPLE_NOT_YET_RECORDED with nothing
|
|
3251
|
+
* stored. The SDKs' signal() sends it again; so does loop_add_signal, the same
|
|
3252
|
+
* way: wait Retry-After (2 seconds when none is given, at most 30) and try
|
|
3253
|
+
* again, at most this many tries in all. An assistant has no way to wait on
|
|
3254
|
+
* its own, so without this the 409 would end the feedback.
|
|
3255
|
+
*/
|
|
3256
|
+
const LOOP_FEEDBACK_TRIES = 6;
|
|
3257
|
+
const LOOP_FEEDBACK_RETRY_DEFAULT_SECONDS = 2;
|
|
3258
|
+
const LOOP_FEEDBACK_RETRY_MAX_SECONDS = 30;
|
|
3259
|
+
async function sendLoopFeedback(send, signal) {
|
|
3260
|
+
for (let tries = 1;; tries++) {
|
|
3261
|
+
try {
|
|
3262
|
+
return await send();
|
|
3263
|
+
}
|
|
3264
|
+
catch (error) {
|
|
3265
|
+
const onItsWay = error instanceof ApiHttpError && error.status === 409 && error.code === "SAMPLE_NOT_YET_RECORDED";
|
|
3266
|
+
if (!onItsWay || tries >= LOOP_FEEDBACK_TRIES || signal?.aborted)
|
|
3267
|
+
throw error;
|
|
3268
|
+
const seconds = Math.min(error.retryAfterSeconds ?? LOOP_FEEDBACK_RETRY_DEFAULT_SECONDS, LOOP_FEEDBACK_RETRY_MAX_SECONDS);
|
|
3269
|
+
await new Promise((resolve) => {
|
|
3270
|
+
const timer = setTimeout(done, seconds * 1000);
|
|
3271
|
+
function done() {
|
|
3272
|
+
clearTimeout(timer);
|
|
3273
|
+
signal?.removeEventListener("abort", done);
|
|
3274
|
+
resolve();
|
|
3275
|
+
}
|
|
3276
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
3277
|
+
});
|
|
3278
|
+
if (signal?.aborted)
|
|
3279
|
+
throw error;
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
server.tool("loop_add_signal", "Say how an answer did: Good, Wrong, or Wrong with the right answer. trace_id is the sample ID (loop_chat's sample_id, the X-Loop-Sample-Id an app got back, or a trace_id from loop_capture_trace or loop_list_traces). Feedback is added, never replaced: a second one sits beside the first, because two reviewers disagreeing is worth knowing, and the newest from a person decides.\n\nWHAT EACH ONE DOES (every version learns from examples):\n• good (stored as accepted) — the answer was right. It is learned as it is.\n• bad (stored as rejected) — it was wrong and you have nothing better. It is LEFT OUT of training and nothing is learned from it; prefer corrected whenever you know the right answer.\n• corrected (stored as edited) — the model was WRONG and here is what it should have said, in correction. The most valuable single action: your right answer is learned instead of the model's. Nothing is learned from the answer it replaced.\n• gold — the right answer for this question, recorded whatever the model happened to say. It trains exactly like corrected; it only makes no claim that the model was wrong.\nTraining that learns from rankings or rewards is not available yet, so bad with no right answer teaches nothing; a correction does.\n\nFeedback sent the instant an answer arrives can reach the platform before the conversation is saved (409 SAMPLE_NOT_YET_RECORDED, nothing recorded). This tool then waits (the Retry-After seconds, 2 when none is given, at most 30) and sends the same feedback again by itself, at most 6 tries in all. If it is still refused, the sample ID names no saved conversation yet: saving is behind, or the call named a pipeline ID no pipeline has and nothing else recorded it. Say so, and check the pipeline ID.", {
|
|
3163
3284
|
trace_id: z.string(),
|
|
3164
|
-
verdict: z.enum(["
|
|
3165
|
-
correction: z.string().optional().describe("REQUIRED
|
|
3285
|
+
verdict: z.enum(["good", "bad", "corrected", "gold", "accepted", "rejected", "edited"]).describe("good = the answer was right; bad = it was wrong, with nothing better to offer; corrected = it was wrong and correction is the right answer; gold = the right answer for this question regardless of what the model said. accepted, rejected and edited are the stored names of good, bad and corrected, and are taken too"),
|
|
3286
|
+
correction: z.string().optional().describe("REQUIRED with 'corrected' or 'edited' (the answer it should have given) and with 'gold' (the right answer for this question)"),
|
|
3166
3287
|
reason: z.string().optional().describe("Why, in your own words"),
|
|
3167
3288
|
author: z.string().optional(),
|
|
3168
|
-
labels: z.array(z.string()).optional().describe("Which pipeline this feedback feeds, named HERE rather than in a second call: a pipeline trains on every conversation carrying any of its tags, so a tag is the pipeline a conversation goes down. Sent with the
|
|
3289
|
+
labels: z.array(z.string()).optional().describe("Which pipeline this feedback feeds, named HERE rather than in a second call: a pipeline trains on every conversation carrying any of its tags, so a tag is the pipeline a conversation goes down. Sent with the feedback it is written in one transaction: either both land or neither does. A separate tagging call is the one that gets skipped, and what it leaves is a reviewed conversation in no pipeline — counted in every 'reviewed' total and selected by nothing."),
|
|
3169
3290
|
attributes: z.record(z.string(), z.string()).optional().describe("The same, by named dimension: {\"category\": \"billing\"}."),
|
|
3170
3291
|
parent: z.string().optional().describe("The master group the bare labels belong to, so selecting the group picks them up without anybody maintaining a list. Leave it out and a label the conversation already carries keeps the group it has; send an empty string to take the labels out of their group."),
|
|
3171
|
-
}, async ({ trace_id, verdict, correction, reason, author, labels, attributes, parent }) => {
|
|
3172
|
-
const data = await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/signals`, {
|
|
3292
|
+
}, async ({ trace_id, verdict, correction, reason, author, labels, attributes, parent }, extra) => {
|
|
3293
|
+
const data = await sendLoopFeedback(() => client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/signals`, {
|
|
3173
3294
|
method: "POST",
|
|
3174
3295
|
body: {
|
|
3175
3296
|
verdict, source: "human", correction, reason, author,
|
|
3176
3297
|
labels, attributes, parent,
|
|
3177
3298
|
},
|
|
3178
|
-
});
|
|
3299
|
+
}), extra?.signal);
|
|
3179
3300
|
return json(data);
|
|
3180
3301
|
});
|
|
3181
|
-
server.tool("loop_list_signals", "
|
|
3302
|
+
server.tool("loop_list_signals", "All the feedback recorded on one conversation, oldest first. Feedback is added, never replaced, so two reviewers who disagreed are both here.", { trace_id: z.string() }, async ({ trace_id }) => json(await client.api(`/api/loop/traces/${encodeURIComponent(trace_id)}/signals`)));
|
|
3182
3303
|
server.tool("loop_label_trace", "Label a recorded conversation so it can be selected later. An average over everything is the least useful thing to train on — a model weak at refunds is fixed with refund examples, and you can only select those if the conversation was labelled when it arrived. A label may name a PARENT, which makes a master group: labelling something 'refunds' with parent 'billing' makes it selectable as either, and selecting 'billing' later gathers every child without anybody maintaining a list of them. Labels are lowercased and trimmed, so 'Refunds' and 'refunds' are one label.", {
|
|
3183
3304
|
trace_id: z.string(),
|
|
3184
3305
|
labels: z.array(z.string()).optional().describe("Bare tags, e.g. ['refunds','escalated']. Lowercase letters, digits, dot, dash or underscore."),
|
|
@@ -3200,14 +3321,14 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3200
3321
|
return json(data);
|
|
3201
3322
|
});
|
|
3202
3323
|
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")));
|
|
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 —
|
|
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
|
|
3324
|
+
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 — training still drops duplicates and answers it cannot use, and keeps some aside for the automatic test. Each pipeline's state says what happens to it there: held_back (kept aside for the automatic test, never trained on), learned (version_no learned it), trained (training attempt_no learned from it and did not become a version), waiting (for the next training) 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`)));
|
|
3325
|
+
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 samples kept aside for the automatic test and conversations too long to train on come out of it when a training starts. capture lists the calls sent with X-Loop-Pipeline in the last capture.days days that were not recorded under a name the header gave, one entry per name (a call naming two is counted under each, so never add entries up as calls): reason no_pipeline (no pipeline has that pipeline ID: 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 the 'traces' total, every sample); pipelines is how many pipelines there are and how many are training, needs_you (waiting on the user), serving or paused.", {}, async () => {
|
|
3205
3326
|
const data = await client.api("/api/loop/stats");
|
|
3206
3327
|
return json(data);
|
|
3207
3328
|
});
|
|
3208
|
-
server.tool("loop_get_metrics_history", "How the loop moved over time: every
|
|
3329
|
+
server.tool("loop_get_metrics_history", "How the loop moved over time: every training as a point, oldest first (its training number attempt_no, the version_no it became — null when it did not become one —, its win_rate (of the test questions where its answer and the live version's scored differently, the share where its answer scored higher), cost -- the money columns as recorded, and cost_cents with cost_up_to, the training priced as the pipeline's list of trainings prices it, so the points add up to the pipeline's totals.spent_cents; cost_up_to true means say 'at most' --, the recipe it trained with, and the bars its test 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 kind. truncated says older trainings exist beyond the limit. Pass rule_id (a pipeline's id) for one pipeline's trainings; 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.", {
|
|
3209
3330
|
rule_id: z.string().optional().describe("One pipeline's id, from loop_list_pipelines. Omit for the whole workspace."),
|
|
3210
|
-
limit: z.number().int().min(1).max(500).optional().describe("How many
|
|
3331
|
+
limit: z.number().int().min(1).max(500).optional().describe("How many trainings at most: 200 when omitted, at most 500"),
|
|
3211
3332
|
days: z.number().int().min(1).max(365).optional().describe("Days of feedback counts: 90 when omitted, at most 365"),
|
|
3212
3333
|
}, async ({ rule_id, limit, days }) => json(await client.api("/api/loop/metrics/history", {
|
|
3213
3334
|
params: { rule_id, limit: limit?.toString(), days: days?.toString() },
|
|
@@ -3217,12 +3338,12 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3217
3338
|
/* */
|
|
3218
3339
|
/* A PIPELINE is one use case: a name, a model, the tags whose samples it */
|
|
3219
3340
|
/* trains on, when to train (at least N new samples, and optionally a */
|
|
3220
|
-
/* schedule), the share of its samples
|
|
3221
|
-
/* how a new version
|
|
3222
|
-
/*
|
|
3341
|
+
/* schedule), the share of its samples kept aside for the automatic test, */
|
|
3342
|
+
/* and how a new version goes live. The platform decides the rest -- the */
|
|
3343
|
+
/* GPUs, how it trains, the test's floor, the test itself -- and SAVING A */
|
|
3223
3344
|
/* PIPELINE IS THE AUTHORIZATION TO SPEND: whenever its data has grown */
|
|
3224
|
-
/* enough it
|
|
3225
|
-
/* room. What is charged is the GPU time the
|
|
3345
|
+
/* enough it runs a training on the workspace's wallet with nobody in the */
|
|
3346
|
+
/* room. What is charged is the GPU time the trainings use and, where the */
|
|
3226
3347
|
/* platform scores samples, the judge's scoring of unreviewed samples. */
|
|
3227
3348
|
/* */
|
|
3228
3349
|
/* The wire shapes are the frozen contract in docs/loop-training-contracts.md.*/
|
|
@@ -3232,16 +3353,16 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3232
3353
|
/* are not changing. */
|
|
3233
3354
|
/* ══════════════════════════════════════════════════════════════════════════ */
|
|
3234
3355
|
const pipelineModelSchema = z.object({
|
|
3235
|
-
id: z.string().describe("A model id from loop_list_models. On a create, any other model is refused MODEL_NOT_TRAINABLE. On an edit only a changed id is checked against the list: an id equal to the one the pipeline has there now (ignoring case) is kept, and whether it can still be trained is checked when
|
|
3356
|
+
id: z.string().describe("A model id from loop_list_models. On a create, any other model is refused MODEL_NOT_TRAINABLE. On an edit only a changed id is checked against the list: an id equal to the one the pipeline has there now (ignoring case) is kept, and whether it can still be trained is checked when a training starts."),
|
|
3236
3357
|
});
|
|
3237
3358
|
/** The pipeline create and edit body: every field the service accepts, and no other. */
|
|
3238
3359
|
const pipelineInputShape = {
|
|
3239
3360
|
workspace_id: z.string().optional().describe("The workspace the pipeline belongs to. Omit to use the key's own workspace."),
|
|
3240
|
-
name: z.string().optional().describe("What the pipeline is called, e.g. 'Support replies'. Its
|
|
3361
|
+
name: z.string().optional().describe("What the pipeline is called, e.g. 'Support replies'. Its pipeline ID (own_tag) is made from it and never changes."),
|
|
3241
3362
|
use_case: z.string().optional().describe("One sentence on what it is for"),
|
|
3242
|
-
model: pipelineModelSchema.optional().describe("The
|
|
3243
|
-
challenger_model: pipelineModelSchema.nullable().optional().describe("Optional second model:
|
|
3244
|
-
tags: z.array(z.string()).optional().describe("More tags whose samples this pipeline trains on, beside its own. On an edit the list replaces the previous one; the
|
|
3363
|
+
model: pipelineModelSchema.optional().describe("The main model: the one the pipeline improves"),
|
|
3364
|
+
challenger_model: pipelineModelSchema.nullable().optional().describe("Optional second model, a different model from the main one (the same model twice is refused: each model keeps building on its own versions). After each training of the main model on new samples it is trained on the same samples -- building on its own latest version, or from its original model on all samples while it has none -- and tested the same way, against the version live at the time; each training that does better goes live in turn (version numbers count up), so both models' trainings can become versions, and a second model that goes live becomes the pipeline's main model while the current main one becomes the second. null removes it; a training of it already running is not stopped."),
|
|
3365
|
+
tags: z.array(z.string()).optional().describe("More tags whose samples this pipeline trains on, beside its own. On an edit the list replaces the previous one; the pipeline ID stays."),
|
|
3245
3366
|
// STRICT: the sample minimums are fixed by the platform, and the service
|
|
3246
3367
|
// refuses a train_when.min_samples by name. zod would otherwise strip the
|
|
3247
3368
|
// key in silence, the save would succeed, and the agent would tell the user
|
|
@@ -3254,16 +3375,16 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3254
3375
|
}).nullable().optional().describe("Also wait for this time. Training starts only when BOTH the platform's fixed sample minimum and the schedule are met. null trains as soon as the minimum is met."),
|
|
3255
3376
|
}).strict().optional().describe("When to train, beyond the platform's fixed sample minimum: only a schedule. There is no min_samples: the minimums are fixed (loop_list_models floors) and cannot be set."),
|
|
3256
3377
|
promotion: z.object({
|
|
3257
|
-
policy: z.enum(["holdout", "all", "primary", "k_of_n", "weighted", "manual"]).describe("holdout (default): better on the
|
|
3378
|
+
policy: z.enum(["holdout", "all", "primary", "k_of_n", "weighted", "manual"]).describe("How a new version goes live. 'holdout' (default): better on the automatic test (questions from the pipeline's own samples it never trained on). all / primary / k_of_n / weighted: by the pipeline's benchmarks. manual: never put live without a person, a rebuild after a deleted conversation included. Under every other policy a rebuild after a deleted conversation goes live unless it is clearly worse than the live version."),
|
|
3258
3379
|
k: z.number().int().min(1).optional().describe("For k_of_n only: how many measurements must improve"),
|
|
3259
3380
|
}).optional(),
|
|
3260
3381
|
// STRICT, and never null: the share is the member's, the floor of 100 and
|
|
3261
3382
|
// the count are the platform's, and the service refuses anything else by
|
|
3262
3383
|
// name. zod would otherwise strip holdout.count in silence.
|
|
3263
3384
|
holdout: z.object({
|
|
3264
|
-
percent: z.number().int().min(5).max(20).describe("The share of the pipeline's samples
|
|
3265
|
-
}).strict().optional().describe("The
|
|
3266
|
-
max_versions: z.number().int().min(1).max(100).nullable().optional().describe("How many versions it may make, 1 to 100; null (the default) is no limit. A version is
|
|
3385
|
+
percent: z.number().int().min(5).max(20).describe("The share of the pipeline's samples kept aside for the automatic test of each training and never trained on: a whole number from 5 to 20 (5 is the default). At least 100 are kept aside whatever it says."),
|
|
3386
|
+
}).strict().optional().describe("The automatic test's share (loop_list_models floors.held_back says the range). It does not change what the pipeline may spend and nothing is asked again: a new share is a new revision, saved at once. But a larger share means more of the automatic test's samples are scored for each training (at most floors.held_back.scored_rows, the ones kept aside longest first, so later trainings are mostly asked the same questions), and scoring is paid from the wallet; a share that keeps aside more than that only keeps samples out of training. Lowering it keeps every sample already kept aside; raising it keeps aside only new samples, and new samples fill the automatic test before they count toward the next training, so after a raise the next training waits for them (the pipeline's minimums say how many). Leave it out to keep the pipeline's (or the default on a create)."),
|
|
3387
|
+
max_versions: z.number().int().min(1).max(100).nullable().optional().describe("How many versions it may make, 1 to 100; null (the default) is no limit. A version is a training that went live; trainings that did not become a version are not counted."),
|
|
3267
3388
|
enabled: z.boolean().optional().describe("false saves it switched off"),
|
|
3268
3389
|
// STRICT, and three-state twice over: training null puts every setting back
|
|
3269
3390
|
// to automatic, and inside it each key is absent (keep), null (automatic) or
|
|
@@ -3274,9 +3395,9 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3274
3395
|
learning_rate: z.number().min(1e-6).max(1e-2).nullable().optional().describe("1e-6 to 1e-2. null: automatic (training_defaults.learning_rate)."),
|
|
3275
3396
|
lora_rank: z.union([z.literal(4), z.literal(8), z.literal(16), z.literal(32), z.literal(64)]).nullable().optional().describe("4, 8, 16, 32 or 64. null: automatic (training_defaults.lora_rank)."),
|
|
3276
3397
|
lora_alpha: z.number().int().min(1).max(512).nullable().optional().describe("1 to 512. null: automatic (training_defaults.lora_alpha)."),
|
|
3277
|
-
replay_percent: z.number().int().min(0).max(50).nullable().optional().describe("0 to 50:
|
|
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
|
|
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
|
|
3398
|
+
replay_percent: z.number().int().min(0).max(50).nullable().optional().describe("0 to 50: when a training builds on the live version, the share of what it trains on that is samples the versions before already learned, gone over again so the new version does not forget them. null: automatic (50)."),
|
|
3399
|
+
}).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 training, and earlier trainings 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."),
|
|
3400
|
+
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 training: nothing is trained again and versions already made keep what they learned."),
|
|
3280
3401
|
// STRICT, and three-state: absent keeps the conditions, null clears them,
|
|
3281
3402
|
// an object replaces them whole. Tags are refused inside it by the service
|
|
3282
3403
|
// (a pipeline's tags are its tags field), so they are not offered.
|
|
@@ -3286,15 +3407,15 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3286
3407
|
model: z.string().min(1).max(200).nullable().optional().describe("Only samples this model answered, exactly."),
|
|
3287
3408
|
from: z.string().datetime({ offset: true }).nullable().optional().describe("Only samples recorded at or after this RFC 3339 time."),
|
|
3288
3409
|
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
|
|
3410
|
+
}).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 training; to make the live version forget samples, delete them (loop_delete_trace)."),
|
|
3290
3411
|
// STRICT: the switch is the only key the service takes under it.
|
|
3291
3412
|
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
|
|
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
|
|
3413
|
+
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 training needs, and the ones scoring 0.8 or more are trained on. false trains only on samples a person reviewed."),
|
|
3414
|
+
}).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 training it was for (sample_scoring_cents, inside cost_cents)."),
|
|
3294
3415
|
};
|
|
3295
|
-
server.tool("loop_list_models", "The
|
|
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
|
|
3297
|
-
server.tool("loop_update_pipeline", "Edit a pipeline: its name, use case, model,
|
|
3416
|
+
server.tool("loop_list_models", "The 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 training needs at the default automatic-test share, the automatic test's samples included), new_samples (each later training waits for new_samples new samples, 1,000, counted since the last training, however many samples that training learned from; where a pipeline builds on its live version -- its chain is set -- that training builds on the live version and learns what it has not learned yet, and also goes over samples it already learned so it does not forget them (about half of what it trains on, picked at random); a version made that way is served as one model), and held_back (the automatic test: at least min_rows samples are kept aside, or the share a pipeline chooses from default_percent to max_percent if that is more; each training is tested on at most scored_rows of them, so a share that keeps aside 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")));
|
|
3417
|
+
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 training after the first; where the pipeline builds on its live version, each training builds on it and learns what it has not learned yet, and also goes over samples picked at random from what it already learned, about half of what it trains on, so it does not forget them). SPEND CONSENT: saving it authorises the platform to start trainings on the workspace's wallet with nobody present — whenever train_when is met it trains on GPUs the platform chooses, tests the training against the live version, and puts it live by the pipeline's going-live choice (the 'promotion' field). What is charged is the GPU time its trainings actually use and, where the platform scores samples, the judge's scoring of unreviewed samples, shown on the training 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 training 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 trainings' tests; 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 })));
|
|
3418
|
+
server.tool("loop_update_pipeline", "Edit a pipeline: its name, use case, model, second model, tags, datasets, conditions, the judge's scoring of unreviewed samples (auto_scoring), when to train, the automatic test's share, how a new version goes live, 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 training (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 second model), 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 change to the training settings applies from the next training; 422 TRAINING_SETTING_OUT_OF_RANGE names the field and its range. Saving is the authorization, as on a create: a change that makes trainings cost more (a bigger model, switching it back on, turning auto_scoring back on) is SPEND CONSENT, so say so first. A larger automatic-test share is not SPEND CONSENT (nothing is asked again and the limits do not move), and what it changes depends on how many are kept aside: while fewer than floors.held_back.scored_rows (1,500) are, each training 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 a training is running is refused RUN_ACTIVE. 409 NOT_A_SIMPLE_PIPELINE: this pipeline was created before pipelines had tags, a use case or a second model, so those three cannot be edited; everything else can. Every other refusal is loop_create_pipeline's, with the same meaning.", {
|
|
3298
3419
|
pipeline_id: z.string().describe("The pipeline's id (rule_id), from loop_list_pipelines"),
|
|
3299
3420
|
...pipelineInputShape,
|
|
3300
3421
|
expected_revision: z.number().int().min(0).optional().describe("The revision you read"),
|
|
@@ -3302,16 +3423,16 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3302
3423
|
method: "PUT",
|
|
3303
3424
|
body,
|
|
3304
3425
|
})));
|
|
3305
|
-
server.tool("loop_delete_pipeline", "Delete a pipeline for good.
|
|
3306
|
-
server.tool("loop_run_pipeline", "Train now: start
|
|
3426
|
+
server.tool("loop_delete_pipeline", "Delete a pipeline for good. A training 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 saved model copy is kept (and billed as storage) until that deployment is deleted. Its history is deleted in the background after the call returns: the saved copies its trainings made, the sets they trained on, and every conversation of their test and benchmark reports. Its samples and tags are kept. Recording from the source named by its pipeline ID 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" })));
|
|
3427
|
+
server.tool("loop_run_pipeline", "Train now: start a training 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: a training is already running. A training waiting for GPUs to be tested (active_run.state waiting_gpus) is the one exception: Train now books its two test 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 original model 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 a training could not be tested 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 training's test 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`, {
|
|
3307
3428
|
method: "POST",
|
|
3308
3429
|
body: {},
|
|
3309
3430
|
})));
|
|
3310
|
-
server.tool("loop_import_into_pipeline", "loop_import_rows with every row given this pipeline's
|
|
3431
|
+
server.tool("loop_import_into_pipeline", "loop_import_rows with every row given this pipeline's ID (own_tag), so the file becomes this pipeline's samples. source defaults to the pipeline ID. default_feedback 'good' records every answered row that brought no feedback of its own as a good example to learn from — use it only when the user says the file's answers are good; without it such rows wait for review. The result, the retry token (import_id) and the 5000-row limit are loop_import_rows' own.", {
|
|
3311
3432
|
pipeline_id: z.string(),
|
|
3312
3433
|
rows: z.array(z.record(z.string(), z.any())).describe("The rows, already parsed, at most 5000, each in the shape it already has"),
|
|
3313
|
-
default_feedback: z.enum(["good"]).optional().describe("'good' makes every answered row without its own
|
|
3314
|
-
source: z.string().optional().describe("Where the rows came from. Defaults to the pipeline
|
|
3434
|
+
default_feedback: z.enum(["good"]).optional().describe("'good' makes every answered row without its own feedback a good example"),
|
|
3435
|
+
source: z.string().optional().describe("Where the rows came from. Defaults to the pipeline ID."),
|
|
3315
3436
|
model: z.string().optional(),
|
|
3316
3437
|
labels: z.array(z.string()).optional().describe("More tags for every row, beside the pipeline's own"),
|
|
3317
3438
|
attributes: z.record(z.string(), z.string()).optional(),
|
|
@@ -3350,6 +3471,21 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3350
3471
|
params: { limit: limit?.toString(), offset: offset ? offset.toString() : undefined },
|
|
3351
3472
|
})));
|
|
3352
3473
|
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)}`)));
|
|
3474
|
+
server.tool("loop_export_dataset", "A dataset's samples as lines in the import's chat format, oldest first: each with sample_id, messages (the conversation, the model's answer last) -- or prompt, the question, for a sample nobody answered -- and the feedback a person gave in the import's words -- label true (good), label false (wrong), correction (wrong, with the right answer), ground_truth (the right answer given as the gold answer, or written on a question nobody answered). No feedback writes none of them. The lines can be sent back with loop_import_rows as they are. At most limit lines (100 when absent, at most 1000); for the whole file use the console's Download or the SDK's exportDataset. stored_elsewhere is how many of the dataset's samples have no conversation kept with them on the platform (kept in the workspace's own storage, or none at all), the most that can be left out: any of them that could not be read just now are left out of the lines, so when it is above 0 say that some samples may be missing and why; at 0 none can be. It writes nothing.", {
|
|
3475
|
+
dataset_id: z.string().describe("The dataset's id, from loop_list_datasets"),
|
|
3476
|
+
limit: z.number().int().min(1).max(1000).optional().describe("How many samples, 100 when absent, at most 1000"),
|
|
3477
|
+
}, async ({ dataset_id, limit }) => {
|
|
3478
|
+
const { text, headers } = await client.apiText(`/api/loop/datasets/${encodeURIComponent(dataset_id)}/export`, {
|
|
3479
|
+
params: { format: "jsonl", limit: String(limit ?? 100) },
|
|
3480
|
+
});
|
|
3481
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
|
|
3482
|
+
const elsewhere = headers.get("X-Loop-Stored-Elsewhere");
|
|
3483
|
+
return json({
|
|
3484
|
+
lines,
|
|
3485
|
+
returned: lines.length,
|
|
3486
|
+
stored_elsewhere: elsewhere != null && /^\d+$/.test(elsewhere) ? Number(elsewhere) : null,
|
|
3487
|
+
});
|
|
3488
|
+
});
|
|
3353
3489
|
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
3490
|
...datasetConditionsShape,
|
|
3355
3491
|
datasets: z.array(z.string()).optional().describe("Saved dataset ids whose samples are added to what the conditions select"),
|
|
@@ -3358,7 +3494,7 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3358
3494
|
name: z.string().min(1).max(80).describe("What the dataset is called, 1 to 80 characters, unique in the workspace"),
|
|
3359
3495
|
...datasetConditionsShape,
|
|
3360
3496
|
}, 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
|
|
3497
|
+
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 training: samples that join count as new samples and can bring a pipeline to its next training, 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
3498
|
dataset_id: z.string().describe("The dataset's id, from loop_list_datasets"),
|
|
3363
3499
|
name: z.string().min(1).max(80).optional(),
|
|
3364
3500
|
...datasetConditionsShape,
|
|
@@ -3367,23 +3503,24 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3367
3503
|
method: "PUT",
|
|
3368
3504
|
body,
|
|
3369
3505
|
})));
|
|
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
|
|
3371
|
-
server.tool("
|
|
3372
|
-
|
|
3373
|
-
|
|
3506
|
+
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 training; 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" })));
|
|
3507
|
+
server.tool("loop_get_training_curve", "How one training went while it trained, step by step: train_loss is its mistakes on the samples it trained on and check_loss its mistakes on the practice questions it set aside and did not train on (lower is better), at most 200 points, every practice check among them. kept_step is the step whose model it kept: say 'it kept the model from step N, where it did best on the practice questions', or, when keeps_last is true, 'it kept its last step: for this model practice mistakes can rise while it is still learning facts, so the test results are what count'. trained_steps and planned_steps say how far it got. When points is empty, read reason back as written. 503 TRAINING_CURVE_UNAVAILABLE: it could not be read just now; try again in a minute. It writes nothing.", { run_id: z.string().describe("The training's id, from loop_list_pipeline_attempts or loop_list_training_runs") }, async ({ run_id }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/training-curve`)));
|
|
3508
|
+
server.tool("loop_list_training_runs", "Every training of every pipeline, newest first: what state it is in, what it has spent, and what its test said. result filters by what became of a training, in loop_list_pipeline_attempts' words, and each item carries 'attempt': the training as its pipeline's own list says it (outcome, cost, pass rates and explanation, the plain words to read back; null for one that is not a numbered training). A training waiting in 'awaiting_review' is waiting for a PERSON — it holds a newly trained model whose machine is being paid for by the minute, so surface those first when the user asks what needs attention. A training in 'waiting_gpus' trained and is waiting for GPUs to serve the new model and what it is tested against (the live version, or before v1 the original model) side by side for its test: 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 start), and Train now tries again at once. billed_candidate_up_to true means billed_candidate_cents is a ceiling: a test 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 'at most', never 'spent'. It is false again once billing has settled every such machine: the training'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 training that went live (a version) or that a person took over is never settled, so that training stays 'at most'. comparison_returned_cents above 0 means a test 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 'Test 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.", {
|
|
3509
|
+
rule_id: z.string().optional().describe("Only this pipeline's trainings (its id, from loop_list_pipelines)"),
|
|
3510
|
+
state: z.string().optional().describe("Only trainings in this state, e.g. 'awaiting_review'"),
|
|
3374
3511
|
result: z.enum(["became_version", "not_better", "inconclusive", "not_tested", "failed", "stopped", "running", "waiting_for_review"]).optional()
|
|
3375
|
-
.describe("Only the
|
|
3512
|
+
.describe("Only the trainings that ended this way, as loop_list_pipeline_attempts filters them; running includes one waiting for a decision"),
|
|
3376
3513
|
limit: z.number().int().min(1).max(200).optional(),
|
|
3377
3514
|
offset: z.number().int().min(0).optional(),
|
|
3378
3515
|
}, async ({ rule_id, state, result, limit, offset }) => json(await client.api("/api/loop/training-runs", {
|
|
3379
3516
|
params: { rule_id, state, result, limit: limit?.toString(), offset: offset?.toString() },
|
|
3380
3517
|
})));
|
|
3381
|
-
server.tool("loop_get_training_run", "One
|
|
3382
|
-
server.tool("loop_list_pipelines", "Every pipeline in the workspace: the
|
|
3518
|
+
server.tool("loop_get_training_run", "One training in full: whether it was tested (tested: its comparison measured something -- a training never tested can never go live, and available_actions.retest says loop_retest_training_run can test it again), the 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 step it kept; read it back as written, and trained_steps is the steps the trainer counted, never an estimate; both null until its training ended, and on a training from before), the samples it learned from and the ones kept aside for its automatic test ('lineage' says what it was built on: trained_from versions -- it builds on built_on's versions -- or base, from the original model, with from_base_reason; and train_rows = new_samples + carried_samples, from trainings that did not become a version, + replayed_samples, samples those versions already learned, gone over again so they are not forgotten (the counts are null on a training from before trainings built on earlier versions, or of a pipeline that does not build on its live version: say only train_rows then); say them as 'learned N new samples', 'N from training #5, which did not become a version' and 'went over N samples it had learned before'; 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 test paid and its samples go to the next training, NO_MERGED_EXPORT that its training finished without the single 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 training asks again (STEP_STALLED:TRAINING_SERVICE_BEHIND: the same after six hours of waiting for that update; the pipeline tries again on its own), STEP_STALLED:SERVING_NOT_CERTIFIED that the platform had not yet checked it can test new versions of this model on its current serving engine, so it waited six hours before its training and stood down with nothing charged and the pipeline not paused (its next training waits the same way until that check is done; CHECKPOINT_NOT_DEPLOYABLE:SERVING_NOT_CERTIFIED is the same found at its comparison, after it trained), 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 a ceiling: a test 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 'at most', 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 training that went live (a version) or that a person took over is never settled, so that training stays 'at most'; comparison_returned_cents above 0 means a test 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 'Test 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 training itself, the newly trained model and its test, benchmark_runs (every benchmark it was tested on; each run_id is that benchmark run's id for loop_get_benchmark_run and loop_list_benchmark_run_items), 'promotion' (outcome as the pipeline's trainings list says it; decision, where rejected means a member chose not to publish it even when the result 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 training whose result 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 automatic test had to clear -- questions compared, failures, a rubric set, mean change, how often the new answer scored higher, and the chance a model no better would score higher this often -- each with needed, got and met; null until the test 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 a training 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 test was decided under an earlier version of the platform's bars, so its bars are not listed -- its result 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 training -- and total_cents, the training's cost_cents on the pipeline; up_to true means comparison_cents and the total are 'at most' figures, counted from the test 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 trainings list: read it back rather than the codes), model_role, builds_on_version, compared_with_version, compared_with and missed_bar (as loop_list_pipeline_attempts says them), 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 training can look ready to go live and not be, because the consent moved or the newly trained model is already gone.", { run_id: z.string() }, async ({ run_id }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}`)));
|
|
3519
|
+
server.tool("loop_list_pipelines", "Every pipeline in the workspace: the versions it has made (a version is a training that went live; attempts[] lists every training, whether it became a version 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 training whose result is better was better: never say the pipeline refused it -- and train_rows and holdout_rows, the samples it learned from and the ones kept aside for its automatic test; each training is tested on at most scored_rows (1,500) of the automatic test's samples, the ones kept aside longest first; a sample once kept aside stays kept aside, so later trainings are mostly asked the same questions, plus newer ones while fewer than 1,500 are kept aside), which one serves the app today -- the live version, the version the app gets answers from right now (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 what it was tested against (the live version of its day, or before v1 the original model) and on the standing benchmark, versions_made against max_versions, the training in progress 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 a training whose active_run.state is waiting_gpus: it trained, but no GPUs were free to serve the new model and the one it is tested against 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 start), and Train now tries again at once. cost_up_to true on a training, active_run or a version means its cost_cents is a ceiling: a test 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 'at most', never 'spent'. It is false again once billing has settled every such machine (the training'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 'at most'. On a training or active_run, comparison_returned_cents above 0 means a test 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 'Test 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 a training 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, pipeline ID or model; status keeps those training, waiting on the user (needs_you), serving or paused. Each pipeline's attempts[] here is its latest 5 trainings and its versions[] the latest few and the live one (attempts_total and versions_total say how many there are); totals is every training it made, as on loop_get_pipeline; loop_get_pipeline has every version, and loop_list_pipeline_attempts reads every training. Each training'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. second_model is as loop_get_pipeline describes it, except that here it can also be null when where the second model stands could not be read just then: when challenger_model still names one, say its state could not be read, never that the pipeline has no second model.", {
|
|
3383
3520
|
limit: z.number().int().min(1).max(100).optional().describe("How many pipelines, 100 when absent, at most 100"),
|
|
3384
3521
|
offset: z.number().int().min(0).optional().describe("How many of the newest pipelines to skip"),
|
|
3385
|
-
q: z.string().max(200).optional().describe("A case-insensitive substring of the name, the
|
|
3386
|
-
status: z.enum(["training", "needs_you", "serving", "paused"]).optional().describe("training:
|
|
3522
|
+
q: z.string().max(200).optional().describe("A case-insensitive substring of the name, the pipeline ID or the model, at most 200 characters"),
|
|
3523
|
+
status: z.enum(["training", "needs_you", "serving", "paused"]).optional().describe("training: a training is running. needs_you: waiting on the user (a review or funds). serving: its live version answers. paused: switched off or paused."),
|
|
3387
3524
|
}, async ({ limit, offset, q, status }) => json(await client.api("/api/loop/pipelines", {
|
|
3388
3525
|
params: {
|
|
3389
3526
|
limit: limit?.toString(),
|
|
@@ -3392,12 +3529,12 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3392
3529
|
status,
|
|
3393
3530
|
},
|
|
3394
3531
|
})));
|
|
3395
|
-
server.tool("loop_list_pipeline_attempts", "A pipeline's
|
|
3532
|
+
server.tool("loop_list_pipeline_attempts", "A pipeline's trainings (in the API's names, 'attempt'), newest first, a page at a time (50 unless limit says, at most 200): every training it ran, whether it became a version or not, each with the fields attempts[] has on loop_get_pipeline -- its outcome, decision, 'lineage', the samples it learned from and the ones kept aside for its automatic test, its automatic-test pass rates (the share each side passed, score 0.70 or more), what it cost (cost_up_to true means the cost is 'at most', never 'spent', and it is false again once billing has settled every test machine charged in it, whose timeline says what each was charged; comparison_returned_cents above 0 means a test machine never started and was not charged: say 'Test machine: not charged (it never started)', or, while the cost still counts a test 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. Each also says model_role (main, or second for the pipeline's second model), builds_on_version (the version it built on; null when it trained from the original model), compared_with_version and compared_with (what its automatic test asked as the opponent: version with that version's number, original for the original model while no version was live, null when it was not tested or nothing recorded which -- say 'v4', never 'the model it was compared with'), and missed_bar {key, what, needed, got} for not_better and 'inconclusive': the first bar its test missed, with what in plain words -- read what back when the user asks why a training that got more right was still not better (null when the pipeline's Going live setting decided). total is how many match the filters; pass offset for the next page. result keeps the trainings 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 main model's trainings (main) or the second model's (second). Read-only.", {
|
|
3396
3533
|
pipeline_id: z.string().describe("The pipeline's id (rule_id), from loop_list_pipelines"),
|
|
3397
|
-
limit: z.number().int().min(1).max(200).optional().describe("How many
|
|
3398
|
-
offset: z.number().int().min(0).optional().describe("How many of the newest
|
|
3399
|
-
result: z.enum(["became_version", "not_better", "inconclusive", "not_tested", "failed", "stopped", "running", "waiting_for_review"]).optional().describe("Only the
|
|
3400
|
-
model: z.enum(["main", "second"]).optional().describe("main: the
|
|
3534
|
+
limit: z.number().int().min(1).max(200).optional().describe("How many trainings, 50 when absent, at most 200"),
|
|
3535
|
+
offset: z.number().int().min(0).optional().describe("How many of the newest trainings to skip"),
|
|
3536
|
+
result: z.enum(["became_version", "not_better", "inconclusive", "not_tested", "failed", "stopped", "running", "waiting_for_review"]).optional().describe("Only the trainings that ended this way; running includes one waiting for a decision"),
|
|
3537
|
+
model: z.enum(["main", "second"]).optional().describe("main: the main model's trainings. second: the second model's."),
|
|
3401
3538
|
}, async ({ pipeline_id, limit, offset, result, model }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}/attempts`, {
|
|
3402
3539
|
params: {
|
|
3403
3540
|
limit: limit?.toString(),
|
|
@@ -3406,9 +3543,9 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3406
3543
|
model,
|
|
3407
3544
|
},
|
|
3408
3545
|
})));
|
|
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
|
|
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
|
|
3546
|
+
server.tool("loop_get_pipeline", "One pipeline in full, by its id. Words to use with the user: a training is one numbered try at a better model (#1, #2 ...; in the API's names, 'attempt', and its id is run_id); a version is a training that went live (v1, v2 ... with no limit); the live version is the version the app gets answers from right now; the automatic test is questions from the pipeline's own samples that the model never trained on. 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 result, the decision a person made, its win_rate (of the test questions where the two answers scored differently, the share where the new one scored higher), its pass rates on the automatic test (candidate_pass_rate for the new model and incumbent_pass_rate for what it was tested against: the live version then, or the original model before v1; the share each side passed, score 0.70 or more -- say it as 'right on 97% of its test (v4 got 85% on the same questions)', or before v1 'right on 97% of its test (the original model got 85% on the same questions)') and benchmark score, what it cost and why it was made (trigger, in the same words as attempts[]), plus the training in progress. When the monthly safety limit holds it, next_reason says so; the limit has no field. Read-only: putting a training live, rejecting it and going back to an earlier version stay on the training tools, and its settings on loop_update_pipeline. minimums says how many samples it has against what the next training 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[] holds ONLY the trainings that went live, numbered v1, v2 ... in the order they went live, each with its 'attempt' (training number), base_model_id and model_family -- and live_version is the one the app's model 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 trainings, whether they became a version or not (attempts_total says how many it ran, and loop_list_pipeline_attempts pages through every one; each also carries model_role, builds_on_version, compared_with_version, compared_with and missed_bar, as loop_list_pipeline_attempts says them), totals is every training it made in one object -- trainings, trainings_finished, trainings_untested (trained but never measured), trainings_by_result, trainings_by_model {main, second}, versions, spent_cents (every training's cost_cents added up, plus scoring_not_yet_in_a_training_cents -- what scoring unreviewed samples cost that no training has taken yet: training and testing, not the live version's serving), spent_up_to (true when any of them is an upper bound), spent_so_far (true while the figure can still change -- a training running or waiting for a member, billing settling one, or scoring running) -- say the two together: neither '$X', only spent_up_to 'at most $X', only spent_so_far '$X so far' (never 'at most' for a figure still growing), both 'at most $X so far' -- spent_parts {training_cents, test_machine_cents, grading_cents, benchmark_cents, sample_scoring_cents} adding up to it, gpu_seconds, first_training_at and last_training_at -- so when the user asks how many trainings, how many versions or what the pipeline has cost in all, read totals rather than adding up attempts[], 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 test 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 -- each model keeps its own line of versions: the second model builds on its own latest version, never on the main model's, and trains from its original model on all samples while it has none; model_role says which model a training trained (main or second) -- or rebuild, a retrain from the original model after a member deleted a conversation the live version learned from), decision (what was decided about it once tested, 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 training whose result is better reads stopped and was better: never say the pipeline refused it), and train_rows and holdout_rows (the samples it learned from and the ones kept aside for its automatic test; null until its set was built). 'lineage' on each version, training and the training in progress says what it was built on: trained_from versions (it builds on the live version: the versions in built_on, oldest first, served together as one model) or base (from the original model, with from_base_reason: first_version, 'challenger' (the second model's first training), rebuild -- a deleted conversation when lineage.rebuild is true, or, when it is false, a model's own latest version already built on 64 versions, the most one training can build on, so that model's line starts again -- model_cannot_build_on, chain_link_unreadable, or line_learned_deleted: a version of that model's own line learned a sample a member deleted, so its line starts again without it), and what it trained on -- train_rows = new_samples + carried_samples (samples of the trainings in carried_from_attempts, which did not become a version, learned again) + replayed_samples (about half: as many as new_samples + carried_samples, or all of learned_before when that is fewer, picked at random from what the versions it builds on already learned and gone over again, so they are not forgotten); learned_before is what those versions had learned and was not trained again, except the ones gone over again; served_as says how it is served -- an expert detail, never needed in an answer; learned_deleted_conversation true means it, or a version it was built on, learned a conversation that was deleted, so it can no longer be gone back to. The counts are null on a training from before trainings built on earlier versions, or of a pipeline that does not build on its live version: say only train_rows then. Say it in these words: 'Builds on v4. Learned 3,000 new samples: 2,000 that arrived since v4 and 1,000 from training #3, which did not become a version. It also went over 3,000 samples it had learned before, so it does not forget them.' -- name the part it went over again whenever replayed_samples is above 0, so the parts add up to train_rows; never say 'replayed', 'on top of' or 'v1 + v2'. second_model, when the pipeline names one, is where the second model stands: state (waiting -- it trains next, or after the main model's next training; training; tested; skipped -- it did not train on the main model's newest samples and will not, a skip is never a training and costs nothing; held -- a money limit, the scoring key, a live version that cannot answer, a rollback under way or a paused pipeline holds it; none -- the pipeline trains neither model now), for_training (the training the state is about), last_training, own_latest_version (the version it builds on next; null: it trains from its original model) and reason, its state in one plain sentence to read back as written; a second-model training that ends untested -- one that trained but could not be tested included -- is trained once more on the same samples (not one a member stopped, one refused for its model, or one stopped by its own spending limit). chain, when set, is the live version and the versions it is built from (versions, live one last), learned_samples and not_learned_samples (learned by trainings that did not become a version, so learned again next), and rebuild when a deleted conversation the live version learned has a retrain from the original model on its way; chain null means the pipeline does not build on its live version and every training starts from the original model on all samples. Nothing in progress is a version yet. 'holdout' is the automatic test's share: percent (5 to 20), min_rows (at least that many samples are kept aside whatever the share), 'rows' (how many it keeps aside today) and held_before (how many of those an earlier training of the pipeline kept aside: they stay kept aside whatever became of that training and whatever the share says, so a lower share releases none of them; before the first version the first training needs held_before plus 1,000 to train on when that is more than the share asks for). Each training is tested on at most scored_rows (1,500) of the automatic test's samples, the ones kept aside longest first (a sample once kept aside stays kept aside, so later trainings are mostly asked the same questions, plus newer ones while fewer than 1,500 are kept aside), so keeping more aside than 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 data its trainings 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 saved model copies its versions answer from (the live one's, and each one going back can still put back; a training that did not become a version keeps its copy, billed, until it is cleaned up within a day) are billed as model storage, like any trained model, and each version's own saved training state is billed as storage, like any training's; its reports, benchmark results and training data 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 a ceiling: a test 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 'at most', never 'spent'. It is false again once billing has settled every such machine (the training'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 'at most'. On attempts[] and active_run, comparison_returned_cents above 0 means a test 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 'Test 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 training finished and is waiting for GPUs to serve the new model and the one it is tested against 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 start), 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 entry per name the header gave (a call naming two is counted under each, so never add entries up as calls): under this pipeline's own names too_large or incomplete (the answer could not be kept), and under any name 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 training (already inside cost_cents, never added to it), 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 training'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)}`)));
|
|
3547
|
+
server.tool("loop_list_pipeline_benchmarks", "The benchmarks a pipeline is measured on (each a fixed set of the user's own questions), in priority order (1 is the primary), then the ones it stopped using, and its going-live choice (the 'promotion' field). Every training is tested on each attached benchmark; the going-live choice 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`)));
|
|
3548
|
+
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 training; older versions are not measured again and show not_measured for it. 409 BENCHMARK_ALREADY_ATTACHED or BENCHMARK_RETIRED.", {
|
|
3412
3549
|
pipeline_id: z.string(),
|
|
3413
3550
|
benchmark_id: z.string(),
|
|
3414
3551
|
priority: z.number().int().min(1).optional(),
|
|
@@ -3430,69 +3567,73 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3430
3567
|
}, async ({ pipeline_id, benchmark_id }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}/benchmarks/${encodeURIComponent(benchmark_id)}`, {
|
|
3431
3568
|
method: "DELETE",
|
|
3432
3569
|
})));
|
|
3433
|
-
server.tool("loop_compare_versions", "A pipeline's versions side by side: each version's
|
|
3570
|
+
server.tool("loop_compare_versions", "A pipeline's versions side by side: each version's result against what it was tested against (the live version then, or the original model before v1) and its score on every benchmark, each with the share of questions each side passed (candidate_pass_rate for the version, incumbent_pass_rate for what it was tested against; score 0.70 or more), with comparable flags. Pass a and b (VERSION numbers, e.g. 1 and 5) for those two and a difference per benchmark; a benchmark attached after a version is not_measured for it, never a zero. Each benchmark says how it scores an answer (check: judge or exact). Compare only scores marked comparable. Each version also says promoted_at, is_live, rolled_back, model_id and model_role (main or second), cost_cents, cost_up_to and gpu_seconds (its training's, as its entry in the list of trainings says them), train_rows, learned_new (the samples it learned that the versions it builds on had not: new plus carried from trainings that did not become a version; null on an older training), compared_with_version and compared_with (what its automatic test asked as the opponent), and holdout.common_pass_rate with holdout.common_rows: how much of common_set -- the samples kept aside for the automatic test before the first tested version (common_set.from), each once, read from stored answers graded against the same right answer -- it got right. common_rows is how many of the set that version's test graded: a sample deleted or past its retention before a later version's test was never asked by it, so say each version's own common_rows unless every version's equals common_set.rows. Use common_pass_rate for 'how has it grown from v1 to now'; each version's own candidate_pass_rate was measured on its own test and is not one line across versions. Each version says whether it was tested (tested): nothing untested goes live any more, and false marks an older version that went live before that held.", {
|
|
3434
3571
|
pipeline_id: z.string(),
|
|
3435
3572
|
a: z.number().int().min(1).optional().describe("A version number"),
|
|
3436
3573
|
b: z.number().int().min(1).optional().describe("Another version number"),
|
|
3437
3574
|
}, async ({ pipeline_id, a, b }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}/versions/compare`, {
|
|
3438
3575
|
params: { a: a?.toString(), b: b?.toString() },
|
|
3439
3576
|
})));
|
|
3440
|
-
server.tool("loop_compare_attempts", "Any two
|
|
3577
|
+
server.tool("loop_compare_attempts", "Any two trainings of a pipeline side by side, whether they went live or not -- the one to use for 'why was training #8 not better than #7' or the second model's training against the main model's training whose samples it learned from. a and b are run ids (attempts[].run_id from loop_get_pipeline), two different trainings of this pipeline. Each side has its outcome, decision, its 'lineage' (what it was built on and learned from: 'training #3 builds on v1 and learned 2,000 samples' beside 'training #4 builds on v2 and learned 1,000' is often the difference that matters) and automatic-test numbers against what it was tested against on its day (the live version then, or the original model before v1), with the share each side passed (score 0.70 or more); never subtract two trainings' win_rate figures, the versions they were tested against differ. benchmarks lists every benchmark both were tested on, with delta (b minus a) only where comparable, why otherwise. shared_sample counts the automatic-test questions 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 training's mean and pass rate over those same questions -- the like-for-like figures, which may be subtracted; each side's 'holdout' numbers are over its own automatic test, which grows between trainings, so do not subtract those. When shared_sample.comparable is false the two were scored differently (another judge model, holdout.judge_model, or another rubric) or how one was scored was not recorded (an older training that froze no rubric is never taken to match): say why in its words, and never read a direction off them. A question whose right answer changed between the two trainings 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.", {
|
|
3441
3578
|
pipeline_id: z.string(),
|
|
3442
|
-
a: z.string().describe("The first
|
|
3443
|
-
b: z.string().describe("The second
|
|
3579
|
+
a: z.string().describe("The first training's run id"),
|
|
3580
|
+
b: z.string().describe("The second training's run id; directions say b against a"),
|
|
3444
3581
|
filter: z.enum(["higher", "lower", "same"]).optional().describe("Only the conversations where b scored higher, lower, or the same"),
|
|
3445
3582
|
limit: z.number().int().min(1).max(200).optional().describe("Conversations per page, 50 when absent"),
|
|
3446
3583
|
offset: z.number().int().min(0).optional(),
|
|
3447
3584
|
}, async ({ pipeline_id, a, b, filter, limit, offset }) => json(await client.api(`/api/loop/pipelines/${encodeURIComponent(pipeline_id)}/attempts/compare`, {
|
|
3448
3585
|
params: { a, b, filter, limit: limit?.toString(), offset: offset ? offset.toString() : undefined },
|
|
3449
3586
|
})));
|
|
3450
|
-
server.tool("loop_promote_training_run", "
|
|
3587
|
+
server.tool("loop_promote_training_run", "Put a training live: point the pipeline's model name at the newly trained model, which becomes the next version and the live version -- from this moment the workspace's traffic is answered by it. Read its test back to the user first (loop_get_evaluation) and get an explicit yes — this changes what real customers get, and it can only be undone for 30 days, with loop_rollback_training_run. A test that COULD NOT DECIDE ('inconclusive') is refused unless force is set, and forcing one means putting live a model nothing showed to be better; say that out loud rather than setting force to get past the error. A training that was never tested (tested false on loop_get_training_run: its test measured nothing) is refused NOT_TESTED, forced or not -- nothing untested goes live; offer loop_retest_training_run instead.", {
|
|
3451
3588
|
run_id: z.string(),
|
|
3452
|
-
expected_revision: z.number().int().min(0).optional().describe("The pipeline revision the decision was made against. Stale answers CONSENT_CHANGED instead of
|
|
3453
|
-
force: z.boolean().optional().describe("
|
|
3589
|
+
expected_revision: z.number().int().min(0).optional().describe("The pipeline revision the decision was made against. Stale answers CONSENT_CHANGED instead of putting it live under settings that have since changed."),
|
|
3590
|
+
force: z.boolean().optional().describe("Put it live even though its test could not decide. Only ever after the user has been told the test did not show the new model was better."),
|
|
3454
3591
|
}, async ({ run_id, expected_revision, force }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/promote`, {
|
|
3455
3592
|
method: "POST",
|
|
3456
3593
|
body: { expected_revision, force },
|
|
3457
3594
|
})));
|
|
3458
|
-
server.tool("loop_reject_training_run", "
|
|
3595
|
+
server.tool("loop_reject_training_run", "Turn a training down: retire the newly trained model and leave the live version exactly where it is. This is the cheap decision: the new model's machine stops being paid for. The trained model's saved copy is kept, so a rejection is not a deletion.", {
|
|
3459
3596
|
run_id: z.string(),
|
|
3460
|
-
reason: z.string().optional().describe("Why, in the user's own words. It goes on the
|
|
3597
|
+
reason: z.string().optional().describe("Why, in the user's own words. It goes on the training's timeline, which is what somebody reads six weeks later."),
|
|
3461
3598
|
}, async ({ run_id, reason }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/reject`, {
|
|
3462
3599
|
method: "POST",
|
|
3463
3600
|
body: { reason },
|
|
3464
3601
|
})));
|
|
3465
|
-
server.tool("loop_rollback_training_run", "
|
|
3602
|
+
server.tool("loop_rollback_training_run", "Go back to the version before: point the pipeline's model name back at what answered before this version went live. Available for 30 days, after which the answer is ROLLBACK_WINDOW_CLOSED and the way back is a new training. 409 RUN_ACTIVE: another training is being tested against this version right now, or builds on it (its lineage.built_on names this version); go back once it finishes, or cancel that training with loop_cancel_training_run first. 409 ROLLBACK_DELETED_DATA: the version it would put back learned from a conversation a member deleted since, so it never answers again; a rebuild without it replaces it. Where the pipeline builds on its live version, the next training builds on the version put back and learns again what the version it replaced learned -- when the version put back is on the pipeline's model now; one on the model before a second model took over cannot be built on, so the next training trains the pipeline's model from its original on every sample. Tell the user what the name will be serving once this lands — the previous target is named on the training.", {
|
|
3466
3603
|
run_id: z.string(),
|
|
3467
|
-
reason: z.string().optional().describe("Why. It goes on the
|
|
3604
|
+
reason: z.string().optional().describe("Why. It goes on the training's timeline."),
|
|
3468
3605
|
}, async ({ run_id, reason }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/rollback`, {
|
|
3469
3606
|
method: "POST",
|
|
3470
3607
|
body: { reason },
|
|
3471
3608
|
})));
|
|
3472
|
-
server.tool("loop_cancel_training_run", "Stop a
|
|
3609
|
+
server.tool("loop_cancel_training_run", "Stop a training that is still running. The training is stopped, the newly trained model is torn down and nothing further is spent on this training. What has already been spent is already spent — cancelling is not a refund, and saying so is more honest than letting the user discover it on the invoice.", {
|
|
3473
3610
|
run_id: z.string(),
|
|
3474
|
-
reason: z.string().optional().describe("Why. It goes on the
|
|
3611
|
+
reason: z.string().optional().describe("Why. It goes on the training's timeline."),
|
|
3475
3612
|
}, async ({ run_id, reason }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/cancel`, {
|
|
3476
3613
|
method: "POST",
|
|
3477
3614
|
body: { reason },
|
|
3478
3615
|
})));
|
|
3479
|
-
server.tool("
|
|
3480
|
-
|
|
3616
|
+
server.tool("loop_retest_training_run", "Test again: a training that finished training but was never tested -- no machine was free to test it, or its test could not score a single question (tested false, available_actions.retest true on loop_get_training_run) -- has its test booked again, on the same training. It is tested against what is live now, on the questions kept aside for its automatic test and with its benchmark list, and spends what is left of the amount set aside for testing it, never a second amount. 202 with the training back in checkpoint_ready; asking again while that test is under way changes nothing. 409 CANNOT_TEST_AGAIN says in one plain sentence why it cannot be (already tested, went live, still training, its trained model deleted, the money for testing it spent, ...): read it back as written. Train now's own refusals apply too: PIPELINE_NEEDS_SAVE, RULE_PAUSED, RUN_ACTIVE, MONTHLY_LIMIT_REACHED and SCORING_CAPPED (both with resumes_at), AGENT_OFF. Testing costs money: say so before asking.", { run_id: z.string() }, async ({ run_id }) => json(await client.api(`/api/loop/training-runs/${encodeURIComponent(run_id)}/retest`, {
|
|
3617
|
+
method: "POST",
|
|
3618
|
+
body: {},
|
|
3619
|
+
})));
|
|
3620
|
+
server.tool("loop_get_evaluation", "The automatic test behind a training's result: how many questions were scored, win_rate (of the questions where the two answers scored differently, the share where the new model's scored higher), how many questions each side got right (score 0.70 or more: candidate_passed for the new model, incumbent_passed for what it was tested against: the live version then, or the original model before v1; and the pass rates over rows_scored, all four null until the report is finished -- say it as 'right on 31 of 40; v4 got 28 of 40 on the same questions', or before v1 'right on 31 of 40; the original model got 28 of 40 on the same questions'), the mean scores per rubric dimension, the warnings, and the margins the result was measured against. Read the WARNINGS out too, not just the result — 'better' from a judge that rarely agrees with this workspace's reviewers, or from a handful of questions, is a number with a caveat attached, and the caveat is in that list.", { evaluation_id: z.string().describe("From the training's links, or loop_get_training_run") }, async ({ evaluation_id }) => json(await client.api(`/api/loop/evaluations/${encodeURIComponent(evaluation_id)}`)));
|
|
3621
|
+
server.tool("loop_list_evaluation_items", "The automatic test question by question: the same prompt, the right answer a person gave for it (reference: answer, kind and the tool_calls it makes; null when none), what the live version answered, what the new model answered, each with the tool_calls it made, what the judge made of each (judge.overall, judge.dimensions and judge.reason, its reason in its own words) and whether each passed (score 0.70 or more), and pairwise: the two answers compared directly, asked in both orders, each with its reason, where one side counts as better only when both orders agree. This is how a person checks a result instead of trusting it. Filter 'winner' to 'serving' to read the questions the new model did WORSE on (the items themselves then say 'incumbent', the live version), which is the honest thing to show somebody before they put it live.", {
|
|
3481
3622
|
evaluation_id: z.string(),
|
|
3482
|
-
winner: z.enum(["candidate", "serving", "tie"]).optional().describe("Only
|
|
3483
|
-
candidate_passed: z.boolean().optional().describe("true: only
|
|
3484
|
-
incumbent_passed: z.boolean().optional().describe("true: only
|
|
3623
|
+
winner: z.enum(["candidate", "serving", "tie"]).optional().describe("Only questions with this outcome: 'candidate' (the new model did better), 'serving' (the live version did better) or 'tie'"),
|
|
3624
|
+
candidate_passed: z.boolean().optional().describe("true: only questions where the new model's answer passed (score 0.70 or more); false: only questions where it was scored below 0.70. With incumbent_passed it narrows to one cell: candidate_passed false and incumbent_passed true are the ones that got worse. A question that could not be scored matches neither"),
|
|
3625
|
+
incumbent_passed: z.boolean().optional().describe("true: only questions where the live version's answer passed (score 0.70 or more); false: only questions where it was scored below 0.70"),
|
|
3485
3626
|
orders: z.enum(["agreed", "disagreed"]).optional().describe("Only pairs whose two side-by-side orders picked the same side (agreed), or did not"),
|
|
3486
3627
|
has_reference: z.boolean().optional().describe("Only pairs with (true) or without (false) a right answer"),
|
|
3487
3628
|
status: z.enum(["scored", "failed"]).optional(),
|
|
3488
|
-
sort: z.enum(["ordinal", "score_change"]).optional().describe("score_change: the
|
|
3489
|
-
q: z.string().min(2).max(200).optional().describe("Search the conversation (not the system prompt every
|
|
3629
|
+
sort: z.enum(["ordinal", "score_change"]).optional().describe("score_change: the new model's score less the live version's, largest first"),
|
|
3630
|
+
q: z.string().min(2).max(200).optional().describe("Search the conversation (not the system prompt every question shares), the right answer, both answers and every reason (case-insensitive); searched in the answer says how many pairs were read. A search can be refused 429 RATE_LIMITED while this workspace already has a search running or while the service is running as many searches as it can at once, so a 429 does not say which: never tell the user they already had one running; try again after the seconds its message gives"),
|
|
3490
3631
|
limit: z.number().int().min(1).max(200).optional().describe("50 when omitted, at most 200"),
|
|
3491
3632
|
offset: z.number().int().min(0).optional(),
|
|
3492
3633
|
}, 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`, {
|
|
3493
3634
|
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() },
|
|
3494
3635
|
})));
|
|
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
|
|
3636
|
+
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 training 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.", {
|
|
3496
3637
|
name: z.string(),
|
|
3497
3638
|
source: z.object({
|
|
3498
3639
|
kind: z.enum(["traces"]).describe("Always 'traces': a benchmark pins the conversations named in trace_ids"),
|
|
@@ -3518,31 +3659,40 @@ create (1-5 choices), and the queue itself stays opt-in.`,
|
|
|
3518
3659
|
}, async ({ benchmark_id, limit, offset }) => json(await client.api(`/api/loop/benchmarks/${encodeURIComponent(benchmark_id)}/items`, {
|
|
3519
3660
|
params: { limit: limit?.toString(), offset: offset?.toString() },
|
|
3520
3661
|
})));
|
|
3521
|
-
server.tool("loop_get_benchmark_history", "Every score a benchmark has produced, newest first: the trend line. Each point names the attempt_no of its pipeline it measured and the version that
|
|
3662
|
+
server.tool("loop_get_benchmark_history", "Every score a benchmark has produced, newest first: the trend line. Each point names the training number (attempt_no) of its pipeline it measured and the version that training became (null when it did not become one); name points by those ('training #6, v5'), never by run_seq, which counts every benchmark run in the workspace. A benchmark run that could not score the whole set has null scores and a status_reason — report the reason, never a number for it. pipeline keeps only the points of one pipeline's trainings, so the trend reads by training number; without it the points of every pipeline using the benchmark are mixed.", {
|
|
3522
3663
|
benchmark_id: z.string(),
|
|
3523
|
-
pipeline: z.string().optional().describe("Only this pipeline's
|
|
3664
|
+
pipeline: z.string().optional().describe("Only this pipeline's trainings (its id, from loop_list_pipelines)"),
|
|
3524
3665
|
limit: z.number().int().min(1).max(200).optional(),
|
|
3525
3666
|
offset: z.number().int().min(0).optional(),
|
|
3526
3667
|
}, async ({ benchmark_id, pipeline, limit, offset }) => json(await client.api(`/api/loop/benchmarks/${encodeURIComponent(benchmark_id)}/history`, {
|
|
3527
3668
|
params: { pipeline: pipeline || undefined, limit: limit?.toString(), offset: offset?.toString() },
|
|
3528
3669
|
})));
|
|
3529
|
-
server.tool("loop_retire_benchmark", "Stop
|
|
3670
|
+
server.tool("loop_retire_benchmark", "Stop testing on a benchmark and detach it from every pipeline. Nothing it measured is removed, and its name is free for a successor. It cannot be un-retired; confirm first.", {
|
|
3530
3671
|
benchmark_id: z.string(),
|
|
3531
3672
|
reason: z.string().optional(),
|
|
3532
3673
|
}, async ({ benchmark_id, reason }) => json(await client.api(`/api/loop/benchmarks/${encodeURIComponent(benchmark_id)}/retire`, {
|
|
3533
3674
|
method: "POST",
|
|
3534
3675
|
body: { reason },
|
|
3535
3676
|
})));
|
|
3536
|
-
server.tool("loop_get_benchmark_run", "One
|
|
3537
|
-
server.tool("
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3677
|
+
server.tool("loop_get_benchmark_run", "One benchmark run in full (one training tested on one benchmark): both scores, the difference, the per-dimension breakdown, how many questions each side passed (score 0.70 or more) and the pass rates, the questions the new model did better, worse and the same on (wins, losses, ties), learned_by_version (how many of these conversations the version under test learned -- trained on itself or learned by a version it was built on; when it is above 0, say \"N of these conversations were learned by this version\", since its score on them is partly recall; null for a version from before versions built on each other: say nothing then), and what it cost. Read status first — a benchmark run that did not score every pinned question publishes no score, and its pass counts, rates and wins are null too. loop_list_benchmark_run_items reads every conversation behind it.", { benchmark_run_id: z.string() }, async ({ benchmark_run_id }) => json(await client.api(`/api/loop/benchmark-runs/${encodeURIComponent(benchmark_run_id)}`)));
|
|
3678
|
+
server.tool("loop_benchmark_questions", "Every question of a benchmark with how one pipeline's recent trainings did on it. trainings lists them oldest first ('attempt' is the training's number, version the version it became or null, status its benchmark result's status: anything but done scored only some questions), and each question's results follow that order with passed (right: a score of 0.70 or more) and score, both null where that training did not score the question. Use it to find the questions a new version got wrong that the one before got right, and say them by question, not by id. It writes nothing.", {
|
|
3679
|
+
benchmark_id: z.string().describe("The benchmark's id, from loop_list_benchmarks or loop_list_pipeline_benchmarks"),
|
|
3680
|
+
pipeline: z.string().describe("The pipeline whose trainings to show (its id, from loop_list_pipelines): training numbers are per pipeline"),
|
|
3681
|
+
trainings: z.number().int().min(1).max(20).optional().describe("How many of its recent trainings, 5 when absent, at most 20"),
|
|
3682
|
+
limit: z.number().int().min(1).max(200).optional().describe("Questions per page, 50 when absent, at most 200"),
|
|
3683
|
+
offset: z.number().int().min(0).optional(),
|
|
3684
|
+
}, async ({ benchmark_id, pipeline, trainings, limit, offset }) => json(await client.api(`/api/loop/benchmarks/${encodeURIComponent(benchmark_id)}/questions`, {
|
|
3685
|
+
params: { pipeline, trainings: trainings?.toString(), limit: limit?.toString(), offset: offset?.toString() },
|
|
3686
|
+
})));
|
|
3687
|
+
server.tool("loop_list_benchmark_run_items", "One benchmark run question by question, in the benchmark's order: the pinned prompt and its right answer (reference: answer, kind and the tool_calls the right answer makes; null when none), what the live version answered, what the new model answered, each with the tool_calls it made, what the judge made of each (judge.overall, judge.dimensions and judge.reason; on an exact benchmark overall and dimensions.match are 1 or 0 and the reason says what matched or what differed) and whether each passed (score 0.70 or more), and which side did better ('winner' compares the two scores by the tie band going live uses: 0.02 or more above counts as higher, less than 0.02 apart is a tie, and it is null until both are scored). Filter 'winner' to 'serving' to read the questions the new model did WORSE on (the items themselves then say 'incumbent', the live version). A benchmark run that stopped before it began has no questions: read its status_reason back instead.", {
|
|
3688
|
+
benchmark_run_id: z.string().describe("The benchmark run's id: from loop_get_benchmark_history, loop_compare_versions, or a training's benchmark_runs"),
|
|
3689
|
+
winner: z.enum(["candidate", "serving", "tie"]).optional().describe("Only questions with this outcome: 'candidate' (the new model did better), 'serving' (the live version did better) or 'tie'"),
|
|
3690
|
+
candidate_passed: z.boolean().optional().describe("true: only questions where the new model's answer passed (score 0.70 or more); false: only ones where it was scored below 0.70. With incumbent_passed it narrows to one cell: candidate_passed false and incumbent_passed true are the ones that got worse. One that could not be scored matches neither"),
|
|
3691
|
+
incumbent_passed: z.boolean().optional().describe("true: only questions where the live version's answer passed (score 0.70 or more); false: only ones where it was scored below 0.70"),
|
|
3542
3692
|
has_reference: z.boolean().optional().describe("Only conversations with (true) or without (false) a right answer"),
|
|
3543
3693
|
status: z.enum(["scored", "failed"]).optional(),
|
|
3544
|
-
sort: z.enum(["ordinal", "score_change"]).optional().describe("score_change: the
|
|
3545
|
-
q: z.string().min(2).max(200).optional().describe("Search the conversation (not the system prompt every
|
|
3694
|
+
sort: z.enum(["ordinal", "score_change"]).optional().describe("score_change: the new model's score less the live version's, largest first"),
|
|
3695
|
+
q: z.string().min(2).max(200).optional().describe("Search the conversation (not the system prompt every question shares), the right answer, both answers and every reason (case-insensitive); searched in the answer says how many were read. A search can be refused 429 RATE_LIMITED while this workspace already has a search running or while the service is running as many searches as it can at once, so a 429 does not say which: never tell the user they already had one running; try again after the seconds its message gives"),
|
|
3546
3696
|
limit: z.number().int().min(1).max(200).optional().describe("50 when omitted, at most 200"),
|
|
3547
3697
|
offset: z.number().int().min(0).optional(),
|
|
3548
3698
|
}, async ({ benchmark_run_id, winner, candidate_passed, incumbent_passed, has_reference, status, sort, q, limit, offset }) => json(await client.api(`/api/loop/benchmark-runs/${encodeURIComponent(benchmark_run_id)}/items`, {
|