@jaypie/llm 1.2.39 → 1.2.41
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/cjs/index.cjs +476 -110
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/observability/llmobs.d.ts +72 -0
- package/dist/cjs/operate/OperateLoop.d.ts +6 -0
- package/dist/cjs/util/fillFormatArrays.d.ts +17 -0
- package/dist/cjs/util/index.d.ts +1 -0
- package/dist/esm/index.js +475 -110
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/observability/llmobs.d.ts +72 -0
- package/dist/esm/operate/OperateLoop.d.ts +6 -0
- package/dist/esm/util/fillFormatArrays.d.ts +17 -0
- package/dist/esm/util/index.d.ts +1 -0
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -7,12 +7,15 @@ var kit = require('@jaypie/kit');
|
|
|
7
7
|
var RandomLib = require('random');
|
|
8
8
|
var openai = require('openai');
|
|
9
9
|
var zod = require('openai/helpers/zod');
|
|
10
|
+
var module$1 = require('module');
|
|
11
|
+
var url = require('url');
|
|
10
12
|
var pdfLib = require('pdf-lib');
|
|
11
13
|
var promises = require('fs/promises');
|
|
12
14
|
var path = require('path');
|
|
13
15
|
var aws = require('@jaypie/aws');
|
|
14
16
|
var openmeteo = require('openmeteo');
|
|
15
17
|
|
|
18
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
16
19
|
const FIRST_CLASS_PROVIDER = {
|
|
17
20
|
// https://docs.anthropic.com/en/docs/about-claude/models/overview
|
|
18
21
|
ANTHROPIC: {
|
|
@@ -572,6 +575,154 @@ function extractReasoning(history) {
|
|
|
572
575
|
return reasoningTexts;
|
|
573
576
|
}
|
|
574
577
|
|
|
578
|
+
function naturalZodSchema(definition) {
|
|
579
|
+
if (Array.isArray(definition)) {
|
|
580
|
+
if (definition.length === 0) {
|
|
581
|
+
// Handle empty array - accept any[]
|
|
582
|
+
return v4.z.array(v4.z.any());
|
|
583
|
+
}
|
|
584
|
+
else if (definition.length === 1) {
|
|
585
|
+
// Handle array types
|
|
586
|
+
const itemType = definition[0];
|
|
587
|
+
switch (itemType) {
|
|
588
|
+
case String:
|
|
589
|
+
return v4.z.array(v4.z.string());
|
|
590
|
+
case Number:
|
|
591
|
+
return v4.z.array(v4.z.number());
|
|
592
|
+
case Boolean:
|
|
593
|
+
return v4.z.array(v4.z.boolean());
|
|
594
|
+
default:
|
|
595
|
+
if (typeof itemType === "object") {
|
|
596
|
+
// Handle array of objects
|
|
597
|
+
return v4.z.array(naturalZodSchema(itemType));
|
|
598
|
+
}
|
|
599
|
+
// Handle enum arrays
|
|
600
|
+
return v4.z.enum(definition);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
// Handle enum arrays
|
|
605
|
+
return v4.z.enum(definition);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else if (definition && typeof definition === "object") {
|
|
609
|
+
if (Object.keys(definition).length === 0) {
|
|
610
|
+
// Handle empty object - accept any key-value pairs
|
|
611
|
+
return v4.z.record(v4.z.string(), v4.z.any());
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
// Handle object with properties
|
|
615
|
+
const schemaShape = {};
|
|
616
|
+
for (const [key, value] of Object.entries(definition)) {
|
|
617
|
+
schemaShape[key] = naturalZodSchema(value);
|
|
618
|
+
}
|
|
619
|
+
return v4.z.object(schemaShape);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
switch (definition) {
|
|
624
|
+
case String:
|
|
625
|
+
return v4.z.string();
|
|
626
|
+
case Number:
|
|
627
|
+
return v4.z.number();
|
|
628
|
+
case Boolean:
|
|
629
|
+
return v4.z.boolean();
|
|
630
|
+
case Object:
|
|
631
|
+
return v4.z.record(v4.z.string(), v4.z.any());
|
|
632
|
+
case Array:
|
|
633
|
+
return v4.z.array(v4.z.any());
|
|
634
|
+
default:
|
|
635
|
+
throw new Error(`Unsupported type: ${definition}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
//
|
|
641
|
+
//
|
|
642
|
+
// Helpers
|
|
643
|
+
//
|
|
644
|
+
function isPlainObject(value) {
|
|
645
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Convert a `format` declaration (Zod schema, NaturalSchema, or a JSON Schema
|
|
649
|
+
* JsonObject) into a plain JSON Schema we can walk. Mirrors the conversion the
|
|
650
|
+
* provider adapters perform in `formatOutputSchema`, but without any
|
|
651
|
+
* provider-specific sanitization.
|
|
652
|
+
*/
|
|
653
|
+
function formatToJsonSchema(format) {
|
|
654
|
+
if (format instanceof v4.z.ZodType) {
|
|
655
|
+
return v4.z.toJSONSchema(format);
|
|
656
|
+
}
|
|
657
|
+
if (isPlainObject(format) && format.type === "json_schema") {
|
|
658
|
+
const clone = structuredClone(format);
|
|
659
|
+
clone.type = "object";
|
|
660
|
+
return clone;
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
return v4.z.toJSONSchema(naturalZodSchema(format));
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Walk a JSON Schema alongside a parsed value, filling any declared array field
|
|
671
|
+
* that is absent (`undefined`/`null`) with `[]`. Recurses into object
|
|
672
|
+
* properties and array items so nested declared arrays are also backfilled.
|
|
673
|
+
*/
|
|
674
|
+
function fillFromSchema(schema, value) {
|
|
675
|
+
if (!isPlainObject(schema)) {
|
|
676
|
+
return value;
|
|
677
|
+
}
|
|
678
|
+
const type = schema.type;
|
|
679
|
+
const isArray = type === "array" || (type === undefined && "items" in schema);
|
|
680
|
+
if (isArray) {
|
|
681
|
+
if (value === undefined || value === null) {
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
const items = schema.items;
|
|
685
|
+
if (Array.isArray(value) && isPlainObject(items)) {
|
|
686
|
+
return value.map((entry) => fillFromSchema(items, entry));
|
|
687
|
+
}
|
|
688
|
+
return value;
|
|
689
|
+
}
|
|
690
|
+
const isObject = type === "object" || (type === undefined && "properties" in schema);
|
|
691
|
+
if (isObject) {
|
|
692
|
+
if (!isPlainObject(value)) {
|
|
693
|
+
return value;
|
|
694
|
+
}
|
|
695
|
+
const properties = schema.properties;
|
|
696
|
+
if (isPlainObject(properties)) {
|
|
697
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
698
|
+
value[key] = fillFromSchema(propSchema, value[key]);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
return value;
|
|
704
|
+
}
|
|
705
|
+
//
|
|
706
|
+
//
|
|
707
|
+
// Main
|
|
708
|
+
//
|
|
709
|
+
/**
|
|
710
|
+
* Ensure every array field declared in `format` is present in `content` as an
|
|
711
|
+
* array. A declared `format` is a schema contract: an empty list should surface
|
|
712
|
+
* as `[]`, not be dropped from the response. Some providers/models omit empty
|
|
713
|
+
* array fields entirely, leaving consumers to read `.length` on `undefined`.
|
|
714
|
+
*
|
|
715
|
+
* Only mutates a (cloned) structured object; strings and non-objects pass
|
|
716
|
+
* through untouched.
|
|
717
|
+
*/
|
|
718
|
+
function fillFormatArrays({ content, format, }) {
|
|
719
|
+
const schema = formatToJsonSchema(format);
|
|
720
|
+
if (!schema) {
|
|
721
|
+
return content;
|
|
722
|
+
}
|
|
723
|
+
return fillFromSchema(schema, structuredClone(content));
|
|
724
|
+
}
|
|
725
|
+
|
|
575
726
|
/**
|
|
576
727
|
* Converts a string to a standardized LlmInputMessage
|
|
577
728
|
* @param input - String to format
|
|
@@ -699,68 +850,6 @@ function maxTurnsFromOptions(options) {
|
|
|
699
850
|
return 1;
|
|
700
851
|
}
|
|
701
852
|
|
|
702
|
-
function naturalZodSchema(definition) {
|
|
703
|
-
if (Array.isArray(definition)) {
|
|
704
|
-
if (definition.length === 0) {
|
|
705
|
-
// Handle empty array - accept any[]
|
|
706
|
-
return v4.z.array(v4.z.any());
|
|
707
|
-
}
|
|
708
|
-
else if (definition.length === 1) {
|
|
709
|
-
// Handle array types
|
|
710
|
-
const itemType = definition[0];
|
|
711
|
-
switch (itemType) {
|
|
712
|
-
case String:
|
|
713
|
-
return v4.z.array(v4.z.string());
|
|
714
|
-
case Number:
|
|
715
|
-
return v4.z.array(v4.z.number());
|
|
716
|
-
case Boolean:
|
|
717
|
-
return v4.z.array(v4.z.boolean());
|
|
718
|
-
default:
|
|
719
|
-
if (typeof itemType === "object") {
|
|
720
|
-
// Handle array of objects
|
|
721
|
-
return v4.z.array(naturalZodSchema(itemType));
|
|
722
|
-
}
|
|
723
|
-
// Handle enum arrays
|
|
724
|
-
return v4.z.enum(definition);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
else {
|
|
728
|
-
// Handle enum arrays
|
|
729
|
-
return v4.z.enum(definition);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
else if (definition && typeof definition === "object") {
|
|
733
|
-
if (Object.keys(definition).length === 0) {
|
|
734
|
-
// Handle empty object - accept any key-value pairs
|
|
735
|
-
return v4.z.record(v4.z.string(), v4.z.any());
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
738
|
-
// Handle object with properties
|
|
739
|
-
const schemaShape = {};
|
|
740
|
-
for (const [key, value] of Object.entries(definition)) {
|
|
741
|
-
schemaShape[key] = naturalZodSchema(value);
|
|
742
|
-
}
|
|
743
|
-
return v4.z.object(schemaShape);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
switch (definition) {
|
|
748
|
-
case String:
|
|
749
|
-
return v4.z.string();
|
|
750
|
-
case Number:
|
|
751
|
-
return v4.z.number();
|
|
752
|
-
case Boolean:
|
|
753
|
-
return v4.z.boolean();
|
|
754
|
-
case Object:
|
|
755
|
-
return v4.z.record(v4.z.string(), v4.z.any());
|
|
756
|
-
case Array:
|
|
757
|
-
return v4.z.array(v4.z.any());
|
|
758
|
-
default:
|
|
759
|
-
throw new Error(`Unsupported type: ${definition}`);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
853
|
//
|
|
765
854
|
// Constants
|
|
766
855
|
//
|
|
@@ -4837,6 +4926,198 @@ class Toolkit {
|
|
|
4837
4926
|
}
|
|
4838
4927
|
}
|
|
4839
4928
|
|
|
4929
|
+
//
|
|
4930
|
+
//
|
|
4931
|
+
// Constants
|
|
4932
|
+
//
|
|
4933
|
+
const ENV = {
|
|
4934
|
+
DD_LLMOBS_ENABLED: "DD_LLMOBS_ENABLED",
|
|
4935
|
+
};
|
|
4936
|
+
const MODULE = {
|
|
4937
|
+
// Computed at runtime so bundlers (esbuild) do not attempt to include
|
|
4938
|
+
// dd-trace, which is provided by the Datadog Lambda layer at runtime.
|
|
4939
|
+
DD_TRACE: ["dd", "trace"].join("-"),
|
|
4940
|
+
};
|
|
4941
|
+
//
|
|
4942
|
+
//
|
|
4943
|
+
// Helpers
|
|
4944
|
+
//
|
|
4945
|
+
// CJS/ESM compatible require - handles bundling to CJS where import.meta.url
|
|
4946
|
+
// becomes undefined (mirrors packages/testkit/src/mock/original.ts).
|
|
4947
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4948
|
+
// @ts-ignore - __filename exists in CJS context when ESM is bundled to CJS
|
|
4949
|
+
const requireModule = typeof __filename !== "undefined"
|
|
4950
|
+
? module$1.createRequire(url.pathToFileURL(__filename).href)
|
|
4951
|
+
: module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
4952
|
+
let resolved = false;
|
|
4953
|
+
let cachedSdk$4 = null;
|
|
4954
|
+
/**
|
|
4955
|
+
* Lazily resolve the dd-trace `llmobs` SDK. Returns null (and never throws)
|
|
4956
|
+
* when dd-trace is absent or the SDK surface is unexpected. Cached after the
|
|
4957
|
+
* first attempt.
|
|
4958
|
+
*/
|
|
4959
|
+
function resolveLlmObs() {
|
|
4960
|
+
if (resolved) {
|
|
4961
|
+
return cachedSdk$4;
|
|
4962
|
+
}
|
|
4963
|
+
resolved = true;
|
|
4964
|
+
try {
|
|
4965
|
+
const ddTrace = requireModule(MODULE.DD_TRACE);
|
|
4966
|
+
const tracer = ddTrace?.default ?? ddTrace;
|
|
4967
|
+
const llmobs = tracer?.llmobs;
|
|
4968
|
+
if (llmobs &&
|
|
4969
|
+
typeof llmobs.trace === "function" &&
|
|
4970
|
+
typeof llmobs.annotate === "function") {
|
|
4971
|
+
cachedSdk$4 = llmobs;
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
catch {
|
|
4975
|
+
cachedSdk$4 = null;
|
|
4976
|
+
}
|
|
4977
|
+
return cachedSdk$4;
|
|
4978
|
+
}
|
|
4979
|
+
//
|
|
4980
|
+
//
|
|
4981
|
+
// Main
|
|
4982
|
+
//
|
|
4983
|
+
/**
|
|
4984
|
+
* True when LLM Observability emission is opted in via `DD_LLMOBS_ENABLED`.
|
|
4985
|
+
* Accepts any truthy value except "false"/"0".
|
|
4986
|
+
*/
|
|
4987
|
+
function isLlmObsEnabled() {
|
|
4988
|
+
const value = process.env[ENV.DD_LLMOBS_ENABLED];
|
|
4989
|
+
if (!value) {
|
|
4990
|
+
return false;
|
|
4991
|
+
}
|
|
4992
|
+
const normalized = value.trim().toLowerCase();
|
|
4993
|
+
return normalized !== "" && normalized !== "false" && normalized !== "0";
|
|
4994
|
+
}
|
|
4995
|
+
/** Resolve the SDK only when enabled and available. */
|
|
4996
|
+
function activeSdk() {
|
|
4997
|
+
if (!isLlmObsEnabled()) {
|
|
4998
|
+
return null;
|
|
4999
|
+
}
|
|
5000
|
+
return resolveLlmObs();
|
|
5001
|
+
}
|
|
5002
|
+
/**
|
|
5003
|
+
* Run `fn` inside an LLM Observability span. When emission is disabled or
|
|
5004
|
+
* dd-trace is absent, `fn` is invoked directly with no overhead. The active
|
|
5005
|
+
* span is established for the duration of `fn`, so nested `withLlmObsSpan`
|
|
5006
|
+
* calls (and `annotateLlmObs`) attach as children/annotations automatically.
|
|
5007
|
+
*/
|
|
5008
|
+
async function withLlmObsSpan(options, fn) {
|
|
5009
|
+
const sdk = activeSdk();
|
|
5010
|
+
if (!sdk) {
|
|
5011
|
+
return fn();
|
|
5012
|
+
}
|
|
5013
|
+
let started = false;
|
|
5014
|
+
try {
|
|
5015
|
+
return (await sdk.trace(options, () => {
|
|
5016
|
+
started = true;
|
|
5017
|
+
return fn();
|
|
5018
|
+
}));
|
|
5019
|
+
}
|
|
5020
|
+
catch (error) {
|
|
5021
|
+
// If `fn` ran, propagate its error (the SDK re-throws after finishing the
|
|
5022
|
+
// span). Only swallow failures from the instrumentation itself, re-running
|
|
5023
|
+
// `fn` so observability never breaks the underlying call.
|
|
5024
|
+
if (started) {
|
|
5025
|
+
throw error;
|
|
5026
|
+
}
|
|
5027
|
+
getLogger$6().warn("[llmobs] span emission failed");
|
|
5028
|
+
getLogger$6().var({ error });
|
|
5029
|
+
return fn();
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
/**
|
|
5033
|
+
* Annotate the currently active LLM Observability span. No-op when disabled.
|
|
5034
|
+
*/
|
|
5035
|
+
function annotateLlmObs(annotation) {
|
|
5036
|
+
const sdk = activeSdk();
|
|
5037
|
+
if (!sdk) {
|
|
5038
|
+
return;
|
|
5039
|
+
}
|
|
5040
|
+
try {
|
|
5041
|
+
sdk.annotate(undefined, annotation);
|
|
5042
|
+
}
|
|
5043
|
+
catch (error) {
|
|
5044
|
+
getLogger$6().warn("[llmobs] annotate failed");
|
|
5045
|
+
getLogger$6().var({ error });
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
/**
|
|
5049
|
+
* Open a manual span that is finished later. Used by streaming, where work is
|
|
5050
|
+
* yielded incrementally and cannot be enclosed in a single callback. The span
|
|
5051
|
+
* is kept open via a deferred promise resolved by `finish()`. Returns null when
|
|
5052
|
+
* emission is disabled or dd-trace is absent.
|
|
5053
|
+
*
|
|
5054
|
+
* Note: because the span is held open across `yield` boundaries (outside the
|
|
5055
|
+
* SDK's AsyncLocalStorage scope), manual spans are emitted flat rather than
|
|
5056
|
+
* nested. Annotations are applied to the explicit span reference.
|
|
5057
|
+
*/
|
|
5058
|
+
function openLlmObsSpan(options) {
|
|
5059
|
+
const sdk = activeSdk();
|
|
5060
|
+
if (!sdk) {
|
|
5061
|
+
return null;
|
|
5062
|
+
}
|
|
5063
|
+
try {
|
|
5064
|
+
let capturedSpan;
|
|
5065
|
+
let release;
|
|
5066
|
+
const held = new Promise((resolve) => {
|
|
5067
|
+
release = resolve;
|
|
5068
|
+
});
|
|
5069
|
+
// trace() runs the callback synchronously, capturing the span; the returned
|
|
5070
|
+
// pending promise keeps the span open until finish() resolves it.
|
|
5071
|
+
void sdk.trace(options, (span) => {
|
|
5072
|
+
capturedSpan = span;
|
|
5073
|
+
return held;
|
|
5074
|
+
});
|
|
5075
|
+
let finished = false;
|
|
5076
|
+
return {
|
|
5077
|
+
annotate: (annotation) => {
|
|
5078
|
+
try {
|
|
5079
|
+
sdk.annotate(capturedSpan, annotation);
|
|
5080
|
+
}
|
|
5081
|
+
catch (error) {
|
|
5082
|
+
getLogger$6().warn("[llmobs] annotate failed");
|
|
5083
|
+
getLogger$6().var({ error });
|
|
5084
|
+
}
|
|
5085
|
+
},
|
|
5086
|
+
finish: () => {
|
|
5087
|
+
if (finished) {
|
|
5088
|
+
return;
|
|
5089
|
+
}
|
|
5090
|
+
finished = true;
|
|
5091
|
+
release?.();
|
|
5092
|
+
},
|
|
5093
|
+
};
|
|
5094
|
+
}
|
|
5095
|
+
catch (error) {
|
|
5096
|
+
getLogger$6().warn("[llmobs] span emission failed");
|
|
5097
|
+
getLogger$6().var({ error });
|
|
5098
|
+
return null;
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
//
|
|
5102
|
+
//
|
|
5103
|
+
// Mapping Helpers
|
|
5104
|
+
//
|
|
5105
|
+
/**
|
|
5106
|
+
* Sum an `LlmUsage` array into LLM Observability metric keys. Returns undefined
|
|
5107
|
+
* when there is no usage data so callers can omit the `metrics` field.
|
|
5108
|
+
*/
|
|
5109
|
+
function usageToLlmObsMetrics(usage) {
|
|
5110
|
+
if (!usage || usage.length === 0) {
|
|
5111
|
+
return undefined;
|
|
5112
|
+
}
|
|
5113
|
+
const metrics = usage.reduce((sum, item) => ({
|
|
5114
|
+
input_tokens: sum.input_tokens + (item.input ?? 0),
|
|
5115
|
+
output_tokens: sum.output_tokens + (item.output ?? 0),
|
|
5116
|
+
total_tokens: sum.total_tokens + (item.total ?? 0),
|
|
5117
|
+
}), { input_tokens: 0, output_tokens: 0, total_tokens: 0 });
|
|
5118
|
+
return metrics;
|
|
5119
|
+
}
|
|
5120
|
+
|
|
4840
5121
|
//
|
|
4841
5122
|
//
|
|
4842
5123
|
// Main
|
|
@@ -5708,30 +5989,46 @@ class OperateLoop {
|
|
|
5708
5989
|
// Initialize state
|
|
5709
5990
|
const state = await this.initializeState(input, options);
|
|
5710
5991
|
const context = this.createContext(options);
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
//
|
|
5714
|
-
|
|
5715
|
-
state.
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5992
|
+
const modelName = options.model ?? this.adapter.defaultModel;
|
|
5993
|
+
// Enclosing LLM Observability span (no-op when DD_LLMOBS_ENABLED is unset).
|
|
5994
|
+
// Child llm/tool spans nest under it via the SDK's active-span context.
|
|
5995
|
+
return withLlmObsSpan({
|
|
5996
|
+
kind: state.toolkit ? "agent" : "llm",
|
|
5997
|
+
modelName,
|
|
5998
|
+
modelProvider: this.adapter.name,
|
|
5999
|
+
name: "jaypie.llm.operate",
|
|
6000
|
+
}, async () => {
|
|
6001
|
+
// Build initial request
|
|
6002
|
+
let request = this.buildInitialRequest(state, options);
|
|
6003
|
+
// Multi-turn loop
|
|
6004
|
+
while (state.currentTurn < state.maxTurns) {
|
|
6005
|
+
state.currentTurn++;
|
|
6006
|
+
// Execute one turn with retry logic
|
|
6007
|
+
const shouldContinue = await this.executeOneTurn(request, state, context, options);
|
|
6008
|
+
if (!shouldContinue) {
|
|
6009
|
+
break;
|
|
6010
|
+
}
|
|
6011
|
+
// Rebuild request with updated history for next turn
|
|
6012
|
+
request = {
|
|
6013
|
+
format: state.formattedFormat,
|
|
6014
|
+
instructions: options.instructions,
|
|
6015
|
+
messages: state.currentInput,
|
|
6016
|
+
model: modelName,
|
|
6017
|
+
providerOptions: options.providerOptions,
|
|
6018
|
+
system: options.system,
|
|
6019
|
+
temperature: options.temperature,
|
|
6020
|
+
tools: state.formattedTools,
|
|
6021
|
+
user: options.user,
|
|
6022
|
+
};
|
|
5720
6023
|
}
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
temperature: options.temperature,
|
|
5730
|
-
tools: state.formattedTools,
|
|
5731
|
-
user: options.user,
|
|
5732
|
-
};
|
|
5733
|
-
}
|
|
5734
|
-
return state.responseBuilder.build();
|
|
6024
|
+
const response = state.responseBuilder.build();
|
|
6025
|
+
annotateLlmObs({
|
|
6026
|
+
inputData: input,
|
|
6027
|
+
metrics: usageToLlmObsMetrics(response.usage),
|
|
6028
|
+
outputData: response.content,
|
|
6029
|
+
});
|
|
6030
|
+
return response;
|
|
6031
|
+
});
|
|
5735
6032
|
}
|
|
5736
6033
|
//
|
|
5737
6034
|
// Private Methods
|
|
@@ -5833,20 +6130,34 @@ class OperateLoop {
|
|
|
5833
6130
|
options,
|
|
5834
6131
|
providerRequest,
|
|
5835
6132
|
});
|
|
5836
|
-
// Execute with retry
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
6133
|
+
// Execute with retry inside a child llm span (no-op when llmobs disabled).
|
|
6134
|
+
// RetryExecutor handles error hooks and throws appropriate errors.
|
|
6135
|
+
const { parsed, response } = await withLlmObsSpan({
|
|
6136
|
+
kind: "llm",
|
|
6137
|
+
modelName: options.model ?? this.adapter.defaultModel,
|
|
6138
|
+
modelProvider: this.adapter.name,
|
|
6139
|
+
name: "jaypie.llm.model",
|
|
6140
|
+
}, async () => {
|
|
6141
|
+
const response = await retryExecutor.execute((signal) => this.adapter.executeRequest(this.client, providerRequest, signal), {
|
|
6142
|
+
context: {
|
|
6143
|
+
input: state.currentInput,
|
|
6144
|
+
options,
|
|
6145
|
+
providerRequest,
|
|
6146
|
+
},
|
|
6147
|
+
hooks: context.hooks,
|
|
6148
|
+
});
|
|
6149
|
+
// Log what was returned from the model
|
|
6150
|
+
log.trace("[operate] Model response received");
|
|
6151
|
+
log.var({ "operate.response": response });
|
|
6152
|
+
// Parse response
|
|
6153
|
+
const parsed = this.adapter.parseResponse(response, options);
|
|
6154
|
+
annotateLlmObs({
|
|
6155
|
+
inputData: state.currentInput,
|
|
6156
|
+
metrics: usageToLlmObsMetrics(parsed.usage ? [parsed.usage] : undefined),
|
|
6157
|
+
outputData: parsed.content ?? "",
|
|
6158
|
+
});
|
|
6159
|
+
return { parsed, response };
|
|
5844
6160
|
});
|
|
5845
|
-
// Log what was returned from the model
|
|
5846
|
-
log.trace("[operate] Model response received");
|
|
5847
|
-
log.var({ "operate.response": response });
|
|
5848
|
-
// Parse response
|
|
5849
|
-
const parsed = this.adapter.parseResponse(response, options);
|
|
5850
6161
|
// Track usage
|
|
5851
6162
|
if (parsed.usage) {
|
|
5852
6163
|
state.responseBuilder.addUsage(parsed.usage);
|
|
@@ -5867,7 +6178,7 @@ class OperateLoop {
|
|
|
5867
6178
|
if (this.adapter.hasStructuredOutput(response)) {
|
|
5868
6179
|
const structuredOutput = this.adapter.extractStructuredOutput(response);
|
|
5869
6180
|
if (structuredOutput) {
|
|
5870
|
-
state.responseBuilder.setContent(structuredOutput);
|
|
6181
|
+
state.responseBuilder.setContent(this.applyFormatArrayDefaults(structuredOutput, options));
|
|
5871
6182
|
state.responseBuilder.complete();
|
|
5872
6183
|
return false; // Stop loop
|
|
5873
6184
|
}
|
|
@@ -5891,11 +6202,19 @@ class OperateLoop {
|
|
|
5891
6202
|
args: toolCall.arguments,
|
|
5892
6203
|
toolName: toolCall.name,
|
|
5893
6204
|
});
|
|
5894
|
-
// Call the tool
|
|
6205
|
+
// Call the tool inside a child tool span (no-op when disabled)
|
|
5895
6206
|
log.trace(`[operate] Calling tool - ${toolCall.name}`);
|
|
5896
|
-
const result = await
|
|
5897
|
-
|
|
5898
|
-
|
|
6207
|
+
const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
|
|
6208
|
+
const result = await state.toolkit.call({
|
|
6209
|
+
arguments: toolCall.arguments,
|
|
6210
|
+
name: toolCall.name,
|
|
6211
|
+
});
|
|
6212
|
+
annotateLlmObs({
|
|
6213
|
+
inputData: toolCall.arguments,
|
|
6214
|
+
metadata: { tool: toolCall.name },
|
|
6215
|
+
outputData: result,
|
|
6216
|
+
});
|
|
6217
|
+
return result;
|
|
5899
6218
|
});
|
|
5900
6219
|
// Execute afterEachTool hook
|
|
5901
6220
|
await this.hookRunnerInstance.runAfterTool(context.hooks, {
|
|
@@ -5984,7 +6303,7 @@ class OperateLoop {
|
|
|
5984
6303
|
}
|
|
5985
6304
|
}
|
|
5986
6305
|
// No tool calls or no toolkit - we're done
|
|
5987
|
-
state.responseBuilder.setContent(parsed.content);
|
|
6306
|
+
state.responseBuilder.setContent(this.applyFormatArrayDefaults(parsed.content, options));
|
|
5988
6307
|
state.responseBuilder.complete();
|
|
5989
6308
|
// Add final history items
|
|
5990
6309
|
const historyItems = this.adapter.responseToHistoryItems(parsed.raw);
|
|
@@ -5993,6 +6312,20 @@ class OperateLoop {
|
|
|
5993
6312
|
}
|
|
5994
6313
|
return false; // Stop loop
|
|
5995
6314
|
}
|
|
6315
|
+
/**
|
|
6316
|
+
* Backfill declared array fields when a `format` is supplied. A declared
|
|
6317
|
+
* `format` is a schema contract: an empty array field should surface as `[]`
|
|
6318
|
+
* rather than be dropped by a provider/model that omits empty lists.
|
|
6319
|
+
*/
|
|
6320
|
+
applyFormatArrayDefaults(content, options) {
|
|
6321
|
+
if (!options.format) {
|
|
6322
|
+
return content;
|
|
6323
|
+
}
|
|
6324
|
+
if (typeof content !== "object" || content === null) {
|
|
6325
|
+
return content;
|
|
6326
|
+
}
|
|
6327
|
+
return fillFormatArrays({ content, format: options.format });
|
|
6328
|
+
}
|
|
5996
6329
|
/**
|
|
5997
6330
|
* Sync the current input state from the updated provider request.
|
|
5998
6331
|
* This is necessary because appendToolResult modifies the provider-specific request,
|
|
@@ -6145,7 +6478,7 @@ class StreamLoop {
|
|
|
6145
6478
|
}
|
|
6146
6479
|
// If we have tool calls, process them
|
|
6147
6480
|
if (toolCalls && toolCalls.length > 0 && state.toolkit) {
|
|
6148
|
-
yield* this.processToolCalls(toolCalls, state, context
|
|
6481
|
+
yield* this.processToolCalls(toolCalls, state, context);
|
|
6149
6482
|
// Check if we've reached max turns
|
|
6150
6483
|
if (state.currentTurn >= state.maxTurns) {
|
|
6151
6484
|
const error = new errors.TooManyRequestsError();
|
|
@@ -6252,6 +6585,16 @@ class StreamLoop {
|
|
|
6252
6585
|
options,
|
|
6253
6586
|
providerRequest,
|
|
6254
6587
|
});
|
|
6588
|
+
// Open a manual llm span held open across the streamed turn. Flat (not
|
|
6589
|
+
// nested) because it spans yield boundaries; no-op when llmobs disabled.
|
|
6590
|
+
const llmSpan = openLlmObsSpan({
|
|
6591
|
+
kind: "llm",
|
|
6592
|
+
modelName: options.model ?? this.adapter.defaultModel,
|
|
6593
|
+
modelProvider: this.adapter.name,
|
|
6594
|
+
name: "jaypie.llm.model",
|
|
6595
|
+
});
|
|
6596
|
+
const inputSnapshot = [...state.currentInput];
|
|
6597
|
+
let streamedText = "";
|
|
6255
6598
|
// Collect tool calls from the stream
|
|
6256
6599
|
const collectedToolCalls = [];
|
|
6257
6600
|
// Retry loop for connection-level failures
|
|
@@ -6271,6 +6614,7 @@ class StreamLoop {
|
|
|
6271
6614
|
// Pass through text chunks
|
|
6272
6615
|
if (chunk.type === exports.LlmStreamChunkType.Text) {
|
|
6273
6616
|
chunksYielded = true;
|
|
6617
|
+
streamedText += chunk.content ?? "";
|
|
6274
6618
|
yield chunk;
|
|
6275
6619
|
}
|
|
6276
6620
|
// Collect tool calls
|
|
@@ -6317,6 +6661,12 @@ class StreamLoop {
|
|
|
6317
6661
|
title: "Stream Error",
|
|
6318
6662
|
},
|
|
6319
6663
|
};
|
|
6664
|
+
llmSpan?.annotate({
|
|
6665
|
+
inputData: inputSnapshot,
|
|
6666
|
+
metrics: usageToLlmObsMetrics(state.usageItems),
|
|
6667
|
+
outputData: streamedText,
|
|
6668
|
+
});
|
|
6669
|
+
llmSpan?.finish();
|
|
6320
6670
|
return { shouldContinue: false };
|
|
6321
6671
|
}
|
|
6322
6672
|
// Check if we've exhausted retries or error is not retryable
|
|
@@ -6325,6 +6675,7 @@ class StreamLoop {
|
|
|
6325
6675
|
log.error(`Stream request failed after ${this.retryPolicy.maxRetries} retries`);
|
|
6326
6676
|
log.var({ error });
|
|
6327
6677
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6678
|
+
llmSpan?.finish();
|
|
6328
6679
|
throw new errors.BadGatewayError(errorMessage);
|
|
6329
6680
|
}
|
|
6330
6681
|
const delay = this.retryPolicy.getDelayForAttempt(attempt);
|
|
@@ -6338,6 +6689,13 @@ class StreamLoop {
|
|
|
6338
6689
|
finally {
|
|
6339
6690
|
guard.remove();
|
|
6340
6691
|
}
|
|
6692
|
+
// Annotate and finish the streamed llm span (no-op when disabled)
|
|
6693
|
+
llmSpan?.annotate({
|
|
6694
|
+
inputData: inputSnapshot,
|
|
6695
|
+
metrics: usageToLlmObsMetrics(state.usageItems),
|
|
6696
|
+
outputData: streamedText,
|
|
6697
|
+
});
|
|
6698
|
+
llmSpan?.finish();
|
|
6341
6699
|
// Execute afterEachModelResponse hook
|
|
6342
6700
|
await this.hookRunnerInstance.runAfterModelResponse(context.hooks, {
|
|
6343
6701
|
content: "",
|
|
@@ -6372,7 +6730,7 @@ class StreamLoop {
|
|
|
6372
6730
|
}
|
|
6373
6731
|
return { shouldContinue: false };
|
|
6374
6732
|
}
|
|
6375
|
-
async *processToolCalls(toolCalls, state, context
|
|
6733
|
+
async *processToolCalls(toolCalls, state, context) {
|
|
6376
6734
|
const log = getLogger$6();
|
|
6377
6735
|
for (const toolCall of toolCalls) {
|
|
6378
6736
|
try {
|
|
@@ -6381,11 +6739,19 @@ class StreamLoop {
|
|
|
6381
6739
|
args: toolCall.arguments,
|
|
6382
6740
|
toolName: toolCall.name,
|
|
6383
6741
|
});
|
|
6384
|
-
// Call the tool
|
|
6742
|
+
// Call the tool inside a child tool span (no-op when disabled)
|
|
6385
6743
|
log.trace(`[stream] Calling tool - ${toolCall.name}`);
|
|
6386
|
-
const result = await
|
|
6387
|
-
|
|
6388
|
-
|
|
6744
|
+
const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
|
|
6745
|
+
const result = await state.toolkit.call({
|
|
6746
|
+
arguments: toolCall.arguments,
|
|
6747
|
+
name: toolCall.name,
|
|
6748
|
+
});
|
|
6749
|
+
annotateLlmObs({
|
|
6750
|
+
inputData: toolCall.arguments,
|
|
6751
|
+
metadata: { tool: toolCall.name },
|
|
6752
|
+
outputData: result,
|
|
6753
|
+
});
|
|
6754
|
+
return result;
|
|
6389
6755
|
});
|
|
6390
6756
|
// Execute afterEachTool hook
|
|
6391
6757
|
await this.hookRunnerInstance.runAfterTool(context.hooks, {
|