@zhivex-ai/core 1.16.1 → 1.18.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/agent.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { refreshAgentTaskOutcome } from "./agent-reconciliation.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { createAgentApprovalMessage, getAgentApprovalRequests } from "./agent-approval.js";
3
4
  import { createAgentHandoffMessage } from "./agent-handoff-contracts.js";
@@ -113,6 +114,7 @@ const cloneMetadata = (...values) => {
113
114
  };
114
115
  const toOutput = (state) => ({
115
116
  status: state.status,
117
+ taskOutcome: state.taskOutcome,
116
118
  outputText: state.outputText,
117
119
  finalOutput: state.status === "completed" && state.finalOutput !== undefined
118
120
  ? state.finalOutput
@@ -637,19 +639,19 @@ export const createSubAgentTool = (options) => {
637
639
  }
638
640
  };
639
641
  };
640
- const saveStateWithRevision = async (store, state) => {
642
+ const saveStateWithRevision = async (store, state, serializedNextState) => {
641
643
  const expectedRevision = state.revision ?? 0;
642
644
  const nextRevision = expectedRevision + 1;
643
645
  const nextState = { ...state, revision: nextRevision };
644
- await store.save(cloneState(nextState), { expectedRevision });
646
+ await store.save(serializedNextState === undefined ? cloneState(nextState) : JSON.parse(serializedNextState), { expectedRevision });
645
647
  state.revision = nextRevision;
646
648
  };
647
649
  const claimAgentExecution = async (agent, state) => {
648
650
  state.status = "running";
649
651
  state.updatedAt = Date.now();
650
- assertStateSize(agent, state);
652
+ const serialized = assertStateSize(agent, agent.store ? normalizeAgentRunState({ ...state, revision: (state.revision ?? 0) + 1 }) : state);
651
653
  if (agent.store) {
652
- await saveStateWithRevision(agent.store, state);
654
+ await saveStateWithRevision(agent.store, state, serialized);
653
655
  }
654
656
  };
655
657
  const assertStateSize = (agent, state, policy) => {
@@ -657,16 +659,19 @@ const assertStateSize = (agent, state, policy) => {
657
659
  if (!Number.isSafeInteger(limit) || limit < 1) {
658
660
  throw new ValidationError('Agent policy "maxStateBytes" must be a positive integer.');
659
661
  }
660
- const bytes = new TextEncoder().encode(JSON.stringify(state)).byteLength;
662
+ const serialized = JSON.stringify(state);
663
+ const bytes = new TextEncoder().encode(serialized).byteLength;
661
664
  if (bytes > limit) {
662
665
  throw new ValidationError(`Agent run state is ${bytes} bytes and exceeds maxStateBytes=${limit}. Offload large tool outputs to artifacts or raise the explicit limit.`);
663
666
  }
667
+ return serialized;
664
668
  };
665
669
  const persistState = async (agent, state, policy) => {
666
670
  state.updatedAt = Date.now();
667
- assertStateSize(agent, state, policy);
671
+ await refreshAgentTaskOutcome(state, agent.store);
672
+ const serialized = assertStateSize(agent, agent.store ? normalizeAgentRunState({ ...state, revision: (state.revision ?? 0) + 1 }) : state, policy);
668
673
  if (agent.store) {
669
- await saveStateWithRevision(agent.store, state);
674
+ await saveStateWithRevision(agent.store, state, serialized);
670
675
  }
671
676
  await emitTelemetryEvent(agent, {
672
677
  type: "state-saved",
@@ -848,6 +853,11 @@ const resolveContext = async (agent, input) => {
848
853
  ensureValidStateInput(normalizedInput);
849
854
  const metadata = cloneMetadata(agent.metadata, loadedState?.metadata, input.metadata, input.handoff?.metadata);
850
855
  if (loadedState) {
856
+ const groupIdentity = input.metadata?.agentGroupIdentity;
857
+ if (groupIdentity !== undefined && (loadedState.metadata?.agentGroupIdentity !== groupIdentity ||
858
+ loadedState.agentId !== agent.id)) {
859
+ throw new ConflictError("Agent group idempotency key belongs to a different member or agent.");
860
+ }
851
861
  bindDurableRuntime(agent, input, loadedState, executionEnvironmentBinding);
852
862
  const maxSteps = input.maxSteps ?? loadedState.maxSteps;
853
863
  const resumed = await applyApprovalResponses(loadedState.messages, input.approvals, loadedState.pendingApprovals, loadedState.approvalHistory, agent.toolApprovalSigner);
@@ -967,6 +977,9 @@ const wrapToolWithJournal = (agent, state, tool) => {
967
977
  throw new ValidationError(`Durable tool "${tool.name}" requires an execution context.`);
968
978
  }
969
979
  const serializedInput = serializeJsonValue(input);
980
+ const confirmed = state.reconciliations?.find(record => record.evidence.toolName === tool.name && canonicalJson(record.evidence.input) === canonicalJson(serializedInput));
981
+ if (confirmed)
982
+ return confirmed.evidence.output;
970
983
  const step = context.step;
971
984
  const toolCallId = durableToolCallId(state.runId, step, context.toolCall.id, tool.name, serializedInput);
972
985
  const idempotencyKey = `${state.runId}:${toolCallId}`;
@@ -975,6 +988,7 @@ const wrapToolWithJournal = (agent, state, tool) => {
975
988
  runId: state.runId,
976
989
  scope: state.scope,
977
990
  toolCallId,
991
+ providerToolCallId: context.toolCall.id,
978
992
  toolName: tool.name,
979
993
  status: "pending",
980
994
  idempotencyKey,
@@ -1255,7 +1269,7 @@ const createGenerateOptions = (agent, state, input, messages, maxSteps, context,
1255
1269
  ].filter((value) => value !== undefined);
1256
1270
  const maxTokens = tokenCeilings.length ? Math.min(...tokenCeilings) : undefined;
1257
1271
  const requestedToolExecution = input.toolExecution ?? agent.toolExecution;
1258
- const toolExecution = agent.subagents?.length
1272
+ const toolExecution = agent.subagents?.length && !(requestedToolExecution?.parallel && requestedToolExecution.independentOnly)
1259
1273
  ? {
1260
1274
  ...requestedToolExecution,
1261
1275
  parallel: false,
@@ -1850,7 +1864,39 @@ export const prepareSubagentsForAgent = (agent, options = {}) => {
1850
1864
  };
1851
1865
  };
1852
1866
  export const runAgentGroup = async (agents, input = {}) => {
1853
- const { stopOnError, runId: _runId, state: _state, approvals: _approvals, handoff: _handoff, ...sharedInput } = input;
1867
+ const { stopOnError, maxConcurrency, runId: _runId, state: _state, approvals: _approvals, handoff: _handoff, ...sharedInput } = input;
1868
+ if (maxConcurrency !== undefined && (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1)) {
1869
+ throw new ValidationError('Agent group "maxConcurrency" must be a positive safe integer.');
1870
+ }
1871
+ // Validate the complete group before any member can claim a key or invoke a model.
1872
+ const identities = new Set();
1873
+ const claims = [];
1874
+ const memberInputs = agents.map((member) => {
1875
+ const merged = { ...sharedInput, ...member.input };
1876
+ ensureValidScope(merged.scope);
1877
+ ensureValidIdempotencyInput(merged, member.agent.store);
1878
+ if (!merged.idempotencyKey)
1879
+ return merged;
1880
+ const identity = member.name ?? member.agent.id;
1881
+ if (!identity || identities.has(identity)) {
1882
+ throw new ValidationError("Idempotent agent group members require unique names or agent IDs.");
1883
+ }
1884
+ identities.add(identity);
1885
+ const key = member.input?.idempotencyKey ?? `agent-group:${JSON.stringify([input.idempotencyKey, identity])}`;
1886
+ const scope = merged.scope;
1887
+ const scopedKey = JSON.stringify([scope?.namespace ?? "default", scope?.tenantId, scope?.userId, key]);
1888
+ if (claims.some((claim) => claim.store === member.agent.store && claim.key === scopedKey)) {
1889
+ throw new ConflictError("Agent group members cannot share an explicit idempotency key in the same store and scope.");
1890
+ }
1891
+ claims.push({ store: member.agent.store, key: scopedKey });
1892
+ return {
1893
+ ...merged,
1894
+ idempotencyKey: key,
1895
+ metadata: cloneMetadata(input.metadata, member.input?.metadata, {
1896
+ agentGroupIdentity: JSON.stringify([input.idempotencyKey ?? null, identity])
1897
+ })
1898
+ };
1899
+ });
1854
1900
  const parentRunId = input.parentRunId;
1855
1901
  const controllers = agents.map(() => new AbortController());
1856
1902
  let failFastTriggered = false;
@@ -1866,15 +1912,17 @@ export const runAgentGroup = async (agents, input = {}) => {
1866
1912
  }
1867
1913
  });
1868
1914
  };
1869
- const runs = agents.map(async (member, index) => {
1915
+ const runMember = async (member, index) => {
1870
1916
  const merged = createMergedAbortSignal(input.abortSignal, member.input?.abortSignal, controllers[index].signal);
1871
1917
  try {
1918
+ if (merged.signal?.aborted) {
1919
+ throw new DOMException("Agent group member aborted before execution.", "AbortError");
1920
+ }
1872
1921
  const runInput = {
1873
- ...sharedInput,
1874
- ...(member.input ?? {}),
1922
+ ...memberInputs[index],
1875
1923
  parentRunId: member.input?.parentRunId ?? parentRunId,
1876
1924
  abortSignal: merged.signal,
1877
- metadata: cloneMetadata(input.metadata, member.input?.metadata, {
1925
+ metadata: cloneMetadata(input.metadata, memberInputs[index]?.metadata, {
1878
1926
  ...(member.name ? { agentGroupMember: member.name } : {})
1879
1927
  })
1880
1928
  };
@@ -1891,8 +1939,20 @@ export const runAgentGroup = async (agents, input = {}) => {
1891
1939
  finally {
1892
1940
  merged.cleanup();
1893
1941
  }
1894
- });
1895
- const settled = await Promise.allSettled(runs);
1942
+ };
1943
+ const settled = new Array(agents.length);
1944
+ let nextIndex = 0;
1945
+ await Promise.all(Array.from({ length: Math.min(maxConcurrency ?? agents.length, agents.length) }, async () => {
1946
+ while (nextIndex < agents.length) {
1947
+ const index = nextIndex++;
1948
+ try {
1949
+ settled[index] = { status: "fulfilled", value: await runMember(agents[index], index) };
1950
+ }
1951
+ catch (reason) {
1952
+ settled[index] = { status: "rejected", reason };
1953
+ }
1954
+ }
1955
+ }));
1896
1956
  const outputs = settled.map((result, index) => {
1897
1957
  const member = agents[index];
1898
1958
  if (result.status === "fulfilled") {
@@ -1916,9 +1976,12 @@ export const runAgentGroup = async (agents, input = {}) => {
1916
1976
  }
1917
1977
  };
1918
1978
  });
1919
- const failed = outputs.some((output) => output.status === "rejected" || output.output?.status === "failed" || output.output?.status === "timed_out");
1979
+ const precedence = [
1980
+ "failed", "timed_out", "cancel_requested", "running", "queued", "waiting_approval", "suspended", "cancelled", "completed"
1981
+ ];
1982
+ const status = precedence.find((status) => outputs.some((output) => output.status === "rejected" ? status === "failed" : output.output?.status === status)) ?? "completed";
1920
1983
  return {
1921
- status: stopOnError && failed ? "failed" : failed ? "failed" : "completed",
1984
+ status,
1922
1985
  parentRunId,
1923
1986
  outputs
1924
1987
  };
@@ -2125,6 +2188,7 @@ export const runAgent = async (agent, input = {}) => {
2125
2188
  ...approvalsFromEvents(newSteps.flatMap((step) => step.response?.messages ?? []))
2126
2189
  ]);
2127
2190
  await persistState(agent, output.state, policy);
2191
+ output.taskOutcome = output.state.taskOutcome;
2128
2192
  await emitRunFinishTelemetry(agent, output.state);
2129
2193
  executionEnvironmentStatus = output.status;
2130
2194
  return returnInvocationOutput(output);
@@ -2456,6 +2520,7 @@ export const streamAgent = (agent, input = {}) => {
2456
2520
  await emitApprovalTelemetry(agent, result.state, approvalsFromEvents(newSteps.flatMap((step) => step.response?.messages ?? [])));
2457
2521
  }
2458
2522
  await persistState(agent, result.state, policy);
2523
+ result.taskOutcome = result.state.taskOutcome;
2459
2524
  await emitRunFinishTelemetry(agent, result.state);
2460
2525
  await publish({
2461
2526
  type: "agent-run-finish",