@runtypelabs/sdk 5.5.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1887,218 +1887,355 @@ function resolveBatchExecutionId(pausedTools) {
1887
1887
  return "";
1888
1888
  }
1889
1889
 
1890
- // src/flows-ensure.ts
1891
- function isPlainObject(value) {
1892
- return value !== null && typeof value === "object" && !Array.isArray(value);
1893
- }
1894
- function normalizeConfigForHash(config) {
1895
- if (!isPlainObject(config)) return {};
1896
- const normalized = {};
1897
- for (const key of Object.keys(config).sort()) {
1898
- const value = config[key];
1899
- if (value === void 0) continue;
1900
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1901
- normalized[key] = normalizeConfigForHash(value);
1902
- } else if (Array.isArray(value)) {
1903
- normalized[key] = value.map((item) => {
1904
- if (item !== null && typeof item === "object" && !Array.isArray(item)) {
1905
- return normalizeConfigForHash(item);
1890
+ // src/evals-ensure.ts
1891
+ var CHECK_GRADER_KINDS = /* @__PURE__ */ new Set([
1892
+ "contains",
1893
+ "not_contains",
1894
+ "matches_expected",
1895
+ "regex",
1896
+ "valid_json",
1897
+ "json_field",
1898
+ "length",
1899
+ "latency",
1900
+ "no_error",
1901
+ // Trace checks.
1902
+ "called_tool",
1903
+ "not_called_tool",
1904
+ "used_no_tools",
1905
+ "max_tool_calls",
1906
+ "tool_order",
1907
+ "ran_step",
1908
+ "step_order",
1909
+ "completed",
1910
+ "cost"
1911
+ ]);
1912
+ function gradeable(data) {
1913
+ const obj = { ...data };
1914
+ const rebuild = (patch) => gradeable({ ...obj, ...patch });
1915
+ Object.defineProperty(obj, "gate", {
1916
+ value: () => rebuild({ severity: "gate" }),
1917
+ enumerable: false
1918
+ });
1919
+ Object.defineProperty(obj, "soft", {
1920
+ value: () => rebuild({ severity: "soft" }),
1921
+ enumerable: false
1922
+ });
1923
+ if (data.kind === "ai") {
1924
+ Object.defineProperty(obj, "atLeast", {
1925
+ value: (threshold) => {
1926
+ if (typeof threshold !== "number" || !Number.isFinite(threshold) || threshold < 1 || threshold > 5) {
1927
+ throw new Error("atLeast() requires a numeric judge threshold between 1 and 5");
1906
1928
  }
1907
- return item;
1908
- });
1909
- } else {
1910
- normalized[key] = value;
1911
- }
1929
+ return rebuild({ threshold });
1930
+ },
1931
+ enumerable: false
1932
+ });
1912
1933
  }
1913
- return normalized;
1934
+ return obj;
1914
1935
  }
1915
- function normalizeStepForHash(step) {
1916
- const stepObj = isPlainObject(step) ? step : {};
1917
- return {
1918
- type: typeof stepObj.type === "string" ? stepObj.type : "",
1919
- name: typeof stepObj.name === "string" ? stepObj.name : "",
1920
- enabled: stepObj.enabled !== false,
1921
- ...typeof stepObj.when === "string" ? { when: stepObj.when } : {},
1922
- config: normalizeConfigForHash(stepObj.config),
1923
- order: typeof stepObj.order === "number" ? stepObj.order : 0
1924
- };
1936
+ function contains(value, opts) {
1937
+ return gradeable({
1938
+ kind: "contains",
1939
+ value,
1940
+ ...opts?.caseSensitive ? { caseSensitive: true } : {}
1941
+ });
1925
1942
  }
1926
- async function computeFlowContentHash(steps) {
1927
- const normalized = [...steps].sort((a, b) => {
1928
- const orderA = isPlainObject(a) && typeof a.order === "number" ? a.order : 0;
1929
- const orderB = isPlainObject(b) && typeof b.order === "number" ? b.order : 0;
1930
- return orderA - orderB;
1931
- }).map(normalizeStepForHash);
1932
- const serialized = JSON.stringify(normalized);
1933
- const encoded = new TextEncoder().encode(serialized);
1934
- const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
1935
- return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
1943
+ function notContains(value, opts) {
1944
+ return gradeable({
1945
+ kind: "not_contains",
1946
+ value,
1947
+ ...opts?.caseSensitive ? { caseSensitive: true } : {}
1948
+ });
1936
1949
  }
1937
- var DEFINE_FLOW_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["name", "steps"]);
1938
- var DEFINE_FLOW_STEP_KEYS = /* @__PURE__ */ new Set([
1939
- "type",
1940
- "name",
1941
- "order",
1942
- "enabled",
1943
- "when",
1944
- "config"
1945
- ]);
1946
- function collectStepNonPortableToolRefs(config, path) {
1947
- const found = [];
1948
- const tools = config.tools;
1949
- const isAccountScoped = (ref) => typeof ref === "string" && ref.startsWith("tool_");
1950
- const isRawId = (ref, prefix) => typeof ref === "string" && ref.startsWith(prefix);
1951
- const scanArray = (value, subPath) => {
1952
- if (!Array.isArray(value)) return;
1953
- value.forEach((ref, i) => {
1954
- if (isAccountScoped(ref)) found.push(`${subPath}[${i}]`);
1955
- });
1956
- };
1957
- const scanKeys = (value, subPath) => {
1958
- if (!isPlainObject(value)) return;
1959
- for (const key of Object.keys(value)) {
1960
- if (isAccountScoped(key)) found.push(`${subPath}.${key}`);
1961
- }
1962
- };
1963
- if (isPlainObject(tools)) {
1964
- scanArray(tools.toolIds, `${path}.tools.toolIds`);
1965
- scanKeys(tools.toolConfigs, `${path}.tools.toolConfigs`);
1966
- scanKeys(tools.perToolLimits, `${path}.tools.perToolLimits`);
1967
- if (isPlainObject(tools.approval)) {
1968
- scanArray(tools.approval.require, `${path}.tools.approval.require`);
1969
- }
1970
- if (isPlainObject(tools.subagentConfig)) {
1971
- scanArray(tools.subagentConfig.toolPool, `${path}.tools.subagentConfig.toolPool`);
1972
- }
1973
- if (isPlainObject(tools.codeModeConfig)) {
1974
- scanArray(tools.codeModeConfig.toolPool, `${path}.tools.codeModeConfig.toolPool`);
1975
- }
1976
- if (Array.isArray(tools.runtimeTools)) {
1977
- tools.runtimeTools.forEach((runtimeTool, i) => {
1978
- if (!isPlainObject(runtimeTool) || !isPlainObject(runtimeTool.config)) return;
1979
- const base = `${path}.tools.runtimeTools[${i}].config`;
1980
- const rtConfig = runtimeTool.config;
1981
- if (runtimeTool.toolType === "subagent" && isRawId(rtConfig.agentId, "agent_")) {
1982
- found.push(`${base}.agentId`);
1983
- } else if (runtimeTool.toolType === "flow" && isRawId(rtConfig.flowId, "flow_")) {
1984
- found.push(`${base}.flowId`);
1985
- }
1986
- });
1987
- }
1988
- }
1989
- if (isAccountScoped(config.toolId)) {
1990
- found.push(`${path}.toolId`);
1991
- }
1992
- if (isRawId(config.agentId, "agent_")) {
1993
- found.push(`${path}.agentId`);
1950
+ function matchesExpected() {
1951
+ return gradeable({ kind: "matches_expected" });
1952
+ }
1953
+ function regex(pattern, flags) {
1954
+ return gradeable({ kind: "regex", pattern, ...flags ? { flags } : {} });
1955
+ }
1956
+ function validJson() {
1957
+ return gradeable({ kind: "valid_json" });
1958
+ }
1959
+ function jsonField(path, opts) {
1960
+ return gradeable({
1961
+ kind: "json_field",
1962
+ path,
1963
+ ...opts && "equals" in opts && opts.equals !== void 0 ? { equals: opts.equals } : {},
1964
+ ...opts && typeof opts.exists === "boolean" ? { exists: opts.exists } : {}
1965
+ });
1966
+ }
1967
+ function length(opts) {
1968
+ if (!opts || opts.minChars === void 0 && opts.maxChars === void 0) {
1969
+ throw new Error("length() requires at least one of minChars or maxChars");
1994
1970
  }
1995
- for (const branch of ["trueSteps", "falseSteps"]) {
1996
- const nested = config[branch];
1997
- if (!Array.isArray(nested)) continue;
1998
- nested.forEach((nestedStep, i) => {
1999
- if (isPlainObject(nestedStep) && isPlainObject(nestedStep.config)) {
2000
- found.push(
2001
- ...collectStepNonPortableToolRefs(
2002
- nestedStep.config,
2003
- `${path}.${branch}[${i}].config`
2004
- )
2005
- );
2006
- }
2007
- });
1971
+ return gradeable({
1972
+ kind: "length",
1973
+ ...opts.minChars !== void 0 ? { minChars: opts.minChars } : {},
1974
+ ...opts.maxChars !== void 0 ? { maxChars: opts.maxChars } : {}
1975
+ });
1976
+ }
1977
+ function latency(maxMs) {
1978
+ if (!Number.isFinite(maxMs) || maxMs <= 0) {
1979
+ throw new Error("latency() requires a positive maxMs");
2008
1980
  }
2009
- return found;
1981
+ return gradeable({ kind: "latency", maxMs });
2010
1982
  }
2011
- function defineFlow(input) {
2012
- if (!input || typeof input !== "object") {
2013
- throw new Error("defineFlow requires a definition object");
1983
+ function noError() {
1984
+ return gradeable({ kind: "no_error" });
1985
+ }
1986
+ function calledTool(name, opts) {
1987
+ if (typeof name !== "string" || name.length === 0) {
1988
+ throw new Error("calledTool() requires a non-empty tool name");
2014
1989
  }
2015
- if (typeof input.name !== "string" || input.name.length === 0) {
2016
- throw new Error('defineFlow requires a non-empty string "name"');
1990
+ if (opts?.times !== void 0 && (!Number.isInteger(opts.times) || opts.times <= 0)) {
1991
+ throw new Error('calledTool() "times" must be a positive integer');
2017
1992
  }
2018
- const unknownKeys = Object.keys(input).filter((key) => !DEFINE_FLOW_TOP_LEVEL_KEYS.has(key));
2019
- if (unknownKeys.length > 0) {
2020
- throw new Error(
2021
- `defineFlow: unknown field(s): ${unknownKeys.join(", ")}. Allowed fields are name and steps. (Description is not part of the v1 ensure surface.)`
2022
- );
1993
+ return gradeable({
1994
+ kind: "called_tool",
1995
+ name,
1996
+ ...opts && "input" in opts && opts.input !== void 0 ? { input: opts.input } : {},
1997
+ ...opts && "output" in opts && opts.output !== void 0 ? { output: opts.output } : {},
1998
+ ...opts && typeof opts.isError === "boolean" ? { isError: opts.isError } : {},
1999
+ ...opts?.times !== void 0 ? { times: opts.times } : {}
2000
+ });
2001
+ }
2002
+ function notCalledTool(name) {
2003
+ if (typeof name !== "string" || name.length === 0) {
2004
+ throw new Error("notCalledTool() requires a non-empty tool name");
2023
2005
  }
2024
- if (!Array.isArray(input.steps) || input.steps.length === 0) {
2025
- throw new Error('defineFlow requires a non-empty "steps" array');
2006
+ return gradeable({ kind: "not_called_tool", name });
2007
+ }
2008
+ function usedNoTools() {
2009
+ return gradeable({ kind: "used_no_tools" });
2010
+ }
2011
+ function maxToolCalls(max) {
2012
+ if (!Number.isInteger(max) || max < 0) {
2013
+ throw new Error("maxToolCalls() requires a non-negative integer");
2026
2014
  }
2027
- const steps = input.steps.map((step, index) => {
2028
- if (!isPlainObject(step)) {
2029
- throw new Error(`defineFlow: steps[${index}] must be an object`);
2030
- }
2031
- if (typeof step.type !== "string" || step.type.length === 0) {
2032
- throw new Error(`defineFlow: steps[${index}] requires a non-empty string "type"`);
2033
- }
2034
- if (typeof step.name !== "string" || step.name.length === 0) {
2035
- throw new Error(`defineFlow: steps[${index}] requires a non-empty string "name"`);
2036
- }
2037
- const unknownStepKeys = Object.keys(step).filter((key) => !DEFINE_FLOW_STEP_KEYS.has(key));
2038
- if (unknownStepKeys.length > 0) {
2039
- throw new Error(
2040
- `defineFlow: steps[${index}] has unknown field(s): ${unknownStepKeys.join(", ")}. Allowed step fields are type, name, order, enabled, when, config. (Step ids are server artifacts and not part of a portable definition.)`
2041
- );
2042
- }
2043
- const config = isPlainObject(step.config) ? step.config : void 0;
2044
- if (config) {
2045
- const nonPortable = collectStepNonPortableToolRefs(config, `steps[${index}].config`);
2046
- if (nonPortable.length > 0) {
2047
- throw new Error(
2048
- `defineFlow: account-scoped reference(s) at ${nonPortable.join(", ")}. Definitions must be environment-portable \u2014 tool_\u2026/agent_\u2026/flow_\u2026 IDs belong to one account/environment. Use builtin:/platform:/mcp: references, or reference a saved resource by name \u2014 tool:<name>, agent:<name>, or flow:<name> instead.`
2049
- );
2050
- }
2051
- }
2052
- return {
2053
- type: step.type,
2054
- name: step.name,
2055
- // Explicit 1-based order (the flow builder's convention) so the local
2056
- // probe hash agrees with the server's persisted step order.
2057
- order: typeof step.order === "number" ? step.order : index + 1,
2058
- ...step.enabled !== void 0 ? { enabled: step.enabled } : {},
2059
- ...typeof step.when === "string" ? { when: step.when } : {},
2060
- ...config ? { config } : {}
2061
- };
2062
- });
2063
- return { name: input.name, steps };
2015
+ return gradeable({ kind: "max_tool_calls", max });
2064
2016
  }
2065
- var FlowEnsureConflictError = class extends Error {
2066
- constructor(body) {
2067
- super(body.error ?? `Flow ensure conflict: ${body.code}`);
2068
- this.name = "FlowEnsureConflictError";
2069
- this.code = body.code;
2070
- this.lastModifiedSource = body.lastModifiedSource;
2071
- this.modifiedAt = body.modifiedAt;
2072
- this.currentHash = body.currentHash;
2017
+ function toolOrder(tools) {
2018
+ if (!Array.isArray(tools) || tools.length === 0) {
2019
+ throw new Error("toolOrder() requires a non-empty array of tool names");
2073
2020
  }
2074
- };
2075
- var FlowDriftError = class extends Error {
2076
- constructor(plan) {
2077
- super(
2078
- `Flow "${plan.flowId ?? "definition"}" drifted: plan is '${plan.changes}' (changed: ${plan.changedKeys.join(", ") || "n/a"}). Run client.flows.pull(name) to absorb the remote edit into your repo, or re-run ensure to converge.`
2079
- );
2080
- this.name = "FlowDriftError";
2081
- this.plan = plan;
2021
+ return gradeable({ kind: "tool_order", tools });
2022
+ }
2023
+ function ranStep(name) {
2024
+ if (typeof name !== "string" || name.length === 0) {
2025
+ throw new Error("ranStep() requires a non-empty step name");
2082
2026
  }
2083
- };
2084
- function parseRequestError(err) {
2085
- if (!(err instanceof Error)) return { status: null, body: null };
2086
- const match = err.message.match(/^API request failed: (\d{3}) .*? - ([\s\S]*)$/);
2087
- if (!match) return { status: null, body: null };
2088
- try {
2089
- return { status: Number(match[1]), body: JSON.parse(match[2]) };
2090
- } catch {
2091
- return { status: Number(match[1]), body: null };
2027
+ return gradeable({ kind: "ran_step", name });
2028
+ }
2029
+ function stepOrder(steps) {
2030
+ if (!Array.isArray(steps) || steps.length === 0) {
2031
+ throw new Error("stepOrder() requires a non-empty array of step names");
2092
2032
  }
2033
+ return gradeable({ kind: "step_order", steps });
2093
2034
  }
2094
- function toConflictError(err) {
2095
- const { status, body } = parseRequestError(err);
2096
- if (status !== 409 || !isPlainObject(body)) return null;
2097
- const code = body.code;
2098
- if (code !== "external_modification" && code !== "remote_changed") return null;
2099
- return new FlowEnsureConflictError(
2100
- body
2101
- );
2035
+ function completed() {
2036
+ return gradeable({ kind: "completed" });
2037
+ }
2038
+ function cost(maxUsd) {
2039
+ if (!Number.isFinite(maxUsd) || maxUsd <= 0) {
2040
+ throw new Error("cost() requires a positive maxUsd");
2041
+ }
2042
+ return gradeable({ kind: "cost", maxUsd });
2043
+ }
2044
+ function judge(criteria, opts) {
2045
+ if (typeof criteria !== "string" || criteria.trim().length === 0) {
2046
+ throw new Error("judge() requires non-empty criteria");
2047
+ }
2048
+ return gradeable({
2049
+ kind: "ai",
2050
+ criteria,
2051
+ ...opts?.preset ? { preset: opts.preset } : {},
2052
+ ...opts?.useExpected ? { useExpected: true } : {},
2053
+ ...opts?.model ? { model: opts.model } : {},
2054
+ ...opts?.threshold !== void 0 ? { threshold: opts.threshold } : {}
2055
+ });
2056
+ }
2057
+ var judges = {
2058
+ answersQuestion: () => judge(
2059
+ "The response directly addresses what the user asked, without dodging or answering a different question.",
2060
+ { preset: "answersQuestion" }
2061
+ ),
2062
+ matchesExpected: () => judge(
2063
+ "The response conveys the same facts and conclusion as the expected answer. Wording may differ.",
2064
+ { preset: "matchesExpected", useExpected: true }
2065
+ ),
2066
+ followsInstructions: () => judge(
2067
+ "The response obeys every instruction in the system prompt (format, tone, constraints, refusals).",
2068
+ { preset: "followsInstructions" }
2069
+ ),
2070
+ grounded: () => judge(
2071
+ "Every factual claim in the response is supported by the provided context or the expected answer. Flag anything invented.",
2072
+ { preset: "grounded" }
2073
+ ),
2074
+ rightTone: (voice = "{describe the voice you want}") => judge(`The response matches this voice: ${voice}.`, { preset: "rightTone" }),
2075
+ safeToSend: () => judge(
2076
+ "The response contains nothing embarrassing to show a customer: no leaked internals, no hostile tone, no policy violations.",
2077
+ { preset: "safeToSend" }
2078
+ )
2079
+ };
2080
+ var DEFINE_EVAL_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
2081
+ "name",
2082
+ "target",
2083
+ "graders",
2084
+ "cases",
2085
+ "virtual"
2086
+ ]);
2087
+ var DEFINE_EVAL_CASE_KEYS = /* @__PURE__ */ new Set(["name", "input", "expected", "expect"]);
2088
+ function isPlainObject(value) {
2089
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2090
+ }
2091
+ function normalizeTarget(target) {
2092
+ if (!isPlainObject(target)) {
2093
+ throw new Error('defineEval requires a "target" object: { flow: name } or { agent: name }');
2094
+ }
2095
+ const hasFlow = typeof target.flow === "string" && target.flow.length > 0;
2096
+ const hasAgent = typeof target.agent === "string" && target.agent.length > 0;
2097
+ if (hasFlow === hasAgent) {
2098
+ throw new Error(
2099
+ 'defineEval "target" must name exactly one of flow or agent: { flow: "name" } XOR { agent: "name" }'
2100
+ );
2101
+ }
2102
+ const extraKeys = Object.keys(target).filter((k) => k !== "flow" && k !== "agent");
2103
+ if (extraKeys.length > 0) {
2104
+ throw new Error(`defineEval "target" has unknown field(s): ${extraKeys.join(", ")}`);
2105
+ }
2106
+ return hasFlow ? { flow: target.flow } : { agent: target.agent };
2107
+ }
2108
+ function validateGrader(grader, where) {
2109
+ if (!isPlainObject(grader) || typeof grader.kind !== "string") {
2110
+ throw new Error(`defineEval: ${where} must be a grader object with a string "kind"`);
2111
+ }
2112
+ if (grader.kind === "ai") {
2113
+ if (typeof grader.criteria !== "string" || grader.criteria.trim().length === 0) {
2114
+ throw new Error(`defineEval: ${where} is an AI grader and requires non-empty "criteria"`);
2115
+ }
2116
+ return grader;
2117
+ }
2118
+ if (!CHECK_GRADER_KINDS.has(grader.kind)) {
2119
+ throw new Error(
2120
+ `defineEval: ${where} has unknown grader kind "${grader.kind}". Known kinds: ${[...CHECK_GRADER_KINDS].join(", ")}, ai.`
2121
+ );
2122
+ }
2123
+ return grader;
2124
+ }
2125
+ function normalizeCaseInput(input, where) {
2126
+ if (input === void 0) return {};
2127
+ if (!isPlainObject(input)) {
2128
+ throw new Error(`defineEval: ${where} "input" must be an object`);
2129
+ }
2130
+ const out = {};
2131
+ if (input.variables !== void 0) {
2132
+ if (!isPlainObject(input.variables)) {
2133
+ throw new Error(`defineEval: ${where} "input.variables" must be an object`);
2134
+ }
2135
+ out.variables = input.variables;
2136
+ }
2137
+ if (input.messages !== void 0) {
2138
+ if (!Array.isArray(input.messages)) {
2139
+ throw new Error(`defineEval: ${where} "input.messages" must be an array`);
2140
+ }
2141
+ out.messages = input.messages.map((m, i) => {
2142
+ if (!isPlainObject(m) || typeof m.role !== "string" || typeof m.content !== "string") {
2143
+ throw new Error(`defineEval: ${where} "input.messages[${i}]" must be { role, content }`);
2144
+ }
2145
+ return { role: m.role, content: m.content };
2146
+ });
2147
+ }
2148
+ return out;
2149
+ }
2150
+ function defineEval(input) {
2151
+ if (!input || typeof input !== "object") {
2152
+ throw new Error("defineEval requires a definition object");
2153
+ }
2154
+ const unknownKeys = Object.keys(input).filter((k) => !DEFINE_EVAL_TOP_LEVEL_KEYS.has(k));
2155
+ if (unknownKeys.length > 0) {
2156
+ throw new Error(
2157
+ `defineEval: unknown field(s): ${unknownKeys.join(", ")}. Allowed fields are target, graders, cases, virtual.`
2158
+ );
2159
+ }
2160
+ const target = normalizeTarget(input.target);
2161
+ if (input.name !== void 0 && (typeof input.name !== "string" || input.name.length === 0)) {
2162
+ throw new Error('defineEval "name" must be a non-empty string when provided');
2163
+ }
2164
+ const name = input.name ?? ("flow" in target ? `flow:${target.flow}` : `agent:${target.agent}`);
2165
+ const suiteGraders = (input.graders ?? []).map((g, i) => validateGrader(g, `graders[${i}]`));
2166
+ if (!Array.isArray(input.cases) || input.cases.length === 0) {
2167
+ throw new Error('defineEval requires a non-empty "cases" array');
2168
+ }
2169
+ const seenNames = /* @__PURE__ */ new Set();
2170
+ const cases = input.cases.map((c, index) => {
2171
+ if (!isPlainObject(c)) {
2172
+ throw new Error(`defineEval: cases[${index}] must be an object`);
2173
+ }
2174
+ if (typeof c.name !== "string" || c.name.length === 0) {
2175
+ throw new Error(`defineEval: cases[${index}] requires a non-empty string "name"`);
2176
+ }
2177
+ if (seenNames.has(c.name)) {
2178
+ throw new Error(`defineEval: duplicate case name "${c.name}" (case names are the identity)`);
2179
+ }
2180
+ seenNames.add(c.name);
2181
+ const unknownCaseKeys = Object.keys(c).filter((k) => !DEFINE_EVAL_CASE_KEYS.has(k));
2182
+ if (unknownCaseKeys.length > 0) {
2183
+ throw new Error(
2184
+ `defineEval: cases[${index}] ("${c.name}") has unknown field(s): ${unknownCaseKeys.join(
2185
+ ", "
2186
+ )}. Allowed case fields are name, input, expected, expect.`
2187
+ );
2188
+ }
2189
+ const caseGraders = (c.expect ?? []).map(
2190
+ (g, i) => validateGrader(g, `cases[${index}].expect[${i}]`)
2191
+ );
2192
+ const expect = [...suiteGraders, ...caseGraders];
2193
+ if (expect.length === 0) {
2194
+ throw new Error(
2195
+ `defineEval: cases[${index}] ("${c.name}") has no graders. Add suite-level "graders" or case-level "expect" so there is something to score.`
2196
+ );
2197
+ }
2198
+ if (c.expected !== void 0 && !isPlainObject(c.expected)) {
2199
+ throw new Error(`defineEval: cases[${index}] ("${c.name}") "expected" must be an object`);
2200
+ }
2201
+ return {
2202
+ name: c.name,
2203
+ input: normalizeCaseInput(c.input, `cases[${index}] ("${c.name}")`),
2204
+ ...c.expected !== void 0 ? { expected: c.expected } : {},
2205
+ expect
2206
+ };
2207
+ });
2208
+ return { name, target, cases, virtual: input.virtual === true };
2209
+ }
2210
+ function normalizeForHash(value) {
2211
+ if (Array.isArray(value)) return value.map(normalizeForHash);
2212
+ if (isPlainObject(value)) {
2213
+ const out = {};
2214
+ for (const key of Object.keys(value).sort()) {
2215
+ const v = value[key];
2216
+ if (v === void 0) continue;
2217
+ out[key] = normalizeForHash(v);
2218
+ }
2219
+ return out;
2220
+ }
2221
+ return value;
2222
+ }
2223
+ async function computeEvalContentHash(definition) {
2224
+ const canonical = {
2225
+ target: normalizeForHash(definition.target),
2226
+ virtual: definition.virtual,
2227
+ cases: [...definition.cases].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((c) => ({
2228
+ name: c.name,
2229
+ input: normalizeForHash(c.input),
2230
+ ...c.expected !== void 0 ? { expected: normalizeForHash(c.expected) } : {},
2231
+ // Grader order preserved on purpose (it maps to the result index).
2232
+ expect: c.expect.map((g) => normalizeForHash(g))
2233
+ }))
2234
+ };
2235
+ const serialized = JSON.stringify(canonical);
2236
+ const encoded = new TextEncoder().encode(serialized);
2237
+ const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
2238
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
2102
2239
  }
2103
2240
  var serverHashMemo = /* @__PURE__ */ new WeakMap();
2104
2241
  function memoFor(client) {
@@ -2109,261 +2246,572 @@ function memoFor(client) {
2109
2246
  }
2110
2247
  return memo;
2111
2248
  }
2112
- function memoize(memo, memoKey, result) {
2113
- if (result.result !== "plan") memo.set(memoKey, result.contentHash);
2114
- }
2115
- async function request(client, body) {
2116
- try {
2117
- return await client.post(
2118
- "/flows/ensure",
2119
- body
2249
+ async function ensureEval(client, definition) {
2250
+ if (definition.virtual) {
2251
+ throw new Error(
2252
+ "Cannot ensure a virtual eval: virtual evals are ephemeral (nothing is persisted to converge). Remove `virtual: true` to converge a durable suite, or run it directly."
2120
2253
  );
2121
- } catch (err) {
2122
- const conflict = toConflictError(err);
2123
- if (conflict) throw conflict;
2124
- throw err;
2125
- }
2126
- }
2127
- async function ensureFlow(client, definition, options = {}) {
2128
- const { dryRun, onConflict, release, expectedRemoteHash, expectNoChanges } = options;
2129
- const passthrough = {
2130
- ...onConflict ? { onConflict } : {},
2131
- ...release ? { release } : {},
2132
- ...expectedRemoteHash ? { expectedRemoteHash } : {}
2133
- };
2134
- if (dryRun || expectNoChanges) {
2135
- const plan = await request(client, {
2136
- name: definition.name,
2137
- definition,
2138
- dryRun: true,
2139
- ...passthrough
2140
- });
2141
- if (plan.result !== "plan") {
2142
- throw new Error(`Expected a plan result from dryRun, got '${plan.result}'`);
2143
- }
2144
- if (expectNoChanges && plan.changes !== "none") {
2145
- throw new FlowDriftError(plan);
2146
- }
2147
- return plan;
2148
2254
  }
2149
2255
  const memo = memoFor(client);
2150
- const localHash = await computeFlowContentHash(definition.steps);
2256
+ const localHash = await computeEvalContentHash(definition);
2151
2257
  const memoKey = `${definition.name} ${localHash}`;
2152
2258
  const contentHash = memo.get(memoKey) ?? localHash;
2153
- const probe = await request(client, {
2154
- name: definition.name,
2155
- contentHash,
2156
- ...passthrough
2157
- });
2259
+ const probe = await client.post(
2260
+ "/eval/ensure",
2261
+ { name: definition.name, contentHash }
2262
+ );
2158
2263
  if (probe.result !== "definitionRequired") {
2159
- memoize(memo, memoKey, probe);
2264
+ memo.set(memoKey, probe.contentHash);
2160
2265
  return probe;
2161
2266
  }
2162
- const converged = await request(client, {
2163
- name: definition.name,
2164
- definition,
2165
- ...passthrough
2166
- });
2267
+ const converged = await client.post(
2268
+ "/eval/ensure",
2269
+ { name: definition.name, definition }
2270
+ );
2167
2271
  if (converged.result === "definitionRequired") {
2168
2272
  throw new Error("Server reported definitionRequired for a full-definition request");
2169
2273
  }
2170
- memoize(memo, memoKey, converged);
2274
+ memo.set(memoKey, converged.contentHash);
2171
2275
  return converged;
2172
2276
  }
2173
- async function pullFlow(client, name) {
2174
- return client.get("/flows/pull", { name });
2277
+ async function pullEval(client, name) {
2278
+ return client.get("/eval/pull", { name });
2279
+ }
2280
+ async function runEvalSuite(client, input) {
2281
+ return client.post("/eval/run", input);
2175
2282
  }
2176
2283
 
2177
- // src/flows-namespace.ts
2178
- var FlowsNamespace = class {
2179
- constructor(getClient) {
2180
- this.getClient = getClient;
2181
- }
2182
- /**
2183
- * Create or update a flow by name (upsert mode)
2184
- *
2185
- * The recommended pattern for code-first flow management when you want to
2186
- * save AND run in one dispatch. For a deploy-time, non-executing converge
2187
- * (CI/CD config-as-code), use {@link ensure} instead upsert and ensure
2188
- * are siblings, not versions of each other: upsert is the runtime verb
2189
- * (save-and-run), ensure is the deploy verb (converge only).
2190
- *
2191
- * @example
2192
- * ```typescript
2193
- * const result = await Runtype.flows.upsert({
2194
- * name: 'My Flow',
2195
- * createVersionOnChange: true
2196
- * })
2197
- * .prompt({ name: 'Analyze', model: 'gpt-4o', userPrompt: '...' })
2198
- * .stream()
2199
- * ```
2200
- */
2201
- upsert(config) {
2202
- return new RuntypeFlowBuilder(this.getClient, "upsert", config);
2284
+ // src/flows-ensure.ts
2285
+ function isPlainObject2(value) {
2286
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2287
+ }
2288
+ function normalizeConfigForHash(config) {
2289
+ if (!isPlainObject2(config)) return {};
2290
+ const normalized = {};
2291
+ for (const key of Object.keys(config).sort()) {
2292
+ const value = config[key];
2293
+ if (value === void 0) continue;
2294
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
2295
+ normalized[key] = normalizeConfigForHash(value);
2296
+ } else if (Array.isArray(value)) {
2297
+ normalized[key] = value.map((item) => {
2298
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
2299
+ return normalizeConfigForHash(item);
2300
+ }
2301
+ return item;
2302
+ });
2303
+ } else {
2304
+ normalized[key] = value;
2305
+ }
2203
2306
  }
2204
- /**
2205
- * Idempotently converge a `defineFlow` definition onto the platform —
2206
- * the deploy-time, non-executing sibling of {@link upsert}. Hash-first:
2207
- * the steady state is one tiny probe request. Creates an immutable version
2208
- * snapshot on every change; never deletes; never executes the flow.
2209
- *
2210
- * @example
2211
- * ```typescript
2212
- * const def = defineFlow({ name: 'Onboarding Digest', steps: [...] })
2213
- *
2214
- * // Converge (CI/deploy).
2215
- * const result = await Runtype.flows.ensure(def)
2216
- *
2217
- * // PR drift gate.
2218
- * await Runtype.flows.ensure(def, { expectNoChanges: true })
2219
- * ```
2220
- */
2221
- async ensure(definition, options = {}) {
2222
- return ensureFlow(this.getClient(), definition, options);
2307
+ return normalized;
2308
+ }
2309
+ function normalizeStepForHash(step) {
2310
+ const stepObj = isPlainObject2(step) ? step : {};
2311
+ return {
2312
+ type: typeof stepObj.type === "string" ? stepObj.type : "",
2313
+ name: typeof stepObj.name === "string" ? stepObj.name : "",
2314
+ enabled: stepObj.enabled !== false,
2315
+ ...typeof stepObj.when === "string" ? { when: stepObj.when } : {},
2316
+ config: normalizeConfigForHash(stepObj.config),
2317
+ order: typeof stepObj.order === "number" ? stepObj.order : 0
2318
+ };
2319
+ }
2320
+ async function computeFlowContentHash(steps) {
2321
+ const normalized = [...steps].sort((a, b) => {
2322
+ const orderA = isPlainObject2(a) && typeof a.order === "number" ? a.order : 0;
2323
+ const orderB = isPlainObject2(b) && typeof b.order === "number" ? b.order : 0;
2324
+ return orderA - orderB;
2325
+ }).map(normalizeStepForHash);
2326
+ const serialized = JSON.stringify(normalized);
2327
+ const encoded = new TextEncoder().encode(serialized);
2328
+ const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
2329
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
2330
+ }
2331
+ var DEFINE_FLOW_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["name", "steps", "evals"]);
2332
+ var DEFINE_FLOW_STEP_KEYS = /* @__PURE__ */ new Set([
2333
+ "type",
2334
+ "name",
2335
+ "order",
2336
+ "enabled",
2337
+ "when",
2338
+ "config"
2339
+ ]);
2340
+ function collectStepNonPortableToolRefs(config, path) {
2341
+ const found = [];
2342
+ const tools = config.tools;
2343
+ const isAccountScoped = (ref) => typeof ref === "string" && ref.startsWith("tool_");
2344
+ const isRawId = (ref, prefix) => typeof ref === "string" && ref.startsWith(prefix);
2345
+ const scanArray = (value, subPath) => {
2346
+ if (!Array.isArray(value)) return;
2347
+ value.forEach((ref, i) => {
2348
+ if (isAccountScoped(ref)) found.push(`${subPath}[${i}]`);
2349
+ });
2350
+ };
2351
+ const scanKeys = (value, subPath) => {
2352
+ if (!isPlainObject2(value)) return;
2353
+ for (const key of Object.keys(value)) {
2354
+ if (isAccountScoped(key)) found.push(`${subPath}.${key}`);
2355
+ }
2356
+ };
2357
+ if (isPlainObject2(tools)) {
2358
+ scanArray(tools.toolIds, `${path}.tools.toolIds`);
2359
+ scanKeys(tools.toolConfigs, `${path}.tools.toolConfigs`);
2360
+ scanKeys(tools.perToolLimits, `${path}.tools.perToolLimits`);
2361
+ if (isPlainObject2(tools.approval)) {
2362
+ scanArray(tools.approval.require, `${path}.tools.approval.require`);
2363
+ }
2364
+ if (isPlainObject2(tools.subagentConfig)) {
2365
+ scanArray(tools.subagentConfig.toolPool, `${path}.tools.subagentConfig.toolPool`);
2366
+ }
2367
+ if (isPlainObject2(tools.codeModeConfig)) {
2368
+ scanArray(tools.codeModeConfig.toolPool, `${path}.tools.codeModeConfig.toolPool`);
2369
+ }
2370
+ if (Array.isArray(tools.runtimeTools)) {
2371
+ tools.runtimeTools.forEach((runtimeTool, i) => {
2372
+ if (!isPlainObject2(runtimeTool) || !isPlainObject2(runtimeTool.config)) return;
2373
+ const base = `${path}.tools.runtimeTools[${i}].config`;
2374
+ const rtConfig = runtimeTool.config;
2375
+ if (runtimeTool.toolType === "subagent" && isRawId(rtConfig.agentId, "agent_")) {
2376
+ found.push(`${base}.agentId`);
2377
+ } else if (runtimeTool.toolType === "flow" && isRawId(rtConfig.flowId, "flow_")) {
2378
+ found.push(`${base}.flowId`);
2379
+ }
2380
+ });
2381
+ }
2223
2382
  }
2224
- /**
2225
- * Pull the canonical definition + provenance for a flow by name — the
2226
- * absorb-drift direction of the ensure protocol.
2227
- */
2228
- async pull(name) {
2229
- return pullFlow(this.getClient(), name);
2383
+ if (isAccountScoped(config.toolId)) {
2384
+ found.push(`${path}.toolId`);
2230
2385
  }
2231
- /**
2232
- * Create a virtual flow (one-off, not saved)
2233
- *
2234
- * Use for temporary or ad-hoc flow execution.
2235
- *
2236
- * @example
2237
- * ```typescript
2238
- * const result = await Runtype.flows.virtual({ name: 'Temp Flow' })
2239
- * .prompt({ name: 'Process', model: 'gpt-4o', userPrompt: '...' })
2240
- * .stream()
2241
- * ```
2242
- */
2243
- virtual(config) {
2244
- return new RuntypeFlowBuilder(this.getClient, "virtual", config);
2386
+ if (isRawId(config.agentId, "agent_")) {
2387
+ found.push(`${path}.agentId`);
2245
2388
  }
2246
- /**
2247
- * Use an existing flow by ID
2248
- *
2249
- * @example
2250
- * ```typescript
2251
- * const result = await Runtype.flows.use('flow_123')
2252
- * .withRecord({ name: 'Test', type: 'data' })
2253
- * .stream()
2254
- * ```
2255
- */
2256
- use(flowId) {
2257
- return new RuntypeFlowBuilder(this.getClient, "existing", void 0, flowId);
2389
+ for (const branch of ["trueSteps", "falseSteps"]) {
2390
+ const nested = config[branch];
2391
+ if (!Array.isArray(nested)) continue;
2392
+ nested.forEach((nestedStep, i) => {
2393
+ if (isPlainObject2(nestedStep) && isPlainObject2(nestedStep.config)) {
2394
+ found.push(
2395
+ ...collectStepNonPortableToolRefs(nestedStep.config, `${path}.${branch}[${i}].config`)
2396
+ );
2397
+ }
2398
+ });
2258
2399
  }
2259
- /**
2260
- * Quick execution of an existing flow
2261
- *
2262
- * @example
2263
- * ```typescript
2264
- * const result = await Runtype.flows.execute('flow_123', {
2265
- * record: { name: 'Test', type: 'data' },
2266
- * streamResponse: true
2267
- * })
2268
- * ```
2269
- */
2270
- async execute(flowId, options) {
2271
- const builder = this.use(flowId);
2272
- if (options?.record) builder.withRecord(options.record);
2273
- if (options?.messages) builder.withMessages(options.messages);
2274
- return options?.streamResponse !== false ? builder.stream() : builder.result();
2400
+ return found;
2401
+ }
2402
+ function defineFlow(input) {
2403
+ if (!input || typeof input !== "object") {
2404
+ throw new Error("defineFlow requires a definition object");
2275
2405
  }
2276
- };
2277
- var RuntypeFlowBuilder = class {
2278
- constructor(getClient, mode, config, flowId) {
2279
- this.getClient = getClient;
2280
- this.steps = [];
2281
- this.stepCounter = 0;
2282
- this.upsertOptions = {};
2283
- this.dispatchOptions = {};
2284
- this.mode = mode;
2285
- if (mode === "existing" && flowId) {
2286
- this.existingFlowId = flowId;
2287
- this.flowConfig = { name: "" };
2288
- } else if (config) {
2289
- const { createVersionOnChange, allowOverwriteExternalChanges, ...flowConfig } = config;
2290
- this.flowConfig = flowConfig;
2291
- if (mode === "upsert") {
2292
- this.upsertOptions = {
2293
- createVersionOnChange: createVersionOnChange ?? true,
2294
- ...allowOverwriteExternalChanges !== void 0 && { allowOverwriteExternalChanges }
2295
- };
2296
- }
2297
- } else {
2298
- this.flowConfig = { name: "Untitled Flow" };
2299
- }
2406
+ if (typeof input.name !== "string" || input.name.length === 0) {
2407
+ throw new Error('defineFlow requires a non-empty string "name"');
2300
2408
  }
2301
- // ============================================================================
2302
- // Configuration Methods
2303
- // ============================================================================
2304
- /**
2305
- * Set the record configuration
2306
- */
2307
- withRecord(config) {
2308
- this.recordConfig = config;
2309
- return this;
2409
+ const unknownKeys = Object.keys(input).filter((key) => !DEFINE_FLOW_TOP_LEVEL_KEYS.has(key));
2410
+ if (unknownKeys.length > 0) {
2411
+ throw new Error(
2412
+ `defineFlow: unknown field(s): ${unknownKeys.join(", ")}. Allowed fields are name, steps, and evals. (Description is not part of the v1 ensure surface.)`
2413
+ );
2310
2414
  }
2311
- /**
2312
- * Set conversation messages
2313
- */
2314
- withMessages(messages) {
2315
- this.messagesConfig = messages;
2316
- return this;
2415
+ if (!Array.isArray(input.steps) || input.steps.length === 0) {
2416
+ throw new Error('defineFlow requires a non-empty "steps" array');
2417
+ }
2418
+ const steps = input.steps.map((step, index) => {
2419
+ if (!isPlainObject2(step)) {
2420
+ throw new Error(`defineFlow: steps[${index}] must be an object`);
2421
+ }
2422
+ if (typeof step.type !== "string" || step.type.length === 0) {
2423
+ throw new Error(`defineFlow: steps[${index}] requires a non-empty string "type"`);
2424
+ }
2425
+ if (typeof step.name !== "string" || step.name.length === 0) {
2426
+ throw new Error(`defineFlow: steps[${index}] requires a non-empty string "name"`);
2427
+ }
2428
+ const unknownStepKeys = Object.keys(step).filter((key) => !DEFINE_FLOW_STEP_KEYS.has(key));
2429
+ if (unknownStepKeys.length > 0) {
2430
+ throw new Error(
2431
+ `defineFlow: steps[${index}] has unknown field(s): ${unknownStepKeys.join(", ")}. Allowed step fields are type, name, order, enabled, when, config. (Step ids are server artifacts and not part of a portable definition.)`
2432
+ );
2433
+ }
2434
+ const config = isPlainObject2(step.config) ? step.config : void 0;
2435
+ if (config) {
2436
+ const nonPortable = collectStepNonPortableToolRefs(config, `steps[${index}].config`);
2437
+ if (nonPortable.length > 0) {
2438
+ throw new Error(
2439
+ `defineFlow: account-scoped reference(s) at ${nonPortable.join(", ")}. Definitions must be environment-portable \u2014 tool_\u2026/agent_\u2026/flow_\u2026 IDs belong to one account/environment. Use builtin:/platform:/mcp: references, or reference a saved resource by name \u2014 tool:<name>, agent:<name>, or flow:<name> instead.`
2440
+ );
2441
+ }
2442
+ }
2443
+ return {
2444
+ type: step.type,
2445
+ name: step.name,
2446
+ // Explicit 1-based order (the flow builder's convention) so the local
2447
+ // probe hash agrees with the server's persisted step order.
2448
+ order: typeof step.order === "number" ? step.order : index + 1,
2449
+ ...step.enabled !== void 0 ? { enabled: step.enabled } : {},
2450
+ ...typeof step.when === "string" ? { when: step.when } : {},
2451
+ ...config ? { config } : {}
2452
+ };
2453
+ });
2454
+ let evals;
2455
+ if (input.evals !== void 0) {
2456
+ if (!Array.isArray(input.evals)) {
2457
+ throw new Error('defineFlow: "evals" must be an array');
2458
+ }
2459
+ const seenEvalNames = /* @__PURE__ */ new Set();
2460
+ evals = input.evals.map((evalInput, i) => {
2461
+ if (!isPlainObject2(evalInput)) {
2462
+ throw new Error(`defineFlow: evals[${i}] must be an object`);
2463
+ }
2464
+ if (evalInput.virtual === true) {
2465
+ throw new Error(
2466
+ `defineFlow: evals[${i}] cannot be virtual (inline evals converge with the flow; run a virtual eval directly instead).`
2467
+ );
2468
+ }
2469
+ const withTarget = evalInput.target === void 0 ? { ...evalInput, target: { flow: input.name } } : evalInput;
2470
+ let def;
2471
+ try {
2472
+ def = defineEval(withTarget);
2473
+ } catch (err) {
2474
+ throw new Error(
2475
+ `defineFlow: evals[${i}] \u2014 ${err instanceof Error ? err.message : String(err)}`,
2476
+ { cause: err }
2477
+ );
2478
+ }
2479
+ if (seenEvalNames.has(def.name)) {
2480
+ throw new Error(
2481
+ `defineFlow: evals[${i}] resolves to the duplicate suite name "${def.name}". Inline eval suites must have distinct names \u2014 give each a \`name\` (two unnamed evals targeting the same flow both default to the same name and would overwrite each other).`
2482
+ );
2483
+ }
2484
+ seenEvalNames.add(def.name);
2485
+ return def;
2486
+ });
2487
+ }
2488
+ return {
2489
+ name: input.name,
2490
+ steps,
2491
+ ...evals && evals.length > 0 ? { evals } : {}
2492
+ };
2493
+ }
2494
+ var FlowEnsureConflictError = class extends Error {
2495
+ constructor(body) {
2496
+ super(body.error ?? `Flow ensure conflict: ${body.code}`);
2497
+ this.name = "FlowEnsureConflictError";
2498
+ this.code = body.code;
2499
+ this.lastModifiedSource = body.lastModifiedSource;
2500
+ this.modifiedAt = body.modifiedAt;
2501
+ this.currentHash = body.currentHash;
2502
+ }
2503
+ };
2504
+ var FlowDriftError = class extends Error {
2505
+ constructor(plan) {
2506
+ super(
2507
+ `Flow "${plan.flowId ?? "definition"}" drifted: plan is '${plan.changes}' (changed: ${plan.changedKeys.join(", ") || "n/a"}). Run client.flows.pull(name) to absorb the remote edit into your repo, or re-run ensure to converge.`
2508
+ );
2509
+ this.name = "FlowDriftError";
2510
+ this.plan = plan;
2511
+ }
2512
+ };
2513
+ function parseRequestError(err) {
2514
+ if (!(err instanceof Error)) return { status: null, body: null };
2515
+ const match = err.message.match(/^API request failed: (\d{3}) .*? - ([\s\S]*)$/);
2516
+ if (!match) return { status: null, body: null };
2517
+ try {
2518
+ return { status: Number(match[1]), body: JSON.parse(match[2]) };
2519
+ } catch {
2520
+ return { status: Number(match[1]), body: null };
2521
+ }
2522
+ }
2523
+ function toConflictError(err) {
2524
+ const { status, body } = parseRequestError(err);
2525
+ if (status !== 409 || !isPlainObject2(body)) return null;
2526
+ const code = body.code;
2527
+ if (code !== "external_modification" && code !== "remote_changed") return null;
2528
+ return new FlowEnsureConflictError(
2529
+ body
2530
+ );
2531
+ }
2532
+ var serverHashMemo2 = /* @__PURE__ */ new WeakMap();
2533
+ function memoFor2(client) {
2534
+ let memo = serverHashMemo2.get(client);
2535
+ if (!memo) {
2536
+ memo = /* @__PURE__ */ new Map();
2537
+ serverHashMemo2.set(client, memo);
2538
+ }
2539
+ return memo;
2540
+ }
2541
+ function memoize(memo, memoKey, result) {
2542
+ if (result.result !== "plan") memo.set(memoKey, result.contentHash);
2543
+ }
2544
+ async function request(client, body) {
2545
+ try {
2546
+ return await client.post(
2547
+ "/flows/ensure",
2548
+ body
2549
+ );
2550
+ } catch (err) {
2551
+ const conflict = toConflictError(err);
2552
+ if (conflict) throw conflict;
2553
+ throw err;
2554
+ }
2555
+ }
2556
+ async function ensureFlow(client, definition, options = {}) {
2557
+ const { dryRun, onConflict, release, expectedRemoteHash, expectNoChanges } = options;
2558
+ const passthrough = {
2559
+ ...onConflict ? { onConflict } : {},
2560
+ ...release ? { release } : {},
2561
+ ...expectedRemoteHash ? { expectedRemoteHash } : {}
2562
+ };
2563
+ const wireDefinition = { name: definition.name, steps: definition.steps };
2564
+ if (dryRun || expectNoChanges) {
2565
+ const plan = await request(client, {
2566
+ name: definition.name,
2567
+ definition: wireDefinition,
2568
+ dryRun: true,
2569
+ ...passthrough
2570
+ });
2571
+ if (plan.result !== "plan") {
2572
+ throw new Error(`Expected a plan result from dryRun, got '${plan.result}'`);
2573
+ }
2574
+ if (expectNoChanges && plan.changes !== "none") {
2575
+ throw new FlowDriftError(plan);
2576
+ }
2577
+ return plan;
2578
+ }
2579
+ const memo = memoFor2(client);
2580
+ const localHash = await computeFlowContentHash(definition.steps);
2581
+ const memoKey = `${definition.name} ${localHash}`;
2582
+ const contentHash = memo.get(memoKey) ?? localHash;
2583
+ const probe = await request(client, {
2584
+ name: definition.name,
2585
+ contentHash,
2586
+ ...passthrough
2587
+ });
2588
+ if (probe.result !== "definitionRequired") {
2589
+ memoize(memo, memoKey, probe);
2590
+ return convergeInlineEvals(client, definition, probe);
2591
+ }
2592
+ const converged = await request(client, {
2593
+ name: definition.name,
2594
+ definition: wireDefinition,
2595
+ ...passthrough
2596
+ });
2597
+ if (converged.result === "definitionRequired") {
2598
+ throw new Error("Server reported definitionRequired for a full-definition request");
2599
+ }
2600
+ memoize(memo, memoKey, converged);
2601
+ return convergeInlineEvals(client, definition, converged);
2602
+ }
2603
+ async function convergeInlineEvals(client, definition, result) {
2604
+ if (result.result === "plan" || !definition.evals?.length) {
2605
+ return result;
2606
+ }
2607
+ const evals = [];
2608
+ for (const evalDef of definition.evals) {
2609
+ evals.push(await ensureEval(client, evalDef));
2610
+ }
2611
+ return { ...result, evals };
2612
+ }
2613
+ async function pullFlow(client, name) {
2614
+ return client.get("/flows/pull", { name });
2615
+ }
2616
+
2617
+ // src/flows-namespace.ts
2618
+ var FlowsNamespace = class {
2619
+ constructor(getClient) {
2620
+ this.getClient = getClient;
2317
2621
  }
2318
2622
  /**
2319
- * Set top-level input variables accessible as {{varName}} in templates.
2623
+ * Create or update a flow by name (upsert mode)
2624
+ *
2625
+ * The recommended pattern for code-first flow management when you want to
2626
+ * save AND run in one dispatch. For a deploy-time, non-executing converge
2627
+ * (CI/CD config-as-code), use {@link ensure} instead — upsert and ensure
2628
+ * are siblings, not versions of each other: upsert is the runtime verb
2629
+ * (save-and-run), ensure is the deploy verb (converge only).
2630
+ *
2631
+ * @example
2632
+ * ```typescript
2633
+ * const result = await Runtype.flows.upsert({
2634
+ * name: 'My Flow',
2635
+ * createVersionOnChange: true
2636
+ * })
2637
+ * .prompt({ name: 'Analyze', model: 'gpt-4o', userPrompt: '...' })
2638
+ * .stream()
2639
+ * ```
2320
2640
  */
2321
- withInputs(inputs) {
2322
- this.inputsConfig = inputs;
2323
- return this;
2641
+ upsert(config) {
2642
+ return new RuntypeFlowBuilder(this.getClient, "upsert", config);
2324
2643
  }
2325
2644
  /**
2326
- * Set dispatch options
2645
+ * Idempotently converge a `defineFlow` definition onto the platform —
2646
+ * the deploy-time, non-executing sibling of {@link upsert}. Hash-first:
2647
+ * the steady state is one tiny probe request. Creates an immutable version
2648
+ * snapshot on every change; never deletes; never executes the flow.
2649
+ *
2650
+ * When the definition carries inline `evals`, each suite is converged via
2651
+ * `/eval/ensure` after the flow itself (real converge path only — not on
2652
+ * dryRun/`expectNoChanges`), and the outcomes are returned as `result.evals`.
2653
+ *
2654
+ * @example
2655
+ * ```typescript
2656
+ * const def = defineFlow({
2657
+ * name: 'Onboarding Digest',
2658
+ * steps: [...],
2659
+ * evals: [{ cases: [{ name: 'smoke', input: {...}, expect: [contains('ok')] }] }],
2660
+ * })
2661
+ *
2662
+ * // Converge the flow AND its inline eval suites (CI/deploy).
2663
+ * const result = await Runtype.flows.ensure(def)
2664
+ *
2665
+ * // PR drift gate.
2666
+ * await Runtype.flows.ensure(def, { expectNoChanges: true })
2667
+ * ```
2327
2668
  */
2328
- withOptions(options) {
2329
- this.dispatchOptions = { ...this.dispatchOptions, ...options };
2330
- return this;
2669
+ async ensure(definition, options = {}) {
2670
+ return ensureFlow(this.getClient(), definition, options);
2331
2671
  }
2332
2672
  /**
2333
- * Add a generic flow step. Prefer the typed helper methods when available;
2334
- * this escape hatch keeps code-first flows compatible with newer dashboard/API
2335
- * step types before a dedicated SDK convenience method exists.
2673
+ * Pull the canonical definition + provenance for a flow by name — the
2674
+ * absorb-drift direction of the ensure protocol.
2336
2675
  */
2337
- step(config) {
2338
- this.addStep(
2339
- config.type,
2340
- config.name || config.type,
2341
- config.config || {},
2342
- config.enabled,
2343
- config.when
2344
- );
2345
- return this;
2676
+ async pull(name) {
2677
+ return pullFlow(this.getClient(), name);
2346
2678
  }
2347
- // ============================================================================
2348
- // Step Methods
2349
- // ============================================================================
2350
2679
  /**
2351
- * Add a prompt step
2680
+ * Create a virtual flow (one-off, not saved)
2681
+ *
2682
+ * Use for temporary or ad-hoc flow execution.
2683
+ *
2684
+ * @example
2685
+ * ```typescript
2686
+ * const result = await Runtype.flows.virtual({ name: 'Temp Flow' })
2687
+ * .prompt({ name: 'Process', model: 'gpt-4o', userPrompt: '...' })
2688
+ * .stream()
2689
+ * ```
2352
2690
  */
2353
- prompt(config) {
2354
- this.addStep(
2355
- "prompt",
2356
- config.name,
2357
- {
2358
- model: config.model,
2359
- userPrompt: config.userPrompt,
2360
- text: config.userPrompt,
2361
- systemPrompt: config.systemPrompt,
2362
- previousMessages: config.previousMessages,
2363
- outputVariable: config.outputVariable,
2364
- mode: config.mode,
2365
- responseFormat: config.responseFormat,
2366
- temperature: config.temperature,
2691
+ virtual(config) {
2692
+ return new RuntypeFlowBuilder(this.getClient, "virtual", config);
2693
+ }
2694
+ /**
2695
+ * Use an existing flow by ID
2696
+ *
2697
+ * @example
2698
+ * ```typescript
2699
+ * const result = await Runtype.flows.use('flow_123')
2700
+ * .withRecord({ name: 'Test', type: 'data' })
2701
+ * .stream()
2702
+ * ```
2703
+ */
2704
+ use(flowId) {
2705
+ return new RuntypeFlowBuilder(this.getClient, "existing", void 0, flowId);
2706
+ }
2707
+ /**
2708
+ * Quick execution of an existing flow
2709
+ *
2710
+ * @example
2711
+ * ```typescript
2712
+ * const result = await Runtype.flows.execute('flow_123', {
2713
+ * record: { name: 'Test', type: 'data' },
2714
+ * streamResponse: true
2715
+ * })
2716
+ * ```
2717
+ */
2718
+ async execute(flowId, options) {
2719
+ const builder = this.use(flowId);
2720
+ if (options?.record) builder.withRecord(options.record);
2721
+ if (options?.messages) builder.withMessages(options.messages);
2722
+ return options?.streamResponse !== false ? builder.stream() : builder.result();
2723
+ }
2724
+ };
2725
+ var RuntypeFlowBuilder = class {
2726
+ constructor(getClient, mode, config, flowId) {
2727
+ this.getClient = getClient;
2728
+ this.steps = [];
2729
+ this.stepCounter = 0;
2730
+ this.upsertOptions = {};
2731
+ this.dispatchOptions = {};
2732
+ this.mode = mode;
2733
+ if (mode === "existing" && flowId) {
2734
+ this.existingFlowId = flowId;
2735
+ this.flowConfig = { name: "" };
2736
+ } else if (config) {
2737
+ const { createVersionOnChange, allowOverwriteExternalChanges, ...flowConfig } = config;
2738
+ this.flowConfig = flowConfig;
2739
+ if (mode === "upsert") {
2740
+ this.upsertOptions = {
2741
+ createVersionOnChange: createVersionOnChange ?? true,
2742
+ ...allowOverwriteExternalChanges !== void 0 && { allowOverwriteExternalChanges }
2743
+ };
2744
+ }
2745
+ } else {
2746
+ this.flowConfig = { name: "Untitled Flow" };
2747
+ }
2748
+ }
2749
+ // ============================================================================
2750
+ // Configuration Methods
2751
+ // ============================================================================
2752
+ /**
2753
+ * Set the record configuration
2754
+ */
2755
+ withRecord(config) {
2756
+ this.recordConfig = config;
2757
+ return this;
2758
+ }
2759
+ /**
2760
+ * Set conversation messages
2761
+ */
2762
+ withMessages(messages) {
2763
+ this.messagesConfig = messages;
2764
+ return this;
2765
+ }
2766
+ /**
2767
+ * Set top-level input variables accessible as {{varName}} in templates.
2768
+ */
2769
+ withInputs(inputs) {
2770
+ this.inputsConfig = inputs;
2771
+ return this;
2772
+ }
2773
+ /**
2774
+ * Set dispatch options
2775
+ */
2776
+ withOptions(options) {
2777
+ this.dispatchOptions = { ...this.dispatchOptions, ...options };
2778
+ return this;
2779
+ }
2780
+ /**
2781
+ * Add a generic flow step. Prefer the typed helper methods when available;
2782
+ * this escape hatch keeps code-first flows compatible with newer dashboard/API
2783
+ * step types before a dedicated SDK convenience method exists.
2784
+ */
2785
+ step(config) {
2786
+ this.addStep(
2787
+ config.type,
2788
+ config.name || config.type,
2789
+ config.config || {},
2790
+ config.enabled,
2791
+ config.when
2792
+ );
2793
+ return this;
2794
+ }
2795
+ // ============================================================================
2796
+ // Step Methods
2797
+ // ============================================================================
2798
+ /**
2799
+ * Add a prompt step
2800
+ */
2801
+ prompt(config) {
2802
+ this.addStep(
2803
+ "prompt",
2804
+ config.name,
2805
+ {
2806
+ model: config.model,
2807
+ userPrompt: config.userPrompt,
2808
+ text: config.userPrompt,
2809
+ systemPrompt: config.systemPrompt,
2810
+ previousMessages: config.previousMessages,
2811
+ outputVariable: config.outputVariable,
2812
+ mode: config.mode,
2813
+ responseFormat: config.responseFormat,
2814
+ temperature: config.temperature,
2367
2815
  topP: config.topP,
2368
2816
  topK: config.topK,
2369
2817
  frequencyPenalty: config.frequencyPenalty,
@@ -3127,516 +3575,154 @@ var RuntypeFlowBuilder = class {
3127
3575
  return await client.dispatch(hashOnlyConfig);
3128
3576
  } catch (err) {
3129
3577
  const is422 = err != null && typeof err === "object" && "statusCode" in err && err.statusCode === 422 || err instanceof Error && /\b422\b/.test(err.message);
3130
- if (!is422) {
3131
- throw err;
3132
- }
3133
- }
3134
- const fullConfig = {
3135
- ...config,
3136
- flow: { ...config.flow, contentHash }
3137
- };
3138
- return client.dispatch(fullConfig);
3139
- }
3140
- async computeContentHash() {
3141
- return computeFlowContentHash(this.steps);
3142
- }
3143
- addRawStep(type, config) {
3144
- const { name, enabled, when, ...stepConfig } = config;
3145
- this.addStep(type, name, stepConfig, enabled, when);
3146
- return this;
3147
- }
3148
- addStep(type, name, config, enabled = true, when) {
3149
- this.stepCounter++;
3150
- const cleanConfig = {};
3151
- for (const [key, value] of Object.entries(config)) {
3152
- if (value !== void 0) {
3153
- cleanConfig[key] = value;
3154
- }
3155
- }
3156
- this.steps.push({
3157
- id: `step-${this.stepCounter}`,
3158
- type,
3159
- name,
3160
- order: this.stepCounter,
3161
- enabled,
3162
- ...when ? { when } : {},
3163
- config: cleanConfig
3164
- });
3165
- }
3166
- };
3167
-
3168
- // src/batches-namespace.ts
3169
- var BatchesNamespace = class {
3170
- constructor(getClient) {
3171
- this.getClient = getClient;
3172
- }
3173
- /**
3174
- * Schedule a batch operation
3175
- *
3176
- * Creates and schedules a batch to run a flow on all records of a type.
3177
- * By default, runs immediately. Use `at` to schedule for a specific time.
3178
- *
3179
- * @example
3180
- * ```typescript
3181
- * // Run immediately
3182
- * const batch = await Runtype.batches.schedule({
3183
- * flowId: 'flow_123',
3184
- * recordType: 'customers',
3185
- * })
3186
- *
3187
- * // Schedule for later
3188
- * const batch = await Runtype.batches.schedule({
3189
- * flowId: 'flow_123',
3190
- * recordType: 'customers',
3191
- * at: new Date('2024-01-15T09:00:00Z'),
3192
- * })
3193
- *
3194
- * // With options
3195
- * const batch = await Runtype.batches.schedule({
3196
- * flowId: 'flow_123',
3197
- * recordType: 'customers',
3198
- * concurrency: 5,
3199
- * continueOnError: true,
3200
- * filter: { status: 'active' },
3201
- * limit: 100,
3202
- * })
3203
- * ```
3204
- */
3205
- async schedule(config) {
3206
- const client = this.getClient();
3207
- const payload = {
3208
- flowId: config.flowId,
3209
- recordType: config.recordType
3210
- };
3211
- if (config.at) {
3212
- payload.scheduledAt = config.at.toISOString();
3213
- }
3214
- const options = {};
3215
- if (config.async !== void 0) options.async = config.async;
3216
- if (config.concurrency !== void 0) options.concurrency = config.concurrency;
3217
- if (config.continueOnError !== void 0) options.continueOnError = config.continueOnError;
3218
- if (config.storeResults !== void 0) options.storeResults = config.storeResults;
3219
- if (config.modelOverride !== void 0) options.modelOverride = config.modelOverride;
3220
- if (Object.keys(options).length > 0) {
3221
- payload.options = options;
3222
- }
3223
- if (config.filter) {
3224
- payload.filter = config.filter;
3225
- }
3226
- if (config.limit !== void 0) {
3227
- payload.limit = config.limit;
3228
- }
3229
- return client.post("/batches", payload);
3230
- }
3231
- /**
3232
- * Get batch status by ID
3233
- *
3234
- * @example
3235
- * ```typescript
3236
- * const status = await Runtype.batches.get('batch_456')
3237
- * console.log(status.status, status.processedRecords, '/', status.totalRecords)
3238
- * ```
3239
- */
3240
- async get(batchId) {
3241
- const client = this.getClient();
3242
- return client.get(`/batches/${batchId}`);
3243
- }
3244
- /**
3245
- * Cancel a batch operation
3246
- *
3247
- * Cancels a queued or running batch. Records already processed are not rolled back.
3248
- *
3249
- * @example
3250
- * ```typescript
3251
- * await Runtype.batches.cancel('batch_456')
3252
- * ```
3253
- */
3254
- async cancel(batchId) {
3255
- const client = this.getClient();
3256
- return client.post(`/batches/${batchId}/cancel`);
3257
- }
3258
- /**
3259
- * List batch operations
3260
- *
3261
- * @example
3262
- * ```typescript
3263
- * // List all batches
3264
- * const batches = await Runtype.batches.list()
3265
- *
3266
- * // Filter by status
3267
- * const running = await Runtype.batches.list({ status: 'running' })
3268
- *
3269
- * // Filter by flow
3270
- * const flowBatches = await Runtype.batches.list({ flowId: 'flow_123' })
3271
- * ```
3272
- */
3273
- async list(params) {
3274
- const client = this.getClient();
3275
- return client.get("/batches", params);
3276
- }
3277
- };
3278
-
3279
- // src/evals-ensure.ts
3280
- var CHECK_GRADER_KINDS = /* @__PURE__ */ new Set([
3281
- "contains",
3282
- "not_contains",
3283
- "matches_expected",
3284
- "regex",
3285
- "valid_json",
3286
- "json_field",
3287
- "length",
3288
- "latency",
3289
- "no_error",
3290
- // Trace checks.
3291
- "called_tool",
3292
- "not_called_tool",
3293
- "used_no_tools",
3294
- "max_tool_calls",
3295
- "tool_order",
3296
- "ran_step",
3297
- "step_order",
3298
- "completed",
3299
- "cost"
3300
- ]);
3301
- function contains(value, opts) {
3302
- return { kind: "contains", value, ...opts?.caseSensitive ? { caseSensitive: true } : {} };
3303
- }
3304
- function notContains(value, opts) {
3305
- return { kind: "not_contains", value, ...opts?.caseSensitive ? { caseSensitive: true } : {} };
3306
- }
3307
- function matchesExpected() {
3308
- return { kind: "matches_expected" };
3309
- }
3310
- function regex(pattern, flags) {
3311
- return { kind: "regex", pattern, ...flags ? { flags } : {} };
3312
- }
3313
- function validJson() {
3314
- return { kind: "valid_json" };
3315
- }
3316
- function jsonField(path, opts) {
3317
- return {
3318
- kind: "json_field",
3319
- path,
3320
- ...opts && "equals" in opts && opts.equals !== void 0 ? { equals: opts.equals } : {},
3321
- ...opts && typeof opts.exists === "boolean" ? { exists: opts.exists } : {}
3322
- };
3323
- }
3324
- function length(opts) {
3325
- if (!opts || opts.minChars === void 0 && opts.maxChars === void 0) {
3326
- throw new Error("length() requires at least one of minChars or maxChars");
3327
- }
3328
- return {
3329
- kind: "length",
3330
- ...opts.minChars !== void 0 ? { minChars: opts.minChars } : {},
3331
- ...opts.maxChars !== void 0 ? { maxChars: opts.maxChars } : {}
3332
- };
3333
- }
3334
- function latency(maxMs) {
3335
- if (!Number.isFinite(maxMs) || maxMs <= 0) {
3336
- throw new Error("latency() requires a positive maxMs");
3337
- }
3338
- return { kind: "latency", maxMs };
3339
- }
3340
- function noError() {
3341
- return { kind: "no_error" };
3342
- }
3343
- function calledTool(name, opts) {
3344
- if (typeof name !== "string" || name.length === 0) {
3345
- throw new Error("calledTool() requires a non-empty tool name");
3346
- }
3347
- if (opts?.times !== void 0 && (!Number.isInteger(opts.times) || opts.times <= 0)) {
3348
- throw new Error('calledTool() "times" must be a positive integer');
3349
- }
3350
- return {
3351
- kind: "called_tool",
3352
- name,
3353
- ...opts && "input" in opts && opts.input !== void 0 ? { input: opts.input } : {},
3354
- ...opts && "output" in opts && opts.output !== void 0 ? { output: opts.output } : {},
3355
- ...opts && typeof opts.isError === "boolean" ? { isError: opts.isError } : {},
3356
- ...opts?.times !== void 0 ? { times: opts.times } : {}
3357
- };
3358
- }
3359
- function notCalledTool(name) {
3360
- if (typeof name !== "string" || name.length === 0) {
3361
- throw new Error("notCalledTool() requires a non-empty tool name");
3362
- }
3363
- return { kind: "not_called_tool", name };
3364
- }
3365
- function usedNoTools() {
3366
- return { kind: "used_no_tools" };
3367
- }
3368
- function maxToolCalls(max) {
3369
- if (!Number.isInteger(max) || max < 0) {
3370
- throw new Error("maxToolCalls() requires a non-negative integer");
3371
- }
3372
- return { kind: "max_tool_calls", max };
3373
- }
3374
- function toolOrder(tools) {
3375
- if (!Array.isArray(tools) || tools.length === 0) {
3376
- throw new Error("toolOrder() requires a non-empty array of tool names");
3377
- }
3378
- return { kind: "tool_order", tools };
3379
- }
3380
- function ranStep(name) {
3381
- if (typeof name !== "string" || name.length === 0) {
3382
- throw new Error("ranStep() requires a non-empty step name");
3383
- }
3384
- return { kind: "ran_step", name };
3385
- }
3386
- function stepOrder(steps) {
3387
- if (!Array.isArray(steps) || steps.length === 0) {
3388
- throw new Error("stepOrder() requires a non-empty array of step names");
3389
- }
3390
- return { kind: "step_order", steps };
3391
- }
3392
- function completed() {
3393
- return { kind: "completed" };
3394
- }
3395
- function cost(maxUsd) {
3396
- if (!Number.isFinite(maxUsd) || maxUsd <= 0) {
3397
- throw new Error("cost() requires a positive maxUsd");
3398
- }
3399
- return { kind: "cost", maxUsd };
3400
- }
3401
- function judge(criteria, opts) {
3402
- if (typeof criteria !== "string" || criteria.trim().length === 0) {
3403
- throw new Error("judge() requires non-empty criteria");
3404
- }
3405
- return {
3406
- kind: "ai",
3407
- criteria,
3408
- ...opts?.preset ? { preset: opts.preset } : {},
3409
- ...opts?.useExpected ? { useExpected: true } : {},
3410
- ...opts?.model ? { model: opts.model } : {},
3411
- ...opts?.threshold !== void 0 ? { threshold: opts.threshold } : {}
3412
- };
3413
- }
3414
- var judges = {
3415
- answersQuestion: () => judge(
3416
- "The response directly addresses what the user asked, without dodging or answering a different question.",
3417
- { preset: "answersQuestion" }
3418
- ),
3419
- matchesExpected: () => judge(
3420
- "The response conveys the same facts and conclusion as the expected answer. Wording may differ.",
3421
- { preset: "matchesExpected", useExpected: true }
3422
- ),
3423
- followsInstructions: () => judge(
3424
- "The response obeys every instruction in the system prompt (format, tone, constraints, refusals).",
3425
- { preset: "followsInstructions" }
3426
- ),
3427
- grounded: () => judge(
3428
- "Every factual claim in the response is supported by the provided context or the expected answer. Flag anything invented.",
3429
- { preset: "grounded" }
3430
- ),
3431
- rightTone: (voice = "{describe the voice you want}") => judge(`The response matches this voice: ${voice}.`, { preset: "rightTone" }),
3432
- safeToSend: () => judge(
3433
- "The response contains nothing embarrassing to show a customer: no leaked internals, no hostile tone, no policy violations.",
3434
- { preset: "safeToSend" }
3435
- )
3436
- };
3437
- var DEFINE_EVAL_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
3438
- "name",
3439
- "target",
3440
- "graders",
3441
- "cases",
3442
- "virtual"
3443
- ]);
3444
- var DEFINE_EVAL_CASE_KEYS = /* @__PURE__ */ new Set(["name", "input", "expected", "expect"]);
3445
- function isPlainObject2(value) {
3446
- return value !== null && typeof value === "object" && !Array.isArray(value);
3447
- }
3448
- function normalizeTarget(target) {
3449
- if (!isPlainObject2(target)) {
3450
- throw new Error('defineEval requires a "target" object: { flow: name } or { agent: name }');
3451
- }
3452
- const hasFlow = typeof target.flow === "string" && target.flow.length > 0;
3453
- const hasAgent = typeof target.agent === "string" && target.agent.length > 0;
3454
- if (hasFlow === hasAgent) {
3455
- throw new Error(
3456
- 'defineEval "target" must name exactly one of flow or agent: { flow: "name" } XOR { agent: "name" }'
3457
- );
3458
- }
3459
- const extraKeys = Object.keys(target).filter((k) => k !== "flow" && k !== "agent");
3460
- if (extraKeys.length > 0) {
3461
- throw new Error(`defineEval "target" has unknown field(s): ${extraKeys.join(", ")}`);
3462
- }
3463
- return hasFlow ? { flow: target.flow } : { agent: target.agent };
3464
- }
3465
- function validateGrader(grader, where) {
3466
- if (!isPlainObject2(grader) || typeof grader.kind !== "string") {
3467
- throw new Error(`defineEval: ${where} must be a grader object with a string "kind"`);
3468
- }
3469
- if (grader.kind === "ai") {
3470
- if (typeof grader.criteria !== "string" || grader.criteria.trim().length === 0) {
3471
- throw new Error(`defineEval: ${where} is an AI grader and requires non-empty "criteria"`);
3472
- }
3473
- return grader;
3474
- }
3475
- if (!CHECK_GRADER_KINDS.has(grader.kind)) {
3476
- throw new Error(
3477
- `defineEval: ${where} has unknown grader kind "${grader.kind}". Known kinds: ${[...CHECK_GRADER_KINDS].join(", ")}, ai.`
3478
- );
3479
- }
3480
- return grader;
3481
- }
3482
- function normalizeCaseInput(input, where) {
3483
- if (input === void 0) return {};
3484
- if (!isPlainObject2(input)) {
3485
- throw new Error(`defineEval: ${where} "input" must be an object`);
3486
- }
3487
- const out = {};
3488
- if (input.variables !== void 0) {
3489
- if (!isPlainObject2(input.variables)) {
3490
- throw new Error(`defineEval: ${where} "input.variables" must be an object`);
3491
- }
3492
- out.variables = input.variables;
3493
- }
3494
- if (input.messages !== void 0) {
3495
- if (!Array.isArray(input.messages)) {
3496
- throw new Error(`defineEval: ${where} "input.messages" must be an array`);
3497
- }
3498
- out.messages = input.messages.map((m, i) => {
3499
- if (!isPlainObject2(m) || typeof m.role !== "string" || typeof m.content !== "string") {
3500
- throw new Error(`defineEval: ${where} "input.messages[${i}]" must be { role, content }`);
3578
+ if (!is422) {
3579
+ throw err;
3501
3580
  }
3502
- return { role: m.role, content: m.content };
3503
- });
3581
+ }
3582
+ const fullConfig = {
3583
+ ...config,
3584
+ flow: { ...config.flow, contentHash }
3585
+ };
3586
+ return client.dispatch(fullConfig);
3504
3587
  }
3505
- return out;
3506
- }
3507
- function defineEval(input) {
3508
- if (!input || typeof input !== "object") {
3509
- throw new Error("defineEval requires a definition object");
3588
+ async computeContentHash() {
3589
+ return computeFlowContentHash(this.steps);
3510
3590
  }
3511
- const unknownKeys = Object.keys(input).filter((k) => !DEFINE_EVAL_TOP_LEVEL_KEYS.has(k));
3512
- if (unknownKeys.length > 0) {
3513
- throw new Error(
3514
- `defineEval: unknown field(s): ${unknownKeys.join(", ")}. Allowed fields are target, graders, cases, virtual.`
3515
- );
3591
+ addRawStep(type, config) {
3592
+ const { name, enabled, when, ...stepConfig } = config;
3593
+ this.addStep(type, name, stepConfig, enabled, when);
3594
+ return this;
3516
3595
  }
3517
- const target = normalizeTarget(input.target);
3518
- if (input.name !== void 0 && (typeof input.name !== "string" || input.name.length === 0)) {
3519
- throw new Error('defineEval "name" must be a non-empty string when provided');
3596
+ addStep(type, name, config, enabled = true, when) {
3597
+ this.stepCounter++;
3598
+ const cleanConfig = {};
3599
+ for (const [key, value] of Object.entries(config)) {
3600
+ if (value !== void 0) {
3601
+ cleanConfig[key] = value;
3602
+ }
3603
+ }
3604
+ this.steps.push({
3605
+ id: `step-${this.stepCounter}`,
3606
+ type,
3607
+ name,
3608
+ order: this.stepCounter,
3609
+ enabled,
3610
+ ...when ? { when } : {},
3611
+ config: cleanConfig
3612
+ });
3520
3613
  }
3521
- const name = input.name ?? ("flow" in target ? `flow:${target.flow}` : `agent:${target.agent}`);
3522
- const suiteGraders = (input.graders ?? []).map((g, i) => validateGrader(g, `graders[${i}]`));
3523
- if (!Array.isArray(input.cases) || input.cases.length === 0) {
3524
- throw new Error('defineEval requires a non-empty "cases" array');
3614
+ };
3615
+
3616
+ // src/batches-namespace.ts
3617
+ var BatchesNamespace = class {
3618
+ constructor(getClient) {
3619
+ this.getClient = getClient;
3525
3620
  }
3526
- const seenNames = /* @__PURE__ */ new Set();
3527
- const cases = input.cases.map((c, index) => {
3528
- if (!isPlainObject2(c)) {
3529
- throw new Error(`defineEval: cases[${index}] must be an object`);
3530
- }
3531
- if (typeof c.name !== "string" || c.name.length === 0) {
3532
- throw new Error(`defineEval: cases[${index}] requires a non-empty string "name"`);
3533
- }
3534
- if (seenNames.has(c.name)) {
3535
- throw new Error(`defineEval: duplicate case name "${c.name}" (case names are the identity)`);
3536
- }
3537
- seenNames.add(c.name);
3538
- const unknownCaseKeys = Object.keys(c).filter((k) => !DEFINE_EVAL_CASE_KEYS.has(k));
3539
- if (unknownCaseKeys.length > 0) {
3540
- throw new Error(
3541
- `defineEval: cases[${index}] ("${c.name}") has unknown field(s): ${unknownCaseKeys.join(
3542
- ", "
3543
- )}. Allowed case fields are name, input, expected, expect.`
3544
- );
3621
+ /**
3622
+ * Schedule a batch operation
3623
+ *
3624
+ * Creates and schedules a batch to run a flow on all records of a type.
3625
+ * By default, runs immediately. Use `at` to schedule for a specific time.
3626
+ *
3627
+ * @example
3628
+ * ```typescript
3629
+ * // Run immediately
3630
+ * const batch = await Runtype.batches.schedule({
3631
+ * flowId: 'flow_123',
3632
+ * recordType: 'customers',
3633
+ * })
3634
+ *
3635
+ * // Schedule for later
3636
+ * const batch = await Runtype.batches.schedule({
3637
+ * flowId: 'flow_123',
3638
+ * recordType: 'customers',
3639
+ * at: new Date('2024-01-15T09:00:00Z'),
3640
+ * })
3641
+ *
3642
+ * // With options
3643
+ * const batch = await Runtype.batches.schedule({
3644
+ * flowId: 'flow_123',
3645
+ * recordType: 'customers',
3646
+ * concurrency: 5,
3647
+ * continueOnError: true,
3648
+ * filter: { status: 'active' },
3649
+ * limit: 100,
3650
+ * })
3651
+ * ```
3652
+ */
3653
+ async schedule(config) {
3654
+ const client = this.getClient();
3655
+ const payload = {
3656
+ flowId: config.flowId,
3657
+ recordType: config.recordType
3658
+ };
3659
+ if (config.at) {
3660
+ payload.scheduledAt = config.at.toISOString();
3545
3661
  }
3546
- const caseGraders = (c.expect ?? []).map(
3547
- (g, i) => validateGrader(g, `cases[${index}].expect[${i}]`)
3548
- );
3549
- const expect = [...suiteGraders, ...caseGraders];
3550
- if (expect.length === 0) {
3551
- throw new Error(
3552
- `defineEval: cases[${index}] ("${c.name}") has no graders. Add suite-level "graders" or case-level "expect" so there is something to score.`
3553
- );
3662
+ const options = {};
3663
+ if (config.async !== void 0) options.async = config.async;
3664
+ if (config.concurrency !== void 0) options.concurrency = config.concurrency;
3665
+ if (config.continueOnError !== void 0) options.continueOnError = config.continueOnError;
3666
+ if (config.storeResults !== void 0) options.storeResults = config.storeResults;
3667
+ if (config.modelOverride !== void 0) options.modelOverride = config.modelOverride;
3668
+ if (Object.keys(options).length > 0) {
3669
+ payload.options = options;
3554
3670
  }
3555
- if (c.expected !== void 0 && !isPlainObject2(c.expected)) {
3556
- throw new Error(`defineEval: cases[${index}] ("${c.name}") "expected" must be an object`);
3671
+ if (config.filter) {
3672
+ payload.filter = config.filter;
3557
3673
  }
3558
- return {
3559
- name: c.name,
3560
- input: normalizeCaseInput(c.input, `cases[${index}] ("${c.name}")`),
3561
- ...c.expected !== void 0 ? { expected: c.expected } : {},
3562
- expect
3563
- };
3564
- });
3565
- return { name, target, cases, virtual: input.virtual === true };
3566
- }
3567
- function normalizeForHash(value) {
3568
- if (Array.isArray(value)) return value.map(normalizeForHash);
3569
- if (isPlainObject2(value)) {
3570
- const out = {};
3571
- for (const key of Object.keys(value).sort()) {
3572
- const v = value[key];
3573
- if (v === void 0) continue;
3574
- out[key] = normalizeForHash(v);
3674
+ if (config.limit !== void 0) {
3675
+ payload.limit = config.limit;
3575
3676
  }
3576
- return out;
3577
- }
3578
- return value;
3579
- }
3580
- async function computeEvalContentHash(definition) {
3581
- const canonical = {
3582
- target: normalizeForHash(definition.target),
3583
- virtual: definition.virtual,
3584
- cases: [...definition.cases].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((c) => ({
3585
- name: c.name,
3586
- input: normalizeForHash(c.input),
3587
- ...c.expected !== void 0 ? { expected: normalizeForHash(c.expected) } : {},
3588
- // Grader order preserved on purpose (it maps to the result index).
3589
- expect: c.expect.map((g) => normalizeForHash(g))
3590
- }))
3591
- };
3592
- const serialized = JSON.stringify(canonical);
3593
- const encoded = new TextEncoder().encode(serialized);
3594
- const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
3595
- return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
3596
- }
3597
- var serverHashMemo2 = /* @__PURE__ */ new WeakMap();
3598
- function memoFor2(client) {
3599
- let memo = serverHashMemo2.get(client);
3600
- if (!memo) {
3601
- memo = /* @__PURE__ */ new Map();
3602
- serverHashMemo2.set(client, memo);
3677
+ return client.post("/batches", payload);
3603
3678
  }
3604
- return memo;
3605
- }
3606
- async function ensureEval(client, definition) {
3607
- if (definition.virtual) {
3608
- throw new Error(
3609
- "Cannot ensure a virtual eval: virtual evals are ephemeral (nothing is persisted to converge). Remove `virtual: true` to converge a durable suite, or run it directly."
3610
- );
3679
+ /**
3680
+ * Get batch status by ID
3681
+ *
3682
+ * @example
3683
+ * ```typescript
3684
+ * const status = await Runtype.batches.get('batch_456')
3685
+ * console.log(status.status, status.processedRecords, '/', status.totalRecords)
3686
+ * ```
3687
+ */
3688
+ async get(batchId) {
3689
+ const client = this.getClient();
3690
+ return client.get(`/batches/${batchId}`);
3611
3691
  }
3612
- const memo = memoFor2(client);
3613
- const localHash = await computeEvalContentHash(definition);
3614
- const memoKey = `${definition.name} ${localHash}`;
3615
- const contentHash = memo.get(memoKey) ?? localHash;
3616
- const probe = await client.post(
3617
- "/eval/ensure",
3618
- { name: definition.name, contentHash }
3619
- );
3620
- if (probe.result !== "definitionRequired") {
3621
- memo.set(memoKey, probe.contentHash);
3622
- return probe;
3692
+ /**
3693
+ * Cancel a batch operation
3694
+ *
3695
+ * Cancels a queued or running batch. Records already processed are not rolled back.
3696
+ *
3697
+ * @example
3698
+ * ```typescript
3699
+ * await Runtype.batches.cancel('batch_456')
3700
+ * ```
3701
+ */
3702
+ async cancel(batchId) {
3703
+ const client = this.getClient();
3704
+ return client.post(`/batches/${batchId}/cancel`);
3623
3705
  }
3624
- const converged = await client.post(
3625
- "/eval/ensure",
3626
- { name: definition.name, definition }
3627
- );
3628
- if (converged.result === "definitionRequired") {
3629
- throw new Error("Server reported definitionRequired for a full-definition request");
3706
+ /**
3707
+ * List batch operations
3708
+ *
3709
+ * @example
3710
+ * ```typescript
3711
+ * // List all batches
3712
+ * const batches = await Runtype.batches.list()
3713
+ *
3714
+ * // Filter by status
3715
+ * const running = await Runtype.batches.list({ status: 'running' })
3716
+ *
3717
+ * // Filter by flow
3718
+ * const flowBatches = await Runtype.batches.list({ flowId: 'flow_123' })
3719
+ * ```
3720
+ */
3721
+ async list(params) {
3722
+ const client = this.getClient();
3723
+ return client.get("/batches", params);
3630
3724
  }
3631
- memo.set(memoKey, converged.contentHash);
3632
- return converged;
3633
- }
3634
- async function pullEval(client, name) {
3635
- return client.get("/eval/pull", { name });
3636
- }
3637
- async function runEvalSuite(client, input) {
3638
- return client.post("/eval/run", input);
3639
- }
3725
+ };
3640
3726
 
3641
3727
  // src/evals-namespace.ts
3642
3728
  var EvalRunner = class {
@@ -5916,7 +6002,7 @@ var Runtype = class {
5916
6002
 
5917
6003
  // src/version.ts
5918
6004
  var FALLBACK_VERSION = "0.0.0";
5919
- var SDK_VERSION = "5.5.0".length > 0 ? "5.5.0" : FALLBACK_VERSION;
6005
+ var SDK_VERSION = "5.6.0".length > 0 ? "5.6.0" : FALLBACK_VERSION;
5920
6006
  var RUNTYPE_CLIENT_KIND = "sdk";
5921
6007
  var SDK_USER_AGENT = `runtype-sdk/${SDK_VERSION} (typescript)`;
5922
6008