@runtypelabs/sdk 10.8.1 → 10.11.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.cjs CHANGED
@@ -2080,21 +2080,6 @@ var AgentAliasNotFoundError = class extends Error {
2080
2080
  this.agentId = body.agentId;
2081
2081
  }
2082
2082
  };
2083
- /**
2084
- * A new preview pointer refused by the active-preview quota (HTTP 429). Archive
2085
- * a preview you no longer need — archiving frees the quota immediately — or wait
2086
- * for one to expire. `live` is never counted.
2087
- */
2088
- var AgentAliasPreviewLimitError = class extends Error {
2089
- constructor(body) {
2090
- super(body.error ?? `This ${body.scope} already has ${body.limit} active preview aliases, the maximum.`);
2091
- this.code = "PREVIEW_ALIAS_LIMIT";
2092
- this.name = "AgentAliasPreviewLimitError";
2093
- this.scope = body.scope;
2094
- this.limit = body.limit;
2095
- this.active = body.active;
2096
- }
2097
- };
2098
2083
  /** A version whose named references no longer resolve in this account (HTTP 422). */
2099
2084
  var AgentAliasDependencyError = class extends Error {
2100
2085
  constructor(body) {
@@ -2114,7 +2099,7 @@ function asRecord(value) {
2114
2099
  * Reading the structured fields FIRST is what makes the typed alias errors fire
2115
2100
  * for `createClient()` and the CLI, not just for `Runtype.agents`.
2116
2101
  */
2117
- function parseRequestError$6(err) {
2102
+ function parseRequestError$7(err) {
2118
2103
  if (!(err instanceof Error)) return {
2119
2104
  status: null,
2120
2105
  body: null
@@ -2149,7 +2134,7 @@ function asString$1(value, fallback) {
2149
2134
  * failure exactly as the transport threw it.
2150
2135
  */
2151
2136
  function toAliasError(err, agentId, alias) {
2152
- const { status, body } = parseRequestError$6(err);
2137
+ const { status, body } = parseRequestError$7(err);
2153
2138
  if (status === null) return null;
2154
2139
  const error = body && typeof body.error === "string" ? body.error : void 0;
2155
2140
  if (status === 412 && body && typeof body.expected === "number") return new AgentAliasRevisionMismatchError({
@@ -2163,12 +2148,6 @@ function toAliasError(err, agentId, alias) {
2163
2148
  alias: asString$1(body.alias, alias),
2164
2149
  agentId: asString$1(body.agentId, agentId)
2165
2150
  });
2166
- if (status === 429 && body?.code === "PREVIEW_ALIAS_LIMIT") return new AgentAliasPreviewLimitError({
2167
- ...error !== void 0 ? { error } : {},
2168
- scope: body.scope === "organization" ? "organization" : "agent",
2169
- limit: typeof body.limit === "number" ? body.limit : 0,
2170
- active: typeof body.active === "number" ? body.active : 0
2171
- });
2172
2151
  if (status === 422 && body?.code === "alias_dependency_unresolved") return new AgentAliasDependencyError({
2173
2152
  ...error !== void 0 ? { error } : {},
2174
2153
  refs: Array.isArray(body.refs) ? body.refs.filter((ref) => typeof ref === "string") : []
@@ -2179,8 +2158,7 @@ const TYPED_ALIAS_ERRORS = [
2179
2158
  AgentAliasRevisionMismatchError,
2180
2159
  AgentAliasRevisionRequiredError,
2181
2160
  AgentAliasNotFoundError,
2182
- AgentAliasDependencyError,
2183
- AgentAliasPreviewLimitError
2161
+ AgentAliasDependencyError
2184
2162
  ];
2185
2163
  /** The refusal code, when this is one of the typed alias errors. */
2186
2164
  function agentAliasErrorCode(err) {
@@ -2339,6 +2317,174 @@ var AgentDeploymentsNamespace = class {
2339
2317
  }
2340
2318
  };
2341
2319
  //#endregion
2320
+ //#region src/content-hash.ts
2321
+ /**
2322
+ * Canonical content-hash primitives shared by the `*.ensure()` config-as-code
2323
+ * surfaces (products, tools, skills, surfaces, products-ensure-fpo). Each of
2324
+ * those modules computes a SHA-256 content hash for its hash-first `/ensure`
2325
+ * wire protocol (see docs/adr/0003-agent-config-as-code-ensure.md) — the hash
2326
+ * output for any given definition is a WIRE CONTRACT with the server, so this
2327
+ * module must stay dependency-free (the SDK is published to npm and must not
2328
+ * import `@runtypelabs/shared`, a private package) and must not change its
2329
+ * normalization behavior without a matching server-side change.
2330
+ *
2331
+ * `isPlainObject` and `normalizeValue` here are mirrored from
2332
+ * `packages/shared/src/utils/*-content-hash.ts`. `normalizeValue` sorts object
2333
+ * keys and drops `null`/`undefined` entries recursively — this is the
2334
+ * canonical normalization used by products, tools, skills, surfaces, and the
2335
+ * whole-FPO hash.
2336
+ *
2337
+ * NOT every `*-ensure.ts` module uses this normalization: `flows-ensure.ts`
2338
+ * (`normalizeConfigForHash`/`normalizeStepForHash`) and `evals-ensure.ts`
2339
+ * (`normalizeForHash`) keep `null` values (they drop only `undefined`) and,
2340
+ * in the flow case, use a shallower array-recursion rule. Those are real
2341
+ * semantic divergences from this canonical normalizer, not incidental
2342
+ * duplication — changing either to reuse `normalizeValue` would alter the
2343
+ * content hash for definitions containing explicit `null`s and break the
2344
+ * wire contract with already-converged flows/evals. They stay local by
2345
+ * design; see the divergence comment at each site.
2346
+ */
2347
+ function isPlainObject$1(value) {
2348
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2349
+ }
2350
+ /**
2351
+ * Recursively normalizes a value for canonical hashing: object keys are
2352
+ * sorted, and `null`/`undefined` entries are dropped. Arrays preserve order
2353
+ * and have each item normalized in turn.
2354
+ */
2355
+ function normalizeValue$1(value) {
2356
+ if (Array.isArray(value)) return value.map((item) => normalizeValue$1(item));
2357
+ if (isPlainObject$1(value)) {
2358
+ const normalized = {};
2359
+ for (const key of Object.keys(value).sort()) {
2360
+ const entry = value[key];
2361
+ if (entry === void 0 || entry === null) continue;
2362
+ normalized[key] = normalizeValue$1(entry);
2363
+ }
2364
+ return normalized;
2365
+ }
2366
+ return value;
2367
+ }
2368
+ /** SHA-256 (hex) over an already-serialized (typically `JSON.stringify`'d) string. */
2369
+ async function sha256Hex(serialized) {
2370
+ const encoded = new TextEncoder().encode(serialized);
2371
+ const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
2372
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
2373
+ }
2374
+ //#endregion
2375
+ //#region src/collections-ensure.ts
2376
+ /** Canonical normalized form of a collection definition (slug excluded — identity). */
2377
+ function normalizeRecordCollectionDefinition(definition) {
2378
+ return {
2379
+ displayName: definition.displayName,
2380
+ ...definition.description ? { description: definition.description } : {},
2381
+ ...definition.icon ? { icon: definition.icon } : {},
2382
+ ...isPlainObject$1(definition.schema) ? { schema: normalizeValue$1(definition.schema) } : {},
2383
+ validationMode: definition.validationMode || "off"
2384
+ };
2385
+ }
2386
+ /** SHA-256 (hex) over the canonical normalized collection definition. */
2387
+ async function computeRecordCollectionContentHash(definition) {
2388
+ return sha256Hex(JSON.stringify(normalizeRecordCollectionDefinition(definition)));
2389
+ }
2390
+ /** A 409 from the ensure protocol (external_modification or remote_changed). */
2391
+ var CollectionEnsureConflictError = class extends Error {
2392
+ constructor(body) {
2393
+ super(body.error ?? `Collection ensure conflict: ${body.code}`);
2394
+ this.name = "CollectionEnsureConflictError";
2395
+ this.code = body.code;
2396
+ this.lastModifiedSource = body.lastModifiedSource;
2397
+ this.modifiedAt = body.modifiedAt;
2398
+ this.currentHash = body.currentHash;
2399
+ }
2400
+ };
2401
+ /** Thrown by `ensure(def, { expectNoChanges: true })` when drift is detected. */
2402
+ var CollectionDriftError = class extends Error {
2403
+ constructor(slug, plan) {
2404
+ super(`Collection "${slug}" drifted: plan is '${plan.changes}' (changed: ${plan.changedKeys.join(", ") || "n/a"}). Run client.collections.pull(slug) to absorb the remote edit into your repo, or re-run ensure to converge.`);
2405
+ this.name = "CollectionDriftError";
2406
+ this.plan = plan;
2407
+ }
2408
+ };
2409
+ function toConflictError$8(err) {
2410
+ if (!(err instanceof Error)) return null;
2411
+ const match = err.message.match(/^API request failed: (\d{3}) .*? - ([\s\S]*)$/);
2412
+ if (!match || match[1] !== "409") return null;
2413
+ let body;
2414
+ try {
2415
+ body = JSON.parse(match[2]);
2416
+ } catch {
2417
+ return null;
2418
+ }
2419
+ if (!isPlainObject$1(body)) return null;
2420
+ if (body.code !== "external_modification" && body.code !== "remote_changed") return null;
2421
+ return new CollectionEnsureConflictError(body);
2422
+ }
2423
+ /** Per-transport memo of `{ slug + localHash → server contentHash }`, as in tools.ensure. */
2424
+ const serverHashMemo$9 = /* @__PURE__ */ new WeakMap();
2425
+ function memoFor$9(transport) {
2426
+ let memo = serverHashMemo$9.get(transport);
2427
+ if (!memo) {
2428
+ memo = /* @__PURE__ */ new Map();
2429
+ serverHashMemo$9.set(transport, memo);
2430
+ }
2431
+ return memo;
2432
+ }
2433
+ async function request$7(transport, body) {
2434
+ try {
2435
+ return await transport.post("/collections/ensure", body);
2436
+ } catch (err) {
2437
+ throw toConflictError$8(err) ?? err;
2438
+ }
2439
+ }
2440
+ /**
2441
+ * Idempotently converge a collection definition. Probes with a content hash and
2442
+ * only ships the full definition when the server reports a miss. Never deletes
2443
+ * a collection and never touches records.
2444
+ */
2445
+ async function ensureCollection(transport, definition, options = {}) {
2446
+ const { dryRun, onConflict, expectedRemoteHash, expectNoChanges } = options;
2447
+ const passthrough = {
2448
+ ...onConflict ? { onConflict } : {},
2449
+ ...expectedRemoteHash ? { expectedRemoteHash } : {}
2450
+ };
2451
+ if (dryRun || expectNoChanges) {
2452
+ const plan = await request$7(transport, {
2453
+ slug: definition.slug,
2454
+ definition,
2455
+ dryRun: true,
2456
+ ...passthrough
2457
+ });
2458
+ if (plan.result !== "plan") throw new Error(`Expected a plan result from dryRun, got '${plan.result}'`);
2459
+ if (expectNoChanges && plan.changes !== "none") throw new CollectionDriftError(definition.slug, plan);
2460
+ return plan;
2461
+ }
2462
+ const memo = memoFor$9(transport);
2463
+ const localHash = await computeRecordCollectionContentHash(definition);
2464
+ const memoKey = `${definition.slug} ${localHash}`;
2465
+ const probe = await request$7(transport, {
2466
+ slug: definition.slug,
2467
+ contentHash: memo.get(memoKey) ?? localHash,
2468
+ ...passthrough
2469
+ });
2470
+ if (probe.result !== "definitionRequired") {
2471
+ if (probe.result !== "plan") memo.set(memoKey, probe.contentHash);
2472
+ return probe;
2473
+ }
2474
+ const converged = await request$7(transport, {
2475
+ slug: definition.slug,
2476
+ definition,
2477
+ ...passthrough
2478
+ });
2479
+ if (converged.result === "definitionRequired") throw new Error("Server reported definitionRequired for a full-definition request");
2480
+ if (converged.result !== "plan") memo.set(memoKey, converged.contentHash);
2481
+ return converged;
2482
+ }
2483
+ /** Pull the canonical definition and provenance for a collection by slug. */
2484
+ async function pullCollection(transport, slug) {
2485
+ return transport.get(`/collections/${encodeURIComponent(slug)}/pull`);
2486
+ }
2487
+ //#endregion
2342
2488
  //#region src/generated-tool-gate.ts
2343
2489
  const TOOL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]{1,63}$/;
2344
2490
  const DEFAULT_MAX_CODE_LENGTH = 12e3;
@@ -2667,6 +2813,239 @@ function normalizeDispatchRequest(request) {
2667
2813
  };
2668
2814
  }
2669
2815
  //#endregion
2816
+ //#region src/schedules-ensure.ts
2817
+ function nonEmptyObject(value) {
2818
+ if (!isPlainObject$1(value)) return void 0;
2819
+ const normalized = normalizeValue$1(value);
2820
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
2821
+ }
2822
+ function nonEmptyString(value) {
2823
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2824
+ }
2825
+ function canonicalRunAt(value) {
2826
+ if (value === null || value === void 0 || value === "") return void 0;
2827
+ const date = value instanceof Date ? value : new Date(value);
2828
+ return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();
2829
+ }
2830
+ /** Canonical normalized schedule content (name and target excluded — identity). */
2831
+ function normalizeScheduleContent(definition) {
2832
+ const cron = nonEmptyString(definition.cron);
2833
+ const timezone = nonEmptyString(definition.timezone);
2834
+ const runAt = canonicalRunAt(definition.runAt);
2835
+ const recordIds = Array.isArray(definition.recordIds) && definition.recordIds.length > 0 ? [...definition.recordIds] : void 0;
2836
+ const recordType = nonEmptyString(definition.recordType);
2837
+ const recordFilter = nonEmptyObject(definition.recordFilter);
2838
+ const messages = Array.isArray(definition.messages) && definition.messages.length > 0 ? normalizeValue$1(definition.messages) : void 0;
2839
+ const inputs = nonEmptyObject(definition.inputs);
2840
+ const executionOptions = nonEmptyObject(definition.executionOptions);
2841
+ return {
2842
+ triggerType: definition.triggerType === "one_time" ? "one_time" : "recurring",
2843
+ ...cron !== void 0 ? { cron } : {},
2844
+ ...timezone !== void 0 ? { timezone } : {},
2845
+ ...runAt !== void 0 ? { runAt } : {},
2846
+ ...recordIds !== void 0 ? { recordIds } : {},
2847
+ ...recordType !== void 0 ? { recordType } : {},
2848
+ ...recordFilter !== void 0 ? { recordFilter } : {},
2849
+ ...messages !== void 0 ? { messages } : {},
2850
+ ...inputs !== void 0 ? { inputs } : {},
2851
+ ...executionOptions !== void 0 ? { executionOptions } : {},
2852
+ ...typeof definition.enabled === "boolean" ? { enabled: definition.enabled } : {}
2853
+ };
2854
+ }
2855
+ /** SHA-256 (hex) over canonical schedule content, in the flat content shape. */
2856
+ async function computeScheduleContentHash(definition) {
2857
+ return sha256Hex(JSON.stringify(normalizeScheduleContent(definition)));
2858
+ }
2859
+ /** The content hash of a `defineSchedule` definition. */
2860
+ async function computeScheduleDefinitionHash(definition) {
2861
+ return computeScheduleContentHash({
2862
+ triggerType: definition.trigger.type,
2863
+ cron: definition.trigger.cron,
2864
+ timezone: definition.trigger.timezone,
2865
+ runAt: definition.trigger.runAt,
2866
+ recordIds: definition.recordIds,
2867
+ recordType: definition.recordType,
2868
+ recordFilter: definition.recordFilter,
2869
+ messages: definition.messages,
2870
+ inputs: definition.inputs,
2871
+ executionOptions: definition.executionOptions,
2872
+ enabled: definition.enabled
2873
+ });
2874
+ }
2875
+ const TARGET_KEYS = [
2876
+ "flowId",
2877
+ "agentId",
2878
+ "flowName",
2879
+ "agentName"
2880
+ ];
2881
+ const DEFINE_SCHEDULE_KEYS = /* @__PURE__ */ new Set([
2882
+ "name",
2883
+ "target",
2884
+ "trigger",
2885
+ "recordIds",
2886
+ "recordType",
2887
+ "recordFilter",
2888
+ "messages",
2889
+ "inputs",
2890
+ "executionOptions",
2891
+ "enabled"
2892
+ ]);
2893
+ /**
2894
+ * Pure-local declarative constructor for a standalone schedule. No I/O.
2895
+ * Validates structure; cron, timezone, record filter and plan limits are
2896
+ * checked server-side at ensure time.
2897
+ *
2898
+ * @example
2899
+ * ```typescript
2900
+ * const nightly = defineSchedule({
2901
+ * name: 'Nightly digest',
2902
+ * target: { flowName: 'Digest' },
2903
+ * trigger: { type: 'recurring', cron: '0 2 * * *', timezone: 'UTC' },
2904
+ * })
2905
+ * ```
2906
+ */
2907
+ function defineSchedule(input) {
2908
+ if (!input || typeof input !== "object") throw new Error("defineSchedule requires a definition object");
2909
+ if (typeof input.name !== "string" || input.name.length === 0) throw new Error("defineSchedule requires a non-empty string \"name\"");
2910
+ if (!isPlainObject$1(input.target)) throw new Error("defineSchedule requires a \"target\" object");
2911
+ const target = input.target;
2912
+ const setKeys = TARGET_KEYS.filter((key) => target[key] !== void 0);
2913
+ const extraKeys = Object.keys(target).filter((key) => !TARGET_KEYS.includes(key));
2914
+ if (setKeys.length !== 1 || extraKeys.length > 0) throw new Error("defineSchedule \"target\" needs exactly one of flowId, agentId, flowName, agentName");
2915
+ const trigger = input.trigger;
2916
+ if (!isPlainObject$1(trigger) || trigger.type !== "recurring" && trigger.type !== "one_time") throw new Error("defineSchedule requires \"trigger.type\" to be \"recurring\" or \"one_time\"");
2917
+ if (trigger.type === "recurring" && (!trigger.cron || !trigger.timezone)) throw new Error("defineSchedule: recurring triggers require \"cron\" and \"timezone\"");
2918
+ if (trigger.type === "one_time" && !trigger.runAt) throw new Error("defineSchedule: one_time triggers require \"runAt\"");
2919
+ const unknownKeys = Object.keys(input).filter((key) => !DEFINE_SCHEDULE_KEYS.has(key));
2920
+ if (unknownKeys.length > 0) throw new Error(`defineSchedule: unknown field(s): ${unknownKeys.join(", ")}. Allowed fields are ${[...DEFINE_SCHEDULE_KEYS].join(", ")}.`);
2921
+ return { ...input };
2922
+ }
2923
+ /** A 409 from the ensure protocol (external_modification or remote_changed). */
2924
+ var ScheduleEnsureConflictError = class extends Error {
2925
+ constructor(body) {
2926
+ super(body.error ?? `Schedule ensure conflict: ${body.code}`);
2927
+ this.name = "ScheduleEnsureConflictError";
2928
+ this.code = body.code;
2929
+ this.lastModifiedSource = body.lastModifiedSource;
2930
+ this.modifiedAt = body.modifiedAt;
2931
+ this.currentHash = body.currentHash;
2932
+ }
2933
+ };
2934
+ /** Thrown by `ensure(def, { expectNoChanges: true })` when drift is detected. */
2935
+ var ScheduleDriftError = class extends Error {
2936
+ constructor(plan) {
2937
+ super(`Schedule "${plan.scheduleId ?? "definition"}" drifted: plan is '${plan.changes}' (changed: ${plan.changedKeys.join(", ") || "n/a"}). Run schedules.pull(name, target) to absorb the remote edit into your repo, or re-run ensure to converge.`);
2938
+ this.name = "ScheduleDriftError";
2939
+ this.plan = plan;
2940
+ }
2941
+ };
2942
+ function errorStatusAndBody(err) {
2943
+ if (!(err instanceof Error)) return {
2944
+ status: null,
2945
+ body: null
2946
+ };
2947
+ const typed = err;
2948
+ if (typeof typed.statusCode === "number") return {
2949
+ status: typed.statusCode,
2950
+ body: typed.data
2951
+ };
2952
+ const match = err.message.match(/^API request failed: (\d{3}) .*? - ([\s\S]*)$/);
2953
+ if (!match) return {
2954
+ status: null,
2955
+ body: null
2956
+ };
2957
+ try {
2958
+ return {
2959
+ status: Number(match[1]),
2960
+ body: JSON.parse(match[2])
2961
+ };
2962
+ } catch {
2963
+ return {
2964
+ status: Number(match[1]),
2965
+ body: null
2966
+ };
2967
+ }
2968
+ }
2969
+ function toConflictError$7(err) {
2970
+ const { status, body } = errorStatusAndBody(err);
2971
+ if (status !== 409 || !isPlainObject$1(body)) return null;
2972
+ if (body.code !== "external_modification" && body.code !== "remote_changed") return null;
2973
+ return new ScheduleEnsureConflictError(body);
2974
+ }
2975
+ const serverHashMemo$8 = /* @__PURE__ */ new WeakMap();
2976
+ function memoFor$8(client) {
2977
+ let memo = serverHashMemo$8.get(client);
2978
+ if (!memo) {
2979
+ memo = /* @__PURE__ */ new Map();
2980
+ serverHashMemo$8.set(client, memo);
2981
+ }
2982
+ return memo;
2983
+ }
2984
+ async function request$6(client, body) {
2985
+ try {
2986
+ return await client.post("/schedules/ensure", body);
2987
+ } catch (err) {
2988
+ const conflict = toConflictError$7(err);
2989
+ if (conflict) throw conflict;
2990
+ throw err;
2991
+ }
2992
+ }
2993
+ /**
2994
+ * Idempotently converge a standalone schedule. Hash-first: probes with a
2995
+ * content hash and ships the full definition only on a miss. Never deletes.
2996
+ */
2997
+ async function ensureSchedule(client, definition, options = {}) {
2998
+ const { dryRun, onConflict, expectedRemoteHash, expectNoChanges } = options;
2999
+ const identity = {
3000
+ name: definition.name,
3001
+ target: definition.target
3002
+ };
3003
+ const passthrough = {
3004
+ ...onConflict ? { onConflict } : {},
3005
+ ...expectedRemoteHash ? { expectedRemoteHash } : {}
3006
+ };
3007
+ if (dryRun || expectNoChanges) {
3008
+ const plan = await request$6(client, {
3009
+ ...identity,
3010
+ definition,
3011
+ dryRun: true,
3012
+ ...passthrough
3013
+ });
3014
+ if (plan.result !== "plan") throw new Error(`Expected a plan result from dryRun, got '${plan.result}'`);
3015
+ if (expectNoChanges && plan.changes !== "none") throw new ScheduleDriftError(plan);
3016
+ return plan;
3017
+ }
3018
+ const memo = memoFor$8(client);
3019
+ const localHash = await computeScheduleDefinitionHash(definition);
3020
+ const memoKey = `${JSON.stringify(identity)} ${localHash}`;
3021
+ if (!expectedRemoteHash) {
3022
+ const probe = await request$6(client, {
3023
+ ...identity,
3024
+ contentHash: memo.get(memoKey) ?? localHash,
3025
+ ...passthrough
3026
+ });
3027
+ if (probe.result !== "definitionRequired") {
3028
+ if (probe.result !== "plan") memo.set(memoKey, probe.contentHash);
3029
+ return probe;
3030
+ }
3031
+ }
3032
+ const converged = await request$6(client, {
3033
+ ...identity,
3034
+ definition,
3035
+ ...passthrough
3036
+ });
3037
+ if (converged.result === "definitionRequired") throw new Error("Server reported definitionRequired for a full-definition request");
3038
+ if (converged.result !== "plan") memo.set(memoKey, converged.contentHash);
3039
+ return converged;
3040
+ }
3041
+ /** Pull the canonical definition + provenance for a schedule by name and target. */
3042
+ async function pullSchedule(client, name, target) {
3043
+ return client.get("/schedules/pull", {
3044
+ name,
3045
+ ...target
3046
+ });
3047
+ }
3048
+ //#endregion
2670
3049
  //#region src/offload-markers.ts
2671
3050
  const LEDGER_ARTIFACT_LINE_PREFIX = "Ledger artifact: ";
2672
3051
  const SPILL_MARKER_PATTERN = /\[Output saved to (art_[A-Za-z0-9_-]+) \(([\d,]+) chars?\); read_offloaded_output to view\]/;
@@ -4640,6 +5019,28 @@ var CollectionsEndpoint = class {
4640
5019
  async typegen() {
4641
5020
  return this.client.get("/collections/types.d.ts");
4642
5021
  }
5022
+ /**
5023
+ * Idempotently converge a repo-declared collection (display fields, schema,
5024
+ * validationMode) by slug. Hash-first; `dryRun` / `expectNoChanges` plan
5025
+ * without writing. Never deletes a collection or touches records.
5026
+ *
5027
+ * @example
5028
+ * ```typescript
5029
+ * await client.collections.ensure({
5030
+ * slug: 'customers',
5031
+ * displayName: 'Customers',
5032
+ * schema: { type: 'object', properties: { status: { type: 'string' } } },
5033
+ * validationMode: 'warn',
5034
+ * })
5035
+ * ```
5036
+ */
5037
+ async ensure(definition, options) {
5038
+ return ensureCollection(this.client, definition, options);
5039
+ }
5040
+ /** Pull the canonical definition and provenance for a collection — the absorb-drift direction. */
5041
+ async pull(slug) {
5042
+ return pullCollection(this.client, slug);
5043
+ }
4643
5044
  };
4644
5045
  /**
4645
5046
  * Agent API key REQUESTS — a device-authorization-style grant.
@@ -8172,6 +8573,21 @@ var SchedulesEndpoint = class {
8172
8573
  return this.client.post("/schedules", data);
8173
8574
  }
8174
8575
  /**
8576
+ * Idempotently converge a `defineSchedule` definition on a standalone flow or
8577
+ * agent. Identity is name + target; the steady state is one hash probe. Never
8578
+ * deletes, and an omitted `enabled` leaves a paused schedule paused.
8579
+ */
8580
+ async ensure(definition, options = {}) {
8581
+ return ensureSchedule(this.client, definition, options);
8582
+ }
8583
+ /**
8584
+ * Pull the canonical definition + provenance for a schedule by name and
8585
+ * target — the absorb-drift direction of `ensure`.
8586
+ */
8587
+ async pull(name, target) {
8588
+ return pullSchedule(this.client, name, target);
8589
+ }
8590
+ /**
8175
8591
  * Update a schedule (partial update supported).
8176
8592
  */
8177
8593
  async update(id, data) {
@@ -8667,61 +9083,6 @@ async function followDetachedResume(client, ack, after, carried = {}) {
8667
9083
  };
8668
9084
  }
8669
9085
  //#endregion
8670
- //#region src/content-hash.ts
8671
- /**
8672
- * Canonical content-hash primitives shared by the `*.ensure()` config-as-code
8673
- * surfaces (products, tools, skills, surfaces, products-ensure-fpo). Each of
8674
- * those modules computes a SHA-256 content hash for its hash-first `/ensure`
8675
- * wire protocol (see docs/adr/0003-agent-config-as-code-ensure.md) — the hash
8676
- * output for any given definition is a WIRE CONTRACT with the server, so this
8677
- * module must stay dependency-free (the SDK is published to npm and must not
8678
- * import `@runtypelabs/shared`, a private package) and must not change its
8679
- * normalization behavior without a matching server-side change.
8680
- *
8681
- * `isPlainObject` and `normalizeValue` here are mirrored from
8682
- * `packages/shared/src/utils/*-content-hash.ts`. `normalizeValue` sorts object
8683
- * keys and drops `null`/`undefined` entries recursively — this is the
8684
- * canonical normalization used by products, tools, skills, surfaces, and the
8685
- * whole-FPO hash.
8686
- *
8687
- * NOT every `*-ensure.ts` module uses this normalization: `flows-ensure.ts`
8688
- * (`normalizeConfigForHash`/`normalizeStepForHash`) and `evals-ensure.ts`
8689
- * (`normalizeForHash`) keep `null` values (they drop only `undefined`) and,
8690
- * in the flow case, use a shallower array-recursion rule. Those are real
8691
- * semantic divergences from this canonical normalizer, not incidental
8692
- * duplication — changing either to reuse `normalizeValue` would alter the
8693
- * content hash for definitions containing explicit `null`s and break the
8694
- * wire contract with already-converged flows/evals. They stay local by
8695
- * design; see the divergence comment at each site.
8696
- */
8697
- function isPlainObject$1(value) {
8698
- return value !== null && typeof value === "object" && !Array.isArray(value);
8699
- }
8700
- /**
8701
- * Recursively normalizes a value for canonical hashing: object keys are
8702
- * sorted, and `null`/`undefined` entries are dropped. Arrays preserve order
8703
- * and have each item normalized in turn.
8704
- */
8705
- function normalizeValue$1(value) {
8706
- if (Array.isArray(value)) return value.map((item) => normalizeValue$1(item));
8707
- if (isPlainObject$1(value)) {
8708
- const normalized = {};
8709
- for (const key of Object.keys(value).sort()) {
8710
- const entry = value[key];
8711
- if (entry === void 0 || entry === null) continue;
8712
- normalized[key] = normalizeValue$1(entry);
8713
- }
8714
- return normalized;
8715
- }
8716
- return value;
8717
- }
8718
- /** SHA-256 (hex) over an already-serialized (typically `JSON.stringify`'d) string. */
8719
- async function sha256Hex(serialized) {
8720
- const encoded = new TextEncoder().encode(serialized);
8721
- const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
8722
- return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
8723
- }
8724
- //#endregion
8725
9086
  //#region src/evals-ensure.ts
8726
9087
  const CHECK_GRADER_KINDS = /* @__PURE__ */ new Set([
8727
9088
  "contains",
@@ -9115,6 +9476,25 @@ async function computeEvalContentHash(definition) {
9115
9476
  };
9116
9477
  return sha256Hex(JSON.stringify(canonical));
9117
9478
  }
9479
+ /** A 409 from the eval ensure protocol (external_modification or remote_changed). */
9480
+ var EvalEnsureConflictError = class extends Error {
9481
+ constructor(body) {
9482
+ super(body.error ?? `Eval ensure conflict: ${body.code}`);
9483
+ this.name = "EvalEnsureConflictError";
9484
+ this.code = body.code;
9485
+ this.lastModifiedSource = body.lastModifiedSource;
9486
+ this.modifiedAt = body.modifiedAt;
9487
+ this.currentHash = body.currentHash;
9488
+ }
9489
+ };
9490
+ /** Thrown by `ensure(def, { expectNoChanges: true })` when drift is detected. */
9491
+ var EvalDriftError = class extends Error {
9492
+ constructor(plan) {
9493
+ super(`Eval suite "${plan.suiteId ?? "definition"}" drifted: plan is '${plan.changes}' (changed: ${plan.changedKeys.join(", ") || "n/a"}). Run client.evals.pull(name) to absorb the remote edit into your repo, or re-run ensure to converge.`);
9494
+ this.name = "EvalDriftError";
9495
+ this.plan = plan;
9496
+ }
9497
+ };
9118
9498
  /**
9119
9499
  * Per-client memo of `{ name + localHash → server contentHash }` so a hot
9120
9500
  * process pays the full-definition round trip once per boot (steady state
@@ -9122,44 +9502,75 @@ async function computeEvalContentHash(definition) {
9122
9502
  * hash, so a definition that changes within the process can never false-match a
9123
9503
  * stale server hash. Mirrors the flow ensure memo.
9124
9504
  */
9125
- const serverHashMemo$6 = /* @__PURE__ */ new WeakMap();
9126
- function memoFor$6(client) {
9127
- let memo = serverHashMemo$6.get(client);
9505
+ const serverHashMemo$7 = /* @__PURE__ */ new WeakMap();
9506
+ function memoFor$7(client) {
9507
+ let memo = serverHashMemo$7.get(client);
9128
9508
  if (!memo) {
9129
9509
  memo = /* @__PURE__ */ new Map();
9130
- serverHashMemo$6.set(client, memo);
9510
+ serverHashMemo$7.set(client, memo);
9131
9511
  }
9132
9512
  return memo;
9133
9513
  }
9134
- /**
9135
- * Idempotently converge an eval suite definition onto the platform. Hash-first:
9136
- * probes with a content hash, and only ships the full definition when the
9137
- * server reports a miss (`definitionRequired`). Upserts the suite + replaces
9138
- * its cases; never executes the eval (use the `runtype eval` CLI / `/eval`
9139
- * submit surface to run it).
9140
- *
9141
- * `virtual: true` definitions are ephemeral and have nothing durable to
9142
- * converge — ensure rejects them. Run a virtual eval directly instead.
9143
- */
9144
- async function ensureEval(client, definition) {
9514
+ function toEvalConflictError(err) {
9515
+ if (!(err instanceof Error)) return null;
9516
+ const match = err.message.match(/^API request failed: 409 .*? - ([\s\S]*)$/);
9517
+ if (!match) return null;
9518
+ let body;
9519
+ try {
9520
+ body = JSON.parse(match[1]);
9521
+ } catch {
9522
+ return null;
9523
+ }
9524
+ if (!isPlainObject$1(body)) return null;
9525
+ if (body.code !== "external_modification" && body.code !== "remote_changed") return null;
9526
+ return new EvalEnsureConflictError(body);
9527
+ }
9528
+ async function postEvalEnsure(client, body) {
9529
+ try {
9530
+ return await client.post("/eval/ensure", body);
9531
+ } catch (err) {
9532
+ throw toEvalConflictError(err) ?? err;
9533
+ }
9534
+ }
9535
+ async function ensureEval(client, definition, options = {}) {
9145
9536
  if (definition.virtual) throw new Error("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.");
9146
- const memo = memoFor$6(client);
9537
+ const { dryRun, onConflict, expectedRemoteHash, expectNoChanges } = options;
9538
+ const passthrough = {
9539
+ ...onConflict ? { onConflict } : {},
9540
+ ...expectedRemoteHash ? { expectedRemoteHash } : {}
9541
+ };
9542
+ if (dryRun || expectNoChanges) {
9543
+ const plan = await postEvalEnsure(client, {
9544
+ name: definition.name,
9545
+ definition,
9546
+ dryRun: true,
9547
+ ...passthrough
9548
+ });
9549
+ if (plan.result !== "plan") throw new Error(`Expected a plan result from dryRun, got '${plan.result}'`);
9550
+ if (expectNoChanges && plan.changes !== "none") throw new EvalDriftError(plan);
9551
+ return plan;
9552
+ }
9553
+ const memo = memoFor$7(client);
9147
9554
  const localHash = await computeEvalContentHash(definition);
9148
9555
  const memoKey = `${definition.name} ${localHash}`;
9149
9556
  const contentHash = memo.get(memoKey) ?? localHash;
9150
- const probe = await client.post("/eval/ensure", {
9557
+ const probe = await postEvalEnsure(client, {
9151
9558
  name: definition.name,
9152
- contentHash
9559
+ contentHash,
9560
+ ...passthrough
9153
9561
  });
9562
+ if (probe.result === "plan") throw new Error("Server returned a plan for a non-dryRun request");
9154
9563
  if (probe.result !== "definitionRequired") {
9155
9564
  memo.set(memoKey, probe.contentHash);
9156
9565
  return probe;
9157
9566
  }
9158
- const converged = await client.post("/eval/ensure", {
9567
+ const converged = await postEvalEnsure(client, {
9159
9568
  name: definition.name,
9160
- definition
9569
+ definition,
9570
+ ...passthrough
9161
9571
  });
9162
9572
  if (converged.result === "definitionRequired") throw new Error("Server reported definitionRequired for a full-definition request");
9573
+ if (converged.result === "plan") throw new Error("Server returned a plan for a non-dryRun request");
9163
9574
  memo.set(memoKey, converged.contentHash);
9164
9575
  return converged;
9165
9576
  }
@@ -9214,9 +9625,11 @@ function normalizeStepForHash(step) {
9214
9625
  }
9215
9626
  /** SHA-256 (hex) over the canonical normalized step list (steps only). */
9216
9627
  async function computeFlowContentHash(steps) {
9217
- const normalized = [...steps].sort((a, b) => {
9218
- return (isPlainObject$1(a) && typeof a.order === "number" ? a.order : 0) - (isPlainObject$1(b) && typeof b.order === "number" ? b.order : 0);
9219
- }).map(normalizeStepForHash);
9628
+ const normalized = steps.map((step) => isPlainObject$1(step) ? step : {}).map((step, index) => ({
9629
+ ...step,
9630
+ name: typeof step.name === "string" ? step.name : `Step ${index + 1}`,
9631
+ order: typeof step.order === "number" ? step.order : index + 1
9632
+ })).sort((a, b) => a.order - b.order).map(normalizeStepForHash);
9220
9633
  return sha256Hex(JSON.stringify(normalized));
9221
9634
  }
9222
9635
  const DEFINE_FLOW_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
@@ -9235,8 +9648,8 @@ const DEFINE_FLOW_STEP_KEYS = /* @__PURE__ */ new Set([
9235
9648
  /**
9236
9649
  * Collect account-scoped `tool_…` references from a step config (inlined copy
9237
9650
  * of `collectNonPortableAgentToolRefs` in @runtypelabs/shared, applied to the
9238
- * step's `config.tools` block and recursively to conditional branches). The
9239
- * v1 ensure surface accepts portable tool references only.
9651
+ * step's `config.tools` block, `toolId` and `agentId`). The v1 ensure surface
9652
+ * accepts portable tool references only.
9240
9653
  */
9241
9654
  function collectStepNonPortableToolRefs(config, path) {
9242
9655
  const found = [];
@@ -9270,12 +9683,73 @@ function collectStepNonPortableToolRefs(config, path) {
9270
9683
  }
9271
9684
  if (isAccountScoped(config.toolId)) found.push(`${path}.toolId`);
9272
9685
  if (isRawId(config.agentId, "agent_")) found.push(`${path}.agentId`);
9273
- for (const branch of ["trueSteps", "falseSteps"]) {
9274
- const nested = config[branch];
9275
- if (!Array.isArray(nested)) continue;
9276
- nested.forEach((nestedStep, i) => {
9277
- if (isPlainObject$1(nestedStep) && isPlainObject$1(nestedStep.config)) found.push(...collectStepNonPortableToolRefs(nestedStep.config, `${path}.${branch}[${i}].config`));
9686
+ return found;
9687
+ }
9688
+ const CONDITIONAL_BRANCH_STEP_KEYS = [
9689
+ "trueSteps",
9690
+ "falseSteps",
9691
+ "true_steps",
9692
+ "false_steps",
9693
+ "otherwiseSteps"
9694
+ ];
9695
+ /** Guards a hostile payload whose nesting is otherwise unbounded. */
9696
+ const MAX_REF_SITE_WALK_DEPTH = 100;
9697
+ /**
9698
+ * Collect account-scoped references at every flow step ref site (inlined copy of
9699
+ * `collectNonPortableFlowStepRefs` in @runtypelabs/shared): loop bodies, conditional
9700
+ * branches, inline flow-as-tool payloads and `errorHandling.fallbacks`.
9701
+ */
9702
+ function collectFlowStepsNonPortableRefs(steps, basePath, depth = 0) {
9703
+ if (!Array.isArray(steps) || depth > MAX_REF_SITE_WALK_DEPTH) return [];
9704
+ const found = [];
9705
+ steps.forEach((step, index) => {
9706
+ if (!isPlainObject$1(step) || !isPlainObject$1(step.config)) return;
9707
+ found.push(...collectStepConfigSiteRefs(step.type, step.config, `${basePath}[${index}].config`, depth));
9708
+ });
9709
+ return found;
9710
+ }
9711
+ function collectStepConfigSiteRefs(type, config, path, depth) {
9712
+ const found = collectStepNonPortableToolRefs(config, path);
9713
+ found.push(...collectToolBagFlowRefs(config.tools, `${path}.tools`, depth + 1));
9714
+ const errorHandling = config.errorHandling;
9715
+ if (isPlainObject$1(errorHandling) && Array.isArray(errorHandling.fallbacks)) errorHandling.fallbacks.forEach((fallback, i) => {
9716
+ if (!isPlainObject$1(fallback)) return;
9717
+ const fallbackPath = `${path}.errorHandling.fallbacks[${i}]`;
9718
+ if (fallback.type === "flow") {
9719
+ if (typeof fallback.flowId === "string" && fallback.flowId.startsWith("flow_")) found.push(`${fallbackPath}.flowId`);
9720
+ } else if (fallback.type === "step" && isPlainObject$1(fallback.stepConfig)) {
9721
+ found.push(...collectStepNonPortableToolRefs(fallback.stepConfig, `${fallbackPath}.stepConfig`));
9722
+ found.push(...collectToolBagFlowRefs(fallback.stepConfig.tools, `${fallbackPath}.stepConfig.tools`, depth + 1));
9723
+ }
9724
+ });
9725
+ if (type === "loop") found.push(...collectFlowStepsNonPortableRefs(config.steps, `${path}.steps`, depth + 1));
9726
+ else if (type === "conditional") {
9727
+ if (Array.isArray(config.branches)) config.branches.forEach((branch, i) => {
9728
+ if (!isPlainObject$1(branch)) return;
9729
+ found.push(...collectFlowStepsNonPortableRefs(branch.steps, `${path}.branches[${i}].steps`, depth + 1));
9278
9730
  });
9731
+ for (const key of CONDITIONAL_BRANCH_STEP_KEYS) found.push(...collectFlowStepsNonPortableRefs(config[key], `${path}.${key}`, depth + 1));
9732
+ }
9733
+ return found;
9734
+ }
9735
+ /** Descend the inline flow-as-tool and skill payloads a step's tool bag can carry. */
9736
+ function collectToolBagFlowRefs(tools, path, depth) {
9737
+ if (!isPlainObject$1(tools) || depth > MAX_REF_SITE_WALK_DEPTH) return [];
9738
+ const found = [];
9739
+ const walkBag = (bag, bagPath, bagDepth) => {
9740
+ if (bagDepth > MAX_REF_SITE_WALK_DEPTH) return;
9741
+ bag.forEach((tool, i) => {
9742
+ if (!isPlainObject$1(tool) || !isPlainObject$1(tool.config)) return;
9743
+ const toolPath = `${bagPath}[${i}].config`;
9744
+ const skill = tool.config.skill;
9745
+ if (isPlainObject$1(skill) && Array.isArray(skill.resolvedTools)) walkBag(skill.resolvedTools, `${toolPath}.skill.resolvedTools`, bagDepth + 1);
9746
+ const flow = tool.config.flow;
9747
+ if (isPlainObject$1(flow) && Array.isArray(flow.steps)) found.push(...collectFlowStepsNonPortableRefs(flow.steps, `${toolPath}.flow.steps`, bagDepth + 1));
9748
+ });
9749
+ };
9750
+ for (const key of ["runtimeTools", "inlineTools"]) {
9751
+ const bag = tools[key];
9752
+ if (Array.isArray(bag)) walkBag(bag, `${path}.${key}`, depth);
9279
9753
  }
9280
9754
  return found;
9281
9755
  }
@@ -9316,7 +9790,7 @@ function defineFlow(input) {
9316
9790
  if (unknownStepKeys.length > 0) throw new Error(`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.)`);
9317
9791
  const config = isPlainObject$1(step.config) ? step.config : void 0;
9318
9792
  if (config) {
9319
- const nonPortable = collectStepNonPortableToolRefs(config, `steps[${index}].config`);
9793
+ const nonPortable = collectStepConfigSiteRefs(step.type, config, `steps[${index}].config`, 0);
9320
9794
  if (nonPortable.length > 0) throw new Error(`defineFlow: account-scoped reference(s) at ${nonPortable.join(", ")}. Definitions must be environment-portable — tool_…/agent_…/flow_… IDs belong to one account/environment. Use builtin:/platform:/mcp: references, or reference a saved resource by name — tool:<name>, agent:<name>, or flow:<name> instead.`);
9321
9795
  }
9322
9796
  return {
@@ -9370,7 +9844,7 @@ var FlowEnsureConflictError = class extends Error {
9370
9844
  /** Thrown by `ensure(def, { expectNoChanges: true })` when drift is detected. */
9371
9845
  var FlowDriftError = class extends Error {
9372
9846
  constructor(plan) {
9373
- super(`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.`);
9847
+ super(`Flow "${plan.flowId ?? "definition"}" drifted: plan is '${plan.changes}' (changed: ${[...plan.changedKeys, ...plan.publishPending ? ["publish (pending)"] : []].join(", ") || "n/a"}). Run client.flows.pull(name) to absorb the remote edit into your repo, or re-run ensure to converge.`);
9374
9848
  this.name = "FlowDriftError";
9375
9849
  this.plan = plan;
9376
9850
  }
@@ -9379,7 +9853,7 @@ var FlowDriftError = class extends Error {
9379
9853
  * Best-effort extraction of `{ status, body }` from the generic request
9380
9854
  * error thrown by RuntypeClient ("API request failed: 409 Conflict - {…}").
9381
9855
  */
9382
- function parseRequestError$5(err) {
9856
+ function parseRequestError$6(err) {
9383
9857
  if (!(err instanceof Error)) return {
9384
9858
  status: null,
9385
9859
  body: null
@@ -9401,8 +9875,8 @@ function parseRequestError$5(err) {
9401
9875
  };
9402
9876
  }
9403
9877
  }
9404
- function toConflictError$5(err) {
9405
- const { status, body } = parseRequestError$5(err);
9878
+ function toConflictError$6(err) {
9879
+ const { status, body } = parseRequestError$6(err);
9406
9880
  if (status !== 409 || !isPlainObject$1(body)) return null;
9407
9881
  const code = body.code;
9408
9882
  if (code !== "external_modification" && code !== "remote_changed") return null;
@@ -9418,23 +9892,23 @@ function toConflictError$5(err) {
9418
9892
  * so a stale server hash from the previous definition version can never be
9419
9893
  * probed (which would false-match and silently skip the update).
9420
9894
  */
9421
- const serverHashMemo$5 = /* @__PURE__ */ new WeakMap();
9422
- function memoFor$5(client) {
9423
- let memo = serverHashMemo$5.get(client);
9895
+ const serverHashMemo$6 = /* @__PURE__ */ new WeakMap();
9896
+ function memoFor$6(client) {
9897
+ let memo = serverHashMemo$6.get(client);
9424
9898
  if (!memo) {
9425
9899
  memo = /* @__PURE__ */ new Map();
9426
- serverHashMemo$5.set(client, memo);
9900
+ serverHashMemo$6.set(client, memo);
9427
9901
  }
9428
9902
  return memo;
9429
9903
  }
9430
9904
  function memoize$4(memo, memoKey, result) {
9431
9905
  if (result.result !== "plan") memo.set(memoKey, result.contentHash);
9432
9906
  }
9433
- async function request$4(client, body) {
9907
+ async function request$5(client, body) {
9434
9908
  try {
9435
9909
  return await client.post("/flows/ensure", body);
9436
9910
  } catch (err) {
9437
- const conflict = toConflictError$5(err);
9911
+ const conflict = toConflictError$6(err);
9438
9912
  if (conflict) throw conflict;
9439
9913
  throw err;
9440
9914
  }
@@ -9459,37 +9933,37 @@ async function ensureFlow(client, definition, options = {}) {
9459
9933
  steps: definition.steps
9460
9934
  };
9461
9935
  if (dryRun || expectNoChanges) {
9462
- const plan = await request$4(client, {
9936
+ const plan = await request$5(client, {
9463
9937
  name: definition.name,
9464
9938
  definition: wireDefinition,
9465
9939
  dryRun: true,
9466
9940
  ...passthrough
9467
9941
  });
9468
9942
  if (plan.result !== "plan") throw new Error(`Expected a plan result from dryRun, got '${plan.result}'`);
9469
- if (expectNoChanges && plan.changes !== "none") throw new FlowDriftError(plan);
9943
+ if (expectNoChanges && (plan.changes !== "none" || release === "publish" && plan.publishPending === true)) throw new FlowDriftError(plan);
9470
9944
  return plan;
9471
9945
  }
9472
- const memo = memoFor$5(client);
9946
+ const memo = memoFor$6(client);
9473
9947
  const localHash = await computeFlowContentHash(definition.steps);
9474
9948
  const memoKey = `${definition.name} ${localHash}`;
9475
9949
  const contentHash = memo.get(memoKey) ?? localHash;
9476
- const probe = await request$4(client, {
9950
+ const probe = await request$5(client, {
9477
9951
  name: definition.name,
9478
9952
  contentHash,
9479
9953
  ...passthrough
9480
9954
  });
9481
9955
  if (probe.result !== "definitionRequired") {
9482
9956
  memoize$4(memo, memoKey, probe);
9483
- return convergeInlineEvals(client, definition, probe);
9957
+ return convergeInlineEvals(client, definition, probe, onConflict);
9484
9958
  }
9485
- const converged = await request$4(client, {
9959
+ const converged = await request$5(client, {
9486
9960
  name: definition.name,
9487
9961
  definition: wireDefinition,
9488
9962
  ...passthrough
9489
9963
  });
9490
9964
  if (converged.result === "definitionRequired") throw new Error("Server reported definitionRequired for a full-definition request");
9491
9965
  memoize$4(memo, memoKey, converged);
9492
- return convergeInlineEvals(client, definition, converged);
9966
+ return convergeInlineEvals(client, definition, converged, onConflict);
9493
9967
  }
9494
9968
  /**
9495
9969
  * After the flow itself converges on the real (non-plan) path, converge each
@@ -9502,10 +9976,10 @@ async function ensureFlow(client, definition, options = {}) {
9502
9976
  * unchanged (no empty `evals` array). The server's `/flows/ensure` surface is
9503
9977
  * never touched by eval semantics — they ride the eval endpoints only.
9504
9978
  */
9505
- async function convergeInlineEvals(client, definition, result) {
9979
+ async function convergeInlineEvals(client, definition, result, onConflict) {
9506
9980
  if (result.result === "plan" || !definition.evals?.length) return result;
9507
9981
  const evals = [];
9508
- for (const evalDef of definition.evals) evals.push(await ensureEval(client, evalDef));
9982
+ for (const evalDef of definition.evals) evals.push(await ensureEval(client, evalDef, onConflict ? { onConflict } : {}));
9509
9983
  return {
9510
9984
  ...result,
9511
9985
  evals
@@ -10741,24 +11215,8 @@ var EvalsNamespace = class {
10741
11215
  run(config) {
10742
11216
  return new EvalRunner(this.getClient, config);
10743
11217
  }
10744
- /**
10745
- * Idempotently converge a `defineEval` suite definition onto the platform —
10746
- * the deploy-time, non-executing converge for code-colocated evals. Hash-first:
10747
- * the steady state is one tiny probe request. Upserts the eval suite + replaces
10748
- * its cases; never runs the eval.
10749
- *
10750
- * @example
10751
- * ```typescript
10752
- * const suite = defineEval({
10753
- * target: { flow: 'support-triage' },
10754
- * graders: [noError()],
10755
- * cases: [{ name: 'billing', input: { variables: { message: 'I was double charged' } }, expect: [contains('finance')] }],
10756
- * })
10757
- * const result = await Runtype.evals.ensure(suite)
10758
- * ```
10759
- */
10760
- async ensure(definition) {
10761
- return ensureEval(this.getClient(), definition);
11218
+ async ensure(definition, options = {}) {
11219
+ return ensureEval(this.getClient(), definition, options);
10762
11220
  }
10763
11221
  /**
10764
11222
  * Pull the canonical definition + provenance for an eval suite by name — the
@@ -11025,7 +11483,7 @@ var SkillDriftError = class extends Error {
11025
11483
  * Best-effort extraction of `{ status, body }` from the generic request error
11026
11484
  * thrown by RuntypeClient ("API request failed: 409 Conflict - {…}").
11027
11485
  */
11028
- function parseRequestError$4(err) {
11486
+ function parseRequestError$5(err) {
11029
11487
  if (!(err instanceof Error)) return {
11030
11488
  status: null,
11031
11489
  body: null
@@ -11047,8 +11505,8 @@ function parseRequestError$4(err) {
11047
11505
  };
11048
11506
  }
11049
11507
  }
11050
- function toConflictError$4(err) {
11051
- const { status, body } = parseRequestError$4(err);
11508
+ function toConflictError$5(err) {
11509
+ const { status, body } = parseRequestError$5(err);
11052
11510
  if (status !== 409 || !isPlainObject$1(body)) return null;
11053
11511
  const code = body.code;
11054
11512
  if (code !== "external_modification" && code !== "remote_changed") return null;
@@ -11060,23 +11518,23 @@ function toConflictError$4(err) {
11060
11518
  * within the process lifetime invalidates a stale server hash (which would
11061
11519
  * false-match and silently skip the update). Same scheme as flows/tools.ensure.
11062
11520
  */
11063
- const serverHashMemo$4 = /* @__PURE__ */ new WeakMap();
11064
- function memoFor$4(client) {
11065
- let memo = serverHashMemo$4.get(client);
11521
+ const serverHashMemo$5 = /* @__PURE__ */ new WeakMap();
11522
+ function memoFor$5(client) {
11523
+ let memo = serverHashMemo$5.get(client);
11066
11524
  if (!memo) {
11067
11525
  memo = /* @__PURE__ */ new Map();
11068
- serverHashMemo$4.set(client, memo);
11526
+ serverHashMemo$5.set(client, memo);
11069
11527
  }
11070
11528
  return memo;
11071
11529
  }
11072
11530
  function memoize$3(memo, memoKey, result) {
11073
11531
  if (result.result !== "plan") memo.set(memoKey, result.contentHash);
11074
11532
  }
11075
- async function request$3(client, body) {
11533
+ async function request$4(client, body) {
11076
11534
  try {
11077
11535
  return await client.post("/skills/ensure", body);
11078
11536
  } catch (err) {
11079
- const conflict = toConflictError$4(err);
11537
+ const conflict = toConflictError$5(err);
11080
11538
  if (conflict) throw conflict;
11081
11539
  throw err;
11082
11540
  }
@@ -11088,14 +11546,15 @@ async function request$3(client, body) {
11088
11546
  * on every change; never deletes.
11089
11547
  */
11090
11548
  async function ensureSkill(client, definition, options = {}) {
11091
- const { dryRun, onConflict, release, expectedRemoteHash, expectNoChanges } = options;
11549
+ const { dryRun, onConflict, release, expectedRemoteHash, version, expectNoChanges } = options;
11092
11550
  const passthrough = {
11093
11551
  ...onConflict ? { onConflict } : {},
11094
11552
  ...release ? { release } : {},
11095
- ...expectedRemoteHash ? { expectedRemoteHash } : {}
11553
+ ...expectedRemoteHash ? { expectedRemoteHash } : {},
11554
+ ...version ? { version } : {}
11096
11555
  };
11097
11556
  if (dryRun || expectNoChanges) {
11098
- const plan = await request$3(client, {
11557
+ const plan = await request$4(client, {
11099
11558
  name: definition.name,
11100
11559
  definition: manifestToWire(definition),
11101
11560
  dryRun: true,
@@ -11105,11 +11564,11 @@ async function ensureSkill(client, definition, options = {}) {
11105
11564
  if (expectNoChanges && plan.changes !== "none") throw new SkillDriftError(plan);
11106
11565
  return plan;
11107
11566
  }
11108
- const memo = memoFor$4(client);
11567
+ const memo = memoFor$5(client);
11109
11568
  const localHash = await computeSkillContentHash(definition);
11110
11569
  const memoKey = `${definition.name} ${localHash}`;
11111
11570
  const contentHash = memo.get(memoKey) ?? localHash;
11112
- const probe = await request$3(client, {
11571
+ const probe = await request$4(client, {
11113
11572
  name: definition.name,
11114
11573
  contentHash,
11115
11574
  ...passthrough
@@ -11118,7 +11577,7 @@ async function ensureSkill(client, definition, options = {}) {
11118
11577
  memoize$3(memo, memoKey, probe);
11119
11578
  return probe;
11120
11579
  }
11121
- const converged = await request$3(client, {
11580
+ const converged = await request$4(client, {
11122
11581
  name: definition.name,
11123
11582
  definition: manifestToWire(definition),
11124
11583
  ...passthrough
@@ -11534,6 +11993,18 @@ function defineAgent(input) {
11534
11993
  config
11535
11994
  };
11536
11995
  }
11996
+ function planHasSkillBindingChanges(plan) {
11997
+ return (plan.skillBindings ?? []).some((binding) => binding.action !== "unchanged");
11998
+ }
11999
+ /** Whether a dry run that asked for a deploy would move the alias or change its binding names. */
12000
+ function planHasDeploymentChanges(plan) {
12001
+ return plan.deployment?.changed === true || plan.deployment?.bindingsChanged === true;
12002
+ }
12003
+ function deploymentChangeLabels(plan) {
12004
+ const deployment = plan.deployment;
12005
+ if (!deployment) return [];
12006
+ return [...deployment.changed ? [`deploy:${deployment.alias} (pointer)`] : [], ...deployment.bindingsChanged ? [`deploy:${deployment.alias} (bindings)`] : []];
12007
+ }
11537
12008
  /** A 409 from the ensure protocol (external_modification or remote_changed). */
11538
12009
  var AgentEnsureConflictError = class extends Error {
11539
12010
  constructor(body) {
@@ -11548,7 +12019,12 @@ var AgentEnsureConflictError = class extends Error {
11548
12019
  /** Thrown by `ensure(def, { expectNoChanges: true })` when drift is detected. */
11549
12020
  var AgentDriftError = class extends Error {
11550
12021
  constructor(plan) {
11551
- super(`Agent "${plan.agentId ?? "definition"}" drifted: plan is '${plan.changes}' (changed: ${plan.changedKeys.join(", ") || "n/a"}). Run client.agents.pull(name) to absorb the remote edit into your repo, or re-run ensure to converge.`);
12022
+ const skillChanges = (plan.skillBindings ?? []).filter((binding) => binding.action !== "unchanged").map((binding) => `skill:${binding.name} (${binding.action})`);
12023
+ super(`Agent "${plan.agentId ?? "definition"}" drifted: plan is '${plan.changes}' (changed: ${[
12024
+ ...plan.changedKeys,
12025
+ ...skillChanges,
12026
+ ...deploymentChangeLabels(plan)
12027
+ ].join(", ") || "n/a"}). Run client.agents.pull(name) to absorb the remote edit into your repo, or re-run ensure to converge.`);
11552
12028
  this.name = "AgentDriftError";
11553
12029
  this.plan = plan;
11554
12030
  }
@@ -11557,7 +12033,7 @@ var AgentDriftError = class extends Error {
11557
12033
  * Best-effort extraction of `{ status, body }` from the generic request
11558
12034
  * error thrown by RuntypeClient ("API request failed: 409 Conflict - {…}").
11559
12035
  */
11560
- function parseRequestError$3(err) {
12036
+ function parseRequestError$4(err) {
11561
12037
  if (!(err instanceof Error)) return {
11562
12038
  status: null,
11563
12039
  body: null
@@ -11579,8 +12055,8 @@ function parseRequestError$3(err) {
11579
12055
  };
11580
12056
  }
11581
12057
  }
11582
- function toConflictError$3(err) {
11583
- const { status, body } = parseRequestError$3(err);
12058
+ function toConflictError$4(err) {
12059
+ const { status, body } = parseRequestError$4(err);
11584
12060
  if (status !== 409 || !isPlainObject(body)) return null;
11585
12061
  const code = body.code;
11586
12062
  if (code !== "external_modification" && code !== "remote_changed") return null;
@@ -11596,12 +12072,12 @@ function toConflictError$3(err) {
11596
12072
  * so a stale server hash from the previous definition version can never be
11597
12073
  * probed (which would false-match and silently skip the update).
11598
12074
  */
11599
- const serverHashMemo$3 = /* @__PURE__ */ new WeakMap();
11600
- function memoFor$3(client) {
11601
- let memo = serverHashMemo$3.get(client);
12075
+ const serverHashMemo$4 = /* @__PURE__ */ new WeakMap();
12076
+ function memoFor$4(client) {
12077
+ let memo = serverHashMemo$4.get(client);
11602
12078
  if (!memo) {
11603
12079
  memo = /* @__PURE__ */ new Map();
11604
- serverHashMemo$3.set(client, memo);
12080
+ serverHashMemo$4.set(client, memo);
11605
12081
  }
11606
12082
  return memo;
11607
12083
  }
@@ -11637,14 +12113,16 @@ var AgentsNamespace = class {
11637
12113
  */
11638
12114
  async ensure(definition, options = {}) {
11639
12115
  const client = this.getClient();
11640
- const { dryRun, onConflict, release, deploy, expectedRemoteHash, version, expectNoChanges } = options;
12116
+ const { dryRun, onConflict, release, deploy, expectedRemoteHash, version, expectNoChanges, skills, capabilities } = options;
11641
12117
  if (release !== void 0 && deploy !== void 0) throw new Error(ENSURE_RELEASE_DEPLOY_CONFLICT_MESSAGE);
11642
12118
  const passthrough = {
11643
12119
  ...onConflict ? { onConflict } : {},
11644
12120
  ...release ? { release } : {},
11645
12121
  ...deploy ? { deploy } : {},
11646
12122
  ...expectedRemoteHash ? { expectedRemoteHash } : {},
11647
- ...version ? { version } : {}
12123
+ ...version ? { version } : {},
12124
+ ...skills ? { skills } : {},
12125
+ ...capabilities ? { capabilities } : {}
11648
12126
  };
11649
12127
  if (dryRun || expectNoChanges) {
11650
12128
  const plan = await this.request(client, {
@@ -11654,15 +12132,16 @@ var AgentsNamespace = class {
11654
12132
  ...passthrough
11655
12133
  });
11656
12134
  if (plan.result !== "plan") throw new Error(`Expected a plan result from dryRun, got '${plan.result}'`);
11657
- if (expectNoChanges && plan.changes !== "none") throw new AgentDriftError(plan);
12135
+ const deployRequested = release === "publish" || deploy !== void 0;
12136
+ if (expectNoChanges && (plan.changes !== "none" || planHasSkillBindingChanges(plan) || deployRequested && planHasDeploymentChanges(plan))) throw new AgentDriftError(plan);
11658
12137
  return plan;
11659
12138
  }
11660
- const memo = memoFor$3(client);
12139
+ const memo = memoFor$4(client);
11661
12140
  const localHash = await computeAgentContentHash({
11662
12141
  ...definition,
11663
12142
  config: definition.config
11664
12143
  });
11665
- const memoKey = `${definition.name}\u0000${localHash}`;
12144
+ const memoKey = `${definition.name}\u0000${localHash}${capabilities ? `\u0000${JSON.stringify(capabilities)}` : ""}`;
11666
12145
  const contentHash = memo.get(memoKey) ?? localHash;
11667
12146
  const probe = await this.request(client, {
11668
12147
  name: definition.name,
@@ -11696,7 +12175,7 @@ var AgentsNamespace = class {
11696
12175
  try {
11697
12176
  return await client.post("/agents/ensure", body);
11698
12177
  } catch (err) {
11699
- const conflict = toConflictError$3(err);
12178
+ const conflict = toConflictError$4(err);
11700
12179
  if (conflict) throw conflict;
11701
12180
  throw err;
11702
12181
  }
@@ -11825,7 +12304,7 @@ var ToolDriftError = class extends Error {
11825
12304
  * Best-effort extraction of `{ status, body }` from the generic request
11826
12305
  * error thrown by RuntypeClient ("API request failed: 409 Conflict - {…}").
11827
12306
  */
11828
- function parseRequestError$2(err) {
12307
+ function parseRequestError$3(err) {
11829
12308
  if (!(err instanceof Error)) return {
11830
12309
  status: null,
11831
12310
  body: null
@@ -11847,8 +12326,8 @@ function parseRequestError$2(err) {
11847
12326
  };
11848
12327
  }
11849
12328
  }
11850
- function toConflictError$2(err) {
11851
- const { status, body } = parseRequestError$2(err);
12329
+ function toConflictError$3(err) {
12330
+ const { status, body } = parseRequestError$3(err);
11852
12331
  if (status !== 409 || !isPlainObject$1(body)) return null;
11853
12332
  const code = body.code;
11854
12333
  if (code !== "external_modification" && code !== "remote_changed") return null;
@@ -11860,23 +12339,23 @@ function toConflictError$2(err) {
11860
12339
  * within the process lifetime invalidates a stale server hash (which would
11861
12340
  * false-match and silently skip the update). Same scheme as flows.ensure.
11862
12341
  */
11863
- const serverHashMemo$2 = /* @__PURE__ */ new WeakMap();
11864
- function memoFor$2(client) {
11865
- let memo = serverHashMemo$2.get(client);
12342
+ const serverHashMemo$3 = /* @__PURE__ */ new WeakMap();
12343
+ function memoFor$3(client) {
12344
+ let memo = serverHashMemo$3.get(client);
11866
12345
  if (!memo) {
11867
12346
  memo = /* @__PURE__ */ new Map();
11868
- serverHashMemo$2.set(client, memo);
12347
+ serverHashMemo$3.set(client, memo);
11869
12348
  }
11870
12349
  return memo;
11871
12350
  }
11872
12351
  function memoize$2(memo, memoKey, result) {
11873
12352
  if (result.result !== "plan") memo.set(memoKey, result.contentHash);
11874
12353
  }
11875
- async function request$2(client, body) {
12354
+ async function request$3(client, body) {
11876
12355
  try {
11877
12356
  return await client.post("/tools/ensure", body);
11878
12357
  } catch (err) {
11879
- const conflict = toConflictError$2(err);
12358
+ const conflict = toConflictError$3(err);
11880
12359
  if (conflict) throw conflict;
11881
12360
  throw err;
11882
12361
  }
@@ -11893,7 +12372,7 @@ async function ensureTool(client, definition, options = {}) {
11893
12372
  ...expectedRemoteHash ? { expectedRemoteHash } : {}
11894
12373
  };
11895
12374
  if (dryRun || expectNoChanges) {
11896
- const plan = await request$2(client, {
12375
+ const plan = await request$3(client, {
11897
12376
  name: definition.name,
11898
12377
  definition,
11899
12378
  dryRun: true,
@@ -11903,11 +12382,11 @@ async function ensureTool(client, definition, options = {}) {
11903
12382
  if (expectNoChanges && plan.changes !== "none") throw new ToolDriftError(plan);
11904
12383
  return plan;
11905
12384
  }
11906
- const memo = memoFor$2(client);
12385
+ const memo = memoFor$3(client);
11907
12386
  const localHash = await computeToolContentHash(definition);
11908
12387
  const memoKey = `${definition.name} ${localHash}`;
11909
12388
  const contentHash = memo.get(memoKey) ?? localHash;
11910
- const probe = await request$2(client, {
12389
+ const probe = await request$3(client, {
11911
12390
  name: definition.name,
11912
12391
  contentHash,
11913
12392
  ...passthrough
@@ -11916,7 +12395,7 @@ async function ensureTool(client, definition, options = {}) {
11916
12395
  memoize$2(memo, memoKey, probe);
11917
12396
  return probe;
11918
12397
  }
11919
- const converged = await request$2(client, {
12398
+ const converged = await request$3(client, {
11920
12399
  name: definition.name,
11921
12400
  definition,
11922
12401
  ...passthrough
@@ -11972,10 +12451,197 @@ var ToolsNamespace = class {
11972
12451
  }
11973
12452
  };
11974
12453
  //#endregion
12454
+ //#region src/question-sets-ensure.ts
12455
+ /** Canonical form of a question set definition, without its slug. */
12456
+ function normalizeQuestionSetDefinition(definition) {
12457
+ return normalizeValue$1({
12458
+ name: definition.name,
12459
+ description: definition.description,
12460
+ questions: isPlainObject$1(definition.questions) ? definition.questions : {},
12461
+ inputMapping: definition.inputMapping,
12462
+ model: definition.model
12463
+ });
12464
+ }
12465
+ /** SHA-256 (hex) over the canonical question set definition. */
12466
+ async function computeQuestionSetContentHash(definition) {
12467
+ return sha256Hex(JSON.stringify(normalizeQuestionSetDefinition(definition)));
12468
+ }
12469
+ const SLUG_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
12470
+ const DEFINE_QUESTION_SET_KEYS = /* @__PURE__ */ new Set([
12471
+ "slug",
12472
+ "name",
12473
+ "description",
12474
+ "questions",
12475
+ "inputMapping",
12476
+ "model"
12477
+ ]);
12478
+ /**
12479
+ * Pure local constructor for a question set definition. Checks the structure
12480
+ * and rejects unknown fields; the server validates the questions at ensure time.
12481
+ *
12482
+ * @example
12483
+ * ```typescript
12484
+ * const triage = defineQuestionSet({
12485
+ * slug: 'ticket-triage',
12486
+ * name: 'Ticket triage',
12487
+ * questions: { urgent: { type: 'boolean', instructions: 'Is the ticket urgent?' } },
12488
+ * })
12489
+ * ```
12490
+ */
12491
+ function defineQuestionSet(input) {
12492
+ if (!input || typeof input !== "object") throw new Error("defineQuestionSet requires a definition object");
12493
+ if (typeof input.slug !== "string" || !SLUG_PATTERN.test(input.slug)) throw new Error("defineQuestionSet requires a \"slug\" of lowercase letters, digits, \"-\" and \"_\", up to 64 characters");
12494
+ if (typeof input.name !== "string" || input.name.length === 0) throw new Error("defineQuestionSet requires a non-empty string \"name\"");
12495
+ if (!isPlainObject$1(input.questions) || Object.keys(input.questions).length === 0) throw new Error("defineQuestionSet requires a non-empty \"questions\" object");
12496
+ const unknownKeys = Object.keys(input).filter((key) => !DEFINE_QUESTION_SET_KEYS.has(key));
12497
+ if (unknownKeys.length > 0) throw new Error(`defineQuestionSet: unknown field(s): ${unknownKeys.join(", ")}. Allowed fields are slug, name, description, questions, inputMapping and model.`);
12498
+ return { ...input };
12499
+ }
12500
+ /** A 409 from the ensure protocol (external_modification or remote_changed). */
12501
+ var QuestionSetEnsureConflictError = class extends Error {
12502
+ constructor(body) {
12503
+ super(body.error ?? `Question set ensure conflict: ${body.code}`);
12504
+ this.name = "QuestionSetEnsureConflictError";
12505
+ this.code = body.code;
12506
+ this.lastModifiedSource = body.lastModifiedSource;
12507
+ this.modifiedAt = body.modifiedAt;
12508
+ this.currentHash = body.currentHash;
12509
+ }
12510
+ };
12511
+ /** Thrown by `ensure(def, { expectNoChanges: true })` when the remote differs. */
12512
+ var QuestionSetDriftError = class extends Error {
12513
+ constructor(slug, plan) {
12514
+ super(`Question set "${slug}" drifted: plan is '${plan.changes}' (changed: ${plan.changedKeys.join(", ") || "n/a"}). Run client.questionSets.pull(slug) to absorb the remote edit, or re-run ensure to converge.`);
12515
+ this.name = "QuestionSetDriftError";
12516
+ this.plan = plan;
12517
+ }
12518
+ };
12519
+ function toConflictError$2(err) {
12520
+ if (!(err instanceof Error)) return null;
12521
+ const match = err.message.match(/^API request failed: (409) .*? - ([\s\S]*)$/);
12522
+ if (!match) return null;
12523
+ let body;
12524
+ try {
12525
+ body = JSON.parse(match[2]);
12526
+ } catch {
12527
+ return null;
12528
+ }
12529
+ if (!isPlainObject$1(body)) return null;
12530
+ if (body.code !== "external_modification" && body.code !== "remote_changed") return null;
12531
+ return new QuestionSetEnsureConflictError(body);
12532
+ }
12533
+ const serverHashMemo$2 = /* @__PURE__ */ new WeakMap();
12534
+ function memoFor$2(client) {
12535
+ let memo = serverHashMemo$2.get(client);
12536
+ if (!memo) {
12537
+ memo = /* @__PURE__ */ new Map();
12538
+ serverHashMemo$2.set(client, memo);
12539
+ }
12540
+ return memo;
12541
+ }
12542
+ async function request$2(client, body) {
12543
+ try {
12544
+ return await client.post("/question-sets/ensure", body);
12545
+ } catch (err) {
12546
+ throw toConflictError$2(err) ?? err;
12547
+ }
12548
+ }
12549
+ function toWireDefinition(definition) {
12550
+ const { slug: _slug, ...rest } = definition;
12551
+ return rest;
12552
+ }
12553
+ /**
12554
+ * Idempotently converge a question set onto the platform. Probes with a
12555
+ * content hash first and sends the full definition only on a miss. Appends a
12556
+ * version when the questions, input mapping or model change; never deletes.
12557
+ */
12558
+ async function ensureQuestionSet(client, definition, options = {}) {
12559
+ const { dryRun, onConflict, release, expectedRemoteHash, expectNoChanges } = options;
12560
+ const passthrough = {
12561
+ ...onConflict ? { onConflict } : {},
12562
+ ...release ? { release } : {},
12563
+ ...expectedRemoteHash ? { expectedRemoteHash } : {}
12564
+ };
12565
+ if (dryRun || expectNoChanges) {
12566
+ const plan = await request$2(client, {
12567
+ slug: definition.slug,
12568
+ definition: toWireDefinition(definition),
12569
+ dryRun: true,
12570
+ ...passthrough
12571
+ });
12572
+ if (plan.result !== "plan") throw new Error(`Expected a plan result from dryRun, got '${plan.result}'`);
12573
+ if (expectNoChanges && plan.changes !== "none") throw new QuestionSetDriftError(definition.slug, plan);
12574
+ return plan;
12575
+ }
12576
+ const memo = memoFor$2(client);
12577
+ const localHash = await computeQuestionSetContentHash(definition);
12578
+ const memoKey = `${definition.slug} ${localHash}`;
12579
+ const probe = await request$2(client, {
12580
+ slug: definition.slug,
12581
+ contentHash: memo.get(memoKey) ?? localHash,
12582
+ ...passthrough
12583
+ });
12584
+ if (probe.result !== "definitionRequired") {
12585
+ if (probe.result !== "plan") memo.set(memoKey, probe.contentHash);
12586
+ return probe;
12587
+ }
12588
+ const converged = await request$2(client, {
12589
+ slug: definition.slug,
12590
+ definition: toWireDefinition(definition),
12591
+ ...passthrough
12592
+ });
12593
+ if (converged.result === "definitionRequired") throw new Error("Server reported definitionRequired for a full-definition request");
12594
+ if (converged.result !== "plan") memo.set(memoKey, converged.contentHash);
12595
+ return converged;
12596
+ }
12597
+ /** Pull the canonical definition and provenance for a question set by slug. */
12598
+ async function pullQuestionSet(client, slug) {
12599
+ return client.get("/question-sets/pull", { slug });
12600
+ }
12601
+ //#endregion
12602
+ //#region src/question-sets-namespace.ts
12603
+ /** Question set config-as-code: `ensure` and `pull`. */
12604
+ var QuestionSetsNamespace = class {
12605
+ constructor(getClient) {
12606
+ this.getClient = getClient;
12607
+ }
12608
+ /**
12609
+ * Converge a `defineQuestionSet` definition. The steady state is one hash
12610
+ * probe. Pass `release: 'publish'` so flows and agents can resolve the
12611
+ * latest version.
12612
+ *
12613
+ * @example
12614
+ * ```typescript
12615
+ * await Runtype.questionSets.ensure(triage, { release: 'publish' })
12616
+ * await Runtype.questionSets.ensure(triage, { expectNoChanges: true })
12617
+ * ```
12618
+ */
12619
+ async ensure(definition, options = {}) {
12620
+ return ensureQuestionSet(this.getClient(), definition, options);
12621
+ }
12622
+ /** Pull the live definition and provenance for a slug, to absorb a dashboard edit. */
12623
+ async pull(slug) {
12624
+ return pullQuestionSet(this.getClient(), slug);
12625
+ }
12626
+ };
12627
+ //#endregion
11975
12628
  //#region src/products-ensure.ts
12629
+ /**
12630
+ * `spec` with its stack as a save stores it: without the server-set fields (`detected`, `declared`,
12631
+ * `hiddenAt`), which their own write paths set, and with `hidden` deduplicated.
12632
+ */
12633
+ function asSavedStack(spec) {
12634
+ if (!isPlainObject$1(spec.stack)) return spec;
12635
+ const { detected: _detected, declared: _declared, hiddenAt: _hiddenAt, ...stack } = spec.stack;
12636
+ if (Array.isArray(stack.hidden)) stack.hidden = [...new Set(stack.hidden)];
12637
+ return {
12638
+ ...spec,
12639
+ stack
12640
+ };
12641
+ }
11976
12642
  /** Canonical normalized form of a product definition (name + canvas excluded). */
11977
12643
  function normalizeProductDefinition(definition) {
11978
- const spec = isPlainObject$1(definition.spec) ? normalizeValue$1(definition.spec) : {};
12644
+ const spec = isPlainObject$1(definition.spec) ? normalizeValue$1(asSavedStack(definition.spec)) : {};
11979
12645
  return {
11980
12646
  ...definition.description ? { description: definition.description } : {},
11981
12647
  ...definition.icon ? { icon: definition.icon } : {},
@@ -12045,7 +12711,7 @@ var ProductDriftError = class extends Error {
12045
12711
  * Best-effort extraction of `{ status, body }` from the generic request
12046
12712
  * error thrown by RuntypeClient ("API request failed: 409 Conflict - {…}").
12047
12713
  */
12048
- function parseRequestError$1(err) {
12714
+ function parseRequestError$2(err) {
12049
12715
  if (!(err instanceof Error)) return {
12050
12716
  status: null,
12051
12717
  body: null
@@ -12068,7 +12734,7 @@ function parseRequestError$1(err) {
12068
12734
  }
12069
12735
  }
12070
12736
  function toConflictError$1(err) {
12071
- const { status, body } = parseRequestError$1(err);
12737
+ const { status, body } = parseRequestError$2(err);
12072
12738
  if (status !== 409 || !isPlainObject$1(body)) return null;
12073
12739
  const code = body.code;
12074
12740
  if (code !== "external_modification" && code !== "remote_changed") return null;
@@ -12190,20 +12856,85 @@ function defineFpo(fpo) {
12190
12856
  if (!product || typeof product.name !== "string" || product.name.length === 0) throw new Error("defineFpo requires a non-empty \"product.name\" (the converge identity)");
12191
12857
  return fpo;
12192
12858
  }
12859
+ const FPO_DRIFT_RESULTS = /* @__PURE__ */ new Set([
12860
+ "created",
12861
+ "updated",
12862
+ "pruned",
12863
+ "failed"
12864
+ ]);
12865
+ /** Thrown by `ensureFpo(fpo, { expectNoChanges: true })` when the plan is not clean. */
12866
+ var FpoDriftError = class extends Error {
12867
+ constructor(plan) {
12868
+ const drifted = plan.entities.filter((entity) => FPO_DRIFT_RESULTS.has(entity.result)).map((entity) => `${entity.kind}:${entity.name} (${entity.result})`);
12869
+ super(`Product "${plan.productId ?? "definition"}" drifted (${drifted.join(", ")}). Run client.products.pullFpo(name) to absorb the remote edit into your repo, or re-run ensureFpo to converge.`);
12870
+ this.name = "FpoDriftError";
12871
+ this.plan = plan;
12872
+ }
12873
+ };
12874
+ function fpoPlanHasDrift(plan) {
12875
+ return plan.entities.some((entity) => FPO_DRIFT_RESULTS.has(entity.result));
12876
+ }
12877
+ function parseRequestError$1(err) {
12878
+ if (!(err instanceof Error)) return {
12879
+ status: null,
12880
+ body: null
12881
+ };
12882
+ const match = err.message.match(/^API request failed: (\d{3}) .*? - ([\s\S]*)$/);
12883
+ if (!match) return {
12884
+ status: null,
12885
+ body: null
12886
+ };
12887
+ try {
12888
+ return {
12889
+ status: Number(match[1]),
12890
+ body: JSON.parse(match[2])
12891
+ };
12892
+ } catch {
12893
+ return {
12894
+ status: Number(match[1]),
12895
+ body: null
12896
+ };
12897
+ }
12898
+ }
12899
+ async function requestFpoEnsure(client, body) {
12900
+ try {
12901
+ return await client.post("/products/ensure-fpo", body);
12902
+ } catch (err) {
12903
+ const { status, body: errorBody } = parseRequestError$1(err);
12904
+ if (status === 409 && isPlainObject$1(errorBody) && (errorBody.code === "external_modification" || errorBody.code === "remote_changed")) throw new ProductEnsureConflictError(errorBody);
12905
+ throw err;
12906
+ }
12907
+ }
12193
12908
  /**
12194
- * Converge an entire FPO onto the platform. Ships the full FPO (no hash-only
12195
- * probe in this release); the server fans out to the per-entity ensure services
12196
- * and returns the whole-FPO hash + per-entity report. Non-atomic: inspect
12197
- * `result.hasFailures` / `entities[].result` and re-run to self-heal.
12909
+ * Converge an entire FPO onto the platform. Ships the full FPO unless a
12910
+ * drafts-only `hashProbe` finds the live graph already matches; the server fans out to the
12911
+ * per-entity ensure services and returns the whole-FPO hash + per-entity report.
12912
+ * Non-atomic: inspect `result.hasFailures` / `entities[].result` and re-run to self-heal.
12198
12913
  */
12199
12914
  async function ensureFpo(client, fpo, options = {}) {
12200
- const { dryRun, onConflict, prune } = options;
12201
- return client.post("/products/ensure-fpo", {
12915
+ const { dryRun, onConflict, prune, expectNoChanges, expectedRemoteHash, hashProbe, release } = options;
12916
+ const plan = dryRun || expectNoChanges;
12917
+ const guard = expectedRemoteHash ? { expectedRemoteHash } : {};
12918
+ if (hashProbe && release === "none" && !plan && !prune) {
12919
+ const probe = await requestFpoEnsure(client, {
12920
+ name: (isPlainObject$1(fpo.product) ? fpo.product : {}).name,
12921
+ contentHash: await computeFpoContentHash(fpo),
12922
+ ...guard,
12923
+ release
12924
+ });
12925
+ if (probe.result !== "definitionRequired") return probe;
12926
+ }
12927
+ const result = await requestFpoEnsure(client, {
12202
12928
  fpo,
12203
- ...dryRun ? { dryRun: true } : {},
12929
+ ...plan ? { dryRun: true } : {},
12204
12930
  ...onConflict ? { onConflict } : {},
12205
- ...prune ? { prune: true } : {}
12931
+ ...prune ? { prune: true } : {},
12932
+ ...guard,
12933
+ ...release ? { release } : {}
12206
12934
  });
12935
+ if (result.result === "definitionRequired") throw new Error("Server reported definitionRequired for a full-FPO request");
12936
+ if (expectNoChanges && fpoPlanHasDrift(result)) throw new FpoDriftError(result);
12937
+ return result;
12207
12938
  }
12208
12939
  /**
12209
12940
  * Reconstruct a self-contained FPO from the live product graph (resolved by
@@ -12378,8 +13109,8 @@ function defineSurface(input) {
12378
13109
  if (typeof input.name !== "string" || input.name.length === 0) throw new Error("defineSurface requires a non-empty string \"name\"");
12379
13110
  if (typeof input.type !== "string" || !SURFACE_DEFINITION_TYPES.has(input.type)) throw new Error(`defineSurface requires "type" to be one of: ${[...SURFACE_DEFINITION_TYPES].join(", ")}`);
12380
13111
  if (input.behavior !== void 0 && !isPlainObject$1(input.behavior)) throw new Error("defineSurface \"behavior\" must be an object when provided");
12381
- if (input.inbound !== void 0 && !isPlainObject$1(input.inbound)) throw new Error("defineSurface \"inbound\" must be an object when provided");
12382
- if (input.outbound !== void 0 && !isPlainObject$1(input.outbound)) throw new Error("defineSurface \"outbound\" must be an object when provided");
13112
+ if (input.inbound !== void 0 && input.inbound !== null && !isPlainObject$1(input.inbound)) throw new Error("defineSurface \"inbound\" must be an object or null when provided");
13113
+ if (input.outbound !== void 0 && input.outbound !== null && !isPlainObject$1(input.outbound)) throw new Error("defineSurface \"outbound\" must be an object or null when provided");
12383
13114
  if (input.status !== void 0 && ![
12384
13115
  "draft",
12385
13116
  "active",
@@ -12501,14 +13232,16 @@ async function ensureSurface(client, productId, definition, options = {}) {
12501
13232
  const localHash = await computeSurfaceContentHash(definition);
12502
13233
  const memoKey = `${productId} ${definition.name} ${localHash}`;
12503
13234
  const contentHash = memo.get(memoKey) ?? localHash;
12504
- const probe = await request(client, productId, {
12505
- name: definition.name,
12506
- contentHash,
12507
- ...passthrough
12508
- });
12509
- if (probe.result !== "definitionRequired") {
12510
- memoize(memo, memoKey, probe);
12511
- return probe;
13235
+ if (!(definition.inbound !== void 0 || definition.outbound !== void 0)) {
13236
+ const probe = await request(client, productId, {
13237
+ name: definition.name,
13238
+ contentHash,
13239
+ ...passthrough
13240
+ });
13241
+ if (probe.result !== "definitionRequired") {
13242
+ memoize(memo, memoKey, probe);
13243
+ return probe;
13244
+ }
12512
13245
  }
12513
13246
  const converged = await request(client, productId, {
12514
13247
  name: definition.name,
@@ -12521,8 +13254,8 @@ async function ensureSurface(client, productId, definition, options = {}) {
12521
13254
  }
12522
13255
  /**
12523
13256
  * Pull the canonical definition + provenance for a surface by name within a
12524
- * product — the absorb-drift direction. The contentHash reflects the live
12525
- * surface state.
13257
+ * product — the absorb-drift direction. Configured credentials come back as
13258
+ * `'<redacted>'`, which `ensure` accepts to keep the stored value.
12526
13259
  */
12527
13260
  async function pullSurface(client, productId, name) {
12528
13261
  return client.get(`/products/${encodeURIComponent(productId)}/surfaces/pull`, { name });
@@ -13237,6 +13970,25 @@ var Runtype = class {
13237
13970
  return new ToolsNamespace(() => this.getClient());
13238
13971
  }
13239
13972
  /**
13973
+ * Question sets namespace - Question set config-as-code (define / ensure / pull)
13974
+ *
13975
+ * @example
13976
+ * ```typescript
13977
+ * import { defineQuestionSet, Runtype } from '@runtypelabs/sdk'
13978
+ *
13979
+ * const triage = defineQuestionSet({
13980
+ * slug: 'ticket-triage',
13981
+ * name: 'Ticket triage',
13982
+ * questions: { urgent: { type: 'boolean', instructions: 'Is the ticket urgent?' } },
13983
+ * })
13984
+ *
13985
+ * await Runtype.questionSets.ensure(triage, { release: 'publish' })
13986
+ * ```
13987
+ */
13988
+ static get questionSets() {
13989
+ return new QuestionSetsNamespace(() => this.getClient());
13990
+ }
13991
+ /**
13240
13992
  * Products namespace - Product config-as-code (define / ensure / pull)
13241
13993
  *
13242
13994
  * Converges the top-level product record (description, icon, spec). Nested
@@ -13525,7 +14277,7 @@ function transformQueryParams(params) {
13525
14277
  }
13526
14278
  //#endregion
13527
14279
  //#region src/version.ts
13528
- const SDK_VERSION = "10.8.1";
14280
+ const SDK_VERSION = "10.11.0";
13529
14281
  /**
13530
14282
  * Canonical client identity the API uses for attribution
13531
14283
  * (`detectActorSource` in apps/api/src/lib/audit-log.ts keys `'sdk'` off the
@@ -15716,7 +16468,6 @@ const STEP_TYPE_TO_METHOD = {
15716
16468
  //#endregion
15717
16469
  exports.AgentAliasDependencyError = AgentAliasDependencyError;
15718
16470
  exports.AgentAliasNotFoundError = AgentAliasNotFoundError;
15719
- exports.AgentAliasPreviewLimitError = AgentAliasPreviewLimitError;
15720
16471
  exports.AgentAliasRevisionMismatchError = AgentAliasRevisionMismatchError;
15721
16472
  exports.AgentAliasRevisionRequiredError = AgentAliasRevisionRequiredError;
15722
16473
  exports.AgentAliasesNamespace = AgentAliasesNamespace;
@@ -15741,6 +16492,8 @@ exports.ClientTokenDriftError = ClientTokenDriftError;
15741
16492
  exports.ClientTokenEnsureError = ClientTokenEnsureError;
15742
16493
  exports.ClientTokensEndpoint = ClientTokensEndpoint;
15743
16494
  exports.ClientTokensNamespace = ClientTokensNamespace;
16495
+ exports.CollectionDriftError = CollectionDriftError;
16496
+ exports.CollectionEnsureConflictError = CollectionEnsureConflictError;
15744
16497
  exports.CollectionsEndpoint = CollectionsEndpoint;
15745
16498
  exports.ContextTemplatesEndpoint = ContextTemplatesEndpoint;
15746
16499
  exports.ConversationsEndpoint = ConversationsEndpoint;
@@ -15750,7 +16503,9 @@ exports.DEFAULT_STALL_STOP_AFTER = DEFAULT_STALL_STOP_AFTER;
15750
16503
  exports.DispatchEndpoint = DispatchEndpoint;
15751
16504
  exports.ENSURE_RELEASE_DEPLOY_CONFLICT_MESSAGE = ENSURE_RELEASE_DEPLOY_CONFLICT_MESSAGE;
15752
16505
  exports.EvalBuilder = EvalBuilder;
16506
+ exports.EvalDriftError = EvalDriftError;
15753
16507
  exports.EvalEndpoint = EvalEndpoint;
16508
+ exports.EvalEnsureConflictError = EvalEnsureConflictError;
15754
16509
  exports.EvalRunner = EvalRunner;
15755
16510
  exports.EvalSuitesNamespace = EvalSuitesNamespace;
15756
16511
  exports.EvalsNamespace = EvalsNamespace;
@@ -15764,6 +16519,7 @@ exports.FlowStepsEndpoint = FlowStepsEndpoint;
15764
16519
  exports.FlowVersionsEndpoint = FlowVersionsEndpoint;
15765
16520
  exports.FlowsEndpoint = FlowsEndpoint;
15766
16521
  exports.FlowsNamespace = FlowsNamespace;
16522
+ exports.FpoDriftError = FpoDriftError;
15767
16523
  exports.IntegrationsEndpoint = IntegrationsEndpoint;
15768
16524
  exports.LEDGER_ARTIFACT_LINE_PREFIX = LEDGER_ARTIFACT_LINE_PREFIX;
15769
16525
  exports.LIVE_AGENT_ALIAS = LIVE_AGENT_ALIAS;
@@ -15775,6 +16531,9 @@ exports.ProductsNamespace = ProductsNamespace;
15775
16531
  exports.PromptsEndpoint = PromptsEndpoint;
15776
16532
  exports.PromptsNamespace = PromptsNamespace;
15777
16533
  exports.ProviderKeysEndpoint = ProviderKeysEndpoint;
16534
+ exports.QuestionSetDriftError = QuestionSetDriftError;
16535
+ exports.QuestionSetEnsureConflictError = QuestionSetEnsureConflictError;
16536
+ exports.QuestionSetsNamespace = QuestionSetsNamespace;
15778
16537
  exports.RUNTYPE_CLIENT_KIND = RUNTYPE_CLIENT_KIND;
15779
16538
  exports.RecordsEndpoint = RecordsEndpoint;
15780
16539
  exports.RunsNamespace = RunsNamespace;
@@ -15786,6 +16545,8 @@ exports.SDK_USER_AGENT = SDK_USER_AGENT;
15786
16545
  exports.SDK_VERSION = SDK_VERSION;
15787
16546
  exports.STEP_FIELD_REGISTRY = STEP_FIELD_REGISTRY;
15788
16547
  exports.STEP_TYPE_TO_METHOD = STEP_TYPE_TO_METHOD;
16548
+ exports.ScheduleDriftError = ScheduleDriftError;
16549
+ exports.ScheduleEnsureConflictError = ScheduleEnsureConflictError;
15789
16550
  exports.SchedulesEndpoint = SchedulesEndpoint;
15790
16551
  exports.SecretsEndpoint = SecretsEndpoint;
15791
16552
  exports.SkillDriftError = SkillDriftError;
@@ -15825,6 +16586,10 @@ exports.computeEvalContentHash = computeEvalContentHash;
15825
16586
  exports.computeFlowContentHash = computeFlowContentHash;
15826
16587
  exports.computeFpoContentHash = computeFpoContentHash;
15827
16588
  exports.computeProductContentHash = computeProductContentHash;
16589
+ exports.computeQuestionSetContentHash = computeQuestionSetContentHash;
16590
+ exports.computeRecordCollectionContentHash = computeRecordCollectionContentHash;
16591
+ exports.computeScheduleContentHash = computeScheduleContentHash;
16592
+ exports.computeScheduleDefinitionHash = computeScheduleDefinitionHash;
15828
16593
  exports.computeSkillContentHash = computeSkillContentHash;
15829
16594
  exports.computeSurfaceContentHash = computeSurfaceContentHash;
15830
16595
  exports.computeToolContentHash = computeToolContentHash;
@@ -15842,6 +16607,8 @@ exports.defineFlow = defineFlow;
15842
16607
  exports.defineFpo = defineFpo;
15843
16608
  exports.definePlaybook = definePlaybook;
15844
16609
  exports.defineProduct = defineProduct;
16610
+ exports.defineQuestionSet = defineQuestionSet;
16611
+ exports.defineSchedule = defineSchedule;
15845
16612
  exports.defineSkill = defineSkill;
15846
16613
  exports.defineSurface = defineSurface;
15847
16614
  exports.defineTool = defineTool;
@@ -15849,8 +16616,10 @@ exports.deployWorkflow = deployWorkflow;
15849
16616
  exports.ensureDefaultWorkflowHooks = ensureDefaultWorkflowHooks;
15850
16617
  exports.ensureEval = ensureEval;
15851
16618
  exports.ensureFpo = ensureFpo;
16619
+ exports.ensureSchedule = ensureSchedule;
15852
16620
  exports.evaluateGeneratedRuntimeToolProposal = evaluateGeneratedRuntimeToolProposal;
15853
16621
  exports.extractDeclaredToolResultChars = extractDeclaredToolResultChars;
16622
+ exports.fpoPlanHasDrift = fpoPlanHasDrift;
15854
16623
  exports.gameWorkflow = gameWorkflow;
15855
16624
  exports.getDefaultPlanPath = getDefaultPlanPath;
15856
16625
  exports.getLikelySupportingCandidatePaths = getLikelySupportingCandidatePaths;
@@ -15875,6 +16644,9 @@ exports.normalizeAgentDefinition = normalizeAgentDefinition;
15875
16644
  exports.normalizeCandidatePath = normalizeCandidatePath;
15876
16645
  exports.normalizeFpoDefinition = normalizeFpoDefinition;
15877
16646
  exports.normalizeProductDefinition = normalizeProductDefinition;
16647
+ exports.normalizeQuestionSetDefinition = normalizeQuestionSetDefinition;
16648
+ exports.normalizeRecordCollectionDefinition = normalizeRecordCollectionDefinition;
16649
+ exports.normalizeScheduleContent = normalizeScheduleContent;
15878
16650
  exports.normalizeSkillDefinition = normalizeSkillDefinition;
15879
16651
  exports.normalizeSurfaceDefinition = normalizeSurfaceDefinition;
15880
16652
  exports.normalizeToolDefinition = normalizeToolDefinition;
@@ -15890,6 +16662,7 @@ exports.promoteAgent = promoteAgent;
15890
16662
  exports.promotionIdempotencyKey = promotionIdempotencyKey;
15891
16663
  exports.pullEval = pullEval;
15892
16664
  exports.pullFpo = pullFpo;
16665
+ exports.pullSchedule = pullSchedule;
15893
16666
  exports.ranStep = ranStep;
15894
16667
  exports.regex = regex;
15895
16668
  exports.registerWorkflowHook = registerWorkflowHook;