@gea-ai/agent-sdk 0.1.260922-alpha.0 → 0.1.260922-alpha.2

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.
@@ -1,6 +1,6 @@
1
1
  import { getSessionPayload, putSessionPayload } from "./agent-session-payload.js";
2
2
  import { agentModelCallsSchema, } from "@gea-ai/contract/agent-usage";
3
- import { AGENT_SESSION_COMPUTER_LEASE_MS, MAX_AGENT_SESSION_TRANSFER_BYTES, agentSessionTransferChunkSchema, agentSessionTransferReferenceSchema, agentSessionTransferIdentitySchema, agentSessionComputerSchema, agentSessionComputerReferenceSchema, AGENT_SESSION_EXECUTION_LEASE_MS, AGENT_SESSION_EXECUTION_PROTOCOL_VERSION, MAX_AGENT_SESSION_RESPONSE_BYTES, MAX_AGENT_SESSION_EXECUTION_ATTEMPTS, MAX_AGENT_SESSION_EXECUTION_BODY_BYTES, MAX_AGENT_SESSION_PAGE_SIZE, agentSessionAttemptIdentitySchema, agentSessionExecutionRenewSchema, agentSessionExecutionCompleteSchema, agentSessionExecutionPauseSchema, agentSessionExecutionControlSchema, agentSessionExecutionMutationSchema, agentSessionExecutionStartSchema, } from "@gea-ai/contract/agent-session";
3
+ import { AGENT_SESSION_COMPUTER_LEASE_MS, MAX_AGENT_SESSION_TRANSFER_BYTES, agentSessionTransferChunkSchema, agentSessionTransferReferenceSchema, agentSessionTransferIdentitySchema, agentSessionComputerSchema, agentSessionComputerReferenceSchema, AGENT_SESSION_EXECUTION_LEASE_MS, AGENT_SESSION_EXECUTION_PROTOCOL_VERSION, MAX_AGENT_SESSION_RESPONSE_BYTES, MAX_AGENT_SESSION_EXECUTION_ATTEMPTS, MAX_AGENT_SESSION_EXECUTION_BODY_BYTES, MAX_AGENT_SESSION_PAGE_SIZE, agentSessionAttemptIdentitySchema, agentSessionExecutionRenewSchema, agentSessionExecutionCompleteSchema, agentSessionExecutionPauseSchema, agentSessionExecutionControlSchema, agentSessionExecutionMutationSchema, agentSessionExecutionStartSchema, agentSessionExecutionPrepareSchema, } from "@gea-ai/contract/agent-session";
4
4
  import { z } from "zod";
5
5
  import { activeComputerOperation } from "./agent-session-computer.js";
6
6
  import { isAgentSessionInboxBusy, scheduleAgentSessionInbox, getAgentSessionRunState, handleAgentSessionInboxRequest, hasAgentSessionInbox, skipAgentSessionInputWait, } from "./agent-session-inbox.js";
@@ -335,17 +335,28 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
335
335
  return { computer };
336
336
  }));
337
337
  }
338
+ const capabilities = {
339
+ protocolVersion: AGENT_SESSION_EXECUTION_PROTOCOL_VERSION,
340
+ managedExecutionVersion: 1,
341
+ logicalRunVersion: 1,
342
+ };
338
343
  if (request.method === "GET" && url.pathname === "/v2/capabilities")
339
- return json({
340
- protocolVersion: AGENT_SESSION_EXECUTION_PROTOCOL_VERSION,
341
- managedExecutionVersion: 1,
342
- logicalRunVersion: 1,
343
- });
344
+ return json(capabilities);
344
345
  if (request.method === "GET" && url.pathname === "/v2/run-state")
345
346
  return json(await storage.transaction((tx) => getAgentSessionRunState(tx, url.searchParams.get("runId") ?? "")));
346
- if (request.method === "POST" && url.pathname === "/v2/turns/start") {
347
- const input = agentSessionExecutionStartSchema.parse(await readBody(request, storage));
348
- return json(await storage.transaction(async (tx) => {
347
+ if (request.method === "POST" &&
348
+ (url.pathname === "/v2/turns/start" ||
349
+ url.pathname === "/v2/turns/prepare")) {
350
+ const preparing = url.pathname === "/v2/turns/prepare";
351
+ const body = await readBody(request, storage);
352
+ const preparation = preparing
353
+ ? agentSessionExecutionPrepareSchema.parse(body)
354
+ : undefined;
355
+ const { resumeWaiting = false, ...input } = preparation ?? {
356
+ ...agentSessionExecutionStartSchema.parse(body),
357
+ resumeWaiting: false,
358
+ };
359
+ const result = await storage.transaction(async (tx) => {
349
360
  if (await activeComputerOperation(tx))
350
361
  throw new SessionProtocolError("computer_busy");
351
362
  if (await tx.get(key(`outcome:${encodeURIComponent(input.runId)}`)))
@@ -353,6 +364,10 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
353
364
  const previousRevision = await readRevision(tx);
354
365
  const active = await tx.get(key("active-turn"));
355
366
  const resuming = active?.status === "waiting";
367
+ // Resolve only a waiting continuation under the admission lock. Expired
368
+ // running attempts still require an explicit expectedAttemptId.
369
+ const expectedAttemptId = input.expectedAttemptId ??
370
+ (resumeWaiting && resuming ? active.attemptId : undefined);
356
371
  if (active) {
357
372
  const sameSelection = active.runId === input.runId &&
358
373
  active.packageVersionId === input.packageVersionId &&
@@ -367,7 +382,7 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
367
382
  throw new SessionProtocolError("turn_already_active");
368
383
  }
369
384
  if (!sameSelection ||
370
- input.expectedAttemptId !== active.attemptId ||
385
+ expectedAttemptId !== active.attemptId ||
371
386
  input.requestId === active.requestId)
372
387
  throw new SessionProtocolError("takeover_mismatch");
373
388
  if (!resuming &&
@@ -406,7 +421,8 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
406
421
  });
407
422
  const revision = await advanceRevision(tx);
408
423
  return { duplicate: false, revision, turn };
409
- }));
424
+ });
425
+ return json(preparing ? { ...result, capabilities } : result);
410
426
  }
411
427
  if (request.method === "POST" && url.pathname === "/v2/turns/renew") {
412
428
  const input = agentSessionExecutionRenewSchema.parse(await readBody(request, storage));
@@ -512,6 +528,10 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
512
528
  const state = await getAgentSessionRunState(tx, input.runId);
513
529
  if (state?.status === "draining")
514
530
  throw new SessionProtocolError("run_cleanup_pending");
531
+ // An invocation lease may still be live after a child wake claims its
532
+ // continuation. Only the inbox can seal that logical Run's outcome.
533
+ if (state?.status === "running" || state?.status === "waiting")
534
+ throw new SessionProtocolError("run_not_terminal");
515
535
  const terminalStatus = state?.status === "terminal"
516
536
  ? {
517
537
  completed: "finished",
@@ -2724,6 +2724,7 @@ export async function runAgentWorkerRequest(input) {
2724
2724
  if (finished)
2725
2725
  return finished;
2726
2726
  finished = (async () => {
2727
+ const cleanupStartedAt = performance.now();
2727
2728
  // AI SDK's onEnd may still write context artifacts after a client
2728
2729
  // disconnects. Settle the model/tools before releasing their filesystem.
2729
2730
  const results = [];
@@ -2733,10 +2734,13 @@ export async function runAgentWorkerRequest(input) {
2733
2734
  drained = background.length;
2734
2735
  results.push(...(await Promise.allSettled(batch)));
2735
2736
  }
2737
+ const drainedAt = performance.now();
2736
2738
  if (stopComputer)
2737
2739
  results.push(...(await Promise.allSettled([stopComputer()])));
2740
+ const computerStoppedAt = performance.now();
2738
2741
  if (input.onSessionFinished)
2739
2742
  results.push(...(await Promise.allSettled([input.onSessionFinished()])));
2743
+ const sessionFinishedAt = performance.now();
2740
2744
  // Keep the Worker lease through draining and Computer cleanup. The
2741
2745
  // host still owns terminal accounting; hand it a fresh lease only once
2742
2746
  // this invocation has stopped writing, then stop all Worker heartbeats.
@@ -2756,6 +2760,12 @@ export async function runAgentWorkerRequest(input) {
2756
2760
  }),
2757
2761
  ])));
2758
2762
  }
2763
+ invocationSpan?.setAttributes({
2764
+ "gea.session.finish.background_ms": Math.round(drainedAt - cleanupStartedAt),
2765
+ "gea.session.finish.computer_stop_ms": Math.round(computerStoppedAt - drainedAt),
2766
+ "gea.session.finish.session_release_ms": Math.round(sessionFinishedAt - computerStoppedAt),
2767
+ "gea.session.finish.lease_handoff_ms": Math.round(performance.now() - sessionFinishedAt),
2768
+ });
2759
2769
  if (input.inboxExecution) {
2760
2770
  const status = cancellation.signal.aborted
2761
2771
  ? "cancelled"
@@ -2848,7 +2858,6 @@ export async function runAgentWorkerRequest(input) {
2848
2858
  let response;
2849
2859
  try {
2850
2860
  if (input.sessionExecution) {
2851
- await maintainManagedLease();
2852
2861
  cancellation.signal.throwIfAborted();
2853
2862
  watchManagedLease();
2854
2863
  }
@@ -2971,6 +2980,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
2971
2980
  path: "/v2/turns/renew",
2972
2981
  schema: agentSessionExecutionStartResultSchema,
2973
2982
  });
2983
+ // Renewal atomically validates the attempt and returns cancellation intent;
2984
+ // a separate metadata read before it only adds a network round trip.
2985
+ if (admission.turn.cancelRequestedAt)
2986
+ throw new Error("Agent Session cancelled.");
2974
2987
  mutationSequence = admission.turn.nextMutationSequence;
2975
2988
  }
2976
2989
  const mutateSession = (values) => {
@@ -3508,25 +3521,28 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3508
3521
  ? (await import("./agent-core.js")).agentCore()
3509
3522
  : input.engine;
3510
3523
  const coreModule = engine?.module();
3511
- let coreConnection = engine
3512
- ? await createGeaCoreConnection(host, configuredModel, requestOptions, input.request.signal, input.snapshot.maxOutputTokens)
3513
- : undefined;
3514
- const model = engine
3515
- ? undefined
3516
- : await createGeaAI(host, configuredModel, requestOptions.extraBody, {
3517
- kind: "agent-dev",
3518
- packageName: input.snapshot.name,
3519
- }, requestOptions.requestBody);
3524
+ const [initialCoreConnection, model, storedContext] = await Promise.all([
3525
+ engine
3526
+ ? createGeaCoreConnection(host, configuredModel, requestOptions, input.request.signal, input.snapshot.maxOutputTokens)
3527
+ : undefined,
3528
+ engine
3529
+ ? undefined
3530
+ : createGeaAI(host, configuredModel, requestOptions.extraBody, {
3531
+ kind: "agent-dev",
3532
+ packageName: input.snapshot.name,
3533
+ }, requestOptions.requestBody),
3534
+ requestSessionJson({
3535
+ chatId: payload.chatId,
3536
+ method: "GET",
3537
+ namespace: input.sessionNamespace,
3538
+ path: sessionExecution ? "/v2/model-context" : "/v1/model-context",
3539
+ schema: agentSessionGetModelContextResultSchema,
3540
+ }),
3541
+ ]);
3542
+ let coreConnection = initialCoreConnection;
3520
3543
  // Model file parts can contain URL and byte values that ordinary JSON cannot preserve.
3521
3544
  const runtimeContext = decodeAgentModelProxyValue(payload.runtimeContext);
3522
3545
  let currentTurn = decodeAgentModelProxyValue(payload.currentTurn);
3523
- const storedContext = await requestSessionJson({
3524
- chatId: payload.chatId,
3525
- method: "GET",
3526
- namespace: input.sessionNamespace,
3527
- path: sessionExecution ? "/v2/model-context" : "/v1/model-context",
3528
- schema: agentSessionGetModelContextResultSchema,
3529
- });
3530
3546
  if (submittedUser &&
3531
3547
  storedContext.context &&
3532
3548
  storedContext.context.runId !== payload.runId) {
@@ -3814,25 +3830,35 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3814
3830
  }));
3815
3831
  }
3816
3832
  let mergedResponseMessageCount = 0;
3817
- const persistModelContext = async () => {
3833
+ let contextCheckpoint;
3834
+ let reusedContextCheckpoints = 0;
3835
+ const persistModelContext = () => {
3818
3836
  const modelContext = {
3819
3837
  contextSize: context.contextSize,
3820
3838
  messages: context.messages.map(encodeAgentModelProxyValue),
3821
- metadata: context.metadata,
3839
+ metadata: structuredClone(context.metadata),
3822
3840
  runId: payload.runId,
3823
3841
  };
3824
- if (sessionExecution) {
3825
- await mutateSession({ context: modelContext });
3826
- return;
3842
+ const serialized = JSON.stringify(modelContext);
3843
+ // Lifecycle callbacks can checkpoint the same projection several times.
3844
+ // Reuse the pending write too: an unchanged snapshot must still await its
3845
+ // durable commit, including propagating a failed commit to every waiter.
3846
+ if (contextCheckpoint?.serialized === serialized) {
3847
+ persistenceSpan?.setAttribute("gea.session.persistence.context_reused", ++reusedContextCheckpoints);
3848
+ return contextCheckpoint.pending;
3827
3849
  }
3828
- await requestSessionJson({
3829
- body: { context: modelContext },
3830
- chatId: payload.chatId,
3831
- method: "POST",
3832
- namespace: input.sessionNamespace,
3833
- path: "/v1/model-context",
3834
- schema: agentSessionPutModelContextResultSchema,
3835
- });
3850
+ const pending = sessionExecution
3851
+ ? mutateSession({ context: modelContext })
3852
+ : requestSessionJson({
3853
+ body: { context: modelContext },
3854
+ chatId: payload.chatId,
3855
+ method: "POST",
3856
+ namespace: input.sessionNamespace,
3857
+ path: "/v1/model-context",
3858
+ schema: agentSessionPutModelContextResultSchema,
3859
+ }).then(() => undefined);
3860
+ contextCheckpoint = { serialized, pending };
3861
+ return pending;
3836
3862
  };
3837
3863
  if (startsNewTurn && findLatestUserMessage(currentTurn)) {
3838
3864
  await persistModelContext();
@@ -4095,9 +4121,16 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4095
4121
  cursor = page.next_cursor ?? undefined;
4096
4122
  } while (cursor);
4097
4123
  };
4124
+ let finalArtifacts;
4125
+ const refreshFinalArtifacts = () =>
4126
+ // Context callbacks can publish outputs. Both terminal consumers use the
4127
+ // same snapshot, taken only after those callbacks and their work settle.
4128
+ (finalArtifacts ??= contextFinished.then(() => refreshArtifacts()));
4098
4129
  const saveResponseMessage = async ({ responseMessage, }) => {
4099
4130
  const saveStartedAt = performance.now();
4100
- await refreshArtifacts();
4131
+ const metadata = await contextFinished;
4132
+ const contextFinishedAt = performance.now();
4133
+ await refreshFinalArtifacts();
4101
4134
  const artifactsFinishedAt = performance.now();
4102
4135
  responseMessage.parts = [
4103
4136
  ...responseMessage.parts.filter((part) => part.type !== "data-artifact" ||
@@ -4123,9 +4156,9 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4123
4156
  context.metadata.suspendedMessageId = responseMessage.id;
4124
4157
  responseMessage.metadata = {
4125
4158
  ...responseMessage.metadata,
4126
- ...(await contextFinished),
4159
+ ...metadata,
4127
4160
  };
4128
- const contextFinishedAt = performance.now();
4161
+ const transcriptStartedAt = performance.now();
4129
4162
  // Retain the generated transcript even if context settlement failed.
4130
4163
  // A failed settlement must never produce a successful Run completion.
4131
4164
  if (sessionExecution) {
@@ -4145,9 +4178,9 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4145
4178
  await persistModelContext();
4146
4179
  const contextPersistedAt = performance.now();
4147
4180
  persistenceSpan?.setAttributes({
4148
- "gea.session.finish.artifacts_ms": Math.round(artifactsFinishedAt - saveStartedAt),
4149
- "gea.session.finish.context_wait_ms": Math.round(contextFinishedAt - artifactsFinishedAt),
4150
- "gea.session.finish.transcript_ms": Math.round(transcriptFinishedAt - contextFinishedAt),
4181
+ "gea.session.finish.artifacts_ms": Math.round(artifactsFinishedAt - contextFinishedAt),
4182
+ "gea.session.finish.context_wait_ms": Math.round(contextFinishedAt - saveStartedAt),
4183
+ "gea.session.finish.transcript_ms": Math.round(transcriptFinishedAt - transcriptStartedAt),
4151
4184
  "gea.session.finish.context_write_ms": Math.round(contextPersistedAt - transcriptFinishedAt),
4152
4185
  });
4153
4186
  const needsInput = !inboxRunner?.suspended &&
@@ -4735,7 +4768,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4735
4768
  chunk.toolCallId === inboxRunner?.suspendedToolCallId)
4736
4769
  return;
4737
4770
  if (chunk.type === "finish-step" || chunk.type === "finish") {
4738
- await refreshArtifacts();
4771
+ if (chunk.type === "finish")
4772
+ await refreshFinalArtifacts();
4773
+ else
4774
+ await refreshArtifacts();
4739
4775
  for (const data of publishedArtifacts.values()) {
4740
4776
  if (emittedArtifacts.has(data.id))
4741
4777
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gea-ai/agent-sdk",
3
- "version": "0.1.260922-alpha.0",
3
+ "version": "0.1.260922-alpha.2",
4
4
  "private": false,
5
5
  "homepage": "https://musegea.com/developers",
6
6
  "license": "Apache-2.0",
@@ -108,7 +108,7 @@
108
108
  "@ai-sdk/otel": "1.0.9",
109
109
  "@ai-sdk/provider": "4.0.1",
110
110
  "@ai-sdk/provider-utils": "5.0.2",
111
- "@gea-ai/contract": "0.1.260922-alpha.0",
111
+ "@gea-ai/contract": "0.1.260922-alpha.2",
112
112
  "@opentelemetry/api": "1.9.1",
113
113
  "@opentelemetry/context-async-hooks": "2.10.0",
114
114
  "@opentelemetry/core": "2.10.0",