@gea-ai/agent-sdk 0.1.260923-alpha.0 → 0.1.260923-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.
@@ -471,31 +471,43 @@ export async function handleAgentSessionExecutionRequest(request, storage) {
471
471
  }));
472
472
  }
473
473
  if (request.method === "POST" && url.pathname === "/v2/mutations") {
474
+ const startedAt = performance.now();
474
475
  const input = agentSessionExecutionMutationSchema.parse(await readBody(request, storage));
475
476
  const inputDigest = await digest(input);
476
- return json(await storage.transaction(async (tx) => {
477
- const turn = await requireAttempt(tx, input);
478
- if (input.mutationSequence === turn.nextMutationSequence - 1 &&
479
- turn.lastMutation?.digest === inputDigest)
480
- return { duplicate: true, revision: turn.lastMutation.revision };
481
- if (input.mutationSequence !== turn.nextMutationSequence)
482
- throw new SessionProtocolError("mutation_sequence_mismatch");
483
- if (input.context && input.context.runId !== input.runId)
484
- throw new SessionProtocolError("context_run_mismatch");
485
- const previousRevision = await readRevision(tx);
486
- await writeValues(tx, input);
487
- const activeTurn = {
488
- ...turn,
489
- nextMutationSequence: turn.nextMutationSequence + 1,
490
- lastMutation: {
491
- digest: inputDigest,
492
- revision: previousRevision + 1,
493
- },
494
- };
495
- await tx.put(key("active-turn"), activeTurn);
496
- const revision = await advanceRevision(tx);
497
- return { duplicate: false, revision };
498
- }));
477
+ const parsedAt = performance.now();
478
+ let handlerMs = 0;
479
+ const result = await storage.transaction(async (tx) => {
480
+ const handlerStartedAt = performance.now();
481
+ try {
482
+ const turn = await requireAttempt(tx, input);
483
+ if (input.mutationSequence === turn.nextMutationSequence - 1 &&
484
+ turn.lastMutation?.digest === inputDigest)
485
+ return { duplicate: true, revision: turn.lastMutation.revision };
486
+ if (input.mutationSequence !== turn.nextMutationSequence)
487
+ throw new SessionProtocolError("mutation_sequence_mismatch");
488
+ if (input.context && input.context.runId !== input.runId)
489
+ throw new SessionProtocolError("context_run_mismatch");
490
+ const previousRevision = await readRevision(tx);
491
+ await writeValues(tx, input);
492
+ const activeTurn = {
493
+ ...turn,
494
+ nextMutationSequence: turn.nextMutationSequence + 1,
495
+ lastMutation: {
496
+ digest: inputDigest,
497
+ revision: previousRevision + 1,
498
+ },
499
+ };
500
+ await tx.put(key("active-turn"), activeTurn);
501
+ const revision = await advanceRevision(tx);
502
+ return { duplicate: false, revision };
503
+ }
504
+ finally {
505
+ handlerMs = performance.now() - handlerStartedAt;
506
+ }
507
+ });
508
+ const response = json(result);
509
+ response.headers.set("server-timing", `session-parse;dur=${(parsedAt - startedAt).toFixed(1)}, session-handler;dur=${handlerMs.toFixed(1)}, session-transaction;dur=${(performance.now() - parsedAt).toFixed(1)}`);
510
+ return response;
499
511
  }
500
512
  if (request.method === "POST" && url.pathname === "/v2/turns/pause") {
501
513
  const input = agentSessionExecutionPauseSchema.parse(await readBody(request, storage));
@@ -2604,6 +2604,7 @@ async function requestSessionJson(input) {
2604
2604
  method: input.method,
2605
2605
  })),
2606
2606
  });
2607
+ input.onResponse?.(response);
2607
2608
  if (!response.ok)
2608
2609
  throw new Error(`AgentSession rejected ${input.path} (${response.status}).`);
2609
2610
  return input.schema.parse(await response.json());
@@ -3025,13 +3026,19 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3025
3026
  }
3026
3027
  const mutateSession = (values) => {
3027
3028
  const queuedAt = performance.now();
3029
+ const mutationSpan = trace
3030
+ .getTracer("gea.agent-session")
3031
+ .startSpan("agent.session.mutation", {
3032
+ attributes: { "gea.agent.run.id": payload.runId },
3033
+ });
3028
3034
  const result = writeTail.then(async () => {
3029
- if (writeFailure)
3030
- throw writeFailure;
3031
3035
  const startedAt = performance.now();
3032
3036
  const queueMs = startedAt - queuedAt;
3037
+ const sequence = mutationSequence;
3033
3038
  let failed = true;
3034
3039
  try {
3040
+ if (writeFailure)
3041
+ throw writeFailure;
3035
3042
  await requestSessionJson({
3036
3043
  body: { ...values, ...sessionExecution, mutationSequence },
3037
3044
  chatId: payload.chatId,
@@ -3039,6 +3046,12 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3039
3046
  namespace: input.sessionNamespace,
3040
3047
  path: "/v2/mutations",
3041
3048
  schema: agentSessionExecutionMutationResultSchema,
3049
+ onResponse(response) {
3050
+ mutationSpan.setAttribute("http.response.status_code", response.status);
3051
+ const timing = response.headers.get("server-timing");
3052
+ if (timing)
3053
+ mutationSpan.setAttribute("gea.session.mutation.server_timing", timing);
3054
+ },
3042
3055
  });
3043
3056
  mutationSequence += 1;
3044
3057
  failed = false;
@@ -3059,6 +3072,22 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3059
3072
  "gea.session.persistence.queue_ms": Math.round(persistenceTotals.queueMs),
3060
3073
  "gea.session.persistence.request_ms": Math.round(persistenceTotals.requestMs),
3061
3074
  });
3075
+ mutationSpan.setAttributes({
3076
+ "gea.session.mutation.sequence": sequence,
3077
+ "gea.session.mutation.kinds": Object.keys(values),
3078
+ "gea.session.mutation.event_count": values.events?.length ?? 0,
3079
+ "gea.session.mutation.queue_ms": Math.round(queueMs),
3080
+ "gea.session.mutation.request_ms": Math.round(requestMs),
3081
+ ...(invocationTiming.lastModelFinishedAt === undefined
3082
+ ? {}
3083
+ : {
3084
+ "gea.session.mutation.start_after_model_ms": Math.round(startedAt - invocationTiming.lastModelFinishedAt),
3085
+ "gea.session.mutation.end_after_model_ms": Math.round(performance.now() - invocationTiming.lastModelFinishedAt),
3086
+ }),
3087
+ });
3088
+ if (failed)
3089
+ mutationSpan.setStatus({ code: SpanStatusCode.ERROR });
3090
+ mutationSpan.end();
3062
3091
  if (failed || queueMs + requestMs >= 250)
3063
3092
  console.info("AgentSession mutation timing", {
3064
3093
  runId: payload.runId,
@@ -3876,6 +3905,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3876
3905
  let contextCheckpoint;
3877
3906
  let reusedContextCheckpoints = 0;
3878
3907
  let modelCallsInitialized = false;
3908
+ const checkpointModelCalls = [];
3879
3909
  const persistModelContext = () => {
3880
3910
  const modelContext = {
3881
3911
  contextSize: context.contextSize,
@@ -3887,14 +3917,19 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3887
3917
  // Lifecycle callbacks can checkpoint the same projection several times.
3888
3918
  // Reuse the pending write too: an unchanged snapshot must still await its
3889
3919
  // durable commit, including propagating a failed commit to every waiter.
3890
- if (contextCheckpoint?.serialized === serialized) {
3920
+ if (contextCheckpoint?.serialized === serialized &&
3921
+ checkpointModelCalls.length === 0) {
3891
3922
  persistenceSpan?.setAttribute("gea.session.persistence.context_reused", ++reusedContextCheckpoints);
3892
3923
  return contextCheckpoint.pending;
3893
3924
  }
3894
3925
  const pending = sessionExecution
3895
3926
  ? mutateSession({
3896
3927
  context: modelContext,
3897
- ...(!modelCallsInitialized ? { modelCalls: [] } : {}),
3928
+ ...(checkpointModelCalls.length > 0
3929
+ ? { modelCalls: checkpointModelCalls.splice(0) }
3930
+ : !modelCallsInitialized
3931
+ ? { modelCalls: [] }
3932
+ : {}),
3898
3933
  })
3899
3934
  : requestSessionJson({
3900
3935
  body: { context: modelContext },
@@ -3941,11 +3976,14 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3941
3976
  },
3942
3977
  });
3943
3978
  const modelCalls = [];
3944
- const recordModelCall = async (entry) => {
3979
+ const recordModelCall = async (entry, checkpoint = false) => {
3945
3980
  const call = agentModelCallSchema.parse(entry);
3946
3981
  modelCalls.push(call);
3947
3982
  if (sessionExecution) {
3948
- await mutateSession({ modelCalls: [call] });
3983
+ if (checkpoint)
3984
+ checkpointModelCalls.push(call);
3985
+ else
3986
+ await mutateSession({ modelCalls: [call] });
3949
3987
  }
3950
3988
  else {
3951
3989
  await appendStreamEvent({ type: "data-model-call", data: call }, `model-call:${call.callId}`);
@@ -4074,7 +4112,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4074
4112
  failure ??= { error };
4075
4113
  recordExecutionFailure(error);
4076
4114
  };
4077
- const recordMainCall = async (status, usage) => {
4115
+ const recordMainCall = async (status, usage, checkpoint = false) => {
4078
4116
  if (!pendingMainCall)
4079
4117
  return;
4080
4118
  const callId = pendingMainCall;
@@ -4087,7 +4125,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4087
4125
  status,
4088
4126
  usage,
4089
4127
  occurredAt: new Date().toISOString(),
4090
- });
4128
+ }, checkpoint);
4091
4129
  };
4092
4130
  let settling;
4093
4131
  const finishContext = (end) => {
@@ -4100,6 +4138,10 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4100
4138
  if (end?.finishReason === "stop" && !failure)
4101
4139
  await inboxRunner?.join();
4102
4140
  await recordMainCall(input.request.signal.aborted ? "aborted" : "failed", null);
4141
+ // A failed/interrupted Core save or offload can skip the normal model
4142
+ // checkpoint. Persist known usage before any fallible onEnd hook.
4143
+ if (checkpointModelCalls.length > 0)
4144
+ await mutateSession({ modelCalls: checkpointModelCalls.splice(0) });
4103
4145
  await contextRuntime.onEnd(lastStepNumber, end ?? {
4104
4146
  totalUsage: mainUsage ?? {
4105
4147
  inputTokens: undefined,
@@ -4150,6 +4192,8 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4150
4192
  })();
4151
4193
  return settling;
4152
4194
  };
4195
+ let terminalMessage;
4196
+ let terminalEvent;
4153
4197
  const saveResponseMessage = async ({ responseMessage, }) => {
4154
4198
  const saveStartedAt = performance.now();
4155
4199
  const metadata = await contextFinished;
@@ -4172,9 +4216,19 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4172
4216
  ...metadata,
4173
4217
  };
4174
4218
  const transcriptStartedAt = performance.now();
4219
+ const combineTerminal = sessionExecution &&
4220
+ !input.channelJournal &&
4221
+ !inboxRunner?.suspended &&
4222
+ !failure &&
4223
+ metadata.finishReason === "stop";
4175
4224
  // Retain the generated transcript even if context settlement failed.
4176
4225
  // A failed settlement must never produce a successful Run completion.
4177
- if (sessionExecution) {
4226
+ if (combineTerminal) {
4227
+ // The managed durable stream drains independently of the HTTP consumer.
4228
+ // Its flush commits this transcript with finish before publishing success.
4229
+ terminalMessage = { ...responseMessage };
4230
+ }
4231
+ else if (sessionExecution) {
4178
4232
  await mutateSession({ messages: [{ ...responseMessage }] });
4179
4233
  }
4180
4234
  else {
@@ -4188,7 +4242,17 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4188
4242
  const transcriptFinishedAt = performance.now();
4189
4243
  if (failure)
4190
4244
  throw failure.error;
4191
- await persistModelContext();
4245
+ try {
4246
+ await persistModelContext();
4247
+ }
4248
+ catch (error) {
4249
+ // Preserve the transcript on a failed final context write as before.
4250
+ if (terminalMessage) {
4251
+ await mutateSession({ messages: [{ ...terminalMessage }] });
4252
+ terminalMessage = undefined;
4253
+ }
4254
+ throw error;
4255
+ }
4192
4256
  const contextPersistedAt = performance.now();
4193
4257
  persistenceSpan?.setAttributes({
4194
4258
  "gea.session.finish.context_wait_ms": Math.round(contextFinishedAt - saveStartedAt),
@@ -4530,7 +4594,9 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4530
4594
  const usage = coreUsage(part.usage);
4531
4595
  mainUsage = addAgentWorkerUsage(mainUsage, usage);
4532
4596
  invocationTiming.lastModelFinishedAt = performance.now();
4533
- await recordMainCall("completed", usage);
4597
+ // Core saves its validated model response before running tools or
4598
+ // declaring completion. Commit usage with that same checkpoint.
4599
+ await recordMainCall("completed", usage, true);
4534
4600
  modelSpan?.setAttribute("gen_ai.output.messages", JSON.stringify(toModelMessages([part.message])));
4535
4601
  if (usage.inputTokens !== undefined)
4536
4602
  modelSpan?.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens);
@@ -4654,7 +4720,7 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4654
4720
  mainUsage = addAgentWorkerUsage(mainUsage, step.usage);
4655
4721
  lastStepNumber = step.stepNumber;
4656
4722
  try {
4657
- await recordMainCall(step.finishReason === "error" ? "failed" : "completed", step.usage);
4723
+ await recordMainCall(step.finishReason === "error" ? "failed" : "completed", step.usage, true);
4658
4724
  await contextRuntime.offload();
4659
4725
  await persistModelContext();
4660
4726
  await contextRuntime.onStepEnd(step.stepNumber, {
@@ -4804,6 +4870,13 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4804
4870
  controller.enqueue(chunk);
4805
4871
  continue;
4806
4872
  }
4873
+ if (sessionExecution &&
4874
+ !input.channelJournal &&
4875
+ !inboxRunner?.suspended &&
4876
+ metadata.finishReason === "stop") {
4877
+ terminalEvent = chunk;
4878
+ continue;
4879
+ }
4807
4880
  }
4808
4881
  events.push(chunk);
4809
4882
  }
@@ -4817,8 +4890,20 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
4817
4890
  for (const event of events)
4818
4891
  controller.enqueue(event);
4819
4892
  },
4893
+ async flush(controller) {
4894
+ if (!terminalMessage && !terminalEvent)
4895
+ return;
4896
+ const startedAt = performance.now();
4897
+ await mutateSession({
4898
+ ...(terminalMessage ? { messages: [{ ...terminalMessage }] } : {}),
4899
+ ...(terminalEvent ? { events: [terminalEvent] } : {}),
4900
+ });
4901
+ persistenceSpan?.setAttribute("gea.session.finish.transcript_ms", Math.round(performance.now() - startedAt));
4902
+ if (terminalEvent)
4903
+ controller.enqueue(terminalEvent);
4904
+ },
4820
4905
  }));
4821
- if (engine) {
4906
+ if (engine || sessionExecution) {
4822
4907
  const [owned, response] = durableStream.tee();
4823
4908
  waitUntil((async () => {
4824
4909
  const reader = owned.getReader();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gea-ai/agent-sdk",
3
- "version": "0.1.260923-alpha.0",
3
+ "version": "0.1.260923-alpha.2",
4
4
  "private": false,
5
5
  "homepage": "https://musegea.com/developers",
6
6
  "license": "Apache-2.0",
@@ -36,15 +36,6 @@
36
36
  "types": "./dist/evals.d.ts",
37
37
  "import": "./dist/evals.js"
38
38
  },
39
- "./studio-client": {
40
- "types": "./dist/studio-client.d.ts",
41
- "import": "./dist/studio-client.js"
42
- },
43
- "./studio-server": {
44
- "browser": null,
45
- "types": "./dist/studio-server.d.ts",
46
- "import": "./dist/studio-server.js"
47
- },
48
39
  "./computer": {
49
40
  "types": "./dist/computer.d.ts",
50
41
  "import": "./dist/computer.js"
@@ -108,7 +99,7 @@
108
99
  "@ai-sdk/otel": "1.0.9",
109
100
  "@ai-sdk/provider": "4.0.1",
110
101
  "@ai-sdk/provider-utils": "5.0.2",
111
- "@gea-ai/contract": "0.1.260923-alpha.0",
102
+ "@gea-ai/contract": "0.1.260923-alpha.2",
112
103
  "@opentelemetry/api": "1.9.1",
113
104
  "@opentelemetry/context-async-hooks": "2.10.0",
114
105
  "@opentelemetry/core": "2.10.0",
@@ -1,22 +0,0 @@
1
- import { DefaultChatTransport, type ChatTransport, type HttpChatTransportInitOptions, type UIMessage } from "ai";
2
- import type { StudioAgentApiRunContext } from "@gea-ai/contract";
3
- import type { StudioAgentStreamReplay } from "@gea-ai/contract/studio-agent-api";
4
- export type StudioAgentChatTransportOptions<UI_MESSAGE extends UIMessage = UIMessage> = Pick<HttpChatTransportInitOptions<UI_MESSAGE>, "headers" | "credentials" | "fetch"> & {
5
- /** Same-origin application backend path, without /run. Never a GEA URL. */
6
- api: string;
7
- /** Restore an existing GEA Chat; the AI SDK's local chat ID is independent. */
8
- chatId?: string;
9
- /** Called as soon as a response identifies its Run, before reading the stream. */
10
- onRun?: (run: StudioAgentApiRunContext) => void;
11
- /** Replace the matching assistant baseline before AI SDK consumes a full replay. */
12
- onReplay?: (replay: StudioAgentStreamReplay<UI_MESSAGE>) => void | Promise<void>;
13
- };
14
- /** One transport per conversation. The caller's backend keeps the Project API key. */
15
- export declare class StudioAgentChatTransport<UI_MESSAGE extends UIMessage = UIMessage> extends DefaultChatTransport<UI_MESSAGE> {
16
- private serverChatId;
17
- private latestRun;
18
- constructor(options: StudioAgentChatTransportOptions<UI_MESSAGE>);
19
- get chatId(): string | undefined;
20
- get lastRun(): StudioAgentApiRunContext | undefined;
21
- reconnectToStream(options: Parameters<ChatTransport<UI_MESSAGE>["reconnectToStream"]>[0]): Promise<ReadableStream<import("ai").UIMessageChunk<unknown, import("ai").UIDataTypes>> | null>;
22
- }
@@ -1,97 +0,0 @@
1
- import { AGENT_APPLICATION_CHAT_ID_HEADER, AGENT_APPLICATION_RUN_ID_HEADER, AGENT_APPLICATION_REQUEST_ID_HEADER, agentApplicationUserMessageSchema, } from "@gea-ai/contract/agent-applications";
2
- import { DefaultChatTransport, } from "ai";
3
- import { restoreStudioStreamReplay } from "./studio-stream-replay.js";
4
- /** One transport per conversation. The caller's backend keeps the Project API key. */
5
- export class StudioAgentChatTransport extends DefaultChatTransport {
6
- serverChatId;
7
- latestRun;
8
- constructor(options) {
9
- const api = options.api.replace(/\/$/u, "");
10
- if (!api.startsWith("/") || api.startsWith("//") || /[\\\s?#]/u.test(api)) {
11
- throw new Error("Studio transport requires an application backend path such as /api/agent.");
12
- }
13
- super({
14
- api: `${api}/run`,
15
- credentials: options.credentials ?? "same-origin",
16
- headers: options.headers,
17
- fetch: async (url, init) => {
18
- const response = await (options.fetch ?? globalThis.fetch)(url, init);
19
- const chatId = response.headers.get(AGENT_APPLICATION_CHAT_ID_HEADER);
20
- const runId = response.headers.get(AGENT_APPLICATION_RUN_ID_HEADER);
21
- const requestId = response.headers.get(AGENT_APPLICATION_REQUEST_ID_HEADER);
22
- // Failed startup can still create a Chat. Retain it for recovery.
23
- if (chatId)
24
- this.serverChatId = chatId;
25
- if (chatId && runId && requestId) {
26
- this.latestRun = { chatId, runId, requestId };
27
- options.onRun?.(this.latestRun);
28
- }
29
- if (init?.method === "GET" &&
30
- response.ok &&
31
- response.status !== 204 &&
32
- options.onReplay) {
33
- return restoreStudioStreamReplay(response, this.serverChatId, options.onReplay);
34
- }
35
- return response;
36
- },
37
- prepareSendMessagesRequest: ({ messages, trigger }) => {
38
- if (trigger === "regenerate-message") {
39
- throw new Error("Studio Agent API does not support regenerating a previous turn.");
40
- }
41
- const message = messages.at(-1);
42
- if (!message)
43
- throw new Error("Studio Agent API requires a message.");
44
- if (message.role === "assistant") {
45
- if (!this.serverChatId)
46
- throw new Error("Tool continuation requires an existing Studio Chat.");
47
- return {
48
- body: {
49
- chatId: this.serverChatId,
50
- message: {
51
- id: message.id,
52
- role: "assistant",
53
- parts: message.parts,
54
- },
55
- },
56
- };
57
- }
58
- const userMessage = agentApplicationUserMessageSchema.parse({
59
- role: message.role,
60
- parts: message.parts,
61
- });
62
- if (userMessage.parts.some((part) => part.type === "file"))
63
- return {
64
- body: {
65
- ...(this.serverChatId ? { chatId: this.serverChatId } : {}),
66
- message: userMessage,
67
- },
68
- };
69
- const text = message.parts
70
- .filter((part) => part.type === "text")
71
- .map((part) => part.text)
72
- .join("\n");
73
- return {
74
- body: {
75
- ...(this.serverChatId ? { chatId: this.serverChatId } : {}),
76
- message: text,
77
- },
78
- };
79
- },
80
- prepareReconnectToStreamRequest: () => ({
81
- api: `${api}/chats/${encodeURIComponent(this.serverChatId)}/stream`,
82
- }),
83
- });
84
- this.serverChatId = options.chatId;
85
- }
86
- get chatId() {
87
- return this.serverChatId;
88
- }
89
- get lastRun() {
90
- return this.latestRun;
91
- }
92
- async reconnectToStream(options) {
93
- if (!this.serverChatId)
94
- return null;
95
- return super.reconnectToStream(options);
96
- }
97
- }
@@ -1,29 +0,0 @@
1
- import { type StudioAgentCreateUploadInput, type StudioAgentApiRunInput } from "@gea-ai/contract/studio-agent-api";
2
- export type StudioAgentClientOptions = {
3
- /** GEA Agent environment base URL or the copied canonical /run URL. */
4
- api: string;
5
- /** Project API key or current GEA user's OAuth access token. */
6
- token: string;
7
- fetch?: typeof globalThis.fetch;
8
- };
9
- /**
10
- * Backend-to-GEA client. The application authenticates users and checks Chat
11
- * ownership before calling this client; it does not accept browser Requests.
12
- */
13
- export declare class StudioAgentClient {
14
- private readonly options;
15
- private readonly api;
16
- private readonly token;
17
- constructor(options: StudioAgentClientOptions);
18
- run(input: StudioAgentApiRunInput, options?: {
19
- signal?: AbortSignal;
20
- }): Promise<Response>;
21
- resumeStream(chatId: string, options?: {
22
- signal?: AbortSignal;
23
- }): Promise<Response>;
24
- /** Reserve a checksum-bound direct upload; PUT the file to the returned uploadUrl. */
25
- createUpload(input: StudioAgentCreateUploadInput, options?: {
26
- signal?: AbortSignal;
27
- }): Promise<Response>;
28
- private request;
29
- }
@@ -1,92 +0,0 @@
1
- import { buildAgentApplicationPath, parseAgentApplicationPath, AGENT_APPLICATION_CHAT_ID_HEADER, AGENT_APPLICATION_RUN_ID_HEADER, AGENT_APPLICATION_REQUEST_ID_HEADER, } from "@gea-ai/contract/agent-applications";
2
- import { studioAgentApiRunInputSchema, studioAgentClientRunInputSchema, studioAgentCreateUploadInputSchema, } from "@gea-ai/contract/studio-agent-api";
3
- /**
4
- * Backend-to-GEA client. The application authenticates users and checks Chat
5
- * ownership before calling this client; it does not accept browser Requests.
6
- */
7
- export class StudioAgentClient {
8
- options;
9
- api;
10
- token;
11
- constructor(options) {
12
- this.options = options;
13
- const url = new URL(options.api);
14
- const local = url.hostname === "localhost" ||
15
- url.hostname.endsWith(".localhost") ||
16
- url.hostname === "127.0.0.1" ||
17
- url.hostname === "[::1]";
18
- if ((url.protocol !== "https:" && !(url.protocol === "http:" && local)) ||
19
- url.username ||
20
- url.password ||
21
- url.search ||
22
- url.hash) {
23
- throw new Error("Studio server client requires an HTTPS Agent URL without credentials, query or fragment (HTTP is allowed on localhost).");
24
- }
25
- if (!options.token?.trim())
26
- throw new Error("Studio server client requires a token.");
27
- const route = parseAgentApplicationPath(url.pathname);
28
- if (route?.agentKey &&
29
- route.segments.length === 1 &&
30
- route.segments[0] === "run") {
31
- url.pathname = buildAgentApplicationPath(route.agentKey);
32
- }
33
- this.token = options.token;
34
- this.api = url.href.replace(/\/$/u, "");
35
- }
36
- async run(input, options = {}) {
37
- const body = studioAgentApiRunInputSchema.parse(input);
38
- return this.request("/run", {
39
- method: "POST",
40
- body: JSON.stringify(body),
41
- signal: options.signal,
42
- });
43
- }
44
- async resumeStream(chatId, options = {}) {
45
- const id = studioAgentClientRunInputSchema.shape.chatId
46
- .unwrap()
47
- .parse(chatId);
48
- return this.request(`/chats/${id}/stream`, {
49
- method: "GET",
50
- signal: options.signal,
51
- });
52
- }
53
- /** Reserve a checksum-bound direct upload; PUT the file to the returned uploadUrl. */
54
- async createUpload(input, options = {}) {
55
- return this.request("/uploads", {
56
- method: "POST",
57
- body: JSON.stringify(studioAgentCreateUploadInputSchema.parse(input)),
58
- signal: options.signal,
59
- });
60
- }
61
- async request(path, init) {
62
- // This is the HTTP response boundary: never expose transport exceptions to the browser.
63
- try {
64
- const upstream = await (this.options.fetch ?? globalThis.fetch)(`${this.api}${path}`, {
65
- ...init,
66
- headers: {
67
- Authorization: `Bearer ${this.token}`,
68
- ...(init.body ? { "Content-Type": "application/json" } : {}),
69
- },
70
- credentials: "omit",
71
- redirect: "error",
72
- cache: "no-store",
73
- });
74
- const headers = new Headers({ "cache-control": "no-store" });
75
- for (const name of [
76
- "content-type",
77
- "x-vercel-ai-ui-message-stream",
78
- AGENT_APPLICATION_CHAT_ID_HEADER,
79
- AGENT_APPLICATION_RUN_ID_HEADER,
80
- AGENT_APPLICATION_REQUEST_ID_HEADER,
81
- ]) {
82
- const value = upstream.headers.get(name);
83
- if (value !== null)
84
- headers.set(name, value);
85
- }
86
- return new Response(upstream.body, { status: upstream.status, headers });
87
- }
88
- catch {
89
- return Response.json({ code: "BAD_GATEWAY", message: "Could not reach the Agent service." }, { status: 502, headers: { "cache-control": "no-store" } });
90
- }
91
- }
92
- }
@@ -1,4 +0,0 @@
1
- import { type UIMessage } from "ai";
2
- import { type StudioAgentStreamReplay } from "@gea-ai/contract/studio-agent-api";
3
- /** Consume only the Run baseline. The remaining SSE stays streamed with backpressure. */
4
- export declare function restoreStudioStreamReplay<MESSAGE extends UIMessage>(response: Response, chatId: string, onReplay: (replay: StudioAgentStreamReplay<MESSAGE>) => void | Promise<void>): Promise<Response>;
@@ -1,62 +0,0 @@
1
- import { validateUIMessages } from "ai";
2
- import { STUDIO_AGENT_REPLAY_PREFIX, studioAgentStreamReplaySchema, } from "@gea-ai/contract/studio-agent-api";
3
- /** Consume only the Run baseline. The remaining SSE stays streamed with backpressure. */
4
- export async function restoreStudioStreamReplay(response, chatId, onReplay) {
5
- if (!response.body)
6
- throw new Error("Missing Agent replay context. Refresh conversation history.");
7
- const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
8
- // The owned replay format starts with one JSON SSE comment line. Its JSON
9
- // escapes embedded CR/LF; the normal AI SDK SSE parser handles all later bytes.
10
- try {
11
- let prefix = "";
12
- while (!prefix.includes("\n")) {
13
- const next = await reader.read();
14
- if (next.done)
15
- throw new Error("Missing Agent replay context. Refresh conversation history.");
16
- prefix += next.value;
17
- }
18
- const end = prefix.indexOf("\n");
19
- const line = prefix.slice(0, end);
20
- if (!line.startsWith(STUDIO_AGENT_REPLAY_PREFIX))
21
- throw new Error("Missing Agent replay context. Refresh conversation history.");
22
- const replay = studioAgentStreamReplaySchema.parse(JSON.parse(line.slice(STUDIO_AGENT_REPLAY_PREFIX.length)));
23
- if (replay.chatId !== chatId)
24
- throw new Error("Agent replay belongs to another Chat.");
25
- // A new Run starts with no parts. This is valid streaming state, although
26
- // AI SDK's validator intentionally rejects empty submitted/history messages.
27
- const message = replay.message.parts.length === 0
28
- ? replay.message
29
- : (await validateUIMessages({ messages: [replay.message] }))[0];
30
- // useChat captures its streaming state after reconnectToStream resolves.
31
- await onReplay({ ...replay, message });
32
- const remainder = prefix.slice(end + 1);
33
- const body = new ReadableStream({
34
- start(controller) {
35
- if (remainder)
36
- controller.enqueue(remainder);
37
- },
38
- async pull(controller) {
39
- const next = await reader.read();
40
- if (next.done) {
41
- reader.releaseLock();
42
- controller.close();
43
- }
44
- else
45
- controller.enqueue(next.value);
46
- },
47
- async cancel(reason) {
48
- await reader.cancel(reason);
49
- reader.releaseLock();
50
- },
51
- }).pipeThrough(new TextEncoderStream());
52
- const headers = new Headers(response.headers);
53
- headers.delete("content-length");
54
- return new Response(body, { status: response.status, headers });
55
- }
56
- catch (error) {
57
- // Cleanup must not replace the original protocol/callback failure.
58
- await reader.cancel(error).catch(() => { });
59
- reader.releaseLock();
60
- throw error;
61
- }
62
- }