@tangle-network/agent-runtime 0.17.1 → 0.18.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 CHANGED
@@ -187,12 +187,9 @@ declare function handleChatTurn(input: RunChatTurnInput): ChatTurnResult;
187
187
  * opaque id. Substrate executionIds are not a secrecy boundary.
188
188
  *
189
189
  * Wire integration:
190
- * - `@tangle-network/sandbox@0.1.x` PromptOptions does not yet expose
191
- * `executionId`. The SDK auto-reconnects in-call by extracting it
192
- * from the response `execution.started` event; products do nothing.
193
- * - For cross-process reconnect today, bypass the SDK and POST to the
194
- * orchestrator's `/agents/run/stream` directly with this id in the
195
- * `X-Execution-ID` header (see tax-agent's `sessions.ts`).
190
+ * - Sandbox PromptOptions accepts `executionId` and `lastEventId`.
191
+ * Products pass this id to make cross-process reconnect land on the
192
+ * same substrate execution instead of spawning a duplicate run.
196
193
  */
197
194
  declare function deriveExecutionId(input: {
198
195
  projectId: string;
package/dist/index.js CHANGED
@@ -177,6 +177,7 @@ function createOpenAICompatibleBackend(options) {
177
177
  const requestBody = JSON.stringify({
178
178
  model: options.model,
179
179
  stream: true,
180
+ stream_options: { include_usage: true },
180
181
  messages: input.messages ?? [
181
182
  { role: "user", content: input.message ?? context.task.intent }
182
183
  ]
@@ -231,7 +232,7 @@ function createOpenAICompatibleBackend(options) {
231
232
  status: lastStatus || 0
232
233
  });
233
234
  }
234
- yield* streamResponseEvents(response, context);
235
+ yield* streamResponseEvents(response, context, options.model);
235
236
  }
236
237
  };
237
238
  }
@@ -337,12 +338,14 @@ function mapCommonBackendEvent(event, context) {
337
338
  }
338
339
  return void 0;
339
340
  }
340
- async function* streamResponseEvents(response, context) {
341
+ async function* streamResponseEvents(response, context, requestedModel) {
341
342
  const body = response.body;
342
343
  if (!body) return;
343
344
  const reader = body.getReader();
344
345
  const decoder = new TextDecoder();
345
346
  let buffer = "";
347
+ const usage = { saw: false };
348
+ const startedAt = Date.now();
346
349
  for (; ; ) {
347
350
  const { done, value } = await reader.read();
348
351
  if (done) break;
@@ -352,16 +355,32 @@ async function* streamResponseEvents(response, context) {
352
355
  buffer += decoder.decode().replace(/\r\n/g, "\n");
353
356
  for (const event of drainStreamBuffer(true)) yield event;
354
357
  if (buffer.trim()) {
355
- const event = parseStreamChunk(buffer, context);
358
+ const event = parseStreamChunk(buffer, context, usage);
356
359
  if (event) yield event;
357
360
  }
361
+ if (usage.saw) {
362
+ yield {
363
+ type: "llm_call",
364
+ task: context.task,
365
+ session: context.session,
366
+ model: usage.model ?? requestedModel,
367
+ tokensIn: usage.tokensIn,
368
+ tokensOut: usage.tokensOut,
369
+ // `costUsd` is intentionally absent — pricing tables live in consumers
370
+ // (agent-eval's `estimateCost`, MetricsCollector). Emitting a wrong
371
+ // number here is worse than emitting none.
372
+ latencyMs: Date.now() - startedAt,
373
+ finishReason: usage.finishReason,
374
+ timestamp: nowIso()
375
+ };
376
+ }
358
377
  function* drainStreamBuffer(flush) {
359
378
  for (; ; ) {
360
379
  const sseBoundary = buffer.indexOf("\n\n");
361
380
  if (sseBoundary >= 0) {
362
381
  const chunk = buffer.slice(0, sseBoundary);
363
382
  buffer = buffer.slice(sseBoundary + 2);
364
- const event = parseStreamChunk(chunk, context);
383
+ const event = parseStreamChunk(chunk, context, usage);
365
384
  if (event) yield event;
366
385
  continue;
367
386
  }
@@ -369,14 +388,14 @@ async function* streamResponseEvents(response, context) {
369
388
  if (newline >= 0 && !buffer.slice(0, newline).startsWith("data:")) {
370
389
  const line = buffer.slice(0, newline);
371
390
  buffer = buffer.slice(newline + 1);
372
- const event = parseStreamChunk(line, context);
391
+ const event = parseStreamChunk(line, context, usage);
373
392
  if (event) yield event;
374
393
  continue;
375
394
  }
376
395
  if (flush && buffer.trim() && !buffer.trimStart().startsWith("data:")) {
377
396
  const line = buffer;
378
397
  buffer = "";
379
- const event = parseStreamChunk(line, context);
398
+ const event = parseStreamChunk(line, context, usage);
380
399
  if (event) yield event;
381
400
  continue;
382
401
  }
@@ -384,13 +403,14 @@ async function* streamResponseEvents(response, context) {
384
403
  }
385
404
  }
386
405
  }
387
- function parseStreamChunk(chunk, context) {
406
+ function parseStreamChunk(chunk, context, usage) {
388
407
  const lines = chunk.split(/\r?\n/);
389
408
  const dataLines = lines.filter((line) => line.startsWith("data:"));
390
409
  const data = dataLines.length > 0 ? dataLines.map((line) => line.slice(5).trimStart()).join("\n") : chunk.trim();
391
410
  if (!data || data === "[DONE]") return void 0;
392
411
  try {
393
412
  const parsed = JSON.parse(data);
413
+ captureStreamUsage(parsed, usage);
394
414
  const choices = parsed.choices;
395
415
  const choice = Array.isArray(choices) ? choices[0] : void 0;
396
416
  const delta = choice?.delta;
@@ -405,6 +425,19 @@ function parseStreamChunk(chunk, context) {
405
425
  timestamp: nowIso()
406
426
  };
407
427
  }
428
+ if (stringValue(parsed.type) === "content_block_delta") {
429
+ const d = parsed.delta;
430
+ const text2 = stringValue(d?.text);
431
+ if (text2) {
432
+ return {
433
+ type: "text_delta",
434
+ task: context.task,
435
+ session: context.session,
436
+ text: text2,
437
+ timestamp: nowIso()
438
+ };
439
+ }
440
+ }
408
441
  return mapCommonBackendEvent(parsed, context);
409
442
  } catch {
410
443
  return {
@@ -416,6 +449,63 @@ function parseStreamChunk(chunk, context) {
416
449
  };
417
450
  }
418
451
  }
452
+ function captureStreamUsage(parsed, usage) {
453
+ const model = stringValue(parsed.model);
454
+ if (model && !usage.model) usage.model = model;
455
+ const openAiUsage = parsed.usage;
456
+ if (openAiUsage && typeof openAiUsage === "object") {
457
+ const promptTokens = numberValue(openAiUsage.prompt_tokens);
458
+ const completionTokens = numberValue(openAiUsage.completion_tokens);
459
+ const inputTokens = numberValue(openAiUsage.input_tokens);
460
+ const outputTokens = numberValue(openAiUsage.output_tokens);
461
+ if (promptTokens !== void 0) {
462
+ usage.tokensIn = promptTokens;
463
+ usage.saw = true;
464
+ } else if (inputTokens !== void 0) {
465
+ usage.tokensIn = (usage.tokensIn ?? 0) + inputTokens;
466
+ usage.saw = true;
467
+ }
468
+ if (completionTokens !== void 0) {
469
+ usage.tokensOut = completionTokens;
470
+ usage.saw = true;
471
+ } else if (outputTokens !== void 0) {
472
+ usage.tokensOut = (usage.tokensOut ?? 0) + outputTokens;
473
+ usage.saw = true;
474
+ }
475
+ }
476
+ const type = stringValue(parsed.type);
477
+ if (type === "message_start") {
478
+ const message = parsed.message;
479
+ const messageModel = stringValue(message?.model);
480
+ if (messageModel && !usage.model) usage.model = messageModel;
481
+ const messageUsage = message?.usage;
482
+ const inputTokens = numberValue(messageUsage?.input_tokens);
483
+ if (inputTokens !== void 0) {
484
+ usage.tokensIn = inputTokens;
485
+ usage.saw = true;
486
+ }
487
+ const outputTokens = numberValue(messageUsage?.output_tokens);
488
+ if (outputTokens !== void 0) {
489
+ usage.tokensOut = (usage.tokensOut ?? 0) + outputTokens;
490
+ usage.saw = true;
491
+ }
492
+ }
493
+ if (type === "message_delta") {
494
+ const delta = parsed.delta;
495
+ const stopReason = stringValue(delta?.stop_reason);
496
+ if (stopReason) usage.finishReason = stopReason;
497
+ }
498
+ const choices = parsed.choices;
499
+ if (Array.isArray(choices)) {
500
+ const finishReason = stringValue(
501
+ choices[0]?.finish_reason
502
+ );
503
+ if (finishReason) usage.finishReason = finishReason;
504
+ }
505
+ }
506
+ function numberValue(value) {
507
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
508
+ }
419
509
  function stringValue(value) {
420
510
  return typeof value === "string" && value.length > 0 ? value : void 0;
421
511
  }
@@ -462,13 +552,7 @@ function handleChatTurn(input) {
462
552
  }
463
553
  const rawFinal = producer.finalText();
464
554
  const finalText = hooks.transformFinalText ? await hooks.transformFinalText(rawFinal) : rawFinal;
465
- try {
466
- await hooks.persistAssistantMessage({ identity, finalText });
467
- } catch (err) {
468
- log("[chat-engine] persistAssistantMessage threw", {
469
- error: err instanceof Error ? err.message : String(err)
470
- });
471
- }
555
+ await hooks.persistAssistantMessage({ identity, finalText });
472
556
  if (hooks.onTurnComplete) {
473
557
  try {
474
558
  await hooks.onTurnComplete({ identity, finalText });