@aexhq/sdk 0.16.0 → 0.16.1

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/README.md CHANGED
@@ -9,7 +9,8 @@ aex is a TypeScript-first SDK + CLI for running autonomous agent sessions across
9
9
  ```ts
10
10
  import {
11
11
  AgentExecutor, // the only client class — submits durable runs to aex
12
- Skill, // workspace / provider / inline skill bundles
12
+ RunModels, // closed public model id constants
13
+ Skill, // local, URL, and catalog skill bundles normalized to assets
13
14
  McpServer, // MCP server declarations (headers split into secrets server-side)
14
15
  ProxyEndpoint, // per-run managed HTTP proxy endpoint
15
16
  AgentsMd, // AGENTS.md / CLAUDE.md uploads
@@ -55,15 +56,15 @@ The aex URL defaults to `https://api.aex.dev`. Set `--aex-url` on the CLI or `ba
55
56
  ## Quickstart (SDK)
56
57
 
57
58
  ```ts
58
- import { AgentExecutor } from "@aexhq/sdk";
59
+ import { AgentExecutor, RunModels } from "@aexhq/sdk";
59
60
 
60
61
  const aex = new AgentExecutor({
61
- apiToken: process.env.AEX_API_TOKEN!
62
+ apiToken: process.env.AEX_API_TOKEN!,
62
63
  // baseUrl defaults to https://api.aex.dev - set it for local or staging planes.
63
64
  });
64
65
 
65
66
  const runId = await aex.submitRun({
66
- model: "claude-haiku-4-5",
67
+ model: RunModels.CLAUDE_HAIKU_4_5,
67
68
  system: "You are a concise automation agent.",
68
69
  prompt: "Write a short answer about agent-first SDK design.",
69
70
  secrets: { apiKey: process.env.ANTHROPIC_API_KEY! }
@@ -87,9 +88,9 @@ Reusable, credential-free configs can be ordinary functions:
87
88
  ```ts
88
89
  function summarise(topic: string) {
89
90
  return {
90
- model: "claude-haiku-4-5",
91
- system: "You are a concise automation agent.",
92
- prompt: `Write a short answer about ${topic}.`
91
+ model: RunModels.CLAUDE_HAIKU_4_5,
92
+ system: "You are a concise automation agent.",
93
+ prompt: `Write a short answer about ${topic}.`
93
94
  };
94
95
  }
95
96
 
@@ -127,7 +128,9 @@ aex run \
127
128
  --follow
128
129
  ```
129
130
 
130
- `--config` accepts a plain run-config JSON file for a single run request: `{ model, system?, prompt, skills?, mcpServers?, environment?, proxyEndpoints?, metadata? }`. There is no saved-definition product or interpolation DSL — build the JSON at the call site.
131
+ `--config` accepts a plain run-config JSON file for a single run request: `{ model, system?, prompt, skills?, mcpServers?, environment?, runtimeSize?, timeout?, postHook?, proxyEndpoints?, metadata? }`. There is no saved-definition product or interpolation DSL — build the JSON at the call site. SDK code should use `RunModels`; config JSON is validated against the same `RUN_MODELS` allowlist.
132
+
133
+ Runtime controls: omit `builtins` to use the default `["developer"]` toolkit, pass `builtins: []` to disable builtins for a pure-MCP run, use `outputMode: "stream"` for per-token assistant text, prefer `RuntimeSizes` for `runtimeSize`, and use `postHook` to run a verifier command after the agent exits successfully.
131
134
 
132
135
  ## Test commands
133
136
 
@@ -1,8 +1,10 @@
1
1
  export * from "./proxy-protocol.js";
2
2
  export * from "./provider-support.js";
3
+ export * from "./models.js";
3
4
  export * from "./status.js";
4
5
  export * from "./submission.js";
5
6
  export * from "./runtime-sizes.js";
7
+ export * from "./post-hook.js";
6
8
  export * from "./runner-event.js";
7
9
  export * from "./event-envelope.js";
8
10
  export * from "./connection-ticket.js";
@@ -1,8 +1,10 @@
1
1
  export * from "./proxy-protocol.js";
2
2
  export * from "./provider-support.js";
3
+ export * from "./models.js";
3
4
  export * from "./status.js";
4
5
  export * from "./submission.js";
5
6
  export * from "./runtime-sizes.js";
7
+ export * from "./post-hook.js";
6
8
  export * from "./runner-event.js";
7
9
  export * from "./event-envelope.js";
8
10
  export * from "./connection-ticket.js";
@@ -1 +1,3 @@
1
+ export * from "./models.js";
2
+ export * from "./post-hook.js";
1
3
  export * from "./submission.js";
@@ -4,5 +4,7 @@
4
4
  // of validator code. NOT part of the public `@aexhq/contracts` surface — do
5
5
  // NOT add this to the package `index`; consumers reach it via the explicit
6
6
  // `@aexhq/contracts/internal` subpath.
7
+ export * from "./models.js";
8
+ export * from "./post-hook.js";
7
9
  export * from "./submission.js";
8
10
  //# sourceMappingURL=internal.js.map
@@ -1,4 +1,5 @@
1
1
  import type { RunProvider, RuntimeKind } from "./submission.js";
2
+ import type { RunModel } from "./models.js";
2
3
  export declare const CREDENTIAL_MODES: readonly ["byok", "managed"];
3
4
  export type CredentialMode = (typeof CREDENTIAL_MODES)[number];
4
5
  export declare const DEFAULT_CREDENTIAL_MODE: CredentialMode;
@@ -31,7 +32,7 @@ export interface ManagedKeyPolicyV1 {
31
32
  readonly billingRequired: true;
32
33
  readonly providers: readonly RunProvider[];
33
34
  readonly runtimes: readonly RuntimeKind[];
34
- readonly models?: readonly string[];
35
+ readonly models?: readonly RunModel[];
35
36
  readonly features: ManagedKeyFeaturePolicyV1;
36
37
  }
37
38
  /**
@@ -75,7 +76,7 @@ export interface ManagedCredentialResolutionInput {
75
76
  readonly runId: string;
76
77
  readonly provider: RunProvider;
77
78
  readonly runtime: RuntimeKind;
78
- readonly model: string;
79
+ readonly model: RunModel;
79
80
  readonly policy: ManagedKeyPolicyV1;
80
81
  }
81
82
  export interface ManagedCredentialLease {
@@ -0,0 +1,33 @@
1
+ import type { RunProvider } from "./submission.js";
2
+ /**
3
+ * Closed set of model ids accepted by the public run-submission schema.
4
+ *
5
+ * The provider-proxy still sends the model id through to the selected upstream
6
+ * provider, but callers cannot submit arbitrary strings. Additions belong here
7
+ * first so SDK types, CLI validation, docs examples, and platform parsing move
8
+ * together.
9
+ */
10
+ export declare const RUN_MODELS: readonly ["claude-haiku-4-5", "claude-3-5-haiku-latest", "claude-3-5-sonnet-latest", "deepseek-chat", "gpt-4.1", "gpt-4o-mini", "gemini-2.0-flash", "gemini-2.5-flash", "mistral-large-latest", "mistral-small-latest"];
11
+ export type RunModel = (typeof RUN_MODELS)[number];
12
+ export declare const RunModels: {
13
+ readonly CLAUDE_HAIKU_4_5: "claude-haiku-4-5";
14
+ readonly CLAUDE_3_5_HAIKU_LATEST: "claude-3-5-haiku-latest";
15
+ readonly CLAUDE_3_5_SONNET_LATEST: "claude-3-5-sonnet-latest";
16
+ readonly DEEPSEEK_CHAT: "deepseek-chat";
17
+ readonly GPT_4_1: "gpt-4.1";
18
+ readonly GPT_4O_MINI: "gpt-4o-mini";
19
+ readonly GEMINI_2_0_FLASH: "gemini-2.0-flash";
20
+ readonly GEMINI_2_5_FLASH: "gemini-2.5-flash";
21
+ readonly MISTRAL_LARGE_LATEST: "mistral-large-latest";
22
+ readonly MISTRAL_SMALL_LATEST: "mistral-small-latest";
23
+ };
24
+ export declare const RUN_MODELS_BY_PROVIDER: {
25
+ readonly anthropic: readonly ["claude-haiku-4-5", "claude-3-5-haiku-latest", "claude-3-5-sonnet-latest"];
26
+ readonly deepseek: readonly ["deepseek-chat"];
27
+ readonly openai: readonly ["gpt-4.1", "gpt-4o-mini"];
28
+ readonly gemini: readonly ["gemini-2.0-flash", "gemini-2.5-flash"];
29
+ readonly mistral: readonly ["mistral-large-latest", "mistral-small-latest"];
30
+ };
31
+ export declare function isRunModel(input: unknown): input is RunModel;
32
+ export declare function parseRunModel(input: unknown, field?: string): RunModel;
33
+ export declare function assertRunModelMatchesProvider(provider: RunProvider, model: RunModel, field?: string): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Closed set of model ids accepted by the public run-submission schema.
3
+ *
4
+ * The provider-proxy still sends the model id through to the selected upstream
5
+ * provider, but callers cannot submit arbitrary strings. Additions belong here
6
+ * first so SDK types, CLI validation, docs examples, and platform parsing move
7
+ * together.
8
+ */
9
+ export const RUN_MODELS = [
10
+ "claude-haiku-4-5",
11
+ "claude-3-5-haiku-latest",
12
+ "claude-3-5-sonnet-latest",
13
+ "deepseek-chat",
14
+ "gpt-4.1",
15
+ "gpt-4o-mini",
16
+ "gemini-2.0-flash",
17
+ "gemini-2.5-flash",
18
+ "mistral-large-latest",
19
+ "mistral-small-latest"
20
+ ];
21
+ export const RunModels = {
22
+ CLAUDE_HAIKU_4_5: "claude-haiku-4-5",
23
+ CLAUDE_3_5_HAIKU_LATEST: "claude-3-5-haiku-latest",
24
+ CLAUDE_3_5_SONNET_LATEST: "claude-3-5-sonnet-latest",
25
+ DEEPSEEK_CHAT: "deepseek-chat",
26
+ GPT_4_1: "gpt-4.1",
27
+ GPT_4O_MINI: "gpt-4o-mini",
28
+ GEMINI_2_0_FLASH: "gemini-2.0-flash",
29
+ GEMINI_2_5_FLASH: "gemini-2.5-flash",
30
+ MISTRAL_LARGE_LATEST: "mistral-large-latest",
31
+ MISTRAL_SMALL_LATEST: "mistral-small-latest"
32
+ };
33
+ export const RUN_MODELS_BY_PROVIDER = {
34
+ anthropic: [
35
+ RunModels.CLAUDE_HAIKU_4_5,
36
+ RunModels.CLAUDE_3_5_HAIKU_LATEST,
37
+ RunModels.CLAUDE_3_5_SONNET_LATEST
38
+ ],
39
+ deepseek: [RunModels.DEEPSEEK_CHAT],
40
+ openai: [RunModels.GPT_4_1, RunModels.GPT_4O_MINI],
41
+ gemini: [RunModels.GEMINI_2_0_FLASH, RunModels.GEMINI_2_5_FLASH],
42
+ mistral: [RunModels.MISTRAL_LARGE_LATEST, RunModels.MISTRAL_SMALL_LATEST]
43
+ };
44
+ export function isRunModel(input) {
45
+ return typeof input === "string" && RUN_MODELS.includes(input);
46
+ }
47
+ export function parseRunModel(input, field = "submission.model") {
48
+ if (!isRunModel(input)) {
49
+ throw new Error(`${field} must be one of: ${RUN_MODELS.join(", ")}`);
50
+ }
51
+ return input;
52
+ }
53
+ export function assertRunModelMatchesProvider(provider, model, field = "submission.model") {
54
+ if (!RUN_MODELS_BY_PROVIDER[provider].includes(model)) {
55
+ throw new Error(`${field} ${JSON.stringify(model)} is not supported for provider ${provider}; ` +
56
+ `expected one of: ${RUN_MODELS_BY_PROVIDER[provider].join(", ")}`);
57
+ }
58
+ }
59
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1,28 @@
1
+ /** Default post-agent-run hook timeout (5 minutes). */
2
+ export declare const DEFAULT_POST_HOOK_TIMEOUT_MS: number;
3
+ /** Default number of repair turns after a failing hook. */
4
+ export declare const DEFAULT_POST_HOOK_MAX_TURNS = 10;
5
+ /**
6
+ * Customer-facing post-agent-run verifier shape. The SDK and run-config JSON
7
+ * send this form on the wire; the server parser normalizes it to
8
+ * {@link PlatformPostHook}.
9
+ */
10
+ export interface PlatformPostHookInput {
11
+ readonly command: string;
12
+ readonly timeout?: string;
13
+ readonly maxTurns?: number;
14
+ readonly maxChars?: number | null;
15
+ }
16
+ /** Parsed post-agent-run verifier carried through dispatch to the runner. */
17
+ export interface PlatformPostHook {
18
+ readonly command: string;
19
+ readonly timeoutMs: number;
20
+ readonly maxTurns: number;
21
+ readonly maxChars: number | null;
22
+ }
23
+ /**
24
+ * Parse the public `postHook` option. An omitted hook, null hook, or hook with
25
+ * an empty/whitespace-only command is treated as omitted so callers can pre-fill
26
+ * configs without enabling the verifier accidentally.
27
+ */
28
+ export declare function parsePostHook(input: unknown, path?: string): PlatformPostHook | undefined;
@@ -0,0 +1,68 @@
1
+ import { parseDurationToMs } from "./runtime-sizes.js";
2
+ /** Default post-agent-run hook timeout (5 minutes). */
3
+ export const DEFAULT_POST_HOOK_TIMEOUT_MS = 5 * 60 * 1000;
4
+ /** Default number of repair turns after a failing hook. */
5
+ export const DEFAULT_POST_HOOK_MAX_TURNS = 10;
6
+ /**
7
+ * Parse the public `postHook` option. An omitted hook, null hook, or hook with
8
+ * an empty/whitespace-only command is treated as omitted so callers can pre-fill
9
+ * configs without enabling the verifier accidentally.
10
+ */
11
+ export function parsePostHook(input, path = "postHook") {
12
+ if (input === undefined || input === null) {
13
+ return undefined;
14
+ }
15
+ const value = requirePostHookRecord(input, path);
16
+ const allowed = new Set(["command", "timeout", "maxTurns", "maxChars"]);
17
+ for (const key of Object.keys(value)) {
18
+ if (!allowed.has(key)) {
19
+ throw new Error(`${path}.${key} is not an allowed field; permitted: command, timeout, maxTurns, maxChars`);
20
+ }
21
+ }
22
+ if (typeof value.command !== "string") {
23
+ throw new Error(`${path}.command must be a string`);
24
+ }
25
+ if (value.command.trim().length === 0) {
26
+ return undefined;
27
+ }
28
+ const timeoutMs = parsePostHookTimeout(value.timeout, `${path}.timeout`);
29
+ const maxTurns = parseNonNegativeInteger(value.maxTurns, `${path}.maxTurns`, DEFAULT_POST_HOOK_MAX_TURNS);
30
+ const maxChars = value.maxChars === null
31
+ ? null
32
+ : parseNonNegativeInteger(value.maxChars, `${path}.maxChars`, null);
33
+ return {
34
+ command: value.command,
35
+ timeoutMs,
36
+ maxTurns,
37
+ maxChars
38
+ };
39
+ }
40
+ function parsePostHookTimeout(input, path) {
41
+ if (input === undefined) {
42
+ return DEFAULT_POST_HOOK_TIMEOUT_MS;
43
+ }
44
+ if (typeof input !== "string") {
45
+ throw new Error(`${path} must be a duration string (e.g. "5m", "30s"); got ${JSON.stringify(input)}`);
46
+ }
47
+ const ms = parseDurationToMs(input);
48
+ if (ms <= 0) {
49
+ throw new Error(`${path} must be greater than 0ms; got ${ms}ms`);
50
+ }
51
+ return ms;
52
+ }
53
+ function parseNonNegativeInteger(input, path, defaultValue) {
54
+ if (input === undefined) {
55
+ return defaultValue;
56
+ }
57
+ if (typeof input !== "number" || !Number.isSafeInteger(input) || input < 0) {
58
+ throw new Error(`${path} must be a non-negative integer`);
59
+ }
60
+ return input;
61
+ }
62
+ function requirePostHookRecord(input, path) {
63
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
64
+ throw new Error(`${path} must be an object`);
65
+ }
66
+ return input;
67
+ }
68
+ //# sourceMappingURL=post-hook.js.map
@@ -3,17 +3,11 @@
3
3
  *
4
4
  * Public composition concepts:
5
5
  *
6
- * - `SkillRef` is the wire-level reference to a skill either an
7
- * `skl_*` id pointing at a workspace-uploaded bundle, a
8
- * `{vendor, skillId, version}` reference to a provider built-in, or
9
- * a `{slot, name, contentHash}` reference to per-run bytes attached
10
- * as a multipart part on the submitRun call (and torn down at run
11
- * terminal). The three shapes are discriminated by `kind` so
12
- * consumers branch mechanically and providers can never accidentally
13
- * be looked up in `skill_bundles`. Transient refs do NOT round-trip
14
- * through JSON (bytes can't be serialised back); `parseRunRequestConfig`
15
- * therefore rejects them while the BFF multipart submission parser
16
- * accepts them.
6
+ * - `SkillRef` is the wire-level reference to a skill. Public run
7
+ * configs use storage-neutral `kind:"asset"` refs produced by the SDK
8
+ * upload path or by workspace catalog records. Provider skill refs remain
9
+ * in the parser for wire compatibility, but the managed runtime rejects
10
+ * them at submission time.
17
11
  *
18
12
  * - `McpServerRef` is the non-secret part of an MCP server declaration:
19
13
  * `name` and `url`. Bearer / cookie / per-request headers travel in
@@ -37,7 +31,9 @@
37
31
  * boundary.
38
32
  */
39
33
  import type { JsonValue, PlatformProxyEndpoint, PlatformEnvironment } from "./submission.js";
34
+ import { type RunModel } from "./models.js";
40
35
  import type { RuntimeSize } from "./runtime-sizes.js";
36
+ import { type PlatformPostHookInput } from "./post-hook.js";
41
37
  /**
42
38
  * Mirrors the server-side CHECK constraint
43
39
  * `skill_bundles_id_format_chk = check (id ~ '^skl_[A-Za-z0-9_-]{8,128}$')`
@@ -62,10 +58,9 @@ export declare const SKILL_BUNDLE_LIMITS: {
62
58
  /** Compressed (.zip) ceiling. */
63
59
  readonly maxCompressedBytes: number;
64
60
  /**
65
- * Hard ceiling for the direct-to-storage (presigned PUT) upload path, where
66
- * bytes never transit the hosted API so its memory/request-payload limits no
67
- * longer cap the bundle. Kept well under the object store's 5 GiB single-PUT
68
- * limit; objects above this would need S3 multipart, which is out of scope.
61
+ * Hard ceiling for the direct-to-storage upload path. Bytes never transit the
62
+ * hosted API, so its memory/request-payload limits do not cap accepted
63
+ * bundles; objects above this product cap are rejected before upload.
69
64
  */
70
65
  readonly maxBytes: number;
71
66
  /** Sum of uncompressed file sizes. */
@@ -259,7 +254,7 @@ export declare function rejectStdioMcpShape(record: Record<string, unknown>): vo
259
254
  * into the normal `submitRun` request.
260
255
  */
261
256
  export interface RunRequestConfig {
262
- readonly model: string;
257
+ readonly model: RunModel;
263
258
  readonly system?: string;
264
259
  readonly prompt: string | readonly string[];
265
260
  readonly skills?: readonly SkillRef[];
@@ -269,6 +264,8 @@ export interface RunRequestConfig {
269
264
  readonly runtimeSize?: RuntimeSize;
270
265
  /** Run deadline as a duration string (`"1h"`, `"30m"`); bounded [1m, 6h] server-side. */
271
266
  readonly timeout?: string;
267
+ /** Post-agent-run verifier command. Empty command is treated as omitted. */
268
+ readonly postHook?: PlatformPostHookInput;
272
269
  readonly proxyEndpoints?: readonly PlatformProxyEndpoint[];
273
270
  readonly metadata?: Readonly<Record<string, JsonValue>>;
274
271
  }
@@ -290,7 +287,7 @@ export declare function parseRunRequestConfig(input: unknown): RunRequestConfig;
290
287
  * the audit log don't have to re-handle two shapes.
291
288
  */
292
289
  export interface NormalisedRunRequestConfig {
293
- readonly model: string;
290
+ readonly model: RunModel;
294
291
  readonly system?: string;
295
292
  readonly prompt: readonly string[];
296
293
  readonly skills: readonly SkillRef[];
@@ -298,6 +295,7 @@ export interface NormalisedRunRequestConfig {
298
295
  readonly environment?: PlatformEnvironment;
299
296
  readonly proxyEndpoints?: readonly PlatformProxyEndpoint[];
300
297
  readonly metadata?: Readonly<Record<string, JsonValue>>;
298
+ readonly postHook?: PlatformPostHookInput;
301
299
  /**
302
300
  * MCP servers whose run-config entry carried `headers`. Keyed by the `name`
303
301
  * that appears in `mcpServers` so the BFF can pair them up.
@@ -3,17 +3,11 @@
3
3
  *
4
4
  * Public composition concepts:
5
5
  *
6
- * - `SkillRef` is the wire-level reference to a skill either an
7
- * `skl_*` id pointing at a workspace-uploaded bundle, a
8
- * `{vendor, skillId, version}` reference to a provider built-in, or
9
- * a `{slot, name, contentHash}` reference to per-run bytes attached
10
- * as a multipart part on the submitRun call (and torn down at run
11
- * terminal). The three shapes are discriminated by `kind` so
12
- * consumers branch mechanically and providers can never accidentally
13
- * be looked up in `skill_bundles`. Transient refs do NOT round-trip
14
- * through JSON (bytes can't be serialised back); `parseRunRequestConfig`
15
- * therefore rejects them while the BFF multipart submission parser
16
- * accepts them.
6
+ * - `SkillRef` is the wire-level reference to a skill. Public run
7
+ * configs use storage-neutral `kind:"asset"` refs produced by the SDK
8
+ * upload path or by workspace catalog records. Provider skill refs remain
9
+ * in the parser for wire compatibility, but the managed runtime rejects
10
+ * them at submission time.
17
11
  *
18
12
  * - `McpServerRef` is the non-secret part of an MCP server declaration:
19
13
  * `name` and `url`. Bearer / cookie / per-request headers travel in
@@ -36,6 +30,8 @@
36
30
  * Keep this as the public source of truth for the SDK/CLI composition
37
31
  * boundary.
38
32
  */
33
+ import { parseRunModel } from "./models.js";
34
+ import { parsePostHook } from "./post-hook.js";
39
35
  // ---------------------------------------------------------------------------
40
36
  // Skill ID + name format
41
37
  // ---------------------------------------------------------------------------
@@ -66,10 +62,9 @@ export const SKILL_BUNDLE_LIMITS = {
66
62
  /** Compressed (.zip) ceiling. */
67
63
  maxCompressedBytes: 10 * 1024 * 1024,
68
64
  /**
69
- * Hard ceiling for the direct-to-storage (presigned PUT) upload path, where
70
- * bytes never transit the hosted API so its memory/request-payload limits no
71
- * longer cap the bundle. Kept well under the object store's 5 GiB single-PUT
72
- * limit; objects above this would need S3 multipart, which is out of scope.
65
+ * Hard ceiling for the direct-to-storage upload path. Bytes never transit the
66
+ * hosted API, so its memory/request-payload limits do not cap accepted
67
+ * bundles; objects above this product cap are rejected before upload.
73
68
  */
74
69
  maxBytes: 2 * 1024 * 1024 * 1024,
75
70
  /** Sum of uncompressed file sizes. */
@@ -609,6 +604,7 @@ export function parseRunRequestConfig(input) {
609
604
  "environment",
610
605
  "runtimeSize",
611
606
  "timeout",
607
+ "postHook",
612
608
  "proxyEndpoints",
613
609
  "metadata"
614
610
  ]);
@@ -617,10 +613,7 @@ export function parseRunRequestConfig(input) {
617
613
  throw new Error(`run request config contains unexpected field: ${key}`);
618
614
  }
619
615
  }
620
- const model = record.model;
621
- if (typeof model !== "string" || model.length === 0) {
622
- throw new Error("run request config model must be a non-empty string");
623
- }
616
+ const model = parseRunModel(record.model, "run request config model");
624
617
  const system = record.system;
625
618
  if (system !== undefined && typeof system !== "string") {
626
619
  throw new Error("run request config system, when provided, must be a string");
@@ -628,6 +621,7 @@ export function parseRunRequestConfig(input) {
628
621
  const prompt = parseRunRequestConfigPrompt(record.prompt);
629
622
  const skills = parseRunRequestConfigSkills(record.skills);
630
623
  const mcpServers = parseRunRequestConfigMcpServers(record.mcpServers);
624
+ const postHook = parsePostHook(record.postHook, "run request config postHook");
631
625
  return {
632
626
  model,
633
627
  ...(system !== undefined ? { system } : {}),
@@ -647,6 +641,9 @@ export function parseRunRequestConfig(input) {
647
641
  ...(record.timeout !== undefined
648
642
  ? { timeout: record.timeout }
649
643
  : {}),
644
+ ...(postHook !== undefined
645
+ ? { postHook: record.postHook }
646
+ : {}),
650
647
  ...(record.proxyEndpoints !== undefined
651
648
  ? { proxyEndpoints: record.proxyEndpoints }
652
649
  : {}),
@@ -724,6 +721,7 @@ export function normaliseRunRequestConfig(config) {
724
721
  ...(config.environment !== undefined ? { environment: config.environment } : {}),
725
722
  ...(config.proxyEndpoints !== undefined ? { proxyEndpoints: config.proxyEndpoints } : {}),
726
723
  ...(config.metadata !== undefined ? { metadata: config.metadata } : {}),
724
+ ...(config.postHook !== undefined ? { postHook: config.postHook } : {}),
727
725
  mcpServerSecrets
728
726
  };
729
727
  }
@@ -185,9 +185,9 @@ export interface RunUnit {
185
185
  * submission. Never throws on minor unknown keys so we can
186
186
  * forward-compat with worker-side enrichment.
187
187
  *
188
- * Returns a typed shape even for malformed snapshots the worst case
189
- * is `{kind: "submission", submission: {model: "", ...}}` with empty
190
- * defaults — because the dashboard must still render *something* for a
191
- * buggy historical row rather than 500ing the whole detail page.
188
+ * Returns a typed shape even for malformed snapshots by falling back to the
189
+ * default public model and empty collection defaults, because the dashboard
190
+ * must still render *something* for a buggy historical row rather than 500ing
191
+ * the whole detail page.
192
192
  */
193
193
  export declare function parseRunUnitSubmission(input: unknown): RunUnitSubmission;
@@ -20,6 +20,7 @@
20
20
  * detail response stays bounded). The archive zip carries the bytes.
21
21
  */
22
22
  import { parseMcpServerRef, parseSkillRef } from "./run-config.js";
23
+ import { RunModels, parseRunModel } from "./models.js";
23
24
  import { PLATFORM_PACKAGE_ECOSYSTEMS } from "./submission.js";
24
25
  // ---------------------------------------------------------------------------
25
26
  // Submission parser
@@ -29,10 +30,10 @@ import { PLATFORM_PACKAGE_ECOSYSTEMS } from "./submission.js";
29
30
  * submission. Never throws on minor unknown keys so we can
30
31
  * forward-compat with worker-side enrichment.
31
32
  *
32
- * Returns a typed shape even for malformed snapshots the worst case
33
- * is `{kind: "submission", submission: {model: "", ...}}` with empty
34
- * defaults — because the dashboard must still render *something* for a
35
- * buggy historical row rather than 500ing the whole detail page.
33
+ * Returns a typed shape even for malformed snapshots by falling back to the
34
+ * default public model and empty collection defaults, because the dashboard
35
+ * must still render *something* for a buggy historical row rather than 500ing
36
+ * the whole detail page.
36
37
  */
37
38
  export function parseRunUnitSubmission(input) {
38
39
  if (!input || typeof input !== "object" || Array.isArray(input)) {
@@ -52,7 +53,7 @@ function parseFlatProjection(value) {
52
53
  const allowedDirs = toOptionalStringArray(outputsRaw.allowedDirs);
53
54
  const deniedDirs = toOptionalStringArray(outputsRaw.deniedDirs);
54
55
  const submission = {
55
- model: typeof submissionRaw.model === "string" ? submissionRaw.model : "",
56
+ model: coerceRunUnitModel(submissionRaw.model),
56
57
  ...(typeof submissionRaw.system === "string" ? { system: submissionRaw.system } : {}),
57
58
  prompt: toStringArray(submissionRaw.prompt),
58
59
  skills: toSkillRefArray(submissionRaw.skills),
@@ -87,7 +88,7 @@ function fallbackFlat() {
87
88
  return {
88
89
  kind: "submission",
89
90
  submission: {
90
- model: "",
91
+ model: RunModels.CLAUDE_HAIKU_4_5,
91
92
  prompt: [],
92
93
  skills: [],
93
94
  agentsMd: [],
@@ -104,6 +105,16 @@ function fallbackFlat() {
104
105
  function isRecord(value) {
105
106
  return typeof value === "object" && value !== null && !Array.isArray(value);
106
107
  }
108
+ function coerceRunUnitModel(value) {
109
+ if (typeof value !== "string")
110
+ return RunModels.CLAUDE_HAIKU_4_5;
111
+ try {
112
+ return parseRunModel(value, "run unit submission.model");
113
+ }
114
+ catch {
115
+ return RunModels.CLAUDE_HAIKU_4_5;
116
+ }
117
+ }
107
118
  function isJsonRecord(value) {
108
119
  return isRecord(value);
109
120
  }
@@ -2,6 +2,8 @@ import { PROXY_ENDPOINT_DEFAULTS, type ProxyAuthShape, type ProxyMethod, type Pr
2
2
  export { PROXY_ENDPOINT_DEFAULTS };
3
3
  import type { AgentsMdRef, FileRef, McpServerRef, SkillRef } from "./run-config.js";
4
4
  import { type RuntimeSize } from "./runtime-sizes.js";
5
+ import { type PlatformPostHook, type PlatformPostHookInput } from "./post-hook.js";
6
+ import { type RunModel } from "./models.js";
5
7
  import { type RuntimeSecurityProfileName } from "./runtime-security-profile.js";
6
8
  import { type CredentialMode, type ManagedKeyPolicyV1 } from "./managed-key.js";
7
9
  export type JsonPrimitive = string | number | boolean | null;
@@ -226,7 +228,7 @@ export declare function optionalPositiveInt(input: unknown, field: string): numb
226
228
  * into `run_skill_snapshots`), provider refs pass through unchanged.
227
229
  */
228
230
  export interface PlatformSubmission {
229
- readonly model: string;
231
+ readonly model: RunModel;
230
232
  readonly system?: string;
231
233
  readonly prompt: readonly string[];
232
234
  readonly skills: readonly SkillRef[];
@@ -333,6 +335,13 @@ export interface PlatformRunSubmissionRequest {
333
335
  * terminal wait window and self-kill deadline.
334
336
  */
335
337
  readonly timeoutMs?: number;
338
+ /**
339
+ * Optional post-agent-run verifier. Parsed from the public `postHook`
340
+ * duration/string shape into fixed runner budgets. The runner executes it
341
+ * after a successful agent process and sends failures back through the model
342
+ * for repair until this budget is exhausted.
343
+ */
344
+ readonly postHook?: PlatformPostHook;
336
345
  }
337
346
  /**
338
347
  * Wire shape posted by the SDK and CLI. `workspaceId` is **omitted by
@@ -347,7 +356,7 @@ export interface PlatformRunSubmissionRequest {
347
356
  * {@link DEFAULT_RUN_PROVIDER} (`anthropic`). The parser fills it in
348
357
  * before the value enters the run snapshot.
349
358
  */
350
- export type PlatformRunSubmissionInput = Omit<PlatformRunSubmissionRequest, "workspaceId" | "credentialMode" | "provider" | "runtime" | "timeoutMs"> & {
359
+ export type PlatformRunSubmissionInput = Omit<PlatformRunSubmissionRequest, "workspaceId" | "credentialMode" | "provider" | "runtime" | "timeoutMs" | "postHook"> & {
351
360
  readonly workspaceId?: string;
352
361
  readonly credentialMode?: CredentialMode;
353
362
  readonly provider?: RunProvider;
@@ -363,6 +372,7 @@ export type PlatformRunSubmissionInput = Omit<PlatformRunSubmissionRequest, "wor
363
372
  * {@link PlatformRunSubmissionRequest.timeoutMs}. Absent ⇒ 1h default.
364
373
  */
365
374
  readonly timeout?: string;
375
+ readonly postHook?: PlatformPostHookInput;
366
376
  };
367
377
  export interface ParseRunSubmissionOptions {
368
378
  readonly managedKeyPolicy?: ManagedKeyPolicyV1;
@@ -6,6 +6,8 @@ import { authShapeHeaderName, PROXY_ALLOWED_METHODS, PROXY_ENDPOINT_DEFAULTS, PR
6
6
  export { PROXY_ENDPOINT_DEFAULTS };
7
7
  import { parseAssetRefFields, parseMcpServerRef, parseSkillRef } from "./run-config.js";
8
8
  import { parseRunTimeout, parseRuntimeSize } from "./runtime-sizes.js";
9
+ import { parsePostHook } from "./post-hook.js";
10
+ import { assertRunModelMatchesProvider, parseRunModel } from "./models.js";
9
11
  import { parseRuntimeSecurityProfile } from "./runtime-security-profile.js";
10
12
  import { assertManagedKeyAdmissionAllowed, parseCredentialMode } from "./managed-key.js";
11
13
  /**
@@ -782,6 +784,7 @@ export function parseRunSubmissionRequest(input, options = {}) {
782
784
  "submission",
783
785
  "runtimeSize",
784
786
  "timeout",
787
+ "postHook",
785
788
  "proxyEndpoints",
786
789
  SECRETS_KEY
787
790
  ]);
@@ -815,11 +818,13 @@ export function parseRunSubmissionRequest(input, options = {}) {
815
818
  }
816
819
  const runtimeSize = parseRuntimeSize(value.runtimeSize);
817
820
  const timeoutMs = parseRunTimeout(value.timeout);
821
+ const postHook = parsePostHook(value.postHook, "submission.postHook");
818
822
  const proxyEndpoints = parseProxyEndpoints(value.proxyEndpoints);
819
823
  const secrets = parseInlineSecrets(value.secrets);
820
824
  enforceCredentialSecretPolicy(credentialMode, secrets);
821
825
  crossValidateProxyEndpointsAndAuth(proxyEndpoints, secrets.proxyEndpointAuth);
822
826
  const submission = parseSubmission(value.submission);
827
+ assertRunModelMatchesProvider(provider, submission.model);
823
828
  // mcpServers names must agree across the submission half and the
824
829
  // secrets half — every secrets.mcpServers[i].name MUST resolve to a
825
830
  // submission.mcpServers entry (no orphan secrets) AND the URL must
@@ -861,6 +866,7 @@ export function parseRunSubmissionRequest(input, options = {}) {
861
866
  submission,
862
867
  ...(runtimeSize ? { runtimeSize } : {}),
863
868
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
869
+ ...(postHook !== undefined ? { postHook } : {}),
864
870
  ...(proxyEndpoints ? { proxyEndpoints } : {}),
865
871
  secrets
866
872
  };
@@ -927,7 +933,7 @@ export function parseSubmission(input) {
927
933
  throw new Error(`submission.${key} is not an allowed field; permitted: ${[...allowed].join(", ")}`);
928
934
  }
929
935
  }
930
- const model = requireString(value.model, "submission.model");
936
+ const model = parseRunModel(value.model, "submission.model");
931
937
  const system = optionalString(value.system, "submission.system");
932
938
  const prompt = parsePrompt(value.prompt);
933
939
  const skills = parseSkills(value.skills);