@rulvar/core 1.59.2 → 1.59.3

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.d.ts CHANGED
@@ -4094,8 +4094,14 @@ interface ToolRuntime {
4094
4094
  * toolset holds any non-inprocess tool; the ctx layer mints the tool
4095
4095
  * span and idempotency key and wires the provider. A throw becomes the
4096
4096
  * call's error tool result exactly like an inprocess execute throw.
4097
- */
4098
- executeExternal?: (def: ToolDef, args: Json) => Promise<unknown>;
4097
+ *
4098
+ * `ordinal` is the call's 1-based position in this agent invocation's
4099
+ * tool loop (checkpoint-stable across suspension and crash resume); the
4100
+ * ctx layer folds it with the agent entry's seq into the idempotency
4101
+ * key, so two separate calls with identical arguments do not collide
4102
+ * while an at-least-once retry of one call keeps its key (P0.4).
4103
+ */
4104
+ executeExternal?: (def: ToolDef, args: Json, ordinal: number) => Promise<unknown>;
4099
4105
  }
4100
4106
  /** One serving target of a phase: the primary or a failover fallback. */
4101
4107
  interface PhaseTarget {
package/dist/index.js CHANGED
@@ -10402,7 +10402,7 @@ async function executeToolCall(options) {
10402
10402
  try {
10403
10403
  let value;
10404
10404
  if (def.executor === "inprocess") value = await def.execute(validation.value, runtime.contextFor(call.name));
10405
- else if (runtime.executeExternal !== void 0) value = await runtime.executeExternal(def, validation.value);
10405
+ else if (runtime.executeExternal !== void 0) value = await runtime.executeExternal(def, validation.value, options.ordinal);
10406
10406
  else return finish({ error: `tool '${call.name}' declares executor '${def.executor}' but no executor is registered` }, "error");
10407
10407
  const serialized = toJournalValue(value === void 0 ? null : value, `tool '${call.name}'`);
10408
10408
  options.retryCounts.delete(call.name);
@@ -10829,6 +10829,7 @@ async function runAgent(options) {
10829
10829
  runtime,
10830
10830
  retryCounts: modelRetryCounts,
10831
10831
  maxModelRetries: options.modelRetryAttempts ?? 2,
10832
+ ordinal: toolCallsUsed,
10832
10833
  ...events === void 0 ? {} : { events },
10833
10834
  ...gateAudit === void 0 ? {} : { audit: gateAudit },
10834
10835
  now
@@ -13170,15 +13171,30 @@ function setLongTimeout(onDue, dueAtMs, now = Date.now) {
13170
13171
  */
13171
13172
  /**
13172
13173
  * Derives the idempotency key for one isolated tool dispatch. The key is
13173
- * a pure function of the run, the tool name, and the JCS-canonical
13174
- * arguments, so the same logical call always yields the same key
13175
- * (byte-identical reruns dedupe) and distinct calls never collide. The
13176
- * key never enters run identity; it exists only for the provider's own
13177
- * side-effect deduplication.
13174
+ * a pure function of the run, the LOGICAL INVOCATION (the seq of the
13175
+ * containing agent's journal entry plus that call's ordinal within the
13176
+ * agent's tool loop), the tool name, and the JCS-canonical arguments.
13177
+ *
13178
+ * The logical-invocation component is what makes the key both stable and
13179
+ * distinguishing (v1.59.x review P0.4): the agent-entry seq and the
13180
+ * per-agent tool-call ordinal are journal- and checkpoint-stable, so a
13181
+ * crash-and-resume re-dispatch of the SAME logical call (the at-least-
13182
+ * once window between execution and the turn checkpoint) reuses the same
13183
+ * dispatch entry and the restored ordinal, and therefore the same key;
13184
+ * while two SEPARATE calls in one run, even with byte-identical
13185
+ * arguments, occupy different ordinals and never collide. Without it two
13186
+ * intended effects sharing arguments would fold into one under external
13187
+ * deduplication.
13188
+ *
13189
+ * The key never enters run identity (it is absent from every content key
13190
+ * and toolset hash); it exists only for the provider's own side-effect
13191
+ * deduplication.
13178
13192
  */
13179
- function deriveExecIdempotencyKey(runId, tool, args) {
13193
+ function deriveExecIdempotencyKey(runId, agentSeq, ordinal, tool, args) {
13180
13194
  const canonical = jcsSerialize({
13181
13195
  runId,
13196
+ agentSeq,
13197
+ ordinal,
13182
13198
  tool,
13183
13199
  args
13184
13200
  });
@@ -13980,7 +13996,8 @@ function createCtx(internals, rootWorkflow) {
13980
13996
  };
13981
13997
  if (internals.executors !== void 0) {
13982
13998
  const executors = internals.executors;
13983
- toolRuntime.executeExternal = async (def, args) => {
13999
+ const agentSeq = running.seq;
14000
+ toolRuntime.executeExternal = async (def, args, ordinal) => {
13984
14001
  const tag = def.executor;
13985
14002
  const provider = executors[tag];
13986
14003
  if (provider === void 0) throw new ConfigError(`no executor registered for '${def.executor}'; register one via createEngine({ executors }) (https://docs.rulvar.com/guide/isolated-executor)`);
@@ -13994,7 +14011,7 @@ function createCtx(internals, rootWorkflow) {
13994
14011
  runId: internals.runId,
13995
14012
  spanId: toolSpanId,
13996
14013
  agentType,
13997
- idempotencyKey: deriveExecIdempotencyKey(internals.runId, def.name, args),
14014
+ idempotencyKey: deriveExecIdempotencyKey(internals.runId, agentSeq, ordinal, def.name, args),
13998
14015
  signal: toolSignal,
13999
14016
  log: (level, msg, data) => internals.events.emit(data === void 0 ? {
14000
14017
  type: "log",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.59.2",
3
+ "version": "1.59.3",
4
4
  "description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",