@mantyx/sdk 0.7.0 → 0.9.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.
@@ -39,11 +39,35 @@ type ZodLikeObject = z.ZodType<Record<string, unknown>> & {
39
39
  * integer in `0..100` (where `0` explicitly disables provider thinking).
40
40
  */
41
41
  type ReasoningLevel = "off" | "low" | "medium" | "high" | number;
42
+ /**
43
+ * Either a Zod schema (auto-converted to JSON Schema by the SDK) or a
44
+ * pre-shaped JSON Schema object. Used for both `parameters` and the new
45
+ * `outputSchema` field — same conversion path either way.
46
+ */
47
+ type LocalToolSchemaInput = ZodLikeObject | Record<string, unknown>;
42
48
  interface LocalTool<TArgs = Record<string, unknown>> {
43
49
  readonly kind: "local";
44
50
  readonly name: string;
45
51
  readonly description: string;
46
52
  readonly parameters: ZodLikeObject | undefined;
53
+ /**
54
+ * Optional JSON Schema (or Zod schema, auto-converted) describing the
55
+ * tool's structured return value. Forwarded to providers with per-tool
56
+ * response schemas (Gemini's `responseJsonSchema` on the
57
+ * FunctionDeclaration); other engines surface it through the description
58
+ * and rely on host-side validation. The model uses it to plan follow-up
59
+ * arguments more reliably. See `docs/wire-protocol.md` §3.1.
60
+ */
61
+ readonly outputSchema: LocalToolSchemaInput | undefined;
62
+ /**
63
+ * When `true`, MANTYX appends a stable hint to the model-facing
64
+ * description telling the model not to re-issue calls while a previous
65
+ * invocation is still pending. Useful for tools that may yield a
66
+ * `pending` / status response and the SDK polls on its own; without the
67
+ * hint, models routinely fire repeat calls and waste turns. Pure
68
+ * declarative — MANTYX does not change scheduling.
69
+ */
70
+ readonly longRunning: boolean;
47
71
  readonly execute: (args: TArgs) => Promise<string> | string;
48
72
  }
49
73
  interface DefineLocalToolOptions<T extends ZodLikeObject | undefined> {
@@ -51,6 +75,17 @@ interface DefineLocalToolOptions<T extends ZodLikeObject | undefined> {
51
75
  name: string;
52
76
  description?: string;
53
77
  parameters?: T;
78
+ /**
79
+ * Optional JSON Schema (or Zod schema) describing the tool's return
80
+ * value. See {@link LocalTool.outputSchema}.
81
+ */
82
+ outputSchema?: LocalToolSchemaInput;
83
+ /**
84
+ * Annotate the tool as long-running so the model doesn't double-call
85
+ * while a previous invocation is still pending. See
86
+ * {@link LocalTool.longRunning}.
87
+ */
88
+ longRunning?: boolean;
54
89
  execute: (args: T extends ZodLikeObject ? z.infer<T> : Record<string, unknown>) => Promise<string> | string;
55
90
  }
56
91
  declare function defineLocalTool<T extends ZodLikeObject | undefined>(opts: DefineLocalToolOptions<T>): LocalTool;
@@ -371,6 +406,44 @@ interface AgentSpecBase {
371
406
  * ajv schema). See `docs/wire-protocol.md` §7.
372
407
  */
373
408
  outputSchema?: OutputSchema;
409
+ /**
410
+ * Loop-detection guard. Tracks an order-invariant `(toolName, args)`
411
+ * signature for every assistant turn that emits one or more tool calls;
412
+ * when the same signature repeats consecutively the pipeline first injects
413
+ * a steering nudge ("either deliver a final answer or change strategy")
414
+ * and eventually forces a tools-disabled finalise turn.
415
+ *
416
+ * Pass an object to override the default thresholds, or `false` to
417
+ * explicitly disable the guard for this run / session. When omitted, the
418
+ * MANTYX runtime defaults apply (`{ consecutiveThreshold: 3,
419
+ * hardCutoffThreshold: 6 }`). See `docs/agent-runs-protocol.md` §4.6.
420
+ *
421
+ * Each intervention emits an observability-only `loop_detected` SSE event
422
+ * the SDK surfaces on the run-event stream (`tools` lists the looping
423
+ * batch; `hardCutoff: false` is the soft nudge round, `true` is the
424
+ * forced finalise). The synthetic skip + nudge are emitted on the normal
425
+ * `tool_result` / `assistant_delta` channels — the SDK does not need to
426
+ * act on the event itself.
427
+ */
428
+ loopDetection?: LoopDetection | false;
429
+ /**
430
+ * Per-tool call caps enforced over the **lifetime of the run** (across
431
+ * every LLM turn). Calls under the cap run normally; calls past the cap
432
+ * are intercepted before execution and returned to the model as a
433
+ * synthetic "budget exceeded — pivot or finalize" tool result.
434
+ *
435
+ * Keys are the model-facing tool names (the same string on
436
+ * `local_tool_call.name`); values are `{ maxCalls: number }`. `maxCalls:
437
+ * 0` disables the tool entirely (the first attempt returns the synthetic
438
+ * body). Budgets are **per-tool, not pooled**.
439
+ *
440
+ * Pass `{}` to start from a clean slate (no defaults applied on top —
441
+ * useful for runs that intentionally want unbounded research). Omit
442
+ * entirely to keep the runtime defaults. Each interception emits an
443
+ * observability-only `tool_budget_exceeded` SSE event. See
444
+ * `docs/agent-runs-protocol.md` §4.7.
445
+ */
446
+ toolBudgets?: ToolBudgets;
374
447
  /**
375
448
  * Flat string→string KV carried alongside the run / session for
376
449
  * observability. Use it to tag runs with your own application identifiers
@@ -409,6 +482,50 @@ interface OutputSchema {
409
482
  /** Required. JSON Schema describing the final assistant text. Root must be a JSON object. */
410
483
  schema: Record<string, unknown>;
411
484
  }
485
+ /**
486
+ * Loop-detection thresholds. See {@link AgentSpecBase.loopDetection} for the
487
+ * full semantics. Pass `false` (instead of an object) to disable the guard.
488
+ *
489
+ * Both fields are optional; omitted ones inherit the MANTYX runtime
490
+ * defaults (`consecutiveThreshold: 3`, `hardCutoffThreshold: 6`).
491
+ */
492
+ interface LoopDetection {
493
+ /**
494
+ * Number of identical consecutive tool-call batches that triggers the
495
+ * **soft nudge** — the pipeline injects a steering message ("either
496
+ * deliver a final answer or change strategy"). Default `3`. Must be
497
+ * `>= 2` (one identical batch is just a single tool call, not a loop).
498
+ * Server-side upper bound: `100`.
499
+ */
500
+ consecutiveThreshold?: number;
501
+ /**
502
+ * Number of identical consecutive tool-call batches that triggers the
503
+ * **hard cutoff** — the pipeline forces a tools-disabled finalise turn.
504
+ * Default `6`. Must be strictly greater than `consecutiveThreshold` (so
505
+ * the soft nudge has a chance to land). Server-side upper bound: `100`.
506
+ */
507
+ hardCutoffThreshold?: number;
508
+ }
509
+ /**
510
+ * Per-tool call cap. See {@link AgentSpecBase.toolBudgets} for the full
511
+ * semantics.
512
+ */
513
+ interface ToolBudget {
514
+ /**
515
+ * Hard cap on executed calls per run. `0` disables the tool entirely
516
+ * (every attempt returns the synthetic "budget exceeded" body on the
517
+ * first try). Server-side upper bound: `1000` (functionally unlimited;
518
+ * the in-runtime `maxToolTurns: 100` fires first).
519
+ */
520
+ maxCalls: number;
521
+ }
522
+ /**
523
+ * Map of model-facing tool name → cap. See
524
+ * {@link AgentSpecBase.toolBudgets}. Pass an empty object (`{}`) to start
525
+ * from a clean slate (no runtime defaults applied on top); omit the field
526
+ * entirely to keep the defaults.
527
+ */
528
+ type ToolBudgets = Record<string, ToolBudget>;
412
529
  interface RunResult {
413
530
  runId: string;
414
531
  text: string;
@@ -492,6 +609,43 @@ interface LocalToolResultInEvent extends RunEventBase {
492
609
  result?: string;
493
610
  error?: string;
494
611
  }
612
+ /**
613
+ * Observability event fired when the loop-detection guard intervenes.
614
+ * The synthetic skip + steering nudge are emitted on the normal
615
+ * `tool_result` / `assistant_delta` channels; this event lets the SDK
616
+ * render a status note (`looping — nudged` / `looping — gave up`).
617
+ *
618
+ * `hardCutoff: false` is the soft nudge round; `true` is the forced
619
+ * finalise. The same run may emit one of each.
620
+ */
621
+ interface LoopDetectedEvent extends RunEventBase {
622
+ type: "loop_detected";
623
+ /** Length of the identical-batch streak that just tripped the threshold. */
624
+ consecutiveCount: number;
625
+ /** `false` for the soft nudge round; `true` once the pipeline forces finalisation. */
626
+ hardCutoff: boolean;
627
+ /** Names of the tool calls in the looping batch (no args). */
628
+ tools: string[];
629
+ }
630
+ /**
631
+ * Observability event fired when a tool-budget interception happens. The
632
+ * synthetic "budget exceeded — pivot or finalize" tool result lands on the
633
+ * normal `tool_result` channel before this event fires; the SDK uses this
634
+ * event to render UI banners (`memory budget exhausted` etc.) without
635
+ * re-parsing tool-result bodies.
636
+ */
637
+ interface ToolBudgetExceededEvent extends RunEventBase {
638
+ type: "tool_budget_exceeded";
639
+ /** Logical tool name (matches the key in `spec.toolBudgets`). */
640
+ tool: string;
641
+ /** Configured cap. */
642
+ maxCalls: number;
643
+ /**
644
+ * 1-based count of attempts to call this tool over the run lifetime.
645
+ * Always strictly greater than `maxCalls`.
646
+ */
647
+ callIndex: number;
648
+ }
495
649
  interface ResultEvent extends RunEventBase {
496
650
  type: "result";
497
651
  subtype: string;
@@ -507,7 +661,7 @@ interface CancelledEvent extends RunEventBase {
507
661
  type: "cancelled";
508
662
  reason?: string;
509
663
  }
510
- type RunEvent = AssistantDeltaEvent | ThinkingDeltaEvent | AssistantMessageEvent | ServerToolResultEvent | LocalToolCallEvent | LocalToolResultInEvent | ResultEvent | ErrorEvent | CancelledEvent | (RunEventBase & {
664
+ type RunEvent = AssistantDeltaEvent | ThinkingDeltaEvent | AssistantMessageEvent | ServerToolResultEvent | LocalToolCallEvent | LocalToolResultInEvent | LoopDetectedEvent | ToolBudgetExceededEvent | ResultEvent | ErrorEvent | CancelledEvent | (RunEventBase & {
511
665
  type: string;
512
666
  [key: string]: unknown;
513
667
  });
@@ -599,12 +753,25 @@ declare class AgentSession {
599
753
  * and does not mutate the session's stored value.
600
754
  */
601
755
  outputSchema?: OutputSchema;
756
+ /**
757
+ * Per-message override for `loopDetection`. Applies only to this run
758
+ * and does not mutate the session's stored value. Pass `false` to
759
+ * disable the guard for this single turn.
760
+ */
761
+ loopDetection?: LoopDetection | false;
762
+ /**
763
+ * Per-message override for `toolBudgets`. Applies only to this run
764
+ * and does not mutate the session's stored value.
765
+ */
766
+ toolBudgets?: ToolBudgets;
602
767
  }): Promise<RunResult>;
603
768
  stream(prompt: string, opts?: {
604
769
  signal?: AbortSignal;
605
770
  metadata?: Record<string, string>;
606
771
  reasoningLevel?: ReasoningLevel;
607
772
  outputSchema?: OutputSchema;
773
+ loopDetection?: LoopDetection | false;
774
+ toolBudgets?: ToolBudgets;
608
775
  }): AsyncGenerator<RunEvent, void, void>;
609
776
  private buildSessionMessageBody;
610
777
  history(): Promise<Array<{
@@ -655,4 +822,4 @@ interface LocalHandlers {
655
822
  */
656
823
  declare function parseRunOutput<T = unknown>(result: RunResult, validator?: (value: unknown) => T): T;
657
824
 
658
- export { type A2AToolRef as A, type RunSpec as B, type CancelledEvent as C, DEFAULT_BASE_URL as D, type ErrorEvent as E, type SessionInfo as F, type SessionSpec as G, type ThinkingDeltaEvent as H, defineLocalA2A as I, defineLocalMcp as J, defineLocalTool as K, type LocalA2ATool as L, MantyxClient as M, isLocalA2ATool as N, type OutputSchema as O, isLocalMcpServer as P, isLocalTool as Q, type ReasoningLevel as R, type ServerToolResultEvent as S, type ToolRef as T, mantyxA2A as U, mantyxMcp as V, mantyxPluginTool as W, mantyxTool as X, parseRunOutput as Y, type ZodLikeObject as Z, AgentSession as a, type AgentSpecBase as b, type AssistantDeltaEvent as c, type AssistantMessageEvent as d, type DefineLocalA2AOptions as e, type DefineLocalMcpOptions as f, type DefineLocalToolOptions as g, type LocalHandlers as h, type LocalMcpHttpTransport as i, type LocalMcpServer as j, type LocalMcpStdioTransport as k, type LocalTool as l, type LocalToolCallEvent as m, type LocalToolResultInEvent as n, type MantyxA2AOptions as o, type MantyxClientOptions as p, type MantyxMcpOptions as q, type MantyxPluginToolRef as r, type MantyxToolRef as s, type McpToolRef as t, type ModelCatalog as u, type ModelInfo as v, type ResultEvent as w, type RunEvent as x, type RunEventBase as y, type RunResult as z };
825
+ export { mantyxMcp as $, type A2AToolRef as A, type RunEventBase as B, type CancelledEvent as C, DEFAULT_BASE_URL as D, type ErrorEvent as E, type RunResult as F, type RunSpec as G, type SessionInfo as H, type SessionSpec as I, type ThinkingDeltaEvent as J, type ToolBudget as K, type LocalA2ATool as L, MantyxClient as M, type ToolBudgetExceededEvent as N, type OutputSchema as O, type ToolBudgets as P, defineLocalA2A as Q, type ReasoningLevel as R, type ServerToolResultEvent as S, type ToolRef as T, defineLocalMcp as U, defineLocalTool as V, isLocalA2ATool as W, isLocalMcpServer as X, isLocalTool as Y, type ZodLikeObject as Z, mantyxA2A as _, AgentSession as a, mantyxPluginTool as a0, mantyxTool as a1, parseRunOutput as a2, type AgentSpecBase as b, type AssistantDeltaEvent as c, type AssistantMessageEvent as d, type DefineLocalA2AOptions as e, type DefineLocalMcpOptions as f, type DefineLocalToolOptions as g, type LocalHandlers as h, type LocalMcpHttpTransport as i, type LocalMcpServer as j, type LocalMcpStdioTransport as k, type LocalTool as l, type LocalToolCallEvent as m, type LocalToolResultInEvent as n, type LoopDetectedEvent as o, type LoopDetection as p, type MantyxA2AOptions as q, type MantyxClientOptions as r, type MantyxMcpOptions as s, type MantyxPluginToolRef as t, type MantyxToolRef as u, type McpToolRef as v, type ModelCatalog as w, type ModelInfo as x, type ResultEvent as y, type RunEvent as z };
package/dist/index.cjs CHANGED
@@ -125,6 +125,8 @@ function defineLocalTool(opts) {
125
125
  name: opts.name,
126
126
  description: opts.description ?? "",
127
127
  parameters: opts.parameters,
128
+ outputSchema: opts.outputSchema,
129
+ longRunning: opts.longRunning ?? false,
128
130
  execute: opts.execute
129
131
  };
130
132
  }
@@ -976,6 +978,12 @@ var AgentSession = class {
976
978
  if (opts.outputSchema !== void 0) {
977
979
  body.outputSchema = normalizeOutputSchema(opts.outputSchema);
978
980
  }
981
+ if (opts.loopDetection !== void 0) {
982
+ body.loopDetection = normalizeLoopDetection(opts.loopDetection);
983
+ }
984
+ if (opts.toolBudgets !== void 0) {
985
+ body.toolBudgets = normalizeToolBudgets(opts.toolBudgets);
986
+ }
979
987
  return body;
980
988
  }
981
989
  async history() {
@@ -1010,6 +1018,12 @@ function serializeAgentSpec(spec, extra = {}) {
1010
1018
  if (spec.outputSchema !== void 0) {
1011
1019
  body.outputSchema = normalizeOutputSchema(spec.outputSchema);
1012
1020
  }
1021
+ if (spec.loopDetection !== void 0) {
1022
+ body.loopDetection = normalizeLoopDetection(spec.loopDetection);
1023
+ }
1024
+ if (spec.toolBudgets !== void 0) {
1025
+ body.toolBudgets = normalizeToolBudgets(spec.toolBudgets);
1026
+ }
1013
1027
  if (spec.budgets) body.budgets = spec.budgets;
1014
1028
  if (spec.metadata && Object.keys(spec.metadata).length > 0) body.metadata = spec.metadata;
1015
1029
  if (extra.prompt !== void 0) body.prompt = extra.prompt;
@@ -1028,7 +1042,9 @@ function serializeToolRefs(tools) {
1028
1042
  kind: "local",
1029
1043
  name: t.name,
1030
1044
  description: t.description,
1031
- parameters: toToolParametersWire(t.parameters)
1045
+ parameters: toToolParametersWire(t.parameters),
1046
+ ...t.outputSchema !== void 0 ? { outputSchema: toToolParametersWire(t.outputSchema) } : {},
1047
+ ...t.longRunning ? { longRunning: true } : {}
1032
1048
  };
1033
1049
  case "a2a":
1034
1050
  return {
@@ -1165,6 +1181,93 @@ function normalizeOutputSchema(value) {
1165
1181
  }
1166
1182
  return out;
1167
1183
  }
1184
+ var LOOP_DETECTION_THRESHOLD_MAX = 100;
1185
+ function normalizeLoopDetection(value) {
1186
+ if (value === false) return false;
1187
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1188
+ throw new MantyxError(
1189
+ `loopDetection must be an object or the literal \`false\`, got ${JSON.stringify(value)}`
1190
+ );
1191
+ }
1192
+ const out = {};
1193
+ if (value.consecutiveThreshold !== void 0) {
1194
+ out.consecutiveThreshold = assertThreshold(
1195
+ "loopDetection.consecutiveThreshold",
1196
+ value.consecutiveThreshold,
1197
+ 2
1198
+ );
1199
+ }
1200
+ if (value.hardCutoffThreshold !== void 0) {
1201
+ out.hardCutoffThreshold = assertThreshold(
1202
+ "loopDetection.hardCutoffThreshold",
1203
+ value.hardCutoffThreshold,
1204
+ 3
1205
+ );
1206
+ }
1207
+ if (typeof out.consecutiveThreshold === "number" && typeof out.hardCutoffThreshold === "number" && out.hardCutoffThreshold <= out.consecutiveThreshold) {
1208
+ throw new MantyxError(
1209
+ `loopDetection.hardCutoffThreshold (${out.hardCutoffThreshold}) must be strictly greater than loopDetection.consecutiveThreshold (${out.consecutiveThreshold})`
1210
+ );
1211
+ }
1212
+ return out;
1213
+ }
1214
+ function assertThreshold(label, value, min) {
1215
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
1216
+ throw new MantyxError(`${label} must be an integer, got ${JSON.stringify(value)}`);
1217
+ }
1218
+ if (value < min) {
1219
+ throw new MantyxError(`${label} must be >= ${min}, got ${value}`);
1220
+ }
1221
+ if (value > LOOP_DETECTION_THRESHOLD_MAX) {
1222
+ throw new MantyxError(
1223
+ `${label} must be <= ${LOOP_DETECTION_THRESHOLD_MAX} (server-enforced), got ${value}`
1224
+ );
1225
+ }
1226
+ return value;
1227
+ }
1228
+ var TOOL_BUDGETS_MAX_ENTRIES = 32;
1229
+ var TOOL_BUDGET_MAX_NAME_LEN = 120;
1230
+ var TOOL_BUDGET_MAX_CALLS = 1e3;
1231
+ function normalizeToolBudgets(value) {
1232
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1233
+ throw new MantyxError(
1234
+ `toolBudgets must be an object of shape { [name]: { maxCalls } }, got ${JSON.stringify(value)}`
1235
+ );
1236
+ }
1237
+ const keys = Object.keys(value);
1238
+ if (keys.length > TOOL_BUDGETS_MAX_ENTRIES) {
1239
+ throw new MantyxError(
1240
+ `toolBudgets has ${keys.length} entries; the server enforces a ${TOOL_BUDGETS_MAX_ENTRIES}-entry limit`
1241
+ );
1242
+ }
1243
+ const out = {};
1244
+ for (const key of keys) {
1245
+ if (typeof key !== "string" || key.length < 1 || key.length > TOOL_BUDGET_MAX_NAME_LEN) {
1246
+ throw new MantyxError(
1247
+ `toolBudgets keys must be 1..${TOOL_BUDGET_MAX_NAME_LEN}-char strings, got ${JSON.stringify(key)}`
1248
+ );
1249
+ }
1250
+ const entry = value[key];
1251
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1252
+ throw new MantyxError(
1253
+ `toolBudgets[${JSON.stringify(key)}] must be an object { maxCalls }, got ${JSON.stringify(entry)}`
1254
+ );
1255
+ }
1256
+ const maxCalls = entry.maxCalls;
1257
+ if (typeof maxCalls !== "number" || !Number.isFinite(maxCalls) || !Number.isInteger(maxCalls) || maxCalls < 0) {
1258
+ throw new MantyxError(
1259
+ `toolBudgets[${JSON.stringify(key)}].maxCalls must be a non-negative integer, got ${JSON.stringify(maxCalls)}`
1260
+ );
1261
+ }
1262
+ if (maxCalls > TOOL_BUDGET_MAX_CALLS) {
1263
+ throw new MantyxError(
1264
+ `toolBudgets[${JSON.stringify(key)}].maxCalls must be <= ${TOOL_BUDGET_MAX_CALLS} (server-enforced), got ${maxCalls}`
1265
+ );
1266
+ }
1267
+ out[key] = { maxCalls };
1268
+ }
1269
+ return out;
1270
+ }
1168
1271
  function parseRunOutput(result, validator) {
1169
1272
  let parsed;
1170
1273
  try {
@@ -1194,7 +1297,7 @@ function sleep(ms) {
1194
1297
  }
1195
1298
 
1196
1299
  // src/version.ts
1197
- var SDK_VERSION = "0.7.0";
1300
+ var SDK_VERSION = "0.9.0";
1198
1301
  // Annotate the CommonJS export names for ESM import in node:
1199
1302
  0 && (module.exports = {
1200
1303
  AgentSession,