11import { Buffer } from 'node:buffer' ;
22
3- import { snapshotJsonValue , type JsonValue } from './lower-mcp.js' ;
3+ import { snapshotJsonValue , type JsonSnapshotBudget , type JsonValue } from './lower-mcp.js' ;
44
55export const AGENT_DOCUMENT_VERSION = 1 as const ;
66
@@ -163,7 +163,7 @@ export class AgentContractError extends Error {
163163 }
164164}
165165
166- const resolveLimits = ( overrides : Partial < AgentRenderLimits > ) : AgentRenderLimits => {
166+ export const resolveAgentRenderLimits = ( overrides : Partial < AgentRenderLimits > = { } ) : AgentRenderLimits => {
167167 const limits = { ...DEFAULT_AGENT_RENDER_LIMITS , ...overrides } ;
168168 for ( const [ name , value ] of Object . entries ( limits ) ) {
169169 if ( ! Number . isSafeInteger ( value ) || value <= 0 ) {
@@ -190,10 +190,51 @@ const text = (value: unknown, field: string): string => {
190190const optionalString = ( value : unknown , field : string ) : string | undefined =>
191191 value === undefined ? undefined : requiredString ( value , field ) ;
192192
193- const snapshotJson = ( value : unknown , message : string ) : JsonValue => {
193+ export const elapsedTimeExceeded = ( maxElapsedMs : number ) : AgentContractError =>
194+ new AgentContractError (
195+ 'elapsed-time-exceeded' ,
196+ `Agent render elapsed time exceeds ${ String ( maxElapsedMs ) } ms` ,
197+ ) ;
198+
199+ const jsonBudget = ( state : NodeSnapshotState ) : JsonSnapshotBudget => ( {
200+ addBytes ( n ) {
201+ state . bytes += n ;
202+ if ( state . bytes > state . limits . maxDocumentBytes ) {
203+ throw new AgentContractError (
204+ 'document-bytes-exceeded' ,
205+ `Agent Document bytes exceed ${ String ( state . limits . maxDocumentBytes ) } ` ,
206+ ) ;
207+ }
208+ } ,
209+ addNode ( ) {
210+ state . nodes += 1 ;
211+ if ( state . nodes > state . limits . maxDocumentNodes ) {
212+ throw new AgentContractError (
213+ 'document-node-count-exceeded' ,
214+ `Agent Document node count exceeds ${ String ( state . limits . maxDocumentNodes ) } ` ,
215+ ) ;
216+ }
217+ } ,
218+ checkDepth ( depth ) {
219+ if ( depth > state . limits . maxDocumentDepth ) {
220+ throw new AgentContractError (
221+ 'document-depth-exceeded' ,
222+ `Agent Document depth exceeds ${ String ( state . limits . maxDocumentDepth ) } ` ,
223+ ) ;
224+ }
225+ } ,
226+ } ) ;
227+
228+ const snapshotJson = (
229+ value : unknown ,
230+ message : string ,
231+ depth : number ,
232+ state : NodeSnapshotState ,
233+ ) : JsonValue => {
194234 try {
195- return snapshotJsonValue ( value , message ) ;
235+ return snapshotJsonValue ( value , message , { depth , limits : jsonBudget ( state ) } ) ;
196236 } catch ( error ) {
237+ if ( error instanceof AgentContractError ) throw error ;
197238 throw new AgentContractError ( 'invalid-document' , error instanceof Error ? error . message : message , { cause : error } ) ;
198239 }
199240} ;
@@ -208,6 +249,7 @@ const progressNumber = (value: unknown, field: string): number => {
208249interface NodeSnapshotState {
209250 readonly ancestors : Set < object > ;
210251 readonly limits : AgentRenderLimits ;
252+ bytes : number ;
211253 nodes : number ;
212254}
213255
@@ -242,7 +284,7 @@ const snapshotNode = (node: AgentDocumentNode, depth: number, state: NodeSnapsho
242284 const children = Object . freeze ( node . children . map ( ( child ) => snapshotNode ( child , depth + 1 , state ) ) ) ;
243285 const metadata = node . metadata === undefined
244286 ? undefined
245- : snapshotJson ( node . metadata , 'Agent result metadata must be JSON-serializable' ) ;
287+ : snapshotJson ( node . metadata , 'Agent result metadata must be JSON-serializable' , depth , state ) ;
246288 return Object . freeze ( {
247289 children,
248290 kind : 'result' ,
@@ -258,7 +300,7 @@ const snapshotNode = (node: AgentDocumentNode, depth: number, state: NodeSnapsho
258300 case 'json' :
259301 return Object . freeze ( {
260302 kind : 'json' ,
261- value : snapshotJson ( node . value , 'Agent JSON node value must be JSON-serializable' ) ,
303+ value : snapshotJson ( node . value , 'Agent JSON node value must be JSON-serializable' , depth , state ) ,
262304 } ) ;
263305 case 'progress' : {
264306 const completed = progressNumber ( node . completed , 'Agent progress completed' ) ;
@@ -337,11 +379,12 @@ export const createAgentDocument = (
337379 `Unsupported Agent Document version: ${ String ( input . version ) } ` ,
338380 ) ;
339381 }
340- const limits = resolveLimits ( limitOverrides ) ;
341- const root = snapshotNode ( input . root , 1 , { ancestors : new Set ( ) , limits, nodes : 0 } ) ;
382+ const limits = resolveAgentRenderLimits ( limitOverrides ) ;
383+ const state : NodeSnapshotState = { ancestors : new Set ( ) , bytes : 0 , limits, nodes : 0 } ;
384+ const root = snapshotNode ( input . root , 1 , state ) ;
342385 const value = input . value === undefined
343386 ? undefined
344- : snapshotJson ( input . value , 'Agent Document value must be JSON-serializable' ) ;
387+ : snapshotJson ( input . value , 'Agent Document value must be JSON-serializable' , 1 , state ) ;
345388 const document : AgentDocument = Object . freeze ( {
346389 root,
347390 status : documentStatus ( input . status ) ,
@@ -358,10 +401,15 @@ export const createAgentDocument = (
358401 return document ;
359402} ;
360403
361- const snapshotRenderError = ( error : AgentRenderError ) : AgentRenderError => {
404+ const snapshotRenderError = ( error : AgentRenderError , limits : AgentRenderLimits ) : AgentRenderError => {
362405 const data = error . data === undefined
363406 ? undefined
364- : snapshotJson ( error . data , 'Agent render error data must be JSON-serializable' ) ;
407+ : snapshotJson (
408+ error . data ,
409+ 'Agent render error data must be JSON-serializable' ,
410+ 1 ,
411+ { ancestors : new Set ( ) , bytes : 0 , limits, nodes : 0 } ,
412+ ) ;
365413 return Object . freeze ( {
366414 code : requiredString ( error . code , 'Agent render error code' ) ,
367415 ...( data === undefined ? { } : { data } ) ,
@@ -403,7 +451,7 @@ const snapshotEvent = (
403451 const boundaryId = optionalString ( input . boundaryId , 'Agent render boundaryId' ) ;
404452 return Object . freeze ( {
405453 ...( boundaryId === undefined ? { } : { boundaryId } ) ,
406- error : snapshotRenderError ( input . error ) ,
454+ error : snapshotRenderError ( input . error , limits ) ,
407455 sequence,
408456 type : 'error' ,
409457 } ) ;
@@ -422,14 +470,16 @@ const snapshotEvent = (
422470
423471export interface AgentRenderEventSequence {
424472 readonly completed : boolean ;
473+ readonly maxElapsedMs : number ;
425474 readonly nextSequence : number ;
475+ readonly remainingMs : number ;
426476 readonly emit : ( input : AgentRenderEventInput ) => AgentRenderEvent ;
427477}
428478
429479export const createAgentRenderEventSequence = (
430480 limitOverrides : Partial < AgentRenderLimits > = { } ,
431481) : AgentRenderEventSequence => {
432- const limits = resolveLimits ( limitOverrides ) ;
482+ const limits = resolveAgentRenderLimits ( limitOverrides ) ;
433483 const startedAt = Date . now ( ) ;
434484 const recentTimes : number [ ] = [ ] ;
435485 let completed = false ;
@@ -438,6 +488,9 @@ export const createAgentRenderEventSequence = (
438488 get completed ( ) {
439489 return completed ;
440490 } ,
491+ get maxElapsedMs ( ) {
492+ return limits . maxElapsedMs ;
493+ } ,
441494 emit ( input : AgentRenderEventInput ) : AgentRenderEvent {
442495 if ( completed ) {
443496 throw new AgentContractError (
@@ -447,10 +500,7 @@ export const createAgentRenderEventSequence = (
447500 }
448501 const now = Date . now ( ) ;
449502 if ( now - startedAt > limits . maxElapsedMs ) {
450- throw new AgentContractError (
451- 'elapsed-time-exceeded' ,
452- `Agent render elapsed time exceeds ${ String ( limits . maxElapsedMs ) } ms` ,
453- ) ;
503+ throw elapsedTimeExceeded ( limits . maxElapsedMs ) ;
454504 }
455505 recentTimes . push ( now ) ;
456506 const windowStart = now - 1000 ;
@@ -484,5 +534,8 @@ export const createAgentRenderEventSequence = (
484534 get nextSequence ( ) {
485535 return nextSequence ;
486536 } ,
537+ get remainingMs ( ) {
538+ return limits . maxElapsedMs - ( Date . now ( ) - startedAt ) ;
539+ } ,
487540 } ) ;
488541} ;
0 commit comments