@gea-ai/agent-sdk 0.1.260925-alpha.0 → 0.1.261002-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.
@@ -19,19 +19,6 @@ export declare function executeChannelBehavior(input: {
19
19
  environment: AgentChannelFetchEnvironment;
20
20
  signal: AbortSignal;
21
21
  }): Promise<{
22
- sequence: number;
23
- duplicate: boolean;
24
- } | {
25
- state: AgentChannelJson;
26
- intents: {
27
- key: string;
28
- payload: AgentChannelJson;
29
- eventId: string;
30
- operationId: string;
31
- }[];
32
- cursor: number;
33
- idempotencyWindowMs?: number | undefined;
34
- } | {
35
22
  status: "sent";
36
23
  receipt?: AgentChannelJson | undefined;
37
24
  } | {
@@ -56,6 +43,19 @@ export declare function executeChannelBehavior(input: {
56
43
  message: string;
57
44
  retryable: boolean;
58
45
  };
46
+ } | {
47
+ sequence: number;
48
+ duplicate: boolean;
49
+ } | {
50
+ state: AgentChannelJson;
51
+ intents: {
52
+ key: string;
53
+ payload: AgentChannelJson;
54
+ eventId: string;
55
+ operationId: string;
56
+ }[];
57
+ cursor: number;
58
+ idempotencyWindowMs?: number | undefined;
59
59
  } | {
60
60
  events: {
61
61
  id: string;
@@ -7,6 +7,9 @@ let compiled;
7
7
  export function agentCore(options = {}) {
8
8
  return {
9
9
  ...options,
10
+ // A silent model stream must not retain a Run until the provider hard deadline.
11
+ // Explicit timeout options (including {}) retain caller-owned policy.
12
+ timeout: options.timeout ?? { chunkMs: 5 * 60_000 },
10
13
  output: options.output === undefined
11
14
  ? undefined
12
15
  : toAgentCoreOutput(options.output),
@@ -0,0 +1,3 @@
1
+ /** Old Core policy denials lacked the rejected approval required by AI SDK.
2
+ * Repair only their presentation; persisted Core decisions still own execution. */
3
+ export declare function repairLegacyToolDenials(message: unknown): unknown;
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+ const assistantHistorySchema = z
3
+ .object({
4
+ role: z.literal("assistant"),
5
+ parts: z.array(z.unknown()),
6
+ })
7
+ .passthrough();
8
+ const legacyDenialSchema = z
9
+ .object({
10
+ type: z.union([z.literal("dynamic-tool"), z.string().startsWith("tool-")]),
11
+ toolCallId: z.string(),
12
+ state: z.literal("output-denied"),
13
+ approval: z
14
+ .object({ id: z.string(), approved: z.literal(true) })
15
+ .passthrough()
16
+ .optional(),
17
+ })
18
+ .passthrough();
19
+ /** Old Core policy denials lacked the rejected approval required by AI SDK.
20
+ * Repair only their presentation; persisted Core decisions still own execution. */
21
+ export function repairLegacyToolDenials(message) {
22
+ const history = assistantHistorySchema.safeParse(message);
23
+ if (!history.success)
24
+ return message;
25
+ return {
26
+ ...history.data,
27
+ parts: history.data.parts.map((part) => {
28
+ const denied = legacyDenialSchema.safeParse(part);
29
+ return denied.success
30
+ ? {
31
+ ...denied.data,
32
+ state: "output-error",
33
+ errorText: "Tool execution was denied by policy.",
34
+ }
35
+ : part;
36
+ }),
37
+ };
38
+ }
@@ -57,60 +57,114 @@ export async function cancelRemoteRun(fetch, run) {
57
57
  const response = await fetch(`/runs/${run.id}/cancel`, { method: "POST" });
58
58
  await responseJson(response);
59
59
  }
60
+ class RemoteRunReadError extends TaggedError("RemoteRunReadError")() {
61
+ }
62
+ async function waitForRemoteRead(ms, signal) {
63
+ signal.throwIfAborted();
64
+ await new Promise((resolve, reject) => {
65
+ const abort = () => {
66
+ clearTimeout(timer);
67
+ reject(signal.reason);
68
+ };
69
+ const timer = setTimeout(() => {
70
+ signal.removeEventListener("abort", abort);
71
+ resolve();
72
+ }, ms);
73
+ signal.addEventListener("abort", abort, { once: true });
74
+ if (signal.aborted)
75
+ abort();
76
+ });
77
+ }
78
+ /** Only idempotent reads retry. Admission and Tool execution are never repeated here. */
79
+ async function readRemoteRunPage(fetch, path, signal) {
80
+ for (let attempt = 0;; attempt++) {
81
+ signal.throwIfAborted();
82
+ const response = await Result.tryPromise(() => fetch(path, { signal }));
83
+ signal.throwIfAborted();
84
+ let status;
85
+ let delay = 250 * 2 ** attempt;
86
+ if (response.isOk()) {
87
+ if (response.value.ok)
88
+ return response.value.json();
89
+ status = response.value.status;
90
+ const retryAfter = response.value.headers.get("retry-after");
91
+ if (retryAfter) {
92
+ const requestedDelay = /^\d+$/u.test(retryAfter)
93
+ ? Number(retryAfter) * 1000
94
+ : Date.parse(retryAfter) - Date.now();
95
+ if (Number.isFinite(requestedDelay))
96
+ delay = Math.max(delay, requestedDelay);
97
+ }
98
+ await response.value.body?.cancel();
99
+ }
100
+ if (attempt >= 2 ||
101
+ delay > 5000 ||
102
+ (status !== undefined && status !== 408 && status !== 429 && status < 500))
103
+ throw new RemoteRunReadError({
104
+ message: "Remote Run read failed.",
105
+ status,
106
+ });
107
+ await waitForRemoteRead(delay, signal);
108
+ }
109
+ }
60
110
  export async function observeRemoteRun(fetch, run, signal, onInputRequired) {
61
111
  let reportedInput;
62
- while (true) {
63
- signal.throwIfAborted();
64
- const record = agentHttpRunSchema.parse(await responseJson(await fetch(`/runs/${run.id}`, { signal })));
65
- if (record.id !== run.id || record.session_id !== run.sessionId)
66
- throw new Error("Remote Run identity mismatch.");
67
- const terminal = ["finished", "error", "aborted", "interrupted"].includes(record.status);
68
- const inputMessageId = record.status === "waiting" ? record.input_message_id : null;
69
- if (terminal || (onInputRequired && inputMessageId)) {
70
- const text = [];
71
- let cursor = null;
72
- do {
73
- const page = messagesSchema.parse(await responseJson(await fetch(`/sessions/${run.sessionId}/messages?limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`, { signal })));
74
- for (const message of page.items)
75
- if (message.run_id === run.id && message.role === "assistant") {
76
- if (!terminal && message.id === inputMessageId && onInputRequired) {
77
- const input = agentApplicationAssistantMessageSchema.parse({
78
- id: message.id,
79
- role: message.role,
80
- parts: message.parts,
81
- });
82
- const key = agentInputRequestKey(input);
83
- if (key !== reportedInput) {
84
- await onInputRequired(input);
85
- reportedInput = key;
112
+ try {
113
+ while (true) {
114
+ signal.throwIfAborted();
115
+ const record = agentHttpRunSchema.parse(await readRemoteRunPage(fetch, `/runs/${run.id}`, signal));
116
+ if (record.id !== run.id || record.session_id !== run.sessionId)
117
+ throw new Error("Remote Run identity mismatch.");
118
+ const terminal = ["finished", "error", "aborted", "interrupted"].includes(record.status);
119
+ const inputMessageId = record.status === "waiting" ? record.input_message_id : null;
120
+ if (terminal || (onInputRequired && inputMessageId)) {
121
+ const text = [];
122
+ let cursor = null;
123
+ do {
124
+ const page = messagesSchema.parse(await readRemoteRunPage(fetch, `/sessions/${run.sessionId}/messages?limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`, signal));
125
+ for (const message of page.items)
126
+ if (message.run_id === run.id && message.role === "assistant") {
127
+ if (!terminal &&
128
+ message.id === inputMessageId &&
129
+ onInputRequired) {
130
+ const input = agentApplicationAssistantMessageSchema.parse({
131
+ id: message.id,
132
+ role: message.role,
133
+ parts: message.parts,
134
+ });
135
+ const key = agentInputRequestKey(input);
136
+ if (key !== reportedInput) {
137
+ await onInputRequired(input);
138
+ reportedInput = key;
139
+ }
86
140
  }
141
+ for (const part of message.parts)
142
+ if (part.type === "text" && typeof part.text === "string")
143
+ text.push(part.text);
87
144
  }
88
- for (const part of message.parts)
89
- if (part.type === "text" && typeof part.text === "string")
90
- text.push(part.text);
91
- }
92
- cursor = page.next_cursor;
93
- } while (cursor);
94
- if (terminal)
95
- return {
96
- status: record.status,
97
- text: text.join("\n").slice(0, 200_000),
98
- };
145
+ cursor = page.next_cursor;
146
+ } while (cursor);
147
+ if (terminal)
148
+ return {
149
+ status: record.status,
150
+ text: text.join("\n").slice(0, 200_000),
151
+ };
152
+ }
153
+ if (!["queued", "running", "waiting"].includes(record.status))
154
+ throw new Error("Unknown remote Run status.");
155
+ await waitForRemoteRead(1000, signal);
99
156
  }
100
- if (!["queued", "running", "waiting"].includes(record.status))
101
- throw new Error("Unknown remote Run status.");
102
- await new Promise((resolve, reject) => {
103
- const abort = () => {
104
- clearTimeout(timer);
105
- reject(signal.reason);
106
- };
107
- const timer = setTimeout(() => {
108
- signal.removeEventListener("abort", abort);
109
- resolve();
110
- }, 1000);
111
- signal.addEventListener("abort", abort, { once: true });
112
- if (signal.aborted)
113
- abort();
114
- });
157
+ }
158
+ catch (error) {
159
+ // Stop only this parent-owned observation. The remote execution/directory is untouched.
160
+ // Host shutdown leaves the relation pending so another dispatcher can resume it.
161
+ signal.throwIfAborted();
162
+ const status = RemoteRunReadError.is(error) && error.status
163
+ ? ` (HTTP ${error.status})`
164
+ : "";
165
+ return {
166
+ status: "interrupted",
167
+ text: `Remote observation interrupted${status}; execution outcome is unknown. Run ${run.id}, Session ${run.sessionId}. Do not resubmit the task; inspect this existing Run.`,
168
+ };
115
169
  }
116
170
  }
@@ -20,7 +20,7 @@ export declare class AgentRunHost {
20
20
  response: Response;
21
21
  dispose?: () => void;
22
22
  }>;
23
- cancelChild(run: AgentChildRun): Promise<void>;
23
+ cancelChild(run: AgentChildRun): Promise<boolean | void>;
24
24
  onError(error: unknown, identity?: {
25
25
  runId?: string;
26
26
  sessionId?: string;
@@ -161,9 +161,7 @@ export class AgentRunHost {
161
161
  dispatch(run, inboxExecution) {
162
162
  if (this.closed)
163
163
  throw new Error("Run dispatcher is stopping.");
164
- if (this.executions.has(run.id) ||
165
- run.outcome ||
166
- (run.cancelRequested && run.destination.type !== "remote"))
164
+ if (this.executions.has(run.id) || run.outcome)
167
165
  return;
168
166
  const cancellation = new AbortController();
169
167
  this.executions.set(run.id, cancellation);
@@ -207,8 +205,18 @@ export class AgentRunHost {
207
205
  });
208
206
  }
209
207
  else {
210
- cancellation.abort();
211
- return;
208
+ // Keep admission alive until the Session has received cancellation.
209
+ // Aborting the request earlier can strand an uninitialized child.
210
+ const cancelled = await Result.tryPromise(() => this.host.cancelChild(run));
211
+ if (cancelled.isOk() && cancelled.value !== false) {
212
+ cancellation.abort();
213
+ return;
214
+ }
215
+ if (cancelled.isErr())
216
+ this.host.onError(cancelled.error, {
217
+ runId: run.id,
218
+ sessionId: run.sessionId,
219
+ });
212
220
  }
213
221
  }
214
222
  timer = setTimeout(() => {
@@ -284,8 +292,9 @@ export class AgentRunHost {
284
292
  this.dispatch(run);
285
293
  }
286
294
  else {
287
- this.executions.get(runId)?.abort();
288
- await this.host.cancelChild(run);
295
+ this.dispatch(run);
296
+ if ((await this.host.cancelChild(run)) !== false)
297
+ this.executions.get(runId)?.abort();
289
298
  }
290
299
  }
291
300
  return Response.json({
@@ -444,6 +444,15 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
444
444
  handlerMs = performance.now() - handlerStartedAt;
445
445
  return response;
446
446
  });
447
+ console.info("AgentSession lease release completed", {
448
+ event: "agent.session.lease_released",
449
+ runId: input.runId,
450
+ attemptId: input.attemptId,
451
+ executionId: input.inbox.executionId,
452
+ inputWait: Boolean(input.inbox.inputWait),
453
+ status: response.status,
454
+ elapsedMs: Math.round(performance.now() - startedAt),
455
+ });
447
456
  response.headers.set("server-timing", `session-handler;dur=${handlerMs.toFixed(1)}, session-transaction;dur=${(performance.now() - startedAt).toFixed(1)}`);
448
457
  return response;
449
458
  }
@@ -683,7 +692,15 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
683
692
  .parse(await readBody(request, storage));
684
693
  const result = await storage.transaction(async (tx) => {
685
694
  const active = await tx.get(key("active-turn"));
686
- if (!active || active.runId !== input.runId)
695
+ if (!active) {
696
+ // A local logical Run can execute or wait without a managed lease. The
697
+ // inbox still owns its descendants and fences cancellation by Run ID.
698
+ const state = await getAgentSessionRunState(tx, input.runId);
699
+ return {
700
+ requested: state?.status === "waiting" || state?.status === "running",
701
+ };
702
+ }
703
+ if (active.runId !== input.runId)
687
704
  return { requested: false };
688
705
  if (!active.cancelRequestedAt) {
689
706
  const turn = {
@@ -467,6 +467,20 @@ async function finishInbox(tx, state, runs, input) {
467
467
  await save(tx, state, runs);
468
468
  return Response.json({ error: "execution_not_expired" }, { status: 409 });
469
469
  }
470
+ console.warn("AgentSession execution lease expired", {
471
+ event: "agent.session.lease_expired",
472
+ sessionId: state.identity.sessionId,
473
+ runId: turn.runId,
474
+ attemptId: turn.attemptId,
475
+ requestId: turn.requestId,
476
+ executionId: state.execution.id,
477
+ leaseOwner: turn.leaseOwner,
478
+ leaseExpiresAt: turn.leaseExpiresAt,
479
+ started: state.started,
480
+ waiting: state.wait !== null,
481
+ pendingChildren: runs.filter((run) => !run.outcome).length,
482
+ pendingStarts: state.pendingStarts.length,
483
+ });
470
484
  }
471
485
  if (inputWait && !outcome && !state.stopped) {
472
486
  state.inputWait = {
@@ -644,6 +658,13 @@ export async function handleAgentSessionInboxRequest(request, storage) {
644
658
  if (previous !== undefined)
645
659
  return Response.json({ error: "message_already_accepted" }, { status: 409 });
646
660
  if (input.message && state.inputWait) {
661
+ // Retried dispatch of this Run is not a new user turn. In particular,
662
+ // a late child-start command must not skip the input it just requested.
663
+ if (state.inputWait.turnId === (input.runId ?? input.executionId))
664
+ return Response.json({ error: "execution_already_started" }, {
665
+ status: 409,
666
+ headers: { "x-gea-session-execution": "already-started" },
667
+ });
647
668
  await skipInputWait(tx, state, runs);
648
669
  if (state.draining)
649
670
  return Response.json({ error: "session_busy" }, { status: 409 });
@@ -1,6 +1,7 @@
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
3
  import { batchAgentStreamEvents, coalesceAgentTextDeltas, } from "./agent-stream-coalescing.js";
4
+ import { repairLegacyToolDenials } from "./agent-message-history.js";
4
5
  import { sendAgentSessionRequest } from "./agent-session-transport.js";
5
6
  import { agentSessionOverridesSchema } from "@gea-ai/contract/agent-session-overrides";
6
7
  import { resolveModelOptions } from "@gea-ai/contract/model-options";
@@ -3146,13 +3147,19 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3146
3147
  const submittedUser = payload.currentTurn.some((message) => message.role === "user")
3147
3148
  ? inputUser
3148
3149
  : undefined;
3149
- if (input.inboxExecution && submittedUser)
3150
- await appendSessionValue({
3151
- body: { message: submittedUser, runId: payload.runId },
3152
- chatId: payload.chatId,
3153
- namespace: input.sessionNamespace,
3154
- path: "/v1/messages",
3155
- });
3150
+ if (input.inboxExecution && submittedUser) {
3151
+ if (sessionExecution) {
3152
+ await mutateSession({ messages: [{ ...submittedUser }] });
3153
+ }
3154
+ else {
3155
+ await appendSessionValue({
3156
+ body: { message: submittedUser, runId: payload.runId },
3157
+ chatId: payload.chatId,
3158
+ namespace: input.sessionNamespace,
3159
+ path: "/v1/messages",
3160
+ });
3161
+ }
3162
+ }
3156
3163
  // Tool continuations still need the current user's durable file references
3157
3164
  // when reconstructing model messages from saved UI history.
3158
3165
  const submittedFiles = inputUser?.parts.flatMap((part) => (part.type === "file" ? [part] : [])) ??
@@ -3615,7 +3622,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3615
3622
  schema: z.object({ message: z.unknown() }),
3616
3623
  });
3617
3624
  const [message] = await validateUIMessages({
3618
- messages: [saved.message],
3625
+ messages: [repairLegacyToolDenials(saved.message)],
3619
3626
  });
3620
3627
  if (!message || message.role !== "assistant")
3621
3628
  throw new Error("Suspended response is not an assistant message.");
@@ -3740,14 +3747,9 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3740
3747
  .filter((file) => file.mediaType.startsWith("image/"))
3741
3748
  .map(({ url: _url, ...file }) => file);
3742
3749
  const turnImages = new Map();
3743
- const addImage = async (file) => {
3744
- const reference = file.id ?? file.key;
3745
- if (turnImages.has(reference))
3746
- return;
3750
+ const readImage = async (file) => {
3747
3751
  if (!artifactScope)
3748
3752
  throw new Error("Images require Agent Artifacts.");
3749
- if (turnImages.size >= MAX_AGENT_ARTIFACT_IMAGES)
3750
- throw new Error(`At most ${MAX_AGENT_ARTIFACT_IMAGES} images can be viewed in one user turn.`);
3751
3753
  const response = await host.fetch(`${HOST_PATH}/tools/attachments/read`, {
3752
3754
  method: "POST",
3753
3755
  headers: { "content-type": "application/json" },
@@ -3762,11 +3764,19 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3762
3764
  const bytes = new Uint8Array(await response.arrayBuffer());
3763
3765
  if (bytes.byteLength > MAX_AGENT_ARTIFACT_IMAGE_BYTES)
3764
3766
  throw new Error("Prepared image exceeds the model image limit.");
3765
- turnImages.set(reference, {
3767
+ return {
3766
3768
  type: "file",
3767
3769
  data: { type: "data", data: bytes },
3768
3770
  mediaType: response.headers.get("content-type") ?? file.mediaType,
3769
- });
3771
+ };
3772
+ };
3773
+ const addImage = async (file) => {
3774
+ const reference = file.id ?? file.key;
3775
+ if (turnImages.has(reference))
3776
+ return;
3777
+ if (turnImages.size >= MAX_AGENT_ARTIFACT_IMAGES)
3778
+ throw new Error(`At most ${MAX_AGENT_ARTIFACT_IMAGES} images can be viewed in one user turn.`);
3779
+ turnImages.set(reference, await readImage(file));
3770
3780
  context.metadata.turnImages ??= [];
3771
3781
  if (!context.metadata.turnImages.some((image) => (image.id ?? image.key) === reference))
3772
3782
  context.metadata.turnImages.push(file);
@@ -4040,12 +4050,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4040
4050
  const coreConnections = new Map([[configuredModel, coreConnection]]);
4041
4051
  const coreCacheStates = new Map([[configuredModel, promptCacheState]]);
4042
4052
  const preparePrompt = async (messages) => {
4043
- messages = await resolveToolArtifacts(messages, (toolName, toolCallId) => artifactScope
4044
- ? createAgentArtifacts({
4045
- scope: artifactScope,
4046
- invoke: (operation) => invokeArtifact(operation, toolName, { toolCallId }),
4047
- })
4048
- : undefined);
4053
+ messages = await resolveToolArtifacts(messages, (id, mediaType) => readImage({ id, mediaType }));
4049
4054
  const instructionCount = payload.instructions.length;
4050
4055
  const prepared = addCacheControlToMessages([...payload.instructions, ...messages], (coreModel === configuredModel
4051
4056
  ? payload.cacheControlProvider
@@ -4657,6 +4662,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4657
4662
  async execute({ writer }) {
4658
4663
  for await (const chunk of projectAgentCoreEvents(tracked, {
4659
4664
  messageId,
4665
+ approvals: pending?.pendingStep.approvals,
4660
4666
  tools,
4661
4667
  messageMetadata: {
4662
4668
  trace: invocationTrace,
@@ -8,6 +8,10 @@ export declare function projectAgentCoreEvents(turn: AsyncGenerator<AgentCoreEve
8
8
  messageMetadata?: unknown;
9
9
  /** Preserve AI SDK's declared Tool kind; raw hosts default to dynamic tools. */
10
10
  tools?: ToolSet;
11
+ approvals?: ReadonlyArray<{
12
+ id: string;
13
+ call_id: string;
14
+ }>;
11
15
  onError?: (error: Error) => string;
12
16
  }): AsyncGenerator<UIMessageChunk>;
13
17
  export declare function createAgentCoreUIStream(core: ReturnType<typeof createAgentCore>, message: AgentCoreInput, host: AgentCoreHost, options: {
@@ -15,6 +19,10 @@ export declare function createAgentCoreUIStream(core: ReturnType<typeof createAg
15
19
  signal?: AbortSignal;
16
20
  messageMetadata?: unknown;
17
21
  tools?: ToolSet;
22
+ approvals?: ReadonlyArray<{
23
+ id: string;
24
+ call_id: string;
25
+ }>;
18
26
  onError?: (error: Error) => string;
19
27
  }): ReadableStream<UIMessageChunk>;
20
28
  export declare function createAgentCoreUIResponse(stream: ReadableStream<UIMessageChunk>, init?: ResponseInit): Response;
@@ -13,6 +13,8 @@ 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 approvalCalls = new Map(options.approvals?.map((approval) => [approval.id, approval.call_id]));
17
+ const rejectedApprovals = new Set();
16
18
  let lastModelHadToolCalls = false;
17
19
  let outcome;
18
20
  try {
@@ -116,6 +118,7 @@ export async function* projectAgentCoreEvents(turn, options) {
116
118
  lastModelHadToolCalls = event.finish_reason === "tool_calls";
117
119
  break;
118
120
  case "approval_requested": {
121
+ approvalCalls.set(event.approval_id, event.call_id);
119
122
  const call = calls.get(event.call_id);
120
123
  if (!call)
121
124
  throw new Error(`Missing approval tool call: ${event.call_id}`);
@@ -133,6 +136,11 @@ export async function* projectAgentCoreEvents(turn, options) {
133
136
  break;
134
137
  }
135
138
  case "approval_resolved":
139
+ if (!event.approved) {
140
+ const callId = approvalCalls.get(event.approval_id);
141
+ if (callId)
142
+ rejectedApprovals.add(callId);
143
+ }
136
144
  yield {
137
145
  type: "tool-approval-response",
138
146
  approvalId: event.approval_id,
@@ -169,14 +177,14 @@ export async function* projectAgentCoreEvents(turn, options) {
169
177
  const call = calls.get(event.call_id);
170
178
  if (!call)
171
179
  throw new Error(`Missing UI tool call: ${event.call_id}`);
172
- if (event.denied) {
180
+ if (event.denied && rejectedApprovals.has(event.call_id)) {
173
181
  part = {
174
182
  type: "tool-output-denied",
175
183
  toolCallId: call.id,
176
184
  toolName: call.name,
177
185
  };
178
186
  }
179
- else if (event.is_error) {
187
+ else if (event.is_error || event.denied) {
180
188
  const error = toolErrorSchema.safeParse(event.output);
181
189
  part = {
182
190
  type: "tool-error",
@@ -273,7 +281,10 @@ export function createAgentCoreUIStream(core, message, host, options) {
273
281
  ? AbortSignal.any([options.signal, cancellation.signal])
274
282
  : cancellation.signal;
275
283
  const turn = runAgentCore(core, message, host, signal);
276
- const projected = projectAgentCoreEvents(turn, options);
284
+ const projected = projectAgentCoreEvents(turn, {
285
+ ...options,
286
+ approvals: core.pendingApprovals,
287
+ });
277
288
  let cancelled = false;
278
289
  return new ReadableStream({
279
290
  async pull(controller) {
@@ -35,6 +35,15 @@ export interface AgentCoreHost {
35
35
  }
36
36
  /** One turn per instance. All model/tool progression executes inside Rust. */
37
37
  export declare function createAgentCore(module: WebAssembly.Module, config: AgentCoreConfig): {
38
+ pendingApprovals: {
39
+ id: string;
40
+ call_id: string;
41
+ response?: {
42
+ id: string;
43
+ approved: boolean;
44
+ reason?: string | undefined;
45
+ } | undefined;
46
+ }[];
38
47
  run(message: AgentCoreInput, host: AgentCoreHost, emit: (event: AgentCoreEvent) => Promise<void>, signal: AbortSignal): Promise<{
39
48
  output?: z.util.JSONType | undefined;
40
49
  reason: "awaiting_approval" | "awaiting_tool_results" | "cancelled" | "completed" | "failed" | "model_limit" | "step_limit" | "stopped";
@@ -355,6 +355,7 @@ export function createAgentCore(module, config) {
355
355
  throw new Error("Unsupported Agent Core ABI.");
356
356
  const run = jspi.promising(exports.agent_run);
357
357
  return {
358
+ pendingApprovals: configuration.pending_step?.approvals ?? [],
358
359
  async run(message, host, emit, signal) {
359
360
  const outcome = await this.runOutcome(message, host, emit, signal);
360
361
  if (outcome.details)
package/dist/index.d.ts CHANGED
@@ -427,22 +427,12 @@ export declare function defineAgentReference<const TSlug extends WorkerAgentSlug
427
427
  export type DefineRemoteAgentInput = {
428
428
  agentId: string;
429
429
  description: string;
430
- url?: string;
431
- environment?: "preview" | "production" | "local";
432
- auth?: {
433
- bearerTokenEnv: string;
434
- };
435
430
  };
436
- /** Invoke a fixed Agent ID through Agents API; cross-service credentials stay in host secrets. */
431
+ /** Invoke an exported Agent in the current Project using the caller's authority and environment. */
437
432
  export declare function defineRemoteAgent(input: DefineRemoteAgentInput): {
438
433
  type: "remote";
439
434
  agentId: string;
440
- url?: string | undefined;
441
- environment?: "local" | "preview" | "production" | undefined;
442
435
  description: string;
443
- auth?: {
444
- bearerTokenEnv: string;
445
- } | undefined;
446
436
  };
447
437
  /** @deprecated Enable defineAgent({ sessionTools: true }) instead. */
448
438
  export declare function chatList(): DefinedManagedTool<"chat_list">;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { agentToolOutputOffloadOptionsSchema, } from "@gea-ai/contract/agent-con
2
2
  import { resolveModelOptions, } from "@gea-ai/contract/model-options";
3
3
  export { aiSdk } from "./ai-sdk.js";
4
4
  import { describeAgentContext } from "./context.js";
5
- import { workerAgentReferenceSchema, remoteAgentReferenceSchema, } from "@gea-ai/contract/agent-references";
5
+ import { workerAgentReferenceSchema, remoteAgentDeclarationSchema, } from "@gea-ai/contract/agent-references";
6
6
  import { agentConnectorAuthSchema, agentConnectorDefinitionSchema, agentConnectorGrantSchema, agentConnectorRuntimeSchema, agentEnvironmentVariableSchema, getAgentConnectorPrincipalType, agentPackageSnapshotSchema, agentSlugSchema, agentSkillSchema, agentToolSchema, GEA_AGENT_PACKAGE_SCHEMA_VERSION, } from "@gea-ai/contract/agent-packages";
7
7
  import { agentChannelKeySchema, agentChannelPathSchema, agentCustomChannelDefinitionSchema, agentHttpChannelDefinitionSchema, canonicalAgentChannelJson, } from "@gea-ai/contract/agent-channels";
8
8
  import { z } from "zod";
@@ -331,9 +331,9 @@ export function defineAgentReference(input) {
331
331
  });
332
332
  return { ...reference, slug: input.slug };
333
333
  }
334
- /** Invoke a fixed Agent ID through Agents API; cross-service credentials stay in host secrets. */
334
+ /** Invoke an exported Agent in the current Project using the caller's authority and environment. */
335
335
  export function defineRemoteAgent(input) {
336
- return remoteAgentReferenceSchema.parse({ ...input, type: "remote" });
336
+ return remoteAgentDeclarationSchema.parse({ ...input, type: "remote" });
337
337
  }
338
338
  /** @deprecated Enable defineAgent({ sessionTools: true }) instead. */
339
339
  export function chatList() {
@@ -4,4 +4,10 @@ import type { AgentArtifacts, AgentToolModelOutput } from "@gea-ai/contract/stud
4
4
  /** Persist only immutable references in the model context, never signed URLs or bytes. */
5
5
  export declare function storeToolModelOutput(output: AgentToolModelOutput, artifacts: AgentArtifacts | undefined, signal?: AbortSignal): Promise<ToolResultOutput>;
6
6
  /** Resolve scoped Artifact access for this request without mutating durable messages. */
7
- export declare function resolveToolArtifacts(messages: ModelMessage[], artifacts: (toolName: string, toolCallId: string) => AgentArtifacts | undefined): Promise<ModelMessage[]>;
7
+ export declare function resolveToolArtifacts(messages: ModelMessage[], readImage: (id: string, mediaType: string) => Promise<{
8
+ mediaType: string;
9
+ data: {
10
+ type: "data";
11
+ data: Uint8Array;
12
+ };
13
+ }>): Promise<ModelMessage[]>;
@@ -68,7 +68,7 @@ export async function storeToolModelOutput(output, artifacts, signal) {
68
68
  return { type: "content", value };
69
69
  }
70
70
  /** Resolve scoped Artifact access for this request without mutating durable messages. */
71
- export async function resolveToolArtifacts(messages, artifacts) {
71
+ export async function resolveToolArtifacts(messages, readImage) {
72
72
  return Promise.all(messages.map(async (message) => {
73
73
  if (message.role !== "tool")
74
74
  return message;
@@ -83,15 +83,11 @@ export async function resolveToolArtifacts(messages, artifacts) {
83
83
  typeof item.data.reference !== "object" ||
84
84
  !item.data.reference.geaArtifactId)
85
85
  return item;
86
- const scoped = artifacts(part.toolName, part.toolCallId);
87
- if (!scoped)
88
- throw new Error("Tool Artifact references require Agent Artifacts.");
89
- const { downloadUrl } = await scoped.get({
90
- id: item.data.reference.geaArtifactId,
91
- });
86
+ const image = await readImage(item.data.reference.geaArtifactId, item.mediaType);
92
87
  return {
93
88
  ...item,
94
- data: { type: "url", url: new URL(downloadUrl) },
89
+ mediaType: image.mediaType,
90
+ data: image.data,
95
91
  };
96
92
  }));
97
93
  return { ...part, output: { ...part.output, value } };
package/dist/tracing.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { agentSdkVersion } from "./version.js";
1
2
  import { Result } from "better-result";
2
3
  import { captureTraceContext } from "./trace-context.js";
3
4
  import { OpenTelemetry } from "@ai-sdk/otel";
@@ -135,7 +136,7 @@ function initializeTracing() {
135
136
  });
136
137
  trace.setGlobalTracerProvider(provider);
137
138
  registerTelemetry(new OpenTelemetry({
138
- tracer: provider.getTracer("gea.agent-sdk", "0.1.260903-alpha.0"),
139
+ tracer: provider.getTracer("gea.agent-sdk", agentSdkVersion),
139
140
  headers: false,
140
141
  providerMetadata: false,
141
142
  runtimeContext: false,
@@ -167,7 +168,7 @@ export async function traceWorkerRequest(request, name, attributes, execute) {
167
168
  };
168
169
  const parent = extractTraceContext(request.headers).setValue(requestTraceScope, scope);
169
170
  const span = currentProvider
170
- .getTracer("gea.agent-sdk", "0.1.260903-alpha.0")
171
+ .getTracer("gea.agent-sdk", agentSdkVersion)
171
172
  .startSpan(name, {
172
173
  attributes,
173
174
  kind: SpanKind.SERVER,
@@ -346,7 +347,9 @@ export function createAgentTracing(signal) {
346
347
  async withSpan(name, execute, options) {
347
348
  if (!available())
348
349
  return await context.with(suppressTracing(ROOT_CONTEXT), () => execute(view()));
349
- const span = trace.getTracer("gea.agent-sdk").startSpan(name, options);
350
+ const span = trace
351
+ .getTracer("gea.agent-sdk", agentSdkVersion)
352
+ .startSpan(name, options);
350
353
  return await context.with(trace.setSpan(context.active(), span), async () => {
351
354
  // This callback boundary annotates outcomes without changing the protocol.
352
355
  try {
@@ -0,0 +1 @@
1
+ export declare const agentSdkVersion = "0.1.261002-alpha.0";
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/build-sdk-version.mjs from package.json.
2
+ export const agentSdkVersion = "0.1.261002-alpha.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gea-ai/agent-sdk",
3
- "version": "0.1.260925-alpha.0",
3
+ "version": "0.1.261002-alpha.0",
4
4
  "private": false,
5
5
  "homepage": "https://musegea.com/developers",
6
6
  "license": "Apache-2.0",
@@ -115,7 +115,7 @@
115
115
  "@ai-sdk/otel": "1.0.9",
116
116
  "@ai-sdk/provider": "4.0.1",
117
117
  "@ai-sdk/provider-utils": "5.0.2",
118
- "@gea-ai/contract": "0.1.260925-alpha.0",
118
+ "@gea-ai/contract": "0.1.261002-alpha.0",
119
119
  "@opentelemetry/api": "1.9.1",
120
120
  "@opentelemetry/context-async-hooks": "2.10.0",
121
121
  "@opentelemetry/core": "2.10.0",
@@ -151,7 +151,7 @@
151
151
  }
152
152
  },
153
153
  "scripts": {
154
- "build": "node ../../scripts/build-worker-ai.mjs --check && node scripts/build-agent-core.mjs && pnpm --filter @gea-ai/contract build && rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/rewrite-esm-relative-imports.mjs dist && cp src/agent-core.wasm dist/agent-core.wasm",
154
+ "build": "node scripts/build-sdk-version.mjs && node ../../scripts/build-worker-ai.mjs --check && node scripts/build-agent-core.mjs && pnpm --filter @gea-ai/contract build && rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/rewrite-esm-relative-imports.mjs dist && cp src/agent-core.wasm dist/agent-core.wasm",
155
155
  "pack:npm": "pnpm pack --pack-destination dist",
156
156
  "publish:npm": "pnpm publish --access public --tag alpha",
157
157
  "test": "vp test run",