@rcrsr/rill-agent-foundry 0.19.0 → 0.21.0

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +87 -32
  2. package/dist/index.js +320 -180
  3. package/package.json +14 -11
package/dist/index.d.ts CHANGED
@@ -250,12 +250,23 @@ export interface SessionManager {
250
250
  */
251
251
  activeCount(): number;
252
252
  }
253
+ /**
254
+ * Options accepted by createSessionManager.
255
+ */
256
+ export interface SessionManagerOptions {
257
+ /**
258
+ * Maximum number of concurrently open sessions. Overrides the
259
+ * MAX_CONCURRENT_SESSIONS env var when provided.
260
+ */
261
+ readonly maxConcurrentSessions?: number | undefined;
262
+ }
253
263
  /**
254
264
  * Create a bounded pool manager for concurrent rill sessions.
255
265
  *
256
- * Max capacity reads from MAX_CONCURRENT_SESSIONS env var, default 10.
266
+ * Max capacity resolution order: options.maxConcurrentSessions, then the
267
+ * MAX_CONCURRENT_SESSIONS env var, then DEFAULT_MAX_SESSIONS.
257
268
  */
258
- export declare function createSessionManager(): SessionManager;
269
+ export declare function createSessionManager(options?: SessionManagerOptions): SessionManager;
259
270
  export interface HandlerDescription {
260
271
  readonly name: string;
261
272
  readonly description?: string | undefined;
@@ -266,6 +277,17 @@ export interface HandlerDescription {
266
277
  readonly description?: string | undefined;
267
278
  readonly defaultValue?: unknown;
268
279
  }>;
280
+ /**
281
+ * Handler return type annotation, formatted with the same grammar as
282
+ * parameter type strings (e.g. `stream(dict(content: string)):string`).
283
+ * Undefined when the handler closure has no `:T` annotation, or when the
284
+ * rill-build emitting the handler is too old to expose the field.
285
+ */
286
+ readonly returnType?: string | undefined;
287
+ }
288
+ export interface InitContext {
289
+ readonly globalVars?: Record<string, string> | undefined;
290
+ readonly ahiResolver?: ((agentName: string, request: RunRequest) => Promise<RunResponse>) | undefined;
269
291
  }
270
292
  export interface RunRequest {
271
293
  readonly params?: Record<string, unknown> | undefined;
@@ -275,13 +297,25 @@ export interface RunContext {
275
297
  readonly sessionVars?: Record<string, string> | undefined;
276
298
  readonly onLog?: ((message: string) => void) | undefined;
277
299
  readonly onChunk?: ((chunk: unknown) => Promise<void>) | undefined;
300
+ readonly signal?: AbortSignal | undefined;
278
301
  }
279
302
  export interface RunResponse {
280
303
  readonly state: "completed" | "error";
281
304
  readonly result: unknown;
282
305
  readonly streamed?: boolean | undefined;
283
306
  }
307
+ export interface AgentHandler {
308
+ describe(): HandlerDescription | null;
309
+ init(context?: InitContext): Promise<void>;
310
+ execute(request?: RunRequest, context?: RunContext): Promise<RunResponse>;
311
+ dispose(): Promise<void>;
312
+ }
313
+ export interface AgentManifest {
314
+ readonly defaultAgent: string;
315
+ readonly agents: ReadonlyMap<string, AgentHandler>;
316
+ }
284
317
  export interface AgentRouter {
318
+ readonly manifest: AgentManifest;
285
319
  run(agentName: string, request: RunRequest, context?: RunContext): Promise<RunResponse>;
286
320
  describe(agentName: string): HandlerDescription | null;
287
321
  agents(): string[];
@@ -293,50 +327,30 @@ export interface AgentRouter {
293
327
  *
294
328
  * State mapping: 'completed' → 'completed', 'error' → 'failed'.
295
329
  * Result is coerced to string; errors are encoded in the response body.
330
+ *
331
+ * When `debugErrors` is false (default), a failed-state error message is
332
+ * redacted with the same generic message `buildErrorResponse` uses for
333
+ * SERVER_ERROR, so internal detail does not leak through the sync response
334
+ * path when it is suppressed everywhere else. The same redaction is applied
335
+ * to the output message `text`, since it otherwise carries the same raw
336
+ * failure detail. When true, the raw message is passed through verbatim.
296
337
  */
297
- export declare function buildSyncResponse(result: RunResponse, responseId: string): FoundryResponse;
338
+ export declare function buildSyncResponse(result: RunResponse, responseId: string, debugErrors?: boolean): FoundryResponse;
298
339
  /**
299
340
  * Build a non-streaming JSON error response body.
300
341
  *
301
342
  * When debug is true, the original message is passed through verbatim.
302
343
  * When debug is false or absent, a generic message is returned based on
303
- * the error code per IR-10 (FOUNDRY_AGENT_DEBUG_ERRORS=false behavior).
344
+ * the error code (FOUNDRY_AGENT_DEBUG_ERRORS=false behavior).
304
345
  */
305
346
  export declare function buildErrorResponse(code: string, message: string, debug?: boolean): ErrorResponse;
306
347
  /**
307
348
  * Generate Foundry tool definitions from the default agent's handler descriptions.
308
349
  *
309
- * Only the default agent's handlers are exposed (AC-21).
350
+ * Only the default agent's handlers are exposed.
310
351
  * Returns an empty array when describe() returns null.
311
352
  */
312
353
  export declare function generateToolDefinitions(router: AgentRouter): FoundryToolDefinition[];
313
- export interface StreamOptions {
314
- readonly onError?: ((err: unknown) => void) | undefined;
315
- /** IdGenerator scoped to the request for correlated message IDs. */
316
- readonly idGenerator?: IdGenerator | undefined;
317
- /** Session ID echoed back in x-agent-session-id response header. */
318
- readonly sessionId?: string | undefined;
319
- /** Invocation ID echoed back in x-agent-invocation-id response header. */
320
- readonly invocationId?: string | undefined;
321
- /** Pre-built x-aml-foundry-agents-metadata JSON string. */
322
- readonly metadataHeader?: string | undefined;
323
- /**
324
- * When true, raw error messages are forwarded to clients. When false
325
- * (default), error events emit a generic message to avoid leaking
326
- * internal details. Mirrors the harness `debugErrors` option used by
327
- * `buildErrorResponse`.
328
- */
329
- readonly debugErrors?: boolean | undefined;
330
- }
331
- /**
332
- * Stream a Foundry Responses lifecycle via SSE.
333
- * Delegates to createFoundryStreamResponse for all paths.
334
- */
335
- export declare function streamFoundryResponse(_c: unknown, responseId: string, resultStream: AsyncIterable<{
336
- value?: unknown;
337
- }>, options: StreamOptions & {
338
- resultPromise?: Promise<string>;
339
- }): Response;
340
354
  export interface ConversationsClient {
341
355
  saveItems(conversationId: string, items: ReadonlyArray<unknown>): Promise<void>;
342
356
  }
@@ -394,5 +408,46 @@ export interface FoundryHarness {
394
408
  * GET /metrics — FoundryMetrics JSON
395
409
  */
396
410
  export declare function createFoundryHarness(router: AgentRouter, options?: FoundryHarnessOptions): FoundryHarness;
411
+ export interface RillHarnessLogger {
412
+ info(...args: unknown[]): void;
413
+ warn(...args: unknown[]): void;
414
+ error(...args: unknown[]): void;
415
+ }
416
+ export interface RillCompiledPackage {
417
+ readonly mount: string;
418
+ readonly buildOutput: {
419
+ readonly outputPath: string;
420
+ };
421
+ }
422
+ export interface RillServeContext {
423
+ readonly config: Record<string, unknown>;
424
+ readonly logger: RillHarnessLogger;
425
+ readonly packages: readonly RillCompiledPackage[];
426
+ readonly requestedMount: string | undefined;
427
+ readonly args: readonly string[];
428
+ readonly onShutdown: (handler: () => void | Promise<void>) => void;
429
+ readonly onSourceChange: (handler: () => void | Promise<void>) => void;
430
+ }
431
+ export interface RillPostBuildContext {
432
+ readonly outputDir: string;
433
+ readonly packages: readonly RillCompiledPackage[];
434
+ readonly logger: RillHarnessLogger;
435
+ }
436
+ export interface RillHarness {
437
+ readonly name: string;
438
+ readonly postBuild?: (ctx: RillPostBuildContext) => Promise<void>;
439
+ readonly serve?: (ctx: RillServeContext) => Promise<number>;
440
+ }
441
+ /**
442
+ * Default export consumed by the rill CLI (`rill install --replace`,
443
+ * `rill run`) when this package is declared as a bundle harness. `serve`
444
+ * assembles a router from the bundle's compiled packages and hosts it over the
445
+ * Foundry Responses harness on `config.port` (default 3000).
446
+ */
447
+ declare const harness: RillHarness;
448
+
449
+ export {
450
+ harness as default,
451
+ };
397
452
 
398
453
  export {};
package/dist/index.js CHANGED
@@ -80,6 +80,38 @@ function findLastUserText(items) {
80
80
  }
81
81
  return text;
82
82
  }
83
+ function isContentPart(value) {
84
+ return typeof value === "object" && value !== null && value.type === "input_text" && typeof value.text === "string";
85
+ }
86
+ function isValidContent(content) {
87
+ if (typeof content === "string") {
88
+ return true;
89
+ }
90
+ return Array.isArray(content) && content.every(isContentPart);
91
+ }
92
+ function isInputItem(item) {
93
+ if (typeof item !== "object" || item === null) {
94
+ return false;
95
+ }
96
+ const type = item.type;
97
+ if (type === "message") {
98
+ const role = item.role;
99
+ const content = item.content;
100
+ return typeof role === "string" && isValidContent(content);
101
+ }
102
+ if (type === "function_call_output") {
103
+ const callId = item.call_id;
104
+ const output = item.output;
105
+ return typeof callId === "string" && typeof output === "string";
106
+ }
107
+ if (type === "function_call") {
108
+ const callId = item.call_id;
109
+ const name = item.name;
110
+ const args = item.arguments;
111
+ return typeof callId === "string" && typeof name === "string" && typeof args === "string";
112
+ }
113
+ return false;
114
+ }
83
115
  function extractFunctionCallOutputs(items) {
84
116
  const callMap = /* @__PURE__ */ new Map();
85
117
  for (const item of items) {
@@ -120,6 +152,11 @@ function extractInput(input) {
120
152
  if (!Array.isArray(input) || input.length === 0) {
121
153
  throw new InputError("Missing required field: input");
122
154
  }
155
+ for (const item of input) {
156
+ if (!isInputItem(item)) {
157
+ throw new InputError("Invalid input item shape");
158
+ }
159
+ }
123
160
  const items = input;
124
161
  const hasFunctionCallOutput = items.some(
125
162
  (item) => item.type === "function_call_output"
@@ -136,25 +173,47 @@ function extractInput(input) {
136
173
 
137
174
  // src/session.ts
138
175
  var DEFAULT_MAX_SESSIONS = 10;
139
- function createSessionManager() {
176
+ function createSessionManager(options) {
140
177
  const raw = process.env["MAX_CONCURRENT_SESSIONS"];
141
- const parsed = raw !== void 0 ? parseInt(raw, 10) : NaN;
142
- const max = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_SESSIONS;
143
- const active = /* @__PURE__ */ new Set();
178
+ const parsedEnv = raw !== void 0 ? parseInt(raw, 10) : NaN;
179
+ const envMax = Number.isFinite(parsedEnv) && parsedEnv > 0 ? parsedEnv : DEFAULT_MAX_SESSIONS;
180
+ const max = options?.maxConcurrentSessions ?? envMax;
181
+ const activeTokens = /* @__PURE__ */ new Map();
182
+ const tokensBySession = /* @__PURE__ */ new Map();
144
183
  return {
145
184
  acquire(conversationId) {
146
- if (active.size >= max) {
185
+ if (activeTokens.size >= max) {
147
186
  throw new CapacityError(max);
148
187
  }
149
188
  const sessionId = conversationId ?? generateId("sess_");
150
- active.add(sessionId);
189
+ const token = generateId("tok_");
190
+ activeTokens.set(token, sessionId);
191
+ const tokens = tokensBySession.get(sessionId);
192
+ if (tokens === void 0) {
193
+ tokensBySession.set(sessionId, /* @__PURE__ */ new Set([token]));
194
+ } else {
195
+ tokens.add(token);
196
+ }
151
197
  return sessionId;
152
198
  },
153
199
  release(sessionId) {
154
- active.delete(sessionId);
200
+ const tokens = tokensBySession.get(sessionId);
201
+ if (tokens === void 0) {
202
+ return;
203
+ }
204
+ const iterResult = tokens.values().next();
205
+ if (iterResult.done) {
206
+ return;
207
+ }
208
+ const token = iterResult.value;
209
+ tokens.delete(token);
210
+ if (tokens.size === 0) {
211
+ tokensBySession.delete(sessionId);
212
+ }
213
+ activeTokens.delete(token);
155
214
  },
156
215
  activeCount() {
157
- return active.size;
216
+ return activeTokens.size;
158
217
  }
159
218
  };
160
219
  }
@@ -172,10 +231,12 @@ function coerceResult(result) {
172
231
  }
173
232
  return JSON.stringify(result);
174
233
  }
175
- function buildSyncResponse(result, responseId) {
234
+ function buildSyncResponse(result, responseId, debugErrors = false) {
176
235
  const status = result.state === "completed" ? "completed" : "failed";
177
- const text = coerceResult(result.result);
236
+ const rawText = coerceResult(result.result);
178
237
  const msgId = generateId("msg_");
238
+ const errorMessage = debugErrors ? rawText : GENERIC_MESSAGES["SERVER_ERROR"] ?? FALLBACK_MESSAGE;
239
+ const text = status === "failed" && !debugErrors ? errorMessage : rawText;
179
240
  return {
180
241
  id: responseId,
181
242
  object: "response",
@@ -196,7 +257,7 @@ function buildSyncResponse(result, responseId) {
196
257
  ]
197
258
  }
198
259
  ],
199
- error: status === "failed" ? { code: "SERVER_ERROR", message: text } : null,
260
+ error: status === "failed" ? { code: "SERVER_ERROR", message: errorMessage } : null,
200
261
  metadata: {},
201
262
  temperature: 0,
202
263
  top_p: 0,
@@ -262,7 +323,184 @@ function generateToolDefinitions(router) {
262
323
  ];
263
324
  }
264
325
 
326
+ // src/conversations.ts
327
+ var PersistenceError = class extends Error {
328
+ statusCode;
329
+ constructor(message) {
330
+ super(message);
331
+ this.name = "PersistenceError";
332
+ this.statusCode = 502;
333
+ }
334
+ };
335
+ var API_VERSION = "2025-11-15-preview";
336
+ var TOKEN_SCOPE = "https://ai.azure.com/.default";
337
+ var REQUEST_TIMEOUT_MS = 3e4;
338
+ function createConversationsClient(projectEndpoint, credential) {
339
+ return {
340
+ async saveItems(conversationId, items) {
341
+ let token;
342
+ try {
343
+ const accessToken = await credential.getToken(TOKEN_SCOPE);
344
+ if (accessToken === null) {
345
+ throw new Error(`Failed to acquire token for scope ${TOKEN_SCOPE}`);
346
+ }
347
+ token = accessToken.token;
348
+ } catch (err) {
349
+ if (err instanceof CredentialError) {
350
+ throw err;
351
+ }
352
+ const message = err instanceof Error ? err.message : String(err);
353
+ throw new CredentialError(message);
354
+ }
355
+ const encodedConversationId = encodeURIComponent(conversationId);
356
+ const url = new URL(
357
+ `${projectEndpoint}/openai/conversations/${encodedConversationId}/items`
358
+ );
359
+ url.searchParams.set("api-version", API_VERSION);
360
+ const controller = new AbortController();
361
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
362
+ try {
363
+ let response;
364
+ try {
365
+ response = await fetch(url.toString(), {
366
+ method: "POST",
367
+ headers: {
368
+ "Content-Type": "application/json",
369
+ Authorization: `Bearer ${token}`
370
+ },
371
+ body: JSON.stringify({ items }),
372
+ signal: controller.signal
373
+ });
374
+ } catch (err) {
375
+ const isTimeout = err instanceof Error && err.name === "AbortError";
376
+ throw new PersistenceError(
377
+ isTimeout ? "Conversations API timeout" : `Conversations API error: ${err instanceof Error ? err.message : String(err)}`
378
+ );
379
+ }
380
+ if (!response.ok) {
381
+ throw new PersistenceError(
382
+ `Conversations API error: ${response.status}`
383
+ );
384
+ }
385
+ } finally {
386
+ clearTimeout(timer);
387
+ }
388
+ }
389
+ };
390
+ }
391
+
392
+ // src/telemetry.ts
393
+ import { trace } from "@opentelemetry/api";
394
+ import { NodeSDK } from "@opentelemetry/sdk-node";
395
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
396
+ import { resourceFromAttributes } from "@opentelemetry/resources";
397
+ var sdk;
398
+ function initTelemetry(options) {
399
+ if (!process.env["OTEL_EXPORTER_OTLP_ENDPOINT"]) return;
400
+ if (sdk !== void 0) return;
401
+ const resource = resourceFromAttributes({
402
+ "service.name": options?.agentName ?? "rill-foundry-harness",
403
+ "service.version": options?.agentVersion ?? "0.0.0"
404
+ });
405
+ const traceExporter = new OTLPTraceExporter();
406
+ sdk = new NodeSDK({ resource, traceExporter });
407
+ sdk.start();
408
+ }
409
+ function getTracer() {
410
+ return trace.getTracer("rill-foundry-harness");
411
+ }
412
+ async function shutdownTelemetry() {
413
+ if (sdk === void 0) return;
414
+ const instance = sdk;
415
+ sdk = void 0;
416
+ await instance.shutdown();
417
+ }
418
+
419
+ // src/harness.ts
420
+ import "hono";
421
+ import { SpanStatusCode } from "@opentelemetry/api";
422
+ import { DefaultAzureCredential } from "@azure/identity";
423
+ import { validateParams, routerErrorToStatus as routerErrorToStatus2 } from "@rcrsr/rill-agent";
424
+
425
+ // ../../shared/hono-kit/src/index.ts
426
+ import { existsSync } from "fs";
427
+ import path from "path";
428
+ import { Hono } from "hono";
429
+ import { serve } from "@hono/node-server";
430
+ function assertJsonObject(parsed) {
431
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
432
+ throw new Error("Request body must be a JSON object");
433
+ }
434
+ return parsed;
435
+ }
436
+ function createHarnessLifecycle(options) {
437
+ const app = new Hono();
438
+ let server;
439
+ async function listen(port) {
440
+ if (server !== void 0) {
441
+ throw new Error("Server is already listening");
442
+ }
443
+ return new Promise((resolve, reject) => {
444
+ const started = serve({ fetch: app.fetch, port }, () => {
445
+ started.off("error", onError);
446
+ options?.serverTweaks?.(started);
447
+ resolve();
448
+ });
449
+ function onError(err) {
450
+ server = void 0;
451
+ reject(err);
452
+ }
453
+ started.once("error", onError);
454
+ server = started;
455
+ });
456
+ }
457
+ async function close() {
458
+ if (server === void 0) return;
459
+ const current = server;
460
+ server = void 0;
461
+ if ("closeAllConnections" in current) {
462
+ current.closeAllConnections();
463
+ }
464
+ await new Promise((resolve, reject) => {
465
+ current.close((err) => {
466
+ if (err) reject(err);
467
+ else resolve();
468
+ });
469
+ });
470
+ }
471
+ return { app, listen, close };
472
+ }
473
+ function compiledPackageEntries(ctx) {
474
+ return ctx.packages.map((p) => ({
475
+ name: p.mount,
476
+ dir: p.buildOutput.outputPath
477
+ }));
478
+ }
479
+ function readHarnessPort(config, fallback) {
480
+ const p = config["port"];
481
+ if (typeof p === "number" && Number.isInteger(p)) return p;
482
+ if (typeof p === "string" && /^\d+$/.test(p)) return Number(p);
483
+ return fallback;
484
+ }
485
+ function assertCompiledHandlers(ctx) {
486
+ for (const pkg of ctx.packages) {
487
+ const handlerPath = path.join(pkg.buildOutput.outputPath, "handler.js");
488
+ if (!existsSync(handlerPath)) {
489
+ throw new Error(`missing handler file: ${handlerPath}`);
490
+ }
491
+ }
492
+ }
493
+ async function runRillServe(ctx, start) {
494
+ const handle = await start(compiledPackageEntries(ctx));
495
+ ctx.onShutdown(async () => {
496
+ await handle.close();
497
+ });
498
+ return new Promise(() => {
499
+ });
500
+ }
501
+
265
502
  // src/stream.ts
503
+ import { routerErrorToStatus } from "@rcrsr/rill-agent";
266
504
  var REDACTED_ERROR_MESSAGE = "Internal server error";
267
505
  var encoder = new TextEncoder();
268
506
  function sseChunk(event, data) {
@@ -321,6 +559,7 @@ function createFoundryStreamResponse(responseId, options) {
321
559
  }
322
560
  }
323
561
  function emitCompletion(controller, fullText) {
562
+ if (closed) return;
324
563
  controller.enqueue(
325
564
  ev("response.output_text.done", {
326
565
  type: "response.output_text.done",
@@ -345,8 +584,10 @@ function createFoundryStreamResponse(responseId, options) {
345
584
  function emitError(controller, err) {
346
585
  clearKeepAlive();
347
586
  options.onError?.(err);
587
+ if (closed) return;
348
588
  const rawMessage = err instanceof Error ? err.message : String(err);
349
589
  const message = options.debugErrors === true ? rawMessage : REDACTED_ERROR_MESSAGE;
590
+ const code = routerErrorToStatus(err) === 404 ? "NOT_FOUND" : "SERVER_ERROR";
350
591
  controller.enqueue(
351
592
  encoder.encode(
352
593
  sseChunk(
@@ -354,7 +595,7 @@ function createFoundryStreamResponse(responseId, options) {
354
595
  JSON.stringify({
355
596
  type: "error",
356
597
  sequence_number: seq++,
357
- code: "SERVER_ERROR",
598
+ code,
358
599
  message,
359
600
  param: ""
360
601
  })
@@ -410,152 +651,6 @@ function createFoundryStreamResponse(responseId, options) {
410
651
  headers: sseHeaders(options)
411
652
  });
412
653
  }
413
- function streamFoundryResponse(_c, responseId, resultStream, options) {
414
- const resultPromise = options.resultPromise ?? (async () => {
415
- let text = "";
416
- for await (const chunk of resultStream) {
417
- if (chunk.value !== null && chunk.value !== void 0) {
418
- text += typeof chunk.value === "string" ? chunk.value : JSON.stringify(chunk.value);
419
- }
420
- }
421
- return text;
422
- })();
423
- return createFoundryStreamResponse(responseId, {
424
- ...options,
425
- resultPromise
426
- });
427
- }
428
-
429
- // src/conversations.ts
430
- var PersistenceError = class extends Error {
431
- statusCode;
432
- constructor(message) {
433
- super(message);
434
- this.name = "PersistenceError";
435
- this.statusCode = 502;
436
- }
437
- };
438
- var API_VERSION = "2025-11-15-preview";
439
- var TOKEN_SCOPE = "https://ai.azure.com/.default";
440
- var REQUEST_TIMEOUT_MS = 3e4;
441
- function createConversationsClient(projectEndpoint, credential) {
442
- return {
443
- async saveItems(conversationId, items) {
444
- let token;
445
- try {
446
- const accessToken = await credential.getToken(TOKEN_SCOPE);
447
- if (accessToken === null) {
448
- throw new Error(`Failed to acquire token for scope ${TOKEN_SCOPE}`);
449
- }
450
- token = accessToken.token;
451
- } catch (err) {
452
- if (err instanceof CredentialError) {
453
- throw err;
454
- }
455
- const message = err instanceof Error ? err.message : String(err);
456
- throw new CredentialError(message);
457
- }
458
- const encodedConversationId = encodeURIComponent(conversationId);
459
- const url = new URL(
460
- `${projectEndpoint}/openai/conversations/${encodedConversationId}/items`
461
- );
462
- url.searchParams.set("api-version", API_VERSION);
463
- const controller = new AbortController();
464
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
465
- try {
466
- let response;
467
- try {
468
- response = await fetch(url.toString(), {
469
- method: "POST",
470
- headers: {
471
- "Content-Type": "application/json",
472
- Authorization: `Bearer ${token}`
473
- },
474
- body: JSON.stringify({ items }),
475
- signal: controller.signal
476
- });
477
- } catch (err) {
478
- const isTimeout = err instanceof Error && err.name === "AbortError";
479
- throw new PersistenceError(
480
- isTimeout ? "Conversations API timeout" : `Conversations API error: ${err instanceof Error ? err.message : String(err)}`
481
- );
482
- }
483
- if (!response.ok) {
484
- throw new PersistenceError(
485
- `Conversations API error: ${response.status}`
486
- );
487
- }
488
- } finally {
489
- clearTimeout(timer);
490
- }
491
- }
492
- };
493
- }
494
-
495
- // src/telemetry.ts
496
- import { trace } from "@opentelemetry/api";
497
- import { NodeSDK } from "@opentelemetry/sdk-node";
498
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
499
- import { resourceFromAttributes } from "@opentelemetry/resources";
500
- var sdk;
501
- function initTelemetry(options) {
502
- if (!process.env["OTEL_EXPORTER_OTLP_ENDPOINT"]) return;
503
- if (sdk !== void 0) return;
504
- const resource = resourceFromAttributes({
505
- "service.name": options?.agentName ?? "rill-foundry-harness",
506
- "service.version": options?.agentVersion ?? "0.0.0"
507
- });
508
- const traceExporter = new OTLPTraceExporter();
509
- sdk = new NodeSDK({ resource, traceExporter });
510
- sdk.start();
511
- }
512
- function getTracer() {
513
- return trace.getTracer("rill-foundry-harness");
514
- }
515
- async function shutdownTelemetry() {
516
- if (sdk === void 0) return;
517
- const instance = sdk;
518
- sdk = void 0;
519
- await instance.shutdown();
520
- }
521
-
522
- // src/harness.ts
523
- import "hono";
524
- import { SpanStatusCode } from "@opentelemetry/api";
525
- import { DefaultAzureCredential } from "@azure/identity";
526
- import { validateParams, routerErrorToStatus } from "@rcrsr/rill-agent";
527
-
528
- // ../../shared/hono-kit/src/index.ts
529
- import { Hono } from "hono";
530
- import { serve } from "@hono/node-server";
531
- function assertJsonObject(parsed) {
532
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
533
- throw new Error("Request body must be a JSON object");
534
- }
535
- return parsed;
536
- }
537
- function createHarnessLifecycle(options) {
538
- const app = new Hono();
539
- let server;
540
- async function listen(port) {
541
- if (server !== void 0) {
542
- throw new Error("Server is already listening");
543
- }
544
- return new Promise((resolve) => {
545
- server = serve({ fetch: app.fetch, port }, () => {
546
- options?.serverTweaks?.(server);
547
- resolve();
548
- });
549
- });
550
- }
551
- async function close() {
552
- if (server !== void 0) {
553
- server.close();
554
- server = void 0;
555
- }
556
- }
557
- return { app, listen, close };
558
- }
559
654
 
560
655
  // src/harness.ts
561
656
  var DEFAULT_PORT = 8088;
@@ -583,7 +678,26 @@ function resolveConversationId(conversation) {
583
678
  if (typeof conversation === "string") {
584
679
  return conversation;
585
680
  }
586
- return conversation.id;
681
+ if (typeof conversation === "object" && conversation !== null && typeof conversation.id === "string") {
682
+ return conversation.id;
683
+ }
684
+ throw new InputError("conversation.id must be a string");
685
+ }
686
+ function resolveMetadataResponseId(metadata) {
687
+ if (metadata === void 0 || metadata === null) {
688
+ return void 0;
689
+ }
690
+ if (typeof metadata !== "object") {
691
+ throw new InputError("metadata must be an object");
692
+ }
693
+ const responseId = metadata["response_id"];
694
+ if (responseId === void 0) {
695
+ return void 0;
696
+ }
697
+ if (typeof responseId !== "string") {
698
+ throw new InputError("metadata.response_id must be a string");
699
+ }
700
+ return responseId;
587
701
  }
588
702
  function createFoundryHarness(router, options) {
589
703
  const port = resolvePort(options);
@@ -595,7 +709,9 @@ function createFoundryHarness(router, options) {
595
709
  agentName: agentName ?? router.defaultAgent(),
596
710
  agentVersion
597
711
  });
598
- const sessions = createSessionManager();
712
+ const sessions = createSessionManager({
713
+ maxConcurrentSessions: options?.maxConcurrentSessions
714
+ });
599
715
  const projectEndpoint = process.env["FOUNDRY_PROJECT_ENDPOINT"];
600
716
  const azureCredential = projectEndpoint !== void 0 ? new DefaultAzureCredential() : void 0;
601
717
  const conversationsClient = projectEndpoint !== void 0 && azureCredential !== void 0 ? createConversationsClient(projectEndpoint, azureCredential) : void 0;
@@ -684,7 +800,19 @@ function createFoundryHarness(router, options) {
684
800
  500
685
801
  );
686
802
  }
687
- const conversationId = resolveConversationId(body.conversation);
803
+ let conversationId;
804
+ let metadataResponseId;
805
+ try {
806
+ conversationId = resolveConversationId(body.conversation);
807
+ metadataResponseId = resolveMetadataResponseId(body.metadata);
808
+ } catch (err) {
809
+ errorCount++;
810
+ const msg = err instanceof InputError ? err.message : String(err);
811
+ return c.json(
812
+ buildErrorResponse("INVALID_REQUEST", msg, debugErrors),
813
+ 400
814
+ );
815
+ }
688
816
  const agentName_ = extracted.targetAgent ?? router.defaultAgent();
689
817
  const validationError = validateParams(
690
818
  extracted.params,
@@ -718,10 +846,7 @@ function createFoundryHarness(router, options) {
718
846
  500
719
847
  );
720
848
  }
721
- const idGen = createIdGenerator(
722
- body.metadata?.["response_id"],
723
- conversationId
724
- );
849
+ const idGen = createIdGenerator(metadataResponseId, conversationId);
725
850
  const responseId = idGen.responseId;
726
851
  const invocationId = c.req.header("x-agent-invocation-id") ?? generateId("inv_");
727
852
  const agentSessionId = c.req.query("session") ?? c.req.header("x-agent-session-id") ?? sessionId;
@@ -813,8 +938,7 @@ function createFoundryHarness(router, options) {
813
938
  }
814
939
  };
815
940
  const onChunk = async (chunk) => {
816
- const text = typeof chunk === "string" ? chunk : JSON.stringify(chunk);
817
- pushChunk(text);
941
+ pushChunk(coerceResult(chunk));
818
942
  };
819
943
  const streamContext = { ...runContext, onChunk };
820
944
  router.run(agentName_, runRequest, streamContext).then((result) => {
@@ -822,10 +946,8 @@ function createFoundryHarness(router, options) {
822
946
  span.end();
823
947
  sessions.release(sessionId);
824
948
  if (!result.streamed) {
825
- if (result.result !== null && result.result !== void 0) {
826
- const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
827
- if (text !== "") pushChunk(text);
828
- }
949
+ const text = coerceResult(result.result);
950
+ if (text !== "") pushChunk(text);
829
951
  }
830
952
  endChunks();
831
953
  }).catch((err) => {
@@ -839,7 +961,6 @@ function createFoundryHarness(router, options) {
839
961
  });
840
962
  return createFoundryStreamResponse(responseId, {
841
963
  chunks,
842
- idGenerator: idGen,
843
964
  onError: (_err) => {
844
965
  errorCount++;
845
966
  },
@@ -870,7 +991,7 @@ function createFoundryHarness(router, options) {
870
991
  span.end();
871
992
  errorCount++;
872
993
  const msg = err instanceof Error ? err.message : String(err);
873
- const status = routerErrorToStatus(err);
994
+ const status = routerErrorToStatus2(err);
874
995
  if (status === 404) {
875
996
  return c.json(buildErrorResponse("NOT_FOUND", msg, debugErrors), 404);
876
997
  }
@@ -880,7 +1001,7 @@ function createFoundryHarness(router, options) {
880
1001
  );
881
1002
  }
882
1003
  span.end();
883
- const response = buildSyncResponse(result, responseId);
1004
+ const response = buildSyncResponse(result, responseId, debugErrors);
884
1005
  const shouldStore = body.store === true;
885
1006
  if (shouldStore && conversationId !== void 0 && conversationsClient !== void 0) {
886
1007
  try {
@@ -926,11 +1047,11 @@ function createFoundryHarness(router, options) {
926
1047
  );
927
1048
  }
928
1049
  } catch (err) {
929
- if (!(err instanceof CredentialError)) {
930
- const message = err instanceof Error ? err.message : String(err);
931
- throw new CredentialError(message);
1050
+ if (err instanceof CredentialError) {
1051
+ throw err;
932
1052
  }
933
- process.exit(1);
1053
+ const message = err instanceof Error ? err.message : String(err);
1054
+ throw new CredentialError(message);
934
1055
  }
935
1056
  }
936
1057
  await lifecycle.listen(port);
@@ -948,6 +1069,25 @@ function createFoundryHarness(router, options) {
948
1069
  }
949
1070
  return { listen, close, app, metrics };
950
1071
  }
1072
+
1073
+ // src/index.ts
1074
+ import { createRouter, assembleManifest } from "@rcrsr/rill-agent";
1075
+ var HARNESS_NAME = "@rcrsr/rill-agent-foundry";
1076
+ var harness = {
1077
+ name: HARNESS_NAME,
1078
+ postBuild: async (ctx) => {
1079
+ assertCompiledHandlers(ctx);
1080
+ },
1081
+ serve: (ctx) => runRillServe(ctx, async (entries) => {
1082
+ const router = await createRouter(await assembleManifest(entries));
1083
+ const port = readHarnessPort(ctx.config, 3e3);
1084
+ const server = createFoundryHarness(router, { port });
1085
+ await server.listen();
1086
+ ctx.logger.info(`[${HARNESS_NAME}] listening on :${port}`);
1087
+ return server;
1088
+ })
1089
+ };
1090
+ var index_default = harness;
951
1091
  export {
952
1092
  CapacityError,
953
1093
  CredentialError,
@@ -959,11 +1099,11 @@ export {
959
1099
  createFoundryHarness,
960
1100
  createIdGenerator,
961
1101
  createSessionManager,
1102
+ index_default as default,
962
1103
  extractInput,
963
1104
  generateId,
964
1105
  generateToolDefinitions,
965
1106
  getTracer,
966
1107
  initTelemetry,
967
- shutdownTelemetry,
968
- streamFoundryResponse
1108
+ shutdownTelemetry
969
1109
  };
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@rcrsr/rill-agent-foundry",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "rill agent foundry — Azure-hosted harness factory",
5
5
  "license": "MIT",
6
6
  "author": "Andre Bremer",
7
7
  "type": "module",
8
+ "rill": {
9
+ "role": "harness"
10
+ },
8
11
  "main": "dist/index.js",
9
12
  "types": "dist/index.d.ts",
10
13
  "keywords": [
@@ -19,19 +22,19 @@
19
22
  "@opentelemetry/api": "^1.9.0"
20
23
  },
21
24
  "dependencies": {
22
- "@azure/identity": "^4.10.0",
23
- "@hono/node-server": "^2.0.1",
24
- "@opentelemetry/exporter-trace-otlp-http": "^0.216.0",
25
- "@opentelemetry/resources": "^2.7.1",
26
- "@opentelemetry/sdk-node": "^0.216.0",
27
- "hono": "^4.12.16",
28
- "@rcrsr/rill-agent": "~0.19.0"
25
+ "@azure/identity": "^4.13.2",
26
+ "@hono/node-server": "^2.1.1",
27
+ "@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
28
+ "@opentelemetry/resources": "^2.11.0",
29
+ "@opentelemetry/sdk-node": "^0.222.0",
30
+ "@rcrsr/rill-agent": "~0.21.0",
31
+ "hono": "^4.13.7"
29
32
  },
30
33
  "devDependencies": {
31
34
  "@opentelemetry/api": "^1.9.0",
35
+ "@rcrsr/rill-agent-hono-kit": "^0.21.0",
32
36
  "dts-bundle-generator": "^9.5.1",
33
- "tsup": "^8.5.0",
34
- "@rcrsr/rill-agent-hono-kit": "^0.19.0"
37
+ "tsup": "^8.5.1"
35
38
  },
36
39
  "files": [
37
40
  "dist"
@@ -52,7 +55,7 @@
52
55
  "build": "tsup && dts-bundle-generator --config dts-bundle-generator.config.cjs && node -e \"const fs=require('fs');const walk=(d)=>fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?walk(d+'/'+e.name):[d+'/'+e.name]);const hits=walk('dist').filter(f=>fs.readFileSync(f,'utf8').includes('@rcrsr/rill-agent-hono-kit'));if(hits.length){console.error('hono-kit leak in dist:',hits);process.exit(1);}\"",
53
56
  "test": "vitest run",
54
57
  "typecheck": "tsc --noEmit",
55
- "lint": "eslint --config ../../../eslint.config.js src/",
58
+ "lint": "oxlint --config ../../../.oxlintrc.json src/ tests/",
56
59
  "check": "pnpm run build && pnpm run test && pnpm run lint"
57
60
  }
58
61
  }