@gea-ai/agent-sdk 0.1.260922-alpha.2 → 0.1.260923-alpha.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.
@@ -1,9 +1,10 @@
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, agentSessionExecutionReleaseSchema, 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
- import { isAgentSessionInboxBusy, scheduleAgentSessionInbox, getAgentSessionRunState, handleAgentSessionInboxRequest, hasAgentSessionInbox, skipAgentSessionInputWait, } from "./agent-session-inbox.js";
7
+ import { isAgentSessionInboxBusy, scheduleAgentSessionInbox, getAgentSessionRunState, handleAgentSessionInboxRequest, finishAgentSessionInbox, hasAgentSessionInbox, skipAgentSessionInputWait, } from "./agent-session-inbox.js";
7
8
  // Managed execution owns its own durable state; PostgreSQL copies the live stream.
8
9
  const prefix = "execution-v2:";
9
10
  const key = (name) => `${prefix}${name}`;
@@ -424,6 +425,28 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
424
425
  });
425
426
  return json(preparing ? { ...result, capabilities } : result);
426
427
  }
428
+ if (request.method === "POST" && url.pathname === "/v2/turns/release") {
429
+ const input = agentSessionExecutionReleaseSchema.parse(await readBody(request, storage));
430
+ const startedAt = performance.now();
431
+ let handlerMs = 0;
432
+ const response = await storage.transaction(async (tx) => {
433
+ const handlerStartedAt = performance.now();
434
+ const active = await requireAttempt(tx, input);
435
+ const response = await finishAgentSessionInbox(tx, input.inbox, input.runId);
436
+ if (!response.ok)
437
+ return response;
438
+ await tx.put(key("active-turn"), {
439
+ ...active,
440
+ leaseOwner: "host",
441
+ leaseExpiresAt: new Date(Date.now() + AGENT_SESSION_EXECUTION_LEASE_MS).toISOString(),
442
+ });
443
+ await advanceRevision(tx);
444
+ handlerMs = performance.now() - handlerStartedAt;
445
+ return response;
446
+ });
447
+ response.headers.set("server-timing", `session-handler;dur=${handlerMs.toFixed(1)}, session-transaction;dur=${(performance.now() - startedAt).toFixed(1)}`);
448
+ return response;
449
+ }
427
450
  if (request.method === "POST" && url.pathname === "/v2/turns/renew") {
428
451
  const input = agentSessionExecutionRenewSchema.parse(await readBody(request, storage));
429
452
  return json(await storage.transaction(async (tx) => {
@@ -506,12 +529,39 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
506
529
  return { duplicate: false, turn, revision };
507
530
  }));
508
531
  }
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);
532
+ if (request.method === "POST" &&
533
+ (url.pathname === "/v2/turns/complete" ||
534
+ url.pathname === "/v2/turns/complete-stored")) {
535
+ const body = await readBody(request, storage);
536
+ const stored = url.pathname === "/v2/turns/complete-stored"
537
+ ? agentSessionExecutionCompleteStoredSchema.parse(body)
538
+ : undefined;
539
+ const { messageReferences: _references, ...storedValues } = stored ?? {};
540
+ const input = agentSessionExecutionCompleteSchema.parse(stored ? storedValues : body);
541
+ const storedRequestDigest = stored ? await digest(stored) : undefined;
542
+ const standardDigest = stored ? undefined : await digest(input);
512
543
  return json(await storage.transaction(async (tx) => {
513
544
  const receiptKey = key(`outcome:${encodeURIComponent(input.runId)}`);
514
545
  const existing = await tx.get(receiptKey);
546
+ if (stored && existing?.storedRequestDigest) {
547
+ if (existing.storedRequestDigest !== storedRequestDigest)
548
+ throw new SessionProtocolError("completion_mismatch");
549
+ return { duplicate: true, outcome: existing.outcome };
550
+ }
551
+ if (stored) {
552
+ for (const reference of stored.messageReferences) {
553
+ const sequence = await tx.get(key(`message-id:${encodeURIComponent(reference.id)}`));
554
+ const entry = sequence === undefined
555
+ ? undefined
556
+ : await getSessionPayload(tx, sequenceKey("message", sequence));
557
+ if (!entry ||
558
+ (await digestAgentSessionMessage(entry.message)) !==
559
+ reference.sha256)
560
+ throw new SessionProtocolError("stored_message_mismatch", 412);
561
+ input.messages.push(entry.message);
562
+ }
563
+ }
564
+ const inputDigest = standardDigest ?? (await digest(input));
515
565
  if (existing) {
516
566
  if (existing.digest !== inputDigest)
517
567
  throw new SessionProtocolError("completion_mismatch");
@@ -566,7 +616,7 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
566
616
  if (input.context && input.context.runId !== input.runId)
567
617
  throw new SessionProtocolError("context_run_mismatch");
568
618
  const previousRevision = await readRevision(tx);
569
- await writeValues(tx, input);
619
+ await writeValues(tx, stored ? { ...input, messages: [] } : input);
570
620
  const modelCalls = await tx.get(key(`model-calls:${encodeURIComponent(input.runId)}`));
571
621
  const outcome = {
572
622
  ...(modelCalls === undefined
@@ -586,6 +636,7 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
586
636
  await tx.put(receiptKey, {
587
637
  outcome,
588
638
  digest: inputDigest,
639
+ ...(storedRequestDigest ? { storedRequestDigest } : {}),
589
640
  });
590
641
  const rangeKey = key(`run-events:${encodeURIComponent(input.runId)}`);
591
642
  const storedRange = await tx.get(rangeKey);
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { agentSessionInboxFinishSchema } from "@gea-ai/contract/agent-session-inbox";
2
3
  import type { AgentChildRun } from "@gea-ai/contract/agent-worker-tools";
3
4
  import type { AgentSessionStorage, AgentSessionTransactionStorage } from "./agent-session";
4
5
  /** Only private definitions/self copies share the owning Agent's business context. */
@@ -47,6 +48,8 @@ export declare function queueAgentSessionChildRun(tx: AgentSessionTransactionSto
47
48
  export declare function acceptAgentSessionChildRun(tx: AgentSessionTransactionStorage, runId: string): Promise<void>;
48
49
  /** Called in the child-run transaction. Progress is recorded by the run store only. */
49
50
  export declare function notifyAgentSessionChildRun(tx: AgentSessionTransactionStorage, run: AgentChildRun): Promise<void>;
51
+ /** Called inside the managed lease handoff transaction. */
52
+ export declare function finishAgentSessionInbox(tx: AgentSessionTransactionStorage, input: z.infer<typeof agentSessionInboxFinishSchema>, runId: string): Promise<Response>;
50
53
  /** Private Session protocol. Ingress must authenticate initialization and sender provenance. */
51
54
  export declare function handleAgentSessionInboxRequest(request: Request, storage: AgentSessionStorage): Promise<Response | undefined>;
52
55
  /** Lazy Durable Object entrypoint. A pending dispatch retries with the same execution ID. */
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  import { agentSessionExecutionTurnSchema } from "@gea-ai/contract/agent-session";
4
4
  import { agentApplicationAssistantMessageSchema } from "@gea-ai/contract/agent-applications";
5
5
  import { canonicalAgentChannelJson } from "@gea-ai/contract/agent-channels";
6
- import { agentSessionIdentitySchema, agentSessionBootstrapSchema, agentSessionTransportSchema, agentSessionDispatchSchema, agentSessionInboxExecutionSchema, agentSessionSendSchema, agentRunWaitInputSchema, agentRunWaitResultSchema, toAgentRunWaitOutput, } from "@gea-ai/contract/agent-session-inbox";
6
+ import { agentSessionIdentitySchema, agentSessionBootstrapSchema, agentSessionTransportSchema, agentSessionDispatchSchema, agentSessionInboxFinishSchema, agentSessionInboxExecutionSchema, agentSessionSendSchema, agentRunWaitInputSchema, agentRunWaitResultSchema, toAgentRunWaitOutput, } from "@gea-ai/contract/agent-session-inbox";
7
7
  import { combineTraceContexts, readTraceContext } from "./trace-context.js";
8
8
  import { traceContextSchema } from "@gea-ai/contract/traces";
9
9
  import { activeComputerOperation } from "./agent-session-computer.js";
@@ -408,6 +408,88 @@ export async function notifyAgentSessionChildRun(tx, run) {
408
408
  }
409
409
  await save(tx, state, runs);
410
410
  }
411
+ async function finishInbox(tx, state, runs, input) {
412
+ if (!state.execution || state.execution.id !== input.executionId)
413
+ return Response.json({ error: "execution_mismatch" }, { status: 409 });
414
+ let { outcome, inputWait, requireExpiredLease } = input;
415
+ if (requireExpiredLease) {
416
+ const stored = await tx.get("execution-v2:active-turn");
417
+ const turn = stored === undefined
418
+ ? null
419
+ : agentSessionExecutionTurnSchema.parse(stored);
420
+ if (outcome?.status !== "interrupted" ||
421
+ turn?.runId !== state.execution.turnId ||
422
+ turn.status !== "running" ||
423
+ Date.parse(turn.leaseExpiresAt) > Date.now()) {
424
+ await save(tx, state, runs);
425
+ return Response.json({ error: "execution_not_expired" }, { status: 409 });
426
+ }
427
+ }
428
+ if (inputWait && !outcome && !state.stopped)
429
+ state.inputWait = {
430
+ turnId: state.execution.turnId,
431
+ messageId: inputWait.messageId,
432
+ };
433
+ // A transport disconnect does not revoke already accepted child runs.
434
+ if (outcome?.status === "cancelled" &&
435
+ !state.stopped &&
436
+ runs.some((run) => !run.outcome)) {
437
+ state.wait = {
438
+ turnId: state.execution.turnId,
439
+ toolCallId: null,
440
+ runIds: runs.filter((run) => !run.outcome).map((run) => run.id),
441
+ deadline: null,
442
+ };
443
+ outcome = undefined;
444
+ }
445
+ if (outcome && outcome.status !== "completed" && state.wait) {
446
+ if (state.wait.toolCallId)
447
+ state.resumeWait = waitResult(state, runs, true);
448
+ state.wait = null;
449
+ }
450
+ if (outcome && outcome.status !== "completed") {
451
+ state.stopped = true;
452
+ for (const run of runs)
453
+ if (!run.outcome) {
454
+ run.cancelRequested = true;
455
+ await putChildRun(tx, run);
456
+ }
457
+ }
458
+ if ((outcome || state.stopped) && state.transport) {
459
+ const terminal = {
460
+ execution: { ...state.execution, messages: [], waitResult: null },
461
+ outcome: outcome ?? {
462
+ status: "cancelled",
463
+ text: "Run cancelled.",
464
+ },
465
+ transport: state.transport,
466
+ };
467
+ if (runs.some((run) => !run.outcome))
468
+ state.draining = terminal;
469
+ else if (state.terminal)
470
+ state.terminalQueue.push(terminal);
471
+ else
472
+ state.terminal = terminal;
473
+ }
474
+ state.execution = null;
475
+ state.started = false;
476
+ await save(tx, state, runs);
477
+ return Response.json({
478
+ finished: true,
479
+ waiting: state.wait !== null || state.inputWait !== null,
480
+ });
481
+ }
482
+ /** Called inside the managed lease handoff transaction. */
483
+ export async function finishAgentSessionInbox(tx, input, runId) {
484
+ if (!tx.setAlarm || !tx.deleteAlarm)
485
+ return Response.json({ error: "session_alarms_unavailable" }, { status: 503 });
486
+ const state = await readState(tx);
487
+ if (!state?.execution ||
488
+ state.execution.id !== input.executionId ||
489
+ state.execution.turnId !== runId)
490
+ return Response.json({ error: "execution_mismatch" }, { status: 409 });
491
+ return finishInbox(tx, state, await readInboxChildRuns(tx, state), input);
492
+ }
411
493
  /** Private Session protocol. Ingress must authenticate initialization and sender provenance. */
412
494
  export async function handleAgentSessionInboxRequest(request, storage) {
413
495
  const path = new URL(request.url).pathname;
@@ -770,89 +852,18 @@ export async function handleAgentSessionInboxRequest(request, storage) {
770
852
  return Response.json(result ? { status: "ready", result } : { status: "suspended" });
771
853
  }
772
854
  if (finishing) {
773
- let { outcome, inputWait, requireExpiredLease } = executionInputSchema
774
- .extend({
775
- outcome: agentSessionDispatchSchema.shape.outcome,
776
- requireExpiredLease: z.literal(true).optional(),
777
- inputWait: z
778
- .strictObject({ messageId: z.string().min(1) })
779
- .optional(),
780
- })
855
+ const input = agentSessionInboxFinishSchema
856
+ .extend({ requireExpiredLease: z.literal(true).optional() })
781
857
  .parse(rejecting
782
858
  ? {
783
- executionId: input.executionId,
859
+ executionId: state.execution.id,
784
860
  outcome: {
785
861
  status: "failed",
786
862
  text: "Session dispatch authorization was denied.",
787
863
  },
788
864
  }
789
865
  : body);
790
- if (requireExpiredLease) {
791
- const stored = await tx.get("execution-v2:active-turn");
792
- const turn = stored === undefined
793
- ? null
794
- : agentSessionExecutionTurnSchema.parse(stored);
795
- if (outcome?.status !== "interrupted" ||
796
- turn?.runId !== state.execution.turnId ||
797
- turn.status !== "running" ||
798
- Date.parse(turn.leaseExpiresAt) > Date.now()) {
799
- await save(tx, state, runs);
800
- return Response.json({ error: "execution_not_expired" }, { status: 409 });
801
- }
802
- }
803
- if (inputWait && !outcome && !state.stopped)
804
- state.inputWait = {
805
- turnId: state.execution.turnId,
806
- messageId: inputWait.messageId,
807
- };
808
- // A transport disconnect does not revoke already accepted child runs.
809
- if (outcome?.status === "cancelled" &&
810
- !state.stopped &&
811
- runs.some((run) => !run.outcome)) {
812
- state.wait = {
813
- turnId: state.execution.turnId,
814
- toolCallId: null,
815
- runIds: runs.filter((run) => !run.outcome).map((run) => run.id),
816
- deadline: null,
817
- };
818
- outcome = undefined;
819
- }
820
- if (outcome && outcome.status !== "completed" && state.wait) {
821
- if (state.wait.toolCallId)
822
- state.resumeWait = waitResult(state, runs, true);
823
- state.wait = null;
824
- }
825
- if (outcome && outcome.status !== "completed") {
826
- state.stopped = true;
827
- for (const run of runs)
828
- if (!run.outcome) {
829
- run.cancelRequested = true;
830
- await putChildRun(tx, run);
831
- }
832
- }
833
- if ((outcome || state.stopped) && state.transport) {
834
- const terminal = {
835
- execution: { ...state.execution, messages: [], waitResult: null },
836
- outcome: outcome ?? {
837
- status: "cancelled",
838
- text: "Run cancelled.",
839
- },
840
- transport: state.transport,
841
- };
842
- if (runs.some((run) => !run.outcome))
843
- state.draining = terminal;
844
- else if (state.terminal)
845
- state.terminalQueue.push(terminal);
846
- else
847
- state.terminal = terminal;
848
- }
849
- state.execution = null;
850
- state.started = false;
851
- await save(tx, state, runs);
852
- return Response.json({
853
- finished: true,
854
- waiting: state.wait !== null || state.inputWait !== null,
855
- });
866
+ return finishInbox(tx, state, runs, input);
856
867
  }
857
868
  const messages = takeMessages(state, true);
858
869
  state.messages = state.messages.filter((entry) => !messages.includes(entry));
@@ -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,7 +2747,8 @@ export async function runAgentWorkerRequest(input) {
2748
2747
  clearTimeout(cancellationTimer);
2749
2748
  if (leaseMaintenance)
2750
2749
  await leaseMaintenance;
2751
- if (input.sessionExecution) {
2750
+ const leaseDrainedAt = performance.now();
2751
+ if (input.sessionExecution && !input.inboxExecution) {
2752
2752
  results.push(...(await Promise.allSettled([
2753
2753
  requestSessionJson({
2754
2754
  body: { ...input.sessionExecution, leaseOwner: "host" },
@@ -2764,33 +2764,62 @@ 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")
2773
2775
  ? "failed"
2774
2776
  : inboxOutcome.status;
2775
2777
  invocationSpan?.setAttribute("gea.session.status", status);
2778
+ const inbox = {
2779
+ executionId: input.inboxExecution.id,
2780
+ ...(status === "waiting"
2781
+ ? { inputWait: inboxOutcome.inputWait }
2782
+ : {
2783
+ outcome: {
2784
+ status,
2785
+ text: inboxOutcome.text.slice(0, 200_000),
2786
+ traceContext: invocationTraceContext,
2787
+ },
2788
+ }),
2789
+ };
2790
+ // Managed execution and Inbox are on the same Session object. Seal
2791
+ // the Inbox and hand its lease back in one fenced durable commit.
2792
+ const releasePath = input.sessionExecution
2793
+ ? "/v2/turns/release"
2794
+ : "/v1/inbox/finish";
2776
2795
  const sessionResponse = await input.sessionNamespace
2777
2796
  .get(input.sessionNamespace.idFromName(sessionChatId))
2778
- .fetch(new Request("https://agent-session.internal/v1/inbox/finish", {
2797
+ .fetch(new Request(`https://agent-session.internal${releasePath}`, {
2779
2798
  method: "POST",
2780
2799
  headers: { "content-type": "application/json" },
2781
- body: JSON.stringify({
2782
- executionId: input.inboxExecution.id,
2783
- ...(status === "waiting"
2784
- ? { inputWait: inboxOutcome.inputWait }
2785
- : {
2786
- outcome: {
2787
- status,
2788
- text: inboxOutcome.text.slice(0, 200_000),
2789
- traceContext: invocationTraceContext,
2790
- },
2791
- }),
2792
- }),
2793
- }));
2800
+ body: JSON.stringify(input.sessionExecution
2801
+ ? { ...input.sessionExecution, inbox }
2802
+ : inbox),
2803
+ }))
2804
+ .finally(() => invocationSpan?.setAttribute("gea.session.finish.inbox_ms", Math.round(performance.now() - inboxStartedAt)));
2805
+ for (const [metric, attribute] of [
2806
+ ["session-handler", "gea.session.finish.inbox_handler_ms"],
2807
+ [
2808
+ "session-transaction",
2809
+ "gea.session.finish.inbox_transaction_ms",
2810
+ ],
2811
+ ]) {
2812
+ const timing = sessionResponse.headers
2813
+ .get("server-timing")
2814
+ ?.split(",")
2815
+ .map((entry) => entry.trim())
2816
+ .find((entry) => entry.startsWith(`${metric};dur=`));
2817
+ if (timing) {
2818
+ const duration = Number(timing.slice(`${metric};dur=`.length));
2819
+ if (Number.isFinite(duration) && duration >= 0)
2820
+ invocationSpan?.setAttribute(attribute, duration);
2821
+ }
2822
+ }
2794
2823
  if (!sessionResponse.ok)
2795
2824
  throw new Error(`Session finish rejected (${sessionResponse.status}).`);
2796
2825
  }
@@ -2873,7 +2902,7 @@ export async function runAgentWorkerRequest(input) {
2873
2902
  stopComputer = stop;
2874
2903
  }, (outcome) => {
2875
2904
  inboxOutcome = outcome;
2876
- });
2905
+ }, invocationTiming);
2877
2906
  }
2878
2907
  catch (error) {
2879
2908
  cancellation.abort(error);
@@ -2917,6 +2946,8 @@ export async function runAgentWorkerRequest(input) {
2917
2946
  }
2918
2947
  signal.removeEventListener("abort", onAbort);
2919
2948
  await finish();
2949
+ if (invocationTiming.lastModelFinishedAt !== undefined)
2950
+ invocationSpan?.setAttribute("gea.session.finish.model_to_close_ms", Math.round(performance.now() - invocationTiming.lastModelFinishedAt));
2920
2951
  controller.close();
2921
2952
  }
2922
2953
  catch (error) {
@@ -2938,7 +2969,7 @@ export async function runAgentWorkerRequest(input) {
2938
2969
  });
2939
2970
  });
2940
2971
  }
2941
- async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessionOutcome) {
2972
+ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessionOutcome, invocationTiming) {
2942
2973
  const invocationTrace = activeTraceRef();
2943
2974
  const host = input.bindings
2944
2975
  ? createAgentWorkerHostAdapter(input.bindings)
@@ -2968,7 +2999,13 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
2968
2999
  let writeTail = Promise.resolve();
2969
3000
  let writeFailure;
2970
3001
  const persistenceSpan = trace.getActiveSpan();
2971
- const persistenceTotals = { writes: 0, queueMs: 0, requestMs: 0 };
3002
+ const persistenceTotals = {
3003
+ writes: 0,
3004
+ queueMs: 0,
3005
+ requestMs: 0,
3006
+ events: 0,
3007
+ eventBatches: 0,
3008
+ };
2972
3009
  if (sessionExecution) {
2973
3010
  if (sessionExecution.runId !== payload.runId)
2974
3011
  throw new Error("Session Run identity mismatch.");
@@ -3009,10 +3046,16 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3009
3046
  finally {
3010
3047
  const requestMs = performance.now() - startedAt;
3011
3048
  persistenceTotals.writes++;
3049
+ if (values.events?.length) {
3050
+ persistenceTotals.events += values.events.length;
3051
+ persistenceTotals.eventBatches++;
3052
+ }
3012
3053
  persistenceTotals.queueMs += queueMs;
3013
3054
  persistenceTotals.requestMs += requestMs;
3014
3055
  persistenceSpan?.setAttributes({
3015
3056
  "gea.session.persistence.writes": persistenceTotals.writes,
3057
+ "gea.session.persistence.events": persistenceTotals.events,
3058
+ "gea.session.persistence.event_batches": persistenceTotals.eventBatches,
3016
3059
  "gea.session.persistence.queue_ms": Math.round(persistenceTotals.queueMs),
3017
3060
  "gea.session.persistence.request_ms": Math.round(persistenceTotals.requestMs),
3018
3061
  });
@@ -3832,6 +3875,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3832
3875
  let mergedResponseMessageCount = 0;
3833
3876
  let contextCheckpoint;
3834
3877
  let reusedContextCheckpoints = 0;
3878
+ let modelCallsInitialized = false;
3835
3879
  const persistModelContext = () => {
3836
3880
  const modelContext = {
3837
3881
  contextSize: context.contextSize,
@@ -3848,7 +3892,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3848
3892
  return contextCheckpoint.pending;
3849
3893
  }
3850
3894
  const pending = sessionExecution
3851
- ? mutateSession({ context: modelContext })
3895
+ ? mutateSession({
3896
+ context: modelContext,
3897
+ ...(!modelCallsInitialized ? { modelCalls: [] } : {}),
3898
+ })
3852
3899
  : requestSessionJson({
3853
3900
  body: { context: modelContext },
3854
3901
  chatId: payload.chatId,
@@ -3857,6 +3904,8 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3857
3904
  path: "/v1/model-context",
3858
3905
  schema: agentSessionPutModelContextResultSchema,
3859
3906
  }).then(() => undefined);
3907
+ if (sessionExecution)
3908
+ modelCallsInitialized = true;
3860
3909
  contextCheckpoint = { serialized, pending };
3861
3910
  return pending;
3862
3911
  };
@@ -3902,8 +3951,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3902
3951
  await appendStreamEvent({ type: "data-model-call", data: call }, `model-call:${call.callId}`);
3903
3952
  }
3904
3953
  };
3905
- if (sessionExecution)
3954
+ if (sessionExecution && !modelCallsInitialized) {
3906
3955
  await mutateSession({ modelCalls: [] });
3956
+ modelCallsInitialized = true;
3957
+ }
3907
3958
  let contextSignal = input.request.signal;
3908
3959
  const contextRuntime = createAgentContextRuntime({
3909
3960
  ai: host,
@@ -4073,12 +4124,16 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4073
4124
  catch (error) {
4074
4125
  fail(error);
4075
4126
  }
4127
+ const finalContextStartedAt = performance.now();
4076
4128
  try {
4077
4129
  await persistModelContext();
4078
4130
  }
4079
4131
  catch (error) {
4080
4132
  fail(error);
4081
4133
  }
4134
+ finally {
4135
+ persistenceSpan?.setAttribute("gea.session.finish.final_context_ms", Math.round(performance.now() - finalContextStartedAt));
4136
+ }
4082
4137
  resolveContext({
4083
4138
  ...(!failure && inboxRunner?.suspended
4084
4139
  ? { sessionStatus: "waiting" }
@@ -4095,52 +4150,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4095
4150
  })();
4096
4151
  return settling;
4097
4152
  };
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
4153
  const saveResponseMessage = async ({ responseMessage, }) => {
4130
4154
  const saveStartedAt = performance.now();
4131
4155
  const metadata = await contextFinished;
4132
4156
  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
4157
  if (inboxRunner?.suspendedToolCallId) {
4145
4158
  responseMessage.parts = responseMessage.parts.map((part) => {
4146
4159
  if (isToolUIPart(part) &&
@@ -4178,7 +4191,6 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4178
4191
  await persistModelContext();
4179
4192
  const contextPersistedAt = performance.now();
4180
4193
  persistenceSpan?.setAttributes({
4181
- "gea.session.finish.artifacts_ms": Math.round(artifactsFinishedAt - contextFinishedAt),
4182
4194
  "gea.session.finish.context_wait_ms": Math.round(contextFinishedAt - saveStartedAt),
4183
4195
  "gea.session.finish.transcript_ms": Math.round(transcriptFinishedAt - transcriptStartedAt),
4184
4196
  "gea.session.finish.context_write_ms": Math.round(contextPersistedAt - transcriptFinishedAt),
@@ -4517,6 +4529,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4517
4529
  : part.finish_reason;
4518
4530
  const usage = coreUsage(part.usage);
4519
4531
  mainUsage = addAgentWorkerUsage(mainUsage, usage);
4532
+ invocationTiming.lastModelFinishedAt = performance.now();
4520
4533
  await recordMainCall("completed", usage);
4521
4534
  modelSpan?.setAttribute("gen_ai.output.messages", JSON.stringify(toModelMessages([part.message])));
4522
4535
  if (usage.inputTokens !== undefined)
@@ -4607,6 +4620,9 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4607
4620
  throw new Error("Agent Worker model is missing.");
4608
4621
  const result = streamText({
4609
4622
  telemetry: agentModelTelemetry,
4623
+ onLanguageModelCallEnd() {
4624
+ invocationTiming.lastModelFinishedAt = performance.now();
4625
+ },
4610
4626
  onError: ({ error }) => {
4611
4627
  fail(error);
4612
4628
  },
@@ -4722,89 +4738,84 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4722
4738
  },
4723
4739
  });
4724
4740
  }
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 };
4741
+ const durableStream = (sessionExecution
4742
+ ? batchAgentStreamEvents(coalesceAgentTextDeltas(stream))
4743
+ : stream.pipeThrough(new TransformStream({
4744
+ transform(chunk, controller) {
4745
+ controller.enqueue([chunk]);
4746
+ },
4747
+ }))).pipeThrough(new TransformStream({
4748
+ async transform(chunks, controller) {
4749
+ const events = [];
4750
+ for (let chunk of chunks) {
4751
+ if (chunk.type === "start" && interruptedResponse) {
4752
+ const events = [
4753
+ ...(interruptedWait ? [interruptedWait] : []),
4754
+ { type: "finish", finishReason: "other" },
4755
+ ];
4779
4756
  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);
4757
+ await mutateSession({
4758
+ messages: [{ ...interruptedResponse }],
4759
+ events,
4760
+ });
4761
+ else {
4762
+ await appendSessionValue({
4763
+ body: { message: interruptedResponse, runId: payload.runId },
4764
+ chatId: payload.chatId,
4765
+ namespace: input.sessionNamespace,
4766
+ path: "/v1/messages",
4767
+ });
4768
+ for (const event of events)
4769
+ await appendStreamEvent(event);
4770
+ }
4785
4771
  }
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") {
4772
+ if (chunk.type === "start" && responseBaseline) {
4773
+ // Each internal invocation still has its own start/finish for accounting.
4774
+ // The public journal represents one response across those invocations.
4797
4775
  controller.enqueue(chunk);
4798
- return;
4776
+ const result = input.inboxExecution?.waitResult;
4777
+ if (result) {
4778
+ const output = {
4779
+ type: "tool-output-available",
4780
+ toolCallId: result.toolCallId,
4781
+ output: toAgentRunWaitOutput(result),
4782
+ };
4783
+ if (sessionExecution)
4784
+ await mutateSession({ events: [output] });
4785
+ else
4786
+ await appendStreamEvent(output);
4787
+ controller.enqueue(output);
4788
+ }
4789
+ continue;
4799
4790
  }
4791
+ if (chunk.type === "tool-output-available" &&
4792
+ chunk.toolCallId === inboxRunner?.suspendedToolCallId)
4793
+ continue;
4794
+ // AI SDK emits finish before awaiting onEnd. Publish success and usage
4795
+ // only after the context strategy and its registered work have settled.
4796
+ if (chunk.type === "finish") {
4797
+ const metadata = await contextFinished;
4798
+ chunk = {
4799
+ ...chunk,
4800
+ finishReason: metadata.finishReason,
4801
+ messageMetadata: metadata,
4802
+ };
4803
+ if (metadata.sessionStatus === "waiting") {
4804
+ controller.enqueue(chunk);
4805
+ continue;
4806
+ }
4807
+ }
4808
+ events.push(chunk);
4800
4809
  }
4801
- if (sessionExecution) {
4802
- await mutateSession({ events: [chunk] });
4803
- controller.enqueue(chunk);
4810
+ if (events.length === 0)
4804
4811
  return;
4805
- }
4806
- await appendStreamEvent(chunk);
4807
- controller.enqueue(chunk);
4812
+ if (sessionExecution)
4813
+ await mutateSession({ events });
4814
+ else
4815
+ for (const event of events)
4816
+ await appendStreamEvent(event);
4817
+ for (const event of events)
4818
+ controller.enqueue(event);
4808
4819
  },
4809
4820
  }));
4810
4821
  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
  };
@@ -13,7 +13,7 @@ export async function* projectAgentCoreEvents(turn, options) {
13
13
  let resumed = false;
14
14
  const blocks = new Map();
15
15
  const calls = new Map();
16
- const steps = [];
16
+ let lastModelHadToolCalls = false;
17
17
  let outcome;
18
18
  try {
19
19
  yield {
@@ -38,7 +38,7 @@ export async function* projectAgentCoreEvents(turn, options) {
38
38
  case "step_resumed":
39
39
  step = event.step;
40
40
  resumed = true;
41
- steps.push({ step, finish_reason: "tool_calls", usage: event.usage });
41
+ lastModelHadToolCalls = true;
42
42
  break;
43
43
  case "step_started":
44
44
  resumed = false;
@@ -113,11 +113,7 @@ export async function* projectAgentCoreEvents(turn, options) {
113
113
  break;
114
114
  }
115
115
  case "model_finished":
116
- steps.push({
117
- step: event.step,
118
- finish_reason: event.finish_reason,
119
- usage: event.usage,
120
- });
116
+ lastModelHadToolCalls = event.finish_reason === "tool_calls";
121
117
  break;
122
118
  case "approval_requested": {
123
119
  const call = calls.get(event.call_id);
@@ -237,12 +233,7 @@ export async function* projectAgentCoreEvents(turn, options) {
237
233
  if (chunk)
238
234
  yield chunk;
239
235
  }
240
- const messageMetadata = {
241
- ...(outcome.output === undefined ? {} : { output: outcome.output }),
242
- agentCore: { reason: outcome.reason, steps },
243
- };
244
236
  if (outcome.reason === "cancelled") {
245
- yield { type: "message-metadata", messageMetadata };
246
237
  yield { type: "abort", reason: "cancelled" };
247
238
  return;
248
239
  }
@@ -259,7 +250,7 @@ export async function* projectAgentCoreEvents(turn, options) {
259
250
  yield {
260
251
  type: "finish",
261
252
  finishReason: outcome.reason === "stopped"
262
- ? steps.at(-1)?.finish_reason === "tool_calls"
253
+ ? lastModelHadToolCalls
263
254
  ? "tool-calls"
264
255
  : "stop"
265
256
  : outcome.reason === "completed"
@@ -271,7 +262,9 @@ export async function* projectAgentCoreEvents(turn, options) {
271
262
  outcome.reason === "awaiting_tool_results"
272
263
  ? "tool-calls"
273
264
  : "error",
274
- messageMetadata,
265
+ ...(outcome.output === undefined
266
+ ? {}
267
+ : { messageMetadata: { output: outcome.output } }),
275
268
  };
276
269
  }
277
270
  export function createAgentCoreUIStream(core, message, host, options) {
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.260923-alpha.0",
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.260923-alpha.0",
112
112
  "@opentelemetry/api": "1.9.1",
113
113
  "@opentelemetry/context-async-hooks": "2.10.0",
114
114
  "@opentelemetry/core": "2.10.0",