@gea-ai/agent-sdk 0.1.260922-alpha.2 → 0.1.260922-alpha.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.
@@ -1,6 +1,7 @@
1
+ import { digestAgentSessionMessage } from "./agent-session-transport.js";
1
2
  import { getSessionPayload, putSessionPayload } from "./agent-session-payload.js";
2
3
  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, agentSessionExecutionPrepareSchema, } from "@gea-ai/contract/agent-session";
4
+ 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, agentSessionExecutionCompleteStoredSchema, agentSessionExecutionPauseSchema, agentSessionExecutionControlSchema, agentSessionExecutionMutationSchema, agentSessionExecutionStartSchema, agentSessionExecutionPrepareSchema, } from "@gea-ai/contract/agent-session";
4
5
  import { z } from "zod";
5
6
  import { activeComputerOperation } from "./agent-session-computer.js";
6
7
  import { isAgentSessionInboxBusy, scheduleAgentSessionInbox, getAgentSessionRunState, handleAgentSessionInboxRequest, hasAgentSessionInbox, skipAgentSessionInputWait, } from "./agent-session-inbox.js";
@@ -506,12 +507,39 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
506
507
  return { duplicate: false, turn, revision };
507
508
  }));
508
509
  }
509
- if (request.method === "POST" && url.pathname === "/v2/turns/complete") {
510
- const input = agentSessionExecutionCompleteSchema.parse(await readBody(request, storage));
511
- const inputDigest = await digest(input);
510
+ if (request.method === "POST" &&
511
+ (url.pathname === "/v2/turns/complete" ||
512
+ url.pathname === "/v2/turns/complete-stored")) {
513
+ const body = await readBody(request, storage);
514
+ const stored = url.pathname === "/v2/turns/complete-stored"
515
+ ? agentSessionExecutionCompleteStoredSchema.parse(body)
516
+ : undefined;
517
+ const { messageReferences: _references, ...storedValues } = stored ?? {};
518
+ const input = agentSessionExecutionCompleteSchema.parse(stored ? storedValues : body);
519
+ const storedRequestDigest = stored ? await digest(stored) : undefined;
520
+ const standardDigest = stored ? undefined : await digest(input);
512
521
  return json(await storage.transaction(async (tx) => {
513
522
  const receiptKey = key(`outcome:${encodeURIComponent(input.runId)}`);
514
523
  const existing = await tx.get(receiptKey);
524
+ if (stored && existing?.storedRequestDigest) {
525
+ if (existing.storedRequestDigest !== storedRequestDigest)
526
+ throw new SessionProtocolError("completion_mismatch");
527
+ return { duplicate: true, outcome: existing.outcome };
528
+ }
529
+ if (stored) {
530
+ for (const reference of stored.messageReferences) {
531
+ const sequence = await tx.get(key(`message-id:${encodeURIComponent(reference.id)}`));
532
+ const entry = sequence === undefined
533
+ ? undefined
534
+ : await getSessionPayload(tx, sequenceKey("message", sequence));
535
+ if (!entry ||
536
+ (await digestAgentSessionMessage(entry.message)) !==
537
+ reference.sha256)
538
+ throw new SessionProtocolError("stored_message_mismatch", 412);
539
+ input.messages.push(entry.message);
540
+ }
541
+ }
542
+ const inputDigest = standardDigest ?? (await digest(input));
515
543
  if (existing) {
516
544
  if (existing.digest !== inputDigest)
517
545
  throw new SessionProtocolError("completion_mismatch");
@@ -566,7 +594,7 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
566
594
  if (input.context && input.context.runId !== input.runId)
567
595
  throw new SessionProtocolError("context_run_mismatch");
568
596
  const previousRevision = await readRevision(tx);
569
- await writeValues(tx, input);
597
+ await writeValues(tx, stored ? { ...input, messages: [] } : input);
570
598
  const modelCalls = await tx.get(key(`model-calls:${encodeURIComponent(input.runId)}`));
571
599
  const outcome = {
572
600
  ...(modelCalls === undefined
@@ -586,6 +614,7 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
586
614
  await tx.put(receiptKey, {
587
615
  outcome,
588
616
  digest: inputDigest,
617
+ ...(storedRequestDigest ? { storedRequestDigest } : {}),
589
618
  });
590
619
  const rangeKey = key(`run-events:${encodeURIComponent(input.runId)}`);
591
620
  const storedRange = await tx.get(rangeKey);
@@ -1,6 +1,10 @@
1
+ import { type AgentSessionMessage } from "@gea-ai/contract/agent-session";
1
2
  /** Both hosted and in-Worker clients use the same bounded, non-retrying transport. */
2
3
  export declare function sendAgentSessionRequest(input: {
3
4
  path: string;
4
5
  body?: unknown;
5
6
  send: (path: string, body: string | undefined) => Promise<Response>;
6
7
  }): Promise<Response>;
8
+ /** Compare message content independently of object-key insertion order. JSON
9
+ * serialization also gives Dates the same representation as persisted values. */
10
+ export declare function digestAgentSessionMessage(message: AgentSessionMessage): Promise<string>;
@@ -1,4 +1,4 @@
1
- import { AGENT_SESSION_TRANSFER_CHARS, agentSessionExecutionMutationSchema, agentSessionExecutionPauseSchema, agentSessionExecutionCompleteSchema, MAX_AGENT_SESSION_EXECUTION_BODY_BYTES, MAX_AGENT_SESSION_TRANSFER_BYTES, agentSessionAttemptIdentitySchema, agentSessionTransferIdentitySchema, } from "@gea-ai/contract/agent-session";
1
+ import { AGENT_SESSION_TRANSFER_CHARS, agentSessionExecutionMutationSchema, agentSessionMessageSchema, agentSessionExecutionPauseSchema, agentSessionExecutionCompleteSchema, MAX_AGENT_SESSION_EXECUTION_BODY_BYTES, MAX_AGENT_SESSION_TRANSFER_BYTES, agentSessionAttemptIdentitySchema, agentSessionTransferIdentitySchema, } from "@gea-ai/contract/agent-session";
2
2
  /** Both hosted and in-Worker clients use the same bounded, non-retrying transport. */
3
3
  export async function sendAgentSessionRequest(input) {
4
4
  const serialized = input.body === undefined ? undefined : JSON.stringify(input.body);
@@ -49,3 +49,11 @@ export async function sendAgentSessionRequest(input) {
49
49
  }
50
50
  return input.send(input.path, JSON.stringify({ $agentSessionTransfer: { ...identity, chunks } }));
51
51
  }
52
+ /** Compare message content independently of object-key insertion order. JSON
53
+ * serialization also gives Dates the same representation as persisted values. */
54
+ export async function digestAgentSessionMessage(message) {
55
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(agentSessionMessageSchema.parse(message), (_key, value) => value !== null && typeof value === "object" && !Array.isArray(value)
56
+ ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))
57
+ : value)));
58
+ return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
59
+ }
@@ -4,3 +4,9 @@ import type { UIMessageChunk } from "ai";
4
4
  * Control events, tool results and part boundaries retain their exact order.
5
5
  */
6
6
  export declare function coalesceAgentTextDeltas<T extends UIMessageChunk>(stream: ReadableStream<T>): ReadableStream<T>;
7
+ /** Group ready events for one durable mutation. Start/finish stay separate so
8
+ * first bytes are immediate and context settlement cannot hold back prior text.
9
+ * At most 5 ms, 64 events or 64 Ki serialized characters are collected. An
10
+ * oversized individual event uses the existing Session transfer protocol.
11
+ */
12
+ export declare function batchAgentStreamEvents<T extends UIMessageChunk>(stream: ReadableStream<T>): ReadableStream<T[]>;
@@ -71,3 +71,86 @@ export function coalesceAgentTextDeltas(stream) {
71
71
  },
72
72
  });
73
73
  }
74
+ /** Group ready events for one durable mutation. Start/finish stay separate so
75
+ * first bytes are immediate and context settlement cannot hold back prior text.
76
+ * At most 5 ms, 64 events or 64 Ki serialized characters are collected. An
77
+ * oversized individual event uses the existing Session transfer protocol.
78
+ */
79
+ export function batchAgentStreamEvents(stream) {
80
+ const reader = stream.getReader();
81
+ let pending;
82
+ let held;
83
+ let failure;
84
+ let cancelled = false;
85
+ const read = () => (pending ??= reader.read());
86
+ const boundary = (chunk) => chunk.type === "start" ||
87
+ chunk.type === "finish" ||
88
+ chunk.type === "abort" ||
89
+ chunk.type === "error";
90
+ return new ReadableStream({
91
+ async pull(controller) {
92
+ let timer;
93
+ try {
94
+ if (failure)
95
+ throw failure.error;
96
+ const first = held ?? (await read());
97
+ held = undefined;
98
+ pending = undefined;
99
+ if (cancelled)
100
+ return;
101
+ if (first.done) {
102
+ reader.releaseLock();
103
+ controller.close();
104
+ return;
105
+ }
106
+ const batch = [first.value];
107
+ let size = JSON.stringify(first.value).length;
108
+ if (!boundary(first.value) && size < 64 * 1024) {
109
+ const deadline = new Promise((resolve) => {
110
+ timer = setTimeout(() => resolve(null), 5);
111
+ });
112
+ while (batch.length < 64 && size < 64 * 1024) {
113
+ let next;
114
+ try {
115
+ next = await Promise.race([read(), deadline]);
116
+ }
117
+ catch (error) {
118
+ // Received events still precede the failure in the durable journal.
119
+ failure = { error };
120
+ break;
121
+ }
122
+ if (next === null)
123
+ break;
124
+ pending = undefined;
125
+ if (next.done || boundary(next.value)) {
126
+ held = next;
127
+ break;
128
+ }
129
+ const nextSize = JSON.stringify(next.value).length;
130
+ if (size + nextSize > 64 * 1024) {
131
+ held = next;
132
+ break;
133
+ }
134
+ batch.push(next.value);
135
+ size += nextSize;
136
+ }
137
+ }
138
+ if (!cancelled)
139
+ controller.enqueue(batch);
140
+ }
141
+ catch (error) {
142
+ if (!cancelled)
143
+ controller.error(error);
144
+ reader.releaseLock();
145
+ }
146
+ finally {
147
+ clearTimeout(timer);
148
+ }
149
+ },
150
+ async cancel(reason) {
151
+ cancelled = true;
152
+ await reader.cancel(reason);
153
+ reader.releaseLock();
154
+ },
155
+ });
156
+ }
@@ -1,6 +1,6 @@
1
1
  import { AGENT_SUBAGENT_CONCURRENCY_HEADER } from "@gea-ai/contract/agent-worker-tools";
2
2
  import { DEFAULT_MAX_CONCURRENT_SUBAGENTS } from "@gea-ai/contract/agent-packages";
3
- import { coalesceAgentTextDeltas } from "./agent-stream-coalescing.js";
3
+ import { batchAgentStreamEvents, coalesceAgentTextDeltas, } from "./agent-stream-coalescing.js";
4
4
  import { sendAgentSessionRequest } from "./agent-session-transport.js";
5
5
  import { agentSessionOverridesSchema } from "@gea-ai/contract/agent-session-overrides";
6
6
  import { resolveModelOptions } from "@gea-ai/contract/model-options";
@@ -13,7 +13,6 @@ export { createGeaAI } from "./gea-ai.js";
13
13
  import { mergeLlmJson, llmRuntimeRequestOptionsSchema, } from "@gea-ai/contract/llm-catalog";
14
14
  import { executeChannelAgentRun, uploadChannelInput, } from "./agent-channel-api.js";
15
15
  export { extractChannelReply } from "./agent-channel-api.js";
16
- import { agentWorkerAttachmentOutputSchema } from "@gea-ai/contract/agent-api-host";
17
16
  import { streamManagedAgentSessionRun } from "./agent-session-stream.js";
18
17
  import { agentSessionReadToolOutputSchema, agentSessionReadToolOutputResultSchema, } from "@gea-ai/contract/agent-session";
19
18
  import { handleAgentComputerRequest, readSessionComputer, } from "./agent-computer-http.js";
@@ -32,7 +31,7 @@ import { agentModelCallSchema, } from "@gea-ai/contract/agent-usage";
32
31
  import { createAgentContextRuntime } from "./context-runtime.js";
33
32
  import { studioExternalUserPrincipalSchema } from "@gea-ai/contract";
34
33
  import { replaceInputFiles, withTurnImages } from "./agent-worker-attachments.js";
35
- import { toAgentHttpFile, } from "@gea-ai/contract/agent-http-api";
34
+ import { toAgentHttpFile } from "@gea-ai/contract/agent-http-api";
36
35
  import { handleHostedAgentHttp } from "./agent-http.js";
37
36
  import { createWorkerAgentTool, createWorkerRunCancelTool } from "./agent-call.js";
38
37
  import { AgentSessionRunner, resumeSessionWait } from "./agent-session-runner.js";
@@ -1602,7 +1601,6 @@ function createAgentWorkerHostAdapter(bindings) {
1602
1601
  }
1603
1602
  if (path === `${HOST_PATH}/tools/initialize` ||
1604
1603
  path === `${HOST_PATH}/tools/attachments/read` ||
1605
- path === `${HOST_PATH}/tools/attachments/outputs` ||
1606
1604
  path === `${HOST_PATH}/tools/attachments/prepare` ||
1607
1605
  path === `${HOST_PATH}/tools/invoke` ||
1608
1606
  path === `${HOST_PATH}/agents/runs` ||
@@ -2709,6 +2707,7 @@ export async function runAgentWorkerRequest(input) {
2709
2707
  }, async (signal, waitUntil) => {
2710
2708
  const invocationTraceContext = captureTraceContext();
2711
2709
  const invocationSpan = trace.getActiveSpan();
2710
+ const invocationTiming = {};
2712
2711
  const cancellation = new AbortController();
2713
2712
  let stopComputer;
2714
2713
  let inboxOutcome = {
@@ -2748,6 +2747,7 @@ export async function runAgentWorkerRequest(input) {
2748
2747
  clearTimeout(cancellationTimer);
2749
2748
  if (leaseMaintenance)
2750
2749
  await leaseMaintenance;
2750
+ const leaseDrainedAt = performance.now();
2751
2751
  if (input.sessionExecution) {
2752
2752
  results.push(...(await Promise.allSettled([
2753
2753
  requestSessionJson({
@@ -2764,9 +2764,11 @@ export async function runAgentWorkerRequest(input) {
2764
2764
  "gea.session.finish.background_ms": Math.round(drainedAt - cleanupStartedAt),
2765
2765
  "gea.session.finish.computer_stop_ms": Math.round(computerStoppedAt - drainedAt),
2766
2766
  "gea.session.finish.session_release_ms": Math.round(sessionFinishedAt - computerStoppedAt),
2767
- "gea.session.finish.lease_handoff_ms": Math.round(performance.now() - sessionFinishedAt),
2767
+ "gea.session.finish.lease_observer_wait_ms": Math.round(leaseDrainedAt - sessionFinishedAt),
2768
+ "gea.session.finish.lease_handoff_ms": Math.round(performance.now() - leaseDrainedAt),
2768
2769
  });
2769
2770
  if (input.inboxExecution) {
2771
+ const inboxStartedAt = performance.now();
2770
2772
  const status = cancellation.signal.aborted
2771
2773
  ? "cancelled"
2772
2774
  : results.some((result) => result.status === "rejected")
@@ -2790,7 +2792,8 @@ export async function runAgentWorkerRequest(input) {
2790
2792
  },
2791
2793
  }),
2792
2794
  }),
2793
- }));
2795
+ }))
2796
+ .finally(() => invocationSpan?.setAttribute("gea.session.finish.inbox_ms", Math.round(performance.now() - inboxStartedAt)));
2794
2797
  if (!sessionResponse.ok)
2795
2798
  throw new Error(`Session finish rejected (${sessionResponse.status}).`);
2796
2799
  }
@@ -2873,7 +2876,7 @@ export async function runAgentWorkerRequest(input) {
2873
2876
  stopComputer = stop;
2874
2877
  }, (outcome) => {
2875
2878
  inboxOutcome = outcome;
2876
- });
2879
+ }, invocationTiming);
2877
2880
  }
2878
2881
  catch (error) {
2879
2882
  cancellation.abort(error);
@@ -2917,6 +2920,8 @@ export async function runAgentWorkerRequest(input) {
2917
2920
  }
2918
2921
  signal.removeEventListener("abort", onAbort);
2919
2922
  await finish();
2923
+ if (invocationTiming.lastModelFinishedAt !== undefined)
2924
+ invocationSpan?.setAttribute("gea.session.finish.model_to_close_ms", Math.round(performance.now() - invocationTiming.lastModelFinishedAt));
2920
2925
  controller.close();
2921
2926
  }
2922
2927
  catch (error) {
@@ -2938,7 +2943,7 @@ export async function runAgentWorkerRequest(input) {
2938
2943
  });
2939
2944
  });
2940
2945
  }
2941
- async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessionOutcome) {
2946
+ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessionOutcome, invocationTiming) {
2942
2947
  const invocationTrace = activeTraceRef();
2943
2948
  const host = input.bindings
2944
2949
  ? createAgentWorkerHostAdapter(input.bindings)
@@ -2968,7 +2973,13 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
2968
2973
  let writeTail = Promise.resolve();
2969
2974
  let writeFailure;
2970
2975
  const persistenceSpan = trace.getActiveSpan();
2971
- const persistenceTotals = { writes: 0, queueMs: 0, requestMs: 0 };
2976
+ const persistenceTotals = {
2977
+ writes: 0,
2978
+ queueMs: 0,
2979
+ requestMs: 0,
2980
+ events: 0,
2981
+ eventBatches: 0,
2982
+ };
2972
2983
  if (sessionExecution) {
2973
2984
  if (sessionExecution.runId !== payload.runId)
2974
2985
  throw new Error("Session Run identity mismatch.");
@@ -3009,10 +3020,16 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3009
3020
  finally {
3010
3021
  const requestMs = performance.now() - startedAt;
3011
3022
  persistenceTotals.writes++;
3023
+ if (values.events?.length) {
3024
+ persistenceTotals.events += values.events.length;
3025
+ persistenceTotals.eventBatches++;
3026
+ }
3012
3027
  persistenceTotals.queueMs += queueMs;
3013
3028
  persistenceTotals.requestMs += requestMs;
3014
3029
  persistenceSpan?.setAttributes({
3015
3030
  "gea.session.persistence.writes": persistenceTotals.writes,
3031
+ "gea.session.persistence.events": persistenceTotals.events,
3032
+ "gea.session.persistence.event_batches": persistenceTotals.eventBatches,
3016
3033
  "gea.session.persistence.queue_ms": Math.round(persistenceTotals.queueMs),
3017
3034
  "gea.session.persistence.request_ms": Math.round(persistenceTotals.requestMs),
3018
3035
  });
@@ -3832,6 +3849,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3832
3849
  let mergedResponseMessageCount = 0;
3833
3850
  let contextCheckpoint;
3834
3851
  let reusedContextCheckpoints = 0;
3852
+ let modelCallsInitialized = false;
3835
3853
  const persistModelContext = () => {
3836
3854
  const modelContext = {
3837
3855
  contextSize: context.contextSize,
@@ -3848,7 +3866,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3848
3866
  return contextCheckpoint.pending;
3849
3867
  }
3850
3868
  const pending = sessionExecution
3851
- ? mutateSession({ context: modelContext })
3869
+ ? mutateSession({
3870
+ context: modelContext,
3871
+ ...(!modelCallsInitialized ? { modelCalls: [] } : {}),
3872
+ })
3852
3873
  : requestSessionJson({
3853
3874
  body: { context: modelContext },
3854
3875
  chatId: payload.chatId,
@@ -3857,6 +3878,8 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3857
3878
  path: "/v1/model-context",
3858
3879
  schema: agentSessionPutModelContextResultSchema,
3859
3880
  }).then(() => undefined);
3881
+ if (sessionExecution)
3882
+ modelCallsInitialized = true;
3860
3883
  contextCheckpoint = { serialized, pending };
3861
3884
  return pending;
3862
3885
  };
@@ -3902,8 +3925,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3902
3925
  await appendStreamEvent({ type: "data-model-call", data: call }, `model-call:${call.callId}`);
3903
3926
  }
3904
3927
  };
3905
- if (sessionExecution)
3928
+ if (sessionExecution && !modelCallsInitialized) {
3906
3929
  await mutateSession({ modelCalls: [] });
3930
+ modelCallsInitialized = true;
3931
+ }
3907
3932
  let contextSignal = input.request.signal;
3908
3933
  const contextRuntime = createAgentContextRuntime({
3909
3934
  ai: host,
@@ -4073,12 +4098,16 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4073
4098
  catch (error) {
4074
4099
  fail(error);
4075
4100
  }
4101
+ const finalContextStartedAt = performance.now();
4076
4102
  try {
4077
4103
  await persistModelContext();
4078
4104
  }
4079
4105
  catch (error) {
4080
4106
  fail(error);
4081
4107
  }
4108
+ finally {
4109
+ persistenceSpan?.setAttribute("gea.session.finish.final_context_ms", Math.round(performance.now() - finalContextStartedAt));
4110
+ }
4082
4111
  resolveContext({
4083
4112
  ...(!failure && inboxRunner?.suspended
4084
4113
  ? { sessionStatus: "waiting" }
@@ -4095,52 +4124,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4095
4124
  })();
4096
4125
  return settling;
4097
4126
  };
4098
- const publishedArtifacts = new Map();
4099
- const emittedArtifacts = new Set();
4100
- const refreshArtifacts = async () => {
4101
- if (!artifactScope || input.request.signal.aborted)
4102
- return;
4103
- let cursor;
4104
- do {
4105
- const response = await host.fetch(`${HOST_PATH}/tools/attachments/outputs`, {
4106
- method: "POST",
4107
- headers: { "content-type": "application/json" },
4108
- body: JSON.stringify({ scope: artifactScope, cursor }),
4109
- signal: input.request.signal,
4110
- });
4111
- if (!response.ok)
4112
- throw new Error(`Could not list published Artifacts (${response.status}).`);
4113
- const page = z
4114
- .object({
4115
- items: z.array(agentWorkerAttachmentOutputSchema),
4116
- next_cursor: z.uuid().nullable(),
4117
- })
4118
- .parse(await response.json());
4119
- for (const artifact of page.items)
4120
- publishedArtifacts.set(artifact.id, artifact);
4121
- cursor = page.next_cursor ?? undefined;
4122
- } while (cursor);
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()));
4129
4127
  const saveResponseMessage = async ({ responseMessage, }) => {
4130
4128
  const saveStartedAt = performance.now();
4131
4129
  const metadata = await contextFinished;
4132
4130
  const contextFinishedAt = performance.now();
4133
- await refreshFinalArtifacts();
4134
- const artifactsFinishedAt = performance.now();
4135
- responseMessage.parts = [
4136
- ...responseMessage.parts.filter((part) => part.type !== "data-artifact" ||
4137
- !publishedArtifacts.has(part.data.id)),
4138
- ...[...publishedArtifacts.values()].map((data) => ({
4139
- type: "data-artifact",
4140
- id: data.id,
4141
- data,
4142
- })),
4143
- ];
4144
4131
  if (inboxRunner?.suspendedToolCallId) {
4145
4132
  responseMessage.parts = responseMessage.parts.map((part) => {
4146
4133
  if (isToolUIPart(part) &&
@@ -4178,7 +4165,6 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4178
4165
  await persistModelContext();
4179
4166
  const contextPersistedAt = performance.now();
4180
4167
  persistenceSpan?.setAttributes({
4181
- "gea.session.finish.artifacts_ms": Math.round(artifactsFinishedAt - contextFinishedAt),
4182
4168
  "gea.session.finish.context_wait_ms": Math.round(contextFinishedAt - saveStartedAt),
4183
4169
  "gea.session.finish.transcript_ms": Math.round(transcriptFinishedAt - transcriptStartedAt),
4184
4170
  "gea.session.finish.context_write_ms": Math.round(contextPersistedAt - transcriptFinishedAt),
@@ -4517,6 +4503,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4517
4503
  : part.finish_reason;
4518
4504
  const usage = coreUsage(part.usage);
4519
4505
  mainUsage = addAgentWorkerUsage(mainUsage, usage);
4506
+ invocationTiming.lastModelFinishedAt = performance.now();
4520
4507
  await recordMainCall("completed", usage);
4521
4508
  modelSpan?.setAttribute("gen_ai.output.messages", JSON.stringify(toModelMessages([part.message])));
4522
4509
  if (usage.inputTokens !== undefined)
@@ -4607,6 +4594,9 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4607
4594
  throw new Error("Agent Worker model is missing.");
4608
4595
  const result = streamText({
4609
4596
  telemetry: agentModelTelemetry,
4597
+ onLanguageModelCallEnd() {
4598
+ invocationTiming.lastModelFinishedAt = performance.now();
4599
+ },
4610
4600
  onError: ({ error }) => {
4611
4601
  fail(error);
4612
4602
  },
@@ -4722,89 +4712,84 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4722
4712
  },
4723
4713
  });
4724
4714
  }
4725
- const durableStream = (sessionExecution ? coalesceAgentTextDeltas(stream) : stream).pipeThrough(new TransformStream({
4726
- async transform(chunk, controller) {
4727
- if (chunk.type === "start" && interruptedResponse) {
4728
- const events = [
4729
- ...(interruptedWait ? [interruptedWait] : []),
4730
- { type: "finish", finishReason: "other" },
4731
- ];
4732
- if (sessionExecution)
4733
- await mutateSession({
4734
- messages: [{ ...interruptedResponse }],
4735
- events,
4736
- });
4737
- else {
4738
- await appendSessionValue({
4739
- body: { message: interruptedResponse, runId: payload.runId },
4740
- chatId: payload.chatId,
4741
- namespace: input.sessionNamespace,
4742
- path: "/v1/messages",
4743
- });
4744
- for (const event of events)
4745
- await appendStreamEvent(event);
4746
- }
4747
- }
4748
- if (chunk.type === "start" && responseBaseline) {
4749
- // Each internal invocation still has its own start/finish for accounting.
4750
- // The public journal represents one response across those invocations.
4751
- controller.enqueue(chunk);
4752
- const result = input.inboxExecution?.waitResult;
4753
- if (result) {
4754
- const output = {
4755
- type: "tool-output-available",
4756
- toolCallId: result.toolCallId,
4757
- output: toAgentRunWaitOutput(result),
4758
- };
4759
- if (sessionExecution)
4760
- await mutateSession({ events: [output] });
4761
- else
4762
- await appendStreamEvent(output);
4763
- controller.enqueue(output);
4764
- }
4765
- return;
4766
- }
4767
- if (chunk.type === "tool-output-available" &&
4768
- chunk.toolCallId === inboxRunner?.suspendedToolCallId)
4769
- return;
4770
- if (chunk.type === "finish-step" || chunk.type === "finish") {
4771
- if (chunk.type === "finish")
4772
- await refreshFinalArtifacts();
4773
- else
4774
- await refreshArtifacts();
4775
- for (const data of publishedArtifacts.values()) {
4776
- if (emittedArtifacts.has(data.id))
4777
- continue;
4778
- const event = { type: "data-artifact", id: data.id, data };
4715
+ const durableStream = (sessionExecution
4716
+ ? batchAgentStreamEvents(coalesceAgentTextDeltas(stream))
4717
+ : stream.pipeThrough(new TransformStream({
4718
+ transform(chunk, controller) {
4719
+ controller.enqueue([chunk]);
4720
+ },
4721
+ }))).pipeThrough(new TransformStream({
4722
+ async transform(chunks, controller) {
4723
+ const events = [];
4724
+ for (let chunk of chunks) {
4725
+ if (chunk.type === "start" && interruptedResponse) {
4726
+ const events = [
4727
+ ...(interruptedWait ? [interruptedWait] : []),
4728
+ { type: "finish", finishReason: "other" },
4729
+ ];
4779
4730
  if (sessionExecution)
4780
- await mutateSession({ events: [event] });
4781
- else
4782
- await appendStreamEvent(event, `artifact:${data.id}`);
4783
- controller.enqueue(event);
4784
- emittedArtifacts.add(data.id);
4731
+ await mutateSession({
4732
+ messages: [{ ...interruptedResponse }],
4733
+ events,
4734
+ });
4735
+ else {
4736
+ await appendSessionValue({
4737
+ body: { message: interruptedResponse, runId: payload.runId },
4738
+ chatId: payload.chatId,
4739
+ namespace: input.sessionNamespace,
4740
+ path: "/v1/messages",
4741
+ });
4742
+ for (const event of events)
4743
+ await appendStreamEvent(event);
4744
+ }
4785
4745
  }
4786
- }
4787
- // AI SDK emits finish before awaiting onEnd. Publish success and usage
4788
- // only after the context strategy and its registered work have settled.
4789
- if (chunk.type === "finish") {
4790
- const metadata = await contextFinished;
4791
- chunk = {
4792
- ...chunk,
4793
- finishReason: metadata.finishReason,
4794
- messageMetadata: metadata,
4795
- };
4796
- if (metadata.sessionStatus === "waiting") {
4746
+ if (chunk.type === "start" && responseBaseline) {
4747
+ // Each internal invocation still has its own start/finish for accounting.
4748
+ // The public journal represents one response across those invocations.
4797
4749
  controller.enqueue(chunk);
4798
- return;
4750
+ const result = input.inboxExecution?.waitResult;
4751
+ if (result) {
4752
+ const output = {
4753
+ type: "tool-output-available",
4754
+ toolCallId: result.toolCallId,
4755
+ output: toAgentRunWaitOutput(result),
4756
+ };
4757
+ if (sessionExecution)
4758
+ await mutateSession({ events: [output] });
4759
+ else
4760
+ await appendStreamEvent(output);
4761
+ controller.enqueue(output);
4762
+ }
4763
+ continue;
4799
4764
  }
4765
+ if (chunk.type === "tool-output-available" &&
4766
+ chunk.toolCallId === inboxRunner?.suspendedToolCallId)
4767
+ continue;
4768
+ // AI SDK emits finish before awaiting onEnd. Publish success and usage
4769
+ // only after the context strategy and its registered work have settled.
4770
+ if (chunk.type === "finish") {
4771
+ const metadata = await contextFinished;
4772
+ chunk = {
4773
+ ...chunk,
4774
+ finishReason: metadata.finishReason,
4775
+ messageMetadata: metadata,
4776
+ };
4777
+ if (metadata.sessionStatus === "waiting") {
4778
+ controller.enqueue(chunk);
4779
+ continue;
4780
+ }
4781
+ }
4782
+ events.push(chunk);
4800
4783
  }
4801
- if (sessionExecution) {
4802
- await mutateSession({ events: [chunk] });
4803
- controller.enqueue(chunk);
4784
+ if (events.length === 0)
4804
4785
  return;
4805
- }
4806
- await appendStreamEvent(chunk);
4807
- controller.enqueue(chunk);
4786
+ if (sessionExecution)
4787
+ await mutateSession({ events });
4788
+ else
4789
+ for (const event of events)
4790
+ await appendStreamEvent(event);
4791
+ for (const event of events)
4792
+ controller.enqueue(event);
4808
4793
  },
4809
4794
  }));
4810
4795
  if (engine) {
@@ -195,21 +195,53 @@ export function createAgentContextRuntime(input) {
195
195
  });
196
196
  },
197
197
  async onEnd(stepNumber, end, runCallback = true) {
198
+ const endSpan = createAgentTracing().currentSpan;
198
199
  const finalize = async () => {
200
+ const startedAt = performance.now();
201
+ let backgroundMs = 0;
202
+ let callbackMs = 0;
203
+ let compactMs = 0;
199
204
  try {
200
- const updated = await compact(false);
201
- if (descriptor.type === "default" || offload !== false)
202
- await persist(updated);
203
- await settle();
204
- if (runCallback && strategy.onEnd)
205
- await invoke(stepNumber, (event) => strategy.onEnd({ ...event, ...end }));
205
+ const compactStartedAt = performance.now();
206
+ try {
207
+ const updated = await compact(false);
208
+ if (descriptor.type === "default" || offload !== false)
209
+ await persist(updated);
210
+ }
211
+ finally {
212
+ compactMs = performance.now() - compactStartedAt;
213
+ }
214
+ const backgroundStartedAt = performance.now();
215
+ try {
216
+ await settle();
217
+ }
218
+ finally {
219
+ backgroundMs += performance.now() - backgroundStartedAt;
220
+ }
221
+ if (runCallback && strategy.onEnd) {
222
+ const callbackStartedAt = performance.now();
223
+ try {
224
+ await invoke(stepNumber, (event) => strategy.onEnd({ ...event, ...end }));
225
+ }
226
+ finally {
227
+ callbackMs = performance.now() - callbackStartedAt;
228
+ }
229
+ }
206
230
  }
207
231
  finally {
232
+ const backgroundStartedAt = performance.now();
208
233
  try {
209
234
  await settle();
210
235
  }
211
236
  finally {
237
+ backgroundMs += performance.now() - backgroundStartedAt;
212
238
  closed = true;
239
+ endSpan?.setAttributes({
240
+ "gea.session.finish.context_compact_ms": Math.round(compactMs),
241
+ "gea.session.finish.context_background_ms": Math.round(backgroundMs),
242
+ "gea.session.finish.context_callback_ms": Math.round(callbackMs),
243
+ "gea.session.finish.context_total_ms": Math.round(performance.now() - startedAt),
244
+ });
213
245
  }
214
246
  }
215
247
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gea-ai/agent-sdk",
3
- "version": "0.1.260922-alpha.2",
3
+ "version": "0.1.260922-alpha.3",
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.2",
111
+ "@gea-ai/contract": "0.1.260922-alpha.3",
112
112
  "@opentelemetry/api": "1.9.1",
113
113
  "@opentelemetry/context-async-hooks": "2.10.0",
114
114
  "@opentelemetry/core": "2.10.0",