@gea-ai/agent-sdk 0.1.260920-alpha.1 → 0.1.260920-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.
package/README.md CHANGED
@@ -19,6 +19,16 @@ provider configuration, or environment values.
19
19
  `toModelOutput({ toolCallId, input, output })`, following AI SDK's separation
20
20
  between the result used by the application and the result shown to the model.
21
21
  Both server and client tools retain output types in `InferAgentUITools`.
22
+ Both the default Agent Core engine and `aiSdk()` support client Tools without
23
+ `execute`. A client Tool waits for an `output-available` or `output-error` part
24
+ on the original assistant message and Tool call ID. Submit all pending client
25
+ results/approval decisions together through the existing continuation flow.
26
+ Core pauses server handlers in the same batch until those interactions complete,
27
+ following its existing whole-batch approval behavior.
28
+ Tool results resume the same Run while its interaction remains pending.
29
+ Waiting for client input releases execution: a new user message skips the old
30
+ interaction and starts a new Run in the same Session, without an explicit cancel.
31
+ Skipped calls become errors in the transcript/model context; late results are rejected.
22
32
 
23
33
  `outputSchema` is a declaration/type hint. It never parses, coerces or validates
24
34
  returned data, including client-supplied results. It is serialized in the Agent
Binary file
@@ -3,7 +3,7 @@ import { agentModelCallsSchema, } from "@gea-ai/contract/agent-usage";
3
3
  import { AGENT_SESSION_COMPUTER_LEASE_MS, 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";
4
4
  import { z } from "zod";
5
5
  import { activeComputerOperation } from "./agent-session-computer.js";
6
- import { isAgentSessionInboxBusy, scheduleAgentSessionInbox, getAgentSessionRunState, handleAgentSessionInboxRequest, hasAgentSessionInbox, } from "./agent-session-inbox.js";
6
+ import { isAgentSessionInboxBusy, scheduleAgentSessionInbox, getAgentSessionRunState, handleAgentSessionInboxRequest, hasAgentSessionInbox, skipAgentSessionInputWait, } from "./agent-session-inbox.js";
7
7
  // Managed execution owns its own durable state; PostgreSQL copies the live stream.
8
8
  const prefix = "execution-v2:";
9
9
  const key = (name) => `${prefix}${name}`;
@@ -11,6 +11,8 @@ const sequenceKey = (name, sequence) => key(`${name}:${sequence.toString().padSt
11
11
  const runEventRangeSchema = z.strictObject({
12
12
  start: z.number().int().nonnegative(),
13
13
  end: z.number().int().nonnegative().nullable(),
14
+ messageId: z.string().optional(),
15
+ messageStart: z.number().int().nonnegative().optional(),
14
16
  });
15
17
  class SessionProtocolError extends Error {
16
18
  reason;
@@ -100,6 +102,21 @@ async function writeValues(tx, input) {
100
102
  await tx.put(key("message-sequence"), messageSequence);
101
103
  let eventSequence = (await tx.get(key("event-sequence"))) ?? 0;
102
104
  for (const event of input.events) {
105
+ if (event.type === "start" &&
106
+ typeof event.messageId === "string" &&
107
+ input.runId) {
108
+ const rangeKey = key(`run-events:${encodeURIComponent(input.runId)}`);
109
+ const stored = await tx.get(rangeKey);
110
+ if (stored !== undefined) {
111
+ const range = runEventRangeSchema.parse(stored);
112
+ if (range.messageId !== event.messageId)
113
+ await tx.put(rangeKey, {
114
+ ...range,
115
+ messageId: event.messageId,
116
+ messageStart: eventSequence,
117
+ });
118
+ }
119
+ }
103
120
  const entry = { event, sequence: ++eventSequence };
104
121
  await putSessionPayload(tx, sequenceKey("event", entry.sequence), entry);
105
122
  }
@@ -449,6 +466,14 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
449
466
  return { duplicate: false, outcome };
450
467
  }));
451
468
  }
469
+ if (request.method === "POST" && url.pathname === "/v2/turns/skip-input") {
470
+ const input = z
471
+ .strictObject({ runId: z.uuid() })
472
+ .parse(await readBody(request));
473
+ return json(await storage.transaction(async (tx) => ({
474
+ requested: await skipAgentSessionInputWait(tx, input.runId),
475
+ })));
476
+ }
452
477
  if (request.method === "POST" && url.pathname === "/v2/turns/cancel") {
453
478
  const input = z
454
479
  .object({ runId: agentSessionAttemptIdentitySchema.shape.runId })
@@ -497,6 +522,14 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
497
522
  return { duplicate: false, revision, turn };
498
523
  }));
499
524
  }
525
+ const messageMatch = /^\/v2\/messages\/([^/]+)$/u.exec(url.pathname);
526
+ if (request.method === "GET" && messageMatch) {
527
+ const id = decodeURIComponent(messageMatch[1]);
528
+ const sequence = await storage.get(key(`message-id:${encodeURIComponent(id)}`));
529
+ if (sequence === undefined)
530
+ throw new SessionProtocolError("message_not_found", 404);
531
+ return json(await storage.transaction((tx) => getSessionPayload(tx, sequenceKey("message", sequence))));
532
+ }
500
533
  if (request.method === "GET" && url.pathname === "/v2/model-context")
501
534
  return json({
502
535
  context: (await storage.transaction((tx) => getSessionPayload(tx, key("model-context")))) ?? null,
@@ -520,7 +553,10 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
520
553
  const end = range.end ?? (await tx.get(key("event-sequence"))) ?? 0;
521
554
  if (after > end || (after !== 0 && after < range.start))
522
555
  throw new SessionProtocolError("invalid_cursor", 400);
523
- const start = Math.max(after, range.start);
556
+ const start = Math.max(after, !url.searchParams.has("after") &&
557
+ url.searchParams.get("latestMessage") === "true"
558
+ ? (range.messageStart ?? range.start)
559
+ : range.start);
524
560
  const events = await entries(tx, "event", start, Math.min(limit, end - start));
525
561
  const nextCursor = events.at(-1)?.sequence ?? start;
526
562
  const receipt = await tx.get(key(`outcome:${encodeURIComponent(runId)}`));
@@ -1,5 +1,7 @@
1
1
  import { type AgentChildRun } from "@gea-ai/contract/agent-worker-tools";
2
2
  import type { AgentSessionStorage, AgentSessionTransactionStorage } from "./agent-session";
3
+ /** Atomically supersede only an idle input wait, never a running invocation. */
4
+ export declare function skipAgentSessionInputWait(tx: AgentSessionTransactionStorage, runId: string): Promise<boolean>;
3
5
  export declare function hasAgentSessionInbox(tx: AgentSessionTransactionStorage): Promise<boolean>;
4
6
  /** Read the logical Run independently of the currently leased invocation. */
5
7
  export declare function getAgentSessionRunState(tx: AgentSessionTransactionStorage, runId: string): Promise<{
@@ -7,6 +7,7 @@ import { agentChildRunSchema, } from "@gea-ai/contract/agent-worker-tools";
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";
10
+ import { getSessionPayload, putSessionPayload } from "./agent-session-payload.js";
10
11
  const stateKey = "inbox:v1";
11
12
  const waitSchema = agentRunWaitInputSchema.omit({ timeoutMs: true }).extend({
12
13
  turnId: z.uuid(),
@@ -39,6 +40,82 @@ const stateSchema = z.strictObject({
39
40
  terminalQueue: z.array(terminalSchema).max(8).default([]),
40
41
  });
41
42
  const executionInputSchema = z.strictObject({ executionId: z.uuid() });
43
+ async function skipInputWait(tx, state, runs) {
44
+ const pending = state.inputWait;
45
+ if (!pending || !state.transport)
46
+ return false;
47
+ // Both journals retain the same assistant ID. Seal visible pending parts as
48
+ // well as the Run so a late client result cannot resurrect the interaction.
49
+ for (const prefix of ["", "execution-v2:"]) {
50
+ const sequence = await tx.get(`${prefix}message-id:${encodeURIComponent(pending.messageId)}`);
51
+ if (sequence === undefined)
52
+ continue;
53
+ const key = `${prefix}message:${String(sequence).padStart(16, "0")}`;
54
+ const stored = await getSessionPayload(tx, key);
55
+ const envelope = prefix
56
+ ? z
57
+ .object({ message: agentApplicationAssistantMessageSchema })
58
+ .passthrough()
59
+ .parse(stored)
60
+ : null;
61
+ const message = envelope?.message ?? agentApplicationAssistantMessageSchema.parse(stored);
62
+ const skipped = {
63
+ ...message,
64
+ parts: message.parts.map((part) => (part.type === "dynamic-tool" || part.type.startsWith("tool-")) &&
65
+ [
66
+ "input-streaming",
67
+ "input-available",
68
+ "approval-requested",
69
+ "approval-responded",
70
+ ].includes(String(part.state))
71
+ ? {
72
+ ...part,
73
+ state: "output-error",
74
+ errorText: "Tool interaction was skipped by a new user message; no result was provided.",
75
+ }
76
+ : part),
77
+ };
78
+ await putSessionPayload(tx, key, envelope ? { ...envelope, message: skipped } : skipped);
79
+ }
80
+ const terminal = {
81
+ execution: {
82
+ id: pending.turnId,
83
+ turnId: pending.turnId,
84
+ messages: [],
85
+ waitResult: null,
86
+ },
87
+ outcome: {
88
+ status: "interrupted",
89
+ text: "Pending tool interaction skipped by a new user message.",
90
+ },
91
+ transport: state.transport,
92
+ };
93
+ state.inputWait = null;
94
+ if (runs.some((run) => !run.outcome)) {
95
+ state.stopped = true;
96
+ for (const run of runs)
97
+ if (!run.outcome)
98
+ run.cancelRequested = true;
99
+ await tx.put("child-runs", { runs });
100
+ state.draining = terminal;
101
+ }
102
+ else if (state.terminal)
103
+ state.terminalQueue.push(terminal);
104
+ else
105
+ state.terminal = terminal;
106
+ await save(tx, state, runs);
107
+ return true;
108
+ }
109
+ /** Atomically supersede only an idle input wait, never a running invocation. */
110
+ export async function skipAgentSessionInputWait(tx, runId) {
111
+ const state = await readState(tx);
112
+ if (!state ||
113
+ state.execution ||
114
+ state.inputWait?.turnId !== runId ||
115
+ state.terminalQueue.length >= 8)
116
+ return false;
117
+ return skipInputWait(tx, state, await readChildRuns(tx));
118
+ }
42
119
  // A turn admits a bounded batch. Messages beyond it retain their original order.
43
120
  function takeMessages(state, steeringOnly) {
44
121
  const messages = [];
@@ -219,7 +296,9 @@ async function save(tx, state, runs) {
219
296
  });
220
297
  const ready = state.wait
221
298
  ? waitResult(state, runs) !== null
222
- : !state.inputWait && state.messages.length > 0;
299
+ : state.inputWait
300
+ ? state.messages.some((message) => message.sender.type === "user")
301
+ : state.messages.length > 0;
223
302
  const cancelling = runs.some((run) => !run.outcome && run.cancelRequested);
224
303
  state.pendingStarts = state.pendingStarts.filter((id) => runs.some((run) => run.id === id && !run.outcome && !run.cancelRequested));
225
304
  const storedTurn = state.execution
@@ -352,14 +431,20 @@ export async function handleAgentSessionInboxRequest(request, storage) {
352
431
  state.terminalQueue.length >= 8 ||
353
432
  (state.wait && input.message?.mode !== "steering") ||
354
433
  (state.inputWait &&
355
- input.continuation?.id !== state.inputWait.messageId))
434
+ input.continuation &&
435
+ input.continuation.id !== state.inputWait.messageId))
356
436
  return Response.json({ error: "session_busy" }, { status: 409 });
357
- state.transport = input.transport;
358
437
  const previous = input.message
359
438
  ? await tx.get(`inbox:message:${input.message.id}`)
360
439
  : undefined;
361
440
  if (previous !== undefined)
362
441
  return Response.json({ error: "message_already_accepted" }, { status: 409 });
442
+ if (input.message && state.inputWait) {
443
+ await skipInputWait(tx, state, runs);
444
+ if (state.draining)
445
+ return Response.json({ error: "session_busy" }, { status: 409 });
446
+ }
447
+ state.transport = input.transport;
363
448
  let resumedWait = state.resumeWait;
364
449
  if (state.wait && input.message) {
365
450
  state.messages.push(input.message);
@@ -458,6 +543,12 @@ export async function handleAgentSessionInboxRequest(request, storage) {
458
543
  await save(tx, state, runs);
459
544
  return Response.json({ execution: null });
460
545
  }
546
+ if (state.inputWait &&
547
+ state.messages.some((message) => message.sender.type === "user") &&
548
+ !state.execution &&
549
+ !state.draining &&
550
+ state.terminalQueue.length < 8)
551
+ await skipInputWait(tx, state, runs);
461
552
  if (state.execution ||
462
553
  state.terminal ||
463
554
  state.draining ||
@@ -468,6 +559,7 @@ export async function handleAgentSessionInboxRequest(request, storage) {
468
559
  return Response.json({ execution: null });
469
560
  const messages = takeMessages(state, state.wait !== null);
470
561
  const joining = state.wait?.toolCallId === null;
562
+ state.stopped = false;
471
563
  state.execution = {
472
564
  id: crypto.randomUUID(),
473
565
  turnId: state.wait?.turnId ?? crypto.randomUUID(),
@@ -4,9 +4,10 @@ import { UI_MESSAGE_STREAM_HEADERS } from "ai";
4
4
  export async function streamManagedAgentSessionRun(request, session) {
5
5
  const url = new URL(request.url);
6
6
  url.pathname = "/v2/run-events";
7
- // Public reconnects replay this Run from the beginning, as the API contract specifies.
7
+ // Replay the active response. User steering starts a new response within the same Run.
8
8
  url.searchParams.delete("after");
9
9
  url.searchParams.delete("limit");
10
+ url.searchParams.set("latestMessage", "true");
10
11
  const abort = new AbortController();
11
12
  let cancelled = false;
12
13
  const onDisconnect = () => abort.abort(request.signal.reason);
@@ -34,14 +35,28 @@ export async function streamManagedAgentSessionRun(request, session) {
34
35
  return new Response(null, { status: 204 });
35
36
  }
36
37
  const encoder = new TextEncoder();
38
+ let messageId;
37
39
  return new Response(new ReadableStream({
38
40
  async pull(controller) {
39
41
  try {
40
42
  while (!abort.signal.aborted) {
41
43
  if (page.events.length) {
42
- controller.enqueue(encoder.encode(page.events
43
- .map(({ event }) => `data: ${JSON.stringify(event)}\n\n`)
44
- .join("")));
44
+ let text = "";
45
+ for (const { event } of page.events) {
46
+ if (event.type === "start" &&
47
+ typeof event.messageId === "string") {
48
+ if (messageId && messageId !== event.messageId) {
49
+ // The old subscriber must never feed a second response into useChat's reducer.
50
+ controller.enqueue(encoder.encode(text + "data: [DONE]\n\n"));
51
+ controller.close();
52
+ dispose();
53
+ return;
54
+ }
55
+ messageId = event.messageId;
56
+ }
57
+ text += `data: ${JSON.stringify(event)}\n\n`;
58
+ }
59
+ controller.enqueue(encoder.encode(text));
45
60
  page = { ...page, events: [] };
46
61
  return;
47
62
  }
@@ -29,7 +29,7 @@ import { toAgentHttpFile, } from "@gea-ai/contract/agent-http-api";
29
29
  import { handleHostedAgentHttp } from "./agent-http.js";
30
30
  import { createWorkerAgentTool, createWorkerRunCancelTool } from "./agent-call.js";
31
31
  import { AgentSessionRunner, resumeSessionWait } from "./agent-session-runner.js";
32
- import { agentRunWaitInputSchema, agentSessionInboxExecutionSchema, sessionExecutionMessage, } from "@gea-ai/contract/agent-session-inbox";
32
+ import { agentRunWaitInputSchema, toAgentRunWaitOutput, agentSessionInboxExecutionSchema, sessionExecutionMessage, } from "@gea-ai/contract/agent-session-inbox";
33
33
  import { agentWorkerInvocationIdentitySchema } from "@gea-ai/contract/agent-worker-tools";
34
34
  import { MAX_AGENT_ARTIFACT_IMAGES, MAX_AGENT_ARTIFACT_IMAGE_BYTES, studioArtifactInputFileSchema, studioArtifactCreateUploadSchema, studioArtifactListSchema, } from "@gea-ai/contract/studio-artifacts";
35
35
  import { parsePatch } from "./vendor/codex-patch/parser.js";
@@ -42,7 +42,7 @@ import { AGENT_APPLICATION_PREFIX, buildAgentApplicationPath, agentApplicationAs
42
42
  import { AGENT_APPLICATION_CHAT_ID_HEADER, AGENT_APPLICATION_MESSAGE_ID_HEADER, AGENT_APPLICATION_RUN_ID_HEADER, agentApplicationKeySchema, agentApplicationRunRequestSchema, } from "@gea-ai/contract";
43
43
  import { AGENT_CHANNEL_EXECUTION_TIMEOUT_MS, agentChannelInvocationContextSchema, agentChannelAddressSchema, agentChannelHttpMessageRequestSchema, agentChannelOpaqueIdSchema, agentChannelProjectTerminalCommandSchema, agentChannelRunTerminalCallbackSchema, agentChannelStartRunCommandSchema, } from "@gea-ai/contract/agent-channels";
44
44
  import { agentSessionClaimChannelRunResultSchema, agentSessionCompleteChannelRunResultSchema, agentSessionGetModelContextResultSchema, agentSessionAttemptIdentitySchema, agentSessionExecutionStartResultSchema, agentSessionExecutionMutationResultSchema, agentSessionPutModelContextResultSchema, } from "@gea-ai/contract/agent-session";
45
- import { InvalidResponseDataError, convertToModelMessages, isToolUIPart, createUIMessageStreamResponse, createUIMessageStream, asSchema, jsonSchema, stepCountIs, streamText, tool, } from "ai";
45
+ import { InvalidResponseDataError, convertToModelMessages, isToolUIPart, validateUIMessages, createUIMessageStreamResponse, createUIMessageStream, asSchema, jsonSchema, stepCountIs, streamText, tool, } from "ai";
46
46
  import { agentWorkerConnectionDeleteOutputSchema, agentWorkerConnectionGetOutputSchema, agentWorkerConnectionSetOutputSchema, agentWorkerToolInitializationSchema, MAX_AGENT_WORKER_TOOLS, } from "@gea-ai/contract/agent-worker-tools";
47
47
  import { agentToolIdentitySchema, } from "@gea-ai/contract/agent-packages";
48
48
  import { z } from "zod";
@@ -475,7 +475,8 @@ async function runPreparedAgent(runtime, agentKey, request, environment, identif
475
475
  const headers = new Headers(response.headers);
476
476
  headers.set(AGENT_APPLICATION_CHAT_ID_HEADER, chatId);
477
477
  headers.set(AGENT_APPLICATION_RUN_ID_HEADER, runId);
478
- headers.set(AGENT_APPLICATION_MESSAGE_ID_HEADER, responseMessageId);
478
+ headers.set(AGENT_APPLICATION_MESSAGE_ID_HEADER, response.headers.get(AGENT_APPLICATION_MESSAGE_ID_HEADER) ??
479
+ responseMessageId);
479
480
  return new Response(response.body, {
480
481
  headers,
481
482
  status: response.status,
@@ -3404,6 +3405,14 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3404
3405
  path: sessionExecution ? "/v2/model-context" : "/v1/model-context",
3405
3406
  schema: agentSessionGetModelContextResultSchema,
3406
3407
  });
3408
+ if (submittedUser &&
3409
+ storedContext.context &&
3410
+ storedContext.context.runId !== payload.runId) {
3411
+ // A new user Run supersedes the previous idle tool interaction. Close its
3412
+ // missing results before appending user input; never execute the skipped batch.
3413
+ storedContext.context.messages = closeInterruptedToolCalls(storedContext.context.messages.map(decodeAgentModelProxyValue), "Tool interaction was skipped by a new user message; no result was provided.").map(encodeAgentModelProxyValue);
3414
+ delete storedContext.context.metadata.agentCore;
3415
+ }
3407
3416
  // Reuse durable conversions when UI history is replayed, so file conversion
3408
3417
  // does not upload another Artifact on every turn or approval continuation.
3409
3418
  const retainedToolCalls = new Set();
@@ -3420,6 +3429,66 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3420
3429
  modelOutputs.set(part.toolCallId, part.output);
3421
3430
  }
3422
3431
  }
3432
+ const suspendedMessageId = storedContext.context?.runId === payload.runId
3433
+ ? storedContext.context.metadata.suspendedMessageId
3434
+ : undefined;
3435
+ const resumesResponse = Boolean(suspendedMessageId &&
3436
+ input.inboxExecution &&
3437
+ !input.inboxExecution.messages.some((message) => message.sender.type === "user"));
3438
+ let responseBaseline;
3439
+ let interruptedResponse;
3440
+ let interruptedWait;
3441
+ if (suspendedMessageId && input.inboxExecution) {
3442
+ const saved = await requestSessionJson({
3443
+ chatId: payload.chatId,
3444
+ method: "GET",
3445
+ namespace: input.sessionNamespace,
3446
+ path: `${sessionExecution ? "/v2" : "/v1"}/messages/${encodeURIComponent(suspendedMessageId)}`,
3447
+ schema: z.object({ message: z.unknown() }),
3448
+ });
3449
+ const [message] = await validateUIMessages({
3450
+ messages: [saved.message],
3451
+ });
3452
+ if (!message || message.role !== "assistant")
3453
+ throw new Error("Suspended response is not an assistant message.");
3454
+ // Invocation completion is not message completion. Do not retain its waiting metadata.
3455
+ if (message.metadata) {
3456
+ delete message.metadata.sessionStatus;
3457
+ delete message.metadata.finishedAt;
3458
+ delete message.metadata.finishReason;
3459
+ }
3460
+ const result = input.inboxExecution.waitResult;
3461
+ if (result) {
3462
+ const part = message.parts.find((part) => isToolUIPart(part) && part.toolCallId === result.toolCallId);
3463
+ if (!part || !isToolUIPart(part))
3464
+ throw new Error("Suspended response is missing its wait tool.");
3465
+ if (resumesResponse) {
3466
+ Object.assign(part, {
3467
+ state: "output-available",
3468
+ output: toAgentRunWaitOutput(result),
3469
+ });
3470
+ }
3471
+ else {
3472
+ const errorText = "Wait interrupted by a new user message.";
3473
+ Object.assign(part, { state: "output-error", errorText });
3474
+ interruptedWait = {
3475
+ type: "tool-output-error",
3476
+ toolCallId: result.toolCallId,
3477
+ errorText,
3478
+ };
3479
+ }
3480
+ }
3481
+ if (resumesResponse)
3482
+ responseBaseline = message;
3483
+ else {
3484
+ message.metadata = {
3485
+ ...message.metadata,
3486
+ finishedAt: new Date(),
3487
+ finishReason: "other",
3488
+ };
3489
+ interruptedResponse = message;
3490
+ }
3491
+ }
3423
3492
  const modelMessages = replaceInputFiles(await convertToModelMessages(payload.originalMessages.map((message) => storedContext.context && message.role === "assistant"
3424
3493
  ? {
3425
3494
  ...message,
@@ -3492,6 +3561,12 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3492
3561
  })
3493
3562
  : [...context.messages, ...currentTurn];
3494
3563
  }
3564
+ if (responseBaseline) {
3565
+ payload.responseMessageId = responseBaseline.id;
3566
+ // Model context was restored independently above; this baseline is only for UI assembly.
3567
+ payload.originalMessages = [responseBaseline];
3568
+ }
3569
+ delete context.metadata.suspendedMessageId;
3495
3570
  if (submittedUser && startsNewTurn)
3496
3571
  context.metadata.turnImages = inputFiles
3497
3572
  .filter((file) => file.mediaType.startsWith("image/"))
@@ -3910,16 +3985,18 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3910
3985
  })),
3911
3986
  ];
3912
3987
  if (inboxRunner?.suspendedToolCallId) {
3913
- responseMessage.parts = responseMessage.parts.map((part) => "toolCallId" in part &&
3914
- part.toolCallId === inboxRunner.suspendedToolCallId
3915
- ? {
3916
- type: "tool-runWait",
3917
- toolCallId: part.toolCallId,
3918
- state: "input-available",
3919
- input: "input" in part ? part.input : {},
3988
+ responseMessage.parts = responseMessage.parts.map((part) => {
3989
+ if (isToolUIPart(part) &&
3990
+ part.toolCallId === inboxRunner.suspendedToolCallId &&
3991
+ part.state === "output-available") {
3992
+ const { output: _output, approval: _approval, ...pending } = part;
3993
+ return { ...pending, state: "input-available" };
3920
3994
  }
3921
- : part);
3995
+ return part;
3996
+ });
3922
3997
  }
3998
+ if (inboxRunner?.suspended)
3999
+ context.metadata.suspendedMessageId = responseMessage.id;
3923
4000
  responseMessage.metadata = {
3924
4001
  ...responseMessage.metadata,
3925
4002
  ...(await contextFinished),
@@ -4009,13 +4086,16 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4009
4086
  const pending = context.metadata.agentCore;
4010
4087
  let lastCoreFinishReason = pending ? "tool-calls" : "stop";
4011
4088
  if (pending && currentTurn.some((message) => message.role === "user"))
4012
- throw new Error("Resolve pending approvals before starting another turn.");
4089
+ throw new Error("Resolve pending tool interactions before starting another turn.");
4013
4090
  const messageId = pending?.messageId ?? payload.responseMessageId;
4014
- const responses = pending
4091
+ const pendingMessage = pending
4015
4092
  ? z
4016
4093
  .object({ parts: z.array(z.unknown()) })
4017
4094
  .parse(payload.originalMessages.find((message) => message.id === pending.messageId))
4018
- .parts.flatMap((part) => {
4095
+ : undefined;
4096
+ const responses = pendingMessage
4097
+ ? pendingMessage.parts
4098
+ .flatMap((part) => {
4019
4099
  const parsed = z
4020
4100
  .object({
4021
4101
  state: z.literal("approval-responded"),
@@ -4029,6 +4109,49 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4029
4109
  // Keep unknown IDs so Rust still rejects foreign/duplicate responses.
4030
4110
  .filter((response) => !pending.pendingStep.approvals.some((approval) => approval.id === response.id && approval.response != null))
4031
4111
  : [];
4112
+ // Model conversion already applies the Tool's toModelOutput (including
4113
+ // managed file adoption); retain the raw client output for the UI stream.
4114
+ const modelResults = new Map(fromModelMessages((pending?.pendingStep.client_calls?.length
4115
+ ? modelMessages
4116
+ : []).flatMap((message) => message.role === "tool"
4117
+ ? [
4118
+ {
4119
+ ...message,
4120
+ content: message.content.filter((part) => part.type === "tool-result"),
4121
+ },
4122
+ ]
4123
+ : [])).flatMap((message) => message.content.flatMap((part) => part.type === "tool_result" ? [[part.call_id, part]] : [])));
4124
+ const clientResults = (pendingMessage?.parts ?? []).flatMap((part) => {
4125
+ const result = z
4126
+ .discriminatedUnion("state", [
4127
+ z.object({
4128
+ state: z.literal("output-available"),
4129
+ toolCallId: z.string(),
4130
+ output: z.json(),
4131
+ }),
4132
+ z.object({
4133
+ state: z.literal("output-error"),
4134
+ toolCallId: z.string(),
4135
+ errorText: z.string(),
4136
+ }),
4137
+ ])
4138
+ .safeParse(part);
4139
+ if (!result.success ||
4140
+ !pending?.pendingStep.client_calls?.includes(result.data.toolCallId))
4141
+ return [];
4142
+ const value = result.data;
4143
+ const modelResult = modelResults.get(value.toolCallId);
4144
+ if (!modelResult)
4145
+ throw new Error(`Missing client Tool model output: ${value.toolCallId}`);
4146
+ return [
4147
+ {
4148
+ call_id: value.toolCallId,
4149
+ output: value.state === "output-error" ? value.errorText : value.output,
4150
+ is_error: value.state === "output-error",
4151
+ model_output: modelResult.model_output,
4152
+ },
4153
+ ];
4154
+ });
4032
4155
  const coreTools = await createCoreWorkerTools({
4033
4156
  tools,
4034
4157
  approval: toolApproval,
@@ -4087,7 +4210,9 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4087
4210
  }
4088
4211
  : undefined,
4089
4212
  pending_step: pending?.pendingStep,
4090
- }), pending ? { type: "approval_response", responses } : { type: "start" }, {
4213
+ }), pending
4214
+ ? { type: "tool_response", responses, results: clientResults }
4215
+ : { type: "start" }, {
4091
4216
  ...coreTools,
4092
4217
  onError: engine.onError,
4093
4218
  fetch: (request, init) => coreConnection.fetch(request, init),
@@ -4191,7 +4316,8 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4191
4316
  ? "stop"
4192
4317
  : outcome.reason === "stopped"
4193
4318
  ? lastCoreFinishReason
4194
- : outcome.reason === "awaiting_approval"
4319
+ : outcome.reason === "awaiting_approval" ||
4320
+ outcome.reason === "awaiting_tool_results"
4195
4321
  ? "tool-calls"
4196
4322
  : "length",
4197
4323
  });
@@ -4432,6 +4558,46 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4432
4558
  }
4433
4559
  const durableStream = stream.pipeThrough(new TransformStream({
4434
4560
  async transform(chunk, controller) {
4561
+ if (chunk.type === "start" && interruptedResponse) {
4562
+ const events = [
4563
+ ...(interruptedWait ? [interruptedWait] : []),
4564
+ { type: "finish", finishReason: "other" },
4565
+ ];
4566
+ if (sessionExecution)
4567
+ await mutateSession({
4568
+ messages: [{ ...interruptedResponse }],
4569
+ events,
4570
+ });
4571
+ else {
4572
+ await appendSessionValue({
4573
+ body: { message: interruptedResponse, runId: payload.runId },
4574
+ chatId: payload.chatId,
4575
+ namespace: input.sessionNamespace,
4576
+ path: "/v1/messages",
4577
+ });
4578
+ for (const event of events)
4579
+ await appendStreamEvent(event);
4580
+ }
4581
+ }
4582
+ if (chunk.type === "start" && responseBaseline) {
4583
+ // Each internal invocation still has its own start/finish for accounting.
4584
+ // The public journal represents one response across those invocations.
4585
+ controller.enqueue(chunk);
4586
+ const result = input.inboxExecution?.waitResult;
4587
+ if (result) {
4588
+ const output = {
4589
+ type: "tool-output-available",
4590
+ toolCallId: result.toolCallId,
4591
+ output: toAgentRunWaitOutput(result),
4592
+ };
4593
+ if (sessionExecution)
4594
+ await mutateSession({ events: [output] });
4595
+ else
4596
+ await appendStreamEvent(output);
4597
+ controller.enqueue(output);
4598
+ }
4599
+ return;
4600
+ }
4435
4601
  if (chunk.type === "tool-output-available" &&
4436
4602
  chunk.toolCallId === inboxRunner?.suspendedToolCallId)
4437
4603
  return;
@@ -4458,6 +4624,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4458
4624
  finishReason: metadata.finishReason,
4459
4625
  messageMetadata: metadata,
4460
4626
  };
4627
+ if (metadata.sessionStatus === "waiting") {
4628
+ controller.enqueue(chunk);
4629
+ return;
4630
+ }
4461
4631
  }
4462
4632
  if (sessionExecution) {
4463
4633
  await mutateSession({ events: [chunk] });
@@ -4486,7 +4656,17 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4486
4656
  reader.releaseLock();
4487
4657
  }
4488
4658
  })());
4489
- return createUIMessageStreamResponse({ stream: response });
4659
+ return createUIMessageStreamResponse({
4660
+ stream: response,
4661
+ headers: {
4662
+ [AGENT_APPLICATION_MESSAGE_ID_HEADER]: payload.responseMessageId,
4663
+ },
4664
+ });
4490
4665
  }
4491
- return createUIMessageStreamResponse({ stream: durableStream });
4666
+ return createUIMessageStreamResponse({
4667
+ stream: durableStream,
4668
+ headers: {
4669
+ [AGENT_APPLICATION_MESSAGE_ID_HEADER]: payload.responseMessageId,
4670
+ },
4671
+ });
4492
4672
  }
@@ -19,7 +19,7 @@ export declare function toFileData(data: AgentCoreFileData): {
19
19
  export declare function toCoreModelOutput(output: ToolResultOutput): AgentCoreModelOutput;
20
20
  export declare function fromModelMessages(messages: ModelMessage[]): AgentCoreMessage[];
21
21
  /** Close only the interrupted trailing batch, after host operations have settled. */
22
- export declare function closeInterruptedToolCalls(messages: ModelMessage[]): ModelMessage[];
22
+ export declare function closeInterruptedToolCalls(messages: ModelMessage[], reason?: string): ModelMessage[];
23
23
  export declare function toToolModelOutput(part: Extract<AgentCoreMessage["content"][number], {
24
24
  type: "tool_result";
25
25
  }>): LanguageModelV4ToolResultOutput;
@@ -137,7 +137,7 @@ export function fromModelMessages(messages) {
137
137
  }));
138
138
  }
139
139
  /** Close only the interrupted trailing batch, after host operations have settled. */
140
- export function closeInterruptedToolCalls(messages) {
140
+ export function closeInterruptedToolCalls(messages, reason = "Agent execution was interrupted before a tool result was recorded. The operation may have taken effect; its outcome is unknown.") {
141
141
  let index = messages.length - 1;
142
142
  while (index >= 0 && messages[index]?.role === "tool")
143
143
  index--;
@@ -165,7 +165,7 @@ export function closeInterruptedToolCalls(messages) {
165
165
  toolName,
166
166
  output: {
167
167
  type: "error-text",
168
- value: "Agent execution was interrupted before a tool result was recorded. The operation may have taken effect; its outcome is unknown.",
168
+ value: reason,
169
169
  },
170
170
  })),
171
171
  },
@@ -7,7 +7,7 @@ export function createAgentCoreTools(options) {
7
7
  return {
8
8
  async decideTool(call, signal) {
9
9
  const action = options.actions[call.name];
10
- if (!action?.handler)
10
+ if (!action)
11
11
  return { type: "denied", reason: `Tool is unavailable: ${call.name}` };
12
12
  const parsed = await action.inputSchema.safeParseAsync(call.input);
13
13
  if (!parsed.success)
@@ -35,8 +35,9 @@ export function createAgentCoreTools(options) {
35
35
  return {
36
36
  type: "approval_required",
37
37
  approval_id: options.approvalId?.(call) ?? crypto.randomUUID(),
38
+ ...(!action.handler ? { client: true } : {}),
38
39
  };
39
- return { type: "approved" };
40
+ return { type: action.handler ? "approved" : "client" };
40
41
  },
41
42
  async tool(call, signal) {
42
43
  const action = options.actions[call.name];
@@ -158,7 +158,7 @@ export async function* projectAgentCoreEvents(turn, options) {
158
158
  break;
159
159
  case "tool_ready":
160
160
  calls.set(event.call.id, event.call);
161
- if (resumed)
161
+ if (resumed && !event.client)
162
162
  break;
163
163
  part = {
164
164
  type: "tool-call",
@@ -267,7 +267,8 @@ export async function* projectAgentCoreEvents(turn, options) {
267
267
  : outcome.reason === "model_limit"
268
268
  ? "length"
269
269
  : outcome.reason === "step_limit" ||
270
- outcome.reason === "awaiting_approval"
270
+ outcome.reason === "awaiting_approval" ||
271
+ outcome.reason === "awaiting_tool_results"
271
272
  ? "tool-calls"
272
273
  : "error",
273
274
  messageMetadata,
@@ -37,7 +37,7 @@ export async function createCoreWorkerTools(options) {
37
37
  async decideTool(call, signal) {
38
38
  signal.throwIfAborted();
39
39
  const tool = options.tools[call.name];
40
- if (!tool?.execute)
40
+ if (!tool)
41
41
  return { type: "denied", reason: `Tool is unavailable: ${call.name}` };
42
42
  const parsed = await safeValidateTypes({
43
43
  value: call.input,
@@ -69,8 +69,12 @@ export async function createCoreWorkerTools(options) {
69
69
  : "Tool execution denied",
70
70
  };
71
71
  if (type === "user-approval" || guardrailType === "user-approval")
72
- return { type: "approval_required", approval_id: crypto.randomUUID() };
73
- return { type: "approved" };
72
+ return {
73
+ type: "approval_required",
74
+ approval_id: crypto.randomUUID(),
75
+ ...(!tool.execute ? { client: true } : {}),
76
+ };
77
+ return { type: tool.execute ? "approved" : "client" };
74
78
  },
75
79
  async tool(call, signal) {
76
80
  const tool = options.tools[call.name];
@@ -26,7 +26,7 @@ export interface AgentCoreHost {
26
26
  prepareStep?(step: AgentCorePrepareStep, signal: AbortSignal): Promise<AgentCoreMessage[] | null | void>;
27
27
  finishStep?(step: AgentCoreFinishedStep, signal: AbortSignal): Promise<AgentCoreMessage[] | null | void>;
28
28
  /** Core awaits this even after cancellation. Drain retained work and persist
29
- * final context here; awaiting_approval is a pause, not a completed turn. */
29
+ * final context here; approval/client-result waits are pauses, not completed turns. */
30
30
  finishRun?(outcome: AgentCoreOutcome, signal: AbortSignal): Promise<void>;
31
31
  model?(request: AgentCoreModelRequest, signal: AbortSignal): AsyncIterable<AgentCoreModelPart>;
32
32
  tool(call: AgentCoreToolCall, signal: AbortSignal): Promise<AgentCoreToolResult>;
@@ -37,13 +37,13 @@ export interface AgentCoreHost {
37
37
  export declare function createAgentCore(module: WebAssembly.Module, config: AgentCoreConfig): {
38
38
  run(message: AgentCoreInput, host: AgentCoreHost, emit: (event: AgentCoreEvent) => Promise<void>, signal: AbortSignal): Promise<{
39
39
  output?: z.util.JSONType | undefined;
40
- reason: "awaiting_approval" | "cancelled" | "completed" | "failed" | "model_limit" | "step_limit" | "stopped";
40
+ reason: "awaiting_approval" | "awaiting_tool_results" | "cancelled" | "completed" | "failed" | "model_limit" | "step_limit" | "stopped";
41
41
  error: string | null;
42
42
  details?: AgentCoreErrorDetails | undefined;
43
43
  }>;
44
44
  runOutcome(message: AgentCoreInput, host: AgentCoreHost, emit: (event: AgentCoreEvent) => Promise<void>, signal: AbortSignal): Promise<{
45
45
  output?: z.util.JSONType | undefined;
46
- reason: "awaiting_approval" | "cancelled" | "completed" | "failed" | "model_limit" | "step_limit" | "stopped";
46
+ reason: "awaiting_approval" | "awaiting_tool_results" | "cancelled" | "completed" | "failed" | "model_limit" | "step_limit" | "stopped";
47
47
  error: string | null;
48
48
  details?: AgentCoreErrorDetails | undefined;
49
49
  }>;
@@ -351,7 +351,7 @@ export function createAgentCore(module, config) {
351
351
  })),
352
352
  },
353
353
  }).exports);
354
- if (exports.agent_abi_version() !== 13)
354
+ if (exports.agent_abi_version() !== 14)
355
355
  throw new Error("Unsupported Agent Core ABI.");
356
356
  const run = jspi.promising(exports.agent_run);
357
357
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gea-ai/agent-sdk",
3
- "version": "0.1.260920-alpha.1",
3
+ "version": "0.1.260920-alpha.2",
4
4
  "private": false,
5
5
  "homepage": "https://musegea.com/developers",
6
6
  "license": "Apache-2.0",
@@ -104,7 +104,7 @@
104
104
  "@ai-sdk/otel": "1.0.9",
105
105
  "@ai-sdk/provider": "4.0.1",
106
106
  "@ai-sdk/provider-utils": "5.0.2",
107
- "@gea-ai/contract": "0.1.260920-alpha.1",
107
+ "@gea-ai/contract": "0.1.260920-alpha.2",
108
108
  "@opentelemetry/api": "1.9.1",
109
109
  "@opentelemetry/context-async-hooks": "2.10.0",
110
110
  "@opentelemetry/core": "2.10.0",