@rcrsr/rill-agent-foundry 0.20.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.
- package/dist/index.d.ts +21 -30
- package/dist/index.js +294 -204
- package/package.json +10 -10
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
|
|
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;
|
|
@@ -316,8 +327,15 @@ export interface AgentRouter {
|
|
|
316
327
|
*
|
|
317
328
|
* State mapping: 'completed' → 'completed', 'error' → 'failed'.
|
|
318
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.
|
|
319
337
|
*/
|
|
320
|
-
export declare function buildSyncResponse(result: RunResponse, responseId: string): FoundryResponse;
|
|
338
|
+
export declare function buildSyncResponse(result: RunResponse, responseId: string, debugErrors?: boolean): FoundryResponse;
|
|
321
339
|
/**
|
|
322
340
|
* Build a non-streaming JSON error response body.
|
|
323
341
|
*
|
|
@@ -333,33 +351,6 @@ export declare function buildErrorResponse(code: string, message: string, debug?
|
|
|
333
351
|
* Returns an empty array when describe() returns null.
|
|
334
352
|
*/
|
|
335
353
|
export declare function generateToolDefinitions(router: AgentRouter): FoundryToolDefinition[];
|
|
336
|
-
export interface StreamOptions {
|
|
337
|
-
readonly onError?: ((err: unknown) => void) | undefined;
|
|
338
|
-
/** IdGenerator scoped to the request for correlated message IDs. */
|
|
339
|
-
readonly idGenerator?: IdGenerator | undefined;
|
|
340
|
-
/** Session ID echoed back in x-agent-session-id response header. */
|
|
341
|
-
readonly sessionId?: string | undefined;
|
|
342
|
-
/** Invocation ID echoed back in x-agent-invocation-id response header. */
|
|
343
|
-
readonly invocationId?: string | undefined;
|
|
344
|
-
/** Pre-built x-aml-foundry-agents-metadata JSON string. */
|
|
345
|
-
readonly metadataHeader?: string | undefined;
|
|
346
|
-
/**
|
|
347
|
-
* When true, raw error messages are forwarded to clients. When false
|
|
348
|
-
* (default), error events emit a generic message to avoid leaking
|
|
349
|
-
* internal details. Mirrors the harness `debugErrors` option used by
|
|
350
|
-
* `buildErrorResponse`.
|
|
351
|
-
*/
|
|
352
|
-
readonly debugErrors?: boolean | undefined;
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Stream a Foundry Responses lifecycle via SSE.
|
|
356
|
-
* Delegates to createFoundryStreamResponse for all paths.
|
|
357
|
-
*/
|
|
358
|
-
export declare function streamFoundryResponse(_c: unknown, responseId: string, resultStream: AsyncIterable<{
|
|
359
|
-
value?: unknown;
|
|
360
|
-
}>, options: StreamOptions & {
|
|
361
|
-
resultPromise?: Promise<string>;
|
|
362
|
-
}): Response;
|
|
363
354
|
export interface ConversationsClient {
|
|
364
355
|
saveItems(conversationId: string, items: ReadonlyArray<unknown>): Promise<void>;
|
|
365
356
|
}
|
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
|
|
142
|
-
const
|
|
143
|
-
const
|
|
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 (
|
|
185
|
+
if (activeTokens.size >= max) {
|
|
147
186
|
throw new CapacityError(max);
|
|
148
187
|
}
|
|
149
188
|
const sessionId = conversationId ?? generateId("sess_");
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
260
|
+
error: status === "failed" ? { code: "SERVER_ERROR", message: errorMessage } : null,
|
|
200
261
|
metadata: {},
|
|
201
262
|
temperature: 0,
|
|
202
263
|
top_p: 0,
|
|
@@ -262,170 +323,6 @@ function generateToolDefinitions(router) {
|
|
|
262
323
|
];
|
|
263
324
|
}
|
|
264
325
|
|
|
265
|
-
// src/stream.ts
|
|
266
|
-
var REDACTED_ERROR_MESSAGE = "Internal server error";
|
|
267
|
-
var encoder = new TextEncoder();
|
|
268
|
-
function sseChunk(event, data) {
|
|
269
|
-
return `event: ${event}
|
|
270
|
-
data: ${data}
|
|
271
|
-
|
|
272
|
-
`;
|
|
273
|
-
}
|
|
274
|
-
function sseHeaders(options) {
|
|
275
|
-
const headers = {
|
|
276
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
277
|
-
"cache-control": "no-store",
|
|
278
|
-
"x-accel-buffering": "no",
|
|
279
|
-
"x-aml-foundry-agents-metadata": options?.metadataHeader ?? JSON.stringify({
|
|
280
|
-
package: { name: "azure-ai-agentserver-core", version: "1.0.0b17" },
|
|
281
|
-
runtime: {
|
|
282
|
-
python_version: "3.11.0",
|
|
283
|
-
platform: "Linux",
|
|
284
|
-
host_name: "",
|
|
285
|
-
replica_name: ""
|
|
286
|
-
}
|
|
287
|
-
})
|
|
288
|
-
};
|
|
289
|
-
if (options?.sessionId !== void 0) {
|
|
290
|
-
headers["x-agent-session-id"] = options.sessionId;
|
|
291
|
-
}
|
|
292
|
-
if (options?.invocationId !== void 0) {
|
|
293
|
-
headers["x-agent-invocation-id"] = options.invocationId;
|
|
294
|
-
}
|
|
295
|
-
return headers;
|
|
296
|
-
}
|
|
297
|
-
function createFoundryStreamResponse(responseId, options) {
|
|
298
|
-
let seq = 0;
|
|
299
|
-
let closed = false;
|
|
300
|
-
let keepAliveTimer;
|
|
301
|
-
const clearKeepAlive = () => {
|
|
302
|
-
if (keepAliveTimer !== void 0) {
|
|
303
|
-
clearInterval(keepAliveTimer);
|
|
304
|
-
keepAliveTimer = void 0;
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
function ev(event, payload) {
|
|
308
|
-
payload["sequence_number"] = seq++;
|
|
309
|
-
return encoder.encode(sseChunk(event, JSON.stringify(payload)));
|
|
310
|
-
}
|
|
311
|
-
function emitDeltas(controller, fullText) {
|
|
312
|
-
const tokens = fullText.split(" ");
|
|
313
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
314
|
-
const piece = i === tokens.length - 1 ? tokens[i] : tokens[i] + " ";
|
|
315
|
-
controller.enqueue(
|
|
316
|
-
ev("response.output_text.delta", {
|
|
317
|
-
type: "response.output_text.delta",
|
|
318
|
-
delta: piece
|
|
319
|
-
})
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
function emitCompletion(controller, fullText) {
|
|
324
|
-
controller.enqueue(
|
|
325
|
-
ev("response.output_text.done", {
|
|
326
|
-
type: "response.output_text.done",
|
|
327
|
-
text: fullText
|
|
328
|
-
})
|
|
329
|
-
);
|
|
330
|
-
controller.enqueue(
|
|
331
|
-
ev("response.completed", {
|
|
332
|
-
type: "response.completed",
|
|
333
|
-
response: {
|
|
334
|
-
object: "response",
|
|
335
|
-
id: responseId,
|
|
336
|
-
status: "completed",
|
|
337
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
338
|
-
output: []
|
|
339
|
-
}
|
|
340
|
-
})
|
|
341
|
-
);
|
|
342
|
-
closed = true;
|
|
343
|
-
controller.close();
|
|
344
|
-
}
|
|
345
|
-
function emitError(controller, err) {
|
|
346
|
-
clearKeepAlive();
|
|
347
|
-
options.onError?.(err);
|
|
348
|
-
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
349
|
-
const message = options.debugErrors === true ? rawMessage : REDACTED_ERROR_MESSAGE;
|
|
350
|
-
controller.enqueue(
|
|
351
|
-
encoder.encode(
|
|
352
|
-
sseChunk(
|
|
353
|
-
"error",
|
|
354
|
-
JSON.stringify({
|
|
355
|
-
type: "error",
|
|
356
|
-
sequence_number: seq++,
|
|
357
|
-
code: "SERVER_ERROR",
|
|
358
|
-
message,
|
|
359
|
-
param: ""
|
|
360
|
-
})
|
|
361
|
-
)
|
|
362
|
-
)
|
|
363
|
-
);
|
|
364
|
-
closed = true;
|
|
365
|
-
controller.close();
|
|
366
|
-
}
|
|
367
|
-
const body = new ReadableStream({
|
|
368
|
-
start(controller) {
|
|
369
|
-
keepAliveTimer = setInterval(() => {
|
|
370
|
-
if (!closed) {
|
|
371
|
-
controller.enqueue(encoder.encode(": keep-alive\n\n"));
|
|
372
|
-
}
|
|
373
|
-
}, 15e3);
|
|
374
|
-
if (options.chunks !== void 0) {
|
|
375
|
-
(async () => {
|
|
376
|
-
let fullText = "";
|
|
377
|
-
for await (const chunk of options.chunks) {
|
|
378
|
-
if (closed) break;
|
|
379
|
-
fullText += chunk;
|
|
380
|
-
controller.enqueue(
|
|
381
|
-
ev("response.output_text.delta", {
|
|
382
|
-
type: "response.output_text.delta",
|
|
383
|
-
delta: chunk
|
|
384
|
-
})
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
if (closed) return;
|
|
388
|
-
clearKeepAlive();
|
|
389
|
-
emitCompletion(controller, fullText);
|
|
390
|
-
})().catch((err) => emitError(controller, err));
|
|
391
|
-
} else if (options.resultPromise !== void 0) {
|
|
392
|
-
options.resultPromise.then((resultText) => {
|
|
393
|
-
if (closed) return;
|
|
394
|
-
clearKeepAlive();
|
|
395
|
-
emitDeltas(controller, resultText);
|
|
396
|
-
emitCompletion(controller, resultText);
|
|
397
|
-
}).catch((err) => emitError(controller, err));
|
|
398
|
-
} else {
|
|
399
|
-
clearKeepAlive();
|
|
400
|
-
emitCompletion(controller, "");
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
|
-
cancel() {
|
|
404
|
-
closed = true;
|
|
405
|
-
clearKeepAlive();
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
return new Response(body, {
|
|
409
|
-
status: 200,
|
|
410
|
-
headers: sseHeaders(options)
|
|
411
|
-
});
|
|
412
|
-
}
|
|
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
326
|
// src/conversations.ts
|
|
430
327
|
var PersistenceError = class extends Error {
|
|
431
328
|
statusCode;
|
|
@@ -523,7 +420,7 @@ async function shutdownTelemetry() {
|
|
|
523
420
|
import "hono";
|
|
524
421
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
525
422
|
import { DefaultAzureCredential } from "@azure/identity";
|
|
526
|
-
import { validateParams, routerErrorToStatus } from "@rcrsr/rill-agent";
|
|
423
|
+
import { validateParams, routerErrorToStatus as routerErrorToStatus2 } from "@rcrsr/rill-agent";
|
|
527
424
|
|
|
528
425
|
// ../../shared/hono-kit/src/index.ts
|
|
529
426
|
import { existsSync } from "fs";
|
|
@@ -543,18 +440,33 @@ function createHarnessLifecycle(options) {
|
|
|
543
440
|
if (server !== void 0) {
|
|
544
441
|
throw new Error("Server is already listening");
|
|
545
442
|
}
|
|
546
|
-
return new Promise((resolve) => {
|
|
547
|
-
|
|
548
|
-
|
|
443
|
+
return new Promise((resolve, reject) => {
|
|
444
|
+
const started = serve({ fetch: app.fetch, port }, () => {
|
|
445
|
+
started.off("error", onError);
|
|
446
|
+
options?.serverTweaks?.(started);
|
|
549
447
|
resolve();
|
|
550
448
|
});
|
|
449
|
+
function onError(err) {
|
|
450
|
+
server = void 0;
|
|
451
|
+
reject(err);
|
|
452
|
+
}
|
|
453
|
+
started.once("error", onError);
|
|
454
|
+
server = started;
|
|
551
455
|
});
|
|
552
456
|
}
|
|
553
457
|
async function close() {
|
|
554
|
-
if (server
|
|
555
|
-
|
|
556
|
-
|
|
458
|
+
if (server === void 0) return;
|
|
459
|
+
const current = server;
|
|
460
|
+
server = void 0;
|
|
461
|
+
if ("closeAllConnections" in current) {
|
|
462
|
+
current.closeAllConnections();
|
|
557
463
|
}
|
|
464
|
+
await new Promise((resolve, reject) => {
|
|
465
|
+
current.close((err) => {
|
|
466
|
+
if (err) reject(err);
|
|
467
|
+
else resolve();
|
|
468
|
+
});
|
|
469
|
+
});
|
|
558
470
|
}
|
|
559
471
|
return { app, listen, close };
|
|
560
472
|
}
|
|
@@ -587,6 +499,159 @@ async function runRillServe(ctx, start) {
|
|
|
587
499
|
});
|
|
588
500
|
}
|
|
589
501
|
|
|
502
|
+
// src/stream.ts
|
|
503
|
+
import { routerErrorToStatus } from "@rcrsr/rill-agent";
|
|
504
|
+
var REDACTED_ERROR_MESSAGE = "Internal server error";
|
|
505
|
+
var encoder = new TextEncoder();
|
|
506
|
+
function sseChunk(event, data) {
|
|
507
|
+
return `event: ${event}
|
|
508
|
+
data: ${data}
|
|
509
|
+
|
|
510
|
+
`;
|
|
511
|
+
}
|
|
512
|
+
function sseHeaders(options) {
|
|
513
|
+
const headers = {
|
|
514
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
515
|
+
"cache-control": "no-store",
|
|
516
|
+
"x-accel-buffering": "no",
|
|
517
|
+
"x-aml-foundry-agents-metadata": options?.metadataHeader ?? JSON.stringify({
|
|
518
|
+
package: { name: "azure-ai-agentserver-core", version: "1.0.0b17" },
|
|
519
|
+
runtime: {
|
|
520
|
+
python_version: "3.11.0",
|
|
521
|
+
platform: "Linux",
|
|
522
|
+
host_name: "",
|
|
523
|
+
replica_name: ""
|
|
524
|
+
}
|
|
525
|
+
})
|
|
526
|
+
};
|
|
527
|
+
if (options?.sessionId !== void 0) {
|
|
528
|
+
headers["x-agent-session-id"] = options.sessionId;
|
|
529
|
+
}
|
|
530
|
+
if (options?.invocationId !== void 0) {
|
|
531
|
+
headers["x-agent-invocation-id"] = options.invocationId;
|
|
532
|
+
}
|
|
533
|
+
return headers;
|
|
534
|
+
}
|
|
535
|
+
function createFoundryStreamResponse(responseId, options) {
|
|
536
|
+
let seq = 0;
|
|
537
|
+
let closed = false;
|
|
538
|
+
let keepAliveTimer;
|
|
539
|
+
const clearKeepAlive = () => {
|
|
540
|
+
if (keepAliveTimer !== void 0) {
|
|
541
|
+
clearInterval(keepAliveTimer);
|
|
542
|
+
keepAliveTimer = void 0;
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
function ev(event, payload) {
|
|
546
|
+
payload["sequence_number"] = seq++;
|
|
547
|
+
return encoder.encode(sseChunk(event, JSON.stringify(payload)));
|
|
548
|
+
}
|
|
549
|
+
function emitDeltas(controller, fullText) {
|
|
550
|
+
const tokens = fullText.split(" ");
|
|
551
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
552
|
+
const piece = i === tokens.length - 1 ? tokens[i] : tokens[i] + " ";
|
|
553
|
+
controller.enqueue(
|
|
554
|
+
ev("response.output_text.delta", {
|
|
555
|
+
type: "response.output_text.delta",
|
|
556
|
+
delta: piece
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function emitCompletion(controller, fullText) {
|
|
562
|
+
if (closed) return;
|
|
563
|
+
controller.enqueue(
|
|
564
|
+
ev("response.output_text.done", {
|
|
565
|
+
type: "response.output_text.done",
|
|
566
|
+
text: fullText
|
|
567
|
+
})
|
|
568
|
+
);
|
|
569
|
+
controller.enqueue(
|
|
570
|
+
ev("response.completed", {
|
|
571
|
+
type: "response.completed",
|
|
572
|
+
response: {
|
|
573
|
+
object: "response",
|
|
574
|
+
id: responseId,
|
|
575
|
+
status: "completed",
|
|
576
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
577
|
+
output: []
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
closed = true;
|
|
582
|
+
controller.close();
|
|
583
|
+
}
|
|
584
|
+
function emitError(controller, err) {
|
|
585
|
+
clearKeepAlive();
|
|
586
|
+
options.onError?.(err);
|
|
587
|
+
if (closed) return;
|
|
588
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
589
|
+
const message = options.debugErrors === true ? rawMessage : REDACTED_ERROR_MESSAGE;
|
|
590
|
+
const code = routerErrorToStatus(err) === 404 ? "NOT_FOUND" : "SERVER_ERROR";
|
|
591
|
+
controller.enqueue(
|
|
592
|
+
encoder.encode(
|
|
593
|
+
sseChunk(
|
|
594
|
+
"error",
|
|
595
|
+
JSON.stringify({
|
|
596
|
+
type: "error",
|
|
597
|
+
sequence_number: seq++,
|
|
598
|
+
code,
|
|
599
|
+
message,
|
|
600
|
+
param: ""
|
|
601
|
+
})
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
);
|
|
605
|
+
closed = true;
|
|
606
|
+
controller.close();
|
|
607
|
+
}
|
|
608
|
+
const body = new ReadableStream({
|
|
609
|
+
start(controller) {
|
|
610
|
+
keepAliveTimer = setInterval(() => {
|
|
611
|
+
if (!closed) {
|
|
612
|
+
controller.enqueue(encoder.encode(": keep-alive\n\n"));
|
|
613
|
+
}
|
|
614
|
+
}, 15e3);
|
|
615
|
+
if (options.chunks !== void 0) {
|
|
616
|
+
(async () => {
|
|
617
|
+
let fullText = "";
|
|
618
|
+
for await (const chunk of options.chunks) {
|
|
619
|
+
if (closed) break;
|
|
620
|
+
fullText += chunk;
|
|
621
|
+
controller.enqueue(
|
|
622
|
+
ev("response.output_text.delta", {
|
|
623
|
+
type: "response.output_text.delta",
|
|
624
|
+
delta: chunk
|
|
625
|
+
})
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
if (closed) return;
|
|
629
|
+
clearKeepAlive();
|
|
630
|
+
emitCompletion(controller, fullText);
|
|
631
|
+
})().catch((err) => emitError(controller, err));
|
|
632
|
+
} else if (options.resultPromise !== void 0) {
|
|
633
|
+
options.resultPromise.then((resultText) => {
|
|
634
|
+
if (closed) return;
|
|
635
|
+
clearKeepAlive();
|
|
636
|
+
emitDeltas(controller, resultText);
|
|
637
|
+
emitCompletion(controller, resultText);
|
|
638
|
+
}).catch((err) => emitError(controller, err));
|
|
639
|
+
} else {
|
|
640
|
+
clearKeepAlive();
|
|
641
|
+
emitCompletion(controller, "");
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
cancel() {
|
|
645
|
+
closed = true;
|
|
646
|
+
clearKeepAlive();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
return new Response(body, {
|
|
650
|
+
status: 200,
|
|
651
|
+
headers: sseHeaders(options)
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
590
655
|
// src/harness.ts
|
|
591
656
|
var DEFAULT_PORT = 8088;
|
|
592
657
|
function resolvePort(options) {
|
|
@@ -613,7 +678,26 @@ function resolveConversationId(conversation) {
|
|
|
613
678
|
if (typeof conversation === "string") {
|
|
614
679
|
return conversation;
|
|
615
680
|
}
|
|
616
|
-
|
|
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;
|
|
617
701
|
}
|
|
618
702
|
function createFoundryHarness(router, options) {
|
|
619
703
|
const port = resolvePort(options);
|
|
@@ -625,7 +709,9 @@ function createFoundryHarness(router, options) {
|
|
|
625
709
|
agentName: agentName ?? router.defaultAgent(),
|
|
626
710
|
agentVersion
|
|
627
711
|
});
|
|
628
|
-
const sessions = createSessionManager(
|
|
712
|
+
const sessions = createSessionManager({
|
|
713
|
+
maxConcurrentSessions: options?.maxConcurrentSessions
|
|
714
|
+
});
|
|
629
715
|
const projectEndpoint = process.env["FOUNDRY_PROJECT_ENDPOINT"];
|
|
630
716
|
const azureCredential = projectEndpoint !== void 0 ? new DefaultAzureCredential() : void 0;
|
|
631
717
|
const conversationsClient = projectEndpoint !== void 0 && azureCredential !== void 0 ? createConversationsClient(projectEndpoint, azureCredential) : void 0;
|
|
@@ -714,7 +800,19 @@ function createFoundryHarness(router, options) {
|
|
|
714
800
|
500
|
|
715
801
|
);
|
|
716
802
|
}
|
|
717
|
-
|
|
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
|
+
}
|
|
718
816
|
const agentName_ = extracted.targetAgent ?? router.defaultAgent();
|
|
719
817
|
const validationError = validateParams(
|
|
720
818
|
extracted.params,
|
|
@@ -748,10 +846,7 @@ function createFoundryHarness(router, options) {
|
|
|
748
846
|
500
|
|
749
847
|
);
|
|
750
848
|
}
|
|
751
|
-
const idGen = createIdGenerator(
|
|
752
|
-
body.metadata?.["response_id"],
|
|
753
|
-
conversationId
|
|
754
|
-
);
|
|
849
|
+
const idGen = createIdGenerator(metadataResponseId, conversationId);
|
|
755
850
|
const responseId = idGen.responseId;
|
|
756
851
|
const invocationId = c.req.header("x-agent-invocation-id") ?? generateId("inv_");
|
|
757
852
|
const agentSessionId = c.req.query("session") ?? c.req.header("x-agent-session-id") ?? sessionId;
|
|
@@ -843,8 +938,7 @@ function createFoundryHarness(router, options) {
|
|
|
843
938
|
}
|
|
844
939
|
};
|
|
845
940
|
const onChunk = async (chunk) => {
|
|
846
|
-
|
|
847
|
-
pushChunk(text);
|
|
941
|
+
pushChunk(coerceResult(chunk));
|
|
848
942
|
};
|
|
849
943
|
const streamContext = { ...runContext, onChunk };
|
|
850
944
|
router.run(agentName_, runRequest, streamContext).then((result) => {
|
|
@@ -852,10 +946,8 @@ function createFoundryHarness(router, options) {
|
|
|
852
946
|
span.end();
|
|
853
947
|
sessions.release(sessionId);
|
|
854
948
|
if (!result.streamed) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
if (text !== "") pushChunk(text);
|
|
858
|
-
}
|
|
949
|
+
const text = coerceResult(result.result);
|
|
950
|
+
if (text !== "") pushChunk(text);
|
|
859
951
|
}
|
|
860
952
|
endChunks();
|
|
861
953
|
}).catch((err) => {
|
|
@@ -869,7 +961,6 @@ function createFoundryHarness(router, options) {
|
|
|
869
961
|
});
|
|
870
962
|
return createFoundryStreamResponse(responseId, {
|
|
871
963
|
chunks,
|
|
872
|
-
idGenerator: idGen,
|
|
873
964
|
onError: (_err) => {
|
|
874
965
|
errorCount++;
|
|
875
966
|
},
|
|
@@ -900,7 +991,7 @@ function createFoundryHarness(router, options) {
|
|
|
900
991
|
span.end();
|
|
901
992
|
errorCount++;
|
|
902
993
|
const msg = err instanceof Error ? err.message : String(err);
|
|
903
|
-
const status =
|
|
994
|
+
const status = routerErrorToStatus2(err);
|
|
904
995
|
if (status === 404) {
|
|
905
996
|
return c.json(buildErrorResponse("NOT_FOUND", msg, debugErrors), 404);
|
|
906
997
|
}
|
|
@@ -910,7 +1001,7 @@ function createFoundryHarness(router, options) {
|
|
|
910
1001
|
);
|
|
911
1002
|
}
|
|
912
1003
|
span.end();
|
|
913
|
-
const response = buildSyncResponse(result, responseId);
|
|
1004
|
+
const response = buildSyncResponse(result, responseId, debugErrors);
|
|
914
1005
|
const shouldStore = body.store === true;
|
|
915
1006
|
if (shouldStore && conversationId !== void 0 && conversationsClient !== void 0) {
|
|
916
1007
|
try {
|
|
@@ -956,11 +1047,11 @@ function createFoundryHarness(router, options) {
|
|
|
956
1047
|
);
|
|
957
1048
|
}
|
|
958
1049
|
} catch (err) {
|
|
959
|
-
if (
|
|
960
|
-
|
|
961
|
-
throw new CredentialError(message);
|
|
1050
|
+
if (err instanceof CredentialError) {
|
|
1051
|
+
throw err;
|
|
962
1052
|
}
|
|
963
|
-
|
|
1053
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1054
|
+
throw new CredentialError(message);
|
|
964
1055
|
}
|
|
965
1056
|
}
|
|
966
1057
|
await lifecycle.listen(port);
|
|
@@ -1014,6 +1105,5 @@ export {
|
|
|
1014
1105
|
generateToolDefinitions,
|
|
1015
1106
|
getTracer,
|
|
1016
1107
|
initTelemetry,
|
|
1017
|
-
shutdownTelemetry
|
|
1018
|
-
streamFoundryResponse
|
|
1108
|
+
shutdownTelemetry
|
|
1019
1109
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rcrsr/rill-agent-foundry",
|
|
3
|
-
"version": "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",
|
|
@@ -22,19 +22,19 @@
|
|
|
22
22
|
"@opentelemetry/api": "^1.9.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@azure/identity": "^4.
|
|
26
|
-
"@hono/node-server": "^2.
|
|
27
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
28
|
-
"@opentelemetry/resources": "^2.
|
|
29
|
-
"@opentelemetry/sdk-node": "^0.
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@opentelemetry/api": "^1.9.0",
|
|
35
|
+
"@rcrsr/rill-agent-hono-kit": "^0.21.0",
|
|
35
36
|
"dts-bundle-generator": "^9.5.1",
|
|
36
|
-
"tsup": "^8.5.
|
|
37
|
-
"@rcrsr/rill-agent-hono-kit": "^0.20.0"
|
|
37
|
+
"tsup": "^8.5.1"
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"dist"
|