Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval-

---

## [0.167.1] — 2026-08-21

### Changed

- `waitForActiveHandlers` and `sendJsonIfOpen` had three byte-identical private copies each, one per local HTTP server (`src/analyst/trace-tool-callback.ts`, `src/campaign/external-optimizer-callback.ts`, `src/campaign/external-optimizer-model-proxy.ts`). Both now live in `src/campaign/external-optimizer-http.ts`, the module all three already imported `closeServer`, `listenLocal`, and `sendJson` from. The drain loop re-reads the handler set on every pass because a running handler can register another; a copy that awaited one snapshot would let the caller close the server with work outstanding. No behavior change and no export change: the helpers are internal to that module's consumers.

---

## [0.167.0] — 2026-08-21

### Changed
Expand Down
2 changes: 1 addition & 1 deletion clients/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "agent-eval-rpc"
version = "0.167.0"
version = "0.167.1"
description = "Python RPC client, official optimizer bridge, and DSPy metric adapter for @tangle-network/agent-eval."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion clients/python/src/agent_eval_rpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
try:
__version__ = version("agent-eval-rpc")
except PackageNotFoundError:
__version__ = "0.167.0"
__version__ = "0.167.1"

__all__ = [
"Client",
Expand Down
2 changes: 1 addition & 1 deletion clients/python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/agent-eval",
"version": "0.167.0",
"version": "0.167.1",
"description": "Evaluate and improve AI agents from runs, traces, judges, and feedback. Compare candidates, cluster failures, measure lift, and gate releases.",
"homepage": "https://github.com/tangle-network/agent-eval#readme",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions src/analyst/benchmark-implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const ANALYST_BENCHMARK_DEPENDENCY_LOCK_FILES = Object.freeze([
])

export const ANALYST_BENCHMARK_DEPENDENCY_LOCK_SHA256 =
'5487ec58c4a939ece38536a982a9a72e3d59363a2ed8677c6ad2aaa9699c06f1'
'a372aba7edb147c768813488fa7c69395f093f4c2b9bb9862aff2d3b468be47a'

/** The published benchmark evidence was produced at this package version, by
* the retired one-shot direct runner, before trace analysts moved to the
Expand Down Expand Up @@ -139,7 +139,7 @@ export const ANALYST_BENCHMARK_IMPLEMENTATION_FILES = Object.freeze([
])

export const ANALYST_BENCHMARK_IMPLEMENTATION_SHA256 =
'c3c6778b7102a635706b76a8bd5a1f0375cd84e30f900a4026c7939cb6fb04b9'
'2ab66a98fac14974e8e3fde86600899a38a1cbba2957bed3afad44261108c43e'

export function analystBenchmarkImplementationDigest() {
return ANALYST_BENCHMARK_IMPLEMENTATION_SHA256
Expand Down
18 changes: 6 additions & 12 deletions src/analyst/trace-tool-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import {
type ExternalOptimizerCallbackLimits,
resolveExternalOptimizerCallbackLimits,
} from '../campaign/external-optimizer-contracts'
import { closeServer, listenLocal, sendJson } from '../campaign/external-optimizer-http'
import {
closeServer,
listenLocal,
sendJsonIfOpen,
waitForActiveHandlers,
} from '../campaign/external-optimizer-http'
import type { TraceAnalysisToolDescriptor } from '../trace-analyst/tools'

export interface TraceToolCallback {
Expand Down Expand Up @@ -144,12 +149,6 @@ export async function startTraceToolCallback(args: {
}
}

async function waitForActiveHandlers(activeHandlers: Set<Promise<void>>): Promise<void> {
while (activeHandlers.size > 0) {
await Promise.allSettled([...activeHandlers])
}
}

function readJson(request: IncomingMessage, maxRequestBytes: number): Promise<unknown> {
return new Promise((resolve, reject) => {
let size = 0
Expand All @@ -174,11 +173,6 @@ function readJson(request: IncomingMessage, maxRequestBytes: number): Promise<un
})
}

function sendJsonIfOpen(response: ServerResponse, status: number, body: unknown): void {
if (response.destroyed || response.writableEnded) return
sendJson(response, status, body)
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
18 changes: 6 additions & 12 deletions src/campaign/external-optimizer-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
isRecord,
resolveExternalOptimizerCallbackLimits,
} from './external-optimizer-contracts'
import { closeServer, listenLocal, sendJson } from './external-optimizer-http'
import {
closeServer,
listenLocal,
sendJsonIfOpen,
waitForActiveHandlers,
} from './external-optimizer-http'

type UnsequencedObservation = ExternalOptimizerEvaluationObservation extends infer T
? T extends ExternalOptimizerEvaluationObservation
Expand Down Expand Up @@ -233,17 +238,6 @@ async function handleCallback<TResponse>(
}
}

async function waitForActiveHandlers(activeHandlers: Set<Promise<void>>): Promise<void> {
while (activeHandlers.size > 0) {
await Promise.allSettled([...activeHandlers])
}
}

function sendJsonIfOpen(response: ServerResponse, status: number, body: unknown): void {
if (response.destroyed || response.writableEnded) return
sendJson(response, status, body)
}

function readJson(request: IncomingMessage, maxRequestBytes: number): Promise<unknown> {
return new Promise((resolvePromise, reject) => {
let size = 0
Expand Down
25 changes: 25 additions & 0 deletions src/campaign/external-optimizer-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,28 @@ export function sendJson(response: ServerResponse, status: number, body: unknown
response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
response.end(JSON.stringify(body))
}

/**
* Send a JSON body only while the response can still take one.
*
* A handler that lost its client — the request was aborted, or the response
* already ended — must not write again; Node throws `ERR_STREAM_WRITE_AFTER_END`
* and the throw escapes into the server's error path rather than the caller's.
*/
export function sendJsonIfOpen(response: ServerResponse, status: number, body: unknown): void {
if (response.destroyed || response.writableEnded) return
sendJson(response, status, body)
}

/**
* Wait until every in-flight handler has settled.
*
* Re-reads the set on each pass: a handler that is still running can register
* another, so awaiting one snapshot would return while work is outstanding and
* the caller would close the server under it.
*/
export async function waitForActiveHandlers(activeHandlers: Set<Promise<void>>): Promise<void> {
while (activeHandlers.size > 0) {
await Promise.allSettled([...activeHandlers])
}
}
18 changes: 6 additions & 12 deletions src/campaign/external-optimizer-model-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ import {
type ExternalOptimizerWireCounts,
isRecord,
} from './external-optimizer-contracts'
import { closeServer, listenLocal, sendJson } from './external-optimizer-http'
import {
closeServer,
listenLocal,
sendJsonIfOpen,
waitForActiveHandlers,
} from './external-optimizer-http'

const MODEL_PROXY_PATHS = new Set(['/v1/chat/completions', '/v1/responses'])
type ModelProxyPath = '/v1/chat/completions' | '/v1/responses' | '/v1/messages'
Expand Down Expand Up @@ -474,17 +479,6 @@ async function handleModelProxyRequest(args: {
}
}

async function waitForActiveHandlers(activeHandlers: Set<Promise<void>>): Promise<void> {
while (activeHandlers.size > 0) {
await Promise.allSettled([...activeHandlers])
}
}

function sendJsonIfOpen(response: ServerResponse, status: number, body: unknown): void {
if (response.destroyed || response.writableEnded) return
sendJson(response, status, body)
}

async function forwardModelProxyRequest(args: {
call: ExternalOptimizerModelCall
callId: string
Expand Down
Loading