@sema-agent/core 5.52.0 → 5.53.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/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.53.0 — 2026-08-21
4
+
5
+ ### Fixed
6
+ - **An MCP refresh no longer rewrites the cacheable tool prefix** (#375 root fix): `RefreshMcpTools`
7
+ re-inserted the server's tool domain at the TAIL of the mount array — the wire order — so a
8
+ byte-identical listing still displaced the whole domain past every later tool, a full provider
9
+ prefix-cache re-bill the model itself was told to trigger ("refresh when a listing looks stale").
10
+ The domain now re-enters at its anchor (the lowest index it occupied): an unchanged listing
11
+ produces a byte-identical array. First-time mounts still append.
12
+ - **Thinking replay survives strict gateways and non-streaming rewrites** (#374 adjacent):
13
+ the replay judge is shape-based (only a non-empty string counts as a signature — a JSON-roundtrip
14
+ `null`/number can no longer ride onto the wire), `signature_delta` refuses non-string coercion,
15
+ and an inlined `content_block_start` thinking block (the compat-gateway SSE-rewrite form) is now
16
+ fully captured — text and signature — instead of being dropped from every subsequent request.
17
+ - **The cache-break detector tells the truth about reorders** (#376): an order-preserving hash
18
+ now attributes a pure reorder to `tool-set` with its own wording (previously it fell through to
19
+ "server-or-ttl … not a client-side prefix bug" — actively misleading, and the exact shape #375
20
+ just fixed); the low-hit-rate summary discriminates "prefix keeps changing" from "this route
21
+ reports no caching" using cacheWrite facts instead of guessing.
22
+ - **The MCP refresh region is transactional and collision-gated** (#377): tool effect records are
23
+ reset before the fold (a refreshed read-only tool no longer silently degrades to write), a
24
+ rejected listing restores both non-monotone tables, two servers whose names normalize to the
25
+ same prefix refuse at connect (`config.mcp_server_name_collision`, both original spellings
26
+ named), and a single listing minting two identical tool names keeps the first with a disclosed
27
+ drop.
28
+ - Behavior narrowings (named): colliding server configs from "silent shared domain" to a loud
29
+ connect refusal; same-listing name collisions from "both mounted under one name" to
30
+ "first kept, drop disclosed".
31
+
32
+ - **Pre-release scan dispositions (three, fixed in-tree before publish)**: the protocol-name
33
+ reservation now covers ALIASES (a `__`-shaped caller alias refuses at prepare with the same loud
34
+ code — it was a dispatchable name sitting inside a server's prefix domain, which the refresh
35
+ region would classify and clear as remote); the refresh receipt states each drop's OWN reason
36
+ (a fixed "invalid schema" label lied about the collision lane); the A2A mount gains the same
37
+ normalized-prefix collision gate as MCP (`config.a2a_peer_name_collision`), making the
38
+ protocol-naming contract's engine-wide claim true.
39
+ - Behavior narrowing (named): a caller tool alias containing `__` from "accepted" to a loud
40
+ prepare refusal; colliding A2A peer names from "silent shared domain" to a connect refusal.
41
+
42
+ ### Notes
43
+ - Residuals ticketed: #379 (open-responses presence judge, inline text-block sibling, two-phase
44
+ refresh observation). #378 resolved by the alias-reservation fix above.
45
+
3
46
  ## 5.52.0 — 2026-08-21
4
47
 
5
48
  ### Added
@@ -66,7 +66,8 @@ function toAnthropicMessages(ctx, model) {
66
66
  for (const c of m.content) {
67
67
  if (c.type === "thinking") {
68
68
  const tc = c;
69
- const sig = tc.thinkingSignature;
69
+ const sigRaw = tc.thinkingSignature;
70
+ const sig = typeof sigRaw === "string" && sigRaw.length > 0 ? sigRaw : undefined;
70
71
  if (tc.redacted) {
71
72
  if (sig)
72
73
  blocks.push({ type: "redacted_thinking", data: sig });
@@ -377,9 +378,19 @@ export function createAnthropicBrain(config = {}) {
377
378
  acc.redacted = true;
378
379
  acc.signature = cb.data ?? "";
379
380
  }
381
+ else {
382
+ if (typeof cb.thinking === "string" && cb.thinking !== "")
383
+ acc.text = cb.thinking;
384
+ if (typeof cb.signature === "string" && cb.signature !== "")
385
+ acc.startSignature = cb.signature;
386
+ }
380
387
  acc.pb = { type: "thinking", thinking: "" };
381
388
  partial.content = [acc.pb];
382
389
  out.push({ type: "thinking_start", contentIndex: idx, partial: { ...partial } });
390
+ if (acc.text !== "") {
391
+ acc.pb.thinking = acc.text;
392
+ out.push({ type: "thinking_delta", contentIndex: idx, delta: acc.text, partial: { ...partial } });
393
+ }
383
394
  }
384
395
  else if (cb.type === "tool_use") {
385
396
  acc.type = "tool_use";
@@ -418,6 +429,7 @@ export function createAnthropicBrain(config = {}) {
418
429
  }
419
430
  }
420
431
  else if (d.type === "thinking_delta" && d.thinking) {
432
+ acc.startSignature = undefined;
421
433
  acc.text += d.thinking;
422
434
  if (acc.pb && acc.pb.type === "thinking")
423
435
  acc.pb.thinking = acc.text;
@@ -430,7 +442,7 @@ export function createAnthropicBrain(config = {}) {
430
442
  }
431
443
  }
432
444
  }
433
- else if (d.type === "signature_delta" && d.signature) {
445
+ else if (d.type === "signature_delta" && typeof d.signature === "string" && d.signature.length > 0) {
434
446
  acc.signature += d.signature;
435
447
  }
436
448
  else if (d.type === "input_json_delta" && d.partial_json) {
@@ -536,8 +548,9 @@ export function createAnthropicBrain(config = {}) {
536
548
  if (acc.text.length > 0 || acc.redacted === true)
537
549
  reasoningSeen = true;
538
550
  const block = { type: "thinking", thinking: acc.text };
539
- if (acc.signature)
540
- block.thinkingSignature = acc.signature;
551
+ const sig = acc.signature || acc.startSignature;
552
+ if (sig)
553
+ block.thinkingSignature = sig;
541
554
  if (acc.redacted)
542
555
  block.redacted = true;
543
556
  finalContent.push(block);
package/dist/core/a2a.js CHANGED
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { Type } from "typebox";
3
3
  import { A2A_TASK_STATES } from "./a2a-task-state.js";
4
4
  import { describeHttpTransportFailure, resolveProtocolHttpHeaders } from "./mcp.js";
5
- import { mintNamespacePrefix, mintNamespacedToolName } from "./protocol-naming.js";
5
+ import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName } from "./protocol-naming.js";
6
6
  import { A2A_NAMESPACE } from "./protocol-table.js";
7
7
  import { delimitUntrusted, inlineUntrusted } from "./untrusted-text.js";
8
8
  const A2A_REQUEST_TIMEOUT_MS = 30_000;
@@ -575,6 +575,17 @@ function asPeerWarning(spec, err) {
575
575
  return warning;
576
576
  }
577
577
  export async function materializeA2aTools(specs, principal, signal) {
578
+ if (specs.length > 0) {
579
+ const collision = findNamespacePrefixCollision(A2A_NAMESPACE, specs.map((s) => s.name));
580
+ if (collision) {
581
+ const [a, b] = collision.peers;
582
+ const e = new Error(a === b
583
+ ? `A2A peer "${a}" is declared twice — each peer needs its own name (both mount under "${collision.prefix}").`
584
+ : `A2A peer names "${a}" and "${b}" both mount under "${collision.prefix}" — the namespaced tool name keeps only [a-zA-Z0-9_-], so they are the same domain to this engine. Rename one.`);
585
+ e.code = "config.a2a_peer_name_collision";
586
+ throw e;
587
+ }
588
+ }
578
589
  const lifecycle = { disposed: false };
579
590
  const tools = [];
580
591
  const toolAxes = [];
@@ -27,6 +27,7 @@ export class CacheBreakDetector {
27
27
  systemHash: fnv1a(input.systemPrompt),
28
28
  perToolHash: new Map(input.tools.map((t) => [t.name, fnv1a(`${t.name}\0${t.description}\0${stableStringify(t.parameters)}`)])),
29
29
  toolSetHash: fnv1a([...input.tools.map((t) => t.name)].sort().join(",")),
30
+ toolOrderHash: fnv1a(input.tools.map((t) => t.name).join(",")),
30
31
  modelKey: input.modelKey,
31
32
  cacheRead: input.cacheRead,
32
33
  at: Date.now(),
@@ -59,7 +60,11 @@ export class CacheBreakDetector {
59
60
  }
60
61
  else if (snap.toolSetHash !== prev.toolSetHash) {
61
62
  cause = "tool-set";
62
- detail = "the tool set changed (a tool was added/removed/reordered)";
63
+ detail = "the tool set changed (a tool was added or removed)";
64
+ }
65
+ else if (snap.toolOrderHash !== prev.toolOrderHash) {
66
+ cause = "tool-set";
67
+ detail = "the tool ORDER changed (same tools, different positions) — a reordered tool list is a byte-different prefix from the first moved tool onward, so the provider re-bills the whole tail";
63
68
  }
64
69
  else if (snap.systemHash !== prev.systemHash) {
65
70
  cause = "system-prefix";
@@ -68,10 +73,11 @@ export class CacheBreakDetector {
68
73
  else {
69
74
  const gapMs = snap.at - prev.at;
70
75
  cause = "server-or-ttl";
76
+ const checked = "nothing this detector fingerprints (system prefix, tool set + order, model)";
71
77
  detail =
72
78
  gapMs < SERVER_GAP_MS
73
- ? `nothing in the prefix changed and the gap was ${Math.round(gapMs / 1000)}s — likely a server-side miss, not a client-side prefix bug`
74
- : `nothing changed but the gap was ${Math.round(gapMs / 60_000)}min — likely a normal TTL expiry (cold cache), not a bug`;
79
+ ? `${checked} changed and the gap was ${Math.round(gapMs / 1000)}s — most likely a server-side miss; the message history is not fingerprinted, so volatile content early in the conversation would also land here`
80
+ : `${checked} changed and the gap was ${Math.round(gapMs / 60_000)}min — most likely a normal TTL expiry (cold cache)`;
75
81
  }
76
82
  }
77
83
  return { turn: input.turn, cacheReadBefore: before, cacheReadAfter: after, cause, detail };
package/dist/core/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MCP_NAMESPACE } from "./protocol-table.js";
2
- import { mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
2
+ import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
3
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
4
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
5
  import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -663,6 +663,17 @@ export function mcpToolSchemaProblem(schema) {
663
663
  return undefined;
664
664
  }
665
665
  export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure, mcpRevocations) {
666
+ {
667
+ const collision = findNamespacePrefixCollision(MCP_NAMESPACE, specs.map((s) => s.name));
668
+ if (collision) {
669
+ const [a, b] = collision.peers;
670
+ const e = new Error(a === b
671
+ ? `MCP server "${inlineUntrusted(a, 120)}" is declared twice — each server needs its own name (both mount under "${collision.prefix}", so their tools would shadow each other and a refresh of one would unmount the other's).`
672
+ : `MCP server names "${inlineUntrusted(a, 120)}" and "${inlineUntrusted(b, 120)}" both mount under "${collision.prefix}" — the namespaced tool name keeps only [a-zA-Z0-9_-], so they are the same domain to this engine (their tools would shadow each other and a refresh of one would unmount the other's). Rename one.`);
673
+ e.code = "config.mcp_server_name_collision";
674
+ throw e;
675
+ }
676
+ }
666
677
  let revocationProbeFailed = false;
667
678
  const isServerRevoked = (serverName) => {
668
679
  if (mcpRevocations === undefined)
@@ -1256,6 +1267,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1256
1267
  const serverTools = [];
1257
1268
  const serverAxes = [];
1258
1269
  const dropped = [];
1270
+ const mintedNames = new Set();
1259
1271
  for (const t of listed.tools) {
1260
1272
  if (spec.allowTools && !spec.allowTools.includes(t.name)) {
1261
1273
  continue;
@@ -1278,6 +1290,14 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1278
1290
  }
1279
1291
  const remoteName = t.name;
1280
1292
  const namespacedName = mintNamespacedToolName(MCP_NAMESPACE, spec.name, remoteName);
1293
+ if (mintedNames.has(namespacedName)) {
1294
+ dropped.push({
1295
+ tool: inlineUntrusted(t.name),
1296
+ reason: `name collides with an earlier tool of this server: both mount as "${namespacedName}" after namespacing (only [a-zA-Z0-9_-] survives), and one name cannot denote two tools`,
1297
+ });
1298
+ continue;
1299
+ }
1300
+ mintedNames.add(namespacedName);
1281
1301
  const hintAxis = mcpAxisFor(namespacedName, t.annotations);
1282
1302
  const axis = applyCallerAxisOverride(namespacedName, hintAxis, spec.toolAxes?.[remoteName]);
1283
1303
  if (axis)
@@ -12,8 +12,11 @@ export declare const MINTED_TOOL_SEGMENT_MIN_CHARS = 16;
12
12
  * `mcp__<server>__<tool>` (services/mcp/normalization.ts `normalizeNameForMCP` + mcpStringUtils.ts
13
13
  * `buildMcpToolName`), so a peer advertising a dotted/spaced/unicode name can't mint a name the provider
14
14
  * rejects. NOTE: only the MODEL-FACING namespaced name is normalized — the raw remote name still goes on
15
- * the wire and still keys the caller-facing maps (`allowTools`/`toolAxes`). Same known collision property
16
- * as CC: two remote names that normalize to the same string collide (rare; CC accepts this).
15
+ * the wire and still keys the caller-facing maps (`allowTools`/`toolAxes`). The collision property is
16
+ * inherent (two remote names that normalize to the same string mint the same string; CC accepts this at
17
+ * the same layer) — what this engine does NOT accept is two PEERS colliding, because a peer prefix is a
18
+ * live domain key here (the refresh splice). That case is decided by
19
+ * {@link findNamespacePrefixCollision} and refused at the mount, loudly; the mint stays total.
17
20
  *
18
21
  * RB-83 (2026-07-25, red probe): the pattern this enforces is `{1,64}`, and only the charset half was
19
22
  * enforced. Both other halves matter for the same reason the charset does — an over-long or empty segment
@@ -37,6 +40,26 @@ export declare function clampNameSegment(seg: string, max?: number): string;
37
40
  * (ticket #10), and the property test holds the two legs to the identical law.
38
41
  */
39
42
  export declare function mintNamespacePrefix(ns: ProtocolNamespace, peer: string): string;
43
+ /**
44
+ * The first pair of peers in `peers` that mint the SAME {@link mintNamespacePrefix} — i.e. that would
45
+ * register into one indistinguishable namespace domain — or `undefined` when every peer owns its own.
46
+ *
47
+ * Two spellings collide whenever charset normalization, the length clamp or the separator fold maps
48
+ * them together: `"prod.db"` and `"prod_db"` both mint `mcp__prod_db__`, and the same name listed twice
49
+ * collides trivially. The mint itself cannot refuse (its input is deployment/remote data and a dotted
50
+ * name must not become a materialization failure — see {@link normalizeNameSegment}), so the refusal
51
+ * belongs to the MOUNT, which is the layer that knows the whole peer list. This function is that
52
+ * layer's decision procedure: pure name arithmetic, decidable before any I/O.
53
+ *
54
+ * Why a collision cannot be tolerated downstream: the prefix IS the domain key. A refresh splices
55
+ * `name.startsWith(prefix)` out and re-inserts only the refreshed peer's listing, so refreshing one of
56
+ * two colliding peers silently unmounts the other's tools; equal tool names additionally shadow each
57
+ * other last-write-wins in the harness map. Both failures are invisible at the moment they happen.
58
+ */
59
+ export declare function findNamespacePrefixCollision(ns: ProtocolNamespace, peers: readonly string[]): {
60
+ prefix: string;
61
+ peers: [string, string];
62
+ } | undefined;
40
63
  /**
41
64
  * The full registered name for `(peer, tool)` in `ns`. Always starts with {@link mintNamespacePrefix}'s
42
65
  * answer for the same peer (the invariant ticket #9's pins hold), and never exceeds
@@ -26,6 +26,17 @@ export function mintNamespacePrefix(ns, peer) {
26
26
  const peerBudget = Math.max(1, TOOL_NAME_MAX_CHARS - ns.prefix.length - NAME_SEP.length - MINTED_TOOL_SEGMENT_MIN_CHARS);
27
27
  return `${ns.prefix}${settlePeerSegment(clampNameSegment(normalizeNameSegment(peer), peerBudget))}${NAME_SEP}`;
28
28
  }
29
+ export function findNamespacePrefixCollision(ns, peers) {
30
+ const seen = new Map();
31
+ for (const peer of peers) {
32
+ const prefix = mintNamespacePrefix(ns, peer);
33
+ const first = seen.get(prefix);
34
+ if (first !== undefined)
35
+ return { prefix, peers: [first, peer] };
36
+ seen.set(prefix, peer);
37
+ }
38
+ return undefined;
39
+ }
29
40
  export function mintNamespacedToolName(ns, peer, tool) {
30
41
  const prefix = mintNamespacePrefix(ns, peer);
31
42
  return `${prefix}${clampNameSegment(normalizeNameSegment(tool), Math.max(1, TOOL_NAME_MAX_CHARS - prefix.length))}`;
@@ -19,6 +19,13 @@ export function prepareSafetyScan(input) {
19
19
  e.code = "config.tool_name_invalid";
20
20
  throw e;
21
21
  }
22
+ for (const alias of t.aliases ?? []) {
23
+ if (alias.includes("__")) {
24
+ const e = new Error(`Tool alias "${alias}" (of "${t.name}") is invalid: "__" is reserved for the engine's protocol tool namespaces (${NAMESPACED_NAME_SHAPES}) and must not appear in a caller tool alias.`);
25
+ e.code = "config.tool_name_invalid";
26
+ throw e;
27
+ }
28
+ }
22
29
  if (t.effect) {
23
30
  toolEffects.set(t.name, t.effect);
24
31
  }
@@ -1221,22 +1221,43 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1221
1221
  const excludedSet = new Set(toolFaceSnapshot.exclude ?? []);
1222
1222
  const pushable = r.tools.filter((t) => !excludedSet.has(t.name));
1223
1223
  const excludedNow = r.tools.filter((t) => excludedSet.has(t.name)).map((t) => t.name);
1224
+ const domainSnapshot = (m) => new Map([...m].filter(([name]) => name.startsWith(r.prefix)));
1225
+ const restoreDomain = (m, snap) => {
1226
+ for (const name of [...m.keys()])
1227
+ if (name.startsWith(r.prefix))
1228
+ m.delete(name);
1229
+ for (const [name, v] of snap)
1230
+ m.set(name, v);
1231
+ };
1232
+ const priorDomainEffects = domainSnapshot(toolEffects);
1233
+ const priorDomainNegatives = domainSnapshot(axisExplicitNegatives);
1234
+ for (const name of priorDomainEffects.keys())
1235
+ toolEffects.delete(name);
1236
+ for (const name of priorDomainNegatives.keys())
1237
+ axisExplicitNegatives.delete(name);
1224
1238
  try {
1225
1239
  foldProtocolAxes((r.axes ?? []).filter((a) => !excludedSet.has(a.name)), "MCP");
1226
1240
  }
1227
1241
  catch (foldErr) {
1242
+ restoreDomain(toolEffects, priorDomainEffects);
1243
+ restoreDomain(axisExplicitNegatives, priorDomainNegatives);
1228
1244
  lines.push(`${r.server}: failed (${foldErr instanceof Error ? foldErr.message : String(foldErr)})`);
1229
1245
  anyActiveFailure = true;
1230
1246
  continue;
1231
1247
  }
1248
+ let domainAnchor = -1;
1232
1249
  for (let i = tools.length - 1; i >= 0; i--) {
1233
1250
  const t = tools[i];
1234
1251
  if (t.name.startsWith(r.prefix)) {
1235
- toolEffects.delete(t.name);
1252
+ domainAnchor = i;
1236
1253
  tools.splice(i, 1);
1237
1254
  }
1238
1255
  }
1239
- tools.push(...pushable.map((t) => remoteToolOffload(t)));
1256
+ const refreshedMounts = pushable.map((t) => remoteToolOffload(t));
1257
+ if (domainAnchor >= 0)
1258
+ tools.splice(domainAnchor, 0, ...refreshedMounts);
1259
+ else
1260
+ tools.push(...refreshedMounts);
1240
1261
  changed = true;
1241
1262
  const detail = [];
1242
1263
  const shownAdded = r.added.filter((n) => !excludedSet.has(n));
@@ -1247,7 +1268,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1247
1268
  if (excludedNow.length > 0)
1248
1269
  detail.push(`excluded by deployment config (not mounted): ${excludedNow.join(", ")}`);
1249
1270
  if ((r.dropped?.length ?? 0) > 0)
1250
- detail.push(`dropped (invalid schema): ${r.dropped.map((d) => d.tool).join(", ")}`);
1271
+ detail.push(`dropped: ${r.dropped.map((d) => `${d.tool} (${d.reason.length > 90 ? `${d.reason.slice(0, 90)}…` : d.reason})`).join("; ")}`);
1251
1272
  lines.push(`${r.server}: refreshed — ${pushable.length} tool${pushable.length === 1 ? "" : "s"}${detail.length > 0 ? ` (${detail.join("; ")})` : ""}`);
1252
1273
  }
1253
1274
  if (changed)
@@ -3473,9 +3473,12 @@ export class Runner {
3473
3473
  }
3474
3474
  stats.cacheHitRate = Math.min(1, rawHit);
3475
3475
  if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.totalInputTokens >= 8000 && stats.cacheHitRate < 0.15) {
3476
+ const cacheWritten = stats.cacheWriteTokens + stats.cacheWriteTokensLong;
3476
3477
  const cause = rs.degrade.degraded
3477
3478
  ? `a mid-task model switch (${rs.degrade.degraded.from} → ${rs.degrade.degraded.to}, degradation) reset the prefix cache — this is the likely cause`
3478
- : `the cacheable prefix looks unstable. Keep volatile content (memory/timestamps/ids) out of the prompt prefix and tool order stable, or it's a server-side/TTL miss`;
3479
+ : cacheWritten > 0
3480
+ ? `this task reported ${cacheWritten} cache-write tokens across its calls (compaction included) but served almost none back as reads — consistent with a prompt prefix that CHANGES between turns (client-side: volatile content up front, or per-turn tool churn/reorder) and, less often, with server-side eviction. Keep volatile content (memory/timestamps/ids) out of the prefix and the tool list stable in membership AND order`
3481
+ : `no call in this task reported any cache-write tokens — and this API family may not report them at all (an openai-shaped usage row carries cached READS only), so the write side is no evidence here; check that the prompt prefix (system prompt + tool list, membership AND order) is byte-stable across turns and that this route caches this model`;
3479
3482
  this.deps.onError?.(new Error(`prompt-cache: low prefix-cache hit rate ${(stats.cacheHitRate * 100).toFixed(0)}% over ${stats.turns} turns (${stats.totalInputTokens} prompt tokens) — ${cause}. See design/09.`), { phase: "prompt-cache", sessionId: prepared.sessionId });
3480
3483
  }
3481
3484
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.52.0",
3
+ "version": "5.53.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",