agent-accelerator 0.4.0 → 0.4.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-accelerator",
3
- "version": "0.4.0",
3
+ "version": "0.4.5",
4
4
  "description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
5
5
  "license": "MIT",
6
6
  "author": "Sashvat Bharat",
@@ -12,7 +12,8 @@ import { runAgentLoop, streamAgentLoop, type SteerEntry } from "./loop.ts";
12
12
  import { createSessionId } from "../utils/session.ts";
13
13
  import { getSubAgentTrace, subscribeToSubAgent, listSubAgentTraceIds, type SubAgentTrace } from "./delegation.ts";
14
14
  import { saveSessionDir, saveSessionFile } from "../session/store.ts";
15
- import { clearSessionRouting } from "../providers.ts";
15
+ import { clearSessionRouting, normalizeStructuredOutput } from "../providers.ts";
16
+ import type { StructuredOutputSpec } from "../types/model.ts";
16
17
  import { clearInteractionChains } from "../providers/google.ts";
17
18
  import { getModel, getSubModel } from "../utils/env.ts";
18
19
  import { validateModelThinking, ensureModelCatalogFresh } from "../models/catalog.ts";
@@ -139,6 +140,10 @@ export class Agent {
139
140
  readonly tools: Record<string, ToolDefinition> = {};
140
141
  /** Normalized reasoning configuration. */
141
142
  readonly thinkingConfig?: ThinkingConfig;
143
+ /** Canonical structured-output request (raw `AgentConfig.output`). */
144
+ readonly output?: unknown;
145
+ /** Normalized structured-output spec sent to providers. */
146
+ readonly outputSpec?: StructuredOutputSpec;
142
147
  /** Cache retention/session settings. */
143
148
  readonly cacheConfig?: CacheConfig;
144
149
  /** Provider service tier. */
@@ -298,6 +303,12 @@ export class Agent {
298
303
  const mpThinking = (rawModel as any)?.thinkingLevel;
299
304
  this.thinkingConfig = normalizeThinking(config, mpThinking);
300
305
 
306
+ // Structured outputs: validate early so invalid schemas fail fast at construction.
307
+ if (config.output !== undefined && config.output !== null) {
308
+ this.output = config.output;
309
+ this.outputSpec = normalizeStructuredOutput(config.output);
310
+ }
311
+
301
312
  // DX5: cache retention short|medium|long, undefined = no explicit
302
313
  this.cacheConfig = {
303
314
  sessionId: this.sessionId,
@@ -798,6 +809,7 @@ export class Agent {
798
809
  : { ...this.cacheConfig, sessionId: runOptions?.sessionId || this.sessionId },
799
810
  serviceTier: this.serviceTier,
800
811
  sessionId: runOptions?.sessionId || this.sessionId,
812
+ ...(this.outputSpec ? { output: this.outputSpec } : {}),
801
813
  };
802
814
 
803
815
  const loopConfig = {
@@ -815,6 +827,7 @@ export class Agent {
815
827
  onSteerInjected: () => this.persistNow(),
816
828
  // Q-22: loop enforces these (plus local timeout races).
817
829
  limits: { ...this.runLimits },
830
+ ...(this.output !== undefined ? { output: this.output } : {}),
818
831
  };
819
832
 
820
833
  const res = await runAgentLoop(loopConfig);
@@ -865,6 +878,7 @@ export class Agent {
865
878
  : { ...this.cacheConfig, sessionId: runOptions?.sessionId || this.sessionId },
866
879
  serviceTier: this.serviceTier,
867
880
  sessionId: runOptions?.sessionId || this.sessionId,
881
+ ...(this.outputSpec ? { output: this.outputSpec } : {}),
868
882
  };
869
883
 
870
884
  const loopConfig = {
@@ -882,6 +896,7 @@ export class Agent {
882
896
  onSteerInjected: () => this.persistNow(),
883
897
  // Q-22: loop enforces these (plus local timeout races).
884
898
  limits: { ...this.runLimits },
899
+ ...(this.output !== undefined ? { output: this.output } : {}),
885
900
  };
886
901
 
887
902
  const s = streamAgentLoop(loopConfig);
package/src/agent/loop.ts CHANGED
@@ -12,7 +12,7 @@ import { toStandardToolDeclarations } from "../tools/tool.ts";
12
12
  import { executeToolCalls } from "../tools/executor.ts";
13
13
  import { getModelFromCatalog, ensureModelCatalogFresh, validateModelThinking } from "../models/catalog.ts";
14
14
  import { preprocessFilePartsForBypass } from "../utils/documents.ts";
15
- import { noteProviderTurn } from "../providers.ts";
15
+ import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
16
16
 
17
17
  /** One mid-session steer request buffered while a run is active. */
18
18
  export interface SteerEntry {
@@ -42,6 +42,12 @@ export interface AgentLoopConfig {
42
42
  steerInbox?: SteerEntry[];
43
43
  /** Called after steer entries are injected (streaming loops also push `steer_injected` events). */
44
44
  onSteerInjected?: (injectedPrompts: string[]) => void;
45
+ /**
46
+ * Canonical structured-output request (raw `AgentConfig.output`).
47
+ * Normalized per turn for the provider wire; validated once at the end
48
+ * into `AgentResponse.parsed`. Agent-level only.
49
+ */
50
+ output?: unknown;
45
51
  }
46
52
 
47
53
  /** Short preview for `steer_injected` / `queued` event payloads (bounded, display-only). */
@@ -105,6 +111,30 @@ function emitProgress(config: { onProgress?: () => void }): void {
105
111
  try { config.onProgress?.(); } catch {}
106
112
  }
107
113
 
114
+ // ---------------------------------------------------------------------------
115
+ // Structured outputs (canonical output wiring for the loop).
116
+ // ---------------------------------------------------------------------------
117
+
118
+ function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: import("../types/model.ts").StructuredOutputSpec | undefined } {
119
+ const raw = (config as { output?: unknown }).output;
120
+ const fromOptions = (config.options as { output?: import("../types/model.ts").StructuredOutputSpec } | undefined)?.output;
121
+ if (raw !== undefined && raw !== null) {
122
+ // Throws ValidationError for invalid shapes — caught by run/stream catch
123
+ // with partial attached, same as other config validation.
124
+ const spec = normalizeStructuredOutput(raw);
125
+ return { raw, spec };
126
+ }
127
+ if (fromOptions) return { raw: fromOptions, spec: fromOptions };
128
+ return { raw: undefined, spec: undefined };
129
+ }
130
+
131
+ function parseFinalStructuredOutput(text: string, raw: unknown, spec: import("../types/model.ts").StructuredOutputSpec | undefined): unknown | undefined {
132
+ if (raw === undefined && spec === undefined) return undefined;
133
+ // Strict-fail when opted in: invalid JSON/schema throws ValidationError.
134
+ // Tool turns skip this (called only for final no-tool text).
135
+ return parseStructuredOutput(text, raw ?? spec);
136
+ }
137
+
108
138
  /**
109
139
  * Normalizes `maxTurns` to a safe positive integer (default Infinity).
110
140
  * `0` means infinite (no limit), matching the `timeout: 0` convention.
@@ -493,6 +523,8 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
493
523
  const fallbackMaxTurns = normalizeMaxTurns(rawMaxTurns);
494
524
  const limits = resolveLoopLimits(config, fallbackMaxTurns);
495
525
  const maxTurns = limits.maxTurns;
526
+ // Structured outputs: normalize once (throws ValidationError for bad shapes).
527
+ const loopOutput = resolveLoopOutput(config);
496
528
  // Q-19: pre-run checkpoint for onFailure rollback.
497
529
  const preRunCp = context.checkpoint();
498
530
  const sessionIdForRollback = loopSessionId(config);
@@ -544,6 +576,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
544
576
  cache: options?.cache,
545
577
  tools: standardTools.length > 0 ? standardTools : undefined,
546
578
  signal: runOptions?.signal,
579
+ ...(loopOutput.spec ? { output: loopOutput.spec } : {}),
547
580
  };
548
581
  // Q-22: per-request timeout pass-through (providers honor timeoutMs).
549
582
  try {
@@ -720,8 +753,16 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
720
753
  );
721
754
  }
722
755
 
756
+ // Structured outputs: validate final text only (tool turns skip this).
757
+ // Strict-fail throws ValidationError with partial attached via catch below.
758
+ let parsedOutput: unknown | undefined;
759
+ if (loopOutput.raw !== undefined && !truncatedByMaxTurns) {
760
+ parsedOutput = parseFinalStructuredOutput(finalResult?.text || "", loopOutput.raw, loopOutput.spec);
761
+ }
762
+
723
763
  return new AgentResponse({
724
764
  text: finalResult?.text || "",
765
+ ...(parsedOutput !== undefined ? { parsed: parsedOutput } : {}),
725
766
  thinking: finalResult?.thinking,
726
767
  thoughtSignature: finalResult?.thoughtSignature,
727
768
  toolCalls: allToolCalls,
@@ -869,6 +910,17 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
869
910
  const maxTurns = streamLimits.maxTurns;
870
911
 
871
912
  const standardTools = toStandardToolDeclarations(tools);
913
+ // Structured outputs: normalize once for the whole stream run.
914
+ let streamOutputRaw: unknown;
915
+ let streamOutputSpec: import("../types/model.ts").StructuredOutputSpec | undefined;
916
+ try {
917
+ const resolved = resolveLoopOutput(config);
918
+ streamOutputRaw = resolved.raw;
919
+ streamOutputSpec = resolved.spec;
920
+ } catch (err) {
921
+ outerStream.fail(err instanceof Error ? err : new Error(String(err)));
922
+ return;
923
+ }
872
924
 
873
925
  accumulatedUsage = {
874
926
  inputTokens: 0,
@@ -900,6 +952,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
900
952
  cache: options?.cache,
901
953
  tools: standardTools.length > 0 ? standardTools : undefined,
902
954
  signal: linked.signal,
955
+ ...(streamOutputSpec ? { output: streamOutputSpec } : {}),
903
956
  };
904
957
  try {
905
958
  if (Number.isFinite(streamLimits.requestTimeoutMs) && streamLimits.requestTimeoutMs !== Infinity) {
@@ -1137,8 +1190,28 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1137
1190
  }
1138
1191
  const truncatedFinish = canonicalFinishReason(lastResponse?.finishReason, truncatedByMaxTurns);
1139
1192
 
1193
+ let streamParsed: unknown | undefined;
1194
+ if (streamOutputRaw !== undefined && !truncatedByMaxTurns) {
1195
+ try {
1196
+ streamParsed = parseFinalStructuredOutput(lastResponse?.text || "", streamOutputRaw, streamOutputSpec);
1197
+ } catch (err) {
1198
+ const raw = err instanceof Error ? err : new Error(String(err));
1199
+ try { (raw as unknown as { partial?: unknown }).partial ??= {
1200
+ usage: accumulatedUsage,
1201
+ cost: accumulatedUsage.cost,
1202
+ turns,
1203
+ toolCalls: allToolCalls,
1204
+ toolResults: allToolResults,
1205
+ text: lastResponse?.text,
1206
+ }; } catch {}
1207
+ outerStream.fail(raw);
1208
+ return;
1209
+ }
1210
+ }
1211
+
1140
1212
  const finalAgentResponse = new AgentResponse({
1141
1213
  text: lastResponse?.text || "",
1214
+ ...(streamParsed !== undefined ? { parsed: streamParsed } : {}),
1142
1215
  thinking: lastResponse?.thinking,
1143
1216
  thoughtSignature: lastResponse?.thoughtSignature,
1144
1217
  toolCalls: allToolCalls,
package/src/index.ts CHANGED
@@ -210,6 +210,15 @@ export {
210
210
  mapToolChoiceToOpenAI,
211
211
  normalizeToolChoice,
212
212
  parseStreamedToolArguments,
213
+ normalizeStructuredOutput,
214
+ extractOutputValidator,
215
+ parseStructuredOutput,
216
+ mapOutputToOpenAI,
217
+ mapOutputToGoogle,
218
+ mapOutputToOpenRouterChat,
219
+ mapOutputToCompatChat,
220
+ buildStructuredOutputInstruction,
221
+ augmentSystemPromptWithStructuredOutput,
213
222
  } from "./providers.ts";
214
223
  export type {
215
224
  ProviderCapabilityStatus,
@@ -217,13 +226,21 @@ export type {
217
226
  OpenRouterToolChoice,
218
227
  OpenRouterChatToolChoice,
219
228
  OpenAIToolChoice,
229
+ ChatCompletionsResponseFormat,
220
230
  } from "./providers.ts";
221
231
 
222
232
  export {
223
233
  GOOGLE_MODELS,
224
234
  OPENAI_MODELS,
225
235
  OPENROUTER_MODELS,
236
+ modelSupportsStructuredOutput,
226
237
  } from "./models/catalog.ts";
238
+ export {
239
+ hasAuthHeader,
240
+ clampToolCallId,
241
+ hasTruncatedToolArguments,
242
+ shortHashId,
243
+ } from "./providers/shared.ts";
227
244
 
228
245
  export {
229
246
  isValidThoughtSignature,
@@ -368,6 +385,7 @@ export type {
368
385
  ProviderRequestOptions,
369
386
  ProviderGenerateResult,
370
387
  ProviderRawData,
388
+ StructuredOutputSpec,
371
389
  } from "./types/model.ts";
372
390
 
373
391
  export type {
@@ -388,6 +406,7 @@ export type {
388
406
  WhenBusyMode,
389
407
  ModelRef,
390
408
  ThinkingOption,
409
+ StructuredOutputOption,
391
410
  SamplingConfig,
392
411
  RetryPolicyConfig,
393
412
  AgentLogger,
@@ -138,6 +138,7 @@ function mapCapabilities(raw: any, cost: ModelCost, reasoning: boolean, toolCall
138
138
  supportsExplicitCaching: cost.cache_read !== undefined,
139
139
  supportsParallelToolCalls: toolCall,
140
140
  supportsStreaming: true,
141
+ supportsStructuredOutput: raw.structured_output === undefined ? true : !!raw.structured_output,
141
142
  supportsReasoningToggle: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "toggle"),
142
143
  supportsReasoningEffort: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "effort"),
143
144
  modalities: [...(modalities.input || []), ...(modalities.output || [])].filter((v: any, i: any, a: any) => a.indexOf(v) === i),
@@ -588,6 +589,30 @@ export {
588
589
  type RefreshCatalogOptions,
589
590
  } from "./catalog-cache.ts";
590
591
 
592
+ /**
593
+ * True when the model can enforce `response_format`/`text.format` natively.
594
+ * Unknown models (custom endpoints, dynamic routers) count as capable —
595
+ * the provider verdict stands, matching `assertModalitiesSupported`.
596
+ * Models with catalog `structured_output: false` return false so callers can
597
+ * emulate via prompt instruction + client validation instead of 400ing.
598
+ *
599
+ * @example `modelSupportsStructuredOutput("openrouter", "inclusionai/ling-3.0-flash-sante:free")`
600
+ */
601
+ export function modelSupportsStructuredOutput(provider: string, modelId: string): boolean {
602
+ try {
603
+ const spec = getModelFromCatalog(provider, modelId);
604
+ if (!spec) return true;
605
+ if (typeof spec.capabilities?.supportsStructuredOutput === "boolean") {
606
+ return spec.capabilities.supportsStructuredOutput;
607
+ }
608
+ const raw = (spec as { raw?: { structured_output?: unknown } }).raw;
609
+ if (raw && typeof raw.structured_output === "boolean") return raw.structured_output;
610
+ return true;
611
+ } catch {
612
+ return true;
613
+ }
614
+ }
615
+
591
616
  /**
592
617
  * Permissive placeholder so custom endpoints never fail preflight:
593
618
  * allows every ThinkingLevel (none -> max + dynamic).
@@ -622,6 +647,7 @@ export function createGenericModelSpec(provider: string, modelId: string): Model
622
647
  supportsLongCacheRetention: false,
623
648
  supportsParallelToolCalls: true,
624
649
  supportsStreaming: true,
650
+ supportsStructuredOutput: true,
625
651
  modalities: ["text"],
626
652
  },
627
653
  pricing: {},
@@ -48,9 +48,11 @@ import {
48
48
  incompleteStreamError,
49
49
  combinedSignal,
50
50
  timeoutFor,
51
+ hasAuthHeader,
52
+ clampToolCallId,
51
53
  } from "./shared.ts";
52
54
  import { createGenericModelSpec } from "../models/catalog.ts";
53
- import { getModelFromCatalog, getModelsForProvider } from "../models/catalog.ts";
55
+ import { getModelFromCatalog, getModelsForProvider, modelSupportsStructuredOutput } from "../models/catalog.ts";
54
56
  import {
55
57
  mapThinkingLevelToGoogle,
56
58
  mapServiceTierToGoogle,
@@ -59,6 +61,10 @@ import {
59
61
  lastProviderFor,
60
62
  noteProviderTurn,
61
63
  parseStreamedToolArguments,
64
+ mapOutputToGoogle,
65
+ normalizeStructuredOutput,
66
+ augmentSystemPromptWithStructuredOutput,
67
+ emitProviderWarning,
62
68
  } from "../providers.ts";
63
69
 
64
70
  // ---------------------------------------------------------------------------
@@ -176,7 +182,10 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
176
182
  }
177
183
 
178
184
  function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
179
- return options?.apiKey || getApiKey("google", undefined, options?.env);
185
+ const key = getApiKey("google", options?.apiKey, options?.env);
186
+ if (key) return key;
187
+ if (hasAuthHeader(options?.headers)) return "unused";
188
+ return undefined;
180
189
  }
181
190
 
182
191
  function cleanModelId(model: string | ModelSpec): string {
@@ -517,11 +526,43 @@ export class GoogleInteractionsProvider implements Provider {
517
526
  input: "",
518
527
  ...(stream ? { stream: true } : {}),
519
528
  };
520
- if (context.systemPrompt) body.system_instruction = context.systemPrompt;
529
+ // Structured outputs: native `response_format` when supported, else
530
+ // emulated via system instruction (catalog `structured_output: false`
531
+ // would 400 on the wire shape).
532
+ let outputEmulated = false;
533
+ let outputSpec: import("../types/model.ts").StructuredOutputSpec | undefined;
534
+ try {
535
+ outputSpec = normalizeStructuredOutput(options?.output ?? (options as { outputSchema?: unknown })?.outputSchema);
536
+ } catch {
537
+ outputSpec = undefined;
538
+ }
539
+ let effectiveSystemPrompt = context.systemPrompt;
540
+ if (outputSpec && !modelSupportsStructuredOutput("google", modelId)) {
541
+ outputEmulated = true;
542
+ effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(context.systemPrompt, outputSpec);
543
+ emitProviderWarning({
544
+ provider: "google",
545
+ capability: "structured output",
546
+ requested: `${outputSpec.name} (google/${modelId})`,
547
+ reason: "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
548
+ fallback: "emulated JSON instruction + client validation (no response_format is sent)",
549
+ });
550
+ }
551
+ if (effectiveSystemPrompt) body.system_instruction = effectiveSystemPrompt;
521
552
  const googleTools = toGoogleTools(tools);
522
553
  if (googleTools) body.tools = googleTools;
523
554
  if (Object.keys(generationConfig).length > 0) body.generation_config = generationConfig;
524
555
  if (serviceTier) body.service_tier = serviceTier;
556
+ // Structured outputs: Interactions `response_format{type:text,mime_type:application/json,schema}`.
557
+ // Skipped when emulated (instruction already in `system_instruction`).
558
+ if (outputSpec && !outputEmulated) {
559
+ try {
560
+ const mapped = mapOutputToGoogle(outputSpec);
561
+ if (mapped) (body as unknown as Record<string, unknown>)["response_format"] = mapped.responseFormat;
562
+ } catch {
563
+ // why: invalid output shapes fail fast at Agent construction/loop validation.
564
+ }
565
+ }
525
566
 
526
567
  const prevProvider = lastProviderFor(sessionId);
527
568
  const switched = !!prevProvider && prevProvider !== "google";
@@ -564,8 +605,9 @@ export class GoogleInteractionsProvider implements Provider {
564
605
  }
565
606
  // Q-17: key travels as the `x-goog-api-key` header (never `?key=` in the
566
607
  // URL, which would leak into logs). Case-insensitive merge so a custom
567
- // `X-Goog-Api-Key` never duplicates the canonical header.
568
- setHeaderCaseInsensitive(headers, "x-goog-api-key", apiKey);
608
+ // `X-Goog-Api-Key` never duplicates the canonical header. Proxy flows
609
+ // (`apiKey === "unused"`) preserve custom auth instead of overwriting.
610
+ if (apiKey !== "unused") setHeaderCaseInsensitive(headers, "x-goog-api-key", apiKey);
569
611
  return { method: "POST", headers, body: JSON.stringify(body), signal };
570
612
  }
571
613
 
@@ -598,14 +640,21 @@ export class GoogleInteractionsProvider implements Provider {
598
640
  modelId: string
599
641
  ): void {
600
642
  if (status >= 200 && status < 300) return;
601
- const { message, code, errorType } = readErrorPayload(bodyText);
602
- const err: Record<string, unknown> & Error = new Error(message) as Record<string, unknown> & Error;
643
+ const { message, code, errorType, raw } = readErrorPayload(bodyText);
644
+ const hint =
645
+ status === 401
646
+ ? " Check GEMINI_API_KEY/GOOGLE_API_KEY (no quotes/spaces, correct provider), GOOGLE_BASE_URL, and that the key is loaded (bun --env-file=.env.local)."
647
+ : "";
648
+ const fullMessage = `${message}${hint}`;
649
+ const err: Record<string, unknown> & Error = new Error(fullMessage) as Record<string, unknown> & Error;
603
650
  (err as Record<string, unknown>)["statusCode"] = status;
604
651
  (err as Record<string, unknown>)["status"] = status;
605
652
  (err as Record<string, unknown>)["responseBody"] = bodyText.slice(0, 500);
606
653
  (err as Record<string, unknown>)["url"] = url.split("?")[0];
654
+ (err as Record<string, unknown>)["data"] = { error: { message: fullMessage } };
607
655
  if (code !== undefined) (err as Record<string, unknown>)["code"] = code;
608
656
  if (errorType) (err as Record<string, unknown>)["errorType"] = errorType;
657
+ if (raw) (err as Record<string, unknown>)["raw"] = raw;
609
658
  throw toConciseProviderError(err, "google", modelId);
610
659
  }
611
660