fix: remove avoidable model runtime limits
Preserve long histories and parsed attachments, expose paged process and Subagent output, isolate MCP discovery failures, and expand browser snapshots and knowledge result selection. Remove fixed Harness output and updated remote Runtime duration limits. Keep released Agent profile and v1 bridge compatibility while enabling larger framed exchanges and optional limits for updated peers. Document remote coverage requirements and real-model boundary validation. Release note: Long model tasks can retain more context and continue reading complete tool output; remote execution preserves compatibility with released Agents. Updated remote limits require an updated Agent and Runtime bundle.
This commit is contained in:
@@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AgentPromptModelProfile } from '../shared/model-bridge-contracts'
|
||||
import { REMOTE_MODEL_GATEWAY_LIMITS } from '../shared/remote-model-gateway-contracts'
|
||||
import {
|
||||
AgentModelCallLedger,
|
||||
AgentModelGateway
|
||||
@@ -65,6 +66,49 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('AgentModelGateway', () => {
|
||||
it('keeps a slow provider request alive past 60 seconds and still cancels it', async () => {
|
||||
vi.useFakeTimers()
|
||||
let providerSignal: AbortSignal | undefined
|
||||
const { gateway, ledger } = setup(vi.fn<typeof fetch>(async (_url, init) => {
|
||||
providerSignal = init!.signal!
|
||||
return await new Promise<Response>((_resolve, reject) => {
|
||||
providerSignal!.addEventListener('abort', () => reject(providerSignal!.reason), { once: true })
|
||||
})
|
||||
}))
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const pending = gateway.dispatch({ ...context, profile: { ...profile, limits: {} } }, request, controller.signal)
|
||||
const rejected = expect(pending).rejects.toMatchObject({ code: 'cancelled' })
|
||||
await vi.advanceTimersByTimeAsync(600_000)
|
||||
expect(providerSignal?.aborted).toBe(false)
|
||||
controller.abort()
|
||||
await rejected
|
||||
} finally {
|
||||
ledger.close()
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it.each([undefined, 128_000])('preserves runtime output hints without an invented token default: %s', async (tokens) => {
|
||||
const payload = { model: 'model-1', ...(tokens === undefined ? {} : { max_output_tokens: tokens }) }
|
||||
const fetcher = vi.fn<typeof fetch>(async (_url, init) => {
|
||||
expect(JSON.parse(String(init?.body))).toEqual(payload)
|
||||
return new Response('x'.repeat(1024 * 1024))
|
||||
})
|
||||
const { gateway, ledger } = setup(fetcher)
|
||||
try {
|
||||
const result = await gateway.dispatch(
|
||||
{ ...context, profile: { ...profile, limits: {} } },
|
||||
{ ...request, bodyBase64: Buffer.from(JSON.stringify(payload)).toString('base64') },
|
||||
new AbortController().signal
|
||||
)
|
||||
expect(Buffer.from(result.response.bodyBase64, 'base64').length).toBe(1024 * 1024)
|
||||
await result.acknowledgeDelivery()
|
||||
} finally {
|
||||
ledger.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('dispatches a stable call exactly once and records delivery ACK without persisting credentials', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>(async (_url, init) => {
|
||||
expect(new Headers(init?.headers).get('authorization')).toBe(
|
||||
@@ -281,7 +325,7 @@ describe('AgentModelGateway', () => {
|
||||
new Response('{}', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-length': String(768 * 1024 + 1)
|
||||
'content-length': String(REMOTE_MODEL_GATEWAY_LIMITS.maximumResponseBodyBytes + 1)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -358,11 +358,12 @@ export class AgentModelGateway {
|
||||
requestDigest
|
||||
})
|
||||
const timeout = new AbortController()
|
||||
const timer = setTimeout(
|
||||
const timeoutMilliseconds = context.profile.limits.requestTimeoutMilliseconds
|
||||
const timer = timeoutMilliseconds === undefined ? undefined : setTimeout(
|
||||
() => timeout.abort(new Error('provider timeout')),
|
||||
context.profile.limits.requestTimeoutMilliseconds
|
||||
timeoutMilliseconds
|
||||
)
|
||||
timer.unref?.()
|
||||
timer?.unref?.()
|
||||
let response: Response
|
||||
try {
|
||||
response = await this.#fetch(prepared.url, {
|
||||
@@ -395,10 +396,13 @@ export class AgentModelGateway {
|
||||
})
|
||||
} catch (error) {
|
||||
clearTimeout(timer)
|
||||
const code =
|
||||
error instanceof BoundedResponseTooLargeError
|
||||
? 'response-too-large'
|
||||
: 'outcome-unknown'
|
||||
const code = signal.aborted
|
||||
? 'cancelled'
|
||||
: timeout.signal.aborted
|
||||
? 'timeout'
|
||||
: error instanceof BoundedResponseTooLargeError
|
||||
? 'response-too-large'
|
||||
: 'outcome-unknown'
|
||||
this.#ledger.outcomeUnknown(callId, code)
|
||||
throw error instanceof AgentModelGatewayError
|
||||
? error
|
||||
@@ -481,7 +485,7 @@ export function createAgentModelCallId(
|
||||
function prepareProviderRequest(
|
||||
profile: AgentPromptModelProfile,
|
||||
request: RemoteModelGatewayRequest,
|
||||
maximumOutputTokens: number
|
||||
maximumOutputTokens: number | undefined
|
||||
): {
|
||||
url: URL
|
||||
headers: Record<string, string>
|
||||
@@ -550,10 +554,12 @@ function prepareProviderRequest(
|
||||
'Provider request has an invalid output token limit'
|
||||
)
|
||||
}
|
||||
normalized[outputField] =
|
||||
requestedOutput === undefined
|
||||
? maximumOutputTokens
|
||||
: Math.min(requestedOutput as number, maximumOutputTokens)
|
||||
if (maximumOutputTokens !== undefined) {
|
||||
normalized[outputField] =
|
||||
requestedOutput === undefined
|
||||
? maximumOutputTokens
|
||||
: Math.min(requestedOutput as number, maximumOutputTokens)
|
||||
}
|
||||
const body = Buffer.from(canonicalJson(normalized), 'utf8')
|
||||
if (
|
||||
body.byteLength >
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('direct Linux stdio Runtime ownership', () => {
|
||||
registry.close()
|
||||
})
|
||||
|
||||
it('does not stop an active prompt at the unbounded Runtime deadline', async () => {
|
||||
it.each([UNBOUNDED_REMOTE_PROMPT_DEADLINE, 'finite'])('respects the requested Runtime deadline: %s', async (deadline) => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const registry = createRegistry()
|
||||
@@ -220,12 +220,14 @@ describe('direct Linux stdio Runtime ownership', () => {
|
||||
const launched = identity()
|
||||
const sendSignal = vi.fn<typeof process.kill>(() => true)
|
||||
const owner = await launchDirectLinuxStdioProcessOwner({
|
||||
manifest: manifest(),
|
||||
manifest: { ...manifest(), limits: { ...manifest().limits, maximumPromptRuntimeMilliseconds: 0 } },
|
||||
profile: profile(),
|
||||
identity: { launchId: 'launch-long', processId: 'process-long' },
|
||||
installationId: 'installation-1',
|
||||
registry,
|
||||
deadlineAt: UNBOUNDED_REMOTE_PROMPT_DEADLINE,
|
||||
deadlineAt: deadline === 'finite'
|
||||
? new Date(Date.now() + 26 * 60 * 60_000).toISOString()
|
||||
: deadline,
|
||||
maximumInputBytes: 1024,
|
||||
platform: 'linux',
|
||||
spawn: () => {
|
||||
@@ -244,6 +246,10 @@ describe('direct Linux stdio Runtime ownership', () => {
|
||||
state: 'running',
|
||||
processTree: 'running'
|
||||
})
|
||||
if (deadline === 'finite') {
|
||||
await vi.advanceTimersByTimeAsync(60 * 60_000)
|
||||
expect(sendSignal).toHaveBeenCalledWith(-launched.processGroupId, 'SIGTERM')
|
||||
}
|
||||
await owner.stop({
|
||||
reason: 'user-cancelled',
|
||||
deadlineAt: new Date(Date.now() + 10_000).toISOString()
|
||||
|
||||
@@ -521,11 +521,17 @@ export class DirectLinuxStdioProcessOwner {
|
||||
this.#clearDeadline()
|
||||
return
|
||||
}
|
||||
const remaining = remainingMilliseconds(deadlineAt, this.#now())
|
||||
const now = this.#now()
|
||||
remainingMilliseconds(deadlineAt, now)
|
||||
const remaining = Date.parse(deadlineAt) - now
|
||||
this.#clearDeadline()
|
||||
const generation = this.#promptGeneration
|
||||
this.#deadlineTimer = setTimeout(() => {
|
||||
if (!this.#promptActive || generation !== this.#promptGeneration) return
|
||||
if (Date.parse(deadlineAt) > this.#now()) {
|
||||
this.#scheduleDeadline(deadlineAt)
|
||||
return
|
||||
}
|
||||
void this.stop({
|
||||
reason: 'deadline-exceeded',
|
||||
deadlineAt: new Date(this.#now() + DEFAULT_STOP_TIMEOUT_MS).toISOString()
|
||||
@@ -1096,7 +1102,9 @@ function boundedPromptDeadline(
|
||||
)
|
||||
}
|
||||
return new Date(
|
||||
Math.min(requested, now + maximumRuntimeMilliseconds)
|
||||
maximumRuntimeMilliseconds === 0
|
||||
? requested
|
||||
: Math.min(requested, now + maximumRuntimeMilliseconds)
|
||||
).toISOString()
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export const MODEL_BRIDGE_BROKER_SOCKET_NAME =
|
||||
|
||||
const BROKER_PROTOCOL_VERSION = 1
|
||||
const DEFAULT_MAXIMUM_CONNECTIONS = 8
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 150_000
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 0
|
||||
const DEFAULT_CONNECT_TIMEOUT_MS = 5_000
|
||||
const CLOSE_TIMEOUT_MS = 2_000
|
||||
const MAXIMUM_UNIX_SOCKET_PATH_BYTES = 107
|
||||
@@ -192,8 +192,8 @@ export class ModelBridgeBrokerServer {
|
||||
)
|
||||
this.#requestTimeoutMs = boundedInteger(
|
||||
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
1,
|
||||
300_000,
|
||||
0,
|
||||
0x7fff_ffff,
|
||||
'Model bridge request timeout'
|
||||
)
|
||||
}
|
||||
@@ -298,12 +298,12 @@ export class ModelBridgeBrokerServer {
|
||||
)
|
||||
}
|
||||
socket.once('close', onClose)
|
||||
const timeout = setTimeout(() => {
|
||||
const timeout = this.#requestTimeoutMs === 0 ? undefined : setTimeout(() => {
|
||||
cancellation.abort(
|
||||
new ModelBridgeBrokerError('request-timeout')
|
||||
)
|
||||
}, this.#requestTimeoutMs)
|
||||
timeout.unref?.()
|
||||
timeout?.unref?.()
|
||||
|
||||
let requestId = `invalid-${randomUUID()}`
|
||||
let dispatched: ModelBridgeExchangeResult | undefined
|
||||
@@ -383,8 +383,8 @@ export function createUnixModelBridgeExchange(options: {
|
||||
)
|
||||
const requestTimeoutMs = boundedInteger(
|
||||
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
1,
|
||||
300_000,
|
||||
0,
|
||||
0x7fff_ffff,
|
||||
'Model bridge request timeout'
|
||||
)
|
||||
|
||||
@@ -394,13 +394,13 @@ export function createUnixModelBridgeExchange(options: {
|
||||
context.signal.throwIfAborted()
|
||||
const socket = createConnection(socketPath)
|
||||
const timeoutController = new AbortController()
|
||||
const timeout = setTimeout(() => {
|
||||
const timeout = requestTimeoutMs === 0 ? undefined : setTimeout(() => {
|
||||
timeoutController.abort(
|
||||
new ModelBridgeBrokerError('request-timeout')
|
||||
)
|
||||
socket.destroy()
|
||||
}, requestTimeoutMs)
|
||||
timeout.unref?.()
|
||||
timeout?.unref?.()
|
||||
const onAbort = (): void => {
|
||||
socket.destroy()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { UNBOUNDED_REMOTE_PROMPT_DEADLINE } from '../shared/remote-agent-contracts'
|
||||
import {
|
||||
decodeModelBridgeMessage,
|
||||
encodeModelBridgeMessage,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
} from '../shared/model-bridge-contracts'
|
||||
import {
|
||||
AGENT_PROTOCOL_VERSION,
|
||||
AGENT_PROTOCOL_LIMITS,
|
||||
type AgentFrame
|
||||
} from '../shared/agent-protocol'
|
||||
import {
|
||||
@@ -65,8 +67,12 @@ async function outboundRequest(
|
||||
sent: AgentFrame[],
|
||||
index = 0
|
||||
): Promise<ModelBridgeRequestMessage> {
|
||||
await waitFor(() => sent.length > index)
|
||||
const decoded = await decodeModelBridgeMessage(sent[index]!.payload)
|
||||
await waitFor(() => {
|
||||
if (sent.length <= index) return false
|
||||
try { JSON.parse(Buffer.concat(sent.slice(index).map(frame => frame.payload)).toString()); return true }
|
||||
catch { return false }
|
||||
})
|
||||
const decoded = await decodeModelBridgeMessage(Buffer.concat(sent.slice(index).map(frame => frame.payload)))
|
||||
if (decoded.kind !== 'request') {
|
||||
throw new Error('Expected request')
|
||||
}
|
||||
@@ -78,9 +84,7 @@ async function deliverResponse(
|
||||
outbound: ModelBridgeRequestMessage,
|
||||
bodyBytes = 12
|
||||
): Promise<void> {
|
||||
await client.onBlobFrame(
|
||||
incomingFrame(
|
||||
await encodeModelBridgeMessage({
|
||||
const payload = await encodeModelBridgeMessage({
|
||||
protocol: 'goodbuddy-model-bridge-v1',
|
||||
kind: 'response',
|
||||
identity: outbound.identity,
|
||||
@@ -90,13 +94,45 @@ async function deliverResponse(
|
||||
headers: { 'content-type': 'application/json' },
|
||||
bodyBase64: Buffer.alloc(bodyBytes, 2).toString('base64')
|
||||
}
|
||||
}),
|
||||
1
|
||||
)
|
||||
)
|
||||
})
|
||||
for (let offset = 0; offset < payload.length; offset += AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes) {
|
||||
await client.onBlobFrame(incomingFrame(
|
||||
payload.subarray(offset, offset + AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes),
|
||||
offset / AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes + 1
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
describe('Agent model bridge blob client', () => {
|
||||
it.each(['finite', 'unbounded'])('waits for the prompt deadline without a fixed provider timeout: %s', async (kind) => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const fixture = createHarness()
|
||||
fixture.client.binding.deadlineAt = kind === 'finite'
|
||||
? new Date(Date.now() + 60 * 60_000).toISOString()
|
||||
: UNBOUNDED_REMOTE_PROMPT_DEADLINE
|
||||
const controller = new AbortController()
|
||||
const pending = fixture.client.exchange(request, {
|
||||
requestId: 'request-long', signal: controller.signal
|
||||
})
|
||||
const rejected = expect(pending).rejects.toMatchObject({
|
||||
code: kind === 'finite' ? 'timeout' : 'cancelled'
|
||||
})
|
||||
await vi.waitFor(() => expect(fixture.sent).toHaveLength(1))
|
||||
await vi.advanceTimersByTimeAsync(10 * 60_000)
|
||||
expect(fixture.poisoned).not.toHaveBeenCalled()
|
||||
if (kind === 'finite') {
|
||||
await vi.advanceTimersByTimeAsync(50 * 60_000)
|
||||
} else {
|
||||
controller.abort()
|
||||
}
|
||||
await rejected
|
||||
await fixture.client.close({ poisonIfActive: false })
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('uses exact sequential identities and two-phase delivery', async () => {
|
||||
const fixture = createHarness()
|
||||
const pending = fixture.client.exchange(request, {
|
||||
@@ -133,30 +169,30 @@ describe('Agent model bridge blob client', () => {
|
||||
expect(fixture.poisoned).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('receives a maximum legal response in one message', async () => {
|
||||
it('receives a response across bounded blob frames', async () => {
|
||||
const fixture = createHarness()
|
||||
const pending = fixture.client.exchange(request, {
|
||||
requestId: 'request-large-response',
|
||||
signal: new AbortController().signal
|
||||
})
|
||||
const outbound = await outboundRequest(fixture.sent)
|
||||
await deliverResponse(fixture.client, outbound, 768 * 1024)
|
||||
await deliverResponse(fixture.client, outbound, 2 * 1024 * 1024)
|
||||
|
||||
const exchange = await pending
|
||||
expect(
|
||||
Buffer.from(exchange.response.bodyBase64, 'base64').byteLength
|
||||
).toBe(768 * 1024)
|
||||
).toBe(2 * 1024 * 1024)
|
||||
await exchange.acknowledgeDelivery()
|
||||
expect(fixture.sent).toHaveLength(2)
|
||||
expect(fixture.poisoned).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends a large request as one bounded message', async () => {
|
||||
it('sends a large request across bounded blob frames', async () => {
|
||||
const fixture = createHarness()
|
||||
const pending = fixture.client.exchange(
|
||||
{
|
||||
...request,
|
||||
bodyBase64: Buffer.alloc(700_000, 1).toString('base64')
|
||||
bodyBase64: Buffer.alloc(2 * 1024 * 1024, 1).toString('base64')
|
||||
},
|
||||
{
|
||||
requestId: 'request-large',
|
||||
@@ -164,7 +200,8 @@ describe('Agent model bridge blob client', () => {
|
||||
}
|
||||
)
|
||||
const outbound = await outboundRequest(fixture.sent)
|
||||
expect(fixture.sent).toHaveLength(1)
|
||||
expect(fixture.sent.length).toBeGreaterThan(1)
|
||||
expect(fixture.sent.every(frame => frame.payload.length <= AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes)).toBe(true)
|
||||
expect(outbound.identity.roundIndex).toBe(0)
|
||||
await fixture.client.close({ poisonIfActive: false })
|
||||
await expect(pending).rejects.toMatchObject({ code: 'cancelled' })
|
||||
@@ -319,7 +356,7 @@ describe('Agent model bridge blob client', () => {
|
||||
await outboundRequest(fixture.sent)
|
||||
await expect(
|
||||
fixture.client.onBlobFrame(
|
||||
incomingFrame(Buffer.from('{ "not": "canonical" }'), 1)
|
||||
incomingFrame(Buffer.from('{ "not": "canonical" }\n'), 1)
|
||||
)
|
||||
).rejects.toBeInstanceOf(Error)
|
||||
await expect(pending).rejects.toBeInstanceOf(Error)
|
||||
@@ -373,4 +410,4 @@ async function waitFor(predicate: () => boolean): Promise<void> {
|
||||
}
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { UNBOUNDED_REMOTE_PROMPT_DEADLINE } from '../shared/remote-agent-contracts'
|
||||
import {
|
||||
AGENT_PROTOCOL_VERSION,
|
||||
AGENT_PROTOCOL_LIMITS,
|
||||
agentIdentifierSchema,
|
||||
type AgentFrame
|
||||
} from '../shared/agent-protocol'
|
||||
@@ -9,6 +11,7 @@ import {
|
||||
decodeModelBridgeMessage,
|
||||
encodeModelBridgeMessage,
|
||||
modelBridgeDeliveryAckMessageSchema,
|
||||
ModelBridgeMessageBuffer,
|
||||
type ModelBridgeError,
|
||||
type ModelBridgeIdentity,
|
||||
type ModelBridgePolicy,
|
||||
@@ -19,8 +22,6 @@ import type {
|
||||
RemoteModelGatewayResponse
|
||||
} from '../shared/remote-model-gateway-contracts'
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000
|
||||
|
||||
export type ModelBridgeClientBinding = {
|
||||
bindingId: string
|
||||
promptOperationId: string
|
||||
@@ -66,7 +67,7 @@ type ActiveRequest = {
|
||||
requestDigest: string
|
||||
resolve: (message: ModelBridgeResponseMessage) => void
|
||||
reject: (error: unknown) => void
|
||||
timeout: NodeJS.Timeout
|
||||
timeout: NodeJS.Timeout | undefined
|
||||
responseReceived: boolean
|
||||
providerDispatchPossible: boolean
|
||||
settled: boolean
|
||||
@@ -77,13 +78,14 @@ export class ModelBridgeBlobClient {
|
||||
readonly #sendBlobFrame: (frame: AgentFrame) => Promise<void>
|
||||
readonly #onPoison: (error: ModelBridgeClientError) => void | Promise<void>
|
||||
readonly #now: () => number
|
||||
readonly #requestTimeoutMs: number
|
||||
readonly #requestTimeoutMs: number | undefined
|
||||
readonly #randomMessageId: () => string
|
||||
#nextRoundIndex = 0
|
||||
#nextOutboundSequence = 1n
|
||||
#active?: ActiveRequest
|
||||
#closed = false
|
||||
#poisoned = false
|
||||
readonly #responseBuffer = new ModelBridgeMessageBuffer()
|
||||
|
||||
constructor(options: {
|
||||
binding: ModelBridgeClientBinding
|
||||
@@ -97,9 +99,9 @@ export class ModelBridgeBlobClient {
|
||||
this.#sendBlobFrame = options.sendBlobFrame
|
||||
this.#onPoison = options.onPoison
|
||||
this.#now = options.now ?? Date.now
|
||||
this.#requestTimeoutMs = boundedTimeout(
|
||||
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
||||
)
|
||||
this.#requestTimeoutMs = options.requestTimeoutMs === undefined
|
||||
? undefined
|
||||
: boundedTimeout(options.requestTimeoutMs)
|
||||
this.#randomMessageId =
|
||||
options.randomMessageId ??
|
||||
(() => `bridge-${randomUUID()}`)
|
||||
@@ -151,15 +153,25 @@ export class ModelBridgeBlobClient {
|
||||
|
||||
const response = new Promise<ModelBridgeResponseMessage>(
|
||||
(resolve, reject) => {
|
||||
const remaining = Math.max(
|
||||
1,
|
||||
Date.parse(this.binding.deadlineAt) - this.#now()
|
||||
const deadline = Math.min(
|
||||
this.binding.deadlineAt === UNBOUNDED_REMOTE_PROMPT_DEADLINE
|
||||
? Infinity
|
||||
: Date.parse(this.binding.deadlineAt),
|
||||
this.#requestTimeoutMs === undefined
|
||||
? Infinity
|
||||
: this.#now() + this.#requestTimeoutMs
|
||||
)
|
||||
const timeout = setTimeout(() => {
|
||||
const expire = (): void => {
|
||||
const active = this.#active
|
||||
if (active?.identity.messageId !== messageId) {
|
||||
return
|
||||
}
|
||||
const remaining = deadline - this.#now()
|
||||
if (remaining > 0) {
|
||||
active.timeout = setTimeout(expire, Math.min(remaining, 0x7fff_ffff))
|
||||
active.timeout.unref?.()
|
||||
return
|
||||
}
|
||||
const error = new ModelBridgeClientError(
|
||||
'Model bridge response timed out',
|
||||
'timeout',
|
||||
@@ -169,8 +181,12 @@ export class ModelBridgeBlobClient {
|
||||
active.settled = true
|
||||
active.reject(error)
|
||||
void this.#poison(error)
|
||||
}, Math.min(this.#requestTimeoutMs, remaining))
|
||||
timeout.unref?.()
|
||||
}
|
||||
const timeout = deadline === Infinity ? undefined : setTimeout(
|
||||
expire,
|
||||
Math.max(1, Math.min(deadline - this.#now(), 0x7fff_ffff))
|
||||
)
|
||||
timeout?.unref?.()
|
||||
this.#active = {
|
||||
identity,
|
||||
requestDigest: message.requestDigest,
|
||||
@@ -339,7 +355,9 @@ export class ModelBridgeBlobClient {
|
||||
)
|
||||
}
|
||||
try {
|
||||
const message = await decodeModelBridgeMessage(frame.payload, {
|
||||
const payload = this.#responseBuffer.push(frame.payload)
|
||||
if (payload === undefined) return
|
||||
const message = await decodeModelBridgeMessage(payload, {
|
||||
expectedIdentity: active.identity,
|
||||
expectedRequestDigest: active.requestDigest
|
||||
})
|
||||
@@ -438,29 +456,34 @@ export class ModelBridgeBlobClient {
|
||||
}
|
||||
|
||||
async #send(payload: Uint8Array): Promise<void> {
|
||||
const sequence = this.#nextOutboundSequence.toString()
|
||||
await this.#sendBlobFrame({
|
||||
header: {
|
||||
protocolMajor: AGENT_PROTOCOL_VERSION.major,
|
||||
protocolMinor: AGENT_PROTOCOL_VERSION.minor,
|
||||
connectionId: this.binding.connectionId,
|
||||
generation: this.binding.controllerGeneration,
|
||||
channelId: this.binding.channelId,
|
||||
channelEpoch: this.binding.channelEpoch,
|
||||
direction: 'agent-to-main',
|
||||
sequence,
|
||||
kind: 'blob',
|
||||
payloadLength: payload.byteLength
|
||||
},
|
||||
payload
|
||||
})
|
||||
this.#nextOutboundSequence += 1n
|
||||
for (let offset = 0; offset < payload.byteLength; offset += AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes) {
|
||||
this.#assertAvailable()
|
||||
const chunk = payload.subarray(offset, offset + AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes)
|
||||
const sequence = this.#nextOutboundSequence.toString()
|
||||
await this.#sendBlobFrame({
|
||||
header: {
|
||||
protocolMajor: AGENT_PROTOCOL_VERSION.major,
|
||||
protocolMinor: AGENT_PROTOCOL_VERSION.minor,
|
||||
connectionId: this.binding.connectionId,
|
||||
generation: this.binding.controllerGeneration,
|
||||
channelId: this.binding.channelId,
|
||||
channelEpoch: this.binding.channelEpoch,
|
||||
direction: 'agent-to-main',
|
||||
sequence,
|
||||
kind: 'blob',
|
||||
payloadLength: chunk.byteLength
|
||||
},
|
||||
payload: chunk
|
||||
})
|
||||
this.#nextOutboundSequence += 1n
|
||||
}
|
||||
}
|
||||
|
||||
#clearActive(active: ActiveRequest): void {
|
||||
clearTimeout(active.timeout)
|
||||
if (this.#active === active) {
|
||||
this.#active = undefined
|
||||
this.#responseBuffer.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +534,7 @@ function boundedTimeout(value: number): number {
|
||||
if (
|
||||
!Number.isSafeInteger(value) ||
|
||||
value < 1 ||
|
||||
value > 300_000
|
||||
value > 0x7fff_ffff
|
||||
) {
|
||||
throw new RangeError('Invalid model bridge timeout')
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export type ModelBridgeHelperSpawn = (
|
||||
|
||||
const LOOPBACK_HOST = '127.0.0.1'
|
||||
const DEFAULT_MAXIMUM_CONNECTIONS = 8
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 180_000
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 0
|
||||
const RESPONSE_CLOSE_TIMEOUT_MS = 2_000
|
||||
const MAXIMUM_HTTP_HEADER_BYTES = 16 * 1024
|
||||
const MAXIMUM_HTTP_HEADER_COUNT = 32
|
||||
@@ -162,8 +162,8 @@ export class ModelBridgeLoopbackProxy {
|
||||
)
|
||||
this.#requestTimeoutMs = boundedInteger(
|
||||
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
1,
|
||||
300_000,
|
||||
0,
|
||||
0x7fff_ffff,
|
||||
'Model bridge loopback request timeout'
|
||||
)
|
||||
}
|
||||
@@ -295,12 +295,12 @@ export class ModelBridgeLoopbackProxy {
|
||||
}
|
||||
incoming.once('aborted', onAborted)
|
||||
outgoing.once('close', onResponseClose)
|
||||
const timeout = setTimeout(() => {
|
||||
const timeout = this.#requestTimeoutMs === 0 ? undefined : setTimeout(() => {
|
||||
cancellation.abort(
|
||||
new ModelBridgeBrokerError('request-timeout')
|
||||
)
|
||||
}, this.#requestTimeoutMs)
|
||||
timeout.unref?.()
|
||||
timeout?.unref?.()
|
||||
|
||||
try {
|
||||
const request = await parseHttpRequest(
|
||||
|
||||
@@ -534,6 +534,11 @@ describe('AgentProtocolServer connection bounds', () => {
|
||||
name: 'runtime/model-bridge',
|
||||
version: 1,
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
name: 'runtime/model-bridge-optional-limits',
|
||||
version: 1,
|
||||
critical: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Socket } from 'node:net'
|
||||
import { MODEL_BRIDGE_OPTIONAL_LIMITS_CAPABILITY } from '../shared/model-bridge-contracts'
|
||||
import {
|
||||
AGENT_PROTOCOL_LIMITS,
|
||||
AGENT_PROTOCOL_VERSION,
|
||||
@@ -256,6 +257,11 @@ export class AgentProtocolServer {
|
||||
version:
|
||||
RUNTIME_MODEL_BRIDGE_CAPABILITY_VERSION,
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
name: MODEL_BRIDGE_OPTIONAL_LIMITS_CAPABILITY,
|
||||
version: 1,
|
||||
critical: false
|
||||
}
|
||||
]
|
||||
: [])
|
||||
|
||||
Reference in New Issue
Block a user