@gea-ai/agent-sdk 0.1.261008-alpha.0 → 0.1.261009-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.
@@ -58,6 +58,13 @@ and packaged Worker conformance fixtures before changing the pin.
58
58
 
59
59
  ## Agent Core WASM
60
60
 
61
+ The bundled Core's Anthropic output-schema sanitizer is ported from
62
+ `@ai-sdk/anthropic` **4.0.67**, `src/sanitize-json-schema.ts` (Vercel AI SDK).
63
+ Copyright 2023 Vercel, Inc.; see
64
+ [license and notice](./third-party/anthropic-schema/LICENSE).
65
+ The Rust port retains the upstream transformation using `serde_json` values;
66
+ the original schema still controls final validation.
67
+
61
68
  The optional Agent Core asset is built from this repository's Apache-2.0 Rust
62
69
  crate with Rust 1.94.1. Its Cargo dependencies are pinned in
63
70
  `crates/agent-core/Cargo.lock`; exact-version license copies and a dependency
Binary file
@@ -633,13 +633,18 @@ export async function handleAgentSessionInboxRequest(request, storage) {
633
633
  canonicalAgentChannelJson(input.identity))
634
634
  return Response.json({ error: "session_identity_conflict" }, { status: 409 });
635
635
  if (state.execution?.id === input.executionId) {
636
- if (path === "/v1/inbox/start") {
637
- if (state.started)
638
- return Response.json({ error: "execution_already_started" }, {
639
- status: 409,
640
- headers: { "x-gea-session-execution": "already-started" },
641
- });
642
- state.started = true;
636
+ if (input.runId && input.runId !== state.execution.turnId)
637
+ return Response.json({ error: "execution_mismatch" }, { status: 409 });
638
+ if (path === "/v1/inbox/start" && state.started)
639
+ return Response.json({ error: "execution_already_started" }, {
640
+ status: 409,
641
+ headers: { "x-gea-session-execution": "already-started" },
642
+ });
643
+ if (!state.started) {
644
+ // An inbox claim retains the previous transport until the Host
645
+ // admits this execution with its current Run authority.
646
+ state.transport = input.transport;
647
+ state.started = path === "/v1/inbox/start";
643
648
  await save(tx, state, runs);
644
649
  }
645
650
  return Response.json({ execution: state.execution });
@@ -66,6 +66,62 @@ function groupMessages(messages) {
66
66
  }
67
67
  return groups;
68
68
  }
69
+ // Provider usage is request-wide, not per message. This deliberately estimates
70
+ // content only: UTF-8 bytes / 3 plus framing, without opaque reasoning signatures
71
+ // or base64 expansion. Media gets a fixed allowance, not an exact token count.
72
+ function estimateMessageTokens(message) {
73
+ const textTokens = (text) => Math.ceil(new TextEncoder().encode(text).length / 3);
74
+ const mediaTokens = 4_096;
75
+ if (typeof message.content === "string")
76
+ return 8 + textTokens(message.content);
77
+ return (8 +
78
+ message.content.reduce((total, part) => {
79
+ switch (part.type) {
80
+ case "text":
81
+ case "reasoning":
82
+ return total + 8 + textTokens(part.text);
83
+ case "image":
84
+ case "reasoning-file":
85
+ return total + mediaTokens;
86
+ case "file":
87
+ return (total +
88
+ (typeof part.data === "object" &&
89
+ "type" in part.data &&
90
+ part.data.type === "text"
91
+ ? textTokens(part.data.text)
92
+ : mediaTokens));
93
+ case "tool-call":
94
+ return (total +
95
+ 16 +
96
+ textTokens(part.toolName) +
97
+ textTokens(JSON.stringify(part.input) ?? ""));
98
+ case "tool-result": {
99
+ const output = part.output;
100
+ let tokens;
101
+ switch (output.type) {
102
+ case "content":
103
+ tokens = output.value.reduce((count, item) => count +
104
+ (item.type === "text"
105
+ ? textTokens(item.text)
106
+ : item.type === "file" && item.data.type === "text"
107
+ ? textTokens(item.data.text)
108
+ : mediaTokens), 0);
109
+ break;
110
+ case "text":
111
+ case "error-text":
112
+ tokens = textTokens(output.value);
113
+ break;
114
+ default:
115
+ tokens = textTokens(JSON.stringify(output));
116
+ }
117
+ return total + 16 + textTokens(part.toolName) + tokens;
118
+ }
119
+ default: {
120
+ return total + 8 + textTokens(JSON.stringify(part));
121
+ }
122
+ }
123
+ }, 0));
124
+ }
69
125
  function getToolOutputText(part) {
70
126
  switch (part.output.type) {
71
127
  case "error-text":
@@ -224,7 +280,7 @@ export async function applyAgentWorkerToolOutputOffload(input) {
224
280
  return input.messages;
225
281
  const groups = groupMessages(input.messages);
226
282
  const firstProtectedIndex = input.protectRecentGroups
227
- ? Math.max(0, groups.length - input.options.preserveRecentGroups)
283
+ ? Math.max(0, groups.length - (input.options.preserveRecentGroups ?? 5))
228
284
  : groups.length;
229
285
  const messages = [];
230
286
  for (const [groupIndex, group] of groups.entries()) {
@@ -263,13 +319,135 @@ function summarySchema() {
263
319
  "## Critical Exact Details\n- Preserve exact IDs, commands, paths, API names, error messages, and config values.",
264
320
  ].join("\n\n");
265
321
  }
322
+ // Summary input is a textual projection, not a provider request. Keep resource
323
+ // references for recovery but never stringify media bytes or opaque metadata.
324
+ function summaryFileData(data) {
325
+ if (data instanceof URL) {
326
+ return data.protocol === "data:"
327
+ ? { omitted: "inline media bytes" }
328
+ : { url: data.href };
329
+ }
330
+ if (typeof data === "string" ||
331
+ data instanceof ArrayBuffer ||
332
+ ArrayBuffer.isView(data)) {
333
+ return { omitted: "inline media bytes; recover from raw chat history" };
334
+ }
335
+ if ("type" in data) {
336
+ switch (data.type) {
337
+ case "text":
338
+ return { text: data.text };
339
+ case "url": {
340
+ const url = String(data.url);
341
+ return url.startsWith("data:")
342
+ ? { omitted: "inline media bytes" }
343
+ : { url };
344
+ }
345
+ case "reference":
346
+ return { reference: data.reference };
347
+ case "data":
348
+ return { omitted: "inline media bytes; recover from raw chat history" };
349
+ }
350
+ }
351
+ return { reference: data };
352
+ }
353
+ function summaryContentPart(part) {
354
+ switch (part.type) {
355
+ case "text":
356
+ case "reasoning":
357
+ return { type: part.type, text: part.text };
358
+ case "tool-call":
359
+ return {
360
+ type: part.type,
361
+ toolName: part.toolName,
362
+ toolCallId: part.toolCallId,
363
+ input: part.input,
364
+ providerExecuted: part.providerExecuted,
365
+ };
366
+ case "tool-result": {
367
+ const output = part.output;
368
+ return {
369
+ type: part.type,
370
+ toolName: part.toolName,
371
+ toolCallId: part.toolCallId,
372
+ output: output.type === "content"
373
+ ? { type: output.type, value: output.value.map(summaryContentPart) }
374
+ : output.type === "execution-denied"
375
+ ? { type: output.type, reason: output.reason }
376
+ : { type: output.type, value: output.value },
377
+ };
378
+ }
379
+ case "file":
380
+ case "file-data":
381
+ return {
382
+ type: part.type,
383
+ mediaType: part.mediaType,
384
+ filename: part.filename,
385
+ data: summaryFileData(part.data),
386
+ };
387
+ case "image":
388
+ return {
389
+ type: part.type,
390
+ mediaType: part.mediaType,
391
+ data: summaryFileData(part.image),
392
+ };
393
+ case "image-data":
394
+ case "reasoning-file":
395
+ return {
396
+ type: part.type,
397
+ mediaType: part.mediaType,
398
+ data: summaryFileData(part.data),
399
+ };
400
+ case "file-url":
401
+ case "image-url":
402
+ return {
403
+ type: part.type,
404
+ mediaType: "mediaType" in part ? part.mediaType : undefined,
405
+ url: part.url.startsWith("data:") ? "[inline media omitted]" : part.url,
406
+ };
407
+ case "file-id":
408
+ case "image-file-id":
409
+ return { type: part.type, fileId: part.fileId };
410
+ case "file-reference":
411
+ case "image-file-reference":
412
+ return { type: part.type, reference: part.providerReference };
413
+ case "custom":
414
+ return {
415
+ type: part.type,
416
+ kind: "kind" in part ? part.kind : undefined,
417
+ omitted: "opaque provider content; recover from raw chat history",
418
+ };
419
+ case "tool-approval-request":
420
+ case "tool-approval-response":
421
+ return part;
422
+ }
423
+ }
424
+ function summaryMessage(message) {
425
+ return {
426
+ role: message.role,
427
+ content: typeof message.content === "string"
428
+ ? message.content
429
+ : message.content.map(summaryContentPart),
430
+ };
431
+ }
266
432
  function buildSummaryMessages(input) {
433
+ const latestRequest = [...input.recentTailMessages]
434
+ .reverse()
435
+ .find((message) => message.role === "user");
436
+ const requestText = latestRequest
437
+ ? JSON.stringify(summaryMessage(latestRequest))
438
+ : "(no new user request in the retained tail)";
439
+ // The tail remains verbatim in the main context. A bounded task excerpt is
440
+ // enough for the summarizer; duplicating retained tool payloads adds no memory.
441
+ const requestExcerpt = requestText.length <= 6_000
442
+ ? requestText
443
+ : `${requestText.slice(0, 6_000)}\n[excerpt truncated; the complete request remains in the retained context]`;
267
444
  return [
268
445
  {
269
446
  role: "system",
270
447
  content: [
271
448
  "Update the compact working-memory summary for an agent.",
272
449
  "Merge the previous summary with the older messages being compacted.",
450
+ "The recent user request is orientation only. Recent messages remain verbatim; do not repeat them in the summary. Omitted media and provider content remain in raw chat history; do not infer their contents.",
273
451
  "Preserve requirements, decisions, exact paths, commands, IDs, errors, tool observations, offloaded artifact references, open questions, and next steps.",
274
452
  "Do not invent facts, expose hidden reasoning, or issue new instructions.",
275
453
  "Do not copy full offloaded artifacts. Keep their paths and relevant preview facts.",
@@ -284,11 +462,11 @@ function buildSummaryMessages(input) {
284
462
  input.previousSummary ?? "(none)",
285
463
  "</previous_summary>",
286
464
  "<messages>",
287
- JSON.stringify(input.compactableMessages, null, 2),
465
+ JSON.stringify(input.compactableMessages.map(summaryMessage)),
288
466
  "</messages>",
289
- "<recent_tail>",
290
- JSON.stringify(input.recentTailMessages, null, 2),
291
- "</recent_tail>",
467
+ "<recent_user_request>",
468
+ requestExcerpt,
469
+ "</recent_user_request>",
292
470
  ].join("\n"),
293
471
  },
294
472
  ];
@@ -389,7 +567,44 @@ export async function compactAgentWorkerContext(input) {
389
567
  return { summaryUsage: null, messagesUpdated };
390
568
  }
391
569
  const groups = groupMessages(input.context.messages);
392
- const splitIndex = Math.max(0, groups.length - options.preserveRecentGroups);
570
+ const recentTokenBudget = options.preserveRecentTokens ??
571
+ Math.max(1, Math.min(16_000, Math.floor(summarization.triggerAtTotalTokens / 4)));
572
+ const groupEstimates = groups.map((group) => group.messages.reduce((total, message) => total + estimateMessageTokens(message), 0));
573
+ const previousSummary = input.context.metadata.contextSummary?.summaryText;
574
+ const projectionEstimate = groupEstimates.reduce((total, tokens) => total + tokens, 0) +
575
+ (previousSummary
576
+ ? estimateMessageTokens(makeSummaryMessage(previousSummary))
577
+ : 0);
578
+ // Usage includes fixed prompt overhead we cannot attribute to individual messages.
579
+ // Allocate it conservatively across the projection instead of treating small
580
+ // local estimates as proof that an over-threshold request already fits.
581
+ const usageScale = Math.max(1, (input.context.contextSize ?? 0) / Math.max(1, projectionEstimate));
582
+ let splitIndex = groups.length;
583
+ let retainedTokens = 0;
584
+ while (splitIndex > 0) {
585
+ const groupTokens = Math.ceil(groupEstimates[splitIndex - 1] * usageScale);
586
+ // Always keep the newest complete group, even when it exceeds the budget.
587
+ if (splitIndex < groups.length &&
588
+ (retainedTokens + groupTokens > recentTokenBudget ||
589
+ (options.preserveRecentGroups !== undefined &&
590
+ groups.length - splitIndex >= options.preserveRecentGroups)))
591
+ break;
592
+ retainedTokens += groupTokens;
593
+ splitIndex--;
594
+ }
595
+ // Fixed overhead alone is not a reason to pay for summarizing a tiny history.
596
+ // Explicit retention limits still take precedence over this default safeguard.
597
+ if (options.preserveRecentTokens === undefined &&
598
+ options.preserveRecentGroups === undefined &&
599
+ groupEstimates
600
+ .slice(0, splitIndex)
601
+ .reduce((total, tokens) => total + tokens, 0) <
602
+ Math.max(1, Math.min(1_024, Math.floor(recentTokenBudget / 4)))) {
603
+ createAgentTracing().currentSpan?.setAttributes({
604
+ "gea.context.compaction.skip_reason": "insufficient_reduction",
605
+ });
606
+ return { summaryUsage: null, messagesUpdated };
607
+ }
393
608
  const compactableMessages = groups
394
609
  .slice(0, splitIndex)
395
610
  .flatMap((group) => group.messages)
@@ -433,7 +648,18 @@ export async function compactAgentWorkerContext(input) {
433
648
  if (part.type === "content_delta" && kinds.get(part.id) === "text")
434
649
  summaryText += part.delta;
435
650
  }
436
- summaryUsage = (await result.result).usage;
651
+ const completion = await result.result;
652
+ summaryUsage = completion.usage;
653
+ if (completion.finishReason !== "stop") {
654
+ // The provider call still counts toward usage, but an incomplete
655
+ // summary must not replace the previous summary or active history.
656
+ span.setAttributes({
657
+ "gea.context.compaction.outcome": "rejected",
658
+ "gea.context.compaction.finish_reason": completion.finishReason,
659
+ "gea.context.messages.output_count": input.context.messages.length,
660
+ });
661
+ return { summaryUsage, messagesUpdated };
662
+ }
437
663
  if (!summaryText.trim())
438
664
  throw new Error("Context summary was empty.");
439
665
  const missingHeadings = REQUIRED_SUMMARY_HEADINGS.filter((heading) => !summaryText.includes(heading));
@@ -480,7 +706,11 @@ export async function compactAgentWorkerContext(input) {
480
706
  "gea.context.tokens.estimate": input.context.contextSize ?? undefined,
481
707
  "gea.context.tokens.threshold": summarization.triggerAtTotalTokens,
482
708
  "gea.context.messages.compacted_count": compactableMessages.length,
483
- "gea.context.groups.preserved": options.preserveRecentGroups,
709
+ "gea.context.groups.preserved": groups.length - splitIndex,
710
+ "gea.context.tokens.retained_estimate": retainedTokens,
711
+ "gea.context.tokens.usage_scale": usageScale,
712
+ "gea.context.tokens.retained_budget": recentTokenBudget,
713
+ "gea.context.tokens.retained_over_budget": retainedTokens > recentTokenBudget,
484
714
  },
485
715
  });
486
716
  }
@@ -1,3 +1,4 @@
1
+ import { contextSystemInputSchema } from "@gea-ai/contract/agent-worker-tools";
1
2
  import { getAgentProgrammaticToolCalling, isAgentConnectorConnectionHelperExposed, } from "@gea-ai/contract/agent-packages";
2
3
  import { AGENT_WORKER_TOOL_METADATA_HEADER, AGENT_WORKER_TOOL_METADATA_VERSION, } from "@gea-ai/contract/agent-worker-tools";
3
4
  import { isAgentConnectorExposed } from "@gea-ai/contract/agent-packages";
@@ -2232,6 +2233,21 @@ function createHostBackedAgentTools(input) {
2232
2233
  inputSchema: webSearchInputSchema,
2233
2234
  });
2234
2235
  }
2236
+ else if (definition.id === "context_system") {
2237
+ registerAgentWorkerTool(tools, "contextSystem", tool({
2238
+ description: "Access the current OAuth user's GEA Context resources. Call action=list to discover operations and their input schemas, then action=call with tool and arguments. Documents use Markdown; read before editing and retain contentHash for conflict checks. Requires OAuth; Project keys, custom principals and local calls are unsupported.",
2239
+ inputSchema: contextSystemInputSchema,
2240
+ toModelOutput: mcpToolModelOutput,
2241
+ execute: async (toolInput, options) => invokeHostTool({
2242
+ host: input.host,
2243
+ messages: options.messages,
2244
+ signal: options.abortSignal,
2245
+ toolCallId: options.toolCallId,
2246
+ toolInput,
2247
+ toolName: definition.id,
2248
+ }),
2249
+ }));
2250
+ }
2235
2251
  else if (definition.id === "agent") {
2236
2252
  // The negotiated target catalog below owns registration, including declared subagents.
2237
2253
  }
@@ -2570,7 +2586,6 @@ async function requestSessionJson(input) {
2570
2586
  method: input.method,
2571
2587
  })),
2572
2588
  });
2573
- input.onResponse?.(response);
2574
2589
  if (!response.ok)
2575
2590
  throw new Error(`AgentSession rejected ${input.path} (${response.status}).`);
2576
2591
  return input.schema.parse(await response.json());
@@ -3027,15 +3042,9 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3027
3042
  }
3028
3043
  const mutateSession = (values) => {
3029
3044
  const queuedAt = performance.now();
3030
- const mutationSpan = trace
3031
- .getTracer("gea.agent-session")
3032
- .startSpan("agent.session.mutation", {
3033
- attributes: { "gea.agent.run.id": payload.runId },
3034
- });
3035
3045
  const result = writeTail.then(async () => {
3036
3046
  const startedAt = performance.now();
3037
3047
  const queueMs = startedAt - queuedAt;
3038
- const sequence = mutationSequence;
3039
3048
  let failed = true;
3040
3049
  try {
3041
3050
  if (writeFailure)
@@ -3047,12 +3056,6 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3047
3056
  namespace: input.sessionNamespace,
3048
3057
  path: "/v2/mutations",
3049
3058
  schema: agentSessionExecutionMutationResultSchema,
3050
- onResponse(response) {
3051
- mutationSpan.setAttribute("http.response.status_code", response.status);
3052
- const timing = response.headers.get("server-timing");
3053
- if (timing)
3054
- mutationSpan.setAttribute("gea.session.mutation.server_timing", timing);
3055
- },
3056
3059
  });
3057
3060
  mutationSequence += 1;
3058
3061
  failed = false;
@@ -3073,22 +3076,6 @@ async function executeAgentWorkerRequest(input, waitUntil, ownComputer, ownSessi
3073
3076
  "gea.session.persistence.queue_ms": Math.round(persistenceTotals.queueMs),
3074
3077
  "gea.session.persistence.request_ms": Math.round(persistenceTotals.requestMs),
3075
3078
  });
3076
- mutationSpan.setAttributes({
3077
- "gea.session.mutation.sequence": sequence,
3078
- "gea.session.mutation.kinds": Object.keys(values),
3079
- "gea.session.mutation.event_count": values.events?.length ?? 0,
3080
- "gea.session.mutation.queue_ms": Math.round(queueMs),
3081
- "gea.session.mutation.request_ms": Math.round(requestMs),
3082
- ...(invocationTiming.lastModelFinishedAt === undefined
3083
- ? {}
3084
- : {
3085
- "gea.session.mutation.start_after_model_ms": Math.round(startedAt - invocationTiming.lastModelFinishedAt),
3086
- "gea.session.mutation.end_after_model_ms": Math.round(performance.now() - invocationTiming.lastModelFinishedAt),
3087
- }),
3088
- });
3089
- if (failed)
3090
- mutationSpan.setStatus({ code: SpanStatusCode.ERROR });
3091
- mutationSpan.end();
3092
3079
  if (failed || queueMs + requestMs >= 250)
3093
3080
  console.info("AgentSession mutation timing", {
3094
3081
  runId: payload.runId,
package/dist/index.d.ts CHANGED
@@ -429,7 +429,7 @@ type CustomAgentToolDefinition = {
429
429
  readonly type: "custom";
430
430
  readonly [agentToolConnectorAliases]?: Readonly<Record<string, string>>;
431
431
  };
432
- type ManagedToolId = "agent" | "chat_list" | "chat_read" | "chat_send" | "web_search";
432
+ type ManagedToolId = "agent" | "chat_list" | "chat_read" | "chat_send" | "web_search" | "context_system";
433
433
  export type DefinedManagedTool<TId extends ManagedToolId = ManagedToolId> = {
434
434
  readonly id: TId;
435
435
  readonly type: "managed";
@@ -477,6 +477,8 @@ export declare function chatRead(): DefinedManagedTool<"chat_read">;
477
477
  /** @deprecated Enable defineAgent({ sessionTools: true }) instead. */
478
478
  export declare function chatSend(): DefinedManagedTool<"chat_send">;
479
479
  export declare function webSearch(): DefinedManagedTool<"web_search">;
480
+ /** Access the current OAuth user's GEA Context without a separate Connection. */
481
+ export declare function contextSystem(): DefinedManagedTool<"context_system">;
480
482
  type LegacyAgentToolPreset = {
481
483
  readonly preset: "coding" | "default";
482
484
  readonly type: "preset";
package/dist/index.js CHANGED
@@ -368,6 +368,10 @@ export function chatSend() {
368
368
  export function webSearch() {
369
369
  return { id: "web_search", type: "managed" };
370
370
  }
371
+ /** Access the current OAuth user's GEA Context without a separate Connection. */
372
+ export function contextSystem() {
373
+ return { id: "context_system", type: "managed" };
374
+ }
371
375
  export function defineToolSet(tools) {
372
376
  return { tools, type: "tool-set" };
373
377
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const agentSdkVersion = "0.1.261008-alpha.0";
1
+ export declare const agentSdkVersion = "0.1.261009-alpha.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/build-sdk-version.mjs from package.json.
2
- export const agentSdkVersion = "0.1.261008-alpha.0";
2
+ export const agentSdkVersion = "0.1.261009-alpha.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gea-ai/agent-sdk",
3
- "version": "0.1.261008-alpha.0",
3
+ "version": "0.1.261009-alpha.0",
4
4
  "private": false,
5
5
  "homepage": "https://musegea.com/developers",
6
6
  "license": "Apache-2.0",
@@ -119,7 +119,7 @@
119
119
  "@ai-sdk/otel": "1.0.119",
120
120
  "@ai-sdk/provider": "4.0.19",
121
121
  "@ai-sdk/provider-utils": "5.0.50",
122
- "@gea-ai/contract": "0.1.261008-alpha.0",
122
+ "@gea-ai/contract": "0.1.261009-alpha.0",
123
123
  "@opentelemetry/api": "1.9.1",
124
124
  "@opentelemetry/context-async-hooks": "2.10.0",
125
125
  "@opentelemetry/core": "2.10.0",
@@ -0,0 +1,13 @@
1
+ Copyright 2023 Vercel, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.