veryfront 0.1.159 → 0.1.161

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.
@@ -1,4 +1,3 @@
1
- import * as dntShim from "../../_dnt.shims.js";
2
1
  import { getMCPRegistry } from "./registry.js";
3
2
  import { executeTool } from "../tool/index.js";
4
3
  import { zodToJsonSchema } from "../tool/schema/index.js";
@@ -7,15 +6,12 @@ import { promptRegistry } from "../prompt/index.js";
7
6
  import { createError, toError } from "../errors/veryfront-error.js";
8
7
  import { withSpan } from "../observability/tracing/otlp-setup.js";
9
8
  import { VERSION } from "../utils/version.js";
10
- import { validateContentType } from "../security/input-validation/limits.js";
11
- import { VeryfrontError } from "../security/input-validation/errors.js";
12
9
  import { logger as baseLogger } from "../utils/index.js";
10
+ import { createMCPHTTPHandler } from "./http-transport.js";
13
11
  import { SessionManager } from "./session.js";
14
12
  import { TaskStore } from "./task-store.js";
15
13
  const logger = baseLogger.component("mcp-server");
16
- const MAX_REQUEST_BODY_SIZE = 1_048_576; // 1 MB
17
14
  const MAX_CONTEXT_HEADER_LENGTH = 255;
18
- const JSON_CONTENT_TYPE = "application/json";
19
15
  const END_USER_ID_PATTERN = /^[a-zA-Z0-9._@-]+$/;
20
16
  const PROJECT_ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
21
17
  class JsonRpcError extends Error {
@@ -46,18 +42,6 @@ function toParamsRecord(params) {
46
42
  return {};
47
43
  return params;
48
44
  }
49
- function createJSONResponse(body, init) {
50
- const headers = new dntShim.Headers(init?.headers);
51
- headers.set("Content-Type", JSON_CONTENT_TYPE);
52
- return new dntShim.Response(JSON.stringify(body), { ...init, headers });
53
- }
54
- function createJSONRPCErrorResponse(status, code, message) {
55
- return createJSONResponse({
56
- jsonrpc: "2.0",
57
- id: null,
58
- error: { code, message },
59
- }, { status });
60
- }
61
45
  function readAllowedHeader(request, headerName, pattern) {
62
46
  const value = request.headers.get(headerName);
63
47
  if (!value || value.length > MAX_CONTEXT_HEADER_LENGTH || !pattern.test(value)) {
@@ -485,96 +469,19 @@ export class MCPServer {
485
469
  return Promise.all(this.pendingTasks.values()).then(() => { });
486
470
  }
487
471
  createHTTPHandler() {
488
- return async (request) => {
489
- const requestOrigin = request.headers.get("Origin");
490
- // CORS preflight
491
- if (request.method === "OPTIONS") {
492
- return new dntShim.Response(null, { status: 204, headers: this.getCORSHeaders(requestOrigin) });
493
- }
494
- // Origin validation (DNS rebinding protection)
495
- if (requestOrigin && this.config.cors?.enabled && this.config.cors.origins?.length) {
496
- if (!this.config.cors.origins.includes(requestOrigin)) {
497
- return createJSONRPCErrorResponse(403, -32600, "Forbidden: Origin not allowed");
498
- }
499
- }
500
- // Auth check (applies to all methods including DELETE)
501
- if (this.config.auth?.type && this.config.auth.type !== "none") {
502
- const authorized = await this.validateAuth(request);
503
- if (!authorized)
504
- return new dntShim.Response("Unauthorized", { status: 401 });
505
- }
506
- // DELETE = terminate session
507
- if (request.method === "DELETE") {
508
- const sessionId = request.headers.get("MCP-Session-Id");
509
- if (sessionId) {
510
- this.sessionManager.terminate(sessionId);
511
- this.sessionCapabilities.delete(sessionId);
512
- }
513
- return new dntShim.Response(null, { status: 200, headers: this.getCORSHeaders(requestOrigin) });
514
- }
515
- // Only POST allowed for JSON-RPC messages
516
- if (request.method !== "POST") {
517
- return new dntShim.Response("Method Not Allowed", { status: 405 });
518
- }
519
- // Enforce request body size limit (fast path via Content-Length header)
520
- const contentLength = request.headers.get("content-length");
521
- if (contentLength && Number(contentLength) > MAX_REQUEST_BODY_SIZE) {
522
- return createJSONRPCErrorResponse(413, -32600, "Request body too large");
523
- }
524
- try {
525
- validateContentType(request, JSON_CONTENT_TYPE);
526
- }
527
- catch (error) {
528
- const message = error instanceof VeryfrontError ? error.message : "Invalid Content-Type";
529
- return createJSONRPCErrorResponse(400, -32700, message);
530
- }
531
- let rpcRequest;
532
- try {
533
- const bodyText = await request.text();
534
- if (bodyText.length > MAX_REQUEST_BODY_SIZE) {
535
- return createJSONRPCErrorResponse(413, -32600, "Request body too large");
536
- }
537
- rpcRequest = JSON.parse(bodyText);
538
- }
539
- catch (_) {
540
- // expected: malformed JSON in request body
541
- return createJSONRPCErrorResponse(400, -32700, "Parse error");
542
- }
543
- // Session management: initialize creates session, everything else requires it
544
- const responseHeaders = {
545
- ...this.getCORSHeaders(requestOrigin),
546
- };
547
- if (rpcRequest.method === "initialize") {
548
- const context = this.extractRequestContext(request);
549
- const rpcResponse = await this.handleRequest(rpcRequest, context);
550
- const clientCaps = toParamsRecord(rpcRequest.params).capabilities ??
551
- {};
552
- const sessionId = this.sessionManager.create();
553
- this.sessionCapabilities.set(sessionId, clientCaps);
554
- responseHeaders["MCP-Session-Id"] = sessionId;
555
- return createJSONResponse(rpcResponse, { headers: responseHeaders });
556
- }
557
- // Post-init: require session ID when sessions are active
558
- if (this.sessionManager.size > 0) {
559
- const sessionId = request.headers.get("MCP-Session-Id");
560
- if (!sessionId) {
561
- return createJSONRPCErrorResponse(400, -32600, "Missing MCP-Session-Id header");
562
- }
563
- if (!this.sessionManager.isValid(sessionId)) {
564
- return createJSONRPCErrorResponse(404, -32600, "Session not found or expired");
565
- }
566
- }
567
- // Notifications have no id member — return 202 Accepted
568
- // Note: id:0 is a valid request ID per JSON-RPC 2.0, so check for undefined
569
- if (rpcRequest.id === undefined) {
570
- const context = this.extractRequestContext(request);
571
- await this.handleRequest(rpcRequest, context);
572
- return new dntShim.Response(null, { status: 202, headers: responseHeaders });
573
- }
574
- const context = this.extractRequestContext(request);
575
- const rpcResponse = await this.handleRequest(rpcRequest, context);
576
- return createJSONResponse(rpcResponse, { headers: responseHeaders });
577
- };
472
+ return createMCPHTTPHandler({
473
+ authEnabled: Boolean(this.config.auth?.type && this.config.auth.type !== "none"),
474
+ getCORSHeaders: (requestOrigin) => this.getCORSHeaders(requestOrigin),
475
+ validateAuth: (request) => this.validateAuth(request),
476
+ handleRequest: (request, context) => this.handleRequest(request, context),
477
+ extractRequestContext: (request) => this.extractRequestContext(request),
478
+ isOriginAllowed: (requestOrigin) => !requestOrigin ||
479
+ !this.config.cors?.enabled ||
480
+ !this.config.cors.origins?.length ||
481
+ this.config.cors.origins.includes(requestOrigin),
482
+ sessionCapabilities: this.sessionCapabilities,
483
+ sessionManager: this.sessionManager,
484
+ });
578
485
  }
579
486
  extractRequestContext(request) {
580
487
  const context = {};
@@ -1 +1 @@
1
- {"version":3,"file":"stat-operations.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/stat-operations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AA2B/D,qBAAa,cAAe,SAAQ,uBAAuB;IACzD,OAAO,CAAC,SAAS,CAAyC;IAC1D,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,aAAa,CAA8B;IAEnD,OAAO,CAAC,sBAAsB,CAA6B;IAC3D,OAAO,CAAC,qBAAqB,CAA8B;IAE3D,OAAO,CAAC,WAAW,CAAkC;IAErD,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAGrC;IAEH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;YAiHvB,gBAAgB;YAwChB,UAAU;IAkDxB,UAAU,IAAI,IAAI;IAMlB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM;YAIpC,cAAc;IAqE5B,OAAO,CAAC,0BAA0B;IA6BlC,OAAO,CAAC,qBAAqB;YAQf,sBAAsB;YAyEtB,iBAAiB;IASzB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWtC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAwK1F"}
1
+ {"version":3,"file":"stat-operations.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/stat-operations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AA2B/D,qBAAa,cAAe,SAAQ,uBAAuB;IACzD,OAAO,CAAC,SAAS,CAAyC;IAC1D,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,aAAa,CAA8B;IAEnD,OAAO,CAAC,sBAAsB,CAA6B;IAC3D,OAAO,CAAC,qBAAqB,CAA8B;IAE3D,OAAO,CAAC,WAAW,CAAkC;IAErD,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAGrC;IAEH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;YAiHvB,gBAAgB;YAwChB,UAAU;IAkDxB,UAAU,IAAI,IAAI;IAMlB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM;YAIpC,cAAc;IAqE5B,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,qBAAqB;YAQf,sBAAsB;YA8EtB,iBAAiB;IASzB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWtC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAkI1F"}
@@ -255,7 +255,7 @@ export class StatOperations extends VeryfrontOperationsBase {
255
255
  this.cache.set(cacheKey, files);
256
256
  return files;
257
257
  }
258
- buildResolveSearchPatterns(normalizedPath, options) {
258
+ buildResolveSearchPatterns(normalizedPath, options, knownExtensionFallback = "exact") {
259
259
  const patterns = new Set();
260
260
  const pathWithoutExt = stripKnownExtension(normalizedPath, EXTENSION_PRIORITY);
261
261
  const allowPagesPrefix = options?.allowPagesPrefix !== false;
@@ -264,7 +264,7 @@ export class StatOperations extends VeryfrontOperationsBase {
264
264
  patterns.add(pattern);
265
265
  };
266
266
  if (EXTENSION_PRIORITY.some((ext) => normalizedPath.endsWith(ext))) {
267
- addPattern(normalizedPath);
267
+ addPattern(knownExtensionFallback === "wildcard" ? `${pathWithoutExt}.*` : normalizedPath);
268
268
  return [...patterns];
269
269
  }
270
270
  addPattern(`${pathWithoutExt}.*`);
@@ -282,7 +282,7 @@ export class StatOperations extends VeryfrontOperationsBase {
282
282
  path: normalizeIndexedFilePath(match).normalizedPath,
283
283
  }));
284
284
  }
285
- async tryResolveViaApiSearch(normalizedPath, options) {
285
+ async tryResolveViaApiSearch(normalizedPath, options, knownExtensionFallback = "exact") {
286
286
  if (isFrameworkSourcePath(normalizedPath)) {
287
287
  logger.debug("Skipping API search for framework path", { normalizedPath });
288
288
  return null;
@@ -291,7 +291,7 @@ export class StatOperations extends VeryfrontOperationsBase {
291
291
  logger.warn("API search circuit breaker open, skipping", { normalizedPath });
292
292
  return undefined;
293
293
  }
294
- const patterns = this.buildResolveSearchPatterns(normalizedPath, options);
294
+ const patterns = this.buildResolveSearchPatterns(normalizedPath, options, knownExtensionFallback);
295
295
  let sawSuccessfulSearch = false;
296
296
  for (const pattern of patterns) {
297
297
  try {
@@ -459,49 +459,17 @@ export class StatOperations extends VeryfrontOperationsBase {
459
459
  logger.debug("Skipping API search for framework path", { normalizedPath });
460
460
  return null;
461
461
  }
462
- // NOTE: Removed optimization that skipped API search for paths with extensions.
463
- // This was causing layout files and other project files to not be found when
464
- // they were missing from the file index (due to cache issues, incomplete fetch, etc.).
465
- // The API pattern search is the fallback to ensure files can still be found.
466
- if (!this.apiSearchCircuitBreaker.canSearch()) {
467
- logger.warn("API search circuit breaker open, skipping", { normalizedPath });
468
- return null;
469
- }
470
- const searchPattern = `${pathWithoutExt}.*`;
471
- logger.debug("Searching for file via API", {
472
- pattern: searchPattern,
473
- normalizedPath,
474
- });
475
- try {
476
- const matches = await this.client.searchFiles(searchPattern);
477
- this.apiSearchCircuitBreaker.recordSuccess();
478
- logger.debug("API search result", {
479
- pattern: searchPattern,
480
- matchCount: matches.length,
481
- matches: matches.map((m) => m.path).slice(0, 5),
482
- });
483
- const sortedMatches = sortPathsByExtensionPriority(matches, EXTENSION_PRIORITY);
484
- const first = sortedMatches[0];
485
- if (first) {
486
- logger.debug("resolveFile found via API search", { path: first.path });
487
- this.cache.set(cacheKey, first.path);
488
- return first.path;
489
- }
462
+ // NOTE: Keep the post-index API fallback aligned with the pre-index helper for extensionless
463
+ // paths, while preserving the older wildcard sibling-extension lookup for known-extension
464
+ // paths. Incomplete file-list snapshots otherwise hide valid files until the cache refreshes.
465
+ const apiResolved = await this.tryResolveViaApiSearch(normalizedPath, options, "wildcard");
466
+ if (typeof apiResolved === "string") {
467
+ this.cache.set(cacheKey, apiResolved);
468
+ return apiResolved;
490
469
  }
491
- catch (error) {
492
- const result = this.apiSearchCircuitBreaker.recordFailure();
493
- if (result.tripped) {
494
- logger.warn("API search circuit breaker tripped", {
495
- failures: result.failures,
496
- });
497
- }
498
- logger.error("API pattern search failed", { pattern: searchPattern, error });
470
+ if (apiResolved === null) {
471
+ this.cache.set(cacheKey, NOT_FOUND_SENTINEL);
499
472
  }
500
- logger.debug("resolveFile not found after API search", {
501
- normalizedPath,
502
- pathWithoutExt,
503
- });
504
- this.cache.set(cacheKey, NOT_FOUND_SENTINEL);
505
473
  return null;
506
474
  }
507
475
  }
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.159";
1
+ export declare const VERSION = "0.1.161";
2
2
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.159";
3
+ export const VERSION = "0.1.161";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.159",
3
+ "version": "0.1.161",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.159",
3
+ "version": "0.1.161",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -97,7 +97,13 @@ export type {
97
97
  MessagePart,
98
98
  ModelProvider,
99
99
  ModelString,
100
+ ModelTransportRequest,
101
+ ModelTransportResolver,
100
102
  ResolvedAgentConfig,
103
+ ResolvedModelTransport,
104
+ ResolvedRuntimeState,
105
+ RuntimeStateRequest,
106
+ RuntimeStateResolver,
101
107
  StreamToolCall,
102
108
  ToolCall,
103
109
  ToolCallPart,
@@ -10,6 +10,8 @@
10
10
  *
11
11
  * @module ai/agent/runtime
12
12
  */
13
+ import * as dntShim from "../../../_dnt.shims.js";
14
+
13
15
 
14
16
  import {
15
17
  type AgentConfig,
@@ -19,6 +21,7 @@ import {
19
21
  getTextFromParts,
20
22
  type Message,
21
23
  type MessagePart,
24
+ type ResolvedRuntimeState,
22
25
  type ToolCall,
23
26
  type ToolResultPart,
24
27
  } from "../types.js";
@@ -264,6 +267,19 @@ function getRuntimeAllowedRemoteTools(config: AgentConfig): string[] | undefined
264
267
  return raw.every((toolName) => typeof toolName === "string") ? raw : [];
265
268
  }
266
269
 
270
+ type ResolvedModelTransport = {
271
+ requestedModel: string;
272
+ resolvedModelString: string;
273
+ languageModel: ModelRuntime;
274
+ headers?: dntShim.HeadersInit;
275
+ providerOptions?: Record<string, unknown>;
276
+ };
277
+
278
+ type RuntimeStepState = {
279
+ systemPrompt: string;
280
+ context?: Record<string, unknown>;
281
+ };
282
+
267
283
  export class AgentRuntime {
268
284
  private id: string;
269
285
  private config: AgentConfig;
@@ -279,6 +295,52 @@ export class AgentRuntime {
279
295
  this.memory = createMemory<Message>(memoryConfig);
280
296
  }
281
297
 
298
+ private async resolveModelTransport(
299
+ context: Record<string, unknown> | undefined,
300
+ modelOverride: string | undefined,
301
+ mode: "generate" | "stream",
302
+ ): Promise<ResolvedModelTransport> {
303
+ const requestedModel = resolveConfiguredAgentModel(modelOverride || this.config.model);
304
+ const resolvedModelString = resolveRuntimeModel(modelOverride || this.config.model);
305
+ const transport = await this.config.resolveModelTransport?.({
306
+ agentId: this.id,
307
+ requestedModel,
308
+ resolvedModel: resolvedModelString,
309
+ context,
310
+ mode,
311
+ });
312
+
313
+ return {
314
+ requestedModel,
315
+ resolvedModelString,
316
+ languageModel: transport?.model ?? resolveModel(resolvedModelString),
317
+ headers: transport?.headers,
318
+ providerOptions: transport?.providerOptions,
319
+ };
320
+ }
321
+
322
+ private async resolveRuntimeState(
323
+ messages: Message[],
324
+ context: Record<string, unknown> | undefined,
325
+ mode: "generate" | "stream",
326
+ step: number,
327
+ systemPrompt: string,
328
+ ): Promise<RuntimeStepState> {
329
+ const refreshed: ResolvedRuntimeState | undefined = await this.config.resolveRuntimeState?.({
330
+ agentId: this.id,
331
+ mode,
332
+ step,
333
+ system: systemPrompt,
334
+ messages: [...messages],
335
+ context,
336
+ });
337
+
338
+ return {
339
+ systemPrompt: refreshed?.system ?? systemPrompt,
340
+ context: refreshed?.context ?? context,
341
+ };
342
+ }
343
+
282
344
  /**
283
345
  * Generate a response (non-streaming)
284
346
  */
@@ -288,8 +350,9 @@ export class AgentRuntime {
288
350
  modelOverride?: string,
289
351
  maxOutputTokensOverride?: number,
290
352
  ): Promise<AgentResponse> {
291
- const requestedModel = resolveConfiguredAgentModel(modelOverride || this.config.model);
292
- const resolvedModelString = resolveRuntimeModel(modelOverride || this.config.model);
353
+ const transport = await this.resolveModelTransport(context, modelOverride, "generate");
354
+ const requestedModel = transport.requestedModel;
355
+ const resolvedModelString = transport.resolvedModelString;
293
356
  if (resolvedModelString !== requestedModel) {
294
357
  logger.info(
295
358
  `⚡ Using runtime model "${resolvedModelString}" instead of "${requestedModel}".`,
@@ -326,9 +389,12 @@ export class AgentRuntime {
326
389
  {
327
390
  agentId: this.id,
328
391
  projectId: tryGetCacheKeyContext()?.projectId,
329
- ...context,
330
392
  },
393
+ context,
331
394
  resolvedModelString,
395
+ transport.languageModel,
396
+ transport.headers,
397
+ transport.providerOptions,
332
398
  maxOutputTokensOverride,
333
399
  ),
334
400
  );
@@ -351,8 +417,9 @@ export class AgentRuntime {
351
417
  maxOutputTokensOverride?: number,
352
418
  abortSignal?: AbortSignal,
353
419
  ): Promise<ReadableStream<Uint8Array>> {
354
- const requestedModel = resolveConfiguredAgentModel(modelOverride || this.config.model);
355
- const resolvedModelString = resolveRuntimeModel(modelOverride || this.config.model);
420
+ const transport = await this.resolveModelTransport(context, modelOverride, "stream");
421
+ const requestedModel = transport.requestedModel;
422
+ const resolvedModelString = transport.resolvedModelString;
356
423
  if (resolvedModelString !== requestedModel) {
357
424
  logger.info(
358
425
  `⚡ Using runtime model "${resolvedModelString}" instead of "${requestedModel}".`,
@@ -389,7 +456,7 @@ export class AgentRuntime {
389
456
  // Resolve model BEFORE creating the ReadableStream — if this throws
390
457
  // (e.g., no_ai_available), the error propagates to the caller who can
391
458
  // return a proper error response (503) instead of a 200 with an error event.
392
- const languageModel = resolveModel(resolvedModelString);
459
+ const languageModel = transport.languageModel;
393
460
 
394
461
  // Determine inference mode from the resolved model object (not the string),
395
462
  // because resolveModel may internally fall back from cloud to local.
@@ -433,8 +500,11 @@ export class AgentRuntime {
433
500
  callbacks,
434
501
  textPartId,
435
502
  toolContext,
503
+ context,
436
504
  resolvedModelString,
437
505
  languageModel,
506
+ transport.headers,
507
+ transport.providerOptions,
438
508
  maxOutputTokensOverride,
439
509
  streamAbortSignal,
440
510
  );
@@ -474,15 +544,19 @@ export class AgentRuntime {
474
544
  private async executeAgentLoop(
475
545
  systemPrompt: string,
476
546
  messages: Message[],
477
- toolContext?: ToolExecutionContext,
547
+ toolContextBase?: ToolExecutionContext,
548
+ runtimeContext?: Record<string, unknown>,
478
549
  modelString?: string,
550
+ resolvedModel?: ModelRuntime,
551
+ headers?: dntShim.HeadersInit,
552
+ providerOptions?: Record<string, unknown>,
479
553
  maxOutputTokensOverride?: number,
480
554
  ): Promise<AgentResponse> {
481
555
  return withSpan("agent.execution_loop", async (loopSpan) => {
482
556
  const { maxAgentSteps } = getPlatformCapabilities();
483
557
  const maxSteps = this.computeMaxSteps(maxAgentSteps);
484
558
  const effectiveModel = resolveRuntimeModel(modelString || this.config.model);
485
- const languageModel = resolveModel(effectiveModel);
559
+ const languageModel = resolvedModel ?? resolveModel(effectiveModel);
486
560
 
487
561
  const toolCalls: ToolCall[] = [];
488
562
  const currentMessages = [...messages];
@@ -502,11 +576,24 @@ export class AgentRuntime {
502
576
  // Request-scoped skill policy (not class-level mutable state)
503
577
  let activeSkillPolicy: string[] | undefined;
504
578
  const allowedRemoteToolNames = getRuntimeAllowedRemoteTools(this.config);
579
+ let currentSystemPrompt = systemPrompt;
580
+ let currentRuntimeContext = runtimeContext;
505
581
 
506
582
  for (let step = 0; step < maxSteps; step++) {
507
583
  this.status = "thinking";
508
584
  addSpanEvent(loopSpan, "step_start", { step });
509
585
 
586
+ const runtimeState = await this.resolveRuntimeState(
587
+ currentMessages,
588
+ currentRuntimeContext,
589
+ "generate",
590
+ step,
591
+ currentSystemPrompt,
592
+ );
593
+ currentSystemPrompt = runtimeState.systemPrompt;
594
+ currentRuntimeContext = runtimeState.context;
595
+ const toolContext = { ...toolContextBase, ...currentRuntimeContext };
596
+
510
597
  let tools = isLocal ? [] : await getAvailableTools(this.config.tools, {
511
598
  includeSkillTools: Boolean(this.config.skills),
512
599
  allowedRemoteToolNames,
@@ -526,7 +613,7 @@ export class AgentRuntime {
526
613
  });
527
614
  return generateText({
528
615
  model: languageModel,
529
- system: systemPrompt,
616
+ system: currentSystemPrompt,
530
617
  messages: convertToModelMessages(currentMessages),
531
618
  tools: convertToolsToRuntimeTools(tools, {
532
619
  model: effectiveModel,
@@ -535,6 +622,8 @@ export class AgentRuntime {
535
622
  experimental_repairToolCall: repairToolCall,
536
623
  maxOutputTokens: this.resolveMaxOutputTokens(maxOutputTokensOverride),
537
624
  temperature: DEFAULT_TEMPERATURE,
625
+ ...(headers ? { headers } : {}),
626
+ ...(providerOptions ? { providerOptions } : {}),
538
627
  });
539
628
  });
540
629
 
@@ -764,9 +853,12 @@ export class AgentRuntime {
764
853
  onFinish?: (response: AgentResponse) => void;
765
854
  },
766
855
  textPartId?: string,
767
- toolContext?: Record<string, unknown>,
856
+ toolContextBase?: Record<string, unknown>,
857
+ runtimeContext?: Record<string, unknown>,
768
858
  modelString?: string,
769
859
  resolvedModel?: ModelRuntime,
860
+ headers?: dntShim.HeadersInit,
861
+ providerOptions?: Record<string, unknown>,
770
862
  maxOutputTokensOverride?: number,
771
863
  abortSignal?: AbortSignal,
772
864
  ): Promise<AgentResponse> {
@@ -795,12 +887,25 @@ export class AgentRuntime {
795
887
  let finalFinishReason: string | undefined;
796
888
  let latestAssistantText = "";
797
889
  const allowedRemoteToolNames = getRuntimeAllowedRemoteTools(this.config);
890
+ let currentSystemPrompt = systemPrompt;
891
+ let currentRuntimeContext = runtimeContext;
798
892
 
799
893
  for (let step = 0; step < maxSteps; step++) {
800
894
  throwIfAborted(abortSignal);
801
895
  sendSSE(controller, encoder, { type: "step-start" });
802
896
  const currentStepToolResults = new Map<string, ToolResultPart>();
803
897
 
898
+ const runtimeState = await this.resolveRuntimeState(
899
+ currentMessages,
900
+ currentRuntimeContext,
901
+ "stream",
902
+ step,
903
+ currentSystemPrompt,
904
+ );
905
+ currentSystemPrompt = runtimeState.systemPrompt;
906
+ currentRuntimeContext = runtimeState.context;
907
+ const toolContext = { ...toolContextBase, ...currentRuntimeContext };
908
+
804
909
  let tools = isLocalStreaming ? [] : await getAvailableTools(this.config.tools, {
805
910
  includeSkillTools: Boolean(this.config.skills),
806
911
  allowedRemoteToolNames,
@@ -815,7 +920,7 @@ export class AgentRuntime {
815
920
 
816
921
  const result = streamText({
817
922
  model: languageModel,
818
- system: systemPrompt,
923
+ system: currentSystemPrompt,
819
924
  messages: convertToModelMessages(currentMessages),
820
925
  tools: convertToolsToRuntimeTools(tools, {
821
926
  model: effectiveModel,
@@ -824,6 +929,8 @@ export class AgentRuntime {
824
929
  experimental_repairToolCall: repairToolCall,
825
930
  maxOutputTokens: this.resolveMaxOutputTokens(maxOutputTokensOverride),
826
931
  temperature: DEFAULT_TEMPERATURE,
932
+ ...(headers ? { headers } : {}),
933
+ ...(providerOptions ? { providerOptions } : {}),
827
934
  abortSignal,
828
935
  });
829
936
 
@@ -4,6 +4,7 @@
4
4
  import * as dntShim from "../../_dnt.shims.js";
5
5
 
6
6
 
7
+ import type { ModelRuntime } from "../provider/types.js";
7
8
  import type { RemoteToolSource, Tool } from "../tool/index.js";
8
9
  import { INVALID_ARGUMENT } from "../errors/error-registry.js";
9
10
  import type { Memory } from "./memory/memory-interface.js";
@@ -91,6 +92,16 @@ export interface AgentConfig {
91
92
  };
92
93
  /** Restrict runtime model overrides to these "provider/model" strings. */
93
94
  allowedModels?: ModelString[];
95
+ /**
96
+ * Optional request-aware hook for overriding the resolved model runtime and
97
+ * provider transport options on a per-call basis.
98
+ */
99
+ resolveModelTransport?: ModelTransportResolver;
100
+ /**
101
+ * Optional step-boundary hook for refreshing the runtime system prompt and
102
+ * host-owned context during a long-lived run.
103
+ */
104
+ resolveRuntimeState?: RuntimeStateResolver;
94
105
  /**
95
106
  * Enable skills for this agent.
96
107
  * - true: include all discovered skills from skills/ directory
@@ -108,6 +119,42 @@ export interface AgentConfig {
108
119
 
109
120
  export type ResolvedAgentConfig = AgentConfig & { model: ModelString };
110
121
 
122
+ export interface ModelTransportRequest {
123
+ agentId: string;
124
+ requestedModel: ModelString;
125
+ resolvedModel: ModelString;
126
+ context?: Record<string, unknown>;
127
+ mode: "generate" | "stream";
128
+ }
129
+
130
+ export interface ResolvedModelTransport {
131
+ model?: ModelRuntime;
132
+ headers?: dntShim.HeadersInit;
133
+ providerOptions?: Record<string, unknown>;
134
+ }
135
+
136
+ export type ModelTransportResolver = (
137
+ request: ModelTransportRequest,
138
+ ) => ResolvedModelTransport | Promise<ResolvedModelTransport>;
139
+
140
+ export interface RuntimeStateRequest {
141
+ agentId: string;
142
+ mode: "generate" | "stream";
143
+ step: number;
144
+ system: string;
145
+ messages: Message[];
146
+ context?: Record<string, unknown>;
147
+ }
148
+
149
+ export interface ResolvedRuntimeState {
150
+ system?: string;
151
+ context?: Record<string, unknown>;
152
+ }
153
+
154
+ export type RuntimeStateResolver = (
155
+ request: RuntimeStateRequest,
156
+ ) => ResolvedRuntimeState | undefined | Promise<ResolvedRuntimeState | undefined>;
157
+
111
158
  // Import for use in AgentMiddleware
112
159
  import type { AgentContext, AgentResponse } from "./schemas/index.js";
113
160