@librechat/agents 3.1.95 → 3.1.97
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/graphs/Graph.cjs +54 -21
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +120 -9
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +30 -226
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +465 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -0
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/run.cjs +142 -69
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +29 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +20 -8
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +10 -6
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +56 -23
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +118 -9
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +28 -224
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +457 -0
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -0
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/run.mjs +144 -71
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +29 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +20 -8
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +10 -6
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +5 -1
- package/dist/types/instrumentation.d.ts +5 -1
- package/dist/types/langfuse.d.ts +6 -28
- package/dist/types/langfuseToolOutputTracing.d.ts +20 -0
- package/dist/types/run.d.ts +5 -1
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +4 -1
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +2 -0
- package/dist/types/types/graph.d.ts +30 -0
- package/dist/types/types/run.d.ts +6 -0
- package/dist/types/types/tools.d.ts +7 -0
- package/package.json +2 -1
- package/src/graphs/Graph.ts +90 -34
- package/src/instrumentation.ts +172 -11
- package/src/langfuse.ts +59 -324
- package/src/langfuseToolOutputTracing.ts +683 -0
- package/src/run.ts +190 -87
- package/src/specs/langfuse-callbacks.test.ts +178 -1
- package/src/specs/langfuse-config.test.ts +112 -76
- package/src/specs/langfuse-instrumentation.test.ts +283 -0
- package/src/specs/langfuse-metadata.test.ts +54 -1
- package/src/specs/langfuse-tool-output-tracing.test.ts +588 -0
- package/src/tools/BashProgrammaticToolCalling.ts +39 -5
- package/src/tools/ToolNode.ts +28 -7
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +54 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +72 -4
- package/src/tools/__tests__/SubagentExecutor.test.ts +32 -0
- package/src/tools/__tests__/ToolNode.langfuse.test.ts +41 -0
- package/src/tools/subagent/SubagentExecutor.ts +11 -6
- package/src/types/graph.ts +32 -0
- package/src/types/run.ts +6 -0
- package/src/types/tools.ts +7 -0
package/dist/cjs/run.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
require('./instrumentation.cjs');
|
|
3
|
+
var instrumentation = require('./instrumentation.cjs');
|
|
4
4
|
var prompts = require('@langchain/core/prompts');
|
|
5
5
|
var runnables = require('@langchain/core/runnables');
|
|
6
6
|
var openai = require('@langchain/openai');
|
|
@@ -19,6 +19,7 @@ require('./hooks/createWorkspacePolicyHook.cjs');
|
|
|
19
19
|
var llm = require('./utils/llm.cjs');
|
|
20
20
|
var callbacks = require('./utils/callbacks.cjs');
|
|
21
21
|
var langfuse = require('./langfuse.cjs');
|
|
22
|
+
var langfuseToolOutputTracing = require('./langfuseToolOutputTracing.cjs');
|
|
22
23
|
|
|
23
24
|
// src/run.ts
|
|
24
25
|
const defaultOmitOptions = new Set([
|
|
@@ -73,6 +74,7 @@ class Run {
|
|
|
73
74
|
handlerRegistry;
|
|
74
75
|
hookRegistry;
|
|
75
76
|
humanInTheLoop;
|
|
77
|
+
langfuse;
|
|
76
78
|
toolOutputReferences;
|
|
77
79
|
eagerEventToolExecution;
|
|
78
80
|
toolExecution;
|
|
@@ -112,6 +114,7 @@ class Run {
|
|
|
112
114
|
this.handlerRegistry = handlerRegistry;
|
|
113
115
|
this.hookRegistry = config.hooks;
|
|
114
116
|
this.humanInTheLoop = config.humanInTheLoop;
|
|
117
|
+
this.langfuse = config.langfuse;
|
|
115
118
|
this.toolOutputReferences = config.toolOutputReferences;
|
|
116
119
|
this.eagerEventToolExecution = config.eagerEventToolExecution;
|
|
117
120
|
this.toolExecution = config.toolExecution;
|
|
@@ -170,6 +173,7 @@ class Run {
|
|
|
170
173
|
signal,
|
|
171
174
|
runId: this.id,
|
|
172
175
|
agents: [agentConfig],
|
|
176
|
+
langfuse: this.langfuse,
|
|
173
177
|
tokenCounter: this.tokenCounter,
|
|
174
178
|
indexTokenCountMap: this.indexTokenCountMap,
|
|
175
179
|
calibrationRatio: this.calibrationRatio,
|
|
@@ -190,6 +194,7 @@ class Run {
|
|
|
190
194
|
runId: this.id,
|
|
191
195
|
agents,
|
|
192
196
|
edges,
|
|
197
|
+
langfuse: this.langfuse,
|
|
193
198
|
tokenCounter: this.tokenCounter,
|
|
194
199
|
indexTokenCountMap: this.indexTokenCountMap,
|
|
195
200
|
calibrationRatio: this.calibrationRatio,
|
|
@@ -423,6 +428,77 @@ class Run {
|
|
|
423
428
|
}
|
|
424
429
|
};
|
|
425
430
|
}
|
|
431
|
+
shouldClearHookSession(streamThrew) {
|
|
432
|
+
return (this._interrupt == null || this._haltedReason != null || streamThrew);
|
|
433
|
+
}
|
|
434
|
+
isAwaitingResume(streamThrew) {
|
|
435
|
+
return (this._interrupt != null && this._haltedReason == null && !streamThrew);
|
|
436
|
+
}
|
|
437
|
+
getStreamLangfuseConfig(graph) {
|
|
438
|
+
const primaryContext = graph.agentContexts.get(graph.defaultAgentId);
|
|
439
|
+
if (primaryContext != null) {
|
|
440
|
+
return langfuseToolOutputTracing.resolveLangfuseConfig(this.langfuse, primaryContext.langfuse);
|
|
441
|
+
}
|
|
442
|
+
for (const context of graph.agentContexts.values()) {
|
|
443
|
+
const langfuse = langfuseToolOutputTracing.resolveLangfuseConfig(this.langfuse, context.langfuse);
|
|
444
|
+
if (langfuse != null) {
|
|
445
|
+
return langfuse;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return this.langfuse;
|
|
449
|
+
}
|
|
450
|
+
getStreamToolOutputTracingLangfuseConfig(graph) {
|
|
451
|
+
const toolOutputTracingConfigs = Array.from(graph.agentContexts.values())
|
|
452
|
+
.map((context) => {
|
|
453
|
+
return langfuseToolOutputTracing.resolveLangfuseConfig(this.langfuse, context.langfuse)
|
|
454
|
+
?.toolOutputTracing;
|
|
455
|
+
})
|
|
456
|
+
.filter((config) => {
|
|
457
|
+
return config != null;
|
|
458
|
+
});
|
|
459
|
+
if (toolOutputTracingConfigs.length === 0) {
|
|
460
|
+
return this.langfuse?.toolOutputTracing != null
|
|
461
|
+
? { toolOutputTracing: this.langfuse.toolOutputTracing }
|
|
462
|
+
: undefined;
|
|
463
|
+
}
|
|
464
|
+
if (toolOutputTracingConfigs.length === 1) {
|
|
465
|
+
return { toolOutputTracing: toolOutputTracingConfigs[0] };
|
|
466
|
+
}
|
|
467
|
+
let enabled;
|
|
468
|
+
let redactionText;
|
|
469
|
+
let redactedToolNameMatchMode;
|
|
470
|
+
const redactedToolNames = new Set();
|
|
471
|
+
for (const config of toolOutputTracingConfigs) {
|
|
472
|
+
if (config.enabled === false) {
|
|
473
|
+
enabled = false;
|
|
474
|
+
}
|
|
475
|
+
else if (enabled !== false && config.enabled != null) {
|
|
476
|
+
enabled = config.enabled;
|
|
477
|
+
}
|
|
478
|
+
redactionText ??= config.redactionText;
|
|
479
|
+
if (config.redactedToolNameMatchMode === 'partial') {
|
|
480
|
+
redactedToolNameMatchMode = 'partial';
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
redactedToolNameMatchMode ??= config.redactedToolNameMatchMode;
|
|
484
|
+
}
|
|
485
|
+
for (const toolName of config.redactedToolNames ?? []) {
|
|
486
|
+
redactedToolNames.add(toolName);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
toolOutputTracing: {
|
|
491
|
+
...(enabled != null ? { enabled } : {}),
|
|
492
|
+
...(redactedToolNames.size > 0
|
|
493
|
+
? { redactedToolNames: Array.from(redactedToolNames) }
|
|
494
|
+
: {}),
|
|
495
|
+
...(redactedToolNameMatchMode != null
|
|
496
|
+
? { redactedToolNameMatchMode }
|
|
497
|
+
: {}),
|
|
498
|
+
...(redactionText != null ? { redactionText } : {}),
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
426
502
|
async processStream(inputs, callerConfig, streamOptions) {
|
|
427
503
|
if (this.graphRunnable == null) {
|
|
428
504
|
throw new Error('Run not initialized. Make sure to use Run.create() to instantiate the Run.');
|
|
@@ -430,6 +506,8 @@ class Run {
|
|
|
430
506
|
if (!this.Graph) {
|
|
431
507
|
throw new Error('Graph not initialized. Make sure to use Run.create() to instantiate the Run.');
|
|
432
508
|
}
|
|
509
|
+
const graphRunnable = this.graphRunnable;
|
|
510
|
+
const graph = this.Graph;
|
|
433
511
|
/**
|
|
434
512
|
* `Command` inputs (currently only `Command({ resume })`) are
|
|
435
513
|
* resume-mode invocations: LangGraph rebuilds graph state from the
|
|
@@ -456,7 +534,7 @@ class Run {
|
|
|
456
534
|
* boundary.
|
|
457
535
|
*/
|
|
458
536
|
if (!isResume) {
|
|
459
|
-
|
|
537
|
+
graph.resetValues(streamOptions?.keepContent);
|
|
460
538
|
}
|
|
461
539
|
this._interrupt = undefined;
|
|
462
540
|
this._haltedReason = undefined;
|
|
@@ -471,28 +549,31 @@ class Run {
|
|
|
471
549
|
});
|
|
472
550
|
customHandler.awaitHandlers = true;
|
|
473
551
|
config.callbacks = callbacks.appendCallbacks(config.callbacks, streamCallbacks ? [streamCallbacks, customHandler] : [customHandler]);
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
552
|
+
const primaryContext = graph.agentContexts.get(graph.defaultAgentId);
|
|
553
|
+
const userId = typeof config.configurable?.user_id === 'string'
|
|
554
|
+
? config.configurable.user_id
|
|
555
|
+
: undefined;
|
|
556
|
+
const sessionId = typeof config.configurable?.thread_id === 'string'
|
|
557
|
+
? config.configurable.thread_id
|
|
558
|
+
: undefined;
|
|
559
|
+
const traceMetadata = langfuse.createLangfuseTraceMetadata({
|
|
560
|
+
messageId: this.id,
|
|
561
|
+
parentMessageId: config.configurable?.requestBody?.parentMessageId,
|
|
562
|
+
agentId: graph.defaultAgentId,
|
|
563
|
+
agentName: primaryContext?.name,
|
|
564
|
+
});
|
|
565
|
+
const streamLangfuseConfig = this.getStreamLangfuseConfig(graph);
|
|
566
|
+
instrumentation.initializeLangfuseTracing(streamLangfuseConfig);
|
|
567
|
+
const langfuseHandler = langfuse.createLangfuseHandler({
|
|
568
|
+
langfuse: streamLangfuseConfig,
|
|
569
|
+
userId,
|
|
570
|
+
sessionId,
|
|
571
|
+
traceMetadata,
|
|
572
|
+
tags: ['librechat', 'agent'],
|
|
573
|
+
});
|
|
574
|
+
if (langfuseHandler != null) {
|
|
494
575
|
config.runName = config.runName ?? langfuse.getLangfuseTraceName(traceMetadata);
|
|
495
|
-
config.callbacks = callbacks.appendCallbacks(config.callbacks, [
|
|
576
|
+
config.callbacks = callbacks.appendCallbacks(config.callbacks, [langfuseHandler]);
|
|
496
577
|
}
|
|
497
578
|
if (!this.id) {
|
|
498
579
|
throw new Error('Run ID not provided');
|
|
@@ -508,24 +589,6 @@ class Run {
|
|
|
508
589
|
return undefined;
|
|
509
590
|
}
|
|
510
591
|
}
|
|
511
|
-
/**
|
|
512
|
-
* `streamEvents` accepts both state inputs and `Command` (resume) at
|
|
513
|
-
* runtime, but our `CompiledStateWorkflow` type narrows the first
|
|
514
|
-
* arg to `BaseGraphState`. Cast on the call so the resume path
|
|
515
|
-
* type-checks without widening the wrapper for every caller.
|
|
516
|
-
*/
|
|
517
|
-
const stream = this.graphRunnable.streamEvents(inputs, config, {
|
|
518
|
-
raiseError: true,
|
|
519
|
-
/**
|
|
520
|
-
* Prevent EventStreamCallbackHandler from processing custom events.
|
|
521
|
-
* Custom events are already handled via our createCustomEventCallback()
|
|
522
|
-
* which routes them through the handlerRegistry.
|
|
523
|
-
* Without this flag, EventStreamCallbackHandler throws errors when
|
|
524
|
-
* custom events are dispatched for run IDs not in its internal map
|
|
525
|
-
* (due to timing issues in parallel execution or after run cleanup).
|
|
526
|
-
*/
|
|
527
|
-
ignoreCustomEvent: true,
|
|
528
|
-
});
|
|
529
592
|
/**
|
|
530
593
|
* Tracks whether the stream loop threw. Used by the `finally`
|
|
531
594
|
* block to decide whether to honor the interrupt-preservation
|
|
@@ -536,7 +599,25 @@ class Run {
|
|
|
536
599
|
* preserving session hooks would leak them into the next run.
|
|
537
600
|
*/
|
|
538
601
|
let streamThrew = false;
|
|
539
|
-
|
|
602
|
+
const consumeStream = async () => {
|
|
603
|
+
/**
|
|
604
|
+
* `streamEvents` accepts both state inputs and `Command` (resume) at
|
|
605
|
+
* runtime, but our `CompiledStateWorkflow` type narrows the first
|
|
606
|
+
* arg to `BaseGraphState`. Cast on the call so the resume path
|
|
607
|
+
* type-checks without widening the wrapper for every caller.
|
|
608
|
+
*/
|
|
609
|
+
const stream = graphRunnable.streamEvents(inputs, config, {
|
|
610
|
+
raiseError: true,
|
|
611
|
+
/**
|
|
612
|
+
* Prevent EventStreamCallbackHandler from processing custom events.
|
|
613
|
+
* Custom events are already handled via our createCustomEventCallback()
|
|
614
|
+
* which routes them through the handlerRegistry.
|
|
615
|
+
* Without this flag, EventStreamCallbackHandler throws errors when
|
|
616
|
+
* custom events are dispatched for run IDs not in its internal map
|
|
617
|
+
* (due to timing issues in parallel execution or after run cleanup).
|
|
618
|
+
*/
|
|
619
|
+
ignoreCustomEvent: true,
|
|
620
|
+
});
|
|
540
621
|
for await (const event of stream) {
|
|
541
622
|
const { data, metadata, ...info } = event;
|
|
542
623
|
const eventName = info.event;
|
|
@@ -621,8 +702,8 @@ class Run {
|
|
|
621
702
|
hook_event_name: 'Stop',
|
|
622
703
|
runId: this.id,
|
|
623
704
|
threadId,
|
|
624
|
-
agentId:
|
|
625
|
-
messages:
|
|
705
|
+
agentId: graph.defaultAgentId,
|
|
706
|
+
messages: graph.getRunMessages() ?? stateInputs?.messages ?? [],
|
|
626
707
|
stopHookActive: false, // will be true when stop is triggered by a hook (Phase 2)
|
|
627
708
|
},
|
|
628
709
|
sessionId: this.id,
|
|
@@ -630,6 +711,9 @@ class Run {
|
|
|
630
711
|
/* Stop hook errors must not masquerade as stream failures */
|
|
631
712
|
});
|
|
632
713
|
}
|
|
714
|
+
};
|
|
715
|
+
try {
|
|
716
|
+
await langfuseToolOutputTracing.withLangfuseToolOutputTracingConfig(streamLangfuseConfig, consumeStream, this.getStreamToolOutputTracingLangfuseConfig(graph));
|
|
633
717
|
}
|
|
634
718
|
catch (err) {
|
|
635
719
|
streamThrew = true;
|
|
@@ -667,9 +751,7 @@ class Run {
|
|
|
667
751
|
* expected, sessions must drop). Every state where no resume
|
|
668
752
|
* is expected clears.
|
|
669
753
|
*/
|
|
670
|
-
if (this.
|
|
671
|
-
this._haltedReason != null ||
|
|
672
|
-
streamThrew) {
|
|
754
|
+
if (this.shouldClearHookSession(streamThrew)) {
|
|
673
755
|
this.hookRegistry?.clearSession(this.id);
|
|
674
756
|
}
|
|
675
757
|
/**
|
|
@@ -681,6 +763,7 @@ class Run {
|
|
|
681
763
|
* unaffected — their entries live under their own session ids.
|
|
682
764
|
*/
|
|
683
765
|
this.hookRegistry?.clearHaltSignal(this.id);
|
|
766
|
+
await langfuse.disposeLangfuseHandler(langfuseHandler);
|
|
684
767
|
/**
|
|
685
768
|
* Break the reference chain that keeps heavy data alive via
|
|
686
769
|
* LangGraph's internal `__pregel_scratchpad.currentTaskInput` →
|
|
@@ -728,7 +811,7 @@ class Run {
|
|
|
728
811
|
* Run from scratch) is a separate concern; see
|
|
729
812
|
* `HumanInTheLoopConfig` JSDoc.
|
|
730
813
|
*/
|
|
731
|
-
const awaitingResume = this.
|
|
814
|
+
const awaitingResume = this.isAwaitingResume(streamThrew);
|
|
732
815
|
if (!this.skipCleanup && !awaitingResume) {
|
|
733
816
|
this.Graph.clearHeavyState();
|
|
734
817
|
}
|
|
@@ -907,25 +990,15 @@ class Run {
|
|
|
907
990
|
const sessionId = typeof chainOptions.configurable?.thread_id === 'string'
|
|
908
991
|
? chainOptions.configurable.thread_id
|
|
909
992
|
: undefined;
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
else if (langfuse.hasLangfuseEnvConfig() && !hasExplicitLangfuse) {
|
|
922
|
-
titleLangfuseHandler = langfuse.createLegacyLangfuseHandler({
|
|
923
|
-
userId,
|
|
924
|
-
sessionId,
|
|
925
|
-
traceMetadata,
|
|
926
|
-
tags: ['librechat', 'title'],
|
|
927
|
-
});
|
|
928
|
-
}
|
|
993
|
+
const titleLangfuseConfig = langfuseToolOutputTracing.resolveLangfuseConfig(this.langfuse, titleContext?.langfuse);
|
|
994
|
+
instrumentation.initializeLangfuseTracing(titleLangfuseConfig);
|
|
995
|
+
titleLangfuseHandler = langfuse.createLangfuseHandler({
|
|
996
|
+
langfuse: titleLangfuseConfig,
|
|
997
|
+
userId,
|
|
998
|
+
sessionId,
|
|
999
|
+
traceMetadata,
|
|
1000
|
+
tags: ['librechat', 'title'],
|
|
1001
|
+
});
|
|
929
1002
|
if (titleLangfuseHandler != null) {
|
|
930
1003
|
chainOptions.callbacks = callbacks.appendCallbacks(chainOptions.callbacks, [
|
|
931
1004
|
titleLangfuseHandler,
|
|
@@ -978,7 +1051,7 @@ class Run {
|
|
|
978
1051
|
});
|
|
979
1052
|
try {
|
|
980
1053
|
try {
|
|
981
|
-
return await fullChain.invoke({ input: inputText, output: response }, invokeConfig);
|
|
1054
|
+
return await langfuseToolOutputTracing.withLangfuseToolOutputTracingConfig(this.langfuse, () => fullChain.invoke({ input: inputText, output: response }, invokeConfig), titleContext?.langfuse);
|
|
982
1055
|
}
|
|
983
1056
|
catch (_e) {
|
|
984
1057
|
// Fallback: strip callbacks to avoid EventStream tracer errors in certain environments
|
|
@@ -988,7 +1061,7 @@ class Run {
|
|
|
988
1061
|
const safeConfig = Object.assign({}, rest, {
|
|
989
1062
|
callbacks: langfuseHandler ? [langfuseHandler] : [],
|
|
990
1063
|
});
|
|
991
|
-
return await fullChain.invoke({ input: inputText, output: response }, safeConfig);
|
|
1064
|
+
return await langfuseToolOutputTracing.withLangfuseToolOutputTracingConfig(this.langfuse, () => fullChain.invoke({ input: inputText, output: response }, safeConfig), titleContext?.langfuse);
|
|
992
1065
|
}
|
|
993
1066
|
}
|
|
994
1067
|
finally {
|